1#![allow(missing_docs)]
2use crate::{
3 self as solend_program,
4 error::LendingError,
5 math::{Decimal, TryDiv, TryMul},
6};
7use pyth_sdk_solana;
8use solana_program::{
9 account_info::AccountInfo, msg, program_error::ProgramError, sysvar::clock::Clock,
10};
11use std::{convert::TryInto, result::Result};
12
13pub fn get_pyth_price(
14 pyth_price_info: &AccountInfo,
15 clock: &Clock,
16) -> Result<Decimal, ProgramError> {
17 const PYTH_CONFIDENCE_RATIO: u64 = 10;
18 const STALE_AFTER_SLOTS_ELAPSED: u64 = 240; if *pyth_price_info.key == solend_program::NULL_PUBKEY {
21 return Err(LendingError::NullOracleConfig.into());
22 }
23
24 let data = &pyth_price_info.try_borrow_data()?;
25 let price_account = pyth_sdk_solana::state::load_price_account(data).map_err(|e| {
26 msg!("Couldn't load price feed from account info: {:?}", e);
27 LendingError::InvalidOracleConfig
28 })?;
29 let pyth_price = price_account
30 .get_price_no_older_than(clock, STALE_AFTER_SLOTS_ELAPSED)
31 .ok_or_else(|| {
32 msg!("Pyth oracle price is too stale!");
33 LendingError::InvalidOracleConfig
34 })?;
35
36 let price: u64 = pyth_price.price.try_into().map_err(|_| {
37 msg!("Oracle price cannot be negative");
38 LendingError::InvalidOracleConfig
39 })?;
40
41 if pyth_price.conf.saturating_mul(PYTH_CONFIDENCE_RATIO) > price {
45 msg!(
46 "Oracle price confidence is too wide. price: {}, conf: {}",
47 price,
48 pyth_price.conf,
49 );
50 return Err(LendingError::InvalidOracleConfig.into());
51 }
52
53 let market_price = if pyth_price.expo >= 0 {
54 let exponent = pyth_price
55 .expo
56 .try_into()
57 .map_err(|_| LendingError::MathOverflow)?;
58 let zeros = 10u64
59 .checked_pow(exponent)
60 .ok_or(LendingError::MathOverflow)?;
61 Decimal::from(price).try_mul(zeros)?
62 } else {
63 let exponent = pyth_price
64 .expo
65 .checked_abs()
66 .ok_or(LendingError::MathOverflow)?
67 .try_into()
68 .map_err(|_| LendingError::MathOverflow)?;
69 let decimals = 10u64
70 .checked_pow(exponent)
71 .ok_or(LendingError::MathOverflow)?;
72 Decimal::from(price).try_div(decimals)?
73 };
74
75 Ok(market_price)
76}
77
78#[cfg(test)]
79mod test {
80 use super::*;
81 use bytemuck::bytes_of_mut;
82 use proptest::prelude::*;
83 use pyth_sdk_solana::state::{
84 AccountType, CorpAction, PriceAccount, PriceInfo, PriceStatus, PriceType, MAGIC, VERSION_2,
85 };
86 use solana_program::pubkey::Pubkey;
87
88 #[derive(Clone, Debug)]
89 struct PythPriceTestCase {
90 price_account: PriceAccount,
91 clock: Clock,
92 expected_result: Result<Decimal, ProgramError>,
93 }
94
95 fn pyth_price_cases() -> impl Strategy<Value = PythPriceTestCase> {
96 prop_oneof![
97 Just(PythPriceTestCase {
99 price_account: PriceAccount {
100 magic: MAGIC + 1,
101 ver: VERSION_2,
102 atype: AccountType::Price as u32,
103 ptype: PriceType::Price,
104 expo: 10,
105 agg: PriceInfo {
106 price: 10,
107 conf: 1,
108 status: PriceStatus::Trading,
109 corp_act: CorpAction::NoCorpAct,
110 pub_slot: 0
111 },
112 ..PriceAccount::default()
113 },
114 clock: Clock {
115 slot: 4,
116 ..Clock::default()
117 },
118 expected_result: Err(LendingError::InvalidOracleConfig.into()),
120 }),
121 Just(PythPriceTestCase {
123 price_account: PriceAccount {
124 magic: MAGIC,
125 ver: VERSION_2 - 1,
126 atype: AccountType::Price as u32,
127 ptype: PriceType::Price,
128 expo: 10,
129 agg: PriceInfo {
130 price: 10,
131 conf: 1,
132 status: PriceStatus::Trading,
133 corp_act: CorpAction::NoCorpAct,
134 pub_slot: 0
135 },
136 ..PriceAccount::default()
137 },
138 clock: Clock {
139 slot: 4,
140 ..Clock::default()
141 },
142 expected_result: Err(LendingError::InvalidOracleConfig.into()),
143 }),
144 Just(PythPriceTestCase {
146 price_account: PriceAccount {
147 magic: MAGIC,
148 ver: VERSION_2,
149 atype: AccountType::Product as u32,
150 ptype: PriceType::Price,
151 expo: 10,
152 agg: PriceInfo {
153 price: 10,
154 conf: 1,
155 status: PriceStatus::Trading,
156 corp_act: CorpAction::NoCorpAct,
157 pub_slot: 0
158 },
159 ..PriceAccount::default()
160 },
161 clock: Clock {
162 slot: 4,
163 ..Clock::default()
164 },
165 expected_result: Err(LendingError::InvalidOracleConfig.into()),
166 }),
167 Just(PythPriceTestCase {
170 price_account: PriceAccount {
171 magic: MAGIC,
172 ver: VERSION_2,
173 atype: AccountType::Price as u32,
174 ptype: PriceType::Price,
175 expo: 1,
176 timestamp: 0,
177 agg: PriceInfo {
178 price: 200,
179 conf: 1,
180 status: PriceStatus::Trading,
181 corp_act: CorpAction::NoCorpAct,
182 pub_slot: 0
183 },
184 ..PriceAccount::default()
185 },
186 clock: Clock {
187 slot: 240,
188 ..Clock::default()
189 },
190 expected_result: Ok(Decimal::from(2000_u64))
191 }),
192 Just(PythPriceTestCase {
194 price_account: PriceAccount {
195 magic: MAGIC,
196 ver: VERSION_2,
197 atype: AccountType::Price as u32,
198 ptype: PriceType::Price,
199 expo: 1,
200 timestamp: 20,
201 agg: PriceInfo {
202 price: 200,
203 conf: 1,
204 status: PriceStatus::Unknown,
205 corp_act: CorpAction::NoCorpAct,
206 pub_slot: 1
207 },
208 prev_price: 190,
209 prev_conf: 10,
210 prev_slot: 0,
211 ..PriceAccount::default()
212 },
213 clock: Clock {
214 slot: 240,
215 ..Clock::default()
216 },
217 expected_result: Ok(Decimal::from(1900_u64))
218 }),
219 Just(PythPriceTestCase {
221 price_account: PriceAccount {
222 magic: MAGIC,
223 ver: VERSION_2,
224 atype: AccountType::Price as u32,
225 ptype: PriceType::Price,
226 expo: 1,
227 timestamp: 0,
228 agg: PriceInfo {
229 price: 200,
230 conf: 1,
231 status: PriceStatus::Trading,
232 corp_act: CorpAction::NoCorpAct,
233 pub_slot: 1
234 },
235 prev_slot: 0, ..PriceAccount::default()
237 },
238 clock: Clock {
239 slot: 242,
240 ..Clock::default()
241 },
242 expected_result: Err(LendingError::InvalidOracleConfig.into())
243 }),
244 Just(PythPriceTestCase {
246 price_account: PriceAccount {
247 magic: MAGIC,
248 ver: VERSION_2,
249 atype: AccountType::Price as u32,
250 ptype: PriceType::Price,
251 expo: 1,
252 timestamp: 1,
253 agg: PriceInfo {
254 price: 200,
255 conf: 1,
256 status: PriceStatus::Unknown,
257 corp_act: CorpAction::NoCorpAct,
258 pub_slot: 1
259 },
260 prev_price: 190,
261 prev_conf: 10,
262 prev_slot: 0,
263 ..PriceAccount::default()
264 },
265 clock: Clock {
266 slot: 241,
267 ..Clock::default()
268 },
269 expected_result: Err(LendingError::InvalidOracleConfig.into())
270 }),
271 Just(PythPriceTestCase {
273 price_account: PriceAccount {
274 magic: MAGIC,
275 ver: VERSION_2,
276 atype: AccountType::Price as u32,
277 ptype: PriceType::Price,
278 expo: 1,
279 timestamp: 1,
280 agg: PriceInfo {
281 price: -200,
282 conf: 1,
283 status: PriceStatus::Trading,
284 corp_act: CorpAction::NoCorpAct,
285 pub_slot: 0
286 },
287 ..PriceAccount::default()
288 },
289 clock: Clock {
290 slot: 240,
291 ..Clock::default()
292 },
293 expected_result: Err(LendingError::InvalidOracleConfig.into())
294 }),
295 Just(PythPriceTestCase {
297 price_account: PriceAccount {
298 magic: MAGIC,
299 ver: VERSION_2,
300 atype: AccountType::Price as u32,
301 ptype: PriceType::Price,
302 expo: 1,
303 timestamp: 1,
304 agg: PriceInfo {
305 price: 200,
306 conf: 40,
307 status: PriceStatus::Trading,
308 corp_act: CorpAction::NoCorpAct,
309 pub_slot: 0
310 },
311 ..PriceAccount::default()
312 },
313 clock: Clock {
314 slot: 240,
315 ..Clock::default()
316 },
317 expected_result: Err(LendingError::InvalidOracleConfig.into())
318 }),
319 ]
320 }
321
322 proptest! {
323 #[test]
324 fn test_pyth_price(mut test_case in pyth_price_cases()) {
325 let mut lamports = 20;
327 let pubkey = Pubkey::new_unique();
328 let account_info = AccountInfo::new(
329 &pubkey,
330 false,
331 false,
332 &mut lamports,
333 bytes_of_mut(&mut test_case.price_account),
334 &pubkey,
335 false,
336 0,
337 );
338
339 let result = get_pyth_price(&account_info, &test_case.clock);
340 assert_eq!(
341 result,
342 test_case.expected_result,
343 "actual: {:#?} expected: {:#?}",
344 result,
345 test_case.expected_result
346 );
347 }
348 }
349}