Skip to content

Commit f20bb11

Browse files
authored
Introduce "group IDs" to token metadata. (#11)
1 parent ee83665 commit f20bb11

File tree

9 files changed

+302
-84
lines changed

9 files changed

+302
-84
lines changed

configs/mainnet/hubble.json

+248-65
Large diffs are not rendered by default.

programs/scope-types/src/lib.rs

+5-3
Original file line numberDiff line numberDiff line change
@@ -139,15 +139,17 @@ pub struct TokensMetadata {
139139
#[derive(Debug, PartialEq, Eq, Default)]
140140
pub struct TokenMetadata {
141141
pub name: [u8; 32],
142-
pub max_age_price_seconds: u64,
143-
pub _reserved: [u64; 16],
142+
pub max_age_price_slots: u64,
143+
pub group_ids_bitset: u64, // a bitset of group IDs in range [0, 64).
144+
pub _reserved: [u64; 15],
144145
}
145146

146147
#[derive(TryFromPrimitive, PartialEq, Eq, Clone, Copy, Debug)]
147148
#[repr(u64)]
148149
pub enum UpdateTokenMetadataMode {
149150
Name = 0,
150-
MaxPriceAgeSeconds = 1,
151+
MaxPriceAgeSlots = 1,
152+
GroupIds = 2,
151153
}
152154

153155
#[error_code]

programs/scope/src/handlers/handler_refresh_prices.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use solana_program::{
1010
};
1111

1212
use crate::{
13-
oracles::{get_price, OracleType},
13+
oracles::{get_non_zero_price, OracleType},
1414
utils::{price_impl::check_ref_price_difference, zero_copy_deserialize},
1515
OracleMappings, ScopeError,
1616
};
@@ -89,15 +89,15 @@ pub fn refresh_price_list<'info>(
8989
return err!(ScopeError::UnexpectedAccount);
9090
}
9191
let clock = Clock::get()?;
92-
let price_res = get_price(
92+
let price_res = get_non_zero_price(
9393
price_type,
9494
received_account,
9595
&mut accounts_iter,
9696
&clock,
9797
&oracle_twaps,
9898
oracle_mappings,
9999
&ctx.accounts.oracle_prices,
100-
token_nb.into(),
100+
token_idx,
101101
);
102102
let price = if fail_tx_on_error {
103103
price_res?

programs/scope/src/handlers/handler_update_token_metadata.rs

+28
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use crate::{utils::pdas::seeds, ScopeError};
77
pub enum UpdateTokenMetadataMode {
88
Name = 0,
99
MaxPriceAgeSlots = 1,
10+
GroupIds = 2,
1011
}
1112

1213
impl UpdateTokenMetadataMode {
@@ -18,6 +19,7 @@ impl UpdateTokenMetadataMode {
1819
match self {
1920
UpdateTokenMetadataMode::Name => 0,
2021
UpdateTokenMetadataMode::MaxPriceAgeSlots => 1,
22+
UpdateTokenMetadataMode::GroupIds => 2,
2123
}
2224
}
2325
}
@@ -70,7 +72,33 @@ pub fn process(
7072
let str_name = std::str::from_utf8(&token_metadata.name).unwrap();
7173
msg!("Setting token name for index {} to {}", index, str_name);
7274
}
75+
UpdateTokenMetadataMode::GroupIds => {
76+
let value = u64::from_le_bytes(value[..8].try_into().unwrap());
77+
msg!(
78+
"Setting token group IDs for index {} to: raw {} == binary {:#b} == positions {:?}",
79+
index,
80+
value,
81+
value,
82+
list_set_bit_positions(value),
83+
);
84+
token_metadata.group_ids_bitset = value;
85+
}
7386
}
7487

7588
Ok(())
7689
}
90+
91+
/// Lists the bit positions (where LSB == 0) of all the set bits (i.e. `1`s) in the given number's
92+
/// binary representation.
93+
/// NOTE: This is a non-critical helper used only for logging of the update operation; should *not*
94+
/// be needed by business logic. The implementation is a compressed version of a crate
95+
/// https://docs.rs/bit-iter/1.2.0/src/bit_iter/lib.rs.html.
96+
fn list_set_bit_positions(mut bits: u64) -> Vec<u8> {
97+
let mut positions = Vec::with_capacity(usize::try_from(bits.count_ones()).unwrap());
98+
while bits != 0 {
99+
let position = u8::try_from(bits.trailing_zeros()).unwrap();
100+
positions.push(position);
101+
bits &= bits.wrapping_sub(1);
102+
}
103+
positions
104+
}

programs/scope/src/oracles/jito_restaking.rs

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
use anchor_lang::prelude::*;
22
use decimal_wad::decimal::Decimal;
33

4-
use crate::utils::consts::FULL_BPS;
5-
use crate::utils::{math, zero_copy_deserialize};
6-
use crate::{DatedPrice, Price};
4+
use crate::{
5+
utils::{consts::FULL_BPS, math, zero_copy_deserialize},
6+
DatedPrice, Price,
7+
};
78

89
/// Jito restaking price oracle gives the amount of JitoSOL per VRT token on withdrawal
910
/// WARNING: Assumes both tokens have the same decimals (9)
@@ -46,10 +47,11 @@ pub fn validate_account(vault: &Option<AccountInfo>) -> Result<()> {
4647
}
4748

4849
pub mod jito_vault_core {
49-
use super::*;
5050
use anchor_lang::Discriminator;
5151
use bytemuck::{Pod, Zeroable};
5252

53+
use super::*;
54+
5355
#[derive(Clone, Copy, PartialEq, Eq, Pod, Zeroable)]
5456
#[repr(C)]
5557
pub struct DelegationState {

programs/scope/src/oracles/meteora_dlmm.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ where
6666
let q64x64_price = if a_to_b {
6767
U192::from(q64x64_price)
6868
} else {
69-
// Invert price
69+
// Invert price - safe, since `lb_clmm::get_x64_price_from_id` never returns 0.
7070
(U192::one() << 128) / q64x64_price
7171
};
7272

programs/scope/src/oracles/mod.rs

+9-2
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ impl OracleType {
151151
/// If needed the `extra_accounts` will be extracted from the provided iterator and checked
152152
/// with the data contained in the `base_account`
153153
#[allow(clippy::too_many_arguments)]
154-
pub fn get_price<'a, 'b>(
154+
pub fn get_non_zero_price<'a, 'b>(
155155
price_type: OracleType,
156156
base_account: &AccountInfo<'a>,
157157
extra_accounts: &mut impl Iterator<Item = &'b AccountInfo<'a>>,
@@ -164,7 +164,7 @@ pub fn get_price<'a, 'b>(
164164
where
165165
'a: 'b,
166166
{
167-
match price_type {
167+
let price = match price_type {
168168
OracleType::Pyth => pyth::get_price(base_account, clock),
169169
OracleType::PythPullBased => pyth_pull_based::get_price(base_account, clock),
170170
OracleType::PythPullBasedEMA => pyth_pull_based_ema::get_price(base_account, clock),
@@ -269,7 +269,14 @@ where
269269
OracleType::DeprecatedPlaceholder1 | OracleType::DeprecatedPlaceholder2 => {
270270
panic!("DeprecatedPlaceholder is not a valid oracle type")
271271
}
272+
}?;
273+
// The price providers above are performing their type-specific validations, but are still free
274+
// to return 0, which we can only tolerate in case of explicit fixed price:
275+
if price.price.value == 0 && price_type != OracleType::FixedPrice {
276+
msg!("Price is 0 (token {index}, type {price_type:?}): {price:?}",);
277+
return err!(ScopeError::PriceNotValid);
272278
}
279+
Ok(price)
273280
}
274281

275282
/// Validate the given account as being an appropriate price account for the

programs/scope/src/oracles/pyth.rs

-5
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,6 @@ pub fn validate_valid_price(
107107
});
108108
}
109109

110-
if price == 0 {
111-
msg!("Pyth price is 0");
112-
return Err(ScopeError::PriceNotValid);
113-
}
114-
115110
let conf: u128 = pyth_price.conf.into();
116111
check_confidence_interval(
117112
price.into(),

programs/scope/src/states.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,8 @@ pub struct TokenMetadatas {
150150
pub struct TokenMetadata {
151151
pub name: [u8; 32],
152152
pub max_age_price_slots: u64,
153-
pub _reserved: [u64; 16],
153+
pub group_ids_bitset: u64, // a bitset of group IDs in range [0, 64).
154+
pub _reserved: [u64; 15],
154155
}
155156

156157
static_assertions::const_assert_eq!(CONFIGURATION_SIZE, std::mem::size_of::<Configuration>());

0 commit comments

Comments
 (0)