Forráskód Böngészése

Initial pyth aptos contract

Tom Pointon 3 éve
szülő
commit
a207c81bee

+ 14 - 0
aptos/contracts/Makefile

@@ -0,0 +1,14 @@
+.PHONY: artifacts
+artifacts: build
+
+.PHONY: build
+build:
+	aptos move compile --save-metadata
+
+.PHONY: clean
+clean:
+	aptos move clean --assume-yes
+
+.PHONY: test
+test:
+	aptos move test

+ 19 - 0
aptos/contracts/Move.toml

@@ -0,0 +1,19 @@
+[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]
+# derived address from sha3_256(deployer + "pyth")
+pyth = "0xe2f37b8ac45d29d5ea23eb7d16dd3f7a7ab6426f5a998d6c23ecd3ae8d9d29eb"
+# derived address from sha3_256(deployer + "wormhole")
+wormhole="0x251011524cd0f76881f16e7c2d822f0c1c9510bfd2430ba24e1b3d52796df204"

+ 215 - 0
aptos/contracts/sources/batch_price_attestation.move

@@ -0,0 +1,215 @@
+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};
+
+    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);
+        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);
+
+        price_info::new(
+            attestation_time,
+            timestamp::now_seconds(),
+            price_feed::new(
+                price_identifier,
+                status,
+                pyth::price::new(price, conf, expo, publish_time),
+                pyth::price::new(ema_price, ema_conf, expo, publish_time),
+                pyth::price::new(prev_price, prev_conf, expo, prev_publish_time),
+            )
+        )
+    }
+
+    #[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 
+        let bytes = x"5032574800030000000102000400951436e0be37536be96f0896366089506a59763d036728332d3e3038047851aea7c6c75c89f14810ec1c54c03ab8f1864a4c4032791f05747f560faec380a695d1000000000000049a0000000000000008fffffffb00000000000005dc0000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000006150000000000000007215258d81468614f6b7e194c5d145609394f67b041e93e6695dcc616faadd0603b9551a68d01d954d6387aff4df1529027ffb2fee413082e509feb29cc4904fe000000000000041a0000000000000003fffffffb00000000000005cb0000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e4000000000000048600000000000000078ac9cf3ab299af710d735163726fdae0db8465280502eb9f801f74b3c1bd190333832fad6e36eb05a8972fe5f219b27b5b2bb2230a79ce79beb4c5c5e7ecc76d00000000000003f20000000000000002fffffffb00000000000005e70000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e40000000000000685000000000000000861db714e9ff987b6fedf00d01f9fea6db7c30632d6fc83b7bc9459d7192bc44a21a28b4c6619968bd8c20e95b0aaed7df2187fd310275347e0376a2cd7427db800000000000006cb0000000000000001fffffffb00000000000005e40000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000007970000000000000001";
+        
+        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_status::new_trading(),
+                        price::new(i64::new(1178, false), 8, i64::new(5, true), 1663680745),
+                        price::new(i64::new(1500, false), 3, i64::new(5, true), 1663680745),
+                        price::new(i64::new(1557, false), 7, i64::new(5, true), 1663680740),
+                    ),
+                ),
+                price_info::new(
+                    1663680747,
+                    arrival_time,
+                    price_feed::new(
+                        price_identifier::from_byte_vec(x"3b9551a68d01d954d6387aff4df1529027ffb2fee413082e509feb29cc4904fe"),
+                        price_status::new_trading(),
+                        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::new(i64::new(1158, false), 7, i64::new(5, true), 1663680740),
+                    ),
+                ),
+                price_info::new(
+                    1663680747,
+                    arrival_time,
+                    price_feed::new(
+                        price_identifier::from_byte_vec(x"33832fad6e36eb05a8972fe5f219b27b5b2bb2230a79ce79beb4c5c5e7ecc76d"),
+                        price_status::new_trading(),
+                        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::new(i64::new(1669, false), 8, i64::new(5, true), 1663680740),
+                    ),
+                ),
+                price_info::new(
+                    1663680747,
+                    arrival_time,
+                    price_feed::new(
+                        price_identifier::from_byte_vec(x"21a28b4c6619968bd8c20e95b0aaed7df2187fd310275347e0376a2cd7427db8"),
+                        price_status::new_trading(),
+                        price::new(i64::new(1739, false), 1, i64::new(5, true), 1663680745),
+                        price::new(i64::new(1508, false), 3, i64::new(5, true), 1663680745),
+                        price::new(i64::new(1943, false), 1, i64::new(5, true), 1663680740),
+                    ),
+                ),
+            ],
+        };
+
+        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);
+    }
+}

