Skip to main content

pyth_lazer_solana_contract/
lib.rs

1#![allow(deprecated, unexpected_cfgs)] // anchor macro triggers it
2
3mod 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    // Keep the size the same when possible. If the size increases, we'll need to perform
63    // a migration that increases the account size on-chain.
64    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    /// Verifies a ed25519 signature on Solana by checking that the transaction contains
148    /// a correct call to the built-in `ed25519_program`.
149    ///
150    /// - `message_data` is the signed message that is being verified.
151    /// - `ed25519_instruction_index` is the index of the `ed25519_program` instruction
152    ///   within the transaction. This instruction must precede the current instruction.
153    /// - `signature_index` is the index of the signature within the inputs to the `ed25519_program`.
154    /// - `message_offset` is the offset of the signed message within the
155    ///   input data for the current instruction.
156    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    /// CHECK: this account doesn't need additional constraints.
266    #[account(mut)]
267    pub treasury: AccountInfo<'info>,
268    pub system_program: Program<'info, System>,
269    /// CHECK: account ID is checked in Solana SDK during calls
270    /// (e.g. in `sysvar::instructions::load_instruction_at_checked`).
271    /// This account is not usable with anchor's `Program` account type because it's not executable.
272    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    /// CHECK: this account doesn't need additional constraints.
286    #[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        // Delete
307        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        // Modify
317        item.expires_at = expires_at;
318    } else {
319        // Add
320        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    // This is data of a storage account created by the previous version of the contract.
347    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}