Ver código fonte

Pyth Aptos Target Chain Contract (#291)

Initial pyth aptos contract
Tom Pointon 3 anos atrás
pai
commit
51080bcf5f

+ 1 - 0
.gitignore

@@ -12,3 +12,4 @@ bigtable-admin.json
 bigtable-writer.json
 .vscode
 .dccache
+.aptos

+ 14 - 0
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

+ 23 - 0
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"

+ 234 - 0
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<PriceInfo>,
+    }
+
+    struct Header {
+        magic: u64,
+        version_major: u64,
+        version_minor: u64,
+        header_size: u64,
+        payload_id: u8,
+    }
+
+    fun deserialize_header(cur: &mut Cursor<u8>): 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<PriceInfo> {
+        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<u8>): 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<u8>): 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<PriceInfo>[
+                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);
+    }
+}

+ 15 - 0
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,
+        }
+    }
+}

+ 140 - 0
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<u8>, n: u64): vector<u8> {
+        deserialize::deserialize_vector(cur, n)
+    }
+
+    public fun deserialize_u8(cur: &mut Cursor<u8>): u8 {
+        deserialize::deserialize_u8(cur)
+    }
+
+    public fun deserialize_u16(cur: &mut Cursor<u8>): u64 {
+        u16::to_u64(deserialize::deserialize_u16(cur))
+    }
+
+    public fun deserialize_u32(cur: &mut Cursor<u8>): u64 {
+        u32::to_u64(deserialize::deserialize_u32(cur))
+    }
+
+    public fun deserialize_i32(cur: &mut Cursor<u8>): 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<u8>): u64 {
+        deserialize::deserialize_u64(cur)
+    }
+
+    public fun deserialize_i64(cur: &mut Cursor<u8>): 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);
+    }
+}

+ 101 - 0
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)
+   }
+}

+ 39 - 0
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<PriceFeedUpdate>
+    }
+
+    public(friend) fun init(pyth: &signer) {
+        move_to(
+            pyth,
+            PriceFeedUpdateHandle {
+                event: account::new_event_handle<PriceFeedUpdate>(pyth)
+            }
+        );
+    }
+
+    public(friend) fun emit_price_feed_update(price_feed: PriceFeed, timestamp: u64) acquires PriceFeedUpdateHandle {
+        let event_handle = borrow_global_mut<PriceFeedUpdateHandle>(@pyth);
+        event::emit_event<PriceFeedUpdate>(
+            &mut event_handle.event,
+            PriceFeedUpdate {
+                price_feed,
+                timestamp,
+            }
+        );
+    } 
+}

+ 78 - 0
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<u8>) {
+        let AuthorizeContractUpgrade {hash: hash} = from_byte_vec(payload);
+        state::set_contract_upgrade_authorized_hash(hash)
+    }
+
+    fun from_byte_vec(bytes: vector<u8>): 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<u8>,
+        code: vector<vector<u8>>,
+    ) {
+        // 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<vector<u8>>, metadata_serialized: vector<u8>, 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<u8>,
+    }
+
+    public fun from_byte_vec(hash: vector<u8>): Hash {
+        assert!(vector::length(&hash) == 32, error::invalid_hash_length());
+        Hash {
+            hash
+        }
+    }
+
+    public fun destroy(hash: Hash): vector<u8> {
+        let Hash { hash } = hash;
+        hash
+    }
+}

+ 408 - 0
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<u8>) {
+        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<u8>): 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<u8>,
+        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<DataSource>[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<DataSource>[
+            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);
+        }
+    }
+}

+ 38 - 0
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 }
+    }
+}

+ 86 - 0
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<u8> = x"5054474d"; // "PTGM": Pyth Governance Message
+    const MODULE: u8 = 1;
+
+    struct GovernanceInstruction {
+        module_: u8,
+        action: GovernanceAction,
+        target_chain_id: u64,
+        payload: vector<u8>,
+    }
+
+    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<u8>): 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<u8> {
+        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));
+    }
+}

+ 42 - 0
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<DataSource>,
+    }
+
+    public(friend) fun execute(payload: vector<u8>) {
+        let SetDataSources { sources } = from_byte_vec(payload);
+        state::set_data_sources(sources);
+    }
+
+    fun from_byte_vec(bytes: vector<u8>): 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
+        }
+    }
+
+}

