Skip to content

Commit 866b6a5

Browse files
authored
feat: pyth pull-based push oracle (#1370)
* feat: implement oracle instance * Go * Remove key * Go * Add instance id, fix conditional deser * Go * Rename * Revert changes to cli * Checkpoint * Cleanup deps * Refactor tests * Cleanup deps * Write test * Fix comment * Shard id * ADd tests * Extract common test utils * Fix test * Better name * Cleanup * Instance -> shard * Update test * Make shard id a u16
1 parent a888ba3 commit 866b6a5

File tree

15 files changed

+644
-53
lines changed

15 files changed

+644
-53
lines changed

pythnet/pythnet_sdk/src/test_utils/mod.rs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use {
66
},
77
hashers::keccak256_160::Keccak160,
88
messages::{
9+
FeedId,
910
Message,
1011
PriceFeedMessage,
1112
TwapMessage,
@@ -95,22 +96,30 @@ pub fn dummy_guardians() -> Vec<SecretKey> {
9596
result
9697
}
9798

98-
pub fn create_dummy_price_feed_message(value: i64) -> Message {
99+
pub fn create_dummy_feed_id(value: i64) -> FeedId {
99100
let mut dummy_id = [0; 32];
100101
dummy_id[0] = value as u8;
102+
dummy_id
103+
}
104+
105+
pub fn create_dummy_price_feed_message_with_feed_id(value: i64, feed_id: FeedId) -> Message {
101106
let msg = PriceFeedMessage {
102-
feed_id: dummy_id,
103-
price: value,
104-
conf: value as u64,
105-
exponent: value as i32,
106-
publish_time: value,
107+
feed_id,
108+
price: value,
109+
conf: value as u64,
110+
exponent: value as i32,
111+
publish_time: value,
107112
prev_publish_time: value,
108-
ema_price: value,
109-
ema_conf: value as u64,
113+
ema_price: value,
114+
ema_conf: value as u64,
110115
};
111116
Message::PriceFeedMessage(msg)
112117
}
113118

119+
pub fn create_dummy_price_feed_message(value: i64) -> Message {
120+
create_dummy_price_feed_message_with_feed_id(value, create_dummy_feed_id(value))
121+
}
122+
114123
pub fn create_dummy_twap_message() -> Message {
115124
let msg = TwapMessage {
116125
feed_id: [0; 32],

target_chains/solana/Cargo.lock

Lines changed: 42 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

target_chains/solana/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ members = [
33
"programs/*",
44
"cli/",
55
"program_simulator/",
6-
"pyth_solana_receiver_sdk/"
6+
"pyth_solana_receiver_sdk/",
7+
"common_test_utils"
78
]
89

910
[profile.release]
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
[package]
2+
name = "common-test-utils"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[lib]
7+
crate-type = ["lib"]
8+
name = "common_test_utils"
9+
10+
[dependencies]
11+
pyth-sdk = "0.8.0"
12+
pyth-sdk-solana = "0.8.0"
13+
solana-program-test = { workspace = true }
14+
solana-sdk = { workspace = true }
15+
tokio = "1.14.1"
16+
bincode = "1.3.3"
17+
libsecp256k1 = "0.7.1"
18+
rand = "0.8.5"
19+
lazy_static = "1.4.0"
20+
program-simulator = { path = "../program_simulator" }
21+
wormhole-vaas-serde = { workspace = true }
22+
serde_wormhole = { workspace = true }
23+
pythnet-sdk = { path = "../../../pythnet/pythnet_sdk", features = ["test-utils"] }
24+
anchor-lang = { workspace = true }
25+
solana-program = { workspace = true }
26+
pyth-solana-receiver = { path = "../programs/pyth-solana-receiver" }
27+
wormhole-core-bridge-solana = {workspace = true}
28+
pyth-solana-receiver-sdk = { path = "../pyth_solana_receiver_sdk"}

target_chains/solana/programs/pyth-solana-receiver/tests/common/mod.rs renamed to target_chains/solana/common_test_utils/src/lib.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@ use {
1111
},
1212
ID,
1313
},
14-
pyth_solana_receiver_sdk::config::{
15-
Config,
16-
DataSource,
14+
pyth_solana_receiver_sdk::{
15+
config::{
16+
Config,
17+
DataSource,
18+
},
19+
PYTH_PUSH_ORACLE_ID,
1720
},
1821
pythnet_sdk::test_utils::{
1922
dummy_guardians,
@@ -163,6 +166,7 @@ pub async fn setup_pyth_receiver(
163166
) -> ProgramTestFixtures {
164167
let mut program_test = ProgramTest::default();
165168
program_test.add_program("pyth_solana_receiver", ID, None);
169+
program_test.add_program("pyth_push_oracle", PYTH_PUSH_ORACLE_ID, None);
166170

167171
let mut encoded_vaa_addresses: Vec<Pubkey> = vec![];
168172
for vaa in vaas {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
[package]
2+
name = "pyth-push-oracle"
3+
version = "0.1.0"
4+
description = "Created with Anchor"
5+
edition = "2021"
6+
7+
[lib]
8+
crate-type = ["cdylib", "lib"]
9+
name = "pyth_push_oracle"
10+
11+
[features]
12+
no-entrypoint = []
13+
no-idl = []
14+
no-log-ix-name = []
15+
cpi = ["no-entrypoint"]
16+
test-bpf = []
17+
18+
[dependencies]
19+
anchor-lang = { workspace = true }
20+
pythnet-sdk = { path = "../../../../pythnet/pythnet_sdk" }
21+
solana-program = { workspace = true }
22+
pyth-solana-receiver-sdk = { path = "../../pyth_solana_receiver_sdk"}
23+
pyth-solana-receiver = { path = "../pyth-solana-receiver", features = ["cpi"]}
24+
25+
[dev-dependencies]
26+
solana-sdk = { workspace = true }
27+
tokio = "1.14.1"
28+
program-simulator = { path = "../../program_simulator" }
29+
wormhole-vaas-serde = { workspace = true }
30+
serde_wormhole = { workspace = true }
31+
common-test-utils = { path = "../../common_test_utils" }
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
use {
2+
anchor_lang::prelude::*,
3+
pyth_solana_receiver::{
4+
cpi::accounts::PostUpdate,
5+
program::PythSolanaReceiver,
6+
PostUpdateParams,
7+
},
8+
pyth_solana_receiver_sdk::{
9+
price_update::PriceUpdateV2,
10+
PYTH_PUSH_ORACLE_ID,
11+
},
12+
pythnet_sdk::messages::FeedId,
13+
};
14+
15+
pub mod sdk;
16+
17+
pub const ID: Pubkey = PYTH_PUSH_ORACLE_ID;
18+
19+
#[error_code]
20+
pub enum PushOracleError {
21+
#[msg("Updates must be monotonically increasing")]
22+
UpdatesNotMonotonic,
23+
#[msg("Trying to update price feed with the wrong feed id")]
24+
PriceFeedMessageMismatch,
25+
}
26+
#[program]
27+
pub mod pyth_push_oracle {
28+
29+
use super::*;
30+
31+
pub fn update_price_feed(
32+
ctx: Context<UpdatePriceFeed>,
33+
params: PostUpdateParams,
34+
shard_id: u16,
35+
feed_id: FeedId,
36+
) -> Result<()> {
37+
let cpi_program = ctx.accounts.pyth_solana_receiver.to_account_info().clone();
38+
let cpi_accounts = PostUpdate {
39+
payer: ctx.accounts.payer.to_account_info().clone(),
40+
encoded_vaa: ctx.accounts.encoded_vaa.to_account_info().clone(),
41+
config: ctx.accounts.config.to_account_info().clone(),
42+
treasury: ctx.accounts.treasury.to_account_info().clone(),
43+
price_update_account: ctx.accounts.price_feed_account.to_account_info().clone(),
44+
system_program: ctx.accounts.system_program.to_account_info().clone(),
45+
write_authority: ctx.accounts.price_feed_account.to_account_info().clone(),
46+
};
47+
48+
let seeds = &[
49+
&shard_id.to_le_bytes(),
50+
feed_id.as_ref(),
51+
&[*ctx.bumps.get("price_feed_account").unwrap()],
52+
];
53+
let signer_seeds = &[&seeds[..]];
54+
let cpi_context = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer_seeds);
55+
56+
57+
let current_timestamp = {
58+
if ctx.accounts.price_feed_account.data_is_empty() {
59+
0
60+
} else {
61+
let price_feed_account_data = ctx.accounts.price_feed_account.try_borrow_data()?;
62+
let price_feed_account =
63+
PriceUpdateV2::try_deserialize(&mut &price_feed_account_data[..])?;
64+
price_feed_account.price_message.publish_time
65+
}
66+
};
67+
pyth_solana_receiver::cpi::post_update(cpi_context, params)?;
68+
{
69+
let price_feed_account_data = ctx.accounts.price_feed_account.try_borrow_data()?;
70+
let price_feed_account =
71+
PriceUpdateV2::try_deserialize(&mut &price_feed_account_data[..])?;
72+
73+
require!(
74+
price_feed_account.price_message.publish_time > current_timestamp,
75+
PushOracleError::UpdatesNotMonotonic
76+
);
77+
require!(
78+
price_feed_account.price_message.feed_id == feed_id,
79+
PushOracleError::PriceFeedMessageMismatch
80+
);
81+
}
82+
Ok(())
83+
}
84+
}
85+
86+
#[derive(Accounts)]
87+
#[instruction(params : PostUpdateParams, shard_id : u16, feed_id : FeedId)]
88+
pub struct UpdatePriceFeed<'info> {
89+
#[account(mut)]
90+
pub payer: Signer<'info>,
91+
pub pyth_solana_receiver: Program<'info, PythSolanaReceiver>,
92+
pub encoded_vaa: AccountInfo<'info>,
93+
pub config: AccountInfo<'info>,
94+
#[account(mut)]
95+
pub treasury: AccountInfo<'info>,
96+
#[account(mut, seeds = [&shard_id.to_le_bytes(), &feed_id], bump)]
97+
pub price_feed_account: AccountInfo<'info>,
98+
pub system_program: Program<'info, System>,
99+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
use {
2+
crate::{
3+
accounts,
4+
instruction,
5+
PostUpdateParams,
6+
ID,
7+
},
8+
anchor_lang::{
9+
prelude::*,
10+
system_program,
11+
InstructionData,
12+
},
13+
pyth_solana_receiver::sdk::{
14+
get_config_address,
15+
get_treasury_address,
16+
},
17+
pythnet_sdk::{
18+
messages::FeedId,
19+
wire::v1::MerklePriceUpdate,
20+
},
21+
solana_program::instruction::Instruction,
22+
};
23+
24+
pub fn get_price_feed_address(shard_id: u16, feed_id: FeedId) -> Pubkey {
25+
Pubkey::find_program_address(&[&shard_id.to_le_bytes(), feed_id.as_ref()], &ID).0
26+
}
27+
28+
impl accounts::UpdatePriceFeed {
29+
pub fn populate(
30+
payer: Pubkey,
31+
encoded_vaa: Pubkey,
32+
shard_id: u16,
33+
feed_id: FeedId,
34+
treasury_id: u8,
35+
) -> Self {
36+
accounts::UpdatePriceFeed {
37+
payer,
38+
encoded_vaa,
39+
config: get_config_address(),
40+
treasury: get_treasury_address(treasury_id),
41+
price_feed_account: get_price_feed_address(shard_id, feed_id),
42+
pyth_solana_receiver: pyth_solana_receiver::ID,
43+
system_program: system_program::ID,
44+
}
45+
}
46+
}
47+
48+
impl instruction::UpdatePriceFeed {
49+
pub fn populate(
50+
payer: Pubkey,
51+
encoded_vaa: Pubkey,
52+
shard_id: u16,
53+
feed_id: FeedId,
54+
treasury_id: u8,
55+
merkle_price_update: MerklePriceUpdate,
56+
) -> Instruction {
57+
let update_price_feed_accounts =
58+
accounts::UpdatePriceFeed::populate(payer, encoded_vaa, shard_id, feed_id, treasury_id)
59+
.to_account_metas(None);
60+
Instruction {
61+
program_id: ID,
62+
accounts: update_price_feed_accounts,
63+
data: instruction::UpdatePriceFeed {
64+
params: PostUpdateParams {
65+
merkle_price_update,
66+
treasury_id,
67+
},
68+
shard_id,
69+
feed_id,
70+
}
71+
.data(),
72+
}
73+
}
74+
}

0 commit comments

Comments
 (0)