Skip to content

Commit 9512981

Browse files
committed
feat: assign price feed indexes to old accounts
1 parent 44bf125 commit 9512981

File tree

9 files changed

+203
-26
lines changed

9 files changed

+203
-26
lines changed

program/rust/build.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,8 @@ fn do_make_build(targets: Vec<&str>, out_dir: &Path) {
9191
"C oracle make build did not exit with 0 (code
9292
({:?}).\n\nstdout:\n{}\n\nstderr:\n{}",
9393
make_output.status.code(),
94-
String::from_utf8(make_output.stdout).unwrap_or("<non-utf8>".to_owned()),
95-
String::from_utf8(make_output.stderr).unwrap_or("<non-utf8>".to_owned())
94+
String::from_utf8_lossy(&make_output.stdout),
95+
String::from_utf8_lossy(&make_output.stderr)
9696
);
9797
}
9898
}

program/rust/src/accounts/permission.rs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,15 @@ use {
1111
Pod,
1212
Zeroable,
1313
},
14-
solana_program::pubkey::Pubkey,
15-
std::mem::size_of,
14+
solana_program::{
15+
account_info::AccountInfo,
16+
program_error::ProgramError,
17+
pubkey::Pubkey,
18+
},
19+
std::{
20+
cell::RefMut,
21+
mem::size_of,
22+
},
1623
};
1724

1825
/// This account stores the pubkeys that can execute administrative instructions in the Pyth
@@ -40,6 +47,9 @@ pub struct PermissionAccount {
4047
}
4148

4249
impl PermissionAccount {
50+
pub const MIN_SIZE_WITH_LAST_FEED_INDEX: usize =
51+
size_of::<PermissionAccount>() + size_of::<u32>();
52+
4353
pub fn is_authorized(&self, key: &Pubkey, command: OracleCommand) -> bool {
4454
#[allow(clippy::match_like_matches_macro)]
4555
match (*key, command) {
@@ -50,9 +60,25 @@ impl PermissionAccount {
5060
_ => false,
5161
}
5262
}
63+
64+
pub fn load_last_feed_index_mut<'a>(
65+
account: &'a AccountInfo,
66+
) -> Result<RefMut<'a, u32>, ProgramError> {
67+
let start = size_of::<PermissionAccount>();
68+
let end = start + size_of::<u32>();
69+
assert_eq!(Self::MIN_SIZE_WITH_LAST_FEED_INDEX, end);
70+
if account.data_len() < end {
71+
return Err(ProgramError::AccountDataTooSmall);
72+
}
73+
Ok(RefMut::map(account.try_borrow_mut_data()?, |data| {
74+
bytemuck::from_bytes_mut(&mut data[start..end])
75+
}))
76+
}
5377
}
5478

5579
impl PythAccount for PermissionAccount {
5680
const ACCOUNT_TYPE: u32 = PC_ACCTYPE_PERMISSIONS;
81+
// TODO: change?
82+
// TODO: add feed_index when creating account
5783
const INITIAL_SIZE: u32 = size_of::<PermissionAccount>() as u32;
5884
}