+ 81 - 0
aptos/contracts/sources/error.move

@@ -0,0 +1,81 @@
+/// 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)
+    }
+}

+ 99 - 0
aptos/contracts/sources/i64.move

@@ -0,0 +1,99 @@
+module pyth::i64 {
+    use pyth::error;
+        
+    /// As Move does not support negative numbers natively, we use our own internal
+    /// representation.
+    struct I64 has copy, drop, store {
+        negative: bool,
+        magnitude: u64,
+    }
+
+    public fun new(magnitude: u64, negative: bool): I64 {
+        I64 {
+            magnitude: magnitude,
+            negative: negative,
+        }
+    }
+
+    public fun get_is_negative(i: &I64): bool {
+        i.negative
+    }
+
+    public fun get_magnitude(in: &I64): u64 {
+        in.magnitude
+    }
+
+    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;
+        return I64 {
+            negative: negative,
+            magnitude: parse_magnitude(from, 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_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() {
+        assert!(get_magnitude(&new(234, false)) == 234, 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);
+    }
+
+}

+ 47 - 0
aptos/contracts/sources/price.move

@@ -0,0 +1,47 @@
+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
+        // can get current timestamp with std::timestamp
+        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
+    }
+}

+ 54 - 0
aptos/contracts/sources/price_feed.move

@@ -0,0 +1,54 @@
+module pyth::price_feed {
+    use pyth::price_identifier::PriceIdentifier;
+    use pyth::price_status::PriceStatus;
+    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 status of the current aggregate price
+        status: PriceStatus,
+        /// The current aggregate price
+        price: Price,
+        /// The current exponentially moving average aggregate price
+        ema_price: Price,
+        /// The most recent previous price with TRADING status
+        previous_price: Price,
+    }
+
+    public fun new(
+        price_identifier: PriceIdentifier,
+        status: PriceStatus,
+        price: Price,
+        ema_price: Price,
+        previous_price: Price): PriceFeed {
+        PriceFeed {
+            price_identifier: price_identifier,
+            status: status,
+            price: price,
+            ema_price: ema_price,
+            previous_price: previous_price
+        }
+    }
+
+    public fun get_price_identifier(price_feed: &PriceFeed): &PriceIdentifier {
+        &price_feed.price_identifier
+    }
+
+    public fun get_status(price_feed: &PriceFeed): PriceStatus {
+        price_feed.status
+    }
+
+    public fun get_price(price_feed: &PriceFeed): Price {
+        price_feed.price
+    }
+
+    public fun get_ema_price(price_feed: &PriceFeed): Price {
+        price_feed.ema_price
+    }
+
+    public fun get_previous_price(price_feed: &PriceFeed): Price {
+        price_feed.previous_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
+    }
+}

+ 51 - 0
aptos/contracts/sources/price_status.move