+ 34 - 0
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<u8>) {
+        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<u8>): 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
+        }
+    }
+}

+ 25 - 0
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<u8>) {
+        let SetStalePriceThreshold { threshold } = from_byte_vec(payload);
+        state::set_stale_price_threshold_secs(threshold);
+    }
+
+    fun from_byte_vec(bytes: vector<u8>): SetStalePriceThreshold {
+        let cursor = cursor::init(bytes);
+        let threshold = deserialize::deserialize_u64(&mut cursor);
+        cursor::destroy_empty(cursor);
+        SetStalePriceThreshold {
+            threshold
+        }
+    }
+}

+ 36 - 0
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<u8>) {
+        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<u8>): 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)
+    }
+}

+ 141 - 0
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);
+    }
+
+}

+ 46 - 0
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
+    }
+}

+ 37 - 0
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
+    }
+}

+ 22 - 0
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<u8>,
+    }
+
+    public fun from_byte_vec(bytes: vector<u8>): PriceIdentifier {
+        assert!(vector::length(&bytes) == IDENTIFIER_BYTES_LENGTH, error::incorrect_identifier_length());
+
+        PriceIdentifier {
+            bytes: bytes
+        }
+    }
+
+    public fun get_bytes(price_identifier: &PriceIdentifier): vector<u8> {
+        price_identifier.bytes
+    }
+}

+ 29 - 0
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
+    }
+}

+ 53 - 0
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);
+    }
+}

