Skip to content
This repository was archived by the owner on Jan 13, 2025. It is now read-only.

Commit 328b1e4

Browse files
authored
Support permissionless price feed updates (#33)
This feature saves operational costs for the program authority. Rather than the authority having to post frequent on chain price oracle updates, the user can post the oracle price update at the time of their transaction. The authority hosts a backend service that the user can query to obtain a fresh price update payload signed by the authority. The user then includes this payload in their transaction.
1 parent 72b42a9 commit 328b1e4

File tree

13 files changed

+358
-28
lines changed

13 files changed

+358
-28
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@
1313
"@types/bn.js": "^5.1.1",
1414
"@types/chai": "^4.3.4",
1515
"@types/mocha": "^10.0.1",
16+
"bn.js": "^5.2.1",
1617
"chai": "^4.3.7",
1718
"mocha": "^10.2.0",
1819
"prettier": "^2.8.1",
1920
"ts-mocha": "^10.0.0",
21+
"tweetnacl": "^1.0.3",
2022
"typescript": "^4.9.4"
2123
}
2224
}

programs/perpetuals/src/error.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,12 @@ pub enum PerpetualsError {
5656
InstructionNotAllowed,
5757
#[msg("Token utilization limit exceeded")]
5858
MaxUtilization,
59+
#[msg("Permissionless oracle update must be preceded by Ed25519 signature verification instruction")]
60+
PermissionlessOracleMissingSignature,
61+
#[msg("Ed25519 signature verification data does not match expected format")]
62+
PermissionlessOracleMalformedEd25519Data,
63+
#[msg("Ed25519 signature was not signed by the oracle authority")]
64+
PermissionlessOracleSignerMismatch,
65+
#[msg("Signed message does not match instruction params")]
66+
PermissionlessOracleMessageMismatch,
5967
}

programs/perpetuals/src/instructions.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ pub mod liquidate;
3434
pub mod open_position;
3535
pub mod remove_collateral;
3636
pub mod remove_liquidity;
37+
pub mod set_custom_oracle_price_permissionless;
3738
pub mod swap;
3839
pub mod update_pool_aum;
3940

@@ -45,7 +46,7 @@ pub use {
4546
get_liquidation_state::*, get_lp_token_price::*, get_oracle_price::*, get_pnl::*,
4647
get_remove_liquidity_amount_and_fee::*, get_swap_amount_and_fees::*, init::*, liquidate::*,
4748
open_position::*, remove_collateral::*, remove_custody::*, remove_liquidity::*, remove_pool::*,
48-
set_admin_signers::*, set_custody_config::*, set_custom_oracle_price::*, set_permissions::*,
49-
set_test_time::*, swap::*, update_pool_aum::*, upgrade_custody::*, withdraw_fees::*,
50-
withdraw_sol_fees::*,
49+
set_admin_signers::*, set_custody_config::*, set_custom_oracle_price::*,
50+
set_custom_oracle_price_permissionless::*, set_permissions::*, set_test_time::*, swap::*,
51+
update_pool_aum::*, upgrade_custody::*, withdraw_fees::*, withdraw_sol_fees::*,
5152
};

programs/perpetuals/src/instructions/set_custom_oracle_price.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,12 @@ pub fn set_custom_oracle_price<'info>(
8989
}
9090