program/rust/src/accounts/price.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ mod price_pythnet {
7171
pub flags: PriceAccountFlags,
7272
/// Globally unique price feed index used for publishing.
7373
/// Limited to 28 bites.
74-
pub feed_index: i32,
74+
pub feed_index: u32,
7575
/// Corresponding product account
7676
pub product_account: Pubkey,
7777
/// Next price account in the list

program/rust/src/error.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ pub enum OracleError {
5252
PermissionViolation = 619,
5353
#[error("NeedsSuccesfulAggregation")]
5454
NeedsSuccesfulAggregation = 620,
55+
#[error("MaxLastFeedIndexReached")]
56+
MaxLastFeedIndexReached = 621,
5557
}
5658

5759
impl From<OracleError> for ProgramError {

program/rust/src/instruction.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@ pub enum OracleCommand {
103103
// account[0] funding account [signer writable]
104104
// account[1] price account [signer writable]
105105
SetMaxLatency = 18,
106+
/// Init price feed index
107+
// account[0] funding account [signer writable]
108+
// account[1] price account [writable]
109+
// account[2] permissions account [writable]
110+
// account[3] system program account []
111+
InitPriceFeedIndex = 19,
106112
}
107113

108114
#[repr(C)]

program/rust/src/lib.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
#![deny(warnings)]
21
// Allow non upper case globals from C
32
#![allow(non_upper_case_globals)]
43

@@ -29,6 +28,7 @@ mod log;
2928
// While we have `pyth-sdk-rs` which exposes a more friendly interface, this is still useful when a
3029
// downstream user wants to confirm for example that they can compile against the binary interface
3130
// of this program for their specific solana version.
31+
pub use crate::error::OracleError;
3232
#[cfg(feature = "strum")]
3333
pub use accounts::MessageType;
3434
#[cfg(feature = "library")]
@@ -47,7 +47,6 @@ pub use accounts::{
4747
};
4848
#[cfg(feature = "library")]
4949
pub use {
50-
crate::error::OracleError,
5150
processor::find_publisher_index,
5251
utils::get_status_for_conf_price_ratio,
5352
};

program/rust/src/processor.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ mod del_product;
2121
mod del_publisher;
2222
mod init_mapping;
2323
mod init_price;
24+
mod init_price_feed_index;
2425
mod set_max_latency;
2526
mod set_min_pub;
2627
mod upd_permissions;
@@ -32,6 +33,7 @@ pub use add_publisher::{
3233
DISABLE_ACCUMULATOR_V2,
3334
ENABLE_ACCUMULATOR_V2,
3435
};
36+
use init_price_feed_index::init_price_feed_index;
3537
pub use {
3638
add_price::add_price,
3739
add_product::add_product,
@@ -85,5 +87,6 @@ pub fn process_instruction(
8587
DelProduct => del_product(program_id, accounts, instruction_data),
8688
UpdPermissions => upd_permissions(program_id, accounts, instruction_data),
8789
SetMaxLatency => set_max_latency(program_id, accounts, instruction_data),
90+
InitPriceFeedIndex => init_price_feed_index(program_id, accounts, instruction_data),
8891
}
8992
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
use {
2+
crate::{
3+
accounts::{
4+
PermissionAccount,
5+
PriceAccount,
6+
},
7+
deserialize::{
8+
load,
9+
load_checked,
10+
},
11+
instruction::CommandHeader,
12+
utils::{
13+
check_permissioned_funding_account,
14+
check_valid_funding_account,
15+
pyth_assert,
16+
},
17+
OracleError,
18+
},
19+
solana_program::{
20+
account_info::AccountInfo,
21+
entrypoint::ProgramResult,
22+
program::invoke,
23+
program_error::ProgramError,
24+
pubkey::Pubkey,
25+
rent::Rent,
26+
system_instruction,
27+
sysvar::Sysvar,
28+
},
29+
std::mem::size_of,
30+
};
31+
32+
/// Init price feed index
33+
// account[0] funding account [signer writable]
34+
// account[1] price account [writable]
35+
// account[2] permissions account [writable]
36+
// account[3] system program account
37+
pub fn init_price_feed_index(
38+
program_id: &Pubkey,
39+
accounts: &[AccountInfo],
40+
instruction_data: &[u8],
41+
) -> ProgramResult {
42+
let cmd = load::<CommandHeader>(instruction_data)?;
43+
44+
pyth_assert(
45+
instruction_data.len() == size_of::<CommandHeader>(),
46+
ProgramError::InvalidArgument,
47+
)?;
48+
49+
let (funding_account, price_account, permissions_account, system_program) = match accounts {
50+
[x, y, p, z] => Ok((x, y, p, z)),
51+
_ => Err(OracleError::InvalidNumberOfAccounts),
52+
}?;
53+
54+
check_valid_funding_account(funding_account)?;
55+
check_permissioned_funding_account(
56+
program_id,
57+
price_account,
58+
funding_account,
59+
permissions_account,
60+
cmd,
61+
)?;
62+
pyth_assert(
63+
solana_program::system_program::check_id(system_program.key),
64+
OracleError::InvalidSystemAccount.into(),
65+
)?;
66+
67+
if permissions_account.data_len() < PermissionAccount::MIN_SIZE_WITH_LAST_FEED_INDEX {
68+
let new_size = PermissionAccount::MIN_SIZE_WITH_LAST_FEED_INDEX;
69+
let rent = Rent::get()?;
70+
let new_minimum_balance = rent.minimum_balance(new_size);
71+
let lamports_diff = new_minimum_balance.saturating_sub(permissions_account.lamports());
72+
if lamports_diff > 0 {
73+
invoke(
74+
&system_instruction::transfer(
75+
funding_account.key,
76+
permissions_account.key,
77+
lamports_diff,
78+
),
79+
&[
80+
funding_account.clone(),
81+
permissions_account.clone(),
82+
system_program.clone(),
83+
],
84+
)?;
85+
}
86+
87+
permissions_account.realloc(new_size, true)?;
88+
}
89+
let mut last_feed_index = PermissionAccount::load_last_feed_index_mut(permissions_account)?;
90+
*last_feed_index += 1;
91+
pyth_assert(
92+
*last_feed_index < (1 << 28),
93+
OracleError::MaxLastFeedIndexReached.into(),
94+
)?;
95+
96+
let mut price_account_data = load_checked::<PriceAccount>(price_account, cmd.version)?;
97+
price_account_data.feed_index = *last_feed_index;
98+
99+
Ok(())
100+
}

program/rust/src/tests/test_upd_price_with_validator.rs

Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ use {
2525
update_clock_slot,
2626
AccountSetup,
2727
},
28-
validator,
28+
validator::{
29+
self,
30+
checked_load_price_account_mut,
31+
},
2932
},
3033
pythnet_sdk::messages::{
3134
PriceFeedMessage,
@@ -125,9 +128,13 @@ fn test_upd_price_with_validator() {
125128
}
126129

127130
// We aggregate the price at the end of each slot now.
128-
let messages1 =
129-
validator::aggregate_price(1, 101, price_account.key, *price_account.data.borrow_mut())
130-
.unwrap();
131+
let messages1 = validator::aggregate_price(
132+
1,
133+
101,
134+
price_account.key,
135+
checked_load_price_account_mut(*price_account.data.borrow_mut()).unwrap(),
136+
)
137+
.unwrap();
131138
let expected_messages1 = [
132139
PriceFeedMessage {
133140
feed_id: price_account.key.to_bytes(),
@@ -155,9 +162,13 @@ fn test_upd_price_with_validator() {
155162
assert_eq!(messages1, expected_messages1);
156163

157164
update_clock_slot(&mut clock_account, 2);
158-
let messages2 =
159-
validator::aggregate_price(2, 102, price_account.key, *price_account.data.borrow_mut())
160-
.unwrap();
165+
let messages2 = validator::aggregate_price(
166+
2,
167+
102,
168+
price_account.key,
169+
checked_load_price_account_mut(*price_account.data.borrow_mut()).unwrap(),
170+
)
171+
.unwrap();
161172

162173
let expected_messages2 = [
163174
PriceFeedMessage {
@@ -214,8 +225,13 @@ fn test_upd_price_with_validator() {
214225

215226
// next price doesn't change but slot does
216227
populate_instruction(&mut instruction_data, 81, 2, 3);
217-
validator::aggregate_price(3, 103, price_account.key, *price_account.data.borrow_mut())
218-
.unwrap();
228+
validator::aggregate_price(
229+
3,
230+
103,
231+
price_account.key,
232+
checked_load_price_account_mut(*price_account.data.borrow_mut()).unwrap(),
233+
)
234+
.unwrap();
219235
update_clock_slot(&mut clock_account, 4);
220236
assert!(process_instruction(
221237
&program_id,
@@ -242,8 +258,13 @@ fn test_upd_price_with_validator() {
242258

243259
// next price doesn't change and neither does aggregate but slot does
244260
populate_instruction(&mut instruction_data, 81, 2, 4);
245-
validator::aggregate_price(4, 104, price_account.key, *price_account.data.borrow_mut())
246-
.unwrap();
261+
validator::aggregate_price(
262+
4,
263+
104,
264+
price_account.key,
265+
checked_load_price_account_mut(*price_account.data.borrow_mut()).unwrap(),
266+
)
267+
.unwrap();
247268
update_clock_slot(&mut clock_account, 5);
248269

249270
assert!(process_instruction(
@@ -299,8 +320,13 @@ fn test_upd_price_with_validator() {
299320
}
300321

301322
populate_instruction(&mut instruction_data, 50, 20, 5);
302-
validator::aggregate_price(5, 105, price_account.key, *price_account.data.borrow_mut())
303-
.unwrap();
323+
validator::aggregate_price(
324+
5,
325+
105,
326+
price_account.key,
327+
checked_load_price_account_mut(*price_account.data.borrow_mut()).unwrap(),
328+
)
329+
.unwrap();
304330
update_clock_slot(&mut clock_account, 6);
305331

306332
// Publishing a wide CI results in a status of unknown.
@@ -337,8 +363,13 @@ fn test_upd_price_with_validator() {
337363
// Crank one more time and aggregate should be unknown
338364
populate_instruction(&mut instruction_data, 50, 20, 6);
339365

340-
validator::aggregate_price(6, 106, price_account.key, *price_account.data.borrow_mut())
341-
.unwrap();
366+
validator::aggregate_price(
367+
6,
368+
106,
369+
price_account.key,
370+
checked_load_price_account_mut(*price_account.data.borrow_mut()).unwrap(),
371+
)
372+
.unwrap();
342373
update_clock_slot(&mut clock_account, 7);
343374

344375

@@ -367,8 +398,13 @@ fn test_upd_price_with_validator() {
367398

368399
// Negative prices are accepted
369400
populate_instruction(&mut instruction_data, -100, 1, 7);
370-
validator::aggregate_price(7, 107, price_account.key, *price_account.data.borrow_mut())
371-
.unwrap();
401+
validator::aggregate_price(
402+
7,
403+
107,
404+
price_account.key,
405+
checked_load_price_account_mut(*price_account.data.borrow_mut()).unwrap(),
406+
)
407+
.unwrap();
372408
update_clock_slot(&mut clock_account, 8);
373409

374410

@@ -397,8 +433,13 @@ fn test_upd_price_with_validator() {
397433

398434
// Crank again for aggregate
399435
populate_instruction(&mut instruction_data, -100, 1, 8);
400-
validator::aggregate_price(8, 108, price_account.key, *price_account.data.borrow_mut())
401-
.unwrap();
436+
validator::aggregate_price(
437+
8,
438+
108,
439+
price_account.key,
440+
checked_load_price_account_mut(*price_account.data.borrow_mut()).unwrap(),
441+
)
442+
.unwrap();
402443
update_clock_slot(&mut clock_account, 9);
403444

404445

0 commit comments

Comments
 (0)