parent
378e99b345
commit
c92329c7c4
|
@ -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
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
);
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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
|
|
@ -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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue