[near] Fix governance inconsistencies. (#453)

* near: update governance inconsistencies
* near: add small script for automated testing
* near: address feedback and update tests
* near: cannot trust outcome(), use failure len
This commit is contained in:
Reisen 2023-01-13 16:20:32 +00:00 committed by GitHub
parent 0e55d0808d
commit 98db3eca10
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 4377 additions and 285 deletions

View File

@ -0,0 +1,2 @@
# Ignore *wasm build artifacts which may enter the directory for workspaces tests.
*.wasm

3320
target-chains/near/receiver/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -13,9 +13,13 @@ crate-type = ["cdylib", "lib"]
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" }
nom = { version = "7.1.2" }
num-traits = { version = "0.2.15" }
num-derive = { version = "0.3.3" }
p2w-sdk = { path = "../../../third_party/pyth/p2w-sdk/rust" }
pyth-sdk = { version = "0.7.0" }
serde_wormhole = { git = "https://github.com/wormhole-foundation/wormhole" }
strum = { version = "0.24.1", features = ["derive"] }
thiserror = { version = "1.0.38" }
wormhole-core = { git = "https://github.com/wormhole-foundation/wormhole" }

View File

@ -6,6 +6,19 @@ use {
thiserror::Error,
};
/// Small macro for throwing errors in the contract when a boolean condition is not met.
///
/// It would be nice to have anyhow::ensure!() here, but the contract acts as a library and a
/// concrete error type is a better API for this case.
#[macro_export]
macro_rules! ensure {
($cond:expr, $err:expr) => {
if !$cond {
return Err($err);
}
};
}
#[derive(Error, Debug, Serialize, FunctionError)]
#[serde(crate = "near_sdk::serde")]
pub enum Error {
@ -18,10 +31,16 @@ pub enum Error {
#[error("A VAA payload could not be deserialized.")]
InvalidPayload,
#[error("Governance Module ID not valid.")]
InvalidGovernanceModule,
#[error("Governance Module Action not valid.")]
InvalidGovernanceAction,
#[error("Source for attestation is not allowed.")]
UnknownSource,
#[error("Unauthorized Upgrade")]
#[error("Unauthorized Upgrade.")]
UnauthorizedUpgrade,
#[error("Insufficient tokens deposited to cover storage.")]
@ -32,6 +51,12 @@ pub enum Error {
#[error("Fee is too large.")]
FeeTooLarge,
#[error("Arithmetic overflow.")]
ArithmeticOverflow,
#[error("Unknown error.")]
Unknown,
}
/// Convert IO errors into Payload errors, the only I/O we do is parsing with `Cursor` so this is a
@ -41,3 +66,10 @@ impl From<std::io::Error> for Error {
Error::InvalidPayload
}
}
/// Convert `nom` errors into local crate `InvalidPayload` errors.
impl From<nom::Err<nom::error::Error<&[u8]>>> for Error {
fn from(_: nom::Err<nom::error::Error<&[u8]>>) -> Self {
Error::InvalidPayload
}
}

View File

@ -2,7 +2,11 @@
use {
crate::{
error::Error,
ensure,
error::Error::{
self,
*,
},
ext::ext_wormhole,
state::{
Chain,
@ -12,11 +16,6 @@ use {
Pyth,
PythExt,
},
byteorder::{
BigEndian,
ReadBytesExt,
WriteBytesExt,
},
near_sdk::{
borsh::{
self,
@ -34,162 +33,218 @@ use {
Gas,
Promise,
},
std::io::Read,
num_traits::FromPrimitive,
strum::EnumDiscriminants,
wormhole::Chain as WormholeChain,
};
/// Magic Header for identifying Governance VAAs.
const GOVERNANCE_MAGIC: [u8; 4] = [0x50, 0x54, 0x47, 0x4d];
const GOVERNANCE_MAGIC: [u8; 4] = *b"PTGM";
/// ID for the module this contract identifies as: Pyth Receiver (0x1).
const GOVERNANCE_MODULE: u8 = 0x01;
/// Enumeration of IDs for different governance actions.
/// The type of contract that can accept a governance instruction.
#[derive(
BorshDeserialize,
BorshSerialize,
Clone,
Copy,
Debug,
Deserialize,
Eq,
PartialEq,
Serialize,
num_derive::FromPrimitive,
num_derive::ToPrimitive,
)]
#[serde(crate = "near_sdk::serde")]
#[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
}
pub enum GovernanceModule {
/// The PythNet executor contract
Executor = 0,
/// A target chain contract (like this one!)
Target = 1,
}
/// A `GovernanceAction` represents the different actions that can be voted on and executed by the
/// governance system.
#[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize)]
///
/// [ref:chain_structure] This type uses a [u8; 32] for contract upgrades which differs from other
/// chains, see the reference for more details.
///
/// [ref:action_discriminants] The discriminants for this enum are duplicated into a separate enum
/// containing only the discriminants with no fields called `GovernanceActionId`. This allow for
/// type-safe matching IDs during deserialization. When new actions are added, this will force the
/// developer to update the parser.
#[derive(
BorshDeserialize,
BorshSerialize,
Debug,
Deserialize,
EnumDiscriminants,
Eq,
PartialEq,
Serialize,
)]
#[strum_discriminants(derive(num_derive::ToPrimitive, num_derive::FromPrimitive))]
#[strum_discriminants(name(GovernanceActionId))]
#[serde(crate = "near_sdk::serde")]
pub enum GovernanceAction {
ContractUpgrade([u8; 32]),
SetDataSources(Vec<Source>),
SetGovernanceSource(Source),
SetStalePriceThreshold(u64),
SetUpdateFee(u64),
UpgradeContract { codehash: [u8; 32] },
AuthorizeGovernanceDataSourceTransfer { claim_vaa: Vec<u8> },
SetDataSources { data_sources: Vec<Source> },
SetFee { base: u64, expo: u64 },
SetValidPeriod { valid_seconds: u64 },
RequestGovernanceDataSourceTransfer { governance_data_source_index: u32 },
}
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,
}
}
#[derive(BorshDeserialize, BorshSerialize, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(crate = "near_sdk::serde")]
pub struct GovernanceInstruction {
pub module: GovernanceModule,
pub action: GovernanceAction,
pub target: Chain,
}
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>()?;
impl GovernanceInstruction {
/// Implements a `deserialize` method for the `GovernanceAction` enum using `nom` to
/// deserialize the payload. The use of `nom` gives us parser safety, error handling, full
/// buffer consumption, and a more readable implementation while staying efficient.
pub fn deserialize(input: impl AsRef<[u8]>) -> Result<Self, Error> {
use nom::{
bytes::complete::take,
combinator::all_consuming,
multi::length_count,
number::complete::{
be_u16,
be_u32,
be_u64,
be_u8,
},
};
assert!(module == GOVERNANCE_MODULE);
assert!(target == 0 || target == u16::from(WormholeChain::Near));
assert!(magic == u32::from_le_bytes(GOVERNANCE_MAGIC));
let input = input.as_ref();
Ok(match action {
ActionId::ContractUpgrade => {
let mut hash = [0u8; 32];
cursor.read_exact(&mut hash)?;
Self::ContractUpgrade(hash)
}
// Verify Governance header is as expected so we can bail to avoid more parsing.
let (input, magic) = take(4usize)(input)?;
let (input, module) = be_u8(input)?;
let (input, action) = be_u8(input)?;
let (input, chain) = be_u16(input)?;
let module = GovernanceModule::from_u8(module).ok_or(InvalidGovernanceModule)?;
let chain = Chain::from(WormholeChain::from(chain));
ActionId::SetDataSources => {
let mut sources = Vec::new();
let count = cursor.read_u8()?;
// Safely parse the action ID. [ref:action_discriminants]
let action = GovernanceActionId::from_u8(action).ok_or(InvalidGovernanceAction)?;
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>()?,
)),
});
ensure!(magic == GOVERNANCE_MAGIC, InvalidGovernanceModule);
ensure!(module == GovernanceModule::Target, InvalidGovernanceModule);
Ok(GovernanceInstruction {
module,
target: chain,
action: match action {
GovernanceActionId::UpgradeContract => {
let (_input, bytes) = all_consuming(take(32usize))(input)?;
let mut codehash = [0u8; 32];
codehash.copy_from_slice(bytes);
GovernanceAction::UpgradeContract { codehash }
}
Self::SetDataSources(sources)
}
GovernanceActionId::AuthorizeGovernanceDataSourceTransfer => {
let (_input, claim_vaa) = all_consuming(take(input.len()))(input)?;
GovernanceAction::AuthorizeGovernanceDataSourceTransfer {
claim_vaa: claim_vaa.to_vec(),
}
}
ActionId::SetGovernanceSource => {
let mut emitter = [0u8; 32];
cursor.read_exact(&mut emitter)?;
Self::SetGovernanceSource(Source {
emitter,
pyth_emitter_chain: Chain(cursor.read_u16::<BigEndian>()?),
})
}
GovernanceActionId::SetDataSources => {
let (_input, data_sources) = all_consuming(length_count(be_u8, |input| {
let (input, chain) = be_u16(input)?;
let (input, bytes) = take(32usize)(input)?;
let chain = Chain::from(WormholeChain::from(chain));
let mut emitter = [0u8; 32];
emitter.copy_from_slice(bytes);
Ok((input, Source { chain, emitter }))
}))(input)?;
GovernanceAction::SetDataSources { data_sources }
}
ActionId::SetStalePriceThreshold => {
let stale_price_threshold = cursor.read_u64::<BigEndian>()?;
Self::SetStalePriceThreshold(stale_price_threshold)
}
GovernanceActionId::SetFee => {
let (_input, (val, expo)) = all_consuming(|input| {
let (input, val) = be_u64(input)?;
let (input, expo) = be_u64(input)?;
Ok((input, (val, expo)))
})(input)?;
GovernanceAction::SetFee { base: val, expo }
}
ActionId::SetUpdateFee => {
let update_fee = cursor.read_u64::<BigEndian>()?;
Self::SetUpdateFee(update_fee)
}
GovernanceActionId::SetValidPeriod => {
let (_input, valid_seconds) = all_consuming(be_u64)(input)?;
GovernanceAction::SetValidPeriod { valid_seconds }
}
GovernanceActionId::RequestGovernanceDataSourceTransfer => {
let (_input, governance_data_source_index) = all_consuming(be_u32)(input)?;
GovernanceAction::RequestGovernanceDataSourceTransfer {
governance_data_source_index,
}
}
},
})
}
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());
/// Implements a `serialize` method for the `GovernanceAction` enum. The `nom` library doesn't
/// provide serialization but serialization is a safer operation, so we can just use a simple
/// push buffer to serialize.
pub fn serialize(&self) -> Result<Vec<u8>, Error> {
let mut buf = Vec::new();
buf.extend_from_slice(&GOVERNANCE_MAGIC);
buf.push(self.module as u8);
match self {
Self::ContractUpgrade(hash) => {
data.extend_from_slice(hash);
match &self.action {
GovernanceAction::UpgradeContract { codehash } => {
buf.push(GovernanceActionId::UpgradeContract as u8);
buf.extend_from_slice(&u16::from(self.target).to_be_bytes());
buf.extend_from_slice(codehash);
}
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());
GovernanceAction::AuthorizeGovernanceDataSourceTransfer { claim_vaa } => {
buf.push(GovernanceActionId::AuthorizeGovernanceDataSourceTransfer as u8);
buf.extend_from_slice(&u16::from(self.target).to_be_bytes());
buf.extend_from_slice(claim_vaa);
}
GovernanceAction::SetDataSources { data_sources } => {
buf.push(GovernanceActionId::SetDataSources as u8);
buf.extend_from_slice(&u16::from(self.target).to_be_bytes());
buf.push(u8::try_from(data_sources.len()).map_err(|_| InvalidPayload)?);
for source in data_sources {
buf.extend_from_slice(&(u16::from(source.chain).to_be_bytes()));
buf.extend_from_slice(&source.emitter);
}
}
Self::SetGovernanceSource(source) => {
data.extend_from_slice(&source.emitter);
data.extend_from_slice(&source.pyth_emitter_chain.0.to_le_bytes());
GovernanceAction::SetFee { base: val, expo } => {
buf.push(GovernanceActionId::SetFee as u8);
buf.extend_from_slice(&u16::from(self.target).to_be_bytes());
buf.extend_from_slice(&val.to_be_bytes());
buf.extend_from_slice(&expo.to_be_bytes());
}
Self::SetStalePriceThreshold(stale_price_threshold) => {
data.extend_from_slice(&stale_price_threshold.to_le_bytes());
GovernanceAction::SetValidPeriod { valid_seconds } => {
buf.push(GovernanceActionId::SetValidPeriod as u8);
buf.extend_from_slice(&u16::from(self.target).to_be_bytes());
buf.extend_from_slice(&valid_seconds.to_be_bytes());
}
Self::SetUpdateFee(update_fee) => {
data.extend_from_slice(&update_fee.to_le_bytes());
GovernanceAction::RequestGovernanceDataSourceTransfer {
governance_data_source_index,
} => {
buf.push(GovernanceActionId::RequestGovernanceDataSourceTransfer as u8);
buf.extend_from_slice(&u16::from(self.target).to_be_bytes());
buf.extend_from_slice(&governance_data_source_index.to_be_bytes());
}
}
data
Ok(buf)
}
}
@ -205,33 +260,28 @@ impl Pyth {
// 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 = hex::decode(&vaa).map_err(|_| InvalidHex)?;
let vaa = serde_wormhole::from_slice_with_payload::<wormhole::Vaa<()>>(&vaa);
let vaa = vaa.map_err(|_| Error::InvalidVaa)?;
let vaa = vaa.map_err(|_| InvalidVaa)?;
let (vaa, _rest) = vaa;
// Convert to local VAA type to catch APi changes.
// 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);
}
ensure!(
self.executed_governance_vaa < vaa.sequence,
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);
ensure!(
self.gov_source
== (Source {
emitter: vaa.emitter_address,
chain: vaa.emitter_chain,
}),
UnknownSource
);
}
// Verify VAA and refund the caller in case of failure.
@ -254,6 +304,10 @@ impl Pyth {
}
/// Invoke handler upon successful verification of a VAA action.
///
/// IMPORTANT: These functions should be idempotent otherwise NEAR's async model would allow
/// for two VAA's to run in parallel before the sequence is updated. Another fix for this would
/// be to pass the previous index and update during failure.
#[payable]
#[private]
#[handle_result]
@ -265,9 +319,7 @@ impl Pyth {
) -> Result<(), Error> {
use GovernanceAction::*;
if !is_promise_success() {
return Err(Error::VaaVerificationFailed);
}
ensure!(is_promise_success(), VaaVerificationFailed);
// Get Storage Usage before execution.
let storage = env::storage_usage();
@ -275,18 +327,40 @@ impl Pyth {
// 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)?;
let vaa = hex::decode(vaa).map_err(|_| InvalidPayload)?;
let (vaa, rest): (wormhole::Vaa<()>, _) =
serde_wormhole::from_slice_with_payload(&vaa).map_err(|_| 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),
// Deserialize and verify the action is destined for this chain.
let instruction = GovernanceInstruction::deserialize(rest)?;
ensure!(
instruction.target == Chain::from(WormholeChain::Near)
|| instruction.target == Chain::from(WormholeChain::Any),
InvalidPayload
);
match GovernanceInstruction::deserialize(rest)?.action {
SetDataSources { data_sources } => self.set_sources(data_sources),
SetFee { base, expo } => self.set_update_fee(base, expo)?,
SetValidPeriod { valid_seconds } => self.set_valid_period(valid_seconds),
RequestGovernanceDataSourceTransfer { .. } => Err(InvalidPayload)?,
AuthorizeGovernanceDataSourceTransfer { claim_vaa } => {
self.authorize_gov_source_transfer(claim_vaa)?
}
UpgradeContract { codehash } => {
// Additionally restrict to only Near for upgrades. This is a safety measure to
// prevent accidental upgrades to the wrong contract.
ensure!(
instruction.target == Chain::from(WormholeChain::Near),
InvalidPayload
);
self.set_upgrade_hash(codehash)
}
}
self.executed_governance_vaa = vaa.sequence;
// Refund storage difference to `account_id` after storage execution.
self.refund_storage_usage(
account_id,
@ -312,18 +386,67 @@ impl Pyth {
}
#[private]
pub fn set_gov_source(&mut self, source: Source) {
self.gov_source = source;
#[handle_result]
pub fn authorize_gov_source_transfer(&mut self, claim_vaa: Vec<u8>) -> Result<(), Error> {
let (vaa, rest): (wormhole::Vaa<()>, _) =
serde_wormhole::from_slice_with_payload(&claim_vaa).expect("Failed to deserialize VAA");
// Convert to local VAA type to catch API changes.
let vaa = Vaa::from(vaa);
// Parse GovernanceInstruction from Payload.
let instruction =
GovernanceInstruction::deserialize(rest).expect("Failed to deserialize action");
// Execute the embedded VAA action.
match instruction.action {
GovernanceAction::RequestGovernanceDataSourceTransfer {
governance_data_source_index,
} => {
ensure!(
self.executed_governance_change_vaa < governance_data_source_index as u64,
Unknown
);
// Additionally restrict to only Near for Authorizations.
ensure!(
instruction.target == Chain::from(WormholeChain::Near)
|| instruction.target == Chain::from(WormholeChain::Any),
InvalidPayload
);
self.executed_governance_change_vaa = governance_data_source_index as u64;
// Update Governance Source
self.gov_source = Source {
emitter: vaa.emitter_address,
chain: vaa.emitter_chain,
};
}
_ => Err(Unknown)?,
}
Ok(())
}
#[private]
pub fn set_stale_price_threshold(&mut self, threshold: u64) {
pub fn set_valid_period(&mut self, threshold: u64) {
self.stale_threshold = threshold;
}
#[private]
pub fn set_update_fee(&mut self, fee: u64) {
self.update_fee = fee;
#[handle_result]
pub fn set_update_fee(&mut self, fee: u64, expo: u64) -> Result<(), Error> {
self.update_fee = (fee as u128)
.checked_mul(
10_u128
.checked_pow(u32::try_from(expo).map_err(|_| ArithmeticOverflow)?)
.ok_or(ArithmeticOverflow)?,
)
.ok_or(ArithmeticOverflow)?;
Ok(())
}
#[private]
@ -347,10 +470,7 @@ impl Pyth {
#[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);
}
ensure!(signature == self.codehash, UnauthorizedUpgrade);
Ok(Promise::new(env::current_account_id())
.deploy_contract(new_code)
@ -380,6 +500,223 @@ impl Pyth {
fn is_valid_governance_source(&self, source: &Source) -> Result<(), Error> {
(self.gov_source == *source)
.then_some(())
.ok_or(Error::UnknownSource)
.ok_or(UnknownSource)
}
}
#[cfg(test)]
mod tests {
use {
super::*,
crate::governance::GovernanceActionId,
near_sdk::{
test_utils::{
accounts,
VMContextBuilder,
},
testing_env,
},
std::io::{
Cursor,
Write,
},
};
fn get_context() -> VMContextBuilder {
let mut context = VMContextBuilder::new();
context
.current_account_id(accounts(0))
.signer_account_id(accounts(0))
.predecessor_account_id(accounts(0))
.attached_deposit(0)
.is_view(false);
context
}
#[test]
fn test_upgrade() {
let mut context = get_context();
context.is_view(false);
testing_env!(context.build());
let mut contract = Pyth::new(
near_sdk::AccountId::new_unchecked("pyth.near".to_owned()),
[0; 32],
Source::default(),
Source::default(),
0.into(),
32,
);
contract.codehash = env::sha256(&[1, 2, 3]).try_into().unwrap();
contract.upgrade(vec![1, 2, 3]).expect("Upgrade failed");
}
#[test]
#[should_panic(expected = "UnauthorizedUpgrade")]
fn test_upgrade_fail() {
let mut context = get_context();
context.is_view(false);
testing_env!(context.build());
let mut contract = Pyth::new(
near_sdk::AccountId::new_unchecked("pyth.near".to_owned()),
[0; 32],
Source::default(),
Source::default(),
0.into(),
32,
);
contract.codehash = env::sha256(&[1, 2, 3]).try_into().unwrap();
contract.upgrade(vec![1, 2, 3, 4]).expect("Upgrade failed");
}
#[test]
fn test_set_valid_period() {
let mut context = get_context();
context.is_view(false);
testing_env!(context.build());
let mut contract = Pyth::new(
near_sdk::AccountId::new_unchecked("pyth.near".to_owned()),
[0; 32],
Source::default(),
Source::default(),
0.into(),
32,
);
contract.set_valid_period(100);
assert_eq!(contract.stale_threshold, 100);
}
#[test]
fn test_set_update_fee() {
let mut context = get_context();
context.is_view(false);
testing_env!(context.build());
let mut contract = Pyth::new(
near_sdk::AccountId::new_unchecked("pyth.near".to_owned()),
[0; 32],
Source::default(),
Source::default(),
0.into(),
32,
);
contract.set_update_fee(100, 2).expect("Failed to set fee");
assert_eq!(contract.update_fee, 10000);
}
#[test]
fn test_governance_serialize_matches_deserialize() {
// We match on the GovernanceActionId so that when new variants are added the test is
// forced to be updated. There's nothing special about SetFee we just need a concrete value
// to match on.
match GovernanceActionId::SetFee {
GovernanceActionId::SetValidPeriod => {
let instruction = GovernanceInstruction {
module: GovernanceModule::Target,
target: Chain::from(WormholeChain::Near),
action: GovernanceAction::SetValidPeriod { valid_seconds: 100 },
};
assert_eq!(
instruction,
GovernanceInstruction::deserialize(instruction.serialize().unwrap()).unwrap()
);
}
GovernanceActionId::SetDataSources => {
let instruction = GovernanceInstruction {
module: GovernanceModule::Target,
target: Chain::from(WormholeChain::Near),
action: GovernanceAction::SetDataSources {
data_sources: vec![Source::default()],
},
};
assert_eq!(
instruction,
GovernanceInstruction::deserialize(instruction.serialize().unwrap()).unwrap()
);
}
GovernanceActionId::SetFee => {
let instruction = GovernanceInstruction {
module: GovernanceModule::Target,
target: Chain::from(WormholeChain::Near),
action: GovernanceAction::SetFee {
base: 100,
expo: 100,
},
};
assert_eq!(
instruction,
GovernanceInstruction::deserialize(instruction.serialize().unwrap()).unwrap()
);
}
GovernanceActionId::UpgradeContract => {
let instruction = GovernanceInstruction {
module: GovernanceModule::Target,
target: Chain::from(WormholeChain::Near),
action: GovernanceAction::UpgradeContract { codehash: [1; 32] },
};
assert_eq!(
instruction,
GovernanceInstruction::deserialize(instruction.serialize().unwrap()).unwrap()
);
}
GovernanceActionId::AuthorizeGovernanceDataSourceTransfer => {
let vaa = {
let vaa = wormhole::Vaa {
emitter_chain: wormhole::Chain::Any,
emitter_address: wormhole::Address([0; 32]),
sequence: 1,
payload: (),
..Default::default()
};
let mut cur = Cursor::new(Vec::new());
serde_wormhole::to_writer(&mut cur, &vaa).expect("Failed to serialize VAA");
cur.write_all(
&GovernanceInstruction {
target: Chain::from(WormholeChain::Near),
module: GovernanceModule::Target,
action: GovernanceAction::RequestGovernanceDataSourceTransfer {
governance_data_source_index: 1,
},
}
.serialize()
.unwrap(),
)
.expect("Failed to write Payload");
cur.into_inner()
};
let instruction = GovernanceInstruction {
module: GovernanceModule::Target,
target: Chain::from(WormholeChain::Near),
action: GovernanceAction::AuthorizeGovernanceDataSourceTransfer {
claim_vaa: vaa,
},
};
assert_eq!(
instruction,
GovernanceInstruction::deserialize(instruction.serialize().unwrap()).unwrap()
);
}
GovernanceActionId::RequestGovernanceDataSourceTransfer => {
unimplemented!()
}
}
}
}

View File

@ -15,6 +15,7 @@ use {
},
env,
is_promise_success,
json_types::U128,
log,
near_bindgen,
AccountId,
@ -47,7 +48,6 @@ pub mod tests;
enum StorageKeys {
Source,
Prices,
Governance,
}
/// The `State` contains all persisted state for the contract. This includes runtime configuration.
@ -63,8 +63,11 @@ pub struct Pyth {
/// The Governance Source.
gov_source: Source,
/// The last executed sequence number for governance actions.
executed_gov_sequences: UnorderedSet<u64>,
/// The last executed sequence number for all governance actions.
executed_governance_vaa: u64,
/// The last executed sequence number only for governance change actions.
executed_governance_change_vaa: u64,
/// A Mapping from PriceFeed ID to Price Info.
prices: UnorderedMap<PriceIdentifier, PriceFeed>,
@ -83,7 +86,7 @@ pub struct Pyth {
stale_threshold: Duration,
/// Fee for updating price.
update_fee: u64,
update_fee: u128,
}
#[near_bindgen]
@ -95,7 +98,7 @@ impl Pyth {
codehash: [u8; 32],
initial_source: Source,
gov_source: Source,
update_fee: u64,
update_fee: U128,
stale_threshold: u64,
) -> Self {
// Add an initial Source so that the contract can be used.
@ -103,13 +106,14 @@ impl Pyth {
sources.insert(&initial_source);
Self {
prices: UnorderedMap::new(StorageKeys::Prices),
executed_gov_sequences: UnorderedSet::new(StorageKeys::Governance),
executed_governance_vaa: 0,
executed_governance_change_vaa: 0,
stale_threshold,
gov_source,
sources,
wormhole,
codehash,
update_fee,
update_fee: update_fee.into(),
}
}
@ -137,8 +141,8 @@ impl Pyth {
let vaa = Vaa::from(vaa);
if !self.sources.contains(&Source {
emitter: vaa.emitter_address,
pyth_emitter_chain: vaa.emitter_chain,
emitter: vaa.emitter_address,
chain: vaa.emitter_chain,
}) {
return Err(Error::UnknownSource);
}
@ -162,11 +166,19 @@ impl Pyth {
Ok(())
}
/// Return the deposit required to update a price feed.
pub fn get_update_fee_estimate(&self) -> u64 {
/// Return the deposit required to update a price feed. This is the upper limit for an update
/// call and any remaining deposit not consumed for storage will be refunded.
#[allow(unused_variables)]
pub fn get_update_fee_estimate(&self, vaa: String) -> U128 {
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
// The const multiplications here are to provide additional headway for any unexpected data
// costs in NEAR's storage calculations.
//
// 5 is the upper limit for PriceFeed amount in a single update.
// 4 is the value obtained through testing for headway.
(5u128 * 4u128 * data_cost + self.update_fee).into()
}
#[payable]
@ -183,10 +195,17 @@ impl Pyth {
}
// 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)?;
// forces the caller to add the required fee to the deposit. The protocol defines the fee
// as a u128, but storage is a u64, so we need to check that the fee does not overflow the
// storage cost as well.
let storage = (env::storage_usage() as u128)
.checked_sub(
self.update_fee
.checked_div(env::storage_byte_cost())
.ok_or(Error::ArithmeticOverflow)?,
)
.ok_or(Error::InsufficientDeposit)
.and_then(|s| u64::try_from(s).map_err(|_| Error::ArithmeticOverflow))?;
// 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

View File

@ -60,7 +60,6 @@ pub struct PriceFeed {
pub ema_price: Price,
}
// TODO: Source the Timestamp
impl From<&PriceAttestation> for PriceFeed {
fn from(price_attestation: &PriceAttestation) -> Self {
Self {
@ -88,6 +87,7 @@ impl From<&PriceAttestation> for PriceFeed {
BorshDeserialize,
BorshSerialize,
Clone,
Copy,
Debug,
Default,
Deserialize,
@ -99,14 +99,22 @@ impl From<&PriceAttestation> for PriceFeed {
)]
#[serde(crate = "near_sdk::serde")]
#[repr(transparent)]
pub struct Chain(pub u16);
pub struct Chain(u16);
/// Converts from a WormholeChain, rather than a u16. This lets us rely on Wormhole's SDK to
/// validate the chain identifier.
impl From<WormholeChain> for Chain {
fn from(chain: WormholeChain) -> Self {
Self(u16::from(chain))
}
}
impl From<Chain> for u16 {
fn from(chain: Chain) -> Self {
chain.0
}
}
/// 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
@ -126,8 +134,8 @@ impl From<WormholeChain> for Chain {
)]
#[serde(crate = "near_sdk::serde")]
pub struct Source {
pub emitter: WormholeAddress,
pub pyth_emitter_chain: Chain,
pub emitter: WormholeAddress,
pub chain: Chain,
}
/// A local `Vaa` type converted to from the Wormhole definition, this helps catch any upstream

View File

@ -19,7 +19,7 @@ mod tests {
[0; 32],
Source::default(),
Source::default(),
1,
1.into(),
32,
)
}

View File

@ -7,8 +7,13 @@ use {
PriceStatus,
},
pyth::{
governance::GovernanceAction,
governance::{
GovernanceAction,
GovernanceInstruction,
GovernanceModule,
},
state::{
Chain,
Price,
PriceIdentifier,
Source,
@ -19,6 +24,7 @@ use {
Cursor,
Write,
},
wormhole::Chain as WormholeChain,
};
async fn initialize_chain() -> (
@ -44,19 +50,20 @@ async fn initialize_chain() -> (
.expect("Failed to deploy wormhole_stub.wasm");
// Initialize Wormhole.
wormhole
let _ = wormhole
.call("new")
.args_json(&json!({}))
.gas(300_000_000_000_000)
.transact()
.transact_async()
.await
.expect("Failed to initialize Wormhole")
.await
.unwrap();
// Initialize Pyth, one time operation that sets the Wormhole contract address.
let codehash = [0u8; 32];
contract
let _ = contract
.call("new")
.args_json(&json!({
"wormhole": wormhole.id(),
@ -67,22 +74,24 @@ async fn initialize_chain() -> (
"stale_threshold": 32,
}))
.gas(300_000_000_000_000)
.transact()
.transact_async()
.await
.expect("Failed to initialize Pyth")
.await
.unwrap();
(worker, contract, wormhole)
}
#[tokio::test]
async fn test_source_add() {
async fn test_set_sources() {
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]),
sequence: 1,
payload: (),
..Default::default()
};
@ -91,14 +100,21 @@ async fn test_source_add() {
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),
&GovernanceInstruction {
target: Chain::from(WormholeChain::Any),
module: GovernanceModule::Target,
action: GovernanceAction::SetDataSources {
data_sources: vec![
Source::default(),
Source {
emitter: [1; 32],
chain: Chain::from(WormholeChain::Solana),
},
],
},
])
.serialize(),
}
.serialize()
.unwrap(),
)
.expect("Failed to write Payload");
hex::encode(cur.into_inner())
@ -111,11 +127,13 @@ async fn test_source_add() {
.args_json(&json!({
"vaa": vaa,
}))
.transact()
.transact_async()
.await
.expect("Failed to submit VAA")
.outcome()
.is_success());
.await
.unwrap()
.failures()
.is_empty());
// There should now be a two sources in the contract state.
assert_eq!(
@ -124,8 +142,8 @@ async fn test_source_add() {
&[
Source::default(),
Source {
emitter: [1; 32],
pyth_emitter_chain: pyth::state::Chain(1),
emitter: [1; 32],
chain: Chain::from(WormholeChain::Solana),
},
]
);
@ -140,18 +158,51 @@ async fn test_set_governance_source() {
emitter_chain: wormhole::Chain::Any,
emitter_address: wormhole::Address([0; 32]),
payload: (),
sequence: 2,
..Default::default()
};
let vaa = {
let request_vaa = wormhole::Vaa {
emitter_chain: wormhole::Chain::Solana,
emitter_address: wormhole::Address([1; 32]),
payload: (),
sequence: 1,
..Default::default()
};
// Data Source Upgrades are submitted with an embedded VAA, generate that one here first
// before we embed it.
let request_vaa = {
let mut cur = Cursor::new(Vec::new());
serde_wormhole::to_writer(&mut cur, &request_vaa).expect("Failed to serialize VAA");
cur.write_all(
&GovernanceInstruction {
target: Chain::from(WormholeChain::Near),
module: GovernanceModule::Target,
action: GovernanceAction::RequestGovernanceDataSourceTransfer {
governance_data_source_index: 1,
},
}
.serialize()
.unwrap(),
)
.expect("Failed to write Payload");
cur.into_inner()
};
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(),
&GovernanceInstruction {
target: Chain::from(WormholeChain::Near),
module: GovernanceModule::Target,
action: GovernanceAction::AuthorizeGovernanceDataSourceTransfer {
claim_vaa: request_vaa,
},
}
.serialize()
.unwrap(),
)
.expect("Failed to write Payload");
hex::encode(cur.into_inner())
@ -164,15 +215,17 @@ async fn test_set_governance_source() {
.args_json(&json!({
"vaa": vaa,
}))
.transact()
.transact_async()
.await
.expect("Failed to submit VAA")
.outcome()
.is_success());
.await
.unwrap()
.failures()
.is_empty());
// An action from the new source should now be accepted.
let vaa = wormhole::Vaa {
sequence: 1, // NOTE: Incremented Governance Sequence
sequence: 3, // NOTE: Incremented Governance Sequence
emitter_chain: wormhole::Chain::Solana,
emitter_address: wormhole::Address([1; 32]),
payload: (),
@ -183,14 +236,21 @@ async fn test_set_governance_source() {
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),
&GovernanceInstruction {
target: Chain::from(WormholeChain::Near),
module: GovernanceModule::Target,
action: GovernanceAction::SetDataSources {
data_sources: vec![
Source::default(),
Source {
emitter: [2; 32],
chain: Chain::from(WormholeChain::Solana),
},
],
},
])
.serialize(),
}
.serialize()
.unwrap(),
)
.expect("Failed to write Payload");
hex::encode(cur.into_inner())
@ -203,15 +263,17 @@ async fn test_set_governance_source() {
.args_json(&json!({
"vaa": vaa,
}))
.transact()
.transact_async()
.await
.expect("Failed to submit VAA")
.outcome()
.is_success());
.await
.unwrap()
.failures()
.is_empty());
// But not from the old source.
let vaa = wormhole::Vaa {
sequence: 1, // NOTE: Incremented Governance Sequence
sequence: 4, // NOTE: Incremented Governance Sequence
emitter_chain: wormhole::Chain::Any,
emitter_address: wormhole::Address([0; 32]),
payload: (),
@ -222,31 +284,40 @@ async fn test_set_governance_source() {
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),
&GovernanceInstruction {
target: Chain::from(WormholeChain::Near),
module: GovernanceModule::Target,
action: GovernanceAction::SetDataSources {
data_sources: vec![
Source::default(),
Source {
emitter: [2; 32],
chain: Chain::from(WormholeChain::Solana),
},
],
},
])
.serialize(),
}
.serialize()
.unwrap(),
)
.expect("Failed to write Payload");
hex::encode(cur.into_inner())
};
assert!(!contract
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()
.transact_async()
.await
.expect("Failed to submit VAA")
.await
.unwrap()
.outcome()
.is_failure());
.is_success());
}
#[tokio::test]
@ -258,6 +329,7 @@ async fn test_stale_threshold() {
emitter_chain: wormhole::Chain::Any,
emitter_address: wormhole::Address([0; 32]),
payload: (),
sequence: 1,
..Default::default()
};
@ -302,13 +374,16 @@ async fn test_stale_threshold() {
let update_fee = serde_json::from_slice::<U128>(
&contract
.view("get_update_fee_estimate")
.args(vec![])
.args_json(&json!({
"vaa": vaa,
}))
.await
.unwrap()
.result,
)
.unwrap();
// Submit price. As there are no prices this should succeed despite being old.
assert!(contract
.call("update_price_feed")
.gas(300_000_000_000_000)
@ -316,14 +391,16 @@ async fn test_stale_threshold() {
.args_json(&json!({
"vaa_hex": vaa,
}))
.transact()
.transact_async()
.await
.expect("Failed to submit VAA")
.outcome()
.is_success());
.await
.unwrap()
.failures()
.is_empty());
// Assert Price cannot be requested, 60 seconds in the past should be considered stale.
// [tag:failed_price_check]
// Despite succeeding, 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>>(
@ -337,11 +414,12 @@ async fn test_stale_threshold() {
.unwrap(),
);
// Submit another Price Attestation to the contract with an even older timestamp.
// Submit another Price Attestation to the contract with an even older timestamp. Which
// should now fail due to the existing newer price.
let vaa = wormhole::Vaa {
emitter_chain: wormhole::Chain::Any,
emitter_address: wormhole::Address([0; 32]),
sequence: 1,
sequence: 2,
payload: (),
..Default::default()
};
@ -376,7 +454,7 @@ async fn test_stale_threshold() {
hex::encode(cur.into_inner())
};
// The update handler should succeed even if price is old, but simply not update the price.
// The update handler should now succeed even if price is old, but simply not update the price.
assert!(contract
.call("update_price_feed")
.gas(300_000_000_000_000)
@ -384,11 +462,13 @@ async fn test_stale_threshold() {
.args_json(&json!({
"vaa_hex": vaa,
}))
.transact()
.transact_async()
.await
.expect("Failed to submit VAA")
.outcome()
.is_success());
.await
.unwrap()
.failures()
.is_empty());
// The price however should _not_ have updated and if we check the unsafe stored price the
// timestamp and price should be unchanged.
@ -414,7 +494,7 @@ async fn test_stale_threshold() {
let vaa = wormhole::Vaa {
emitter_chain: wormhole::Chain::Any,
emitter_address: wormhole::Address([0; 32]),
sequence: 2,
sequence: 3,
payload: (),
..Default::default()
};
@ -422,8 +502,16 @@ async fn test_stale_threshold() {
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();
cur.write_all(
&GovernanceInstruction {
target: Chain::from(WormholeChain::Near),
module: GovernanceModule::Target,
action: GovernanceAction::SetValidPeriod { valid_seconds: 256 },
}
.serialize()
.unwrap(),
)
.unwrap();
hex::encode(cur.into_inner())
};
@ -434,11 +522,13 @@ async fn test_stale_threshold() {
.args_json(&json!({
"vaa": vaa,
}))
.transact()
.transact_async()
.await
.expect("Failed to submit VAA")
.outcome()
.is_success());
.await
.unwrap()
.failures()
.is_empty());
// It should now be possible to request the price that previously returned None.
// [ref:failed_price_check]
@ -470,34 +560,45 @@ async fn test_contract_fees() {
.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: (),
sequence: 1,
..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();
cur.write_all(
&GovernanceInstruction {
target: Chain::from(WormholeChain::Near),
module: GovernanceModule::Target,
action: GovernanceAction::SetFee { base: 128, expo: 8 },
}
.serialize()
.unwrap(),
)
.unwrap();
hex::encode(cur.into_inner())
};
// Now set the update_fee too high for the deposit to cover.
// Fetch Update fee before changing it.
let update_fee = serde_json::from_slice::<U128>(
&contract
.view("get_update_fee_estimate")
.args_json(&json!({
"vaa": vaa,
}))
.await
.unwrap()
.result,
)
.unwrap();
// Now set the update_fee so that it is too high for the deposit to cover.
assert!(contract
.call("execute_governance_instruction")
.gas(300_000_000_000_000)
@ -505,30 +606,37 @@ async fn test_contract_fees() {
.args_json(&json!({
"vaa": vaa,
}))
.transact()
.transact_async()
.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,
)
.await
.unwrap()
.failures()
.is_empty());
// Check the state has actually changed before we try and execute another VAA.
assert_ne!(
u128::from(update_fee),
u128::from(
serde_json::from_slice::<U128>(
&contract
.view("get_update_fee_estimate")
.args_json(&json!({
"vaa": vaa,
}))
.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,
sequence: 2,
payload: (),
..Default::default()
};
@ -570,11 +678,13 @@ async fn test_contract_fees() {
.args_json(&json!({
"vaa_hex": vaa,
}))
.transact()
.transact_async()
.await
.expect("Failed to submit VAA")
.outcome()
.is_success());
.await
.unwrap()
.failures()
.is_empty());
// Submitting a Price should have failed because the fee was not enough.
assert_eq!(
@ -590,3 +700,243 @@ async fn test_contract_fees() {
.unwrap(),
);
}
// A test that attempts to SetFee twice with the same governance action, the first should succeed,
// the second should fail.
#[tokio::test]
async fn test_same_governance_sequence_fails() {
let (_, contract, _) = initialize_chain().await;
// 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: (),
sequence: 1,
..Default::default()
};
let vaa = {
let mut cur = Cursor::new(Vec::new());
serde_wormhole::to_writer(&mut cur, &vaa).unwrap();
cur.write_all(
&GovernanceInstruction {
target: Chain::from(WormholeChain::Near),
module: GovernanceModule::Target,
action: GovernanceAction::SetFee { base: 128, expo: 8 },
}
.serialize()
.unwrap(),
)
.unwrap();
hex::encode(cur.into_inner())
};
// Attempt our first SetFee.
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_async()
.await
.expect("Failed to submit VAA")
.await
.unwrap()
.failures()
.is_empty());
// Attempt to run the same VAA again.
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_async()
.await
.expect("Failed to submit VAA")
.await
.unwrap()
.failures()
.is_empty());
}
// A test that attempts to SetFee twice with the same governance action, the first should succeed,
// the second should fail.
#[tokio::test]
async fn test_out_of_order_sequences_fail() {
let (_, contract, _) = initialize_chain().await;
// 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: (),
sequence: 1,
..Default::default()
};
let vaa = {
let mut cur = Cursor::new(Vec::new());
serde_wormhole::to_writer(&mut cur, &vaa).unwrap();
cur.write_all(
&GovernanceInstruction {
target: Chain::from(WormholeChain::Near),
module: GovernanceModule::Target,
action: GovernanceAction::SetFee { base: 128, expo: 8 },
}
.serialize()
.unwrap(),
)
.unwrap();
hex::encode(cur.into_inner())
};
// Attempt our first SetFee.
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_async()
.await
.expect("Failed to submit VAA")
.await
.unwrap()
.failures()
.is_empty());
// Generate another VAA with sequence 3.
let vaa = wormhole::Vaa {
emitter_chain: wormhole::Chain::Any,
emitter_address: wormhole::Address([0; 32]),
payload: (),
sequence: 3,
..Default::default()
};
let vaa = {
let mut cur = Cursor::new(Vec::new());
serde_wormhole::to_writer(&mut cur, &vaa).unwrap();
cur.write_all(
&GovernanceInstruction {
target: Chain::from(WormholeChain::Near),
module: GovernanceModule::Target,
action: GovernanceAction::SetFee { base: 128, expo: 8 },
}
.serialize()
.unwrap(),
)
.unwrap();
hex::encode(cur.into_inner())
};
// This should succeed.
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_async()
.await
.expect("Failed to submit VAA")
.await
.unwrap()
.failures()
.is_empty());
// Generate another VAA with sequence 2.
let vaa = wormhole::Vaa {
emitter_chain: wormhole::Chain::Any,
emitter_address: wormhole::Address([0; 32]),
payload: (),
sequence: 2,
..Default::default()
};
let vaa = {
let mut cur = Cursor::new(Vec::new());
serde_wormhole::to_writer(&mut cur, &vaa).unwrap();
cur.write_all(
&GovernanceInstruction {
target: Chain::from(WormholeChain::Near),
module: GovernanceModule::Target,
action: GovernanceAction::SetFee { base: 128, expo: 8 },
}
.serialize()
.unwrap(),
)
.unwrap();
hex::encode(cur.into_inner())
};
// This should fail due to being out of order.
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_async()
.await
.expect("Failed to submit VAA")
.await
.unwrap()
.failures()
.is_empty());
}
// A test that fails if the governance action payload target is not NEAR.
#[tokio::test]
async fn test_governance_target_fails_if_not_near() {
let (_, contract, _) = initialize_chain().await;
let vaa = wormhole::Vaa {
emitter_chain: wormhole::Chain::Any,
emitter_address: wormhole::Address([0; 32]),
payload: (),
sequence: 1,
..Default::default()
};
let vaa = {
let mut cur = Cursor::new(Vec::new());
serde_wormhole::to_writer(&mut cur, &vaa).unwrap();
cur.write_all(
&GovernanceInstruction {
target: Chain::from(WormholeChain::Solana),
module: GovernanceModule::Target,
action: GovernanceAction::SetFee { base: 128, expo: 8 },
}
.serialize()
.unwrap(),
)
.unwrap();
hex::encode(cur.into_inner())
};
// This should fail as the target is Solana, when Near is expected.
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_async()
.await
.expect("Failed to submit VAA")
.await
.unwrap()
.failures()
.is_empty());
}

View File

@ -0,0 +1,20 @@
#!/usr/bin/env bash
#
# This script is used to prepare the environment in order to run the NEAR
# workspaces based tests. It relies on the relative position of the wormhole-
# stub contract to this directory.
set -euo pipefail
# Setup rust to build wasm.
rustup target add wasm32-unknown-unknown
cargo build --release --target wasm32-unknown-unknown
cp target/wasm32-unknown-unknown/release/pyth.wasm .
(
cd ../wormhole-stub
cargo build --release --target wasm32-unknown-unknown
cp target/wasm32-unknown-unknown/release/wormhole_stub.wasm ../receiver
)
RUST_LOG=info cargo nextest run