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::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::>(&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::( &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::>( &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::( &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::>( &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::( &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::( &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::>( &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()); }