[near] Add receiver contract (#407)

near: add initial near receiver
This commit is contained in:
Reisen 2023-01-02 14:33:46 +00:00 committed by GitHub
parent 378e99b345
commit c92329c7c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 3241 additions and 0 deletions

40
near/receiver/Cargo.toml Normal file
View File

@ -0,0 +1,40 @@
[package]
name = "pyth-near"
version = "0.1.0"
authors = ["Pyth Data Association"]
edition = "2021"
description = "A Pyth Receiver for Near"
[lib]
name = "pyth"
crate-type = ["cdylib", "lib"]
[dependencies]
byteorder = { version = "1.4.3" }
hex = { version = "0.4.3" }
near-sdk = { version = "4.1.1" }
p2w-sdk = { path = "../../third_party/pyth/p2w-sdk/rust" }
pyth-sdk = { version = "0.7.0" }
serde_wormhole = { git = "https://github.com/wormhole-foundation/wormhole" }
thiserror = { version = "1.0.38" }
wormhole-core = { git = "https://github.com/wormhole-foundation/wormhole" }
[patch.crates-io]
serde_wormhole = { git = "https://github.com/wormhole-foundation/wormhole" }
[dev-dependencies]
lazy_static = { version = "1.4.0" }
serde_json = { version = "1.0.91" }
serde = { version = "1.0.152", features = ["derive"] }
tokio = { version = "1.23.0", features = ["full"] }
serde_wormhole = { git = "https://github.com/wormhole-foundation/wormhole" }
workspaces = { version = "0.7.0" }
wormhole-core = { git = "https://github.com/wormhole-foundation/wormhole" }
[profile.release]
codegen-units = 1
opt-level = 3
lto = "fat"
debug = false
panic = "abort"
overflow-checks = true

View File

@ -0,0 +1,43 @@
use {
near_sdk::{
serde::Serialize,
FunctionError,
},
thiserror::Error,
};
#[derive(Error, Debug, Serialize, FunctionError)]
#[serde(crate = "near_sdk::serde")]
pub enum Error {
#[error("A hex argument could not be decoded.")]
InvalidHex,
#[error("A VAA could not be deserialized.")]
InvalidVaa,
#[error("A VAA payload could not be deserialized.")]
InvalidPayload,
#[error("Source for attestation is not allowed.")]
UnknownSource,
#[error("Unauthorized Upgrade")]
UnauthorizedUpgrade,
#[error("Insufficient tokens deposited to cover storage.")]
InsufficientDeposit,
#[error("VAA verification failed.")]
VaaVerificationFailed,
#[error("Fee is too large.")]
FeeTooLarge,
}
/// Convert IO errors into Payload errors, the only I/O we do is parsing with `Cursor` so this is a
/// reasonable conversion.
impl From<std::io::Error> for Error {
fn from(_: std::io::Error) -> Self {
Error::InvalidPayload
}
}

13
near/receiver/src/ext.rs Normal file
View File

@ -0,0 +1,13 @@
//! This module defines external contract API's that are used by the contract. This includes
//! Wormhole and perhaps any ancillary Pyth contracts.
use near_sdk::ext_contract;
/// Defines the external contract API we care about for interacting with Wormhole. Note that
/// Wormhole on NEAR passes VAA's as hex encoded strings so that the explorer can display them in a
/// clean way. This may require juggling between Vec<u8> and HexString.
#[ext_contract(ext_wormhole)]
pub trait Wormhole {
/// Returns the Governance Index of the current GuardianSet only if the VAA verifies.
fn verify_vaa(&self, vaa: String) -> u32;
}

View File

@ -0,0 +1,385 @@
//! Implement Governance Processing
use {
crate::{
error::Error,
ext::ext_wormhole,
state::{
Chain,
Source,
Vaa,
},
Pyth,
PythExt,
},
byteorder::{
BigEndian,
ReadBytesExt,
WriteBytesExt,
},
near_sdk::{
borsh::{
self,
BorshDeserialize,
BorshSerialize,
},
env,
is_promise_success,
near_bindgen,
serde::{
Deserialize,
Serialize,
},
AccountId,
Gas,
Promise,
},
std::io::Read,
wormhole::Chain as WormholeChain,
};
/// Magic Header for identifying Governance VAAs.
const GOVERNANCE_MAGIC: [u8; 4] = [0x50, 0x54, 0x47, 0x4d];
/// ID for the module this contract identifies as: Pyth Receiver (0x1).
const GOVERNANCE_MODULE: u8 = 0x01;
/// Enumeration of IDs for different governance actions.
#[repr(u8)]
pub enum ActionId {
ContractUpgrade = 0,
SetDataSources = 1,
SetGovernanceSource = 2,
SetStalePriceThreshold = 3,
SetUpdateFee = 4,
}
impl TryInto<ActionId> for u8 {
type Error = Error;
fn try_into(self) -> Result<ActionId, Error> {
match self {
0 => Ok(ActionId::ContractUpgrade),
1 => Ok(ActionId::SetDataSources),
2 => Ok(ActionId::SetGovernanceSource),
3 => Ok(ActionId::SetStalePriceThreshold),
4 => Ok(ActionId::SetUpdateFee),
_ => Err(Error::InvalidPayload),
}
}
}
impl From<ActionId> for u8 {
fn from(val: ActionId) -> Self {
val as u8
}
}
/// A `GovernanceAction` represents the different actions that can be voted on and executed by the
/// governance system.
#[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize)]
#[serde(crate = "near_sdk::serde")]
pub enum GovernanceAction {
ContractUpgrade([u8; 32]),
SetDataSources(Vec<Source>),
SetGovernanceSource(Source),
SetStalePriceThreshold(u64),
SetUpdateFee(u64),
}
impl GovernanceAction {
pub fn id(&self) -> ActionId {
match self {
GovernanceAction::ContractUpgrade(_) => ActionId::ContractUpgrade,
GovernanceAction::SetDataSources(_) => ActionId::SetDataSources,
GovernanceAction::SetGovernanceSource(_) => ActionId::SetGovernanceSource,
GovernanceAction::SetStalePriceThreshold(_) => ActionId::SetStalePriceThreshold,
GovernanceAction::SetUpdateFee(_) => ActionId::SetUpdateFee,
}
}
pub fn deserialize(data: &[u8]) -> Result<Self, Error> {
let mut cursor = std::io::Cursor::new(data);
let magic = cursor.read_u32::<BigEndian>()?;
let module = cursor.read_u8()?;
let action = cursor.read_u8()?.try_into()?;
let target = cursor.read_u16::<BigEndian>()?;
assert!(module == GOVERNANCE_MODULE);
assert!(target == 0 || target == u16::from(WormholeChain::Near));
assert!(magic == u32::from_le_bytes(GOVERNANCE_MAGIC));
Ok(match action {
ActionId::ContractUpgrade => {
let mut hash = [0u8; 32];
cursor.read_exact(&mut hash)?;
Self::ContractUpgrade(hash)
}
ActionId::SetDataSources => {
let mut sources = Vec::new();
let count = cursor.read_u8()?;
for _ in 0..count {
let mut emitter = [0u8; 32];
cursor.read_exact(&mut emitter)?;
sources.push(Source {
emitter,
pyth_emitter_chain: Chain::from(WormholeChain::from(
cursor.read_u16::<BigEndian>()?,
)),
});
}
Self::SetDataSources(sources)
}
ActionId::SetGovernanceSource => {
let mut emitter = [0u8; 32];
cursor.read_exact(&mut emitter)?;
Self::SetGovernanceSource(Source {
emitter,
pyth_emitter_chain: Chain(cursor.read_u16::<BigEndian>()?),
})
}
ActionId::SetStalePriceThreshold => {
let stale_price_threshold = cursor.read_u64::<BigEndian>()?;
Self::SetStalePriceThreshold(stale_price_threshold)
}
ActionId::SetUpdateFee => {
let update_fee = cursor.read_u64::<BigEndian>()?;
Self::SetUpdateFee(update_fee)
}
})
}
pub fn serialize(&self) -> Vec<u8> {
let mut data = Vec::new();
let magic = u32::from_le_bytes(GOVERNANCE_MAGIC);
data.write_u32::<BigEndian>(magic).unwrap();
data.push(GOVERNANCE_MODULE);
data.push(self.id() as u8);
data.extend_from_slice(&0u16.to_le_bytes());
match self {
Self::ContractUpgrade(hash) => {
data.extend_from_slice(hash);
}
Self::SetDataSources(sources) => {
data.push(sources.len() as u8);
for source in sources {
data.extend_from_slice(&source.emitter);
data.extend_from_slice(&source.pyth_emitter_chain.0.to_le_bytes());
}
}
Self::SetGovernanceSource(source) => {
data.extend_from_slice(&source.emitter);
data.extend_from_slice(&source.pyth_emitter_chain.0.to_le_bytes());
}
Self::SetStalePriceThreshold(stale_price_threshold) => {
data.extend_from_slice(&stale_price_threshold.to_le_bytes());
}
Self::SetUpdateFee(update_fee) => {
data.extend_from_slice(&update_fee.to_le_bytes());
}
}
data
}
}
#[near_bindgen]
impl Pyth {
/// Instruction for processing Governance VAA's relayed via Wormhole.
///
/// Note that VAA verification requires calling Wormhole so processing of the VAA itself is
/// done in a callback handler, see `process_vaa_callback`.
#[payable]
#[handle_result]
pub fn execute_governance_instruction(&mut self, vaa: String) -> Result<(), Error> {
// Verify the VAA is coming from a trusted source chain before attempting to verify VAA
// signatures. Avoids a cross-contract call early.
{
let vaa = hex::decode(&vaa).map_err(|_| Error::InvalidHex)?;
let vaa = serde_wormhole::from_slice_with_payload::<wormhole::Vaa<()>>(&vaa);
let vaa = vaa.map_err(|_| Error::InvalidVaa)?;
let (vaa, _rest) = vaa;
// Convert to local VAA type to catch APi changes.
let vaa = Vaa::from(vaa);
// Prevent VAA re-execution.
if self.executed_gov_sequences.contains(&vaa.sequence) {
return Err(Error::VaaVerificationFailed);
}
// Confirm the VAA is coming from a trusted source chain.
if self.gov_source
!= (Source {
emitter: vaa.emitter_address,
pyth_emitter_chain: vaa.emitter_chain,
})
{
return Err(Error::UnknownSource);
}
// Insert before calling Wormhole to prevent re-execution. If we wait until after the
// Wormhole call we could end up with multiple VAA's with the same sequence being
// executed in parallel.
self.executed_gov_sequences.insert(&vaa.sequence);
}
// Verify VAA and refund the caller in case of failure.
ext_wormhole::ext(self.wormhole.clone())
.with_static_gas(Gas(30_000_000_000_000))
.verify_vaa(vaa.clone())
.then(
Self::ext(env::current_account_id())
.with_static_gas(Gas(30_000_000_000_000))
.with_attached_deposit(env::attached_deposit())
.verify_gov_vaa_callback(env::predecessor_account_id(), vaa),
)
.then(
Self::ext(env::current_account_id())
.with_static_gas(Gas(30_000_000_000_000))
.refund_vaa(env::predecessor_account_id(), env::attached_deposit()),
);
Ok(())
}
/// Invoke handler upon successful verification of a VAA action.
#[payable]
#[private]
#[handle_result]
pub fn verify_gov_vaa_callback(
&mut self,
account_id: AccountId,
vaa: String,
#[callback_result] _result: Result<u32, near_sdk::PromiseError>,
) -> Result<(), Error> {
use GovernanceAction::*;
if !is_promise_success() {
return Err(Error::VaaVerificationFailed);
}
// Get Storage Usage before execution.
let storage = env::storage_usage();
// Deserialize VAA, note that we already deserialized and verified the VAA in `process_vaa`
// at this point so we only care about the `rest` component which contains bytes we can
// deserialize into an Action.
let vaa = hex::decode(&vaa).unwrap();
let (_, rest): (wormhole::Vaa<()>, _) =
serde_wormhole::from_slice_with_payload(&vaa).map_err(|_| Error::InvalidPayload)?;
match GovernanceAction::deserialize(rest)? {
ContractUpgrade(codehash) => self.set_upgrade_hash(codehash),
SetDataSources(sources) => self.set_sources(sources),
SetGovernanceSource(source) => self.set_gov_source(source),
SetStalePriceThreshold(threshold) => self.set_stale_price_threshold(threshold),
SetUpdateFee(fee) => self.set_update_fee(fee),
}
// Refund storage difference to `account_id` after storage execution.
self.refund_storage_usage(
account_id,
storage,
env::storage_usage(),
env::attached_deposit(),
)
}
/// If submitting an action fails then this callback will refund the caller.
#[private]
pub fn refund_vaa(&mut self, account_id: AccountId, amount: u128) {
if !is_promise_success() {
// No calculations needed as deposit size will have not changed. Can just refund the
// whole deposit amount.
Promise::new(account_id).transfer(amount);
}
}
#[private]
pub fn set_upgrade_hash(&mut self, codehash: [u8; 32]) {
self.codehash = codehash;
}
#[private]
pub fn set_gov_source(&mut self, source: Source) {
self.gov_source = source;
}
#[private]
pub fn set_stale_price_threshold(&mut self, threshold: u64) {
self.stale_threshold = threshold;
}
#[private]
pub fn set_update_fee(&mut self, fee: u64) {
self.update_fee = fee;
}
#[private]
#[handle_result]
pub fn set_sources(&mut self, sources: Vec<Source>) {
self.sources.clear();
sources.iter().for_each(|s| {
self.sources.insert(s);
});
}
/// This method allows self-upgrading the contract to a new implementation.
///
/// This function is open to call by anyone, but to perform an authorized upgrade a VAA
/// containing the hash of the `new_code` must have previously been relayed to this contract's
/// `process_vaa` endpoint. otherwise the upgrade will fail.
///
/// NOTE: This function is pub only within crate scope so that it can only be called by the
/// `upgrade_contract` method, this is much much cheaper than serializing a Vec<u8> to call
/// this method as a normal public method.
#[handle_result]
pub(crate) fn upgrade(&mut self, new_code: Vec<u8>) -> Result<Promise, Error> {
let signature = env::sha256(&new_code);
if signature != self.codehash {
return Err(Error::UnauthorizedUpgrade);
}
Ok(Promise::new(env::current_account_id())
.deploy_contract(new_code)
.then(Self::ext(env::current_account_id()).refund_upgrade(
env::predecessor_account_id(),
env::attached_deposit(),
env::storage_usage(),
)))
}
/// This method is called after the upgrade to refund the caller for the storage used by the
/// old contract.
#[private]
#[handle_result]
pub fn refund_upgrade(
&mut self,
account_id: AccountId,
amount: u128,
storage: u64,
) -> Result<(), Error> {
self.refund_storage_usage(account_id, storage, env::storage_usage(), amount)
}
}
impl Pyth {
#[allow(dead_code)]
fn is_valid_governance_source(&self, source: &Source) -> Result<(), Error> {
(self.gov_source == *source)
.then_some(())
.ok_or(Error::UnknownSource)
}
}

388
near/receiver/src/lib.rs Normal file
View File

@ -0,0 +1,388 @@
//#![deny(warnings)]
use {
error::Error,
ext::ext_wormhole,
near_sdk::{
borsh::{
self,
BorshDeserialize,
BorshSerialize,
},
collections::{
UnorderedMap,
UnorderedSet,
},
env,
is_promise_success,
log,
near_bindgen,
AccountId,
Balance,
BorshStorageKey,
Duration,
Gas,
PanicOnDefault,
Promise,
StorageUsage,
},
p2w_sdk::BatchPriceAttestation,
state::{
Price,
PriceFeed,
PriceIdentifier,
Source,
Vaa,
},
std::io::Cursor,
};
pub mod error;
pub mod ext;
pub mod governance;
pub mod state;
pub mod tests;
#[derive(BorshSerialize, BorshStorageKey)]
enum StorageKeys {
Source,
Prices,
Governance,
}
/// The `State` contains all persisted state for the contract. This includes runtime configuration.
///
/// There is no valid Default state for this contract, so we derive PanicOnDefault to force
/// deployment using one of the #[init] functions in the impl below.
#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)]
pub struct Pyth {
/// The set of `Source`s from which Pyth attestations are allowed to be relayed from.
sources: UnorderedSet<Source>,
/// The Governance Source.
gov_source: Source,
/// The last executed sequence number for governance actions.
executed_gov_sequences: UnorderedSet<u64>,
/// A Mapping from PriceFeed ID to Price Info.
prices: UnorderedMap<PriceIdentifier, PriceFeed>,
/// The `AccountId` of the Wormhole contract used to verify VAA's.
wormhole: AccountId,
/// A hash of the current contract code.
///
/// If this `hash` does not match the current contract code, then it indicates that a pending
/// upgrade has been allowed, calling `upgrade` with code that matches this hash will cause the
/// contract to upgrade itself.
codehash: [u8; 32],
/// Staleness threshold for rejecting price updates.
stale_threshold: Duration,
/// Fee for updating price.
update_fee: u64,
}
#[near_bindgen]
impl Pyth {
#[init]
#[allow(clippy::new_without_default)]
pub fn new(
wormhole: AccountId,
codehash: [u8; 32],
initial_source: Source,
gov_source: Source,
update_fee: u64,
stale_threshold: u64,
) -> Self {
// Add an initial Source so that the contract can be used.
let mut sources = UnorderedSet::new(StorageKeys::Source);
sources.insert(&initial_source);
Self {
prices: UnorderedMap::new(StorageKeys::Prices),
executed_gov_sequences: UnorderedSet::new(StorageKeys::Governance),
stale_threshold,
gov_source,
sources,
wormhole,
codehash,
update_fee,
}
}
#[init(ignore_state)]
pub fn migrate() -> Self {
let state: Self = env::state_read().expect("Failed to read state");
state
}
/// Instruction for processing VAA's relayed via Wormhole.
///
/// Note that VAA verification requires calling Wormhole so processing of the VAA itself is
/// done in a callback handler, see `process_vaa_callback`.
#[payable]
#[handle_result]
pub fn update_price_feed(&mut self, vaa_hex: String) -> Result<(), Error> {
// We Verify the VAA is coming from a trusted source chain before attempting to verify
// VAA signatures. Avoids a cross-contract call early.
let vaa = hex::decode(&vaa_hex).map_err(|_| Error::InvalidHex)?;
let vaa = serde_wormhole::from_slice_with_payload::<wormhole::Vaa<()>>(&vaa);
let vaa = vaa.map_err(|_| Error::InvalidVaa)?;
let (vaa, _rest) = vaa;
// Convert to local VAA type to catch APi changes.
let vaa = Vaa::from(vaa);
if !self.sources.contains(&Source {
emitter: vaa.emitter_address,
pyth_emitter_chain: vaa.emitter_chain,
}) {
return Err(Error::UnknownSource);
}
// Verify VAA and refund the caller in case of failure.
ext_wormhole::ext(self.wormhole.clone())
.with_static_gas(Gas(30_000_000_000_000))
.verify_vaa(vaa_hex.clone())
.then(
Self::ext(env::current_account_id())
.with_static_gas(Gas(30_000_000_000_000))
.with_attached_deposit(env::attached_deposit())
.verify_vaa_callback(env::predecessor_account_id(), vaa_hex),
)
.then(
Self::ext(env::current_account_id())
.with_static_gas(Gas(30_000_000_000_000))
.refund_vaa(env::predecessor_account_id(), env::attached_deposit()),
);
Ok(())
}
/// Return the deposit required to update a price feed.
pub fn get_update_fee_estimate(&self) -> u64 {
let byte_cost = env::storage_byte_cost();
let data_cost = byte_cost * std::mem::size_of::<PriceFeed>() as u128;
4u64 * u64::try_from(data_cost).unwrap() + self.update_fee
}
#[payable]
#[private]
#[handle_result]
pub fn verify_vaa_callback(
&mut self,
account_id: AccountId,
vaa: String,
#[callback_result] _result: Result<u32, near_sdk::PromiseError>,
) -> Result<(), Error> {
if !is_promise_success() {
return Err(Error::VaaVerificationFailed);
}
// Get Storage Usage before execution, subtracting the fee from the deposit has the effect
// forces the caller to add the required fee to the deposit.
let storage = env::storage_usage()
.checked_sub(self.update_fee)
.ok_or(Error::InsufficientDeposit)?;
// Deserialize VAA, note that we already deserialized and verified the VAA in `process_vaa`
// at this point so we only care about the `rest` component which contains bytes we can
// deserialize into an Action.
let vaa = hex::decode(&vaa).unwrap();
let (_, rest): (wormhole::Vaa<()>, _) =
serde_wormhole::from_slice_with_payload(&vaa).unwrap();
// Attempt to deserialize the Batch of Price Attestations.
let bytes = &mut Cursor::new(rest);
let batch = BatchPriceAttestation::deserialize(bytes).unwrap();
// Verify the PriceAttestation's are new enough, and if so, store them.
let mut count_updates = 0;
for price_attestation in &batch.price_attestations {
if self.update_price_feed_if_new(PriceFeed::from(price_attestation)) {
count_updates += 1;
}
}
log!(
r#"
{{
"standard": "pyth",
"version": "1.0",
"event": "BatchAttest",
"data": {{
"count": {},
"diffs": {},
"costs": {},
}}
}}
"#,
count_updates,
env::storage_usage() - storage,
env::storage_byte_cost() * (env::storage_usage() - storage) as u128,
);
// Refund storage difference to `account_id` after storage execution.
self.refund_storage_usage(
account_id,
storage,
env::storage_usage(),
env::attached_deposit(),
)
}
/// Read the list of accepted `Source` chains for a price attestation.
pub fn get_sources(&self) -> Vec<Source> {
self.sources.iter().collect()
}
/// Get the current staleness threshold.
pub fn get_stale_threshold(&self) -> u64 {
self.stale_threshold
}
/// Determine if a price feed for the given price_identifier exists
pub fn price_feed_exists(&self, price_identifier: PriceIdentifier) -> bool {
self.prices.get(&price_identifier).is_some()
}
/// Get the latest available price cached for the given price identifier, if that price is
/// no older than the stale price threshold.
///
/// Please refer to the documentation at https://docs.pyth.network/consumers/best-practices for
/// how to how this price safely.
///
/// IMPORTANT:
/// Pyth uses an on-demand update model, where consumers need to update the cached prices
/// before using them. Please read more about this at https://docs.pyth.network/consume-data/on-demand.
pub fn get_price(&self, price_identifier: PriceIdentifier) -> Option<Price> {
self.get_price_no_older_than(price_identifier, self.stale_threshold)
}
/// Get the latest available price cached for the given price identifier.
///
/// WARNING:
///
/// the returned price can be from arbitrarily far in the past. This function makes no
/// guarantees that the returned price is recent or useful for any particular application.
/// Users of this function should check the returned timestamp to ensure that the returned
/// price is sufficiently recent for their application. The checked get_price_no_older_than()
/// function should be used in preference to this.
pub fn get_price_unsafe(&self, price_identifier: PriceIdentifier) -> Option<Price> {
self.get_price_no_older_than(price_identifier, u64::MAX)
}
/// Get the latest available price cached for the given price identifier, if that price is
/// no older than the given age.
pub fn get_price_no_older_than(&self, price_id: PriceIdentifier, age: u64) -> Option<Price> {
self.prices.get(&price_id).and_then(|feed| {
let block_timestamp = env::block_timestamp() / 1_000_000_000;
let price_timestamp = feed.price.timestamp;
// - If Price older than STALENESS_THRESHOLD, set status to Unknown.
// - If Price newer than now by more than STALENESS_THRESHOLD, set status to Unknown.
// - Any other price around the current time is considered valid.
if u64::abs_diff(block_timestamp, price_timestamp) > age {
return None;
}
Some(feed.price)
})
}
/// EMA version of `get_price`.
pub fn get_ema_price(&self, price_id: PriceIdentifier) -> Option<Price> {
self.get_ema_price_no_older_than(price_id, self.get_stale_threshold())
}
/// EMA version of `get_price_unsafe`.
pub fn get_ema_price_unsafe(&self, price_id: PriceIdentifier) -> Option<Price> {
self.get_ema_price_no_older_than(price_id, u64::MAX)
}
/// EMA version of `get_price_no_older_than`.
pub fn get_ema_price_no_older_than(
&self,
price_id: PriceIdentifier,
age: u64,
) -> Option<Price> {
self.prices.get(&price_id).and_then(|feed| {
let block_timestamp = env::block_timestamp();
let price_timestamp = feed.ema_price.timestamp;
// - If Price older than STALENESS_THRESHOLD, set status to Unknown.
// - If Price newer than now by more than STALENESS_THRESHOLD, set status to Unknown.
// - Any other price around the current time is considered valid.
if u64::abs_diff(block_timestamp, price_timestamp) > age {
return None;
}
Some(feed.ema_price)
})
}
}
/// This second `impl Pyth` block contains only private methods that are called internally that
/// have no transaction semantics associated with them. Note that these do not need `#[private]`
/// annotations as they are already uncallable.
impl Pyth {
/// Updates the Price Feed only if it is newer than the current one. This function never fails
/// and will either update in-place or not update at all. The return value indicates whether
/// the update was performed or not.
fn update_price_feed_if_new(&mut self, price_feed: PriceFeed) -> bool {
match self.prices.get(&price_feed.id) {
Some(stored_price_feed) => {
let update = price_feed.price.timestamp > stored_price_feed.price.timestamp;
update.then(|| self.prices.insert(&price_feed.id, &price_feed));
update
}
None => {
self.prices.insert(&price_feed.id, &price_feed);
true
}
}
}
/// Checks storage usage invariants and additionally refunds the caller if they overpay.
fn refund_storage_usage(
&self,
refunder: AccountId,
before: StorageUsage,
after: StorageUsage,
deposit: Balance,
) -> Result<(), Error> {
if let Some(diff) = after.checked_sub(before) {
// Handle storage increases if checked_sub succeeds.
let cost = Balance::from(diff);
let cost = cost * env::storage_byte_cost();
// If the cost is higher than the deposit we bail.
if cost > deposit {
return Err(Error::InsufficientDeposit);
}
// Otherwise we refund whatever is left over.
if deposit - cost > 0 {
Promise::new(refunder).transfer(cost);
}
} else {
// Handle storage decrease if checked_sub fails. We know storage used now is <=
let refund = Balance::from(before - after);
let refund = refund * env::storage_byte_cost();
Promise::new(refunder).transfer(refund);
}
Ok(())
}
}
#[no_mangle]
pub extern "C" fn update_contract() {
env::setup_panic_hook();
let mut contract: Pyth = env::state_read().expect("Failed to Read State");
contract.upgrade(env::input().unwrap()).unwrap();
}

163
near/receiver/src/state.rs Normal file
View File

@ -0,0 +1,163 @@
use {
near_sdk::{
borsh::{
self,
BorshDeserialize,
BorshSerialize,
},
serde::{
Deserialize,
Serialize,
},
},
p2w_sdk::PriceAttestation,
wormhole::Chain as WormholeChain,
};
/// Type alias for Wormhole's compact Signature format.
pub type WormholeSignature = [u8; 65];
/// Type alias for Wormhole's cross-chain 32-byte address.
pub type WormholeAddress = [u8; 32];
#[derive(BorshDeserialize, BorshSerialize, Deserialize, Serialize)]
#[serde(crate = "near_sdk::serde")]
#[repr(transparent)]
pub struct PriceIdentifier(pub [u8; 32]);
/// A price with a degree of uncertainty, represented as a price +- a confidence interval.
///
/// The confidence interval roughly corresponds to the standard error of a normal distribution.
/// Both the price and confidence are stored in a fixed-point numeric representation,
/// `x * (10^expo)`, where `expo` is the exponent.
//
/// Please refer to the documentation at https://docs.pyth.network/consumers/best-practices for how
/// to how this price safely.
#[derive(BorshDeserialize, BorshSerialize, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(crate = "near_sdk::serde")]
pub struct Price {
pub price: i64,
/// Confidence interval around the price
pub conf: u64,
/// The exponent
pub expo: i32,
/// Unix timestamp of when this price was computed
pub timestamp: u64,
}
/// The PriceFeed structure is stored in the contract under a Price Feed Identifier.
///
/// This structure matches the layout of the PriceFeed structure in other Pyth receiver contracts
/// but uses types that are native to NEAR.
#[derive(BorshDeserialize, BorshSerialize, Deserialize, Serialize)]
#[serde(crate = "near_sdk::serde")]
pub struct PriceFeed {
/// Unique identifier for this price.
pub id: PriceIdentifier,
/// The current aggregation price.
pub price: Price,
/// Exponentially moving average price.
pub ema_price: Price,
}
// TODO: Source the Timestamp
impl From<&PriceAttestation> for PriceFeed {
fn from(price_attestation: &PriceAttestation) -> Self {
Self {
id: PriceIdentifier(price_attestation.price_id.to_bytes()),
price: Price {
price: price_attestation.price,
conf: price_attestation.conf,
expo: price_attestation.expo,
timestamp: price_attestation.publish_time.try_into().unwrap(),
},
ema_price: Price {
price: price_attestation.ema_price,
conf: price_attestation.ema_conf,
expo: price_attestation.expo,
timestamp: price_attestation.publish_time.try_into().unwrap(),
},
}
}
}
/// A wrapper around a 16bit chain identifier. We can't use Chain from the Wormhole SDK as it does
/// not provide borsh serialization but we can re-wrap it here relying on the validation from
/// `wormhole::Chain`.
#[derive(
BorshDeserialize,
BorshSerialize,
Clone,
Debug,
Default,
Deserialize,
Eq,
Hash,
PartialEq,
PartialOrd,
Serialize,
)]
#[serde(crate = "near_sdk::serde")]
#[repr(transparent)]
pub struct Chain(pub u16);
impl From<WormholeChain> for Chain {
fn from(chain: WormholeChain) -> Self {
Self(u16::from(chain))
}
}
/// A `Source` describes an origin chain from which Pyth attestations are allowed.
///
/// This allows for example Pyth prices to be sent from either Pythnet or Solana, but can be used
/// to add any additional trusted source chains.
#[derive(
BorshDeserialize,
BorshSerialize,
Clone,
Debug,
Default,
Deserialize,
Eq,
Hash,
PartialEq,
PartialOrd,
Serialize,
)]
#[serde(crate = "near_sdk::serde")]
pub struct Source {
pub emitter: WormholeAddress,
pub pyth_emitter_chain: Chain,
}
/// A local `Vaa` type converted to from the Wormhole definition, this helps catch any upstream
/// changes to the Wormhole VAA format.
pub struct Vaa<P> {
pub version: u8,
pub guardian_set_index: u32,
pub signatures: Vec<WormholeSignature>,
pub timestamp: u32, // Seconds since UNIX epoch
pub nonce: u32,
pub emitter_chain: Chain,
pub emitter_address: WormholeAddress,
pub sequence: u64,
pub consistency_level: u8,
pub payload: P,
}
impl<P> From<wormhole::Vaa<P>> for Vaa<P> {
fn from(vaa: wormhole::Vaa<P>) -> Self {
Self {
version: vaa.version,
guardian_set_index: vaa.guardian_set_index,
signatures: vaa.signatures.iter().map(|s| s.signature).collect(),
timestamp: vaa.timestamp,
nonce: vaa.nonce,
emitter_chain: vaa.emitter_chain.into(),
emitter_address: vaa.emitter_address.0,
sequence: vaa.sequence,
consistency_level: vaa.consistency_level,
payload: vaa.payload,
}
}
}