@@ -0,0 +1,51 @@
+module pyth::price_status {
+    
+    /// 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;
+    /// The price feed is not currently updating because trading in the product has been halted.
+    const HALTED:  u64 = 2;
+    /// The price feed is not currently updating because an auction is setting the price.
+    const AUCTION: u64 = 3;
+
+    /// PriceStatus represents the availability status of a price feed.
+    /// Prices should only be used if they have TRADING status.
+    struct PriceStatus has copy, drop, store {
+        status: u64,
+    }
+
+    public fun from_u64(status: u64): PriceStatus {
+        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,
+        }
+    }
+
+    public fun new_halted(): PriceStatus {
+        PriceStatus {
+            status: HALTED,
+        }
+    }
+
+    public fun new_auction(): PriceStatus {
+        PriceStatus {
+            status: AUCTION,
+        }
+    }
+}

+ 774 - 0
aptos/contracts/sources/pyth.move

@@ -0,0 +1,774 @@
+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::price_status;
+    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 deployer::deployer;
+    use pyth::error;
+
+// -----------------------------------------------------------------------------
+// Initialisation functions
+
+    public entry fun init(
+        deployer: &signer,
+        stale_price_threshold: u64,
+        governance_emitter_chain_id: u64,
+        governance_emitter_address: 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,
+            update_fee
+        )
+    }
+
+    // Private initialisation function, for use in tests
+    fun init_internal(
+        signer_capability: account::SignerCapability,
+        stale_price_threshold: u64,
+        governance_emitter_chain_id: u64,
+        governance_emitter_address: vector<u8>,
+        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)),
+            signer_capability
+        );
+        coin::register<AptosCoin>(&pyth);
+    }
+
+// -----------------------------------------------------------------------------
+// Update the cached prices
+
+    /// Update the cached price feeds with the data in the given vaa_bytes payload.
+    /// The given fee must contain a sufficient number of coins to pay the update fee:
+    /// this amount can be queried by calling get_update_fee().
+    public entry fun update_price_feeds(vaa_bytes: vector<u8>, fee: Coin<AptosCoin>) {
+        // Deserialize the VAA
+        let vaa = vaa::parse_and_verify(vaa_bytes);
+
+        // Charge the message update fee
+        assert!(state::get_update_fee() <= coin::value(&fee), error::insufficient_fee());
+        coin::deposit(@pyth, fee);
+
+        // 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,
+                )
+            }
+        };
+        vector::destroy_empty(updates);
+    }
+
+    /// Update the cached price feeds with the data in the given vaa_bytes payload, 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 paramaters
+    /// are used to determine if the update is fresh without doing any serialisation or verification
+    /// of the VAA, 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(
+        vaa_bytes: vector<u8>,
+        price_identifiers: vector<PriceIdentifier>,
+        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 = vector::borrow(&price_identifiers, i);
+            if (!state::price_info_cached(*price_identifier)) {
+                fresh_data = true;
+                break
+            };
+
+            let price_feed = price_info::get_price_feed(&state::get_latest_price_info(*price_identifier));
+            let cached_timestamp = price::get_timestamp(&price_feed::get_price(price_feed));
+            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(vaa_bytes, 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(
+            &price_feed::get_price(price_info::get_price_feed(&state::get_latest_price_info(*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 cached data for the given price identifier, if that data is 
+    /// no older than the stale price threshold.
+    /// 
+    /// Important: it is recommended to call update_price_feeds() to update the cached data
+    /// before calling this function.
+    public fun get_price(price_identifier: PriceIdentifier): Price {
+        get_price_no_older_than(price_identifier, state::get_stale_price_threshold_secs())
+    }
+
+    /// Get the latest cached data for the given price identifier, if that data 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);
+        let age = abs_diff(timestamp::now_seconds(), price::get_timestamp(&price));
+
+        assert!(age < max_age_secs, error::stale_price_update());
+
+        price
+    }
+
+    /// Get the latest cached exponential moving average price for the given price_identifier.
+    public fun get_ema_price(price_identifier: PriceIdentifier): Price {
+        price_feed::get_ema_price(
+            price_info::get_price_feed(&state::get_latest_price_info(price_identifier)))
+    }
+
+    /// Get the latest available price 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 {
+        let price_feed = price_info::get_price_feed(&state::get_latest_price_info(price_identifier));
+        
+        // If the current price has a status of TRADING, return that
+        let is_trading = price_feed::get_status(price_feed) == price_status::new_trading();
+        if (is_trading) {
+            return price_feed::get_price(price_feed)
+        };
+
+        // Otherwise return the most recent price which has a status of TRADING
+        return price_feed::get_previous_price(price_feed)
+    }
+
+    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().
+    public fun get_stale_price_threshold_secs(): u64 {
+        state::get_stale_price_threshold_secs()
+    }
+
+    /// Get the number of AptosCoin's requried 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>,
+        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);
+
+        // Create a mock signer capability for the Pyth contract
+        let deployer_signer_capability = account::create_test_signer_cap(@0x277fa055b6a73c42c0662d5236c65c864ccbf2d4abd21f174a30c8b786eab84b);
+        let deployer = account::create_signer_with_capability(&deployer_signer_capability);
+        let (_pyth, signer_capability) = account::create_resource_account(&deployer, b"pyth");
+
+        // Initialize the Pyth contract
+        init_internal(signer_capability, stale_price_threshold, governance_emitter_chain_id, governance_emitter_address, 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_status::new_trading(),
+                        price::new(i64::new(1178, false), 8, i64::new(5, true), 1663680745),
+                        price::new(i64::new(1500, false), 3, i64::new(5, true), 1663680745),
+                        price::new(i64::new(1557, false), 7, i64::new(5, true), 1663680740),
+                    ),
+                ),
+                price_info::new(
+                    1663680747,
+                    1663074349,
+                    price_feed::new(
+                        price_identifier::from_byte_vec(x"3b9551a68d01d954d6387aff4df1529027ffb2fee413082e509feb29cc4904fe"),
+                        price_status::new_trading(),
+                        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::new(i64::new(1158, false), 7, i64::new(5, true), 1663680740),
+                    ),
+                ),
+                price_info::new(
+                    1663680747,
+                    1663074349,
+                    price_feed::new(
+                        price_identifier::from_byte_vec(x"33832fad6e36eb05a8972fe5f219b27b5b2bb2230a79ce79beb4c5c5e7ecc76d"),
+                        price_status::new_trading(),
+                        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::new(i64::new(1669, false), 8, i64::new(5, true), 1663680740),
+                    ),
+                ),
+                price_info::new(
+                    1663680747,
+                    1663074349,
+                    price_feed::new(
+                        price_identifier::from_byte_vec(x"21a28b4c6619968bd8c20e95b0aaed7df2187fd310275347e0376a2cd7427db8"),
+                        price_status::new_trading(),
+                        price::new(i64::new(1739, false), 1, i64::new(5, true), 1663680745),
+                        price::new(i64::new(1508, false), 3, i64::new(5, true), 1663680745),
+                        price::new(i64::new(1943, false), 1, i64::new(5, true), 1663680740),
+                    ),
+                ),
+            ]
+    }
+
+    #[test_only]
+    /// A 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_VAA: vector<u8> = x"01000000000100e000fb400939c4cb52ffbf8e91d03d749f6e00868cd056e9d0289f60428971386080d8b98e39b910bbbdeb4c8a7767a3053d1d2884c0a5e297476334d35feb8e00527e4f9b00000001001171f8dcb863d176e2c420ad6610cf687359612b6fb392e0642b0ca6b1f186aa3b0000000000000000005032574800030000000102000400951436e0be37536be96f0896366089506a59763d036728332d3e3038047851aea7c6c75c89f14810ec1c54c03ab8f1864a4c4032791f05747f560faec380a695d1000000000000049a0000000000000008fffffffb00000000000005dc0000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000006150000000000000007215258d81468614f6b7e194c5d145609394f67b041e93e6695dcc616faadd0603b9551a68d01d954d6387aff4df1529027ffb2fee413082e509feb29cc4904fe000000000000041a0000000000000003fffffffb00000000000005cb0000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e4000000000000048600000000000000078ac9cf3ab299af710d735163726fdae0db8465280502eb9f801f74b3c1bd190333832fad6e36eb05a8972fe5f219b27b5b2bb2230a79ce79beb4c5c5e7ecc76d00000000000003f20000000000000002fffffffb00000000000005e70000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e40000000000000685000000000000000861db714e9ff987b6fedf00d01f9fea6db7c30632d6fc83b7bc9459d7192bc44a21a28b4c6619968bd8c20e95b0aaed7df2187fd310275347e0376a2cd7427db800000000000006cb0000000000000001fffffffb00000000000005e40000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000007970000000000000001";
+
+    #[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",
+            50,
+            100);
+
+        // Pass in a corrupt VAA, which should fail deseriaizing
+        let corrupt_vaa = x"90F8bf6A479f320ead074411a4B0e7944Ea8c9C1";
+        update_price_feeds(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_initially(aptos_framework: &signer) {
+        let (burn_capability, mint_capability, coins) = setup_test(
+            aptos_framework,
+            500,
+            1,
+            x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92",
+            50,
+            100);
+        
+        // Without setting any valid data source, the check should fail
+        update_price_feeds(TEST_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) {
+        let (burn_capability, mint_capability, coins) = setup_test(
+            aptos_framework,
+            500,
+            1,
+            x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92",
+            50,
+            100);
+        
+        // Set some valid data sources, excluding our test VAA's source
+        state::set_data_sources(vector<DataSource>[
+            data_source::new(
+                4, external_address::from_bytes(x"0000000000000000000000000000000000000000000000000000000000007742")),
+                data_source::new(
+                5, external_address::from_bytes(x"0000000000000000000000000000000000000000000000000000000000007637"))
+        ]);
+
+        update_price_feeds(TEST_VAA, coins);
+
+        cleanup_test(burn_capability, mint_capability);
+    }
+
+    #[test_only]
+    fun set_data_source_for_test_vaa() {
+        // Set some valid data sources, including our test VAA's source
+        state::set_data_sources(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",
+            // Update fee
+            50,
+            // Coins provided to update < update fee
+            20);
+
+        // Set some valid data sources, excluding our test VAA's source
+        state::set_data_sources(vector<DataSource>[
+            data_source::new(
+                1, external_address::from_bytes(x"0000000000000000000000000000000000000000000000000000000000000004")),
+                data_source::new(
+                5, external_address::from_bytes(x"0000000000000000000000000000000000000000000000000000000000007637"))
+        ]);
+
+        update_price_feeds(TEST_VAA, 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",
+            50,
+            100);
+
+        set_data_source_for_test_vaa();
+    
+        // Update the price feeds from the VAA
+        update_price_feeds(TEST_VAA, 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_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",
+            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,
+            500,
+            1,
+            x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92",
+            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), 1257212500);
+        let prev_price = price::new(i64::new(74637846, false), 8784, i64::new(8768, false), 1257212400);
+        let update = price_info::new(
+            1257278600,
+            1690226180,
+            price_feed::new(
+                    price_identifier,
+                    price_status::new_trading(),
+                    price,
+                    ema_price,
+                    prev_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,
+                    price_status::new_trading(),
+                    old_price,
+                    old_ema_price,
+                    prev_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,
+                    price_status::new_trading(),
+                    fresh_price,
+                    fresh_ema_price,
+                    prev_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)]
+    fun test_get_latest_available_price_unsafe(aptos_framework: &signer) {
+        let (burn_capability, mint_capability, coins) = setup_test(
+            aptos_framework,
+            500,
+            1,
+            x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92",
+            50,
+            0);
+
+        // Submit a price update with a status of trading
+        let timestamp = 1257212500;
+        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 update = price_info::new(
+            1257278600,
+            1690226180,
+            price_feed::new(
+                    price_identifier,
+                    price_status::new_trading(),
+                    price,
+                    price::new(i64::new(1536, true), 869, i64::new(100, false), 1257212500),
+                    price::new(i64::new(74637846, false), 8784, i64::new(8768, false), 1257212400),
+            )
+        );
+        update_cache(vector<PriceInfo>[update]);
+        assert!(get_price_unsafe(price_identifier) == price, 1);
+
+        // Submit a second, newer, update, this time with unknown and a different "previous trading price"
+        let second_timestamp = timestamp + 50;
+        let second_price = price::new(i64::new(7648, false), 674, i64::new(8, true), second_timestamp);
+        let second_previous_price = price::new(i64::new(8794, true), 989, i64::new(10, false), second_timestamp - 100);
+        let second_update = price_info::new(
+            1257278600,
+            1690226180,
+            price_feed::new(
+                    price_identifier,
+                    price_status::new_unknown(),
+                    second_price,
+                    price::new(i64::new(87876, false), 46659, i64::new(9978, false), 1257212400),
+                    second_previous_price
+            )
+        );
+        update_cache(vector<PriceInfo>[second_update]);
+
+        // This time, the latest "unsafe" available price should be second_previous_price,
+        // because the second update had unknown status
+        assert!(get_price_unsafe(price_identifier) == second_previous_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",
+            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_status::new_trading(),
+                    price,
+                    price::new(i64::new(1536, true), 869, i64::new(100, false), 1257212500),
+                    price::new(i64::new(74637846, false), 8784, i64::new(8768, false), 1257212400),
+            )
+        );
+        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 = 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",
+            50,
+            0);
+        
+        // Update the price feeds 
+        let bytes = vector[0u8, 1u8, 2u8];
+        let price_identifiers = vector[
+            price_identifier::from_byte_vec(x"baa284eaf23edf975b371ba2818772f93dbae72836bbdea28b07d40f3cf8b485"),
+            price_identifier::from_byte_vec(x"c9d5fe0d836688f4c88c221415d23e4bcabee21a6a21124bfcc9a5410a297818"),
+            price_identifier::from_byte_vec(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",
+            50,
+            50);
+        set_data_source_for_test_vaa();
+        
+        // Update the price feeds 
+        let bytes = TEST_VAA;
+        let price_identifiers = vector[
+            price_identifier::from_byte_vec(x"c6c75c89f14810ec1c54c03ab8f1864a4c4032791f05747f560faec380a695d1"),
+            price_identifier::from_byte_vec(x"3b9551a68d01d954d6387aff4df1529027ffb2fee413082e509feb29cc4904fe"),
+            price_identifier::from_byte_vec(x"33832fad6e36eb05a8972fe5f219b27b5b2bb2230a79ce79beb4c5c5e7ecc76d"),
+            price_identifier::from_byte_vec(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",
+            50,
+            50);
+        set_data_source_for_test_vaa();
+
+        // 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_VAA;
+        let price_identifiers = vector[
+            price_identifier::from_byte_vec(x"c6c75c89f14810ec1c54c03ab8f1864a4c4032791f05747f560faec380a695d1"),
+            price_identifier::from_byte_vec(x"3b9551a68d01d954d6387aff4df1529027ffb2fee413082e509feb29cc4904fe"),
+            price_identifier::from_byte_vec(x"33832fad6e36eb05a8972fe5f219b27b5b2bb2230a79ce79beb4c5c5e7ecc76d"),
+            price_identifier::from_byte_vec(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. 
+}

+ 176 - 0
aptos/contracts/sources/state.move

@@ -0,0 +1,176 @@
+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>,
+    }
+
+    /// Governance structs
+    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,
+        signer_capability: account::SignerCapability) {
+            move_to(pyth, StalePriceThreshold{
+                threshold_secs: stale_price_threshold,
+            });
+            move_to(pyth, UpdateFee{
+                fee: update_fee,
+            });
+            move_to(pyth, DataSources{
+                sources: set::new<DataSource>(),
+            });
+            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