From 51080bcf5ff8cd2cea6e6500b4df1b6bbca9efc8 Mon Sep 17 00:00:00 2001 From: Tom Pointon Date: Tue, 4 Oct 2022 14:59:34 +0100 Subject: [PATCH] Pyth Aptos Target Chain Contract (#291) Initial pyth aptos contract --- .gitignore | 1 + aptos/contracts/Makefile | 14 + aptos/contracts/Move.toml | 23 + .../sources/batch_price_attestation.move | 234 +++++ aptos/contracts/sources/data_source.move | 15 + aptos/contracts/sources/deserialize.move | 140 +++ aptos/contracts/sources/error.move | 101 +++ aptos/contracts/sources/event.move | 39 + .../sources/governance/contract_upgrade.move | 78 ++ .../sources/governance/governance.move | 408 +++++++++ .../sources/governance/governance_action.move | 38 + .../governance/governance_instruction.move | 86 ++ .../sources/governance/set_data_sources.move | 42 + .../set_governance_data_source.move | 34 + .../governance/set_stale_price_threshold.move | 25 + .../sources/governance/set_update_fee.move | 36 + aptos/contracts/sources/i64.move | 141 +++ aptos/contracts/sources/price.move | 46 + aptos/contracts/sources/price_feed.move | 37 + aptos/contracts/sources/price_identifier.move | 22 + aptos/contracts/sources/price_info.move | 29 + aptos/contracts/sources/price_status.move | 53 ++ aptos/contracts/sources/pyth.move | 803 ++++++++++++++++++ aptos/contracts/sources/set.move | 45 + aptos/contracts/sources/state.move | 181 ++++ aptos/start_node.sh | 2 + 26 files changed, 2673 insertions(+) create mode 100644 aptos/contracts/Makefile create mode 100644 aptos/contracts/Move.toml create mode 100644 aptos/contracts/sources/batch_price_attestation.move create mode 100644 aptos/contracts/sources/data_source.move create mode 100644 aptos/contracts/sources/deserialize.move create mode 100644 aptos/contracts/sources/error.move create mode 100644 aptos/contracts/sources/event.move create mode 100644 aptos/contracts/sources/governance/contract_upgrade.move create mode 100644 aptos/contracts/sources/governance/governance.move create mode 100644 aptos/contracts/sources/governance/governance_action.move create mode 100644 aptos/contracts/sources/governance/governance_instruction.move create mode 100644 aptos/contracts/sources/governance/set_data_sources.move create mode 100644 aptos/contracts/sources/governance/set_governance_data_source.move create mode 100644 aptos/contracts/sources/governance/set_stale_price_threshold.move create mode 100644 aptos/contracts/sources/governance/set_update_fee.move create mode 100644 aptos/contracts/sources/i64.move create mode 100644 aptos/contracts/sources/price.move create mode 100644 aptos/contracts/sources/price_feed.move create mode 100644 aptos/contracts/sources/price_identifier.move create mode 100644 aptos/contracts/sources/price_info.move create mode 100644 aptos/contracts/sources/price_status.move create mode 100644 aptos/contracts/sources/pyth.move create mode 100644 aptos/contracts/sources/set.move create mode 100644 aptos/contracts/sources/state.move create mode 100644 aptos/start_node.sh diff --git a/.gitignore b/.gitignore index f3d5209f..e38f4adb 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ bigtable-admin.json bigtable-writer.json .vscode .dccache +.aptos diff --git a/aptos/contracts/Makefile b/aptos/contracts/Makefile new file mode 100644 index 00000000..ab73c8c8 --- /dev/null +++ b/aptos/contracts/Makefile @@ -0,0 +1,14 @@ +.PHONY: artifacts +artifacts: build + +.PHONY: build +build: + aptos move compile --save-metadata --named-addresses wormhole=0x251011524cd0f76881f16e7c2d822f0c1c9510bfd2430ba24e1b3d52796df204,deployer=0x277fa055b6a73c42c0662d5236c65c864ccbf2d4abd21f174a30c8b786eab84b,pyth=0x277fa055b6a73c42c0662d5236c65c864ccbf2d4abd21f174a30c8b786eab84b + +.PHONY: clean +clean: + aptos move clean --assume-yes + +.PHONY: test +test: + aptos move test diff --git a/aptos/contracts/Move.toml b/aptos/contracts/Move.toml new file mode 100644 index 00000000..7c9a2d3e --- /dev/null +++ b/aptos/contracts/Move.toml @@ -0,0 +1,23 @@ +[package] +name = "Pyth" +version = "0.0.1" +upgrade_policy = "compatible" + +[dependencies] +# TODO: pin versions before mainnet release +AptosFramework = { git = "https://github.com/aptos-labs/aptos-core.git", subdir = "aptos-move/framework/aptos-framework/", rev = "main" } +MoveStdlib = { git = "https://github.com/aptos-labs/aptos-core.git", subdir = "aptos-move/framework/move-stdlib/", rev = "main" } +AptosStdlib = { git = "https://github.com/aptos-labs/aptos-core.git", subdir = "aptos-move/framework/aptos-stdlib/", rev = "main" } +AptosToken = { git = "https://github.com/aptos-labs/aptos-core.git", subdir = "aptos-move/framework/aptos-token/", rev = "main" } +Wormhole = { git = "https://github.com/wormhole-foundation/wormhole.git", subdir = "aptos/wormhole", rev = "aptos/integration" } +Deployer = { git = "https://github.com/wormhole-foundation/wormhole.git", subdir = "aptos/deployer", rev = "aptos/integration" } + +[addresses] +pyth = "_" +deployer = "_" +wormhole = "_" + +[dev-addresses] +pyth = "0xe2f37b8ac45d29d5ea23eb7d16dd3f7a7ab6426f5a998d6c23ecd3ae8d9d29eb" +deployer = "0x277fa055b6a73c42c0662d5236c65c864ccbf2d4abd21f174a30c8b786eab84b" +wormhole = "0x251011524cd0f76881f16e7c2d822f0c1c9510bfd2430ba24e1b3d52796df204" diff --git a/aptos/contracts/sources/batch_price_attestation.move b/aptos/contracts/sources/batch_price_attestation.move new file mode 100644 index 00000000..25100bef --- /dev/null +++ b/aptos/contracts/sources/batch_price_attestation.move @@ -0,0 +1,234 @@ +module pyth::batch_price_attestation { + use pyth::price_feed::{Self}; + use pyth::price; + use pyth::error; + use pyth::i64; + use pyth::price_info::{Self, PriceInfo}; + use pyth::price_identifier::{Self}; + use pyth::price_status; + use pyth::deserialize::{Self}; + use aptos_framework::account; + use aptos_framework::timestamp; + use wormhole::cursor::{Self, Cursor}; + use std::vector::{Self}; + + 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); + assert!(magic == MAGIC, 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, error::invalid_batch_attestation_header_size()); + let unknown_header_bytes = header_size - 1; + let _unknown = deserialize::deserialize_vector(cur, unknown_header_bytes); + + Header { + magic: magic, + header_size: header_size, + version_minor: version_minor, + version_major: version_major, + 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): BatchPriceAttestation { + let cur = cursor::init(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); + 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 = deserialize::deserialize_vector(&mut cur, attestation_size - parsed_bytes); + + i = i + 1; + }; + cursor::destroy_empty(cur); + + BatchPriceAttestation { + header, + attestation_count: attestation_count, + attestation_size: attestation_size, + price_infos: price_infos, + } + } + + fun deserialize_price_info(cur: &mut Cursor): 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, + timestamp::now_seconds(), + price_feed::new( + price_identifier, + current_price, + pyth::price::new(ema_price, ema_conf, expo, ema_timestamp), + ) + ) + } + + #[test] + #[expected_failure(abort_code = 65560)] + fun test_deserialize_batch_price_attestation_invalid_magic() { + // A batch price attestation with a magic number of 0x50325749 + let bytes = x"5032574900030000000102000400951436e0be37536be96f0896366089506a59763d036728332d3e3038047851aea7c6c75c89f14810ec1c54c03ab8f1864a4c4032791f05747f560faec380a695d1000000000000049a0000000000000008fffffffb00000000000005dc0000000000000003000000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000006150000000000000007215258d81468614f6b7e194c5d145609394f67b041e93e6695dcc616faadd0603b9551a68d01d954d6387aff4df1529027ffb2fee413082e509feb29cc4904fe000000000000041a0000000000000003fffffffb00000000000005cb0000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e4000000000000048600000000000000078ac9cf3ab299af710d735163726fdae0db8465280502eb9f801f74b3c1bd190333832fad6e36eb05a8972fe5f219b27b5b2bb2230a79ce79beb4c5c5e7ecc76d00000000000003f20000000000000002fffffffb00000000000005e70000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e40000000000000685000000000000000861db714e9ff987b6fedf00d01f9fea6db7c30632d6fc83b7bc9459d7192bc44a21a28b4c6619968bd8c20e95b0aaed7df2187fd310275347e0376a2cd7427db800000000000006cb0000000000000001fffffffb00000000000005e40000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000007970000000000000001"; + destroy(deserialize(bytes)); + } + + #[test(aptos_framework = @aptos_framework)] + fun test_deserialize_batch_price_attestation(aptos_framework: signer) { + + // Set the arrival time + account::create_account_for_test(@aptos_framework); + timestamp::set_time_has_started_for_testing(&aptos_framework); + let arrival_time = 1663074349; + timestamp::update_global_time_for_test(1663074349 * 1000000); + + // 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); + + assert!(&expected == &deserialized, 1); + destroy(expected); + destroy(deserialized); + } +} diff --git a/aptos/contracts/sources/data_source.move b/aptos/contracts/sources/data_source.move new file mode 100644 index 00000000..c79e548f --- /dev/null +++ b/aptos/contracts/sources/data_source.move @@ -0,0 +1,15 @@ +module pyth::data_source { + use wormhole::external_address::ExternalAddress; + + struct DataSource has copy, drop, store { + emitter_chain: u64, + emitter_address: ExternalAddress, + } + + public fun new(emitter_chain: u64, emitter_address: ExternalAddress): DataSource { + DataSource { + emitter_chain: emitter_chain, + emitter_address: emitter_address, + } + } +} diff --git a/aptos/contracts/sources/deserialize.move b/aptos/contracts/sources/deserialize.move new file mode 100644 index 00000000..c0a2f62e --- /dev/null +++ b/aptos/contracts/sources/deserialize.move @@ -0,0 +1,140 @@ +module pyth::deserialize { + use wormhole::deserialize; + use wormhole::u16; + use wormhole::u32; + use wormhole::cursor::{Self, Cursor}; + use pyth::i64::{Self, I64}; + + public fun deserialize_vector(cur: &mut Cursor, n: u64): vector { + deserialize::deserialize_vector(cur, n) + } + + public fun deserialize_u8(cur: &mut Cursor): u8 { + deserialize::deserialize_u8(cur) + } + + public fun deserialize_u16(cur: &mut Cursor): u64 { + u16::to_u64(deserialize::deserialize_u16(cur)) + } + + public fun deserialize_u32(cur: &mut Cursor): u64 { + u32::to_u64(deserialize::deserialize_u32(cur)) + } + + public fun deserialize_i32(cur: &mut Cursor): I64 { + let deserialized = deserialize_u32(cur); + + // If negative, pad the value + let negative = (deserialized >> 31) == 1; + if (negative) { + let padded = (0xFFFFFFFF << 32) + deserialized; + i64::from_u64(padded) + } else { + i64::from_u64(deserialized) + } + } + + public fun deserialize_u64(cur: &mut Cursor): u64 { + deserialize::deserialize_u64(cur) + } + + public fun deserialize_i64(cur: &mut Cursor): I64 { + i64::from_u64(deserialize_u64(cur)) + } + + #[test] + fun test_deserialize_u8() { + let input = x"48258963"; + let cursor = cursor::init(input); + + let result = deserialize_u8(&mut cursor); + assert!(result == 0x48, 1); + + let rest = cursor::rest(cursor); + assert!(rest == x"258963", 1); + } + + #[test] + fun test_deserialize_u16() { + let input = x"48258963"; + let cursor = cursor::init(input); + + let result = deserialize_u16(&mut cursor); + assert!(result == 0x4825, 1); + + let rest = cursor::rest(cursor); + assert!(rest == x"8963", 1); + } + + #[test] + fun test_deserialize_u32() { + let input = x"4825896349741695"; + let cursor = cursor::init(input); + + let result = deserialize_u32(&mut cursor); + assert!(result == 0x48258963, 1); + + let rest = cursor::rest(cursor); + assert!(rest == x"49741695", 1); + } + + #[test] + fun test_deserialize_i32_positive() { + let input = x"4825896349741695"; + let cursor = cursor::init(input); + + let result = deserialize_i32(&mut cursor); + assert!(result == i64::from_u64(0x48258963), 1); + + let rest = cursor::rest(cursor); + assert!(rest == x"49741695", 1); + } + + #[test] + fun test_deserialize_i32_negative() { + let input = x"FFFFFDC349741695"; + let cursor = cursor::init(input); + + let result = deserialize_i32(&mut cursor); + assert!(result == i64::from_u64(0xFFFFFFFFFFFFFDC3), 1); + + let rest = cursor::rest(cursor); + assert!(rest == x"49741695", 1); + } + + #[test] + fun test_deserialize_u64() { + let input = x"48258963497416957497253486"; + let cursor = cursor::init(input); + + let result = deserialize_u64(&mut cursor); + assert!(result == 0x4825896349741695, 1); + + let rest = cursor::rest(cursor); + assert!(rest == x"7497253486", 1); + } + + #[test] + fun test_deserialize_i64_positive() { + let input = x"48258963497416957497253486"; + let cursor = cursor::init(input); + + let result = deserialize_i64(&mut cursor); + assert!(result == i64::from_u64(0x4825896349741695), 1); + + let rest = cursor::rest(cursor); + assert!(rest == x"7497253486", 1); + } + + #[test] + fun test_deserialize_i64_negative() { + let input = x"FFFFFFFFFFFFFDC37497253486"; + let cursor = cursor::init(input); + + let result = deserialize_i64(&mut cursor); + assert!(result == i64::from_u64(0xFFFFFFFFFFFFFDC3), 1); + + let rest = cursor::rest(cursor); + assert!(rest == x"7497253486", 1); + } +} diff --git a/aptos/contracts/sources/error.move b/aptos/contracts/sources/error.move new file mode 100644 index 00000000..6c02f3e7 --- /dev/null +++ b/aptos/contracts/sources/error.move @@ -0,0 +1,101 @@ +/// Constructors for all expected abort codes thrown by the Pyth contract. +/// Each error is in the appropiate error category. +module pyth::error { + use std::error; + + public fun negative_value(): u64 { + error::invalid_state(1) + } + + public fun incorrect_identifier_length(): u64 { + error::invalid_argument(2) + } + + public fun invalid_data_source(): u64 { + error::invalid_argument(3) + } + + public fun stale_price_update(): u64 { + error::already_exists(4) + } + + public fun invalid_publish_times_length(): u64 { + error::invalid_argument(5) + } + + public fun insufficient_fee(): u64 { + error::invalid_argument(6) + } + + public fun no_fresh_data(): u64 { + error::already_exists(7) + } + + public fun unknown_price_feed(): u64 { + error::not_found(8) + } + + public fun unauthorized_upgrade(): u64 { + error::permission_denied(9) + } + + public fun invalid_upgrade_hash(): u64 { + error::invalid_argument(10) + } + + public fun invalid_hash_length(): u64 { + error::invalid_argument(11) + } + + public fun invalid_governance_module(): u64 { + error::invalid_argument(12) + } + + public fun invalid_governance_target_chain_id(): u64 { + error::invalid_argument(13) + } + + public fun invalid_governance_data_source(): u64 { + error::invalid_argument(14) + } + + public fun invalid_governance_sequence_number(): u64 { + error::invalid_argument(15) + } + + public fun invalid_governance_action(): u64 { + error::invalid_argument(16) + } + + public fun overflow(): u64 { + error::out_of_range(17) + } + + public fun invalid_batch_attestation_header_size(): u64 { + error::invalid_argument(18) + } + + public fun positive_value(): u64 { + error::invalid_state(19) + } + + public fun invalid_governance_magic_value(): u64 { + error::invalid_argument(20) + } + + public fun magnitude_too_large(): u64 { + error::invalid_argument(21) + } + + public fun governance_contract_upgrade_chain_id_zero(): u64 { + error::invalid_argument(22) + } + + public fun invalid_price_status(): u64 { + error::invalid_argument(23) + } + + public fun invalid_attestation_magic_value(): u64 { + error::invalid_argument(24) + } +} diff --git a/aptos/contracts/sources/event.move b/aptos/contracts/sources/event.move new file mode 100644 index 00000000..6c770b50 --- /dev/null +++ b/aptos/contracts/sources/event.move @@ -0,0 +1,39 @@ +module pyth::event { + use std::event::{Self, EventHandle}; + use pyth::price_feed::{PriceFeed}; + use std::account; + + friend pyth::pyth; + + /// Signifies that a price feed has been updated + struct PriceFeedUpdate has store, drop { + /// Value of the price feed + price_feed: PriceFeed, + /// Timestamp of the update + timestamp: u64, + } + + struct PriceFeedUpdateHandle has key, store { + event: EventHandle + } + + public(friend) fun init(pyth: &signer) { + move_to( + pyth, + PriceFeedUpdateHandle { + event: account::new_event_handle(pyth) + } + ); + } + + public(friend) fun emit_price_feed_update(price_feed: PriceFeed, timestamp: u64) acquires PriceFeedUpdateHandle { + let event_handle = borrow_global_mut(@pyth); + event::emit_event( + &mut event_handle.event, + PriceFeedUpdate { + price_feed, + timestamp, + } + ); + } +} diff --git a/aptos/contracts/sources/governance/contract_upgrade.move b/aptos/contracts/sources/governance/contract_upgrade.move new file mode 100644 index 00000000..958ce170 --- /dev/null +++ b/aptos/contracts/sources/governance/contract_upgrade.move @@ -0,0 +1,78 @@ +module pyth::contract_upgrade { + use wormhole::cursor; + use pyth::deserialize; + use pyth::contract_upgrade_hash::{Self, Hash}; + use pyth::state::{Self}; + use std::vector; + use std::aptos_hash; + use aptos_framework::code; + use pyth::error; + + friend pyth::governance; + + const HASH_LENGTH: u64 = 32; + + struct AuthorizeContractUpgrade { + hash: Hash, + } + + public(friend) fun execute(payload: vector) { + let AuthorizeContractUpgrade {hash: hash} = from_byte_vec(payload); + state::set_contract_upgrade_authorized_hash(hash) + } + + fun from_byte_vec(bytes: vector): AuthorizeContractUpgrade { + let cursor = cursor::init(bytes); + let hash = contract_upgrade_hash::from_byte_vec(deserialize::deserialize_vector(&mut cursor, HASH_LENGTH)); + cursor::destroy_empty(cursor); + + AuthorizeContractUpgrade { + hash, + } + } + + public entry fun do_contract_upgrade( + metadata_serialized: vector, + code: vector>, + ) { + // Check to see if the hash of the given code and metadata matches the authorized hash. + // The aptos framework does no validation of the metadata, so we include it in the hash. + assert!(matches_hash(code, metadata_serialized, state::get_contract_upgrade_authorized_hash()), error::invalid_upgrade_hash()); + // Perform the upgrade + let pyth = state::pyth_signer(); + code::publish_package_txn(&pyth, metadata_serialized, code); + } + + fun matches_hash(code: vector>, metadata_serialized: vector, hash: Hash): bool { + + // We compute the hash of the hashes of each component (metadata + module). + // code is a vector of vectors of bytes (one for each component), so we need to flatten it before hashing. + let reversed = copy code; + vector::reverse(&mut reversed); + let flattened = aptos_hash::keccak256(metadata_serialized); + while (!vector::is_empty(&reversed)) vector::append(&mut flattened, aptos_hash::keccak256(vector::pop_back(&mut reversed))); + + aptos_hash::keccak256(flattened) == contract_upgrade_hash::destroy(hash) + } +} + +module pyth::contract_upgrade_hash { + use std::vector; + use pyth::error; + + struct Hash has store, drop { + hash: vector, + } + + public fun from_byte_vec(hash: vector): Hash { + assert!(vector::length(&hash) == 32, error::invalid_hash_length()); + Hash { + hash + } + } + + public fun destroy(hash: Hash): vector { + let Hash { hash } = hash; + hash + } +} diff --git a/aptos/contracts/sources/governance/governance.move b/aptos/contracts/sources/governance/governance.move new file mode 100644 index 00000000..7ed9c778 --- /dev/null +++ b/aptos/contracts/sources/governance/governance.move @@ -0,0 +1,408 @@ +module pyth::governance { + use wormhole::vaa::{Self, VAA}; + use pyth::data_source::{Self, DataSource}; + use wormhole::u16; + use pyth::governance_instruction; + use pyth::pyth; + use pyth::governance_action; + use pyth::contract_upgrade; + use pyth::contract_upgrade_hash; + use pyth::set_governance_data_source; + use pyth::set_data_sources; + use pyth::set_stale_price_threshold; + use pyth::error; + use pyth::set_update_fee; + use pyth::state; + use wormhole::external_address; + use std::account; + use std::vector; + + public entry fun execute_governance_instruction(vaa_bytes: vector) { + let parsed_vaa = parse_and_verify_governance_vaa(vaa_bytes); + let instruction = governance_instruction::from_byte_vec(vaa::destroy(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, + error::governance_contract_upgrade_chain_id_zero()); + contract_upgrade::execute(governance_instruction::destroy(instruction)); + } else if (action == governance_action::new_set_governance_data_source()) { + set_governance_data_source::execute(governance_instruction::destroy(instruction)); + } else if (action == governance_action::new_set_data_sources()) { + set_data_sources::execute(governance_instruction::destroy(instruction)); + } else if (action == governance_action::new_set_update_fee()) { + set_update_fee::execute(governance_instruction::destroy(instruction)); + } else if (action == governance_action::new_set_stale_price_threshold()) { + set_stale_price_threshold::execute(governance_instruction::destroy(instruction)); + } else { + governance_instruction::destroy(instruction); + assert!(false, error::invalid_governance_action()); + } + } + + fun parse_and_verify_governance_vaa(bytes: vector): VAA { + let parsed_vaa = vaa::parse_and_verify(bytes); + + // Check that the governance data source is valid + assert!( + state::is_valid_governance_data_source( + data_source::new( + u16::to_u64(vaa::get_emitter_chain(&parsed_vaa)), + vaa::get_emitter_address(&parsed_vaa))), + error::invalid_governance_data_source()); + + // Check that the sequence number is greater than the last executed governance VAA + let sequence = vaa::get_sequence(&parsed_vaa); + assert!(sequence > state::get_last_executed_governance_sequence(), error::invalid_governance_sequence_number()); + state::set_last_executed_governance_sequence(sequence); + + parsed_vaa + } + + #[test_only] + fun setup_test( + stale_price_threshold: u64, + governance_emitter_chain_id: u64, + governance_emitter_address: vector, + update_fee: u64, + ) { + // Initialize wormhole with a large message collection fee + wormhole::wormhole_test::setup(100000); + + // Deploy and initialize a test instance of the Pyth contract + let deployer = account::create_signer_with_capability(& + account::create_test_signer_cap(@0x277fa055b6a73c42c0662d5236c65c864ccbf2d4abd21f174a30c8b786eab84b)); + let (_pyth, signer_capability) = account::create_resource_account(&deployer, b"pyth"); + pyth::init_test(signer_capability, stale_price_threshold, governance_emitter_chain_id, governance_emitter_address, vector[], update_fee); + } + + #[test] + #[expected_failure(abort_code = 6)] + fun test_execute_governance_instruction_invalid_vaa() { + setup_test(50, 24, x"f06413c0148c78916554f134dcd17a7c8029a3a2bda475a4a1182305c53078bf", 100); + let vaa_bytes = x"6c436741b108"; + execute_governance_instruction(vaa_bytes); + } + + #[test] + #[expected_failure(abort_code = 65550)] + fun test_execute_governance_instruction_invalid_data_source() { + setup_test(100, 50, x"f06413c0148c78916554f134dcd17a7c8029a3a2bda475a4a1182305c53078bf", 100); + + // A VAA with: + // - Emitter chain ID of 20 + // - Emitter address of 0xed67fcc21620d1bf9f69db61ea65ea36ae2df4f86c8e1b9503f0da287c24ed41 + let vaa_bytes = x"0100000000010066359039306c20c8e6d0047ca82aef1b3d1059a3196ab9b21ee9eb8d8438c4e06c3f181d86687cf52f8c4a167ce8af6a5dbadad22253a4016dc28a25f181a37301527e4f9b000000010014ed67fcc21620d1bf9f69db61ea65ea36ae2df4f86c8e1b9503f0da287c24ed410000000000000000005054474eb01087a85361f738f19454e66664d3c9"; + execute_governance_instruction(vaa_bytes); + } + + #[test] + #[expected_failure(abort_code = 65551)] + fun test_execute_governance_instruction_invalid_sequence_number_0() { + setup_test(100, 50, x"f06413c0148c78916554f134dcd17a7c8029a3a2bda475a4a1182305c53078bf", 100); + assert!(state::get_last_executed_governance_sequence() == 0, 1); + + // A VAA with: + // - Emitter chain ID 50 + // - Emitter address 0xf06413c0148c78916554f134dcd17a7c8029a3a2bda475a4a1182305c53078bf + // - Sequence number 0 + let vaa_bytes = x"010000000001004d7facf7151ada96a35a3f099843c5f13bd0e0a6cbf50722d4e456d370bbce8641ecc16450979d4c403888f9f08d5975503d810732dc95575880d2a4c64d40aa01527e4f9b000000010032f06413c0148c78916554f134dcd17a7c8029a3a2bda475a4a1182305c53078bf0000000000000000005054474eb01087a85361f738f19454e66664d3c9"; + execute_governance_instruction(vaa_bytes); + } + + #[test] + #[expected_failure(abort_code = 65556)] + fun test_execute_governance_instruction_invalid_instruction_magic() { + setup_test(100, 50, x"f06413c0148c78916554f134dcd17a7c8029a3a2bda475a4a1182305c53078bf", 100); + + // A VAA with: + // - Emitter chain ID 50 + // - Emitter address 0xf06413c0148c78916554f134dcd17a7c8029a3a2bda475a4a1182305c53078bf + // - Sequence number 1 + // - A payload with the value x"5054474eb01087a85361f738f19454e66664d3c9", so the magic number will be 5054474e + let vaa_bytes = x"01000000000100583334c65aff30780bf7f2ac783398a2a985e3e4873264e46c3cddfdfb2eaa484365e9f4a3ecc14d059ac1cf0a7b6a58075749ad17a3bfd4153d8f45b9084a3500527e4f9b000000010032f06413c0148c78916554f134dcd17a7c8029a3a2bda475a4a1182305c53078bf0000000000000001005054474eb01087a85361f738f19454e66664d3c9"; + execute_governance_instruction(vaa_bytes); + } + + #[test] + #[expected_failure(abort_code = 65548)] + fun test_execute_governance_instruction_invalid_module() { + setup_test(100, 50, x"f06413c0148c78916554f134dcd17a7c8029a3a2bda475a4a1182305c53078bf", 100); + + // A VAA with: + // - Emitter chain ID 50 + // - Emitter address 0xf06413c0148c78916554f134dcd17a7c8029a3a2bda475a4a1182305c53078bf + // - Sequence number 1 + // - A payload representing a governance instruction with: + // - Module number 2 + let vaa_bytes = x"010000000001001d9fd73b3fb0fc522eae5eb5bd40ddf68941894495d7cec8c8efdbf462e48715171b5c6d4bbca0c1e3843b3c28d0ca6f3f76874624b5595a3a2cbfdb3907b62501527e4f9b000000010032f06413c0148c78916554f134dcd17a7c8029a3a2bda475a4a1182305c53078bf0000000000000001005054474d0202001003001793a28e2e5b4cb88f69e96fb29a8287a88b23f0e99f5502f81744e904da8e3b4d000c9a4066ce1fa26da1c102a3e268abd3ca58e3b3c25f250e6ad9a3525066fbf8b00012f7778ca023d5cbe37449bab2faa2a133fe02b056c2c25605950320df08750f35"; + execute_governance_instruction(vaa_bytes); + } + + #[test] + #[expected_failure(abort_code = 65549)] + fun test_execute_governance_instruction_invalid_target_chain() { + setup_test(100, 50, x"f06413c0148c78916554f134dcd17a7c8029a3a2bda475a4a1182305c53078bf", 100); + + // A VAA with: + // - Emitter chain ID 50 + // - Emitter address 0xf06413c0148c78916554f134dcd17a7c8029a3a2bda475a4a1182305c53078bf + // - Sequence number 1 + // - A payload representing a governance instruction with: + // - Module number 1 + // - Target chain 17 != wormhole test chain ID 22 + let vaa_bytes = x"010000000001001ed81e10f8e52e6a7daeca12bf0859c14e8dabed737eaed9a1f8227190a9d11c48d58856713243c5d7de08ed49de4aa1efe7c5e6020c11056802e2d702aa4b2e00527e4f9b000000010032f06413c0148c78916554f134dcd17a7c8029a3a2bda475a4a1182305c53078bf0000000000000001005054474d0102001103001793a28e2e5b4cb88f69e96fb29a8287a88b23f0e99f5502f81744e904da8e3b4d000c9a4066ce1fa26da1c102a3e268abd3ca58e3b3c25f250e6ad9a3525066fbf8b00012f7778ca023d5cbe37449bab2faa2a133fe02b056c2c25605950320df08750f35"; + execute_governance_instruction(vaa_bytes); + } + + #[test] + #[expected_failure(abort_code = 65552)] + fun test_execute_governance_instruction_invalid_action() { + setup_test(100, 50, x"f06413c0148c78916554f134dcd17a7c8029a3a2bda475a4a1182305c53078bf", 100); + + // A VAA with: + // - Emitter chain ID 50 + // - Emitter address 0xf06413c0148c78916554f134dcd17a7c8029a3a2bda475a4a1182305c53078bf + // - Sequence number 1 + // - A payload representing a governance instruction with: + // - Module number 1 + // - Target chain 22 + // - Action 19 (invalid) + let vaa_bytes = x"0100000000010049fdadd56a51e8bd30637dbf9fc79a154a80c96479ce223061ec1f5094f2908715d6c691e5f06068873daa79c87fc25deb62555db7c520468d05aa2437fda97201527e4f9b000000010032f06413c0148c78916554f134dcd17a7c8029a3a2bda475a4a1182305c53078bf0000000000000001005054474d0113001603001793a28e2e5b4cb88f69e96fb29a8287a88b23f0e99f5502f81744e904da8e3b4d000c9a4066ce1fa26da1c102a3e268abd3ca58e3b3c25f250e6ad9a3525066fbf8b00012f7778ca023d5cbe37449bab2faa2a133fe02b056c2c25605950320df08750f35"; + execute_governance_instruction(vaa_bytes); + } + + #[test] + fun test_execute_governance_instruction_upgrade_contract() { + setup_test(100, 50, x"f06413c0148c78916554f134dcd17a7c8029a3a2bda475a4a1182305c53078bf", 100); + + // A VAA with: + // - Emitter chain ID 50 + // - Emitter address 0xf06413c0148c78916554f134dcd17a7c8029a3a2bda475a4a1182305c53078bf + // - Sequence number 5 + // - A payload representing a governance instruction with: + // - Module number 1 + // - Target chain 22 + // - AuthorizeContractUpgrade { + // hash: 0xa381a47fd0e97f34c71ef491c82208f58cd0080e784c697e65966d2a25d20d56, + // } + let vaa_bytes = x"010000000001002242229aec7d320a437cb241672dacfbc34c9155c02f60cd806bbfcd69bb7ba667fc069e372ae0443a7f3e08eaad61930b00784faeb2b72ecf5d1b0f0fa486a101527e4f9b000000010032f06413c0148c78916554f134dcd17a7c8029a3a2bda475a4a1182305c53078bf0000000000000005005054474d01000016a381a47fd0e97f34c71ef491c82208f58cd0080e784c697e65966d2a25d20d56"; + + execute_governance_instruction(vaa_bytes); + assert!(state::get_last_executed_governance_sequence() == 5, 1); + + assert!(state::get_contract_upgrade_authorized_hash() == + contract_upgrade_hash::from_byte_vec(x"a381a47fd0e97f34c71ef491c82208f58cd0080e784c697e65966d2a25d20d56"), 1); + } + + #[test] + #[expected_failure(abort_code = 65558)] + fun test_execute_governance_instruction_upgrade_contract_chain_id_zero() { + setup_test(100, 50, x"f06413c0148c78916554f134dcd17a7c8029a3a2bda475a4a1182305c53078bf", 100); + + // A VAA with: + // - Emitter chain ID 50 + // - Emitter address 0xf06413c0148c78916554f134dcd17a7c8029a3a2bda475a4a1182305c53078bf + // - Sequence number 5 + // - A payload representing a governance instruction with: + // - Module number 1 + // - Target chain 0 + // - AuthorizeContractUpgrade { + // hash: 0xa381a47fd0e97f34c71ef491c82208f58cd0080e784c697e65966d2a25d20d56, + // } + let vaa_bytes = x"01000000000100303c10020c537205ed0322b7ec9d9b296f4e3e12e39ebde985ed4ef4c8f5565256cfc6f90800c4683dba62b577cc994e2ca9135d32b955040b94718cdcb5527600527e4f9b000000010032f06413c0148c78916554f134dcd17a7c8029a3a2bda475a4a1182305c53078bf0000000000000005005054474d01000000a381a47fd0e97f34c71ef491c82208f58cd0080e784c697e65966d2a25d20d56"; + + execute_governance_instruction(vaa_bytes); + assert!(state::get_last_executed_governance_sequence() == 5, 1); + + assert!(state::get_contract_upgrade_authorized_hash() == + contract_upgrade_hash::from_byte_vec(x"a381a47fd0e97f34c71ef491c82208f58cd0080e784c697e65966d2a25d20d56"), 1); + } + + #[test] + fun test_execute_governance_instruction_set_governance_data_source() { + let initial_governance_emitter_chain_id = 50; + let initial_governance_emitter_address = x"f06413c0148c78916554f134dcd17a7c8029a3a2bda475a4a1182305c53078bf"; + setup_test(100, initial_governance_emitter_chain_id, initial_governance_emitter_address, 100); + + state::set_last_executed_governance_sequence(25); + + let initial_governance_data_source = data_source::new(initial_governance_emitter_chain_id, external_address::from_bytes(initial_governance_emitter_address)); + assert!(state::is_valid_governance_data_source(initial_governance_data_source), 1); + + // A VAA with: + // - Emitter chain ID 50 + // - Emitter address 0xf06413c0148c78916554f134dcd17a7c8029a3a2bda475a4a1182305c53078bf + // - Sequence number 27 + // - A payload representing a governance instruction with: + // - Module number 1 + // - Target chain 22 + // - SetGovernanceDataSource { + // emitter_chain_id: 9, + // emitter_address: 0x625bae57728a368652a0ab0a89808de5fffa61d3312f1a27c3e200e99b1f3058, + // initial_sequence: 10, + // } + let vaa_bytes = x"01000000000100e8ce9e581b64ab7fbe168a0d9f86d1d2220e57947fb0c75174849838104d5fdf39ceb52ca44706bbe2817e6d33dd84ff92dc13ffe024578722178602ffd1775b01527e4f9b000000010032f06413c0148c78916554f134dcd17a7c8029a3a2bda475a4a1182305c53078bf000000000000001b005054474d010100160009625bae57728a368652a0ab0a89808de5fffa61d3312f1a27c3e200e99b1f3058000000000000000a"; + + execute_governance_instruction(vaa_bytes); + + // Check that the governance data source and sequence number has been updated correctly + assert!(state::get_last_executed_governance_sequence() == 10, 1); + assert!(!state::is_valid_governance_data_source(initial_governance_data_source), 1); + assert!(state::is_valid_governance_data_source( + data_source::new(9, external_address::from_bytes(x"625bae57728a368652a0ab0a89808de5fffa61d3312f1a27c3e200e99b1f3058") + )), 1); + + // Check that we can successfully execute a governance VAA from the new governance data source + // A VAA with: + // - Emitter chain ID 9 + // - Emitter address 0x625bae57728a368652a0ab0a89808de5fffa61d3312f1a27c3e200e99b1f3058 + // - Sequence number 15 + // - A payload representing a governance instruction with: + // - Module number 1 + // - Target chain 22 + // - SetStalePriceThreshold { + // threshold: 900 + // } + let second_vaa_bytes = x"010000000001008df31b9853fe9f49b1949b66e10795595c37dfc5dede5ea15c1d136cc104843e2048488dfffc3d791ac1c11c71cdb7b73f250b00eb6977cd80e943542142c3a500527e4f9b000000010009625bae57728a368652a0ab0a89808de5fffa61d3312f1a27c3e200e99b1f3058000000000000000f005054474d010400160000000000000384"; + execute_governance_instruction(second_vaa_bytes); + + assert!(state::get_last_executed_governance_sequence() == 15, 1); + assert!(state::get_stale_price_threshold_secs() == 900, 1); + } + + #[test] + #[expected_failure(abort_code = 65550)] + fun test_execute_governance_instruction_set_governance_data_source_old_source_invalid() { + let initial_governance_emitter_chain_id = 50; + let initial_governance_emitter_address = x"f06413c0148c78916554f134dcd17a7c8029a3a2bda475a4a1182305c53078bf"; + setup_test(100, initial_governance_emitter_chain_id, initial_governance_emitter_address, 100); + + state::set_last_executed_governance_sequence(25); + + let initial_governance_data_source = data_source::new(initial_governance_emitter_chain_id, external_address::from_bytes(initial_governance_emitter_address)); + assert!(state::is_valid_governance_data_source(initial_governance_data_source), 1); + + // A VAA with: + // - Emitter chain ID 50 + // - Emitter address x"f06413c0148c78916554f134dcd17a7c8029a3a2bda475a4a1182305c53078bf" + // - Sequence number 27 + // - A payload representing a governance instruction with: + // - Module number 1 + // - Target chain 22 + // - SetGovernanceDataSource { + // emitter_chain_id: 9, + // emitter_address: 0x625bae57728a368652a0ab0a89808de5fffa61d3312f1a27c3e200e99b1f3058, + // initial_sequence: 10, + // } + let vaa_bytes = x"01000000000100e8ce9e581b64ab7fbe168a0d9f86d1d2220e57947fb0c75174849838104d5fdf39ceb52ca44706bbe2817e6d33dd84ff92dc13ffe024578722178602ffd1775b01527e4f9b000000010032f06413c0148c78916554f134dcd17a7c8029a3a2bda475a4a1182305c53078bf000000000000001b005054474d010100160009625bae57728a368652a0ab0a89808de5fffa61d3312f1a27c3e200e99b1f3058000000000000000a"; + + execute_governance_instruction(vaa_bytes); + + // Check that the governance data source and sequence number has been updated correctly + assert!(state::get_last_executed_governance_sequence() == 10, 1); + assert!(!state::is_valid_governance_data_source(initial_governance_data_source), 1); + assert!(state::is_valid_governance_data_source( + data_source::new(9, external_address::from_bytes(x"625bae57728a368652a0ab0a89808de5fffa61d3312f1a27c3e200e99b1f3058") + )), 1); + + // Check that we can not longer execute governance VAA's from the old governance data source + // A VAA with: + // - Emitter chain ID 50 + // - Emitter address 0xf06413c0148c78916554f134dcd17a7c8029a3a2bda475a4a1182305c53078bf + // - Sequence number 30 + let second_vaa_bytes = x"010000000001000e2670b14d716673d44f3766684a42a55c49feaf9a38acffb6971ec66fee2a211e7260413ccf4e3de608111dc0b92a131e8c9b8f5e83e6c36d5fc2228e46eb2d01527e4f9b000000010032f06413c0148c78916554f134dcd17a7c8029a3a2bda475a4a1182305c53078bf000000000000001e005054474d010400160000000000000384"; + execute_governance_instruction(second_vaa_bytes); + } + + #[test] + fun test_execute_governance_instruction_set_update_fee() { + let initial_update_fee = 325; + setup_test(100, 50, x"f06413c0148c78916554f134dcd17a7c8029a3a2bda475a4a1182305c53078bf", initial_update_fee); + assert!(state::get_update_fee() == initial_update_fee, 1); + + // A VAA with: + // - Emitter chain ID 50 + // - Emitter address 0xf06413c0148c78916554f134dcd17a7c8029a3a2bda475a4a1182305c53078bf + // - Sequence number 1 + // - A payload representing a governance instruction with: + // - Module number 1 + // - Target chain 22 + // - SetUpdateFee { + // mantissa: 17, + // exponent: 3, + // } + let vaa_bytes = x"010000000001009f843a3359e75940cad00eaec50a1ac075aca3248634576437cfd53d95c2e29859a3a1902a3ef3e0529b434cf63ce96b21e4e6c05204ba62a446371aa132174000527e4f9b000000010032f06413c0148c78916554f134dcd17a7c8029a3a2bda475a4a1182305c53078bf0000000000000001005054474d0103001600000000000000110000000000000003"; + + execute_governance_instruction(vaa_bytes); + assert!(state::get_last_executed_governance_sequence() == 1, 1); + + let expected = 17000; + assert!(state::get_update_fee() == expected, 1); + } + + #[test] + fun test_execute_governance_instruction_set_stale_price_threshold() { + let initial_stale_price_threshold = 125; + setup_test(initial_stale_price_threshold, 50, x"f06413c0148c78916554f134dcd17a7c8029a3a2bda475a4a1182305c53078bf", 100); + assert!(state::get_stale_price_threshold_secs() == initial_stale_price_threshold, 1); + + // A VAA with: + // - Emitter chain ID 50 + // - Emitter address 0xf06413c0148c78916554f134dcd17a7c8029a3a2bda475a4a1182305c53078bf + // - Sequence number 1 + // - A payload representing a governance instruction with: + // - Module number 1 + // - Target chain 22 + // - SetStalePriceThreshold { + // threshold: 756 + // } + let vaa_bytes = x"01000000000100e863ad8824f2c2a1695c6b028fa36c5f654b5f3e8d33712032aa3a2197329f3e2c59fc86cc026e6c68608d9e13982f2a22098bbc877ae2b106f6659ea320850a00527e4f9b000000010032f06413c0148c78916554f134dcd17a7c8029a3a2bda475a4a1182305c53078bf0000000000000001005054474d0104001600000000000002f4"; + + execute_governance_instruction(vaa_bytes); + assert!(state::get_last_executed_governance_sequence() == 1, 1); + + assert!(state::get_stale_price_threshold_secs() == 756, 1); + } + + #[test] + fun test_execute_governance_instruction_set_data_sources() { + setup_test(100, 50, x"f06413c0148c78916554f134dcd17a7c8029a3a2bda475a4a1182305c53078bf", 100); + + // A VAA with: + // - Emitter chain ID 50 + // - Emitter address 0xf06413c0148c78916554f134dcd17a7c8029a3a2bda475a4a1182305c53078bf + // - Sequence number 1 + // - A payload representing a governance instruction with: + // - Module number 1 + // - Target chain 22 + // - SetDataSources { + // sources: [ + // (23, 0x93a28e2e5b4cb88f69e96fb29a8287a88b23f0e99f5502f81744e904da8e3b4d), + // (12, 0x9a4066ce1fa26da1c102a3e268abd3ca58e3b3c25f250e6ad9a3525066fbf8b0), + // (18, 0xf7778ca023d5cbe37449bab2faa2a133fe02b056c2c25605950320df08750f35) + // ] + // } + let vaa_bytes = x"01000000000100d6c0b6dad041866337af989010c88e4230c77ea16aea579a6422aa44a4f0f57e5d0948e40606445bc0753554ffa0c2f9d5c45abf3d3b16a0158957f01cddb6d600527e4f9b000000010032f06413c0148c78916554f134dcd17a7c8029a3a2bda475a4a1182305c53078bf0000000000000001005054474d0102001603001793a28e2e5b4cb88f69e96fb29a8287a88b23f0e99f5502f81744e904da8e3b4d000c9a4066ce1fa26da1c102a3e268abd3ca58e3b3c25f250e6ad9a3525066fbf8b00012f7778ca023d5cbe37449bab2faa2a133fe02b056c2c25605950320df08750f35"; + + // Set an initial data source + let initial_data_source = data_source::new(11, external_address::from_bytes(x"4eeb85a8ee41cccd0becb6428cb8f12fd0790b3ad9e378f4dfd81014bc42db1d")); + state::set_data_sources(vector[initial_data_source]); + + // Execute the VAA + execute_governance_instruction(vaa_bytes); + assert!(state::get_last_executed_governance_sequence() == 1, 1); + + // Check that the data sources have been set correctly + let expected = vector[ + data_source::new(23, external_address::from_bytes(x"93a28e2e5b4cb88f69e96fb29a8287a88b23f0e99f5502f81744e904da8e3b4d")), + data_source::new(12, external_address::from_bytes(x"9a4066ce1fa26da1c102a3e268abd3ca58e3b3c25f250e6ad9a3525066fbf8b0")), + data_source::new(18, external_address::from_bytes(x"f7778ca023d5cbe37449bab2faa2a133fe02b056c2c25605950320df08750f35")), + ]; + assert!(!state::is_valid_data_source(initial_data_source), 1); + while(vector::is_empty(&expected)) { + assert!(state::is_valid_data_source(vector::pop_back(&mut expected)), 1); + } + } +} diff --git a/aptos/contracts/sources/governance/governance_action.move b/aptos/contracts/sources/governance/governance_action.move new file mode 100644 index 00000000..77a5bd2c --- /dev/null +++ b/aptos/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, 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/aptos/contracts/sources/governance/governance_instruction.move b/aptos/contracts/sources/governance/governance_instruction.move new file mode 100644 index 00000000..ae9f9311 --- /dev/null +++ b/aptos/contracts/sources/governance/governance_instruction.move @@ -0,0 +1,86 @@ +module pyth::governance_instruction { + use wormhole::cursor; + use pyth::deserialize; + use pyth::error; + use pyth::governance_action::{Self, GovernanceAction}; + use wormhole::u16; + + 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, error::invalid_governance_module()); + let target_chain_id = instruction.target_chain_id; + assert!(target_chain_id == u16::to_u64(wormhole::state::get_chain_id()) || target_chain_id == 0, error::invalid_governance_target_chain_id()); + } + + public fun from_byte_vec(bytes: vector): GovernanceInstruction { + let cursor = cursor::init(bytes); + let magic = deserialize::deserialize_vector(&mut cursor, 4); + assert!(magic == MAGIC, 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::rest(cursor); + + let instruction = GovernanceInstruction { + module_, + action, + target_chain_id, + 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(abort_code = 65556)] + fun test_from_byte_vec_invalid_magic() { + let bytes = x"5054474eb01087a85361f738f19454e66664d3c9"; + destroy(from_byte_vec(bytes)); + } + + #[test] + #[expected_failure(abort_code = 65548)] + fun test_from_byte_vec_invalid_module() { + let bytes = x"5054474db00187a85361f738f19454e66664d3c9"; + destroy(from_byte_vec(bytes)); + } + + #[test] + #[expected_failure(abort_code = 65548)] + fun test_from_byte_vec_invalid_target_chain_id() { + let bytes = x"5054474db00187a85361f738f19454e66664d3c9"; + destroy(from_byte_vec(bytes)); + } +} diff --git a/aptos/contracts/sources/governance/set_data_sources.move b/aptos/contracts/sources/governance/set_data_sources.move new file mode 100644 index 00000000..408f0c86 --- /dev/null +++ b/aptos/contracts/sources/governance/set_data_sources.move @@ -0,0 +1,42 @@ +module pyth::set_data_sources { + use wormhole::cursor; + use pyth::deserialize; + use wormhole::external_address::{Self}; + use pyth::data_source::{Self, DataSource}; + use pyth::state; + use std::vector; + + friend pyth::governance; + + struct SetDataSources { + sources: vector, + } + + public(friend) fun execute(payload: vector) { + let SetDataSources { sources } = from_byte_vec(payload); + state::set_data_sources(sources); + } + + fun from_byte_vec(bytes: vector): SetDataSources { + let cursor = cursor::init(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, emitter_address)); + + i = i + 1; + }; + + cursor::destroy_empty(cursor); + + SetDataSources { + sources + } + } + +} diff --git a/aptos/contracts/sources/governance/set_governance_data_source.move b/aptos/contracts/sources/governance/set_governance_data_source.move new file mode 100644 index 00000000..ed53c6eb --- /dev/null +++ b/aptos/contracts/sources/governance/set_governance_data_source.move @@ -0,0 +1,34 @@ +module pyth::set_governance_data_source { + use wormhole::cursor; + use pyth::deserialize; + use wormhole::external_address::{Self, ExternalAddress}; + use pyth::data_source; + use pyth::state; + + friend pyth::governance; + + struct SetGovernanceDataSource { + emitter_chain_id: u64, + emitter_address: ExternalAddress, + initial_sequence: u64, + } + + public(friend) fun execute(payload: vector) { + let SetGovernanceDataSource { emitter_chain_id, emitter_address, initial_sequence } = from_byte_vec(payload); + state::set_governance_data_source(data_source::new(emitter_chain_id, emitter_address)); + state::set_last_executed_governance_sequence(initial_sequence); + } + + fun from_byte_vec(bytes: vector): SetGovernanceDataSource { + let cursor = cursor::init(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_address, + initial_sequence + } + } +} diff --git a/aptos/contracts/sources/governance/set_stale_price_threshold.move b/aptos/contracts/sources/governance/set_stale_price_threshold.move new file mode 100644 index 00000000..f2d2d95f --- /dev/null +++ b/aptos/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; + + friend pyth::governance; + + struct SetStalePriceThreshold { + threshold: u64, + } + + public(friend) fun execute(payload: vector) { + let SetStalePriceThreshold { threshold } = from_byte_vec(payload); + state::set_stale_price_threshold_secs(threshold); + } + + fun from_byte_vec(bytes: vector): SetStalePriceThreshold { + let cursor = cursor::init(bytes); + let threshold = deserialize::deserialize_u64(&mut cursor); + cursor::destroy_empty(cursor); + SetStalePriceThreshold { + threshold + } + } +} diff --git a/aptos/contracts/sources/governance/set_update_fee.move b/aptos/contracts/sources/governance/set_update_fee.move new file mode 100644 index 00000000..fbf0e6f7 --- /dev/null +++ b/aptos/contracts/sources/governance/set_update_fee.move @@ -0,0 +1,36 @@ +module pyth::set_update_fee { + use wormhole::cursor; + use pyth::deserialize; + use std::math64; + use pyth::state; + + friend pyth::governance; + + const MAX_U64: u128 = (1 << 64) - 1; + + struct SetUpdateFee { + mantissa: u64, + exponent: u64, + } + + public(friend) fun execute(payload: vector) { + let SetUpdateFee { mantissa, exponent } = from_byte_vec(payload); + let fee = apply_exponent(mantissa, exponent); + state::set_update_fee(fee); + } + + fun from_byte_vec(bytes: vector): SetUpdateFee { + let cursor = cursor::init(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: u64): u64 { + mantissa * math64::pow(10, exponent) + } +} diff --git a/aptos/contracts/sources/i64.move b/aptos/contracts/sources/i64.move new file mode 100644 index 00000000..da696942 --- /dev/null +++ b/aptos/contracts/sources/i64.move @@ -0,0 +1,141 @@ +module pyth::i64 { + use pyth::error; + + const MAX_POSITIVE_MAGNITUDE: u64 = (1 << 63) - 1; + const MAX_NEGATIVE_MAGNITUDE: u64 = (1 << 63); + + /// As Move does not support negative numbers natively, we use our own internal + /// representation. + /// + /// To consume these values, first call `get_is_negative()` to determine if the I64 + /// represents a negative or positive value. Then call `get_magnitude_if_positive()` or + /// `get_magnitude_if_negative()` to get the magnitude of the number in unsigned u64 format. + /// This API forces consumers to handle positive and negative numbers safely. + struct I64 has copy, drop, store { + negative: bool, + magnitude: u64, + } + + public fun new(magnitude: u64, negative: bool): I64 { + let max_magnitude = MAX_POSITIVE_MAGNITUDE; + if (negative) { + max_magnitude = MAX_NEGATIVE_MAGNITUDE; + }; + assert!(magnitude <= max_magnitude, error::magnitude_too_large()); + + + // Ensure we have a single zero representation: (0, false). + // (0, true) is invalid. + if (magnitude == 0) { + negative = false; + }; + + I64 { + magnitude: magnitude, + negative: negative, + } + } + + public fun get_is_negative(i: &I64): bool { + i.negative + } + + public fun get_magnitude_if_positive(in: &I64): u64 { + assert!(!in.negative, error::negative_value()); + in.magnitude + } + + public fun get_magnitude_if_negative(in: &I64): u64 { + assert!(in.negative, error::positive_value()); + in.magnitude + } + + public fun from_u64(from: u64): I64 { + // Use the MSB to determine whether the number is negative or not. + let negative = (from >> 63) == 1; + let magnitude = parse_magnitude(from, negative); + + new(magnitude, negative) + } + + fun parse_magnitude(from: u64, negative: bool): u64 { + // If positive, then return the input verbatamin + if (!negative) { + return from + }; + + // Otherwise convert from two's complement by inverting and adding 1 + let inverted = from ^ 0xFFFFFFFFFFFFFFFF; + inverted + 1 + } + + #[test] + fun test_max_positive_magnitude() { + new(0x7FFFFFFFFFFFFFFF, false); + assert!(&new(1<<63 - 1, false) == &from_u64(1<<63 - 1), 1); + } + + #[test] + #[expected_failure(abort_code = 65557)] + fun test_magnitude_too_large_positive() { + new(0x8000000000000000, false); + } + + #[test] + fun test_max_negative_magnitude() { + new(0x8000000000000000, true); + assert!(&new(1<<63, true) == &from_u64(1<<63), 1); + } + + #[test] + #[expected_failure(abort_code = 65557)] + fun test_magnitude_too_large_negative() { + new(0x8000000000000001, true); + } + + #[test] + fun test_from_u64_positive() { + assert!(from_u64(0x64673) == new(0x64673, false), 1); + } + + #[test] + fun test_from_u64_negative() { + assert!(from_u64(0xFFFFFFFFFFFEDC73) == new(0x1238D, true), 1); + } + + #[test] + fun test_get_is_negative() { + assert!(get_is_negative(&new(234, true)) == true, 1); + assert!(get_is_negative(&new(767, false)) == false, 1); + } + + #[test] + fun test_get_magnitude_if_positive_positive() { + assert!(get_magnitude_if_positive(&new(7686, false)) == 7686, 1); + } + + #[test] + #[expected_failure(abort_code = 196609)] + fun test_get_magnitude_if_positive_negative() { + assert!(get_magnitude_if_positive(&new(7686, true)) == 7686, 1); + } + + #[test] + fun test_get_magnitude_if_negative_negative() { + assert!(get_magnitude_if_negative(&new(7686, true)) == 7686, 1); + } + + #[test] + #[expected_failure(abort_code = 196627)] + fun test_get_magnitude_if_negative_positive() { + assert!(get_magnitude_if_negative(&new(7686, false)) == 7686, 1); + } + + #[test] + fun test_single_zero_representation() { + assert!(&new(0, true) == &new(0, false), 1); + assert!(&new(0, true) == &from_u64(0), 1); + assert!(&new(0, false) == &from_u64(0), 1); + } + +} diff --git a/aptos/contracts/sources/price.move b/aptos/contracts/sources/price.move new file mode 100644 index 00000000..8cf4892d --- /dev/null +++ b/aptos/contracts/sources/price.move @@ -0,0 +1,46 @@ +module pyth::price { + use pyth::i64::I64; + + /// A price with a degree of uncertainty, represented as a price +- a confidence interval. + /// + /// The confidence interval roughly corresponds to the standard error of a normal distribution. + /// Both the price and confidence are stored in a fixed-point numeric representation, + /// `x * (10^expo)`, where `expo` is the exponent. + // + /// Please refer to the documentation at https://docs.pyth.network/consumers/best-practices for how + /// to how this price safely. + struct Price has copy, drop, store { + price: I64, + /// Confidence interval around the price + conf: u64, + /// The exponent + expo: I64, + /// Unix timestamp of when this price was computed + timestamp: u64, + } + + public fun new(price: I64, conf: u64, expo: I64, timestamp: u64): Price { + Price { + price: price, + conf: conf, + expo: expo, + timestamp: timestamp, + } + } + + public fun get_price(price: &Price): I64 { + price.price + } + + public fun get_conf(price: &Price): u64 { + price.conf + } + + public fun get_timestamp(price: &Price): u64 { + price.timestamp + } + + public fun get_expo(price: &Price): I64 { + price.expo + } +} diff --git a/aptos/contracts/sources/price_feed.move b/aptos/contracts/sources/price_feed.move new file mode 100644 index 00000000..01ae10ba --- /dev/null +++ b/aptos/contracts/sources/price_feed.move @@ -0,0 +1,37 @@ +module pyth::price_feed { + use pyth::price_identifier::PriceIdentifier; + use pyth::price::Price; + + /// PriceFeed represents a current aggregate price for a particular product. + struct PriceFeed has copy, drop, store { + /// The price identifier + price_identifier: PriceIdentifier, + /// The current aggregate price + price: Price, + /// The current exponentially moving average aggregate price + ema_price: Price, + } + + public fun new( + price_identifier: PriceIdentifier, + price: Price, + ema_price: Price): PriceFeed { + PriceFeed { + price_identifier: price_identifier, + price: price, + ema_price: ema_price, + } + } + + public fun get_price_identifier(price_feed: &PriceFeed): &PriceIdentifier { + &price_feed.price_identifier + } + + public fun get_price(price_feed: &PriceFeed): Price { + price_feed.price + } + + public fun get_ema_price(price_feed: &PriceFeed): Price { + price_feed.ema_price + } +} diff --git a/aptos/contracts/sources/price_identifier.move b/aptos/contracts/sources/price_identifier.move new file mode 100644 index 00000000..c46f6eda --- /dev/null +++ b/aptos/contracts/sources/price_identifier.move @@ -0,0 +1,22 @@ +module pyth::price_identifier { + use std::vector; + use pyth::error; + + const IDENTIFIER_BYTES_LENGTH: u64 = 32; + + struct PriceIdentifier has copy, drop, store { + bytes: vector, + } + + public fun from_byte_vec(bytes: vector): PriceIdentifier { + assert!(vector::length(&bytes) == IDENTIFIER_BYTES_LENGTH, error::incorrect_identifier_length()); + + PriceIdentifier { + bytes: bytes + } + } + + public fun get_bytes(price_identifier: &PriceIdentifier): vector { + price_identifier.bytes + } +} diff --git a/aptos/contracts/sources/price_info.move b/aptos/contracts/sources/price_info.move new file mode 100644 index 00000000..9f0d1de2 --- /dev/null +++ b/aptos/contracts/sources/price_info.move @@ -0,0 +1,29 @@ +module pyth::price_info { + use pyth::price_feed::PriceFeed; + + 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 { + PriceInfo { + attestation_time: attestation_time, + arrival_time: arrival_time, + price_feed: price_feed, + } + } + + public fun get_price_feed(price_info: &PriceInfo): &PriceFeed { + &price_info.price_feed + } + + public fun get_attestation_time(price_info: &PriceInfo): u64 { + price_info.attestation_time + } + + public fun get_arrival_time(price_info: &PriceInfo): u64 { + price_info.arrival_time + } +} diff --git a/aptos/contracts/sources/price_status.move b/aptos/contracts/sources/price_status.move new file mode 100644 index 00000000..fb0f4898 --- /dev/null +++ b/aptos/contracts/sources/price_status.move @@ -0,0 +1,53 @@ +module pyth::price_status { + use pyth::error; + + /// The price feed is not currently updating for an unknown reason. + const UNKNOWN: u64 = 0; + /// The price feed is updating as expected. + const TRADING: u64 = 1; + + /// PriceStatus represents the availability status of a price feed. + /// Prices should only be used if they have a status of trading. + struct PriceStatus has copy, drop, store { + status: u64, + } + + public fun from_u64(status: u64): PriceStatus { + assert!(status <= TRADING, error::invalid_price_status()); + PriceStatus { + status: status + } + } + + public fun get_status(price_status: &PriceStatus): u64 { + price_status.status + } + + public fun new_unknown(): PriceStatus { + PriceStatus { + status: UNKNOWN, + } + } + + public fun new_trading(): PriceStatus { + PriceStatus { + status: TRADING, + } + } + + #[test] + fun test_unknown_status() { + assert!(PriceStatus{ status: UNKNOWN } == from_u64(0), 1); + } + + #[test] + fun test_trading_status() { + assert!(PriceStatus{ status: TRADING } == from_u64(1), 1); + } + + #[test] + #[expected_failure(abort_code = 65559)] + fun test_invalid_price_status() { + from_u64(3); + } +} diff --git a/aptos/contracts/sources/pyth.move b/aptos/contracts/sources/pyth.move new file mode 100644 index 00000000..2cfd9da7 --- /dev/null +++ b/aptos/contracts/sources/pyth.move @@ -0,0 +1,803 @@ +module pyth::pyth { + use pyth::batch_price_attestation::{Self}; + use pyth::price_identifier::{Self, PriceIdentifier}; + use pyth::price_info::{Self, PriceInfo}; + use pyth::price_feed::{Self}; + use aptos_framework::coin::{Self, Coin, BurnCapability, MintCapability}; + use aptos_framework::aptos_coin::{Self, AptosCoin}; + use pyth::i64; + use pyth::price::Price; + use pyth::price; + use pyth::data_source::{Self, DataSource}; + use aptos_framework::timestamp; + use std::vector; + use pyth::state; + use wormhole::vaa; + use wormhole::u16; + use wormhole::external_address; + use std::account; + use std::signer; + use deployer::deployer; + use pyth::error; + use pyth::event; + +// ----------------------------------------------------------------------------- +// Initialisation functions + + public entry fun init( + deployer: &signer, + 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, + ) { + // Claim the signer capability from the deployer. Note that this is a one-time operation, + // so that this function can only be called once. + let signer_capability = deployer::claim_signer_capability(deployer, @pyth); + init_internal( + signer_capability, + stale_price_threshold, + governance_emitter_chain_id, + governance_emitter_address, + parse_data_sources( + data_sources_emitter_chain_ids, + data_sources_emitter_addresses, + ), + update_fee + ) + } + + fun init_internal( + signer_capability: account::SignerCapability, + stale_price_threshold: u64, + governance_emitter_chain_id: u64, + governance_emitter_address: vector, + data_sources: vector, + update_fee: u64) { + let pyth = account::create_signer_with_capability(&signer_capability); + state::init( + &pyth, + stale_price_threshold, + update_fee, + data_source::new( + governance_emitter_chain_id, + external_address::from_bytes(governance_emitter_address)), + data_sources, + signer_capability + ); + event::init(&pyth); + coin::register(&pyth); + } + + fun parse_data_sources( + emitter_chain_ids: vector, + emitter_addresses: vector>): vector { + 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 + } + + #[test_only] + /// Expose a public initialization function for use in tests + public fun init_test( + signer_capability: account::SignerCapability, + stale_price_threshold: u64, + governance_emitter_chain_id: u64, + governance_emitter_address: vector, + data_sources: vector, + update_fee: u64, + ) { + init_internal( + signer_capability, + stale_price_threshold, + governance_emitter_chain_id, + governance_emitter_address, + data_sources, + update_fee + ) + } + +// ----------------------------------------------------------------------------- +// Update the cached prices + + /// Update the cached price feeds with the data in the given VAAs. This is a + /// convenience wrapper around update_price_feeds(), which allows you to update the price feeds + /// using an entry function. + /// + /// If possible, it is recommended to use update_price_feeds() instead, which avoids the need + /// to pass a signer account. update_price_feeds_with_funder() should only be used when + /// you need to call an entry function. + /// + /// This function will charge an update fee, transferring some AptosCoin's + /// from the given funder account to the Pyth contract. The amount of coins transferred can be + /// queried with get_update_fee(). The signer must have sufficient account balance to + /// pay this fee, otherwise the transaction will abort. + public entry fun update_price_feeds_with_funder(account: &signer, vaas: vector>) { + let coins = coin::withdraw(account, get_update_fee()); + update_price_feeds(vaas, coins); + } + + /// Update the cached price feeds with the data in the given VAAs. + /// The vaas argument is a vector of VAAs encoded as bytes. + /// + /// The given fee must contain a sufficient number of coins to pay the update fee. + /// The update fee amount can be queried by calling get_update_fee(). + public fun update_price_feeds(vaas: vector>, fee: Coin) { + // Update the price feed from each VAA + while (!vector::is_empty(&vaas)) { + update_price_feed_from_single_vaa(vector::pop_back(&mut vaas)); + }; + + // Charge the message update fee + assert!(get_update_fee() <= coin::value(&fee), error::insufficient_fee()); + coin::deposit(@pyth, fee); + } + + fun update_price_feed_from_single_vaa(vaa: vector) { + // Deserialize the VAA + let vaa = vaa::parse_and_verify(vaa); + + // Check that the VAA is from a valid data source (emitter) + assert!( + state::is_valid_data_source( + data_source::new( + u16::to_u64(vaa::get_emitter_chain(&vaa)), + vaa::get_emitter_address(&vaa))), + error::invalid_data_source()); + + // Deserialize the batch price attestation + update_cache(batch_price_attestation::destroy(batch_price_attestation::deserialize(vaa::destroy(vaa)))); + } + + /// Update the cache with given price updates, if they are newer than the ones currently cached. + fun update_cache(updates: vector) { + while (!vector::is_empty(&updates)) { + let update = vector::pop_back(&mut updates); + if (is_fresh_update(&update)) { + let price_feed = *price_info::get_price_feed(&update); + let price_identifier = price_feed::get_price_identifier(&price_feed); + state::set_latest_price_info( + *price_identifier, + update, + ); + event::emit_price_feed_update(price_feed, timestamp::now_microseconds()); + } + }; + 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 entry fun update_price_feeds_if_fresh( + vaas: vector>, + price_identifiers: vector>, + publish_times: vector, + fee: Coin) { + + assert!(vector::length(&price_identifiers) == vector::length(&publish_times), + error::invalid_publish_times_length()); + + let fresh_data = false; + let i = 0; + while (i < vector::length(&publish_times)) { + let price_identifier = price_identifier::from_byte_vec( + *vector::borrow(&price_identifiers, i)); + if (!state::price_info_cached(price_identifier)) { + fresh_data = true; + break + }; + + let cached_timestamp = price::get_timestamp(&get_price_unsafe(price_identifier)); + if (cached_timestamp < *vector::borrow(&publish_times, i)) { + fresh_data = true; + break + }; + + i = i + 1; + }; + + assert!(fresh_data, error::no_fresh_data()); + update_price_feeds(vaas, fee); + } + + /// Determine if the given price update is "fresh": we have nothing newer already cached for that + /// price feed. + fun is_fresh_update(update: &PriceInfo): 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 price_identifier = price_feed::get_price_identifier(price_feed); + if (!price_feed_exists(*price_identifier)) { + return true + }; + let cached_timestamp = price::get_timestamp(&get_price_unsafe(*price_identifier)); + + 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. + + /// Determine if a price feed for the given price_identifier exists + public fun price_feed_exists(price_identifier: PriceIdentifier): bool { + state::price_info_cached(price_identifier) + } + + /// Get the latest available price cached for the given price identifier, if that price is + /// no older than the stale price threshold. + /// + /// Important: it is recommended to call update_price_feeds() to update the cached price + /// before calling this function, as get_price() will abort if the cached price is older + /// than the stale price threshold. + public fun get_price(price_identifier: PriceIdentifier): Price { + get_price_no_older_than(price_identifier, state::get_stale_price_threshold_secs()) + } + + /// 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_identifier: PriceIdentifier, max_age_secs: u64): Price { + let price = get_price_unsafe(price_identifier); + check_price_is_fresh(&price, 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_identifier: PriceIdentifier): Price { + price_feed::get_price( + price_info::get_price_feed(&state::get_latest_price_info(price_identifier))) + } + + 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(): u64 { + state::get_stale_price_threshold_secs() + } + + fun check_price_is_fresh(price: &Price, max_age_secs: u64) { + let age = abs_diff(timestamp::now_seconds(), price::get_timestamp(price)); + assert!(age < max_age_secs, error::stale_price_update()); + } + + /// Get the latest available exponentially moving average price cached for the given + /// price identifier, if that price is no older than the stale price threshold. + /// + /// Important: it is recommended to call update_price_feeds() to update the cached EMA price + /// before calling this function, as get_ema_price() will abort if the cached EMA price is older + /// than the stale price threshold. + public fun get_ema_price(price_identifier: PriceIdentifier): Price { + get_ema_price_no_older_than(price_identifier, state::get_stale_price_threshold_secs()) + } + + /// Get the latest available exponentially moving average price cached for the given price identifier, + /// if that price is no older than the given age. + public fun get_ema_price_no_older_than(price_identifier: PriceIdentifier, max_age_secs: u64): Price { + let price = get_ema_price_unsafe(price_identifier); + check_price_is_fresh(&price, max_age_secs); + + price + } + + /// Get the latest available exponentially moving average 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_ema_price_no_older_than() + /// function should be used in preference to this. + public fun get_ema_price_unsafe(price_identifier: PriceIdentifier): Price { + price_feed::get_ema_price( + price_info::get_price_feed(&state::get_latest_price_info(price_identifier))) + } + + /// Get the number of AptosCoin's required to perform one batch update + public fun get_update_fee(): u64 { + state::get_update_fee() + } + +// ----------------------------------------------------------------------------- +// Tests + + #[test_only] + fun setup_test( + aptos_framework: &signer, + stale_price_threshold: u64, + governance_emitter_chain_id: u64, + governance_emitter_address: vector, + data_sources: vector, + update_fee: u64, + to_mint: u64): (BurnCapability, MintCapability, Coin) { + // Initialize wormhole with a large message collection fee + wormhole::wormhole_test::setup(100000); + + // Set the current time + timestamp::update_global_time_for_test_secs(1663680745); + + // Deploy and initialize a test instance of the Pyth contract + let deployer = account::create_signer_with_capability(& + account::create_test_signer_cap(@0x277fa055b6a73c42c0662d5236c65c864ccbf2d4abd21f174a30c8b786eab84b)); + let (_pyth, signer_capability) = account::create_resource_account(&deployer, b"pyth"); + init_test(signer_capability, stale_price_threshold, governance_emitter_chain_id, governance_emitter_address, data_sources, update_fee); + + let (burn_capability, mint_capability) = aptos_coin::initialize_for_test(aptos_framework); + let coins = coin::mint(to_mint, &mint_capability); + (burn_capability, mint_capability, coins) + } + + #[test_only] + fun cleanup_test(burn_capability: BurnCapability, mint_capability: MintCapability) { + coin::destroy_mint_cap(mint_capability); + coin::destroy_burn_cap(burn_capability); + } + + #[test_only] + fun get_mock_price_infos(): vector { + vector[ + price_info::new( + 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( + 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( + 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( + 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), + ), + ), + ] + } + + #[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(aptos_framework = @aptos_framework)] + #[expected_failure(abort_code = 6)] + fun test_update_price_feeds_corrupt_vaa(aptos_framework: &signer) { + let (burn_capability, mint_capability, coins) = setup_test(aptos_framework, 500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", vector[], 50, 100); + + // Pass in a corrupt VAA, which should fail deseriaizing + let corrupt_vaa = x"90F8bf6A479f320ead074411a4B0e7944Ea8c9C1"; + update_price_feeds(vector[corrupt_vaa], coins); + + cleanup_test(burn_capability, mint_capability); + } + + #[test(aptos_framework = @aptos_framework)] + #[expected_failure(abort_code = 65539)] + fun test_update_price_feeds_invalid_data_source(aptos_framework: &signer) { + // Initialize the contract with some valid data sources, excluding our test VAA's source + let data_sources = vector[ + data_source::new( + 4, external_address::from_bytes(x"0000000000000000000000000000000000000000000000000000000000007742")), + data_source::new( + 5, external_address::from_bytes(x"0000000000000000000000000000000000000000000000000000000000007637")) + ]; + let (burn_capability, mint_capability, coins) = setup_test(aptos_framework, 500, 1, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources, 50, 100); + + update_price_feeds(TEST_VAAS, coins); + + cleanup_test(burn_capability, mint_capability); + } + + #[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::from_bytes(x"0000000000000000000000000000000000000000000000000000000000000004")), + data_source::new( + 5, external_address::from_bytes(x"0000000000000000000000000000000000000000000000000000000000007637")), + data_source::new( + 17, external_address::from_bytes(x"71f8dcb863d176e2c420ad6610cf687359612b6fb392e0642b0ca6b1f186aa3b")) + ] + } + + #[test(aptos_framework = @aptos_framework)] + #[expected_failure(abort_code = 65542)] + fun test_update_price_feeds_insufficient_fee(aptos_framework: &signer) { + let (burn_capability, mint_capability, coins) = setup_test(aptos_framework, 500, 1, + x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", + data_sources_for_test_vaa(), + // Update fee + 50, + // Coins provided to update < update fee + 20); + + update_price_feeds(TEST_VAAS, coins); + + cleanup_test(burn_capability, mint_capability); + } + + #[test(aptos_framework = @aptos_framework)] + fun test_update_price_feeds_success(aptos_framework: &signer) { + let (burn_capability, mint_capability, coins) = setup_test(aptos_framework, 500, 1, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources_for_test_vaa(), 50, 100); + + // Update the price feeds from the VAA + update_price_feeds(TEST_VAAS, coins); + + // Check that the cache has been updated + let expected = get_mock_price_infos(); + check_price_feeds_cached(&expected); + + cleanup_test(burn_capability, mint_capability); + } + + #[test(aptos_framework = @aptos_framework)] + fun test_update_price_feeds_with_funder(aptos_framework: &signer) { + let update_fee = 50; + let initial_balance = 75; + let (burn_capability, mint_capability, coins) = setup_test(aptos_framework, 500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources_for_test_vaa(), update_fee, initial_balance); + + // Create a test funder account and register it to store funds + let funder_addr = @0xbfbffd8e2af9a3e3ce20d2d2b22bd640; + let funder = account::create_account_for_test(funder_addr); + coin::register(&funder); + coin::deposit(funder_addr, coins); + + assert!(get_update_fee() == update_fee, 1); + assert!(coin::balance(signer::address_of(&funder)) == initial_balance, 1); + assert!(coin::balance(@pyth) == 0, 1); + + // Update the price feeds using the funder + update_price_feeds_with_funder(&funder, TEST_VAAS); + + // Check that the price feeds are now cached + check_price_feeds_cached(&get_mock_price_infos()); + + // Check that the funder's balance has decreased by the update_fee amount + assert!(coin::balance(signer::address_of(&funder)) == initial_balance - get_update_fee(), 1); + + // Check that the amount has been transferred to the Pyth contract + assert!(coin::balance(@pyth) == get_update_fee(), 1); + + cleanup_test(burn_capability, mint_capability); + } + + #[test(aptos_framework = @aptos_framework)] + #[expected_failure(abort_code = 65542)] + fun test_update_price_feeds_with_funder_insufficient_balance(aptos_framework: &signer) { + let update_fee = 50; + let initial_balance = 25; + let (burn_capability, mint_capability, coins) = setup_test(aptos_framework, 500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources_for_test_vaa(), update_fee, initial_balance); + + // Create a test funder account and register it to store funds + let funder_addr = @0xbfbffd8e2af9a3e3ce20d2d2b22bd640; + let funder = account::create_account_for_test(funder_addr); + coin::register(&funder); + coin::deposit(funder_addr, coins); + + assert!(get_update_fee() == update_fee, 1); + assert!(coin::balance(signer::address_of(&funder)) == initial_balance, 1); + assert!(coin::balance(@pyth) == 0, 1); + + // Update the price feeds using the funder + update_price_feeds_with_funder(&funder, TEST_VAAS); + + cleanup_test(burn_capability, mint_capability); + } + + #[test_only] + fun check_price_feeds_cached(expected: &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 price_identifier = *price_feed::get_price_identifier(price_feed); + assert!(price_feed_exists(price_identifier), 1); + let cached_price = get_price(price_identifier); + + assert!(cached_price == price, 1); + + let ema_price = price_feed::get_ema_price(price_feed); + let cached_ema_price = get_ema_price(price_identifier); + + assert!(cached_ema_price == ema_price, 1); + + i = i + 1; + }; + + } + + #[test(aptos_framework = @aptos_framework)] + fun test_update_cache(aptos_framework: &signer) { + let (burn_capability, mint_capability, coins) = setup_test(aptos_framework, 500, 1, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources_for_test_vaa(), 50, 0); + + let updates = get_mock_price_infos(); + + // Check that initially the price feeds are not cached + let i = 0; + while (i < vector::length(&updates)) { + let price_feed = price_info::get_price_feed(vector::borrow(&updates, i)); + assert!(!price_feed_exists(*price_feed::get_price_identifier(price_feed)), 1); + i = i + 1; + }; + + // Submit the updates + update_cache(updates); + + // Check that the price feeds are now cached + check_price_feeds_cached(&updates); + + cleanup_test(burn_capability, mint_capability); + coin::destroy_zero(coins); + } + + #[test(aptos_framework = @aptos_framework)] + fun test_update_cache_old_update(aptos_framework: &signer) { + let (burn_capability, mint_capability, coins) = setup_test(aptos_framework, 1000, 1, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources_for_test_vaa(), 50, 0); + + // Submit a price update + let timestamp = 1663680700; + let price_identifier = price_identifier::from_byte_vec(x"baa284eaf23edf975b371ba2818772f93dbae72836bbdea28b07d40f3cf8b485"); + let price = price::new(i64::new(7648, false), 674, i64::new(8, true), timestamp); + let ema_price = price::new(i64::new(1536, true), 869, i64::new(100, false), timestamp); + let update = price_info::new( + 1257278600, + 1690226180, + price_feed::new( + price_identifier, + price, + ema_price, + ) + ); + update_cache(vector[update]); + + // Check that we can retrieve the current price + assert!(get_price(price_identifier) == price, 1); + + // 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( + 1257278600, + 1690226180, + price_feed::new( + price_identifier, + old_price, + old_ema_price, + ) + ); + update_cache(vector[old_update]); + + // Confirm that the current price and ema price didn't change + assert!(get_price(price_identifier) == price, 1); + assert!(get_ema_price(price_identifier) == ema_price, 1); + + // Update the cache with a fresh update + let fresh_price = price::new(i64::new(4857, true), 9979, i64::new(243, false), timestamp + 200); + let fresh_ema_price = price::new(i64::new(74637, false), 9979, i64::new(1433, false), timestamp + 1); + let fresh_update = price_info::new( + 1257278600, + 1690226180, + price_feed::new( + price_identifier, + fresh_price, + fresh_ema_price, + ) + ); + update_cache(vector[fresh_update]); + + // Confirm that the current price was updated + assert!(get_price(price_identifier) == fresh_price, 1); + assert!(get_ema_price(price_identifier) == fresh_ema_price, 1); + + cleanup_test(burn_capability, mint_capability); + coin::destroy_zero(coins); + } + + #[test(aptos_framework = @aptos_framework)] + #[expected_failure(abort_code = 524292)] + fun test_stale_price_threshold_exceeded(aptos_framework: &signer) { + let stale_price_threshold = 500; + let (burn_capability, mint_capability, coins) = setup_test(aptos_framework, stale_price_threshold, 1, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources_for_test_vaa(), 50, 0); + + // Submit a price update + let current_timestamp = timestamp::now_seconds(); + let price_identifier = price_identifier::from_byte_vec(x"baa284eaf23edf975b371ba2818772f93dbae72836bbdea28b07d40f3cf8b485"); + let price = price::new(i64::new(7648, false), 674, i64::new(8, true), current_timestamp); + let update = price_info::new( + 1257278600, + 1690226180, + price_feed::new( + price_identifier, + price, + price::new(i64::new(1536, true), 869, i64::new(100, false), 1257212500), + ) + ); + update_cache(vector[update]); + assert!(get_price(price_identifier) == price, 1); + + // Now advance the clock on the target chain, until the age of the cached update exceeds the + // stale_price_threshold. + timestamp::update_global_time_for_test_secs(current_timestamp + stale_price_threshold); + + // Check that we can access the price if we increase the threshold by 1 + assert!(get_price_no_older_than( + price_identifier, get_stale_price_threshold_secs() + 1) == price, 1); + + // However, retrieving the latest price fails + assert!(get_price(price_identifier) == price, 1); + + cleanup_test(burn_capability, mint_capability); + coin::destroy_zero(coins); + } + + #[test(aptos_framework = @aptos_framework)] + #[expected_failure(abort_code = 524292)] + fun test_stale_price_threshold_exceeded_ema(aptos_framework: &signer) { + let stale_price_threshold = 500; + let (burn_capability, mint_capability, coins) = setup_test(aptos_framework, stale_price_threshold, 1, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources_for_test_vaa(), 50, 0); + + // Submit a price update + let current_timestamp = timestamp::now_seconds(); + let price_identifier = price_identifier::from_byte_vec(x"baa284eaf23edf975b371ba2818772f93dbae72836bbdea28b07d40f3cf8b485"); + let ema_price = price::new(i64::new(1536, true), 869, i64::new(100, false), current_timestamp); + let update = price_info::new( + 1257278600, + 1690226180, + price_feed::new( + price_identifier, + price::new(i64::new(7648, false), 674, i64::new(8, true), 1257212500), + ema_price, + ) + ); + update_cache(vector[update]); + + // Check that the EMA price has been updated + assert!(get_ema_price(price_identifier) == ema_price, 1); + + // Now advance the clock on the target chain, until the age of the cached update exceeds the + // stale_price_threshold. + timestamp::update_global_time_for_test_secs(current_timestamp + stale_price_threshold); + + // Check that we can access the EMA price if we increase the threshold by 1 + assert!(get_ema_price_no_older_than( + price_identifier, get_stale_price_threshold_secs() + 1) == ema_price, 1); + + // However, retrieving the latest EMA price fails + assert!(get_ema_price(price_identifier) == ema_price, 1); + + cleanup_test(burn_capability, mint_capability); + coin::destroy_zero(coins); + } + + #[test(aptos_framework = @aptos_framework)] + #[expected_failure(abort_code = 65541)] + fun test_update_price_feeds_if_fresh_invalid_length(aptos_framework: &signer) { + let (burn_capability, mint_capability, coins) = setup_test(aptos_framework, 500, 1, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources_for_test_vaa(), 50, 0); + + // Update the price feeds + let bytes = vector[vector[0u8, 1u8, 2u8]]; + let price_identifiers = vector[ + x"baa284eaf23edf975b371ba2818772f93dbae72836bbdea28b07d40f3cf8b485", + x"c9d5fe0d836688f4c88c221415d23e4bcabee21a6a21124bfcc9a5410a297818", + x"eaa020c61cc479712813461ce153894a96a6c00b21ed0cfc2798d1f9a9e9c94a", + ]; + let publish_times = vector[ + 734639463 + ]; + update_price_feeds_if_fresh(bytes, price_identifiers, publish_times, coins); + + cleanup_test(burn_capability, mint_capability); + } + + #[test(aptos_framework = @aptos_framework)] + fun test_update_price_feeds_if_fresh_fresh_data(aptos_framework: &signer) { + let (burn_capability, mint_capability, coins) = setup_test(aptos_framework, 500, 1, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources_for_test_vaa(), 50, 50); + + // Update the price feeds + let bytes = TEST_VAAS; + let price_identifiers = vector[ + x"c6c75c89f14810ec1c54c03ab8f1864a4c4032791f05747f560faec380a695d1", + x"3b9551a68d01d954d6387aff4df1529027ffb2fee413082e509feb29cc4904fe", + x"33832fad6e36eb05a8972fe5f219b27b5b2bb2230a79ce79beb4c5c5e7ecc76d", + x"21a28b4c6619968bd8c20e95b0aaed7df2187fd310275347e0376a2cd7427db8", + ]; + let publish_times = vector[ + 1663680745, 1663680730, 1663680760, 1663680720 + ]; + update_price_feeds_if_fresh(bytes, price_identifiers, publish_times, coins); + + // Check that the cache has been updated + let expected = get_mock_price_infos(); + check_price_feeds_cached(&expected); + + cleanup_test(burn_capability, mint_capability); + } + + #[test(aptos_framework = @aptos_framework)] + #[expected_failure(abort_code = 524295)] + fun test_update_price_feeds_if_fresh_stale_data(aptos_framework: &signer) { + let (burn_capability, mint_capability, coins) = setup_test(aptos_framework, 500, 1, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources_for_test_vaa(), 50, 50); + + // First populate the cache + update_cache(get_mock_price_infos()); + + // Now attempt to update the price feeds with publish_times that are older than those we have cached + // This should abort with error::no_fresh_data() + let bytes = TEST_VAAS; + let price_identifiers = vector[ + x"c6c75c89f14810ec1c54c03ab8f1864a4c4032791f05747f560faec380a695d1", + x"3b9551a68d01d954d6387aff4df1529027ffb2fee413082e509feb29cc4904fe", + x"33832fad6e36eb05a8972fe5f219b27b5b2bb2230a79ce79beb4c5c5e7ecc76d", + x"21a28b4c6619968bd8c20e95b0aaed7df2187fd310275347e0376a2cd7427db8", + ]; + let publish_times = vector[ + 67, 35, 26, 64 + ]; + update_price_feeds_if_fresh(bytes, price_identifiers, publish_times, coins); + + cleanup_test(burn_capability, mint_capability); + } +} diff --git a/aptos/contracts/sources/set.move b/aptos/contracts/sources/set.move new file mode 100644 index 00000000..b4a41366 --- /dev/null +++ b/aptos/contracts/sources/set.move @@ -0,0 +1,45 @@ +/// A set data structure. +module pyth::set { + use std::table::{Self, Table}; + 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(): Set { + Set { + keys: vector::empty(), + elems: table::new(), + } + } + + /// 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/aptos/contracts/sources/state.move b/aptos/contracts/sources/state.move new file mode 100644 index 00000000..a6bfb243 --- /dev/null +++ b/aptos/contracts/sources/state.move @@ -0,0 +1,181 @@ +module pyth::state { + use pyth::price_identifier::PriceIdentifier; + use pyth::contract_upgrade_hash::Hash; + use pyth::data_source::DataSource; + use pyth::price_info::PriceInfo; + use std::table::{Self, Table}; + use pyth::set::{Self, Set}; + use std::vector; + use pyth::error; + use std::account; + + friend pyth::pyth; + friend pyth::governance; + friend pyth::contract_upgrade; + friend pyth::set_governance_data_source; + friend pyth::set_update_fee; + friend pyth::set_stale_price_threshold; + friend pyth::set_data_sources; + + /// The valid data sources an attestation VAA can be emitted from + struct DataSources has key { + sources: Set, + } + + /// How long a cached price is considered valid for + struct StalePriceThreshold has key { + threshold_secs: u64, + } + + /// The fee charged per batch update + struct UpdateFee has key { + fee: u64, + } + + /// The Pyth contract signer capability + struct SignerCapability has key { + signer_capability: account::SignerCapability, + } + + /// Mapping of cached price information + struct LatestPriceInfo has key { + info: Table, + } + + /// The allowed data source for governance VAAs + struct GovernanceDataSource has key { + source: DataSource, + } + + /// The last executed governance VAA sequence number + struct LastExecutedGovernanceSequence has key { + sequence: u64, + } + + /// The hash of the code of the authorized contract upgrade + struct ContractUpgradeAuthorized has key { + hash: Hash, + } + + // Initialization + public(friend) fun init( + pyth: &signer, + stale_price_threshold: u64, + update_fee: u64, + governance_data_source: DataSource, + data_sources: vector, + signer_capability: account::SignerCapability) { + move_to(pyth, StalePriceThreshold{ + threshold_secs: stale_price_threshold, + }); + move_to(pyth, UpdateFee{ + fee: update_fee, + }); + let sources = set::new(); + while (!vector::is_empty(&data_sources)) { + set::add(&mut sources, vector::pop_back(&mut data_sources)); + }; + move_to(pyth, DataSources{ + sources, + }); + move_to(pyth, GovernanceDataSource{ + source: governance_data_source, + }); + move_to(pyth, LastExecutedGovernanceSequence{ + sequence: 0, + }); + move_to(pyth, SignerCapability{ + signer_capability: signer_capability, + }); + move_to(pyth, LatestPriceInfo{ + info: table::new(), + }); + } + + // Accessors + public fun get_stale_price_threshold_secs(): u64 acquires StalePriceThreshold { + borrow_global(@pyth).threshold_secs + } + + public fun get_update_fee(): u64 acquires UpdateFee { + borrow_global(@pyth).fee + } + + public fun is_valid_data_source(data_source: DataSource): bool acquires DataSources { + set::contains(&borrow_global(@pyth).sources, data_source) + } + + public fun is_valid_governance_data_source(source: DataSource): bool acquires GovernanceDataSource { + let governance_data_source = borrow_global(@pyth); + governance_data_source.source == source + } + + public fun get_last_executed_governance_sequence(): u64 acquires LastExecutedGovernanceSequence { + let last_executed_governance_sequence = borrow_global(@pyth); + last_executed_governance_sequence.sequence + } + + public fun price_info_cached(price_identifier: PriceIdentifier): bool acquires LatestPriceInfo { + let latest_price_info = borrow_global(@pyth); + table::contains(&latest_price_info.info, price_identifier) + } + + public fun get_latest_price_info(price_identifier: PriceIdentifier): PriceInfo acquires LatestPriceInfo { + assert!(price_info_cached(price_identifier), error::unknown_price_feed()); + + let latest_price_info = borrow_global(@pyth); + *table::borrow(&latest_price_info.info, price_identifier) + } + + public fun get_contract_upgrade_authorized_hash(): Hash acquires ContractUpgradeAuthorized { + assert!(exists(@pyth), error::unauthorized_upgrade()); + let ContractUpgradeAuthorized { hash } = move_from(@pyth); + hash + } + + // Setters + public(friend) fun set_data_sources(new_sources: vector) acquires DataSources { + let sources = &mut borrow_global_mut(@pyth).sources; + set::empty(sources); + while (!vector::is_empty(&new_sources)) { + set::add(sources, vector::pop_back(&mut new_sources)); + } + } + + 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_update_fee(fee: u64) acquires UpdateFee { + 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 + } +} diff --git a/aptos/start_node.sh b/aptos/start_node.sh new file mode 100644 index 00000000..4128fda1 --- /dev/null +++ b/aptos/start_node.sh @@ -0,0 +1,2 @@ +#!/bin/bash +aptos node run-local-testnet --with-faucet --force-restart