Skip to content

Commit

Permalink
feat(target_chains/starknet): parce price feeds
Browse files Browse the repository at this point in the history
  • Loading branch information
Riateche committed May 28, 2024
1 parent 2df53c9 commit 4a95ac6
Show file tree
Hide file tree
Showing 7 changed files with 441 additions and 62 deletions.
211 changes: 152 additions & 59 deletions target_chains/starknet/contracts/src/pyth.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ pub use pyth::{
};
pub use errors::{
GetPriceUnsafeError, GovernanceActionError, UpdatePriceFeedsError, GetPriceNoOlderThanError,
UpdatePriceFeedsIfNecessaryError,
UpdatePriceFeedsIfNecessaryError, ParsePriceFeedsError,
};
pub use interface::{
IPyth, IPythDispatcher, IPythDispatcherTrait, DataSource, Price, PriceFeedPublishTime
IPyth, IPythDispatcher, IPythDispatcherTrait, DataSource, Price, PriceFeedPublishTime, PriceFeed
};

#[starknet::contract]
Expand All @@ -36,12 +36,13 @@ mod pyth {
use super::{
DataSource, UpdatePriceFeedsError, GovernanceActionError, Price, GetPriceUnsafeError,
IPythDispatcher, IPythDispatcherTrait, PriceFeedPublishTime, GetPriceNoOlderThanError,
UpdatePriceFeedsIfNecessaryError,
UpdatePriceFeedsIfNecessaryError, PriceFeed, ParsePriceFeedsError,
};
use super::governance;
use super::governance::GovernancePayload;
use openzeppelin::token::erc20::interface::{IERC20CamelDispatcherTrait, IERC20CamelDispatcher};
use pyth::util::ResultMapErrInto;
use core::nullable::{NullableTrait, match_nullable, FromNullableResult};

#[event]
#[derive(Drop, PartialEq, starknet::Event)]
Expand Down Expand Up @@ -204,48 +205,7 @@ mod pyth {
}

fn update_price_feeds(ref self: ContractState, data: ByteArray) {
let mut reader = ReaderImpl::new(data);
read_and_verify_header(ref reader);
let wormhole_proof_size = reader.read_u16();
let wormhole_proof = reader.read_byte_array(wormhole_proof_size.into());

let wormhole = IWormholeDispatcher { contract_address: self.wormhole_address.read() };
let vm = wormhole.parse_and_verify_vm(wormhole_proof);

let source = DataSource {
emitter_chain_id: vm.emitter_chain_id, emitter_address: vm.emitter_address
};
if !self.is_valid_data_source.read(source) {
panic_with_felt252(UpdatePriceFeedsError::InvalidUpdateDataSource.into());
}

let root_digest = parse_wormhole_proof(vm.payload);

let num_updates = reader.read_u8();
let total_fee = self.get_total_fee(num_updates);
let fee_contract = IERC20CamelDispatcher {
contract_address: self.fee_contract_address.read()
};
let execution_info = get_execution_info().unbox();
let caller = execution_info.caller_address;
let contract = execution_info.contract_address;
if fee_contract.allowance(caller, contract) < total_fee {
panic_with_felt252(UpdatePriceFeedsError::InsufficientFeeAllowance.into());
}
if !fee_contract.transferFrom(caller, contract, total_fee) {
panic_with_felt252(UpdatePriceFeedsError::InsufficientFeeAllowance.into());
}

let mut i = 0;
while i < num_updates {
let message = read_and_verify_message(ref reader, root_digest);
self.update_latest_price_if_necessary(message);
i += 1;
};

if reader.len() != 0 {
panic_with_felt252(UpdatePriceFeedsError::InvalidUpdateData.into());
}
self.parse_price_feed_updates_internal(data, array![], 0, 0, false);
}

fn get_update_fee(self: @ContractState, data: ByteArray) -> u256 {
Expand Down Expand Up @@ -279,6 +239,32 @@ mod pyth {
}
}

fn parse_price_feed_updates(
ref self: ContractState,
data: ByteArray,
price_ids: Array<u256>,
min_publish_time: u64,
max_publish_time: u64
) -> Array<PriceFeed> {
self
.parse_price_feed_updates_internal(
data, price_ids, min_publish_time, max_publish_time, false
)
}

fn parse_unique_price_feed_updates(
ref self: ContractState,
data: ByteArray,
price_ids: Array<u256>,
publish_time: u64,
max_staleness: u64,
) -> Array<PriceFeed> {
self
.parse_price_feed_updates_internal(
data, price_ids, publish_time, publish_time + max_staleness, true
)
}

fn execute_governance_instruction(ref self: ContractState, data: ByteArray) {
let wormhole = IWormholeDispatcher { contract_address: self.wormhole_address.read() };
let vm = wormhole.parse_and_verify_vm(data.clone());
Expand Down Expand Up @@ -362,24 +348,24 @@ mod pyth {
old_data_sources
}

fn update_latest_price_if_necessary(ref self: ContractState, message: PriceFeedMessage) {
let latest_publish_time = self.latest_price_info.read(message.price_id).publish_time;
if message.publish_time > latest_publish_time {
fn update_latest_price_if_necessary(ref self: ContractState, message: @PriceFeedMessage) {
let latest_publish_time = self.latest_price_info.read(*message.price_id).publish_time;
if *message.publish_time > latest_publish_time {
let info = PriceInfo {
price: message.price,
conf: message.conf,
expo: message.expo,
publish_time: message.publish_time,
ema_price: message.ema_price,
ema_conf: message.ema_conf,
price: *message.price,
conf: *message.conf,
expo: *message.expo,
publish_time: *message.publish_time,
ema_price: *message.ema_price,
ema_conf: *message.ema_conf,
};
self.latest_price_info.write(message.price_id, info);
self.latest_price_info.write(*message.price_id, info);

let event = PriceFeedUpdated {
price_id: message.price_id,
publish_time: message.publish_time,
price: message.price,
conf: message.conf,
price_id: *message.price_id,
publish_time: *message.publish_time,
price: *message.price,
conf: *message.conf,
};
self.emit(event);
}
Expand Down Expand Up @@ -490,6 +476,98 @@ mod pyth {
let event = ContractUpgraded { new_class_hash: new_implementation };
self.emit(event);
}

fn parse_price_feed_updates_internal(
ref self: ContractState,
data: ByteArray,
price_ids: Array<u256>,
min_publish_time: u64,
max_publish_time: u64,
unique: bool,
) -> Array<PriceFeed> {
let mut output: Felt252Dict<Nullable<PriceFeed>> = Default::default();
let mut reader = ReaderImpl::new(data);
read_and_verify_header(ref reader);
let wormhole_proof_size = reader.read_u16();
let wormhole_proof = reader.read_byte_array(wormhole_proof_size.into());

let wormhole = IWormholeDispatcher { contract_address: self.wormhole_address.read() };
let vm = wormhole.parse_and_verify_vm(wormhole_proof);

let source = DataSource {
emitter_chain_id: vm.emitter_chain_id, emitter_address: vm.emitter_address
};
if !self.is_valid_data_source.read(source) {
panic_with_felt252(UpdatePriceFeedsError::InvalidUpdateDataSource.into());
}

let root_digest = parse_wormhole_proof(vm.payload);

let num_updates = reader.read_u8();
let total_fee = self.get_total_fee(num_updates);
let fee_contract = IERC20CamelDispatcher {
contract_address: self.fee_contract_address.read()
};
let execution_info = get_execution_info().unbox();
let caller = execution_info.caller_address;
let contract = execution_info.contract_address;
if fee_contract.allowance(caller, contract) < total_fee {
panic_with_felt252(UpdatePriceFeedsError::InsufficientFeeAllowance.into());
}
if !fee_contract.transferFrom(caller, contract, total_fee) {
panic_with_felt252(UpdatePriceFeedsError::InsufficientFeeAllowance.into());
}

let mut i = 0;
let price_ids2 = @price_ids;
while i < num_updates {
let message = read_and_verify_message(ref reader, root_digest);
self.update_latest_price_if_necessary(@message);
println!("message publish_time {:?}", message.publish_time);
println!("message prev_publish_time {:?}", message.prev_publish_time);
println!("message price_id {:?}", message.price_id);

let output_index = find_index_of_price_id(price_ids2, message.price_id);
match output_index {
Option::Some(output_index) => {
if output.get(output_index.into()).is_null() {
let should_output = message.publish_time >= min_publish_time
&& message.publish_time <= max_publish_time
&& (!unique || min_publish_time > message.prev_publish_time);
if should_output {
output
.insert(
output_index.into(), NullableTrait::new(message.into())
);
}
}
},
Option::None => {}
}

i += 1;
};

if reader.len() != 0 {
panic_with_felt252(UpdatePriceFeedsError::InvalidUpdateData.into());
}

let mut output_array = array![];
let mut i = 0;
while i < price_ids.len() {
let value = output.get(i.into());
match match_nullable(value) {
FromNullableResult::Null => {
panic_with_felt252(
ParsePriceFeedsError::PriceFeedNotFoundWithinRange.into()
)
},
FromNullableResult::NotNull(value) => { output_array.append(value.unbox()); }
}
i += 1;
};
output_array
}
}

fn apply_decimal_expo(value: u64, expo: u64) -> u256 {
Expand All @@ -511,4 +589,19 @@ mod pyth {
};
actual_age <= age
}

fn find_index_of_price_id(ids: @Array<u256>, value: u256) -> Option<usize> {
let mut i = 0;
while i < ids.len() {
if ids.at(i) == @value {
break;
}
i += 1;
};
if i == ids.len() {
Option::None
} else {
Option::Some(i)
}
}
}
15 changes: 15 additions & 0 deletions target_chains/starknet/contracts/src/pyth/errors.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,18 @@ impl UpdatePriceFeedsIfNecessaryErrorIntoFelt252 of Into<
}
}
}

