1#![allow(deprecated, unexpected_cfgs)] mod signature;
4
5use {
6 crate::signature::VerifiedMessage,
7 anchor_lang::{
8 prelude::*,
9 solana_program::{keccak, pubkey::PUBKEY_BYTES, secp256k1_recover::secp256k1_recover},
10 system_program,
11 },
12 std::mem::size_of,
13};
14
15pub use {
16 crate::signature::{ed25519_program_args, Ed25519SignatureOffsets},
17 pyth_lazer_protocol as protocol,
18};
19
20declare_id!("pytd2yyk641x7ak7mkaasSJVXh6YYZnC7wTmtgAyxPt");
21
22pub const STORAGE_ID: Pubkey = pubkey!("3rdJbqfnagQ4yx9HXJViD4zc4xpiSqmFsKpPuSCQVyQL");
23
24#[test]
25fn test_ids() {
26 assert_eq!(
27 Pubkey::find_program_address(&[STORAGE_SEED], &ID).0,
28 STORAGE_ID
29 );
30}
31
32pub const ANCHOR_DISCRIMINATOR_BYTES: usize = 8;
33pub const MAX_NUM_TRUSTED_SIGNERS: usize = 2;
34pub const SPACE_FOR_TRUSTED_SIGNERS: usize = 5;
35pub const SPACE_FOR_TRUSTED_ECDSA_SIGNERS: usize = 2;
36pub const EXTRA_SPACE: usize = 43;
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, AnchorSerialize, AnchorDeserialize)]
39pub struct TrustedSignerInfo<T> {
40 pub pubkey: T,
41 pub expires_at: i64,
42}
43
44pub const EVM_ADDRESS_LEN: usize = 20;
45pub type EvmAddress = [u8; EVM_ADDRESS_LEN];
46
47#[account]
48#[derive(Debug, PartialEq)]
49pub struct Storage {
50 pub top_authority: Pubkey,
51 pub treasury: Pubkey,
52 pub single_update_fee_in_lamports: u64,
53 pub num_trusted_signers: u8,
54 pub trusted_signers: [TrustedSignerInfo<Pubkey>; SPACE_FOR_TRUSTED_SIGNERS],
55 pub num_trusted_ecdsa_signers: u8,
56 pub trusted_ecdsa_signers: [TrustedSignerInfo<EvmAddress>; SPACE_FOR_TRUSTED_ECDSA_SIGNERS],
57 pub _extra_space: [u8; EXTRA_SPACE],
58}
59
60#[test]
61fn storage_size() {
62 assert_eq!(Storage::SERIALIZED_LEN, 373);
65}
66
67impl Storage {
68 const SERIALIZED_LEN: usize = PUBKEY_BYTES
69 + PUBKEY_BYTES
70 + size_of::<u64>()
71 + size_of::<u8>()
72 + (PUBKEY_BYTES + size_of::<i64>()) * SPACE_FOR_TRUSTED_SIGNERS
73 + size_of::<u8>()
74 + (EVM_ADDRESS_LEN + size_of::<i64>()) * SPACE_FOR_TRUSTED_ECDSA_SIGNERS
75 + EXTRA_SPACE;
76
77 pub fn initialized_trusted_signers(&self) -> &[TrustedSignerInfo<Pubkey>] {
78 &self.trusted_signers[0..usize::from(self.num_trusted_signers)]
79 }
80
81 pub fn initialized_trusted_ecdsa_signers(&self) -> &[TrustedSignerInfo<EvmAddress>] {
82 &self.trusted_ecdsa_signers[0..usize::from(self.num_trusted_ecdsa_signers)]
83 }
84
85 pub fn is_trusted(&self, signer: &Pubkey) -> std::result::Result<bool, ProgramError> {
86 let now = Clock::get()?.unix_timestamp;
87
88 Ok(self
89 .initialized_trusted_signers()
90 .iter()
91 .any(|s| &s.pubkey == signer && s.expires_at > now))
92 }
93
94 pub fn is_ecdsa_trusted(&self, signer: &EvmAddress) -> std::result::Result<bool, ProgramError> {
95 let now = Clock::get()?.unix_timestamp;
96
97 Ok(self
98 .initialized_trusted_ecdsa_signers()
99 .iter()
100 .any(|s| &s.pubkey == signer && s.expires_at > now))
101 }
102}
103
104pub const STORAGE_SEED: &[u8] = b"storage";
105
106#[program]
107pub mod pyth_lazer_solana_contract {
108 use pyth_lazer_protocol::message::LeEcdsaMessage;
109
110 use super::*;
111
112 pub fn initialize(
113 ctx: Context<Initialize>,
114 top_authority: Pubkey,
115 treasury: Pubkey,
116 ) -> Result<()> {
117 ctx.accounts.storage.top_authority = top_authority;
118 ctx.accounts.storage.treasury = treasury;
119 ctx.accounts.storage.single_update_fee_in_lamports = 1;
120 Ok(())
121 }
122
123 pub fn update(ctx: Context<Update>, trusted_signer: Pubkey, expires_at: i64) -> Result<()> {
124 let storage = &mut *ctx.accounts.storage;
125 update_trusted_signer(
126 &mut storage.num_trusted_signers,
127 &mut storage.trusted_signers,
128 trusted_signer,
129 expires_at,
130 )
131 }
132
133 pub fn update_ecdsa_signer(
134 ctx: Context<Update>,
135 trusted_signer: EvmAddress,
136 expires_at: i64,
137 ) -> Result<()> {
138 let storage = &mut *ctx.accounts.storage;
139 update_trusted_signer(
140 &mut storage.num_trusted_ecdsa_signers,
141 &mut storage.trusted_ecdsa_signers,
142 trusted_signer,
143 expires_at,
144 )
145 }
146
147 pub fn verify_message(
157 ctx: Context<VerifyMessage>,
158 message_data: Vec<u8>,
159 ed25519_instruction_index: u16,
160 signature_index: u8,
161 ) -> Result<VerifiedMessage> {
162 system_program::transfer(
163 CpiContext::new(
164 ctx.accounts.system_program.to_account_info(),
165 system_program::Transfer {
166 from: ctx.accounts.payer.to_account_info(),
167 to: ctx.accounts.treasury.to_account_info(),
168 },
169 ),
170 ctx.accounts.storage.single_update_fee_in_lamports,
171 )?;
172
173 signature::verify_message(
174 &ctx.accounts.storage,
175 &ctx.accounts.instructions_sysvar,
176 &message_data,
177 ed25519_instruction_index,
178 signature_index,
179 )
180 .map_err(|err| {
181 msg!("signature verification error: {:?}", err);
182 err.into()
183 })
184 }
185
186 pub fn verify_ecdsa_message(
187 ctx: Context<VerifyEcdsaMessage>,
188 message_data: Vec<u8>,
189 ) -> Result<()> {
190 system_program::transfer(
191 CpiContext::new(
192 ctx.accounts.system_program.to_account_info(),
193 system_program::Transfer {
194 from: ctx.accounts.payer.to_account_info(),
195 to: ctx.accounts.treasury.to_account_info(),
196 },
197 ),
198 ctx.accounts.storage.single_update_fee_in_lamports,
199 )?;
200
201 let message = LeEcdsaMessage::deserialize_slice(&message_data)
202 .map_err(|_| ProgramError::InvalidInstructionData)?;
203
204 let pubkey = secp256k1_recover(
205 &keccak::hash(&message.payload).0,
206 message.recovery_id,
207 &message.signature,
208 )
209 .map_err(|err| {
210 msg!("secp256k1_recover failed: {:?}", err);
211 ProgramError::InvalidInstructionData
212 })?;
213 let addr: EvmAddress = keccak::hash(&pubkey.0).0[12..]
214 .try_into()
215 .expect("invalid addr len");
216 if addr == EvmAddress::default() {
217 msg!("secp256k1_recover failed: zero output");
218 return Err(ProgramError::InvalidInstructionData.into());
219 }
220 if !ctx.accounts.storage.is_ecdsa_trusted(&addr)? {
221 msg!("untrusted signer: {:?}", addr);
222 return Err(ProgramError::MissingRequiredSignature.into());
223 }
224 Ok(())
225 }
226}
227
228#[derive(Accounts)]
229pub struct Initialize<'info> {
230 #[account(mut)]
231 pub payer: Signer<'info>,
232 #[account(
233 init,
234 payer = payer,
235 space = ANCHOR_DISCRIMINATOR_BYTES + Storage::SERIALIZED_LEN,
236 seeds = [STORAGE_SEED],
237 bump,
238 )]
239 pub storage: Account<'info, Storage>,
240 pub system_program: Program<'info, System>,
241}
242
243#[derive(Accounts)]
244pub struct Update<'info> {
245 pub top_authority: Signer<'info>,
246 #[account(
247 mut,
248 seeds = [STORAGE_SEED],
249 bump,
250 has_one = top_authority,
251 )]
252 pub storage: Account<'info, Storage>,
253}
254
255#[derive(Accounts)]
256pub struct VerifyMessage<'info> {
257 #[account(mut)]
258 pub payer: Signer<'info>,
259 #[account(
260 seeds = [STORAGE_SEED],
261 bump,
262 has_one = treasury
263 )]
264 pub storage: Account<'info, Storage>,
265 #[account(mut)]
267 pub treasury: AccountInfo<'info>,
268 pub system_program: Program<'info, System>,
269 pub instructions_sysvar: AccountInfo<'info>,
273}
274
275#[derive(Accounts)]
276pub struct VerifyEcdsaMessage<'info> {
277 #[account(mut)]
278 pub payer: Signer<'info>,
279 #[account(
280 seeds = [STORAGE_SEED],
281 bump,
282 has_one = treasury
283 )]
284 pub storage: Account<'info, Storage>,
285 #[account(mut)]
287 pub treasury: AccountInfo<'info>,
288 pub system_program: Program<'info, System>,
289}
290
291fn update_trusted_signer<T: Copy + PartialEq + Default>(
292 stored_num_trusted_signers: &mut u8,
293 stored_trusted_signers: &mut [TrustedSignerInfo<T>],
294 trusted_signer: T,
295 expires_at: i64,
296) -> Result<()> {
297 let num_trusted_signers: usize = (*stored_num_trusted_signers).into();
298 if num_trusted_signers > stored_trusted_signers.len() {
299 return Err(ProgramError::InvalidAccountData.into());
300 }
301 if num_trusted_signers > MAX_NUM_TRUSTED_SIGNERS {
302 return Err(ProgramError::InvalidAccountData.into());
303 }
304 let mut trusted_signers = stored_trusted_signers[..num_trusted_signers].to_vec();
305 if expires_at == 0 {
306 let pos = trusted_signers
308 .iter()
309 .position(|item| item.pubkey == trusted_signer)
310 .ok_or(ProgramError::InvalidInstructionData)?;
311 trusted_signers.remove(pos);
312 } else if let Some(item) = trusted_signers
313 .iter_mut()
314 .find(|item| item.pubkey == trusted_signer)
315 {
316 item.expires_at = expires_at;
318 } else {
319 trusted_signers.push(TrustedSignerInfo {
321 pubkey: trusted_signer,
322 expires_at,
323 });
324 }
325
326 if trusted_signers.len() > stored_trusted_signers.len() {
327 return Err(ProgramError::AccountDataTooSmall.into());
328 }
329 if trusted_signers.len() > MAX_NUM_TRUSTED_SIGNERS {
330 return Err(ProgramError::InvalidInstructionData.into());
331 }
332
333 stored_trusted_signers[..trusted_signers.len()].copy_from_slice(&trusted_signers);
334 for item in &mut stored_trusted_signers[trusted_signers.len()..] {
335 *item = Default::default();
336 }
337 *stored_num_trusted_signers = trusted_signers
338 .len()
339 .try_into()
340 .expect("num signers overflow");
341 Ok(())
342}
343
344#[test]
345fn test_storage_compat_after_adding_ecdsa() {
346 let data = [
348 209, 117, 255, 185, 196, 175, 68, 9, 221, 56, 75, 202, 174, 248, 122, 155, 212, 29, 112,
349 50, 82, 65, 161, 137, 16, 164, 61, 134, 119, 132, 149, 1, 178, 177, 3, 187, 25, 187, 143,
350 244, 233, 140, 161, 230, 115, 255, 214, 103, 208, 40, 16, 101, 45, 35, 153, 15, 145, 134,
351 250, 244, 248, 255, 51, 165, 169, 186, 183, 210, 155, 137, 30, 84, 1, 0, 0, 0, 0, 0, 0, 0,
352 1, 116, 49, 58, 101, 37, 237, 249, 153, 54, 170, 20, 119, 233, 76, 114, 188, 92, 198, 23,
353 178, 23, 69, 245, 240, 50, 150, 243, 21, 68, 97, 242, 20, 255, 255, 255, 255, 255, 255,
354 255, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
355 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
356 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
357 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
358 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
359 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
360 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
361 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
362 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
363 ];
364 let storage = Storage::deserialize(&mut &data[..]).unwrap();
365 assert_eq!(
366 storage,
367 Storage {
368 top_authority: pubkey!("F6eZvgfuPtncCUDzYgzaFPRodHwZXQHe1pC4kkyvkYwa"),
369 treasury: pubkey!("D2Y884NqR9TVagZftdzzuEgtTEwd3AsS2nLMHEnVkXCQ"),
370 single_update_fee_in_lamports: 6061433450835458729,
371 num_trusted_signers: 1,
372 trusted_signers: [
373 TrustedSignerInfo {
374 pubkey: pubkey!("1111111avyLnoUfmuX6KZaaTrSfth7n9tX4u4rVV"),
375 expires_at: 1509375770176493106
376 },
377 TrustedSignerInfo {
378 pubkey: pubkey!("JEKNVnkbo2qryGmQn1b2RCJcGKVCn6WvNZmFdEiZGVSo"),
379 expires_at: 0
380 },
381 TrustedSignerInfo {
382 pubkey: Pubkey::default(),
383 expires_at: 0
384 },
385 TrustedSignerInfo {
386 pubkey: Pubkey::default(),
387 expires_at: 0
388 },
389 TrustedSignerInfo {
390 pubkey: Pubkey::default(),
391 expires_at: 0
392 }
393 ],
394 num_trusted_ecdsa_signers: 0,
395 trusted_ecdsa_signers: [
396 TrustedSignerInfo {
397 pubkey: Default::default(),
398 expires_at: 0
399 },
400 TrustedSignerInfo {
401 pubkey: Default::default(),
402 expires_at: 0
403 },
404 ],
405 _extra_space: [0; 43],
406 }
407 );
408}