Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions target_chains/solana/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ rand = "0.8.5"

[dev-dependencies]
solana-sdk = { workspace = true }
solana-program-test = { workspace = true }
tokio = { workspace = true }
program-simulator = { path = "../../program_simulator" }
wormhole-vaas-serde = { workspace = true }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
use {
anchor_lang::{InstructionData, ToAccountMetas},
common_test_utils::{
assert_treasury_balance, build_guardian_set_account, default_receiver_config,
WrongSetupOption, DEFAULT_GUARDIAN_SET_INDEX,
},
program_simulator::ProgramSimulator,
pyth_solana_receiver::{
instruction::{Initialize, PostUpdate},
sdk::{
deserialize_accumulator_update_data, get_guardian_set_address, DEFAULT_TREASURY_ID,
VAA_SPLIT_INDEX,
},
},
pyth_solana_receiver_sdk::{
config::Config,
pda::get_config_address,
price_update::{PriceUpdateV2, VerificationLevel},
PYTH_PUSH_ORACLE_ID,
},
pythnet_sdk::{
messages::Message,
test_utils::{create_accumulator_message, create_dummy_price_feed_message},
},
solana_program::instruction::Instruction,
solana_program_test::ProgramTest,
solana_sdk::{rent::Rent, signature::Keypair, signer::Signer},
wormhole_core_bridge_solana::{
sdk::{WriteEncodedVaaArgs, VAA_START},
ID as BRIDGE_ID,
},
};

/// Creates a ProcessInstruction-compatible function pointer from the Anchor-generated
/// entry function. Anchor constrains `&'info [AccountInfo<'info>]` (matching lifetimes),
/// while ProcessInstruction has independent lifetimes. This is safe because ProgramTest's
/// invoke_builtin_function always provides matching lifetimes (both derived from the same buffer).
fn wormhole_process_instruction() -> solana_program::entrypoint::ProcessInstruction {
unsafe { std::mem::transmute(wormhole_core_bridge_solana::entry as usize) }
}
Comment on lines +38 to +40
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Unsafe transmute used where a safe wrapper function would suffice

The unsafe { std::mem::transmute(wormhole_core_bridge_solana::entry as usize) } at line 39 violates the REVIEW.md rule "No unsafe code unless absolutely necessary" and doc/rust-code-guidelines.md which states "Avoid writing unsafe code" and recommends unsafe_code = "deny" in clippy config.

The transmute converts the Anchor entry function (which ties the slice and AccountInfo lifetimes: &'info [AccountInfo<'info>]) to a ProcessInstruction pointer (which has independent lifetimes). A safe wrapper function can achieve this without unsafe because Rust's lifetime subtyping allows independent lifetimes to be unified when calling a function that requires matching lifetimes:

Safe alternative
fn wormhole_process_instruction(
    program_id: &solana_program::pubkey::Pubkey,
    accounts: &[solana_program::account_info::AccountInfo],
    instruction_data: &[u8],
) -> solana_program::entrypoint::ProgramResult {
    wormhole_core_bridge_solana::entry(program_id, accounts, instruction_data)
}

Beyond the rule violation, the transmute through usize is fragile: it relies on an assumption about ProgramTest's internal implementation providing matching lifetimes, which could change in future versions of the dependency.

