attest.rs 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. use crate::config::P2WConfigAccount;
  2. use borsh::{
  3. BorshDeserialize,
  4. BorshSerialize,
  5. };
  6. use solana_program::{
  7. clock::Clock,
  8. instruction::{
  9. AccountMeta,
  10. Instruction,
  11. },
  12. program::{
  13. invoke,
  14. invoke_signed,
  15. },
  16. program_error::ProgramError,
  17. pubkey::Pubkey,
  18. rent::Rent,
  19. };
  20. use p2w_sdk::{
  21. BatchPriceAttestation,
  22. P2WEmitter,
  23. PriceAttestation,
  24. Identifier,
  25. };
  26. use bridge::{
  27. accounts::BridgeData,
  28. types::ConsistencyLevel,
  29. PostMessageData,
  30. };
  31. use solitaire::{
  32. invoke_seeded,
  33. trace,
  34. AccountState,
  35. Derive,
  36. ExecutionContext,
  37. FromAccounts,
  38. Info,
  39. Keyed,
  40. Mut,
  41. Peel,
  42. Result as SoliResult,
  43. Seeded,
  44. Signer,
  45. SolitaireError,
  46. Sysvar,
  47. };
  48. /// Important: must be manually maintained until native Solitaire
  49. /// variable len vector support.
  50. ///
  51. /// The number must reflect how many pyth product/price pairs are
  52. /// expected in the Attest struct below. The constant itself is only
  53. /// used in the on-chain config in order for attesters to learn the
  54. /// correct value dynamically.
  55. pub const P2W_MAX_BATCH_SIZE: u16 = 5;
  56. #[derive(FromAccounts)]
  57. pub struct Attest<'b> {
  58. // Payer also used for wormhole
  59. pub payer: Mut<Signer<Info<'b>>>,
  60. pub system_program: Info<'b>,
  61. pub config: P2WConfigAccount<'b, { AccountState::Initialized }>,
  62. // Hardcoded product/price pairs, bypassing Solitaire's variable-length limitations
  63. // Any change to the number of accounts must include an appropriate change to P2W_MAX_BATCH_SIZE
  64. pub pyth_product: Info<'b>,
  65. pub pyth_price: Info<'b>,
  66. pub pyth_product2: Option<Info<'b>>,
  67. pub pyth_price2: Option<Info<'b>>,
  68. pub pyth_product3: Option<Info<'b>>,
  69. pub pyth_price3: Option<Info<'b>>,
  70. pub pyth_product4: Option<Info<'b>>,
  71. pub pyth_price4: Option<Info<'b>>,
  72. pub pyth_product5: Option<Info<'b>>,
  73. pub pyth_price5: Option<Info<'b>>,
  74. // Did you read the comment near `pyth_product`?
  75. // pub pyth_product6: Option<Info<'b>>,
  76. // pub pyth_price6: Option<Info<'b>>,
  77. // pub pyth_product7: Option<Info<'b>>,
  78. // pub pyth_price7: Option<Info<'b>>,
  79. // pub pyth_product8: Option<Info<'b>>,
  80. // pub pyth_price8: Option<Info<'b>>,
  81. // pub pyth_product9: Option<Info<'b>>,
  82. // pub pyth_price9: Option<Info<'b>>,
  83. // pub pyth_product10: Option<Info<'b>>,
  84. // pub pyth_price10: Option<Info<'b>>,
  85. pub clock: Sysvar<'b, Clock>,
  86. /// Wormhole program address - must match the config value
  87. pub wh_prog: Info<'b>,
  88. // wormhole's post_message accounts
  89. //
  90. // This contract makes no attempt to exhaustively validate
  91. // Wormhole's account inputs. Only the wormhole contract address
  92. // is validated (see above).
  93. /// Bridge config needed for fee calculation
  94. pub wh_bridge: Mut<Info<'b>>,
  95. /// Account to store the posted message
  96. pub wh_message: Signer<Mut<Info<'b>>>,
  97. /// Emitter of the VAA
  98. pub wh_emitter: P2WEmitter<'b>,
  99. /// Tracker for the emitter sequence
  100. pub wh_sequence: Mut<Info<'b>>,
  101. // We reuse our payer
  102. // pub wh_payer: Mut<Signer<Info<'b>>>,
  103. /// Account to collect tx fee
  104. pub wh_fee_collector: Mut<Info<'b>>,
  105. pub wh_rent: Sysvar<'b, Rent>,
  106. }
  107. #[derive(BorshDeserialize, BorshSerialize)]
  108. pub struct AttestData {
  109. pub consistency_level: ConsistencyLevel,
  110. }
  111. pub fn attest(ctx: &ExecutionContext, accs: &mut Attest, data: AttestData) -> SoliResult<()> {
  112. if !accs.config.is_active {
  113. // msg instead of trace makes sure we're not silent about this in prod
  114. solana_program::msg!("This attester program is disabled!");
  115. return Err(SolitaireError::Custom(4242));
  116. }
  117. accs.config.verify_derivation(ctx.program_id, None)?;
  118. if accs.config.wh_prog != *accs.wh_prog.key {
  119. trace!(&format!(
  120. "Wormhole program account mismatch (expected {:?}, got {:?})",
  121. accs.config.wh_prog, accs.wh_prog.key
  122. ));
  123. return Err(ProgramError::InvalidAccountData.into());
  124. }
  125. // Make the specified prices iterable
  126. let price_pair_opts = [
  127. Some(&accs.pyth_product),
  128. Some(&accs.pyth_price),
  129. accs.pyth_product2.as_ref(),
  130. accs.pyth_price2.as_ref(),
  131. accs.pyth_product3.as_ref(),
  132. accs.pyth_price3.as_ref(),
  133. accs.pyth_product4.as_ref(),
  134. accs.pyth_price4.as_ref(),
  135. accs.pyth_product5.as_ref(),
  136. accs.pyth_price5.as_ref(),
  137. // Did you read the comment near `pyth_product`?
  138. // accs.pyth_product6.as_ref(),
  139. // accs.pyth_price6.as_ref(),
  140. // accs.pyth_product7.as_ref(),
  141. // accs.pyth_price7.as_ref(),
  142. // accs.pyth_product8.as_ref(),
  143. // accs.pyth_price8.as_ref(),
  144. // accs.pyth_product9.as_ref(),
  145. // accs.pyth_price9.as_ref(),
  146. // accs.pyth_product10.as_ref(),
  147. // accs.pyth_price10.as_ref(),
  148. ];
  149. let price_pairs: Vec<_> = price_pair_opts.into_iter().filter_map(|acc| *acc).collect();
  150. if price_pairs.len() % 2 != 0 {
  151. trace!(&format!(
  152. "Uneven product/price count detected: {}",
  153. price_pairs.len()
  154. ));
  155. return Err(ProgramError::InvalidAccountData.into());
  156. }
  157. trace!("{} Pyth symbols received", price_pairs.len() / 2);
  158. // Collect the validated symbols for batch serialization
  159. let mut attestations = Vec::with_capacity(price_pairs.len() / 2);
  160. for pair in price_pairs.as_slice().chunks_exact(2) {
  161. let product = pair[0];
  162. let price = pair[1];
  163. if accs.config.pyth_owner != *price.owner || accs.config.pyth_owner != *product.owner {
  164. trace!(&format!(
  165. "Pair {:?} - {:?}: pyth_owner pubkey mismatch (expected {:?}, got product owner {:?} and price owner {:?}",
  166. product, price,
  167. accs.config.pyth_owner, product.owner, price.owner
  168. ));
  169. return Err(SolitaireError::InvalidOwner(accs.pyth_price.owner.clone()).into());
  170. }
  171. let attestation = PriceAttestation::from_pyth_price_bytes(
  172. Identifier::new(product.key.to_bytes()),
  173. accs.clock.unix_timestamp,
  174. &*price.try_borrow_data()?,
  175. )
  176. .map_err(|e| {
  177. trace!(&e.to_string());
  178. ProgramError::InvalidAccountData
  179. })?;
  180. // The following check is crucial against poorly ordered
  181. // account inputs, e.g. [Some(prod1), Some(price1),
  182. // Some(prod2), None, None, Some(price)], interpreted by
  183. // earlier logic as [(prod1, price1), (prod2, price3)].
  184. //
  185. // Failing to verify the product/price relationship could lead
  186. // to mismatched product/price metadata, which would result in
  187. // a false attestation.
  188. if attestation.product_id.to_bytes() != product.key.to_bytes() {
  189. trace!(&format!(
  190. "Price's product_id does not match the pased account (points at {:?} instead)",
  191. attestation.product_id
  192. ));
  193. return Err(ProgramError::InvalidAccountData.into());
  194. }
  195. attestations.push(attestation);
  196. }
  197. let batch_attestation = BatchPriceAttestation {
  198. price_attestations: attestations,
  199. };
  200. trace!("Attestations successfully created");
  201. let bridge_config = BridgeData::try_from_slice(&accs.wh_bridge.try_borrow_mut_data()?)?.config;
  202. // Pay wormhole fee
  203. let transfer_ix = solana_program::system_instruction::transfer(
  204. accs.payer.key,
  205. accs.wh_fee_collector.info().key,
  206. bridge_config.fee,
  207. );
  208. solana_program::program::invoke(&transfer_ix, ctx.accounts)?;
  209. // Send payload
  210. let post_message_data = (
  211. bridge::instruction::Instruction::PostMessage,
  212. PostMessageData {
  213. nonce: 0, // Superseded by the sequence number
  214. payload: batch_attestation.serialize().map_err(|e| {
  215. trace!(&e.to_string());
  216. ProgramError::InvalidAccountData
  217. })?,
  218. consistency_level: data.consistency_level,
  219. },
  220. );
  221. let ix = Instruction::new_with_bytes(
  222. accs.config.wh_prog,
  223. post_message_data.try_to_vec()?.as_slice(),
  224. vec![
  225. AccountMeta::new(*accs.wh_bridge.key, false),
  226. AccountMeta::new(*accs.wh_message.key, true),
  227. AccountMeta::new_readonly(*accs.wh_emitter.key, true),
  228. AccountMeta::new(*accs.wh_sequence.key, false),
  229. AccountMeta::new(*accs.payer.key, true),
  230. AccountMeta::new(*accs.wh_fee_collector.key, false),
  231. AccountMeta::new_readonly(*accs.clock.info().key, false),
  232. AccountMeta::new_readonly(solana_program::sysvar::rent::ID, false),
  233. AccountMeta::new_readonly(solana_program::system_program::id(), false),
  234. ],
  235. );
  236. trace!("Before cross-call");
  237. invoke_seeded(&ix, ctx, &accs.wh_emitter, None)?;
  238. Ok(())
  239. }