#[derive(Copy, Drop, Debug, Serde, PartialEq)]
pub enum ParsePriceFeedsError {
Update: UpdatePriceFeedsError,
PriceFeedNotFoundWithinRange,
}

impl ParsePriceFeedsErrorIntoFelt252 of Into<ParsePriceFeedsError, felt252> {
fn into(self: ParsePriceFeedsError) -> felt252 {
match self {
ParsePriceFeedsError::Update(err) => err.into(),
ParsePriceFeedsError::PriceFeedNotFoundWithinRange => 'price feed not found',
}
}
}
23 changes: 22 additions & 1 deletion target_chains/starknet/contracts/src/pyth/interface.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ pub trait IPyth<T> {
fn update_price_feeds_if_necessary(
ref self: T, update: ByteArray, required_publish_times: Array<PriceFeedPublishTime>
);
fn parse_price_feed_updates(
ref self: T,
data: ByteArray,
price_ids: Array<u256>,
min_publish_time: u64,
max_publish_time: u64
) -> Array<PriceFeed>;
fn parse_unique_price_feed_updates(
ref self: T, data: ByteArray, price_ids: Array<u256>, publish_time: u64, max_staleness: u64,
) -> Array<PriceFeed>;
fn get_update_fee(self: @T, data: ByteArray) -> u256;
fn execute_governance_instruction(ref self: T, data: ByteArray);
fn pyth_upgradable_magic(self: @T) -> u32;
Expand All @@ -26,7 +36,7 @@ pub struct DataSource {
pub emitter_address: u256,
}

#[derive(Drop, Clone, Serde)]
#[derive(Drop, Copy, PartialEq, Serde)]
pub struct Price {
pub price: i64,
pub conf: u64,
Expand All @@ -39,3 +49,14 @@ pub struct PriceFeedPublishTime {
pub price_id: u256,
pub publish_time: u64,
}

// PriceFeed represents a current aggregate price from pyth publisher feeds.
#[derive(Drop, Copy, PartialEq, Serde)]
pub struct PriceFeed {
// The price ID.
pub id: u256,
// Latest available price
pub price: Price,
// Latest available exponentially-weighted moving average price
pub ema_price: Price,
}
22 changes: 21 additions & 1 deletion target_chains/starknet/contracts/src/pyth/price_update.cairo
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use pyth::reader::{Reader, ReaderImpl};
use pyth::pyth::UpdatePriceFeedsError;
use pyth::pyth::{UpdatePriceFeedsError, PriceFeed, Price};
use core::panic_with_felt252;
use pyth::byte_array::ByteArray;
use pyth::merkle_tree::read_and_verify_proof;
Expand Down Expand Up @@ -136,3 +136,23 @@ pub fn read_and_verify_message(ref reader: Reader, root_digest: u256) -> PriceFe
price_id, price, conf, expo, publish_time, prev_publish_time, ema_price, ema_conf,
}
}

impl PriceFeedMessageIntoPriceFeed of Into<PriceFeedMessage, PriceFeed> {
fn into(self: PriceFeedMessage) -> PriceFeed {
PriceFeed {
id: self.price_id,
price: Price {
price: self.price,
conf: self.conf,
expo: self.expo,
publish_time: self.publish_time,
},
ema_price: Price {
price: self.ema_price,
conf: self.ema_conf,
expo: self.expo,
publish_time: self.publish_time,
},
}
}
}
Loading

0 comments on commit 4a95ac6

Please sign in to comment.