|
|
@@ -1,12 +1,14 @@
|
|
|
-use std::collections::HashMap;
|
|
|
-
|
|
|
use anchor_lang::{
|
|
|
prelude::{
|
|
|
+ AccountMeta,
|
|
|
+ ProgramError,
|
|
|
Pubkey,
|
|
|
Rent,
|
|
|
UpgradeableLoaderState,
|
|
|
},
|
|
|
solana_program::hash::Hash,
|
|
|
+ AccountDeserialize,
|
|
|
+ AnchorDeserialize,
|
|
|
AnchorSerialize,
|
|
|
InstructionData as AnchorInstructionData,
|
|
|
Key,
|
|
|
@@ -24,23 +26,36 @@ use solana_program_test::{
|
|
|
use solana_sdk::{
|
|
|
account::Account,
|
|
|
bpf_loader_upgradeable,
|
|
|
- instruction::Instruction,
|
|
|
+ instruction::{
|
|
|
+ Instruction,
|
|
|
+ InstructionError,
|
|
|
+ },
|
|
|
signature::Keypair,
|
|
|
signer::Signer,
|
|
|
stake_history::Epoch,
|
|
|
system_instruction,
|
|
|
- transaction::Transaction,
|
|
|
+ transaction::{
|
|
|
+ Transaction,
|
|
|
+ TransactionError,
|
|
|
+ },
|
|
|
};
|
|
|
+use std::collections::HashMap;
|
|
|
use wormhole::Chain;
|
|
|
use wormhole_solana::VAA;
|
|
|
|
|
|
-use crate::state::{
|
|
|
- governance_payload::{
|
|
|
- ExecutorPayload,
|
|
|
- GovernanceHeader,
|
|
|
- InstructionData,
|
|
|
+use crate::{
|
|
|
+ error::ExecutorError,
|
|
|
+ state::{
|
|
|
+ claim_record::ClaimRecord,
|
|
|
+ governance_payload::{
|
|
|
+ ExecutorPayload,
|
|
|
+ GovernanceHeader,
|
|
|
+ InstructionData,
|
|
|
+ },
|
|
|
+ posted_vaa::AnchorVaa,
|
|
|
},
|
|
|
- posted_vaa::AnchorVaa,
|
|
|
+ CLAIM_RECORD_SEED,
|
|
|
+ EXECUTOR_KEY_SEED,
|
|
|
};
|
|
|
|
|
|
/// Bench for the tests, the goal of this struct is to be able to setup solana accounts before starting the local validator
|
|
|
@@ -50,6 +65,17 @@ pub struct ExecutorBench {
|
|
|
seqno: HashMap<Pubkey, u64>,
|
|
|
}
|
|
|
|
|
|
+/// When passed to `add_vaa_account` modify the posted vaa in a way that makes the vaa invalid
|
|
|
+/// - `WrongOwner` : the owner is not the wormhole bridge
|
|
|
+/// - `WrongData` : data is random bytes
|
|
|
+/// - `WrongEmitterChain` : emitter chain of the vaa is ethereum
|
|
|
+pub enum VaaAttack {
|
|
|
+ None,
|
|
|
+ WrongOwner,
|
|
|
+ WrongData,
|
|
|
+ WrongEmitterChain,
|
|
|
+}
|
|
|
+
|
|
|
impl ExecutorBench {
|
|
|
/// Deploys the executor program as upgradable
|
|
|
pub fn new() -> ExecutorBench {
|
|
|
@@ -117,7 +143,22 @@ impl ExecutorBench {
|
|
|
}
|
|
|
|
|
|
/// Add VAA account with emitter and instructions for consumption by the remote_executor
|
|
|
- pub fn add_vaa_account(&mut self, emitter: &Pubkey, instructions: &Vec<Instruction>) -> Pubkey {
|
|
|
+ pub fn add_vaa_account(
|
|
|
+ &mut self,
|
|
|
+ emitter: &Pubkey,
|
|
|
+ instructions: &Vec<Instruction>,
|
|
|
+ validity: VaaAttack,
|
|
|
+ ) -> Pubkey {
|
|
|
+ let emitter_chain: u16 = match validity {
|
|
|
+ VaaAttack::WrongEmitterChain => Chain::Ethereum.into(),
|
|
|
+ _ => Chain::Solana.into(),
|
|
|
+ };
|
|
|
+
|
|
|
+ let owner: Pubkey = match validity {
|
|
|
+ VaaAttack::WrongOwner => Pubkey::new_unique(),
|
|
|
+ _ => AnchorVaa::owner(),
|
|
|
+ };
|
|
|
+
|
|
|
let payload = ExecutorPayload {
|
|
|
header: GovernanceHeader::executor_governance_header(),
|
|
|
instructions: instructions
|
|
|
@@ -136,7 +177,7 @@ impl ExecutorBench {
|
|
|
submission_time: 0,
|
|
|
nonce: 0,
|
|
|
sequence: self.seqno.get(&emitter).unwrap_or(&0) + 1,
|
|
|
- emitter_chain: Chain::Solana.into(),
|
|
|
+ emitter_chain,
|
|
|
emitter_address: emitter.to_bytes(),
|
|
|
payload: payload_bytes,
|
|
|
};
|
|
|
@@ -145,10 +186,15 @@ impl ExecutorBench {
|
|
|
|
|
|
let vaa_bytes = vaa.try_to_vec().unwrap();
|
|
|
|
|
|
+ let data: Vec<u8> = match validity {
|
|
|
+ VaaAttack::WrongData => (0..vaa_bytes.len()).map(|_| rand::random::<u8>()).collect(),
|
|
|
+ _ => vaa_bytes,
|
|
|
+ };
|
|
|
+
|
|
|
let vaa_account = Account {
|
|
|
- lamports: Rent::default().minimum_balance(vaa_bytes.len()),
|
|
|
- data: vaa_bytes,
|
|
|
- owner: AnchorVaa::owner(),
|
|
|
+ lamports: Rent::default().minimum_balance(data.len()),
|
|
|
+ data,
|
|
|
+ owner,
|
|
|
executable: false,
|
|
|
rent_epoch: Epoch::default(),
|
|
|
};
|
|
|
@@ -157,6 +203,24 @@ impl ExecutorBench {
|
|
|
self.program_test.add_account(vaa_pubkey, vaa_account);
|
|
|
return vaa_pubkey;
|
|
|
}
|
|
|
+
|
|
|
+ // Get executor key of an emitter, useful to construct instructions that will be in the VAA
|
|
|
+ pub fn get_executor_key(&self, emitter: &Pubkey) -> Pubkey {
|
|
|
+ Pubkey::find_program_address(
|
|
|
+ &[EXECUTOR_KEY_SEED.as_bytes(), &emitter.to_bytes()],
|
|
|
+ &self.program_id,
|
|
|
+ )
|
|
|
+ .0
|
|
|
+ }
|
|
|
+
|
|
|
+ // Get claim record of an emitter
|
|
|
+ pub fn get_claim_record_key(&self, emitter: &Pubkey) -> Pubkey {
|
|
|
+ Pubkey::find_program_address(
|
|
|
+ &[CLAIM_RECORD_SEED.as_bytes(), &emitter.to_bytes()],
|
|
|
+ &self.program_id,
|
|
|
+ )
|
|
|
+ .0
|
|
|
+ }
|
|
|
}
|
|
|
pub struct ExecutorSimulator {
|
|
|
banks_client: BanksClient,
|
|
|
@@ -165,8 +229,20 @@ pub struct ExecutorSimulator {
|
|
|
program_id: Pubkey,
|
|
|
}
|
|
|
|
|
|
+/// When passed to execute_posted_vaa, try to impersonate some of the accounts
|
|
|
+/// - WrongVaaAddress(Pubkey) : pass the VAA address specified
|
|
|
+/// - WrongEmptyClaimAddress : pass a claim_record address that is a PDA of the program but with the wrong seeds and also an empty account
|
|
|
+/// - WrongClaimAddress(Pubkey) : pass the claim_record specified
|
|
|
+/// - WrongSystemProgram : pass a random pubkey as the system program
|
|
|
+pub enum ExecutorAttack {
|
|
|
+ None,
|
|
|
+ WrongVaaAddress(Pubkey),
|
|
|
+ WrongEmptyClaimAddress,
|
|
|
+ WrongClaimAddress(Pubkey),
|
|
|
+ WrongSystemProgram,
|
|
|
+}
|
|
|
+
|
|
|
impl ExecutorSimulator {
|
|
|
- #[allow(dead_code)]
|
|
|
pub async fn airdrop(&mut self, to: &Pubkey, lamports: u64) -> Result<(), BanksClientError> {
|
|
|
let instruction = system_instruction::transfer(&self.payer.pubkey(), to, lamports);
|
|
|
|
|
|
@@ -192,7 +268,6 @@ impl ExecutorSimulator {
|
|
|
|
|
|
transaction.partial_sign(&[&self.payer], self.last_blockhash);
|
|
|
transaction.partial_sign(signers, self.last_blockhash);
|
|
|
-
|
|
|
self.banks_client.process_transaction(transaction).await
|
|
|
}
|
|
|
|
|
|
@@ -200,6 +275,8 @@ impl ExecutorSimulator {
|
|
|
pub async fn execute_posted_vaa(
|
|
|
&mut self,
|
|
|
posted_vaa_address: &Pubkey,
|
|
|
+ signers: &Vec<&Keypair>,
|
|
|
+ executor_attack: ExecutorAttack,
|
|
|
) -> Result<(), BanksClientError> {
|
|
|
let posted_vaa_data: VAA = self
|
|
|
.banks_client
|
|
|
@@ -207,7 +284,7 @@ impl ExecutorSimulator {
|
|
|
.await
|
|
|
.unwrap();
|
|
|
|
|
|
- let account_metas = crate::accounts::ExecutePostedVaa::populate(
|
|
|
+ let mut account_metas = crate::accounts::ExecutePostedVaa::populate(
|
|
|
&self.program_id,
|
|
|
&self.payer.pubkey(),
|
|
|
&Pubkey::new(&posted_vaa_data.emitter_address),
|
|
|
@@ -215,12 +292,88 @@ impl ExecutorSimulator {
|
|
|
)
|
|
|
.to_account_metas(None);
|
|
|
|
|
|
+ // ExecutorAttack overrides
|
|
|
+ match executor_attack {
|
|
|
+ ExecutorAttack::WrongVaaAddress(key) => account_metas[1].pubkey = key,
|
|
|
+ ExecutorAttack::WrongEmptyClaimAddress => {
|
|
|
+ account_metas[2].pubkey = Pubkey::find_program_address(
|
|
|
+ &[
|
|
|
+ CLAIM_RECORD_SEED.as_bytes(),
|
|
|
+ &Pubkey::new_unique().to_bytes(),
|
|
|
+ ],
|
|
|
+ &self.program_id,
|
|
|
+ )
|
|
|
+ .0
|
|
|
+ }
|
|
|
+ ExecutorAttack::WrongClaimAddress(key) => account_metas[2].pubkey = key,
|
|
|
+ ExecutorAttack::WrongSystemProgram => account_metas[3].pubkey = Pubkey::new_unique(),
|
|
|
+ _ => {}
|
|
|
+ };
|
|
|
+
|
|
|
+ let executor_payload: ExecutorPayload =
|
|
|
+ AnchorDeserialize::try_from_slice(posted_vaa_data.payload.as_slice()).unwrap();
|
|
|
+
|
|
|
+ let executor_key = Pubkey::find_program_address(
|
|
|
+ &[
|
|
|
+ EXECUTOR_KEY_SEED.as_bytes(),
|
|
|
+ &posted_vaa_data.emitter_address,
|
|
|
+ ],
|
|
|
+ &self.program_id,
|
|
|
+ )
|
|
|
+ .0;
|
|
|
+
|
|
|
+ // We need to add `executor_key` to the list of accounts
|
|
|
+ account_metas.push(AccountMeta {
|
|
|
+ pubkey: executor_key,
|
|
|
+ is_signer: false,
|
|
|
+ is_writable: true,
|
|
|
+ });
|
|
|
+
|
|
|
+ // Add the rest of `remaining_accounts` from parsing the payload
|
|
|
+ for instruction in executor_payload.instructions {
|
|
|
+ for account_meta in Instruction::from(&instruction).accounts {
|
|
|
+ if account_meta.pubkey != executor_key {
|
|
|
+ account_metas.push(account_meta.clone());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
let instruction = Instruction {
|
|
|
program_id: self.program_id,
|
|
|
accounts: account_metas,
|
|
|
data: crate::instruction::ExecutePostedVaa.data(),
|
|
|
};
|
|
|
|
|
|
- self.process_ix(instruction, &vec![]).await
|
|
|
+ self.process_ix(instruction, signers).await
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Get the account at `key`. Returns `None` if no such account exists.
|
|
|
+ pub async fn get_account(&mut self, key: Pubkey) -> Option<Account> {
|
|
|
+ self.banks_client.get_account(key).await.unwrap()
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Get claim record
|
|
|
+ #[allow(dead_code)]
|
|
|
+ pub async fn get_claim_record(&mut self, emitter: Pubkey) -> ClaimRecord {
|
|
|
+ let claim_record_key = Pubkey::find_program_address(
|
|
|
+ &[CLAIM_RECORD_SEED.as_bytes(), &emitter.to_bytes()],
|
|
|
+ &self.program_id,
|
|
|
+ )
|
|
|
+ .0;
|
|
|
+
|
|
|
+ let account = self.get_account(claim_record_key).await.unwrap();
|
|
|
+ ClaimRecord::try_deserialize(&mut account.data.as_slice()).unwrap()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl Into<TransactionError> for ExecutorError {
|
|
|
+ fn into(self) -> TransactionError {
|
|
|
+ TransactionError::InstructionError(
|
|
|
+ 0,
|
|
|
+ InstructionError::try_from(u64::from(ProgramError::from(
|
|
|
+ anchor_lang::prelude::Error::from(self),
|
|
|
+ )))
|
|
|
+ .unwrap(),
|
|
|
+ )
|
|
|
}
|
|
|
}
|