Skip to content

Commit d430b7b

Browse files
authored
feat(target_chains/starknet): parce price feeds (#1622)
* feat(target_chains/starknet): parce price feeds * refactor(target_chains/starknet): rename fn and add comment
1 parent f8ed6dd commit d430b7b

File tree

7 files changed

+448
-62
lines changed

7 files changed

+448
-62
lines changed

target_chains/starknet/contracts/src/pyth.cairo

Lines changed: 159 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ pub use pyth::{
1111
};
1212
pub use errors::{
1313
GetPriceUnsafeError, GovernanceActionError, UpdatePriceFeedsError, GetPriceNoOlderThanError,
14-
UpdatePriceFeedsIfNecessaryError,
14+
UpdatePriceFeedsIfNecessaryError, ParsePriceFeedsError,
1515
};
1616
pub use interface::{
17-
IPyth, IPythDispatcher, IPythDispatcherTrait, DataSource, Price, PriceFeedPublishTime
17+
IPyth, IPythDispatcher, IPythDispatcherTrait, DataSource, Price, PriceFeedPublishTime, PriceFeed
1818
};
1919

2020
#[starknet::contract]
@@ -36,12 +36,13 @@ mod pyth {
3636
use super::{
3737
DataSource, UpdatePriceFeedsError, GovernanceActionError, Price, GetPriceUnsafeError,
3838
IPythDispatcher, IPythDispatcherTrait, PriceFeedPublishTime, GetPriceNoOlderThanError,
39-
UpdatePriceFeedsIfNecessaryError,
39+
UpdatePriceFeedsIfNecessaryError, PriceFeed, ParsePriceFeedsError,
4040
};
4141
use super::governance;
4242
use super::governance::GovernancePayload;
4343
use openzeppelin::token::erc20::interface::{IERC20CamelDispatcherTrait, IERC20CamelDispatcher};
4444
use pyth::util::ResultMapErrInto;
45+
use core::nullable::{NullableTrait, match_nullable, FromNullableResult};
4546

4647
#[event]
4748
#[derive(Drop, PartialEq, starknet::Event)]
@@ -204,48 +205,7 @@ mod pyth {
204205
}
205206

206207
fn update_price_feeds(ref self: ContractState, data: ByteArray) {
207-
let mut reader = ReaderImpl::new(data);
208-
read_and_verify_header(ref reader);
209-
let wormhole_proof_size = reader.read_u16();
210-
let wormhole_proof = reader.read_byte_array(wormhole_proof_size.into());
211-
212-
let wormhole = IWormholeDispatcher { contract_address: self.wormhole_address.read() };
213-
let vm = wormhole.parse_and_verify_vm(wormhole_proof);
214-
215-
let source = DataSource {
216-
emitter_chain_id: vm.emitter_chain_id, emitter_address: vm.emitter_address
217-
};
218-
if !self.is_valid_data_source.read(source) {
219-
panic_with_felt252(UpdatePriceFeedsError::InvalidUpdateDataSource.into());
220-
}
221-
222-
let root_digest = parse_wormhole_proof(vm.payload);
223-
224-
let num_updates = reader.read_u8();
225-
let total_fee = self.get_total_fee(num_updates);
226-
let fee_contract = IERC20CamelDispatcher {
227-
contract_address: self.fee_contract_address.read()
228-
};
229-
let execution_info = get_execution_info().unbox();
230-
let caller = execution_info.caller_address;
231-
let contract = execution_info.contract_address;
232-
if fee_contract.allowance(caller, contract) < total_fee {
233-
panic_with_felt252(UpdatePriceFeedsError::InsufficientFeeAllowance.into());
234-
}
235-
if !fee_contract.transferFrom(caller, contract, total_fee) {
236-
panic_with_felt252(UpdatePriceFeedsError::InsufficientFeeAllowance.into());
237-
}
238-
239-
let mut i = 0;
240-
while i < num_updates {
241-
let message = read_and_verify_message(ref reader, root_digest);
242-
self.update_latest_price_if_necessary(message);
243-
i += 1;
244-
};
245-
246-
if reader.len() != 0 {
247-
panic_with_felt252(UpdatePriceFeedsError::InvalidUpdateData.into());
248-
}
208+
self.update_price_feeds_internal(data, array![], 0, 0, false);
249209
}
250210

251211
fn get_update_fee(self: @ContractState, data: ByteArray) -> u256 {
@@ -279,6 +239,32 @@ mod pyth {
279239
}
280240
}
281241

242+
fn parse_price_feed_updates(
243+
ref self: ContractState,
244+
data: ByteArray,
245+
price_ids: Array<u256>,
246+
min_publish_time: u64,
247+
max_publish_time: u64
248+
) -> Array<PriceFeed> {
249+
self
250+
.update_price_feeds_internal(
251+
data, price_ids, min_publish_time, max_publish_time, false
252+
)
253+
}
254+
255+
fn parse_unique_price_feed_updates(
256+
ref self: ContractState,
257+
data: ByteArray,
258+
price_ids: Array<u256>,
259+
publish_time: u64,
260+
max_staleness: u64,
261+
) -> Array<PriceFeed> {
262+
self
263+
.update_price_feeds_internal(
264+
data, price_ids, publish_time, publish_time + max_staleness, true
265+
)
266+
}
267+
282268
fn execute_governance_instruction(ref self: ContractState, data: ByteArray) {
283269
let wormhole = IWormholeDispatcher { contract_address: self.wormhole_address.read() };
284270
let vm = wormhole.parse_and_verify_vm(data.clone());
@@ -362,24 +348,24 @@ mod pyth {
362348
old_data_sources
363349
}
364350

365-
fn update_latest_price_if_necessary(ref self: ContractState, message: PriceFeedMessage) {
366-
let latest_publish_time = self.latest_price_info.read(message.price_id).publish_time;
367-
if message.publish_time > latest_publish_time {
351+
fn update_latest_price_if_necessary(ref self: ContractState, message: @PriceFeedMessage) {
352+
let latest_publish_time = self.latest_price_info.read(*message.price_id).publish_time;
353+
if *message.publish_time > latest_publish_time {
368354
let info = PriceInfo {
369-
price: message.price,
370-
conf: message.conf,
371-
expo: message.expo,
372-
publish_time: message.publish_time,
373-
ema_price: message.ema_price,
374-
ema_conf: message.ema_conf,
355+
price: *message.price,
356+
conf: *message.conf,
357+
expo: *message.expo,
358+
publish_time: *message.publish_time,
359+
ema_price: *message.ema_price,
360+
ema_conf: *message.ema_conf,
375361
};
376-
self.latest_price_info.write(message.price_id, info);
362+
self.latest_price_info.write(*message.price_id, info);
377363

378364
let event = PriceFeedUpdated {
379-
price_id: message.price_id,
380-
publish_time: message.publish_time,
381-
price: message.price,
382-
conf: message.conf,
365+
price_id: *message.price_id,
366+
publish_time: *message.publish_time,
367+
price: *message.price,
368+
conf: *message.conf,
383369
};
384370
self.emit(event);
385371
}
@@ -490,6 +476,105 @@ mod pyth {
490476
let event = ContractUpgraded { new_class_hash: new_implementation };
491477
self.emit(event);
492478
}
479+
480+
// Applies all price feed updates encoded in `data` and extracts requested information
481+
// about the new updates. `price_ids` specifies price feeds of interest. The output will
482+
// contain as many items as `price_ids`, with price feeds returned in the same order as
483+
// specified in `price_ids`.
484+
//
485+
// If `unique == false`, for each price feed, the first encountered update
486+
// in the specified time interval (both timestamps inclusive) will be returned.
487+
// If `unique == true`, the globally unique first update will be returned, as verified by
488+
// the `prev_publish_time` value of the update. Panics if a matching update was not found
489+
// for any of the specified feeds.
490+
fn update_price_feeds_internal(
491+
ref self: ContractState,
492+
data: ByteArray,
493+
price_ids: Array<u256>,
494+
min_publish_time: u64,
495+
max_publish_time: u64,
496+
unique: bool,
497+
) -> Array<PriceFeed> {
498+
let mut output: Felt252Dict<Nullable<PriceFeed>> = Default::default();
499+
let mut reader = ReaderImpl::new(data);
500+
read_and_verify_header(ref reader);
501+
let wormhole_proof_size = reader.read_u16();
502+
let wormhole_proof = reader.read_byte_array(wormhole_proof_size.into());
503+
504+
let wormhole = IWormholeDispatcher { contract_address: self.wormhole_address.read() };
505+
let vm = wormhole.parse_and_verify_vm(wormhole_proof);
506+
507+
let source = DataSource {
508+
emitter_chain_id: vm.emitter_chain_id, emitter_address: vm.emitter_address
509+
};
510+
if !self.is_valid_data_source.read(source) {
511+
panic_with_felt252(UpdatePriceFeedsError::InvalidUpdateDataSource.into());
512+
}
513+
514+
let root_digest = parse_wormhole_proof(vm.payload);
515+
516+
let num_updates = reader.read_u8();
517+
let total_fee = self.get_total_fee(num_updates);
518+
let fee_contract = IERC20CamelDispatcher {
519+
contract_address: self.fee_contract_address.read()
520+
};
521+
let execution_info = get_execution_info().unbox();
522+
let caller = execution_info.caller_address;
523+
let contract = execution_info.contract_address;
524+
if fee_contract.allowance(caller, contract) < total_fee {
525+
panic_with_felt252(UpdatePriceFeedsError::InsufficientFeeAllowance.into());
526+
}
527+
if !fee_contract.transferFrom(caller, contract, total_fee) {
528+
panic_with_felt252(UpdatePriceFeedsError::InsufficientFeeAllowance.into());
529+
}
530+
531+
let mut i = 0;
532+
let price_ids2 = @price_ids;
533+
while i < num_updates {
534+
let message = read_and_verify_message(ref reader, root_digest);
535+
self.update_latest_price_if_necessary(@message);
536+
537+
let output_index = find_index_of_price_id(price_ids2, message.price_id);
538+
match output_index {
539+
Option::Some(output_index) => {
540+
if output.get(output_index.into()).is_null() {
541+
let should_output = message.publish_time >= min_publish_time
542+
&& message.publish_time <= max_publish_time
543+
&& (!unique || min_publish_time > message.prev_publish_time);
544+
if should_output {
545+
output
546+
.insert(
547+
output_index.into(), NullableTrait::new(message.into())
548+
);
549+
}
550+
}
551+
},
552+
Option::None => {}
553+
}
554+
555+
i += 1;
556+
};
557+
558+
if reader.len() != 0 {
559+
panic_with_felt252(UpdatePriceFeedsError::InvalidUpdateData.into());
560+
}
561+
562+
let mut output_array = array![];
563+
let mut i = 0;
564+
while i < price_ids.len() {
565+
let value = output.get(i.into());
566+
match match_nullable(value) {
567+
FromNullableResult::Null => {
568+
panic_with_felt252(
569+
ParsePriceFeedsError::PriceFeedNotFoundWithinRange.into()
570+
)
571+
},
572+
FromNullableResult::NotNull(value) => { output_array.append(value.unbox()); }
573+
}
574+
i += 1;
575+
};
576+
output_array
577+
}
493578
}
494579

495580
fn apply_decimal_expo(value: u64, expo: u64) -> u256 {
@@ -511,4 +596,19 @@ mod pyth {
511596
};
512597
actual_age <= age
513598
}
599+
600+
fn find_index_of_price_id(ids: @Array<u256>, value: u256) -> Option<usize> {
601+
let mut i = 0;
602+
while i < ids.len() {
603+
if ids.at(i) == @value {
604+
break;
605+
}
606+
i += 1;
607+
};
608+
if i == ids.len() {
609+
Option::None
610+
} else {
611+
Option::Some(i)
612+
}
613+
}
514614
}

target_chains/starknet/contracts/src/pyth/errors.cairo

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,18 @@ impl UpdatePriceFeedsIfNecessaryErrorIntoFelt252 of Into<
9898
}
9999
}
100100
}
101+
102+
#[derive(Copy, Drop, Debug, Serde, PartialEq)]
103+
pub enum ParsePriceFeedsError {
104+
Update: UpdatePriceFeedsError,
105+
PriceFeedNotFoundWithinRange,
106+
}
107+
108+
impl ParsePriceFeedsErrorIntoFelt252 of Into<ParsePriceFeedsError, felt252> {
109+
fn into(self: ParsePriceFeedsError) -> felt252 {
110+
match self {
111+
ParsePriceFeedsError::Update(err) => err.into(),
112+
ParsePriceFeedsError::PriceFeedNotFoundWithinRange => 'price feed not found',
113+
}
114+
}
115+
}

target_chains/starknet/contracts/src/pyth/interface.cairo

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,16 @@ pub trait IPyth<T> {
1515
fn update_price_feeds_if_necessary(
1616
ref self: T, update: ByteArray, required_publish_times: Array<PriceFeedPublishTime>
1717
);
18+
fn parse_price_feed_updates(
19+
ref self: T,
20+
data: ByteArray,
21+
price_ids: Array<u256>,
22+
min_publish_time: u64,
23+
max_publish_time: u64
24+
) -> Array<PriceFeed>;
25+
fn parse_unique_price_feed_updates(
26+
ref self: T, data: ByteArray, price_ids: Array<u256>, publish_time: u64, max_staleness: u64,
27+
) -> Array<PriceFeed>;
1828
fn get_update_fee(self: @T, data: ByteArray) -> u256;
1929
fn execute_governance_instruction(ref self: T, data: ByteArray);
2030
fn pyth_upgradable_magic(self: @T) -> u32;
@@ -26,7 +36,7 @@ pub struct DataSource {
2636
pub emitter_address: u256,
2737
}
2838

29-
#[derive(Drop, Clone, Serde)]
39+
#[derive(Drop, Copy, PartialEq, Serde)]
3040
pub struct Price {
3141
pub price: i64,
3242
pub conf: u64,
@@ -39,3 +49,14 @@ pub struct PriceFeedPublishTime {
3949
pub price_id: u256,
4050
pub publish_time: u64,
4151
}
52+
53+
// PriceFeed represents a current aggregate price from pyth publisher feeds.
54+
#[derive(Drop, Copy, PartialEq, Serde)]
55+
pub struct PriceFeed {
56+
// The price ID.
57+
pub id: u256,
58+
// Latest available price
59+
pub price: Price,
60+
// Latest available exponentially-weighted moving average price
61+
pub ema_price: Price,
62+
}

target_chains/starknet/contracts/src/pyth/price_update.cairo

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use pyth::reader::{Reader, ReaderImpl};
2-
use pyth::pyth::UpdatePriceFeedsError;
2+
use pyth::pyth::{UpdatePriceFeedsError, PriceFeed, Price};
33
use core::panic_with_felt252;
44
use pyth::byte_array::ByteArray;
55
use pyth::merkle_tree::read_and_verify_proof;
@@ -136,3 +136,23 @@ pub fn read_and_verify_message(ref reader: Reader, root_digest: u256) -> PriceFe
136136
price_id, price, conf, expo, publish_time, prev_publish_time, ema_price, ema_conf,
137137
}
138138
}
139+
140+
impl PriceFeedMessageIntoPriceFeed of Into<PriceFeedMessage, PriceFeed> {
141+
fn into(self: PriceFeedMessage) -> PriceFeed {
142+
PriceFeed {
143+
id: self.price_id,
144+
price: Price {
145+
price: self.price,
146+
conf: self.conf,
147+
expo: self.expo,
148+
publish_time: self.publish_time,
149+
},
150+
ema_price: Price {
151+
price: self.ema_price,
152+
conf: self.ema_conf,
153+
expo: self.expo,
154+
publish_time: self.publish_time,
155+
},
156+
}
157+
}
158+
}

0 commit comments

Comments
 (0)