Skip to content

Commit 51080bc

Browse files
authored
Pyth Aptos Target Chain Contract (#291)
Initial pyth aptos contract
1 parent c81a420 commit 51080bc

26 files changed

+2673
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ bigtable-admin.json
1212
bigtable-writer.json
1313
.vscode
1414
.dccache
15+
.aptos

aptos/contracts/Makefile

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
.PHONY: artifacts
2+
artifacts: build
3+
4+
.PHONY: build
5+
build:
6+
aptos move compile --save-metadata --named-addresses wormhole=0x251011524cd0f76881f16e7c2d822f0c1c9510bfd2430ba24e1b3d52796df204,deployer=0x277fa055b6a73c42c0662d5236c65c864ccbf2d4abd21f174a30c8b786eab84b,pyth=0x277fa055b6a73c42c0662d5236c65c864ccbf2d4abd21f174a30c8b786eab84b
7+
8+
.PHONY: clean
9+
clean:
10+
aptos move clean --assume-yes
11+
12+
.PHONY: test
13+
test:
14+
aptos move test

aptos/contracts/Move.toml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[package]
2+
name = "Pyth"
3+
version = "0.0.1"
4+
upgrade_policy = "compatible"
5+
6+
[dependencies]
7+
# TODO: pin versions before mainnet release
8+
AptosFramework = { git = "https://github.com/aptos-labs/aptos-core.git", subdir = "aptos-move/framework/aptos-framework/", rev = "main" }
9+
MoveStdlib = { git = "https://github.com/aptos-labs/aptos-core.git", subdir = "aptos-move/framework/move-stdlib/", rev = "main" }
10+
AptosStdlib = { git = "https://github.com/aptos-labs/aptos-core.git", subdir = "aptos-move/framework/aptos-stdlib/", rev = "main" }
11+
AptosToken = { git = "https://github.com/aptos-labs/aptos-core.git", subdir = "aptos-move/framework/aptos-token/", rev = "main" }
12+
Wormhole = { git = "https://github.com/wormhole-foundation/wormhole.git", subdir = "aptos/wormhole", rev = "aptos/integration" }
13+
Deployer = { git = "https://github.com/wormhole-foundation/wormhole.git", subdir = "aptos/deployer", rev = "aptos/integration" }
14+
15+
[addresses]
16+
pyth = "_"
17+
deployer = "_"
18+
wormhole = "_"
19+
20+
[dev-addresses]
21+
pyth = "0xe2f37b8ac45d29d5ea23eb7d16dd3f7a7ab6426f5a998d6c23ecd3ae8d9d29eb"
22+
deployer = "0x277fa055b6a73c42c0662d5236c65c864ccbf2d4abd21f174a30c8b786eab84b"
23+
wormhole = "0x251011524cd0f76881f16e7c2d822f0c1c9510bfd2430ba24e1b3d52796df204"
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
module pyth::batch_price_attestation {
2+
use pyth::price_feed::{Self};
3+
use pyth::price;
4+
use pyth::error;
5+
use pyth::i64;
6+
use pyth::price_info::{Self, PriceInfo};
7+
use pyth::price_identifier::{Self};
8+
use pyth::price_status;
9+
use pyth::deserialize::{Self};
10+
use aptos_framework::account;
11+
use aptos_framework::timestamp;
12+
use wormhole::cursor::{Self, Cursor};
13+
use std::vector::{Self};
14+
15+
const MAGIC: u64 = 0x50325748; // "P2WH" (Pyth2Wormhole) raw ASCII bytes
16+
17+
struct BatchPriceAttestation {
18+
header: Header,
19+
attestation_size: u64,
20+
attestation_count: u64,
21+
price_infos: vector<PriceInfo>,
22+
}
23+
24+
struct Header {
25+
magic: u64,
26+
version_major: u64,
27+
version_minor: u64,
28+
header_size: u64,
29+
payload_id: u8,
30+
}
31+
32+
fun deserialize_header(cur: &mut Cursor<u8>): Header {
33+
let magic = deserialize::deserialize_u32(cur);
34+
assert!(magic == MAGIC, error::invalid_attestation_magic_value());
35+
let version_major = deserialize::deserialize_u16(cur);
36+
let version_minor = deserialize::deserialize_u16(cur);
37+
let header_size = deserialize::deserialize_u16(cur);
38+
let payload_id = deserialize::deserialize_u8(cur);
39+
40+
assert!(header_size >= 1, error::invalid_batch_attestation_header_size());
41+
let unknown_header_bytes = header_size - 1;
42+
let _unknown = deserialize::deserialize_vector(cur, unknown_header_bytes);
43+
44+
Header {
45+
magic: magic,
46+
header_size: header_size,
47+
version_minor: version_minor,
48+
version_major: version_major,
49+
payload_id: payload_id,
50+
}
51+
}
52+
53+
public fun destroy(batch: BatchPriceAttestation): vector<PriceInfo> {
54+
let BatchPriceAttestation {
55+
header: Header {
56+
magic: _,
57+
version_major: _,
58+
version_minor: _,
59+
header_size: _,
60+
payload_id: _,
61+
},
62+
attestation_size: _,
63+
attestation_count: _,
64+
price_infos,
65+
} = batch;
66+
price_infos
67+
}
68+
69+
public fun get_attestation_count(batch: &BatchPriceAttestation): u64 {
70+
batch.attestation_count
71+
}
72+
73+
public fun get_price_info(batch: &BatchPriceAttestation, index: u64): &PriceInfo {
74+
vector::borrow(&batch.price_infos, index)
75+
}
76+
77+
public fun deserialize(bytes: vector<u8>): BatchPriceAttestation {
78+
let cur = cursor::init(bytes);
79+
let header = deserialize_header(&mut cur);
80+
81+
let attestation_count = deserialize::deserialize_u16(&mut cur);
82+
let attestation_size = deserialize::deserialize_u16(&mut cur);
83+
let price_infos = vector::empty();
84+
85+
let i = 0;
86+
while (i < attestation_count) {
87+
let price_info = deserialize_price_info(&mut cur);
88+
vector::push_back(&mut price_infos, price_info);
89+
90+
// Consume any excess bytes
91+
let parsed_bytes = 32+32+8+8+4+8+8+1+4+4+8+8+8+8+8;
92+
let _excess = deserialize::deserialize_vector(&mut cur, attestation_size - parsed_bytes);
93+
94+
i = i + 1;
95+
};
96+
cursor::destroy_empty(cur);
97+
98+
BatchPriceAttestation {
99+
header,
100+
attestation_count: attestation_count,
101+
attestation_size: attestation_size,
102+
price_infos: price_infos,
103+
}
104+
}
105+
106+
fun deserialize_price_info(cur: &mut Cursor<u8>): PriceInfo {
107+
108+
// Skip obselete field
109+
let _product_identifier = deserialize::deserialize_vector(cur, 32);
110+
let price_identifier = price_identifier::from_byte_vec(deserialize::deserialize_vector(cur, 32));
111+
let price = deserialize::deserialize_i64(cur);
112+
let conf = deserialize::deserialize_u64(cur);
113+
let expo = deserialize::deserialize_i32(cur);
114+
let ema_price = deserialize::deserialize_i64(cur);
115+
let ema_conf = deserialize::deserialize_u64(cur);
116+
let status = price_status::from_u64((deserialize::deserialize_u8(cur) as u64));
117+
118+
// Skip obselete fields
119+
let _num_publishers = deserialize::deserialize_u32(cur);
120+
let _max_num_publishers = deserialize::deserialize_u32(cur);
121+
122+
let attestation_time = deserialize::deserialize_u64(cur);
123+
let publish_time = deserialize::deserialize_u64(cur); //
124+
let prev_publish_time = deserialize::deserialize_u64(cur);
125+
let prev_price = deserialize::deserialize_i64(cur);
126+
let prev_conf = deserialize::deserialize_u64(cur);
127+
128+
// Handle the case where the status is not trading. This logic will soon be moved into
129+
// the attester.
130+
131+
// If status is trading, use the current price.
132+
// If not, use the the last known trading price.
133+
let current_price = pyth::price::new(price, conf, expo, publish_time);
134+
if (status != price_status::new_trading()) {
135+
current_price = pyth::price::new(prev_price, prev_conf, expo, prev_publish_time);
136+
};
137+
138+
// If status is trading, use the timestamp of the aggregate as the timestamp for the
139+
// EMA price. If not, the EMA will have last been updated when the aggregate last had
140+
// trading status, so use prev_publish_time (the time when the aggregate last had trading status).
141+
let ema_timestamp = publish_time;
142+
if (status != price_status::new_trading()) {
143+
ema_timestamp = prev_publish_time;
144+
};
145+
146+
price_info::new(
147+
attestation_time,
148+
timestamp::now_seconds(),
149+
price_feed::new(
150+
price_identifier,
151+
current_price,
152+
pyth::price::new(ema_price, ema_conf, expo, ema_timestamp),
153+
)
154+
)
155+
}
156+
157+
#[test]
158+
#[expected_failure(abort_code = 65560)]
159+
fun test_deserialize_batch_price_attestation_invalid_magic() {
160+
// A batch price attestation with a magic number of 0x50325749
161+
let bytes = x"5032574900030000000102000400951436e0be37536be96f0896366089506a59763d036728332d3e3038047851aea7c6c75c89f14810ec1c54c03ab8f1864a4c4032791f05747f560faec380a695d1000000000000049a0000000000000008fffffffb00000000000005dc0000000000000003000000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000006150000000000000007215258d81468614f6b7e194c5d145609394f67b041e93e6695dcc616faadd0603b9551a68d01d954d6387aff4df1529027ffb2fee413082e509feb29cc4904fe000000000000041a0000000000000003fffffffb00000000000005cb0000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e4000000000000048600000000000000078ac9cf3ab299af710d735163726fdae0db8465280502eb9f801f74b3c1bd190333832fad6e36eb05a8972fe5f219b27b5b2bb2230a79ce79beb4c5c5e7ecc76d00000000000003f20000000000000002fffffffb00000000000005e70000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e40000000000000685000000000000000861db714e9ff987b6fedf00d01f9fea6db7c30632d6fc83b7bc9459d7192bc44a21a28b4c6619968bd8c20e95b0aaed7df2187fd310275347e0376a2cd7427db800000000000006cb0000000000000001fffffffb00000000000005e40000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000007970000000000000001";
162+
destroy(deserialize(bytes));
163+
}
164+
165+
#[test(aptos_framework = @aptos_framework)]
166+
fun test_deserialize_batch_price_attestation(aptos_framework: signer) {
167+
168+
// Set the arrival time
169+
account::create_account_for_test(@aptos_framework);
170+
timestamp::set_time_has_started_for_testing(&aptos_framework);
171+
let arrival_time = 1663074349;
172+
timestamp::update_global_time_for_test(1663074349 * 1000000);
173+
174+
// A raw batch price attestation
175+
// The first attestation has a status of UNKNOWN
176+
let bytes = x"5032574800030000000102000400951436e0be37536be96f0896366089506a59763d036728332d3e3038047851aea7c6c75c89f14810ec1c54c03ab8f1864a4c4032791f05747f560faec380a695d1000000000000049a0000000000000008fffffffb00000000000005dc0000000000000003000000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000006150000000000000007215258d81468614f6b7e194c5d145609394f67b041e93e6695dcc616faadd0603b9551a68d01d954d6387aff4df1529027ffb2fee413082e509feb29cc4904fe000000000000041a0000000000000003fffffffb00000000000005cb0000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e4000000000000048600000000000000078ac9cf3ab299af710d735163726fdae0db8465280502eb9f801f74b3c1bd190333832fad6e36eb05a8972fe5f219b27b5b2bb2230a79ce79beb4c5c5e7ecc76d00000000000003f20000000000000002fffffffb00000000000005e70000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e40000000000000685000000000000000861db714e9ff987b6fedf00d01f9fea6db7c30632d6fc83b7bc9459d7192bc44a21a28b4c6619968bd8c20e95b0aaed7df2187fd310275347e0376a2cd7427db800000000000006cb0000000000000001fffffffb00000000000005e40000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000007970000000000000001";
177+
178+
let expected = BatchPriceAttestation {
179+
header: Header {
180+
magic: 0x50325748,
181+
version_major: 3,
182+
version_minor: 0,
183+
payload_id: 2,
184+
header_size: 1,
185+
},
186+
attestation_count: 4,
187+
attestation_size: 149,
188+
price_infos: vector<PriceInfo>[
189+
price_info::new(
190+
1663680747,
191+
arrival_time,
192+
price_feed::new(
193+
price_identifier::from_byte_vec(x"c6c75c89f14810ec1c54c03ab8f1864a4c4032791f05747f560faec380a695d1"),
194+
price::new(i64::new(1557, false), 7, i64::new(5, true), 1663680740),
195+
price::new(i64::new(1500, false), 3, i64::new(5, true), 1663680740),
196+
),
197+
),
198+
price_info::new(
199+
1663680747,
200+
arrival_time,
201+
price_feed::new(
202+
price_identifier::from_byte_vec(x"3b9551a68d01d954d6387aff4df1529027ffb2fee413082e509feb29cc4904fe"),
203+
price::new(i64::new(1050, false), 3, i64::new(5, true), 1663680745),
204+
price::new(i64::new(1483, false), 3, i64::new(5, true), 1663680745),
205+
),
206+
),
207+
price_info::new(
208+
1663680747,
209+
arrival_time,
210+
price_feed::new(
211+
price_identifier::from_byte_vec(x"33832fad6e36eb05a8972fe5f219b27b5b2bb2230a79ce79beb4c5c5e7ecc76d"),
212+
price::new(i64::new(1010, false), 2, i64::new(5, true), 1663680745),
213+
price::new(i64::new(1511, false), 3, i64::new(5, true), 1663680745),
214+
),
215+
),
216+
price_info::new(
217+
1663680747,
218+
arrival_time,
219+
price_feed::new(
220+
price_identifier::from_byte_vec(x"21a28b4c6619968bd8c20e95b0aaed7df2187fd310275347e0376a2cd7427db8"),
221+
price::new(i64::new(1739, false), 1, i64::new(5, true), 1663680745),
222+
price::new(i64::new(1508, false), 3, i64::new(5, true), 1663680745),
223+
),
224+
),
225+
],
226+
};
227+
228+
let deserialized = deserialize(bytes);
229+
230+
assert!(&expected == &deserialized, 1);
231+
destroy(expected);
232+
destroy(deserialized);
233+
}
234+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
module pyth::data_source {
2+
use wormhole::external_address::ExternalAddress;
3+
4+
struct DataSource has copy, drop, store {
5+
emitter_chain: u64,
6+
emitter_address: ExternalAddress,
7+
}
8+
9+
public fun new(emitter_chain: u64, emitter_address: ExternalAddress): DataSource {
10+
DataSource {
11+
emitter_chain: emitter_chain,
12+
emitter_address: emitter_address,
13+
}
14+
}
15+
}

0 commit comments

Comments
 (0)