11use {
22 crate :: {
3- attestation_state:: {
4- AttestationState ,
5- AttestationStateMapPDA ,
6- } ,
3+ attestation_state:: AttestationStatePDA ,
74 config:: P2WConfigAccount ,
85 message:: {
96 P2WMessage ,
3936 solitaire:: {
4037 trace,
4138 AccountState ,
42- CreationLamports ,
4339 ExecutionContext ,
4440 FromAccounts ,
4541 Info ,
5753/// Important: must be manually maintained until native Solitaire
5854/// variable len vector support.
5955///
60- /// The number must reflect how many pyth product /price pairs are
56+ /// The number must reflect how many pyth state /price pairs are
6157/// expected in the Attest struct below. The constant itself is only
6258/// used in the on-chain config in order for attesters to learn the
6359/// correct value dynamically.
@@ -66,42 +62,41 @@ pub const P2W_MAX_BATCH_SIZE: u16 = 5;
6662#[ derive( FromAccounts ) ]
6763pub struct Attest < ' b > {
6864 // Payer also used for wormhole
69- pub payer : Mut < Signer < Info < ' b > > > ,
70- pub system_program : Info < ' b > ,
71- pub config : P2WConfigAccount < ' b , { AccountState :: Initialized } > ,
72- pub attestation_state : Mut < AttestationStateMapPDA < ' b > > ,
65+ pub payer : Mut < Signer < Info < ' b > > > ,
66+ pub system_program : Info < ' b > ,
67+ pub config : P2WConfigAccount < ' b , { AccountState :: Initialized } > ,
7368
74- // Hardcoded product /price pairs, bypassing Solitaire's variable-length limitations
69+ // Hardcoded state /price pairs, bypassing Solitaire's variable-length limitations
7570 // Any change to the number of accounts must include an appropriate change to P2W_MAX_BATCH_SIZE
76- pub pyth_product : Info < ' b > ,
77- pub pyth_price : Info < ' b > ,
71+ pub pyth_state : Mut < AttestationStatePDA < ' b > > ,
72+ pub pyth_price : Info < ' b > ,
7873
79- pub pyth_product2 : Option < Info < ' b > > ,
80- pub pyth_price2 : Option < Info < ' b > > ,
74+ pub pyth_state2 : Option < Mut < AttestationStatePDA < ' b > > > ,
75+ pub pyth_price2 : Option < Info < ' b > > ,
8176
82- pub pyth_product3 : Option < Info < ' b > > ,
83- pub pyth_price3 : Option < Info < ' b > > ,
77+ pub pyth_state3 : Option < Mut < AttestationStatePDA < ' b > > > ,
78+ pub pyth_price3 : Option < Info < ' b > > ,
8479
85- pub pyth_product4 : Option < Info < ' b > > ,
86- pub pyth_price4 : Option < Info < ' b > > ,
80+ pub pyth_state4 : Option < Mut < AttestationStatePDA < ' b > > > ,
81+ pub pyth_price4 : Option < Info < ' b > > ,
8782
88- pub pyth_product5 : Option < Info < ' b > > ,
89- pub pyth_price5 : Option < Info < ' b > > ,
83+ pub pyth_state5 : Option < Mut < AttestationStatePDA < ' b > > > ,
84+ pub pyth_price5 : Option < Info < ' b > > ,
9085
91- // Did you read the comment near `pyth_product `?
92- // pub pyth_product6 : Option<Info<'b >>,
86+ // Did you read the comment near `pyth_state `?
87+ // pub pyth_state6 : Option<Mut<AttestationStatePDA<'b> >>,
9388 // pub pyth_price6: Option<Info<'b>>,
9489
95- // pub pyth_product7 : Option<Info<'b >>,
90+ // pub pyth_state7 : Option<Mut<AttestationStatePDA<'b> >>,
9691 // pub pyth_price7: Option<Info<'b>>,
9792
98- // pub pyth_product8 : Option<Info<'b >>,
93+ // pub pyth_state8 : Option<Mut<AttestationStatePDA<'b> >>,
9994 // pub pyth_price8: Option<Info<'b>>,
10095
101- // pub pyth_product9 : Option<Info<'b >>,
96+ // pub pyth_state9 : Option<Mut<AttestationStatePDA<'b> >>,
10297 // pub pyth_price9: Option<Info<'b>>,
10398
104- // pub pyth_product10 : Option<Info<'b >>,
99+ // pub pyth_state10 : Option<Mut<AttestationStatePDA<'b> >>,
105100 // pub pyth_price10: Option<Info<'b>>,
106101 pub clock : Sysvar < ' b , Clock > ,
107102
@@ -161,57 +156,55 @@ pub fn attest(ctx: &ExecutionContext, accs: &mut Attest, data: AttestData) -> So
161156
162157
163158 // Make the specified prices iterable
164- let price_pair_opts = [
165- Some ( & accs. pyth_product ) ,
166- Some ( & accs. pyth_price ) ,
167- accs. pyth_product2 . as_ref ( ) ,
168- accs. pyth_price2 . as_ref ( ) ,
169- accs. pyth_product3 . as_ref ( ) ,
170- accs. pyth_price3 . as_ref ( ) ,
171- accs. pyth_product4 . as_ref ( ) ,
172- accs. pyth_price4 . as_ref ( ) ,
173- accs. pyth_product5 . as_ref ( ) ,
174- accs. pyth_price5 . as_ref ( ) ,
175- // Did you read the comment near `pyth_product`?
176- // accs.pyth_product6.as_ref(),
177- // accs.pyth_price6.as_ref(),
178- // accs.pyth_product7.as_ref(),
179- // accs.pyth_price7.as_ref(),
180- // accs.pyth_product8.as_ref(),
181- // accs.pyth_price8.as_ref(),
182- // accs.pyth_product9.as_ref(),
183- // accs.pyth_price9.as_ref(),
184- // accs.pyth_product10.as_ref(),
185- // accs.pyth_price10.as_ref(),
159+ let mut price_pair_opts = [
160+ ( Some ( & mut accs. pyth_state ) , Some ( & accs. pyth_price ) ) ,
161+ ( accs. pyth_state2 . as_mut ( ) , accs. pyth_price2 . as_ref ( ) ) ,
162+ ( accs. pyth_state3 . as_mut ( ) , accs. pyth_price3 . as_ref ( ) ) ,
163+ ( accs. pyth_state4 . as_mut ( ) , accs. pyth_price4 . as_ref ( ) ) ,
164+ ( accs. pyth_state5 . as_mut ( ) , accs. pyth_price5 . as_ref ( ) ) ,
165+ // Did you read the comment near `pyth_state`?
166+ // (accs.pyth_state6.as_mut(), accs.pyth_price6.as_ref()),
167+ // (accs.pyth_state7.as_mut(), accs.pyth_price7.as_ref()),
168+ // (accs.pyth_state8.as_mut(), accs.pyth_price8.as_ref()),
169+ // (accs.pyth_state9.as_mut(), accs.pyth_price9.as_ref()),
170+ // (accs.pyth_state10.as_mut(), accs.pyth_price10.as_ref()),
186171 ] ;
187172
188- let price_pairs: Vec < _ > = price_pair_opts. iter ( ) . filter_map ( |acc| * acc) . collect ( ) ;
173+ let price_pairs: Vec < ( _ , _ ) > = price_pair_opts
174+ . iter_mut ( )
175+ . filter_map ( |pair| match pair {
176+ // Only use this pair if both accounts are Some
177+ ( Some ( state) , Some ( price) ) => Some ( ( state, price) ) ,
178+ _other => None ,
179+ } )
180+ . collect ( ) ;
189181
190- if price_pairs. len ( ) % 2 != 0 {
191- trace ! ( & format!(
192- "Uneven product/price count detected: {}" ,
193- price_pairs. len( )
194- ) ) ;
195- return Err ( ProgramError :: InvalidAccountData . into ( ) ) ;
196- }
197182
198- trace ! ( "{} Pyth symbols received" , price_pairs. len( ) / 2 ) ;
183+ trace ! ( "{} Pyth symbols received" , price_pairs. len( ) ) ;
199184
200- // Collect the validated symbols for batch serialization
201- let mut attestations = Vec :: with_capacity ( price_pairs. len ( ) / 2 ) ;
185+ // Collect the validated symbols here for batch serialization
186+ let mut attestations = Vec :: with_capacity ( price_pairs. len ( ) ) ;
202187
203- for pair in price_pairs. as_slice ( ) . chunks_exact ( 2 ) {
204- let product = pair[ 0 ] ;
205- let price = pair[ 1 ] ;
188+ for ( state, price) in price_pairs. into_iter ( ) {
189+ // Pyth must own the price
190+ if accs. config . pyth_owner != * price. owner {
191+ trace ! ( & format!(
192+ "Price {:?}: owner pubkey mismatch (expected pyth_owner {:?}, got unknown price owner {:?})" ,
193+ price, accs. config. pyth_owner, price. owner
194+ ) ) ;
195+ return Err ( SolitaireError :: InvalidOwner ( * price. owner ) ) ;
196+ }
206197
207- if accs. config . pyth_owner != * price. owner || accs. config . pyth_owner != * product. owner {
198+ // State pubkey must reproduce from the price id
199+ let state_addr_from_price = AttestationStatePDA :: key ( price. key , ctx. program_id ) ;
200+ if state_addr_from_price != * state. 0 . 0 . info ( ) . key {
208201 trace ! ( & format!(
209- "Pair {:?} - {:?}: pyth_owner pubkey mismatch (expected {:?}, got product owner {:?} and price owner {:?}" ,
210- product, price,
211- accs. config. pyth_owner, product. owner, price. owner
212- ) ) ;
213- return Err ( SolitaireError :: InvalidOwner ( * accs. pyth_price . owner ) ) ;
202+ "Price {:?}: pubkey does not produce the passed state account (expected {:?} from seeds, {:?} was passed)" ,
203+ price. key, state_addr_from_price, state. 0.0 . info( ) . key
204+ ) ) ;
205+ return Err ( ProgramError :: InvalidAccountData . into ( ) ) ;
214206 }
207+
215208 let attestation_time = accs. clock . unix_timestamp ;
216209
217210 let price_data_ref = price. try_borrow_data ( ) ?;
@@ -224,53 +217,63 @@ pub fn attest(ctx: &ExecutionContext, accs: &mut Attest, data: AttestData) -> So
224217 ProgramError :: InvalidAccountData
225218 } ) ?;
226219
227- // prev_publish_time is picked if the price is not trading
228- let last_trading_publish_time = match price_struct. agg . status {
220+ // Retrieve and rotate last_attested_tradind_publish_time
221+
222+ // Pick the value to store for the next attestation of this
223+ // symbol. We use the prev_ value if the symbol is not
224+ // currently being traded. The oracle marks the last known
225+ // trading timestamp with it.
226+ let new_last_attested_trading_publish_time = match price_struct. agg . status {
229227 PriceStatus :: Trading => price_struct. timestamp ,
230228 _ => price_struct. prev_timestamp ,
231229 } ;
232230
233- // Take a mut reference to this price's metadata
234- let state_entry: & mut AttestationState = accs
235- . attestation_state
236- . entries
237- . entry ( * price. key )
238- . or_insert ( AttestationState {
239- // Use the same value if no state
240- // exists for the symbol, the new value _becomes_ the
241- // last attested trading publish time
242- last_attested_trading_publish_time : last_trading_publish_time,
243- } ) ;
231+ // Retrieve the timestamp saved during the previous
232+ // attestation. Use the new_* value if no existind state is
233+ // present on-chain
234+ let current_last_attested_trading_publish_time = if state. 0 . 0 . is_initialized ( ) {
235+ // Use the existing on-chain value
236+ state. 0 . 0 . 1 . last_attested_trading_publish_time
237+ } else {
238+ // Fall back to the new value if the state is not initialized
239+ new_last_attested_trading_publish_time
240+ } ;
244241
242+ // Build an attestatioin struct for this symbol using the just decided current value
245243 let attestation = PriceAttestation :: from_pyth_price_struct (
246244 Identifier :: new ( price. key . to_bytes ( ) ) ,
247245 attestation_time,
248- state_entry . last_attested_trading_publish_time , // Used as last_attested_publish_time
246+ current_last_attested_trading_publish_time ,
249247 price_struct,
250248 ) ;
251249
252-
253- // update last_attested_publish_time with this price's
254- // publish_time. Yes, it may be redundant for the entry() used
255- // above in the rare first attestation edge case.
256- state_entry. last_attested_trading_publish_time = last_trading_publish_time;
257-
258- // The following check is crucial against poorly ordered
259- // account inputs, e.g. [Some(prod1), Some(price1),
260- // Some(prod2), None, None, Some(price)], interpreted by
261- // earlier logic as [(prod1, price1), (prod2, price3)].
262- //
263- // Failing to verify the product/price relationship could lead
264- // to mismatched product/price metadata, which would result in
265- // a false attestation.
266- if attestation. product_id . to_bytes ( ) != product. key . to_bytes ( ) {
267- trace ! ( & format!(
268- "Price's product_id does not match the pased account (points at {:?} instead)" ,
269- attestation. product_id
270- ) ) ;
271- return Err ( ProgramError :: InvalidAccountData . into ( ) ) ;
250+ // Save the new value for the next attestation of this symbol
251+ state. 0 . 0 . last_attested_trading_publish_time = new_last_attested_trading_publish_time;
252+
253+ // handling of last_attested_trading_publish_time ends here
254+
255+ if !state. 0 . 0 . is_initialized ( ) {
256+ // Serialize the state to learn account size for creation
257+ let state_serialized = state. 0 . 0 . 1 . try_to_vec ( ) ?;
258+
259+ let seeds = state. self_bumped_seeds ( price. key , ctx. program_id ) ;
260+ solitaire:: create_account (
261+ ctx,
262+ state. 0 . 0 . info ( ) ,
263+ accs. payer . key ,
264+ solitaire:: CreationLamports :: Exempt ,
265+ state_serialized. len ( ) ,
266+ ctx. program_id ,
267+ solitaire:: IsSigned :: SignedWithSeeds ( & [ seeds
268+ . iter ( )
269+ . map ( |s| s. as_slice ( ) )
270+ . collect :: < Vec < _ > > ( )
271+ . as_slice ( ) ] ) ,
272+ ) ?;
273+ trace ! ( "Attestation state init OK" ) ;
272274 }
273275
276+
274277 attestations. push ( attestation) ;
275278 }
276279
@@ -280,51 +283,6 @@ pub fn attest(ctx: &ExecutionContext, accs: &mut Attest, data: AttestData) -> So
280283
281284 trace ! ( "Attestations successfully created" ) ;
282285
283- // Serialize the state to calculate rent/account size adjustments
284- let serialized = accs. attestation_state . 1 . try_to_vec ( ) ?;
285-
286- if accs. attestation_state . is_initialized ( ) {
287- accs. attestation_state
288- . info ( )
289- . realloc ( serialized. len ( ) , false ) ?;
290- trace ! ( "Attestation state resize OK" ) ;
291-
292- let target_rent = CreationLamports :: Exempt . amount ( serialized. len ( ) ) ;
293- let current_rent = accs. attestation_state . info ( ) . lamports ( ) ;
294-
295- // Adjust rent, but only if there isn't enough
296- if target_rent > current_rent {
297- let transfer_amount = target_rent - current_rent;
298-
299- let transfer_ix = system_instruction:: transfer (
300- accs. payer . info ( ) . key ,
301- accs. attestation_state . info ( ) . key ,
302- transfer_amount,
303- ) ;
304-
305- invoke ( & transfer_ix, ctx. accounts ) ?;
306- }
307-
308- trace ! ( "Attestation state rent transfer OK" ) ;
309- } else {
310- let seeds = accs
311- . attestation_state
312- . self_bumped_seeds ( None , ctx. program_id ) ;
313- solitaire:: create_account (
314- ctx,
315- accs. attestation_state . info ( ) ,
316- accs. payer . key ,
317- solitaire:: CreationLamports :: Exempt ,
318- serialized. len ( ) ,
319- ctx. program_id ,
320- solitaire:: IsSigned :: SignedWithSeeds ( & [ seeds
321- . iter ( )
322- . map ( |s| s. as_slice ( ) )
323- . collect :: < Vec < _ > > ( )
324- . as_slice ( ) ] ) ,
325- ) ?;
326- trace ! ( "Attestation state init OK" ) ;
327- }
328286 let bridge_config = BridgeData :: try_from_slice ( & accs. wh_bridge . try_borrow_mut_data ( ) ?) ?. config ;
329287
330288 // Pay wormhole fee
0 commit comments