pyth-crosschain/target_chains/near/receiver/tests/workspaces.rs

943 lines
28 KiB
Rust

use {
near_sdk::json_types::U128,
pyth_wormhole_attester_sdk::{
BatchPriceAttestation,
Identifier,
PriceAttestation,
PriceStatus,
},
pyth::{
governance::{
GovernanceAction,
GovernanceInstruction,
GovernanceModule,
},
state::{
Chain,
Price,
PriceIdentifier,
Source,
},
},
serde_json::json,
std::io::{
Cursor,
Write,
},
wormhole::Chain as WormholeChain,
};
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.
let _ = wormhole
.call("new")
.args_json(&json!({}))
.gas(300_000_000_000_000)
.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];
let _ = 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_async()
.await
.expect("Failed to initialize Pyth")
.await
.unwrap();
(worker, contract, wormhole)
}
#[tokio::test]
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()
};
let vaa = {
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::Any),
module: GovernanceModule::Target,
action: GovernanceAction::SetDataSources {
data_sources: vec![
Source::default(),
Source {
emitter: [1; 32],
chain: Chain::from(WormholeChain::Solana),
},
],
},
}
.serialize()
.unwrap(),
)
.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_async()
.await
.expect("Failed to submit VAA")
.await
.unwrap()
.failures()
.is_empty());
// 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],
chain: Chain::from(WormholeChain::Solana),
},
]
);
}
#[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: (),
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(
&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())
};
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());
// An action from the new source should now be accepted.
let vaa = wormhole::Vaa {
sequence: 3, // 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(
&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()
.unwrap(),
)
.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_async()
.await
.expect("Failed to submit VAA")
.await
.unwrap()
.failures()
.is_empty());
// But not from the old source.
let vaa = wormhole::Vaa {
sequence: 4, // 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(
&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()
.unwrap(),
)
.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_async()
.await
.expect("Failed to submit VAA")
.await
.unwrap()
.outcome()
.is_success());
}
#[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: (),
sequence: 1,
..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_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)
.deposit(update_fee.into())
.args_json(&json!({
"vaa_hex": vaa,
}))
.transact_async()
.await
.expect("Failed to submit VAA")
.await
.unwrap()
.failures()
.is_empty());
// 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>>(
&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. 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: 2,
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 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)
.deposit(update_fee.into())
.args_json(&json!({
"vaa_hex": vaa,
}))
.transact_async()
.await
.expect("Failed to submit VAA")
.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.
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: 3,
payload: (),
..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::SetValidPeriod { valid_seconds: 256 },
}
.serialize()
.unwrap(),
)
.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_async()
.await
.expect("Failed to submit VAA")
.await
.unwrap()
.failures()
.is_empty());
// 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();
// 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())
};
// 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)
.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());
// 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: 2,
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_async()
.await
.expect("Failed to submit VAA")
.await
.unwrap()
.failures()
.is_empty());
// 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(),
);
}
// 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());
}