Suggested change
fn wormhole_process_instruction() -> solana_program::entrypoint::ProcessInstruction {
unsafe { std::mem::transmute(wormhole_core_bridge_solana::entry as usize) }
}
fn wormhole_process_instruction(
program_id: &solana_program::pubkey::Pubkey,
accounts: &[solana_program::account_info::AccountInfo],
instruction_data: &[u8],
) -> solana_program::entrypoint::ProgramResult {
wormhole_core_bridge_solana::entry(program_id, accounts, instruction_data)
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


#[tokio::test]
async fn test_post_update_with_wormhole() {
// 1. Setup: Create accumulator message with dummy price feeds
let feed_1 = create_dummy_price_feed_message(100);
let feed_2 = create_dummy_price_feed_message(200);
let message =
create_accumulator_message(&[&feed_1, &feed_2], &[&feed_1, &feed_2], false, false, None);
let (vaa, merkle_price_updates) = deserialize_accumulator_update_data(message).unwrap();

// 2. Program setup: ProgramTest with pyth_solana_receiver, pyth_push_oracle, and wormhole core-bridge
let mut program_test = ProgramTest::default();
program_test.add_program("pyth_solana_receiver", pyth_solana_receiver::ID, None);
program_test.add_program("pyth_push_oracle", PYTH_PUSH_ORACLE_ID, None);
program_test.add_program(
"wormhole_core_bridge_solana",
BRIDGE_ID,
solana_program_test::processor!(wormhole_process_instruction()),
);

// Add guardian set account at the correct PDA
program_test.add_account(
get_guardian_set_address(BRIDGE_ID, DEFAULT_GUARDIAN_SET_INDEX),
build_guardian_set_account(WrongSetupOption::None),
);

let mut program_simulator = ProgramSimulator::start_from_program_test(program_test).await;

// Initialize pyth receiver config
let setup_keypair = program_simulator.get_funded_keypair().await.unwrap();
let initial_config = default_receiver_config(setup_keypair.pubkey());

program_simulator
.process_ix_with_default_compute_limit(
Initialize::populate(&setup_keypair.pubkey(), initial_config.clone()),
&vec![&setup_keypair],
None,
)
.await
.unwrap();

let config_account = program_simulator
.get_anchor_account_data::<Config>(get_config_address())
.await
.unwrap();
assert_eq!(config_account, initial_config);

// 3. Create encoded VAA via core-bridge instructions
let write_authority = program_simulator.get_funded_keypair().await.unwrap();
let encoded_vaa_keypair = Keypair::new();
let encoded_vaa_size: usize = vaa.len() + VAA_START;

// TX1: Create account for encoded VAA
let create_encoded_vaa = solana_sdk::system_instruction::create_account(
&write_authority.pubkey(),
&encoded_vaa_keypair.pubkey(),
Rent::default().minimum_balance(encoded_vaa_size),
encoded_vaa_size as u64,
&BRIDGE_ID,
);

program_simulator
.process_ix_with_default_compute_limit(
create_encoded_vaa,
&vec![&encoded_vaa_keypair],
Some(&write_authority),
)
.await
.unwrap();

// TX2: Init encoded VAA
let init_encoded_vaa_instruction = Instruction {
program_id: BRIDGE_ID,
accounts: wormhole_core_bridge_solana::accounts::InitEncodedVaa {
write_authority: write_authority.pubkey(),
encoded_vaa: encoded_vaa_keypair.pubkey(),
}
.to_account_metas(None),
data: wormhole_core_bridge_solana::instruction::InitEncodedVaa.data(),
};

program_simulator
.process_ix_with_default_compute_limit(
init_encoded_vaa_instruction,
&vec![],
Some(&write_authority),
)
.await
.unwrap();

// TX3: Write first part of VAA data
let write_encoded_vaa_instruction_1 = Instruction {
program_id: BRIDGE_ID,
accounts: wormhole_core_bridge_solana::accounts::WriteEncodedVaa {
write_authority: write_authority.pubkey(),
draft_vaa: encoded_vaa_keypair.pubkey(),
}
.to_account_metas(None),
data: wormhole_core_bridge_solana::instruction::WriteEncodedVaa {
args: WriteEncodedVaaArgs {
index: 0,
data: vaa[..VAA_SPLIT_INDEX].to_vec(),
},
}
.data(),
};

program_simulator
.process_ix_with_default_compute_limit(
write_encoded_vaa_instruction_1,
&vec![],
Some(&write_authority),
)
.await
.unwrap();

// TX4: Write remaining VAA data
let write_encoded_vaa_instruction_2 = Instruction {
program_id: BRIDGE_ID,
accounts: wormhole_core_bridge_solana::accounts::WriteEncodedVaa {
write_authority: write_authority.pubkey(),
draft_vaa: encoded_vaa_keypair.pubkey(),
}
.to_account_metas(None),
data: wormhole_core_bridge_solana::instruction::WriteEncodedVaa {
args: WriteEncodedVaaArgs {
index: VAA_SPLIT_INDEX.try_into().unwrap(),
data: vaa[VAA_SPLIT_INDEX..].to_vec(),
},
}
.data(),
};

program_simulator
.process_ix_with_default_compute_limit(
write_encoded_vaa_instruction_2,
&vec![],
Some(&write_authority),
)
.await
.unwrap();

// TX5: Verify encoded VAA
let guardian_set = get_guardian_set_address(BRIDGE_ID, DEFAULT_GUARDIAN_SET_INDEX);

let verify_encoded_vaa_instruction = Instruction {
program_id: BRIDGE_ID,
accounts: wormhole_core_bridge_solana::accounts::VerifyEncodedVaaV1 {
write_authority: write_authority.pubkey(),
draft_vaa: encoded_vaa_keypair.pubkey(),
guardian_set,
}
.to_account_metas(None),
data: wormhole_core_bridge_solana::instruction::VerifyEncodedVaaV1 {}.data(),
};

program_simulator
.process_ix_with_default_compute_limit(
verify_encoded_vaa_instruction,
&vec![],
Some(&write_authority),
)
.await
.unwrap();

// 4. Post update using the core-bridge-verified encoded VAA
assert_treasury_balance(&mut program_simulator, 0, DEFAULT_TREASURY_ID).await;

let poster = program_simulator.get_funded_keypair().await.unwrap();
let price_update_keypair = Keypair::new();

program_simulator
.process_ix_with_default_compute_limit(
PostUpdate::populate(
poster.pubkey(),
poster.pubkey(),
encoded_vaa_keypair.pubkey(),
price_update_keypair.pubkey(),
merkle_price_updates[0].clone(),
DEFAULT_TREASURY_ID,
),
&vec![&poster, &price_update_keypair],
None,
)
.await
.unwrap();

// 5. Assertions
assert_treasury_balance(
&mut program_simulator,
Rent::default().minimum_balance(0),
DEFAULT_TREASURY_ID,
)
.await;

let price_update_account = program_simulator
.get_anchor_account_data::<PriceUpdateV2>(price_update_keypair.pubkey())
.await
.unwrap();

assert_eq!(price_update_account.write_authority, poster.pubkey());
assert_eq!(
price_update_account.verification_level,
VerificationLevel::Full
);
assert_eq!(
Message::PriceFeedMessage(price_update_account.price_message),
feed_1
);
assert_eq!(
price_update_account.posted_slot,
program_simulator.get_clock().await.unwrap().slot
);
}
Loading