parent
c81a420a79
commit
51080bcf5f
|
@ -12,3 +12,4 @@ bigtable-admin.json
|
|||
bigtable-writer.json
|
||||
.vscode
|
||||
.dccache
|
||||
.aptos
|
||||
|
|
|
@ -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
|
|
@ -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"
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
#!/bin/bash
|
||||
aptos node run-local-testnet --with-faucet --force-restart
|
Loading…
Reference in New Issue