9191
// update oracle data
92-
let oracle_account = ctx.accounts.oracle_account.as_mut();
93-
oracle_account.price = params.price;
94-
oracle_account.expo = params.expo;
95-
oracle_account.conf = params.conf;
96-
oracle_account.ema = params.ema;
97-
oracle_account.publish_time = params.publish_time;
98-
92+
ctx.accounts.oracle_account.set(
93+
params.price,
94+
params.expo,
95+
params.conf,
96+
params.ema,
97+
params.publish_time,
98+
);
9999
Ok(0)
100100
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
//! SetCustomOraclePricePermissionless instruction handler
2+
3+
use {
4+
crate::{
5+
error::PerpetualsError,
6+
state::{custody::Custody, oracle::CustomOracle, perpetuals::Perpetuals, pool::Pool},
7+
},
8+
anchor_lang::prelude::*,
9+
solana_program::{ed25519_program, instruction::Instruction, sysvar},
10+
};
11+
12+
#[derive(Accounts)]
13+
#[instruction(params: SetCustomOraclePricePermissionlessParams)]
14+
pub struct SetCustomOraclePricePermissionless<'info> {
15+
#[account(
16+
seeds = [b"perpetuals"],
17+
bump = perpetuals.perpetuals_bump
18+
)]
19+
pub perpetuals: Box<Account<'info, Perpetuals>>,
20+
21+
#[account(
22+
seeds = [b"pool",
23+
pool.name.as_bytes()],
24+
bump = pool.bump
25+
)]
26+
pub pool: Box<Account<'info, Pool>>,
27+
28+
#[account(
29+
seeds = [b"custody",
30+
pool.key().as_ref(),
31+
custody.mint.as_ref()],
32+
constraint = custody.key() == params.custody_account,
33+
bump = custody.bump
34+
)]
35+
pub custody: Box<Account<'info, Custody>>,
36+
37+
#[account(
38+
// Custom oracle must first be initialized by authority before permissionless updates.
39+
mut,
40+
seeds = [b"oracle_account",
41+
pool.key().as_ref(),
42+
custody.mint.as_ref()],
43+
bump
44+
)]
45+
pub oracle_account: Box<Account<'info, CustomOracle>>,
46+
47+
/// CHECK: Needed for ed25519 signature verification, to inspect all instructions in this transaction.
48+
#[account(address = sysvar::instructions::ID)]
49+
pub ix_sysvar: AccountInfo<'info>,
50+
}
51+
52+
#[derive(AnchorSerialize, AnchorDeserialize, Copy, Clone, PartialEq)]
53+
pub struct SetCustomOraclePricePermissionlessParams {
54+
pub custody_account: Pubkey,
55+
pub price: u64,
56+
pub expo: i32,
57+
pub conf: u64,
58+
pub ema: u64,
59+
pub publish_time: i64,
60+
}
61+
62+
pub fn set_custom_oracle_price_permissionless(
63+
ctx: Context<SetCustomOraclePricePermissionless>,
64+
params: &SetCustomOraclePricePermissionlessParams,
65+
) -> Result<()> {
66+
if params.publish_time <= ctx.accounts.oracle_account.publish_time {
67+
msg!("Custom oracle price did not update because the requested publish time is stale.");
68+
return Ok(());
69+
}
70+
// Get what should be the Ed25519Program signature verification instruction.
71+
let signature_ix: Instruction =
72+
sysvar::instructions::load_instruction_at_checked(0, &ctx.accounts.ix_sysvar)?;
73+
74+
validate_ed25519_signature_instruction(
75+
&signature_ix,
76+
&ctx.accounts.custody.oracle.oracle_authority,
77+
params,
78+
)?;
79+
80+
ctx.accounts.oracle_account.set(
81+
params.price,
82+
params.expo,
83+
params.conf,
84+
params.ema,
85+
params.publish_time,
86+
);
87+
Ok(())
88+
}
89+
90+
fn validate_ed25519_signature_instruction(
91+
signature_ix: &Instruction,
92+
expected_pubkey: &Pubkey,
93+
expected_params: &SetCustomOraclePricePermissionlessParams,
94+
) -> Result<()> {
95+
require_eq!(
96+
signature_ix.program_id,
97+
ed25519_program::ID,
98+
PerpetualsError::PermissionlessOracleMissingSignature
99+
);
100+
require!(
101+
signature_ix.accounts.is_empty() /* no accounts touched */
102+
&& signature_ix.data[0] == 0x01 /* only one ed25519 signature */
103+
&& signature_ix.data.len() == 180, /* data len matches exactly the expected */
104+
PerpetualsError::PermissionlessOracleMalformedEd25519Data
105+
);
106+
107+
// Manually access offsets for signer pubkey and message data according to:
108+
// https://docs.solana.com/developing/runtime-facilities/programs#ed25519-program
109+
let signer_pubkey = &signature_ix.data[16..16 + 32];
110+
let mut verified_message = &signature_ix.data[112..];
111+
112+
let deserialized_instruction_params =
113+
SetCustomOraclePricePermissionlessParams::deserialize(&mut verified_message)?;
114+
115+
require!(
116+
signer_pubkey == expected_pubkey.to_bytes(),
117+
PerpetualsError::PermissionlessOracleSignerMismatch
118+
);
119+
require!(
120+
deserialized_instruction_params == *expected_params,
121+
PerpetualsError::PermissionlessOracleMessageMismatch
122+
);
123+
Ok(())
124+
}

programs/perpetuals/src/lib.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,4 +238,13 @@ pub mod perpetuals {
238238
) -> Result<u64> {
239239
instructions::get_lp_token_price(ctx, &params)
240240
}
241+
242+
// This instruction must be part of a larger transaction where the **first** instruction
243+
// is an ed25519 verification of the serialized oracle price update params.
244+
pub fn set_custom_oracle_price_permissionless(
245+
ctx: Context<SetCustomOraclePricePermissionless>,
246+
params: SetCustomOraclePricePermissionlessParams,
247+
) -> Result<()> {
248+
instructions::set_custom_oracle_price_permissionless(ctx, &params)
249+
}
241250
}

programs/perpetuals/src/state/oracle.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ pub struct OraclePrice {
3333
pub struct OracleParams {
3434
pub oracle_account: Pubkey,
3535
pub oracle_type: OracleType,
36+
// The oracle_authority pubkey is allowed to sign permissionless off-chain price updates.
37+
pub oracle_authority: Pubkey,
3638
pub max_price_error: u64,
3739
pub max_price_age_sec: u32,
3840
}
@@ -49,6 +51,14 @@ pub struct CustomOracle {
4951

5052
impl CustomOracle {
5153
pub const LEN: usize = 8 + std::mem::size_of::<CustomOracle>();
54+
55+
pub fn set(&mut self, price: u64, expo: i32, conf: u64, ema: u64, publish_time: i64) {
56+
self.price = price;
57+
self.expo = expo;
58+
self.conf = conf;
59+
self.ema = ema;
60+
self.publish_time = publish_time;
61+
}
5262
}
5363

5464
impl PartialOrd for OraclePrice {

programs/perpetuals/src/state/pool.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1144,6 +1144,7 @@ mod test {
11441144
let oracle = OracleParams {
11451145
oracle_account: Pubkey::default(),
11461146
oracle_type: OracleType::Custom,
1147+
oracle_authority: Pubkey::default(),
11471148
max_price_error: 100,
11481149
max_price_age_sec: 1,
11491150
};

0 commit comments

Comments
 (0)