View File

@ -0,0 +1,42 @@
#[cfg(test)]
#[allow(clippy::module_inception)]
mod tests {
use {
crate::{
state::Source,
Pyth,
},
near_sdk::{
test_utils::VMContextBuilder,
testing_env,
VMContext,
},
};
fn create_contract() -> Pyth {
Pyth::new(
"wormhole.near".parse().unwrap(),
[0; 32],
Source::default(),
Source::default(),
1,
32,
)
}
fn get_context() -> VMContext {
VMContextBuilder::new()
.signer_account_id("pda".parse().unwrap())
.is_view(false)
.build()
}
#[test]
fn test_contract_init() {
let contract = create_contract();
let context = get_context();
testing_env!(context);
assert_eq!(contract.sources.len(), 1);
assert_eq!(contract.prices.len(), 0);
}
}

View File

@ -0,0 +1,592 @@
use {
near_sdk::json_types::U128,
p2w_sdk::{
BatchPriceAttestation,
Identifier,
PriceAttestation,
PriceStatus,
},
pyth::{
governance::GovernanceAction,
state::{
Price,
PriceIdentifier,
Source,
},
},
serde_json::json,
std::io::{
Cursor,
Write,
},
};
async fn initialize_chain() -> (
workspaces::Worker<workspaces::network::Sandbox>,
workspaces::Contract,
workspaces::Contract,
) {
let worker = workspaces::sandbox().await.expect("Workspaces Failed");
// Deploy Pyth
let contract = worker
.dev_deploy(&std::fs::read("pyth.wasm").expect("Failed to find pyth.wasm"))
.await
.expect("Failed to deploy pyth.wasm");
// Deploy Wormhole Stub, this is a dummy contract that always verifies VAA's correctly so we
// can test the ext_wormhole API.
let wormhole = worker
.dev_deploy(
&std::fs::read("wormhole_stub.wasm").expect("Failed to find wormhole_stub.wasm"),
)
.await
.expect("Failed to deploy wormhole_stub.wasm");
// Initialize Wormhole.
wormhole
.call("new")
.args_json(&json!({}))
.gas(300_000_000_000_000)
.transact()
.await
.expect("Failed to initialize Wormhole")
.unwrap();
// Initialize Pyth, one time operation that sets the Wormhole contract address.
let codehash = [0u8; 32];
contract
.call("new")
.args_json(&json!({
"wormhole": wormhole.id(),
"codehash": codehash,
"initial_source": Source::default(),
"gov_source": Source::default(),
"update_fee": U128::from(1u128),
"stale_threshold": 32,
}))
.gas(300_000_000_000_000)
.transact()
.await
.expect("Failed to initialize Pyth")
.unwrap();
(worker, contract, wormhole)
}
#[tokio::test]
async fn test_source_add() {
let (_, contract, _) = initialize_chain().await;
// Submit a new Source to the contract, this will trigger a cross-contract call to wormhole
let vaa = wormhole::Vaa {
emitter_chain: wormhole::Chain::Any,
emitter_address: wormhole::Address([0; 32]),
payload: (),
..Default::default()
};
let vaa = {
let mut cur = Cursor::new(Vec::new());
serde_wormhole::to_writer(&mut cur, &vaa).expect("Failed to serialize VAA");
cur.write_all(
&GovernanceAction::SetDataSources(vec![
Source::default(),
Source {
emitter: [1; 32],
pyth_emitter_chain: pyth::state::Chain(1),
},
])
.serialize(),
)
.expect("Failed to write Payload");
hex::encode(cur.into_inner())
};
assert!(contract
.call("execute_governance_instruction")
.gas(300_000_000_000_000)
.deposit(300_000_000_000_000_000_000_000)
.args_json(&json!({
"vaa": vaa,
}))
.transact()
.await
.expect("Failed to submit VAA")
.outcome()
.is_success());
// There should now be a two sources in the contract state.
assert_eq!(
serde_json::from_slice::<Vec<Source>>(&contract.view("get_sources").await.unwrap().result)
.unwrap(),
&[
Source::default(),
Source {
emitter: [1; 32],
pyth_emitter_chain: pyth::state::Chain(1),
},
]
);
}
#[tokio::test]
async fn test_set_governance_source() {
let (_, contract, _) = initialize_chain().await;
// Submit a new Source to the contract, this will trigger a cross-contract call to wormhole
let vaa = wormhole::Vaa {
emitter_chain: wormhole::Chain::Any,
emitter_address: wormhole::Address([0; 32]),
payload: (),
..Default::default()
};
let vaa = {
let mut cur = Cursor::new(Vec::new());
serde_wormhole::to_writer(&mut cur, &vaa).expect("Failed to serialize VAA");
cur.write_all(
&GovernanceAction::SetGovernanceSource(Source {
emitter: [1; 32],
pyth_emitter_chain: pyth::state::Chain(1),
})
.serialize(),
)
.expect("Failed to write Payload");
hex::encode(cur.into_inner())
};
assert!(contract
.call("execute_governance_instruction")
.gas(300_000_000_000_000)
.deposit(300_000_000_000_000_000_000_000)
.args_json(&json!({
"vaa": vaa,
}))
.transact()
.await
.expect("Failed to submit VAA")
.outcome()
.is_success());
// An action from the new source should now be accepted.
let vaa = wormhole::Vaa {
sequence: 1, // NOTE: Incremented Governance Sequence
emitter_chain: wormhole::Chain::Solana,
emitter_address: wormhole::Address([1; 32]),
payload: (),
..Default::default()
};
let vaa = {
let mut cur = Cursor::new(Vec::new());
serde_wormhole::to_writer(&mut cur, &vaa).expect("Failed to serialize VAA");
cur.write_all(
&GovernanceAction::SetDataSources(vec![
Source::default(),
Source {
emitter: [2; 32],
pyth_emitter_chain: pyth::state::Chain(2),
},
])
.serialize(),
)
.expect("Failed to write Payload");
hex::encode(cur.into_inner())
};
assert!(contract
.call("execute_governance_instruction")
.gas(300_000_000_000_000)
.deposit(300_000_000_000_000_000_000_000)
.args_json(&json!({
"vaa": vaa,
}))
.transact()
.await
.expect("Failed to submit VAA")
.outcome()
.is_success());
// But not from the old source.
let vaa = wormhole::Vaa {
sequence: 1, // NOTE: Incremented Governance Sequence
emitter_chain: wormhole::Chain::Any,
emitter_address: wormhole::Address([0; 32]),
payload: (),
..Default::default()
};
let vaa = {
let mut cur = Cursor::new(Vec::new());
serde_wormhole::to_writer(&mut cur, &vaa).expect("Failed to serialize VAA");
cur.write_all(
&GovernanceAction::SetDataSources(vec![
Source::default(),
Source {
emitter: [2; 32],
pyth_emitter_chain: pyth::state::Chain(2),
},
])
.serialize(),
)
.expect("Failed to write Payload");
hex::encode(cur.into_inner())
};
assert!(!contract
.call("execute_governance_instruction")
.gas(300_000_000_000_000)
.deposit(300_000_000_000_000_000_000_000)
.args_json(&json!({
"vaa": vaa,
}))
.transact()
.await
.expect("Failed to submit VAA")
.outcome()
.is_failure());
}
#[tokio::test]
async fn test_stale_threshold() {
let (_, contract, _) = initialize_chain().await;
// Submit a Price Attestation to the contract.
let vaa = wormhole::Vaa {
emitter_chain: wormhole::Chain::Any,
emitter_address: wormhole::Address([0; 32]),
payload: (),
..Default::default()
};
// Get current UNIX timestamp and subtract a minute from it to place the price attestation in
// the past. This should be accepted but untrusted.
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("Failed to get UNIX timestamp")
.as_secs()
- 60;
let vaa = {
let mut cur = Cursor::new(Vec::new());
serde_wormhole::to_writer(&mut cur, &vaa).expect("Failed to serialize VAA");
cur.write_all(
&BatchPriceAttestation {
price_attestations: vec![PriceAttestation {
product_id: Identifier::default(),
price_id: Identifier::default(),
price: 100,
conf: 1,
expo: 8,
ema_price: 100,
ema_conf: 1,
status: PriceStatus::Trading,
num_publishers: 8,
max_num_publishers: 8,
attestation_time: now.try_into().unwrap(),
publish_time: now.try_into().unwrap(),
prev_publish_time: now.try_into().unwrap(),
prev_price: 100,
prev_conf: 1,
}],
}
.serialize()
.unwrap(),
)
.expect("Failed to write Payload");
hex::encode(cur.into_inner())
};
let update_fee = serde_json::from_slice::<U128>(
&contract
.view("get_update_fee_estimate")
.args(vec![])
.await
.unwrap()
.result,
)
.unwrap();
assert!(contract
.call("update_price_feed")
.gas(300_000_000_000_000)
.deposit(update_fee.into())
.args_json(&json!({
"vaa_hex": vaa,
}))
.transact()
.await
.expect("Failed to submit VAA")
.outcome()
.is_success());
// Assert Price cannot be requested, 60 seconds in the past should be considered stale.
// [tag:failed_price_check]
assert_eq!(
None,
serde_json::from_slice::<Option<Price>>(
&contract
.view("get_price")
.args_json(&json!({ "price_identifier": PriceIdentifier([0; 32]) }))
.await
.unwrap()
.result
)
.unwrap(),
);
// Submit another Price Attestation to the contract with an even older timestamp.
let vaa = wormhole::Vaa {
emitter_chain: wormhole::Chain::Any,
emitter_address: wormhole::Address([0; 32]),
sequence: 1,
payload: (),
..Default::default()
};
let vaa = {
let mut cur = Cursor::new(Vec::new());
serde_wormhole::to_writer(&mut cur, &vaa).expect("Failed to serialize VAA");
cur.write_all(
&BatchPriceAttestation {
price_attestations: vec![PriceAttestation {
product_id: Identifier::default(),
price_id: Identifier::default(),
price: 1000,
conf: 1,
expo: 8,
ema_price: 1000,
ema_conf: 1,
status: PriceStatus::Trading,
num_publishers: 8,
max_num_publishers: 8,
attestation_time: (now - 1024).try_into().unwrap(),
publish_time: (now - 1024).try_into().unwrap(),
prev_publish_time: (now - 1024).try_into().unwrap(),
prev_price: 90,
prev_conf: 1,
}],
}
.serialize()
.unwrap(),
)
.expect("Failed to write Payload");
hex::encode(cur.into_inner())
};
// The update handler should succeed even if price is old, but simply not update the price.
assert!(contract
.call("update_price_feed")
.gas(300_000_000_000_000)
.deposit(update_fee.into())
.args_json(&json!({
"vaa_hex": vaa,
}))
.transact()
.await
.expect("Failed to submit VAA")
.outcome()
.is_success());
// The price however should _not_ have updated and if we check the unsafe stored price the
// timestamp and price should be unchanged.
assert_eq!(
Price {
price: 100,
conf: 1,
expo: 8,
timestamp: now,
},
serde_json::from_slice::<Price>(
&contract
.view("get_price_unsafe")
.args_json(&json!({ "price_identifier": PriceIdentifier([0; 32]) }))
.await
.unwrap()
.result
)
.unwrap(),
);
// Now we extend the staleness threshold with a Governance VAA.
let vaa = wormhole::Vaa {
emitter_chain: wormhole::Chain::Any,
emitter_address: wormhole::Address([0; 32]),
sequence: 2,
payload: (),
..Default::default()
};
let vaa = {
let mut cur = Cursor::new(Vec::new());
serde_wormhole::to_writer(&mut cur, &vaa).unwrap();
cur.write_all(&GovernanceAction::SetStalePriceThreshold(256).serialize())
.unwrap();
hex::encode(cur.into_inner())
};
assert!(contract
.call("execute_governance_instruction")
.gas(300_000_000_000_000)
.deposit(300_000_000_000_000_000_000_000)
.args_json(&json!({
"vaa": vaa,
}))
.transact()
.await
.expect("Failed to submit VAA")
.outcome()
.is_success());
// It should now be possible to request the price that previously returned None.
// [ref:failed_price_check]
assert_eq!(
Some(Price {
price: 100,
conf: 1,
expo: 8,
timestamp: now,
}),
serde_json::from_slice::<Option<Price>>(
&contract
.view("get_price")
.args_json(&json!({ "price_identifier": PriceIdentifier([0; 32]) }))
.await
.unwrap()
.result
)
.unwrap(),
);
}
#[tokio::test]
async fn test_contract_fees() {
let (_, contract, _) = initialize_chain().await;
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("Failed to get UNIX timestamp")
.as_secs();
// Fetch Update fee before changing it.
let update_fee = serde_json::from_slice::<U128>(
&contract
.view("get_update_fee_estimate")
.args(vec![])
.await
.unwrap()
.result,
)
.unwrap();
// Set a high fee for the contract needed to submit a price.
let vaa = wormhole::Vaa {
emitter_chain: wormhole::Chain::Any,
emitter_address: wormhole::Address([0; 32]),
payload: (),
..Default::default()
};
let vaa = {
let mut cur = Cursor::new(Vec::new());
serde_wormhole::to_writer(&mut cur, &vaa).unwrap();
cur.write_all(&GovernanceAction::SetUpdateFee(u64::MAX).serialize())
.unwrap();
hex::encode(cur.into_inner())
};
// Now set the update_fee too high for the deposit to cover.
assert!(contract
.call("execute_governance_instruction")
.gas(300_000_000_000_000)
.deposit(300_000_000_000_000_000_000_000)
.args_json(&json!({
"vaa": vaa,
}))
.transact()
.await
.expect("Failed to submit VAA")
.outcome()
.is_success());
assert_ne!(
update_fee,
serde_json::from_slice::<U128>(
&contract
.view("get_update_fee_estimate")
.args(vec![])
.await
.unwrap()
.result,
)
.unwrap()
);
// Attempt to update the price feed with a now too low deposit.
let vaa = wormhole::Vaa {
emitter_chain: wormhole::Chain::Any,
emitter_address: wormhole::Address([0; 32]),
sequence: 1,
payload: (),
..Default::default()
};
let vaa = {
let mut cur = Cursor::new(Vec::new());
serde_wormhole::to_writer(&mut cur, &vaa).expect("Failed to serialize VAA");
cur.write_all(
&BatchPriceAttestation {
price_attestations: vec![PriceAttestation {
product_id: Identifier::default(),
price_id: Identifier::default(),
price: 1000,
conf: 1,
expo: 8,
ema_price: 1000,
ema_conf: 1,
status: PriceStatus::Trading,
num_publishers: 8,
max_num_publishers: 8,
attestation_time: (now - 1024).try_into().unwrap(),
publish_time: (now - 1024).try_into().unwrap(),
prev_publish_time: (now - 1024).try_into().unwrap(),
prev_price: 90,
prev_conf: 1,
}],
}
.serialize()
.unwrap(),
)
.expect("Failed to write Payload");
hex::encode(cur.into_inner())
};
assert!(contract
.call("update_price_feed")
.gas(300_000_000_000_000)
.deposit(update_fee.into())
.args_json(&json!({
"vaa_hex": vaa,
}))
.transact()
.await
.expect("Failed to submit VAA")
.outcome()
.is_success());
// Submitting a Price should have failed because the fee was not enough.
assert_eq!(
None,
serde_json::from_slice::<Option<Price>>(
&contract
.view("get_price")
.args_json(&json!({ "price_identifier": PriceIdentifier([0; 32]) }))
.await
.unwrap()
.result
)
.unwrap(),
);
}

1512
near/wormhole-stub/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,19 @@
[package]
name = "wormhole-stub"
version = "0.1.0"
authors = ["Pyth Data Association"]
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
near-sdk = { version = "4.1.1" }
[profile.release]
codegen-units = 1
opt-level = "z"
lto = true
debug = false
panic = "abort"
overflow-checks = true

View File

@ -0,0 +1,44 @@
//#![deny(warnings)]
use near_sdk::{
borsh::{
self,
BorshDeserialize,
BorshSerialize,
},
log,
near_bindgen,
PanicOnDefault,
};
#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)]
pub struct State {}
#[near_bindgen]
impl State {
#[init]
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self {}
}
#[payable]
pub fn verify_vaa(&mut self, vaa: String) -> u32 {
log!(
r#"
{{
"standard": "pyth",
"version": "1.0",
"event": "Wormhole::verify_vaa",
"data": {{
"vaa": "{}"
}}
}}
"#,
vaa,
);
0
}
}