+ 803 - 0
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<u8>,
+        data_sources_emitter_chain_ids: vector<u64>,
+        data_sources_emitter_addresses: vector<vector<u8>>,
+        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<u8>,
+        data_sources: vector<DataSource>,
+        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<AptosCoin>(&pyth);
+    }
+
+    fun parse_data_sources(
+        emitter_chain_ids: vector<u64>,
+        emitter_addresses: vector<vector<u8>>): vector<DataSource> {
+        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<u8>,
+        data_sources: vector<DataSource>,
+        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<vector<u8>>) {
+        let coins = coin::withdraw<AptosCoin>(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<vector<u8>>, fee: Coin<AptosCoin>) {
+        // 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<u8>) {
+        // 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<PriceInfo>) {
+        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<vector<u8>>,
+        price_identifiers: vector<vector<u8>>,
+        publish_times: vector<u64>,
+        fee: Coin<AptosCoin>) {
+
+        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<u8>,
+        data_sources: vector<DataSource>,
+        update_fee: u64,
+        to_mint: u64): (BurnCapability<AptosCoin>, MintCapability<AptosCoin>, Coin<AptosCoin>) {
+        // 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<AptosCoin>, mint_capability: MintCapability<AptosCoin>) {
+        coin::destroy_mint_cap(mint_capability);
+        coin::destroy_burn_cap(burn_capability);
+    }
+
+    #[test_only]
+    fun get_mock_price_infos(): vector<PriceInfo> {
+        vector<PriceInfo>[
+                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<u8>> = 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<DataSource>[
+            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<DataSource> {
+        // Set some valid data sources, including our test VAA's source
+        vector<DataSource>[
+            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<AptosCoin>(&funder);
+        coin::deposit(funder_addr, coins);
+
+        assert!(get_update_fee() == update_fee, 1);
+        assert!(coin::balance<AptosCoin>(signer::address_of(&funder)) == initial_balance, 1);
+        assert!(coin::balance<AptosCoin>(@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<AptosCoin>(signer::address_of(&funder)) == initial_balance - get_update_fee(), 1);
+
+        // Check that the amount has been transferred to the Pyth contract
+        assert!(coin::balance<AptosCoin>(@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<AptosCoin>(&funder);
+        coin::deposit(funder_addr, coins);
+
+        assert!(get_update_fee() == update_fee, 1);
+        assert!(coin::balance<AptosCoin>(signer::address_of(&funder)) == initial_balance, 1);
+        assert!(coin::balance<AptosCoin>(@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<PriceInfo>) {
+
+        // 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<PriceInfo>[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<PriceInfo>[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<PriceInfo>[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<PriceInfo>[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<PriceInfo>[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);
+    }
+}

+ 45 - 0
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<A: copy + drop> has store {
+        keys: vector<A>,
+        elems: Table<A, Unit>
+    }
+
+    /// Create a new Set.
+    public fun new<A: copy + drop>(): Set<A> {
+        Set {
+            keys: vector::empty<A>(),
+            elems: table::new(),
+        }
+    }
+
+    /// Add a new element to the set.
+    /// Aborts if the element already exists
+    public fun add<A: copy + drop>(set: &mut Set<A>, 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<A: copy + drop>(set: &Set<A>, key: A): bool {
+        table::contains(&set.elems, key)
+    }
+
+    /// Removes all elements from the set
+    public fun empty<A: copy + drop>(set: &mut Set<A>) {
+        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. 
+}

+ 181 - 0
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<DataSource>,
+    }
+
+    /// 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<PriceIdentifier, PriceInfo>,
+    }
+
+    /// 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<DataSource>,
+        signer_capability: account::SignerCapability) {
+            move_to(pyth, StalePriceThreshold{
+                threshold_secs: stale_price_threshold,
+            });
+            move_to(pyth, UpdateFee{
+                fee: update_fee,
+            });
+            let sources = set::new<DataSource>();
+            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<PriceIdentifier, PriceInfo>(),
+            });
+    }
+
+    // Accessors
+    public fun get_stale_price_threshold_secs(): u64 acquires StalePriceThreshold {
+        borrow_global<StalePriceThreshold>(@pyth).threshold_secs
+    }
+
+    public fun get_update_fee(): u64 acquires UpdateFee {
+        borrow_global<UpdateFee>(@pyth).fee
+    }
+
+    public fun is_valid_data_source(data_source: DataSource): bool acquires DataSources {
+        set::contains(&borrow_global<DataSources>(@pyth).sources, data_source)
+    }
+
+    public fun is_valid_governance_data_source(source: DataSource): bool acquires GovernanceDataSource {
+        let governance_data_source = borrow_global<GovernanceDataSource>(@pyth);
+        governance_data_source.source == source
+    }
+
+    public fun get_last_executed_governance_sequence(): u64 acquires LastExecutedGovernanceSequence {
+        let last_executed_governance_sequence = borrow_global<LastExecutedGovernanceSequence>(@pyth);
+        last_executed_governance_sequence.sequence
+    }
+
+    public fun price_info_cached(price_identifier: PriceIdentifier): bool acquires LatestPriceInfo {
+        let latest_price_info = borrow_global<LatestPriceInfo>(@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<LatestPriceInfo>(@pyth);
+        *table::borrow(&latest_price_info.info, price_identifier)
+    }
+
+    public fun get_contract_upgrade_authorized_hash(): Hash acquires ContractUpgradeAuthorized {
+        assert!(exists<ContractUpgradeAuthorized>(@pyth), error::unauthorized_upgrade());
+        let ContractUpgradeAuthorized { hash } = move_from<ContractUpgradeAuthorized>(@pyth);
+        hash
+    }
+
+    // Setters
+    public(friend) fun set_data_sources(new_sources: vector<DataSource>) acquires DataSources {
+        let sources = &mut borrow_global_mut<DataSources>(@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<LatestPriceInfo>(@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<LastExecutedGovernanceSequence>(@pyth);
+        last_executed_governance_sequence.sequence = sequence
+    }
+
+    public(friend) fun pyth_signer(): signer acquires SignerCapability {
+        account::create_signer_with_capability(&borrow_global<SignerCapability>(@pyth).signer_capability)
+    }
+
+    public(friend) fun set_contract_upgrade_authorized_hash(hash: Hash) acquires ContractUpgradeAuthorized, SignerCapability {
+        if (exists<ContractUpgradeAuthorized>(@pyth)) {
+            let ContractUpgradeAuthorized { hash: _ } = move_from<ContractUpgradeAuthorized>(@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<GovernanceDataSource>(@pyth);
+        valid_governance_data_source.source = source;
+    }
+
+    public(friend) fun set_update_fee(fee: u64) acquires UpdateFee {
+        let update_fee = borrow_global_mut<UpdateFee>(@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<StalePriceThreshold>(@pyth);
+        stale_price_threshold.threshold_secs = threshold_secs
+    }
+}

+ 2 - 0
aptos/start_node.sh

@@ -0,0 +1,2 @@
+#!/bin/bash
+aptos node run-local-testnet --with-faucet --force-restart