Skip to content

Commit 9c49daf

Browse files
authored
feat(lazer): add fees to solana contract (#2146)
* refactor(lazer): change verify_message to a contract call, add test * feat(lazer): add treasury and fees to solana contract * feat(lazer): make treasury configurable, add migration * feat(lazer): add extra space to contract's pda * fix(lazer): keep old names for legacy structs for compatibility * refactor(lazer): keep good names for new types and methods
1 parent 65baa14 commit 9c49daf

File tree

11 files changed

+5739
-784
lines changed

11 files changed

+5739
-784
lines changed

lazer/Cargo.lock

Lines changed: 5225 additions & 675 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lazer/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ resolver = "2"
33
members = [
44
"sdk/rust/protocol",
55
"contracts/solana/programs/pyth-lazer-solana-contract",
6-
"sdk/solana",
76
]
87

98
# TODO: only for solana programs

lazer/contracts/solana/programs/pyth-lazer-solana-contract/Cargo.toml

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
[package]
22
name = "pyth-lazer-solana-contract"
3-
version = "0.1.0"
3+
version = "0.2.0"
44
edition = "2021"
5-
description = "Pyth Lazer Solana contract."
5+
description = "Pyth Lazer Solana contract and SDK."
66
license = "Apache-2.0"
77
repository = "https://github.com/pyth-network/pyth-crosschain"
88

@@ -19,4 +19,15 @@ no-log-ix-name = []
1919
idl-build = ["anchor-lang/idl-build"]
2020

2121
[dependencies]
22+
pyth-lazer-protocol = { version = "0.1.0", path = "../../../../sdk/rust/protocol" }
23+
2224
anchor-lang = "0.30.1"
25+
bytemuck = "1.20.0"
26+
byteorder = "1.5.0"
27+
thiserror = "2.0.3"
28+
29+
[dev-dependencies]
30+
hex = "0.4.3"
31+
solana-program-test = "1.18.26"
32+
solana-sdk = "1.18.26"
33+
tokio = { version = "1.40.0", features = ["full"] }

lazer/contracts/solana/programs/pyth-lazer-solana-contract/src/lib.rs

Lines changed: 169 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,34 @@
1+
mod signature;
2+
13
use {
2-
anchor_lang::{prelude::*, solana_program::pubkey::PUBKEY_BYTES},
3-
std::mem::size_of,
4+
crate::signature::VerifiedMessage,
5+
anchor_lang::{
6+
prelude::*, solana_program::pubkey::PUBKEY_BYTES, system_program, Discriminator,
7+
},
8+
std::{io::Cursor, mem::size_of},
49
};
510

6-
declare_id!("pytd2yyk641x7ak7mkaasSJVXh6YYZnC7wTmtgAyxPt");
7-
8-
pub mod storage {
9-
use anchor_lang::prelude::{pubkey, Pubkey};
11+
pub use {
12+
crate::signature::{ed25519_program_args, Ed25519SignatureOffsets},
13+
pyth_lazer_protocol as protocol,
14+
};
1015

11-
pub const ID: Pubkey = pubkey!("3rdJbqfnagQ4yx9HXJViD4zc4xpiSqmFsKpPuSCQVyQL");
16+
declare_id!("pytd2yyk641x7ak7mkaasSJVXh6YYZnC7wTmtgAyxPt");
1217

13-
#[test]
14-
fn test_storage_id() {
15-
use {crate::STORAGE_SEED, anchor_lang::prelude::Pubkey};
18+
pub const STORAGE_ID: Pubkey = pubkey!("3rdJbqfnagQ4yx9HXJViD4zc4xpiSqmFsKpPuSCQVyQL");
1619

17-
assert_eq!(
18-
Pubkey::find_program_address(&[STORAGE_SEED], &super::ID).0,
19-
ID
20-
);
21-
}
20+
#[test]
21+
fn test_ids() {
22+
assert_eq!(
23+
Pubkey::find_program_address(&[STORAGE_SEED], &ID).0,
24+
STORAGE_ID
25+
);
2226
}
2327

28+
pub const ANCHOR_DISCRIMINATOR_BYTES: usize = 8;
2429
pub const MAX_NUM_TRUSTED_SIGNERS: usize = 2;
30+
pub const SPACE_FOR_TRUSTED_SIGNERS: usize = 5;
31+
pub const EXTRA_SPACE: usize = 100;
2532

2633
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, AnchorSerialize, AnchorDeserialize)]
2734
pub struct TrustedSignerInfo {
@@ -33,17 +40,41 @@ impl TrustedSignerInfo {
3340
const SERIALIZED_LEN: usize = PUBKEY_BYTES + size_of::<i64>();
3441
}
3542

43+
/// TODO: remove this legacy storage type
44+
#[derive(AnchorDeserialize)]
45+
pub struct StorageV010 {
46+
pub top_authority: Pubkey,
47+
pub num_trusted_signers: u8,
48+
pub trusted_signers: [TrustedSignerInfo; MAX_NUM_TRUSTED_SIGNERS],
49+
}
50+
51+
impl StorageV010 {
52+
pub const SERIALIZED_LEN: usize = PUBKEY_BYTES
53+
+ size_of::<u8>()
54+
+ TrustedSignerInfo::SERIALIZED_LEN * MAX_NUM_TRUSTED_SIGNERS;
55+
56+
pub fn initialized_trusted_signers(&self) -> &[TrustedSignerInfo] {
57+
&self.trusted_signers[0..usize::from(self.num_trusted_signers)]
58+
}
59+
}
60+
3661
#[account]
3762
pub struct Storage {
3863
pub top_authority: Pubkey,
64+
pub treasury: Pubkey,
65+
pub single_update_fee_in_lamports: u64,
3966
pub num_trusted_signers: u8,
40-
pub trusted_signers: [TrustedSignerInfo; MAX_NUM_TRUSTED_SIGNERS],
67+
pub trusted_signers: [TrustedSignerInfo; SPACE_FOR_TRUSTED_SIGNERS],
68+
pub _extra_space: [u8; EXTRA_SPACE],
4169
}
4270

4371
impl Storage {
4472
const SERIALIZED_LEN: usize = PUBKEY_BYTES
73+
+ PUBKEY_BYTES
74+
+ size_of::<u64>()
4575
+ size_of::<u8>()
46-
+ TrustedSignerInfo::SERIALIZED_LEN * MAX_NUM_TRUSTED_SIGNERS;
76+
+ TrustedSignerInfo::SERIALIZED_LEN * SPACE_FOR_TRUSTED_SIGNERS
77+
+ EXTRA_SPACE;
4778

4879
pub fn initialized_trusted_signers(&self) -> &[TrustedSignerInfo] {
4980
&self.trusted_signers[0..usize::from(self.num_trusted_signers)]
@@ -56,8 +87,48 @@ pub const STORAGE_SEED: &[u8] = b"storage";
5687
pub mod pyth_lazer_solana_contract {
5788
use super::*;
5889

59-
pub fn initialize(ctx: Context<Initialize>, top_authority: Pubkey) -> Result<()> {
90+
pub fn initialize(
91+
ctx: Context<Initialize>,
92+
top_authority: Pubkey,
93+
treasury: Pubkey,
94+
) -> Result<()> {
6095
ctx.accounts.storage.top_authority = top_authority;
96+
ctx.accounts.storage.treasury = treasury;
97+
ctx.accounts.storage.single_update_fee_in_lamports = 1;
98+
Ok(())
99+
}
100+
101+
pub fn migrate_from_0_1_0(ctx: Context<MigrateFrom010>, treasury: Pubkey) -> Result<()> {
102+
let old_data = ctx.accounts.storage.data.borrow();
103+
if old_data[0..ANCHOR_DISCRIMINATOR_BYTES] != Storage::DISCRIMINATOR {
104+
return Err(ProgramError::InvalidAccountData.into());
105+
}
106+
let old_storage = StorageV010::deserialize(&mut &old_data[ANCHOR_DISCRIMINATOR_BYTES..])?;
107+
if old_storage.top_authority != ctx.accounts.top_authority.key() {
108+
return Err(ProgramError::MissingRequiredSignature.into());
109+
}
110+
drop(old_data);
111+
112+
let space = ANCHOR_DISCRIMINATOR_BYTES + Storage::SERIALIZED_LEN;
113+
ctx.accounts.storage.realloc(space, false)?;
114+
let min_lamports = Rent::get()?.minimum_balance(space);
115+
if ctx.accounts.storage.lamports() < min_lamports {
116+
return Err(ProgramError::AccountNotRentExempt.into());
117+
}
118+
119+
let mut new_storage = Storage {
120+
top_authority: old_storage.top_authority,
121+
treasury,
122+
single_update_fee_in_lamports: 1,
123+
num_trusted_signers: old_storage.num_trusted_signers,
124+
trusted_signers: Default::default(),
125+
_extra_space: [0; EXTRA_SPACE],
126+
};
127+
new_storage.trusted_signers[..old_storage.trusted_signers.len()]
128+
.copy_from_slice(&old_storage.trusted_signers);
129+
new_storage.try_serialize(&mut Cursor::new(
130+
&mut **ctx.accounts.storage.data.borrow_mut(),
131+
))?;
61132
Ok(())
62133
}
63134

@@ -66,6 +137,9 @@ pub mod pyth_lazer_solana_contract {
66137
if num_trusted_signers > ctx.accounts.storage.trusted_signers.len() {
67138
return Err(ProgramError::InvalidAccountData.into());
68139
}
140+
if num_trusted_signers > MAX_NUM_TRUSTED_SIGNERS {
141+
return Err(ProgramError::InvalidAccountData.into());
142+
}
69143
let mut trusted_signers =
70144
ctx.accounts.storage.trusted_signers[..num_trusted_signers].to_vec();
71145
if expires_at == 0 {
@@ -92,6 +166,9 @@ pub mod pyth_lazer_solana_contract {
92166
if trusted_signers.len() > ctx.accounts.storage.trusted_signers.len() {
93167
return Err(ProgramError::AccountDataTooSmall.into());
94168
}
169+
if trusted_signers.len() > MAX_NUM_TRUSTED_SIGNERS {
170+
return Err(ProgramError::InvalidInstructionData.into());
171+
}
95172

96173
ctx.accounts.storage.trusted_signers = Default::default();
97174
ctx.accounts.storage.trusted_signers[..trusted_signers.len()]
@@ -102,6 +179,47 @@ pub mod pyth_lazer_solana_contract {
102179
.expect("num signers overflow");
103180
Ok(())
104181
}
182+
183+
/// Verifies a ed25519 signature on Solana by checking that the transaction contains
184+
/// a correct call to the built-in `ed25519_program`.
185+
///
186+
/// - `message_data` is the signed message that is being verified.
187+
/// - `ed25519_instruction_index` is the index of the `ed25519_program` instruction
188+
/// within the transaction. This instruction must precede the current instruction.
189+
/// - `signature_index` is the index of the signature within the inputs to the `ed25519_program`.
190+
/// - `message_offset` is the offset of the signed message within the
191+
/// input data for the current instruction.
192+
pub fn verify_message(
193+
ctx: Context<VerifyMessage>,
194+
message_data: Vec<u8>,
195+
ed25519_instruction_index: u16,
196+
signature_index: u8,
197+
message_offset: u16,
198+
) -> Result<VerifiedMessage> {
199+
system_program::transfer(
200+
CpiContext::new(
201+
ctx.accounts.system_program.to_account_info(),
202+
system_program::Transfer {
203+
from: ctx.accounts.payer.to_account_info(),
204+
to: ctx.accounts.treasury.to_account_info(),
205+
},
206+
),
207+
ctx.accounts.storage.single_update_fee_in_lamports,
208+
)?;
209+
210+
signature::verify_message(
211+
&ctx.accounts.storage,
212+
&ctx.accounts.instructions_sysvar,
213+
&message_data,
214+
ed25519_instruction_index,
215+
signature_index,
216+
message_offset,
217+
)
218+
.map_err(|err| {
219+
msg!("signature verification error: {:?}", err);
220+
err.into()
221+
})
222+
}
105223
}
106224

107225
#[derive(Accounts)]
@@ -111,14 +229,27 @@ pub struct Initialize<'info> {
111229
#[account(
112230
init,
113231
payer = payer,
114-
space = 8 + Storage::SERIALIZED_LEN,
232+
space = ANCHOR_DISCRIMINATOR_BYTES + Storage::SERIALIZED_LEN,
115233
seeds = [STORAGE_SEED],
116234
bump,
117235
)]
118236
pub storage: Account<'info, Storage>,
119237
pub system_program: Program<'info, System>,
120238
}
121239

240+
#[derive(Accounts)]
241+
pub struct MigrateFrom010<'info> {
242+
pub top_authority: Signer<'info>,
243+
#[account(
244+
mut,
245+
seeds = [STORAGE_SEED],
246+
bump,
247+
)]
248+
/// CHECK: top_authority in storage must match top_authority account.
249+
pub storage: AccountInfo<'info>,
250+
pub system_program: Program<'info, System>,
251+
}
252+
122253
#[derive(Accounts)]
123254
pub struct Update<'info> {
124255
pub top_authority: Signer<'info>,
@@ -130,3 +261,22 @@ pub struct Update<'info> {
130261
)]
131262
pub storage: Account<'info, Storage>,
132263
}
264+
265+
#[derive(Accounts)]
266+
pub struct VerifyMessage<'info> {
267+
#[account(mut)]
268+
pub payer: Signer<'info>,
269+
#[account(
270+
seeds = [STORAGE_SEED],
271+
bump,
272+
has_one = treasury
273+
)]
274+
pub storage: Account<'info, Storage>,
275+
/// CHECK: this account doesn't need additional constraints.
276+
pub treasury: AccountInfo<'info>,
277+
pub system_program: Program<'info, System>,
278+
/// CHECK: account ID is checked in Solana SDK during calls
279+
/// (e.g. in `sysvar::instructions::load_instruction_at_checked`).
280+
/// This account is not usable with anchor's `Program` account type because it's not executable.
281+
pub instructions_sysvar: AccountInfo<'info>,
282+
}

0 commit comments

Comments
 (0)