Pārlūkot izejas kodu

Better program (#297)

* Add tests

* Add tests to CI

* Remote test from precommit hook

* New test

* Remove merge error

* Comments and increase seqno

* Remove unnecesary dep

* Fix rebase

* Fix feedback

* Move anchor checks

* Fix comment

* Restore deny warnings

* Better comments

* Typo

* Improve comments

* Better comment

* Tests for the remote executor (#298)

* Tests

* Cleanup

* Comments

* Comments

* Tests complete
guibescos 3 gadi atpakaļ
vecāks
revīzija
6898b390e6

+ 1 - 0
pythnet/remote-executor/Cargo.lock

@@ -2713,6 +2713,7 @@ dependencies = [
  "anchor-lang",
  "bincode",
  "boolinator",
+ "rand 0.8.5",
  "solana-program-test",
  "solana-sdk",
  "tokio",

+ 1 - 0
pythnet/remote-executor/programs/remote-executor/Cargo.toml

@@ -29,3 +29,4 @@ solana-program-test = "=1.10.31"
 tokio = "1.14.1"
 solana-sdk = "=1.10.31"
 bincode = "1.3.3"
+rand = "0.8.5"

+ 20 - 25
pythnet/remote-executor/programs/remote-executor/src/lib.rs

@@ -6,17 +6,23 @@ use anchor_lang::{
     solana_program::borsh::get_packed_len,
     system_program,
 };
+use error::ExecutorError;
 use state::{
     claim_record::ClaimRecord,
     posted_vaa::AnchorVaa,
 };
+use wormhole::Chain::{
+    self,
+    Solana,
+};
 
 mod error;
 mod state;
 
-#[cfg(test)] //Conditional compilation of the tests
+#[cfg(test)]
 mod tests;
 
+//Anchor requires the program to declare its own id
 declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
 
 #[program]
@@ -25,32 +31,24 @@ pub mod remote_executor {
         instruction::Instruction,
         program::invoke_signed,
     };
-    use boolinator::Boolinator;
-    use wormhole::Chain::{
-        self,
-        Solana,
-    };
 
-    use crate::{
-        error::ExecutorError,
-        state::governance_payload::ExecutorPayload,
-    };
+    use crate::state::governance_payload::ExecutorPayload;
 
     use super::*;
 
     pub fn execute_posted_vaa(ctx: Context<ExecutePostedVaa>) -> Result<()> {
         let posted_vaa = &ctx.accounts.posted_vaa;
         let claim_record = &mut ctx.accounts.claim_record;
-
-        (Chain::from(posted_vaa.emitter_chain) == Solana)
-            .ok_or(error!(ExecutorError::EmitterChainNotSolana))?;
-        (posted_vaa.sequence > claim_record.sequence)
-            .ok_or(error!(ExecutorError::NonIncreasingSequence))?;
         claim_record.sequence = posted_vaa.sequence;
 
         let payload = ExecutorPayload::try_from_slice(&posted_vaa.payload)?;
         payload.check_header()?;
 
+        let (_, bump) = Pubkey::find_program_address(
+            &[EXECUTOR_KEY_SEED.as_bytes(), &posted_vaa.emitter_address],
+            &id(),
+        );
+
         for instruction in payload.instructions.iter().map(Instruction::from) {
             // TO DO: We currently pass `remaining_accounts` down to the CPIs, is there a more efficient way to do it?
             invoke_signed(
@@ -59,7 +57,7 @@ pub mod remote_executor {
                 &[&[
                     EXECUTOR_KEY_SEED.as_bytes(),
                     &posted_vaa.emitter_address,
-                    &[*ctx.bumps.get("executor_key").unwrap()],
+                    &[bump],
                 ]],
             )?;
         }
@@ -74,13 +72,16 @@ const CLAIM_RECORD_SEED: &str = "CLAIM_RECORD";
 pub struct ExecutePostedVaa<'info> {
     #[account(mut)]
     pub payer: Signer<'info>,
+    #[account(constraint = Chain::from(posted_vaa.emitter_chain) == Solana @ ExecutorError::EmitterChainNotSolana, constraint = posted_vaa.sequence > claim_record.sequence @ExecutorError::NonIncreasingSequence )]
     pub posted_vaa: Account<'info, AnchorVaa>,
-    #[account(seeds = [EXECUTOR_KEY_SEED.as_bytes(), &posted_vaa.emitter_address], bump)]
-    pub executor_key: UncheckedAccount<'info>,
-    /// The reason claim record is separated from executor_key is that executor key might need to pay in the CPI, so we want it to be a wallet
+    /// The reason claim_record has different seeds than executor_key is that executor key might need to pay in the CPI, so we want it to be a native wallet
     #[account(init_if_needed, space = 8 + get_packed_len::<ClaimRecord>(), payer=payer, seeds = [CLAIM_RECORD_SEED.as_bytes(), &posted_vaa.emitter_address], bump)]
     pub claim_record: Account<'info, ClaimRecord>,
     pub system_program: Program<'info, System>,
+    // Additional accounts passed to the instruction will be passed down to the CPIs. Very importantly executor_key needs to be passed as it will be the signer of the CPIs.
+    // Below is the "anchor specification" of that account
+    // #[account(seeds = [EXECUTOR_KEY_SEED.as_bytes(), &posted_vaa.emitter_address], bump)]
+    // pub executor_key: UncheckedAccount<'info>,
 }
 
 impl crate::accounts::ExecutePostedVaa {
@@ -90,11 +91,6 @@ impl crate::accounts::ExecutePostedVaa {
         emitter: &Pubkey,
         posted_vaa: &Pubkey,
     ) -> Self {
-        let executor_key = Pubkey::find_program_address(
-            &[EXECUTOR_KEY_SEED.as_bytes(), &emitter.to_bytes()],
-            program_id,
-        )
-        .0;
         let claim_record = Pubkey::find_program_address(
             &[CLAIM_RECORD_SEED.as_bytes(), &emitter.to_bytes()],
             program_id,
@@ -102,7 +98,6 @@ impl crate::accounts::ExecutePostedVaa {
         .0;
         crate::accounts::ExecutePostedVaa {
             payer: *payer,
-            executor_key,
             claim_record,
             posted_vaa: *posted_vaa,
             system_program: system_program::ID,

+ 172 - 19
pythnet/remote-executor/programs/remote-executor/src/tests/executor_simulator.rs

@@ -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(),
+        )
     }
 }

+ 2 - 1
pythnet/remote-executor/programs/remote-executor/src/tests/mod.rs

@@ -1,2 +1,3 @@
 mod executor_simulator;
-mod test_create_account;
+mod test_adversarial;
+mod test_basic_instructions;

+ 186 - 0
pythnet/remote-executor/programs/remote-executor/src/tests/test_adversarial.rs

@@ -0,0 +1,186 @@
+use crate::error::ExecutorError;
+
+use super::executor_simulator::{
+    ExecutorAttack,
+    ExecutorBench,
+    VaaAttack,
+};
+use anchor_lang::prelude::{
+    ErrorCode,
+    ProgramError,
+    Pubkey,
+    Rent,
+};
+use solana_sdk::{
+    instruction::InstructionError,
+    native_token::LAMPORTS_PER_SOL,
+    system_instruction::transfer,
+    transaction::TransactionError,
+};
+
+#[tokio::test]
+/// This test file tests that the executor fails (and checks the errors to make sure it fails for the right reason) when :
+/// - The VAA has a bad format
+/// - The VAA is not owned by the bridge
+/// - The VAA was not emitted by Solana
+/// - Another account is passed in place of the system program
+/// - A claim_record account not seeded by the emitter of the VAA is passed
+
+async fn test_adversarial() {
+    let mut bench = ExecutorBench::new();
+    let emitter = Pubkey::new_unique();
+    let emitter_2 = Pubkey::new_unique();
+    let executor_key = bench.get_executor_key(&emitter);
+    let claim_record_2 = bench.get_claim_record_key(&emitter_2);
+
+    let receiver = Pubkey::new_unique();
+
+    // Setup VAAs
+    let vaa_account_valid = bench.add_vaa_account(
+        &emitter,
+        &vec![transfer(
+            &executor_key,
+            &&receiver,
+            Rent::default().minimum_balance(0),
+        )],
+        VaaAttack::None,
+    );
+    let vaa_account_wrong_data = bench.add_vaa_account(
+        &emitter,
+        &vec![transfer(
+            &executor_key,
+            &&receiver,
+            Rent::default().minimum_balance(0),
+        )],
+        VaaAttack::WrongData,
+    );
+    let vaa_account_wrong_owner = bench.add_vaa_account(
+        &emitter,
+        &vec![transfer(
+            &executor_key,
+            &&receiver,
+            Rent::default().minimum_balance(0),
+        )],
+        VaaAttack::WrongOwner,
+    );
+    let vaa_account_wrong_emitter_chain = bench.add_vaa_account(
+        &emitter,
+        &vec![transfer(
+            &executor_key,
+            &&receiver,
+            Rent::default().minimum_balance(0),
+        )],
+        VaaAttack::WrongEmitterChain,
+    );
+
+    // The goal of this account is creating a claim_record that the attacker is going to try to use to impersonate
+    // the right claim_record
+    let vaa_account_valid_2 = bench.add_vaa_account(&emitter_2, &vec![], VaaAttack::None);
+
+    let mut sim = bench.start().await;
+    sim.airdrop(&executor_key, LAMPORTS_PER_SOL).await.unwrap();
+
+    // VAA with random bytes
+    assert_eq!(
+        sim.execute_posted_vaa(
+            &vaa_account_valid,
+            &vec![],
+            ExecutorAttack::WrongVaaAddress(vaa_account_wrong_data)
+        )
+        .await
+        .unwrap_err()
+        .unwrap(),
+        ErrorCode::AccountDidNotDeserialize.into_transation_error()
+    );
+
+    // VAA not owned by the wormhole bridge
+    assert_eq!(
+        sim.execute_posted_vaa(&vaa_account_wrong_owner, &vec![], ExecutorAttack::None)
+            .await
+            .unwrap_err()
+            .unwrap(),
+        ErrorCode::AccountOwnedByWrongProgram.into_transation_error()
+    );
+
+    // VAA not emitted by a soruce in Solana
+    assert_eq!(
+        sim.execute_posted_vaa(
+            &vaa_account_wrong_emitter_chain,
+            &vec![],
+            ExecutorAttack::None
+        )
+        .await
+        .unwrap_err()
+        .unwrap(),
+        ExecutorError::EmitterChainNotSolana.into()
+    );
+
+    // Claim record does not correspond to the emitter's claim record
+    // Next error is privilege scalation because anchor tries to create the account at wrong address but signing with the right seeds
+    assert_eq!(
+        sim.execute_posted_vaa(
+            &vaa_account_valid,
+            &vec![],
+            ExecutorAttack::WrongEmptyClaimAddress
+        )
+        .await
+        .unwrap_err()
+        .unwrap(),
+        InstructionError::PrivilegeEscalation.into_transation_error()
+    );
+
+    // Claim record does not correspond to the emitter's claim record, but this time it is initialized
+    sim.execute_posted_vaa(&vaa_account_valid_2, &vec![], ExecutorAttack::None)
+        .await
+        .unwrap();
+    assert_eq!(
+        sim.execute_posted_vaa(
+            &vaa_account_valid,
+            &vec![],
+            ExecutorAttack::WrongClaimAddress(claim_record_2)
+        )
+        .await
+        .unwrap_err()
+        .unwrap(),
+        ErrorCode::ConstraintSeeds.into_transation_error()
+    );
+
+    // System program impersonation
+    assert_eq!(
+        sim.execute_posted_vaa(
+            &vaa_account_valid,
+            &vec![],
+            ExecutorAttack::WrongSystemProgram
+        )
+        .await
+        .unwrap_err()
+        .unwrap(),
+        ErrorCode::InvalidProgramId.into_transation_error()
+    );
+
+    //Success!
+    sim.execute_posted_vaa(&vaa_account_valid, &vec![], ExecutorAttack::None)
+        .await
+        .unwrap()
+}
+
+pub trait IntoTransactionError {
+    fn into_transation_error(self) -> TransactionError;
+}
+
+impl IntoTransactionError for ErrorCode {
+    fn into_transation_error(self) -> TransactionError {
+        TransactionError::InstructionError(
+            0,
+            InstructionError::try_from(u64::from(ProgramError::from(
+                anchor_lang::prelude::Error::from(self),
+            )))
+            .unwrap(),
+        )
+    }
+}
+impl IntoTransactionError for InstructionError {
+    fn into_transation_error(self) -> TransactionError {
+        TransactionError::InstructionError(0, self)
+    }
+}

+ 150 - 0
pythnet/remote-executor/programs/remote-executor/src/tests/test_basic_instructions.rs

@@ -0,0 +1,150 @@
+use crate::{
+    error::ExecutorError,
+    tests::executor_simulator::{
+        ExecutorAttack,
+        VaaAttack,
+    },
+};
+
+use super::executor_simulator::ExecutorBench;
+use anchor_lang::{
+    prelude::{
+        Pubkey,
+        Rent,
+    },
+    solana_program::{
+        system_instruction::create_account,
+        system_program,
+    },
+};
+use solana_sdk::{
+    native_token::LAMPORTS_PER_SOL,
+    signature::Keypair,
+    signer::Signer,
+    system_instruction::transfer,
+};
+
+#[tokio::test]
+/// This test file tests that the executor can :
+/// - Execute instructions with multiple signers
+/// - Execute multiple instructions in one call
+/// - Protect against replay attacks by updating the claim_record
+async fn test_basic_instructions() {
+    let mut bench = ExecutorBench::new();
+
+    let emitter = Pubkey::new_unique();
+
+    let executor_key = bench.get_executor_key(&emitter);
+
+    let receiver = Keypair::new();
+    let receiver2 = Keypair::new();
+    let receiver3 = Keypair::new();
+    let receiver4 = Keypair::new();
+
+    let instruction1 = create_account(
+        &executor_key,
+        &receiver.pubkey(),
+        Rent::default().minimum_balance(10),
+        10,
+        &Pubkey::new_unique(),
+    );
+    let instruction2 = create_account(
+        &executor_key,
+        &receiver2.pubkey(),
+        Rent::default().minimum_balance(5),
+        5,
+        &Pubkey::new_unique(),
+    );
+    let instruction3 = transfer(
+        &executor_key,
+        &receiver3.pubkey(),
+        Rent::default().minimum_balance(0),
+    );
+    let instruction4 = transfer(
+        &executor_key,
+        &receiver4.pubkey(),
+        Rent::default().minimum_balance(0),
+    );
+
+    let vaa_account_create =
+        bench.add_vaa_account(&emitter, &vec![instruction1, instruction2], VaaAttack::None);
+    let vaa_account_transfer1 =
+        bench.add_vaa_account(&emitter, &vec![instruction3], VaaAttack::None);
+    let vaa_account_transfer2 =
+        bench.add_vaa_account(&emitter, &vec![instruction4], VaaAttack::None);
+
+    let mut sim = bench.start().await;
+
+    // All these accounts are unitialized before we execute the governance messages
+    let pre_account = sim.get_account(receiver.pubkey()).await;
+    let pre_account2 = sim.get_account(receiver2.pubkey()).await;
+    let pre_account3 = sim.get_account(receiver3.pubkey()).await;
+    let pre_account4 = sim.get_account(receiver4.pubkey()).await;
+    assert_eq!(pre_account, None);
+    assert_eq!(pre_account2, None);
+    assert_eq!(pre_account3, None);
+    assert_eq!(pre_account4, None);
+
+    sim.airdrop(&executor_key, LAMPORTS_PER_SOL).await.unwrap();
+
+    // Execute two account creations in 1 call to the remote executor
+    sim.execute_posted_vaa(
+        &vaa_account_create,
+        &vec![&receiver, &receiver2],
+        ExecutorAttack::None,
+    )
+    .await
+    .unwrap();
+
+    // Check state post call
+    let post_account = sim.get_account(receiver.pubkey()).await.unwrap();
+    assert_eq!(post_account.lamports, Rent::default().minimum_balance(10));
+    assert_eq!(post_account.data.len(), 10);
+
+    let post_account2 = sim.get_account(receiver2.pubkey()).await.unwrap();
+    assert_eq!(post_account2.lamports, Rent::default().minimum_balance(5));
+    assert_eq!(post_account2.data.len(), 5);
+
+    let claim_record_data = sim.get_claim_record(emitter).await;
+    assert_eq!(claim_record_data.sequence, 1);
+
+    // Execute one transfer
+    sim.execute_posted_vaa(&vaa_account_transfer2, &vec![], ExecutorAttack::None)
+        .await
+        .unwrap();
+
+    // Check state post call
+    let post_account4 = sim.get_account(receiver4.pubkey()).await.unwrap();
+    assert_eq!(post_account4.lamports, Rent::default().minimum_balance(0));
+    assert_eq!(post_account4.data.len(), 0);
+    assert_eq!(post_account4.owner, system_program::id());
+
+    let claim_record_data = sim.get_claim_record(emitter).await;
+    assert_eq!(claim_record_data.sequence, 3);
+
+    // Replay attack
+    assert_eq!(
+        sim.execute_posted_vaa(&vaa_account_transfer2, &vec![], ExecutorAttack::None)
+            .await
+            .unwrap_err()
+            .unwrap(),
+        ExecutorError::NonIncreasingSequence.into()
+    );
+
+    let claim_record_data = sim.get_claim_record(emitter).await;
+    assert_eq!(claim_record_data.sequence, 3);
+
+    // Using a governance message with a lower sequence number
+    assert_eq!(
+        sim.execute_posted_vaa(&vaa_account_transfer1, &vec![], ExecutorAttack::None)
+            .await
+            .unwrap_err()
+            .unwrap(),
+        ExecutorError::NonIncreasingSequence.into()
+    );
+    let claim_record_data = sim.get_claim_record(emitter).await;
+    assert_eq!(claim_record_data.sequence, 3);
+
+    let post_account3 = sim.get_account(receiver3.pubkey()).await;
+    assert_eq!(post_account3, None);
+}

+ 0 - 16
pythnet/remote-executor/programs/remote-executor/src/tests/test_create_account.rs

@@ -1,16 +0,0 @@
-use super::executor_simulator::ExecutorBench;
-use anchor_lang::prelude::Pubkey;
-
-#[tokio::test]
-async fn test_create_account() {
-    let mut bench = ExecutorBench::new();
-
-    let emitter = Pubkey::new_unique();
-    let instructions = vec![];
-
-    let vaa_account = bench.add_vaa_account(&emitter, &instructions);
-
-    let mut sim = bench.start().await;
-
-    sim.execute_posted_vaa(&vaa_account).await.unwrap();
-}