| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285 |
- use crate::config::P2WConfigAccount;
- use borsh::{
- BorshDeserialize,
- BorshSerialize,
- };
- use solana_program::{
- clock::Clock,
- instruction::{
- AccountMeta,
- Instruction,
- },
- program::{
- invoke,
- invoke_signed,
- },
- program_error::ProgramError,
- pubkey::Pubkey,
- rent::Rent,
- };
- use p2w_sdk::{
- BatchPriceAttestation,
- P2WEmitter,
- PriceAttestation,
- Identifier,
- };
- use bridge::{
- accounts::BridgeData,
- types::ConsistencyLevel,
- PostMessageData,
- };
- use solitaire::{
- invoke_seeded,
- trace,
- AccountState,
- Derive,
- ExecutionContext,
- FromAccounts,
- Info,
- Keyed,
- Mut,
- Peel,
- Result as SoliResult,
- Seeded,
- Signer,
- SolitaireError,
- Sysvar,
- };
- /// Important: must be manually maintained until native Solitaire
- /// variable len vector support.
- ///
- /// The number must reflect how many pyth product/price pairs are
- /// expected in the Attest struct below. The constant itself is only
- /// used in the on-chain config in order for attesters to learn the
- /// correct value dynamically.
- pub const P2W_MAX_BATCH_SIZE: u16 = 5;
- #[derive(FromAccounts)]
- pub struct Attest<'b> {
- // Payer also used for wormhole
- pub payer: Mut<Signer<Info<'b>>>,
- pub system_program: Info<'b>,
- pub config: P2WConfigAccount<'b, { AccountState::Initialized }>,
- // Hardcoded product/price pairs, bypassing Solitaire's variable-length limitations
- // Any change to the number of accounts must include an appropriate change to P2W_MAX_BATCH_SIZE
- pub pyth_product: Info<'b>,
- pub pyth_price: Info<'b>,
- pub pyth_product2: Option<Info<'b>>,
- pub pyth_price2: Option<Info<'b>>,
- pub pyth_product3: Option<Info<'b>>,
- pub pyth_price3: Option<Info<'b>>,
- pub pyth_product4: Option<Info<'b>>,
- pub pyth_price4: Option<Info<'b>>,
- pub pyth_product5: Option<Info<'b>>,
- pub pyth_price5: Option<Info<'b>>,
- // Did you read the comment near `pyth_product`?
- // pub pyth_product6: Option<Info<'b>>,
- // pub pyth_price6: Option<Info<'b>>,
- // pub pyth_product7: Option<Info<'b>>,
- // pub pyth_price7: Option<Info<'b>>,
- // pub pyth_product8: Option<Info<'b>>,
- // pub pyth_price8: Option<Info<'b>>,
- // pub pyth_product9: Option<Info<'b>>,
- // pub pyth_price9: Option<Info<'b>>,
- // pub pyth_product10: Option<Info<'b>>,
- // pub pyth_price10: Option<Info<'b>>,
- pub clock: Sysvar<'b, Clock>,
- /// Wormhole program address - must match the config value
- pub wh_prog: Info<'b>,
- // wormhole's post_message accounts
- //
- // This contract makes no attempt to exhaustively validate
- // Wormhole's account inputs. Only the wormhole contract address
- // is validated (see above).
- /// Bridge config needed for fee calculation
- pub wh_bridge: Mut<Info<'b>>,
- /// Account to store the posted message
- pub wh_message: Signer<Mut<Info<'b>>>,
- /// Emitter of the VAA
- pub wh_emitter: P2WEmitter<'b>,
- /// Tracker for the emitter sequence
- pub wh_sequence: Mut<Info<'b>>,
- // We reuse our payer
- // pub wh_payer: Mut<Signer<Info<'b>>>,
- /// Account to collect tx fee
- pub wh_fee_collector: Mut<Info<'b>>,
- pub wh_rent: Sysvar<'b, Rent>,
- }
- #[derive(BorshDeserialize, BorshSerialize)]
- pub struct AttestData {
- pub consistency_level: ConsistencyLevel,
- }
- pub fn attest(ctx: &ExecutionContext, accs: &mut Attest, data: AttestData) -> SoliResult<()> {
- if !accs.config.is_active {
- // msg instead of trace makes sure we're not silent about this in prod
- solana_program::msg!("This attester program is disabled!");
- return Err(SolitaireError::Custom(4242));
- }
- accs.config.verify_derivation(ctx.program_id, None)?;
- if accs.config.wh_prog != *accs.wh_prog.key {
- trace!(&format!(
- "Wormhole program account mismatch (expected {:?}, got {:?})",
- accs.config.wh_prog, accs.wh_prog.key
- ));
- return Err(ProgramError::InvalidAccountData.into());
- }
- // Make the specified prices iterable
- let price_pair_opts = [
- Some(&accs.pyth_product),
- Some(&accs.pyth_price),
- accs.pyth_product2.as_ref(),
- accs.pyth_price2.as_ref(),
- accs.pyth_product3.as_ref(),
- accs.pyth_price3.as_ref(),
- accs.pyth_product4.as_ref(),
- accs.pyth_price4.as_ref(),
- accs.pyth_product5.as_ref(),
- accs.pyth_price5.as_ref(),
- // Did you read the comment near `pyth_product`?
- // accs.pyth_product6.as_ref(),
- // accs.pyth_price6.as_ref(),
- // accs.pyth_product7.as_ref(),
- // accs.pyth_price7.as_ref(),
- // accs.pyth_product8.as_ref(),
- // accs.pyth_price8.as_ref(),
- // accs.pyth_product9.as_ref(),
- // accs.pyth_price9.as_ref(),
- // accs.pyth_product10.as_ref(),
- // accs.pyth_price10.as_ref(),
- ];
- let price_pairs: Vec<_> = price_pair_opts.into_iter().filter_map(|acc| *acc).collect();
- if price_pairs.len() % 2 != 0 {
- trace!(&format!(
- "Uneven product/price count detected: {}",
- price_pairs.len()
- ));
- return Err(ProgramError::InvalidAccountData.into());
- }
- trace!("{} Pyth symbols received", price_pairs.len() / 2);
- // Collect the validated symbols for batch serialization
- let mut attestations = Vec::with_capacity(price_pairs.len() / 2);
- for pair in price_pairs.as_slice().chunks_exact(2) {
- let product = pair[0];
- let price = pair[1];
- if accs.config.pyth_owner != *price.owner || accs.config.pyth_owner != *product.owner {
- trace!(&format!(
- "Pair {:?} - {:?}: pyth_owner pubkey mismatch (expected {:?}, got product owner {:?} and price owner {:?}",
- product, price,
- accs.config.pyth_owner, product.owner, price.owner
- ));
- return Err(SolitaireError::InvalidOwner(accs.pyth_price.owner.clone()).into());
- }
- let attestation = PriceAttestation::from_pyth_price_bytes(
- Identifier::new(product.key.to_bytes()),
- accs.clock.unix_timestamp,
- &*price.try_borrow_data()?,
- )
- .map_err(|e| {
- trace!(&e.to_string());
- ProgramError::InvalidAccountData
- })?;
- // The following check is crucial against poorly ordered
- // account inputs, e.g. [Some(prod1), Some(price1),
- // Some(prod2), None, None, Some(price)], interpreted by
- // earlier logic as [(prod1, price1), (prod2, price3)].
- //
- // Failing to verify the product/price relationship could lead
- // to mismatched product/price metadata, which would result in
- // a false attestation.
- if attestation.product_id.to_bytes() != product.key.to_bytes() {
- trace!(&format!(
- "Price's product_id does not match the pased account (points at {:?} instead)",
- attestation.product_id
- ));
- return Err(ProgramError::InvalidAccountData.into());
- }
- attestations.push(attestation);
- }
- let batch_attestation = BatchPriceAttestation {
- price_attestations: attestations,
- };
- trace!("Attestations successfully created");
- let bridge_config = BridgeData::try_from_slice(&accs.wh_bridge.try_borrow_mut_data()?)?.config;
- // Pay wormhole fee
- let transfer_ix = solana_program::system_instruction::transfer(
- accs.payer.key,
- accs.wh_fee_collector.info().key,
- bridge_config.fee,
- );
- solana_program::program::invoke(&transfer_ix, ctx.accounts)?;
- // Send payload
- let post_message_data = (
- bridge::instruction::Instruction::PostMessage,
- PostMessageData {
- nonce: 0, // Superseded by the sequence number
- payload: batch_attestation.serialize().map_err(|e| {
- trace!(&e.to_string());
- ProgramError::InvalidAccountData
- })?,
- consistency_level: data.consistency_level,
- },
- );
- let ix = Instruction::new_with_bytes(
- accs.config.wh_prog,
- post_message_data.try_to_vec()?.as_slice(),
- vec![
- AccountMeta::new(*accs.wh_bridge.key, false),
- AccountMeta::new(*accs.wh_message.key, true),
- AccountMeta::new_readonly(*accs.wh_emitter.key, true),
- AccountMeta::new(*accs.wh_sequence.key, false),
- AccountMeta::new(*accs.payer.key, true),
- AccountMeta::new(*accs.wh_fee_collector.key, false),
- AccountMeta::new_readonly(*accs.clock.info().key, false),
- AccountMeta::new_readonly(solana_program::sysvar::rent::ID, false),
- AccountMeta::new_readonly(solana_program::system_program::id(), false),
- ],
- );
- trace!("Before cross-call");
- invoke_seeded(&ix, ctx, &accs.wh_emitter, None)?;
- Ok(())
- }
|