Skip to content

Commit

Permalink
feat(lazer): add fees to solana contract (#2146)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Riateche authored Dec 4, 2024
1 parent 65baa14 commit 9c49daf
Show file tree
Hide file tree
Showing 11 changed files with 5,739 additions and 784 deletions.
5,900 changes: 5,225 additions & 675 deletions lazer/Cargo.lock

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion lazer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ resolver = "2"
members = [
"sdk/rust/protocol",
"contracts/solana/programs/pyth-lazer-solana-contract",
"sdk/solana",
]

# TODO: only for solana programs
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
[package]
name = "pyth-lazer-solana-contract"
version = "0.1.0"
version = "0.2.0"
edition = "2021"
description = "Pyth Lazer Solana contract."
description = "Pyth Lazer Solana contract and SDK."
license = "Apache-2.0"
repository = "https://github.com/pyth-network/pyth-crosschain"

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

[dependencies]
pyth-lazer-protocol = { version = "0.1.0", path = "../../../../sdk/rust/protocol" }

anchor-lang = "0.30.1"
bytemuck = "1.20.0"
byteorder = "1.5.0"
thiserror = "2.0.3"

[dev-dependencies]
hex = "0.4.3"
solana-program-test = "1.18.26"
solana-sdk = "1.18.26"
tokio = { version = "1.40.0", features = ["full"] }
188 changes: 169 additions & 19 deletions lazer/contracts/solana/programs/pyth-lazer-solana-contract/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,34 @@
mod signature;

use {
anchor_lang::{prelude::*, solana_program::pubkey::PUBKEY_BYTES},
std::mem::size_of,
crate::signature::VerifiedMessage,
anchor_lang::{
prelude::*, solana_program::pubkey::PUBKEY_BYTES, system_program, Discriminator,
},
std::{io::Cursor, mem::size_of},
};

declare_id!("pytd2yyk641x7ak7mkaasSJVXh6YYZnC7wTmtgAyxPt");

pub mod storage {
use anchor_lang::prelude::{pubkey, Pubkey};
pub use {
crate::signature::{ed25519_program_args, Ed25519SignatureOffsets},
pyth_lazer_protocol as protocol,
};

pub const ID: Pubkey = pubkey!("3rdJbqfnagQ4yx9HXJViD4zc4xpiSqmFsKpPuSCQVyQL");
declare_id!("pytd2yyk641x7ak7mkaasSJVXh6YYZnC7wTmtgAyxPt");

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

assert_eq!(
Pubkey::find_program_address(&[STORAGE_SEED], &super::ID).0,
ID
);
}
#[test]
fn test_ids() {
assert_eq!(
Pubkey::find_program_address(&[STORAGE_SEED], &ID).0,
STORAGE_ID
);
}

pub const ANCHOR_DISCRIMINATOR_BYTES: usize = 8;
pub const MAX_NUM_TRUSTED_SIGNERS: usize = 2;
pub const SPACE_FOR_TRUSTED_SIGNERS: usize = 5;
pub const EXTRA_SPACE: usize = 100;

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

/// TODO: remove this legacy storage type
#[derive(AnchorDeserialize)]
pub struct StorageV010 {
pub top_authority: Pubkey,
pub num_trusted_signers: u8,
pub trusted_signers: [TrustedSignerInfo; MAX_NUM_TRUSTED_SIGNERS],
}

impl StorageV010 {
pub const SERIALIZED_LEN: usize = PUBKEY_BYTES
+ size_of::<u8>()
+ TrustedSignerInfo::SERIALIZED_LEN * MAX_NUM_TRUSTED_SIGNERS;

pub fn initialized_trusted_signers(&self) -> &[TrustedSignerInfo] {
&self.trusted_signers[0..usize::from(self.num_trusted_signers)]
}
}

#[account]
pub struct Storage {
pub top_authority: Pubkey,
pub treasury: Pubkey,
pub single_update_fee_in_lamports: u64,
pub num_trusted_signers: u8,
pub trusted_signers: [TrustedSignerInfo; MAX_NUM_TRUSTED_SIGNERS],
pub trusted_signers: [TrustedSignerInfo; SPACE_FOR_TRUSTED_SIGNERS],
pub _extra_space: [u8; EXTRA_SPACE],
}

impl Storage {
const SERIALIZED_LEN: usize = PUBKEY_BYTES
+ PUBKEY_BYTES
+ size_of::<u64>()
+ size_of::<u8>()
+ TrustedSignerInfo::SERIALIZED_LEN * MAX_NUM_TRUSTED_SIGNERS;
+ TrustedSignerInfo::SERIALIZED_LEN * SPACE_FOR_TRUSTED_SIGNERS
+ EXTRA_SPACE;

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

pub fn initialize(ctx: Context<Initialize>, top_authority: Pubkey) -> Result<()> {
pub fn initialize(
ctx: Context<Initialize>,
top_authority: Pubkey,
treasury: Pubkey,
) -> Result<()> {
ctx.accounts.storage.top_authority = top_authority;
ctx.accounts.storage.treasury = treasury;
ctx.accounts.storage.single_update_fee_in_lamports = 1;
Ok(())
}

pub fn migrate_from_0_1_0(ctx: Context<MigrateFrom010>, treasury: Pubkey) -> Result<()> {
let old_data = ctx.accounts.storage.data.borrow();
if old_data[0..ANCHOR_DISCRIMINATOR_BYTES] != Storage::DISCRIMINATOR {
return Err(ProgramError::InvalidAccountData.into());
}
let old_storage = StorageV010::deserialize(&mut &old_data[ANCHOR_DISCRIMINATOR_BYTES..])?;
if old_storage.top_authority != ctx.accounts.top_authority.key() {
return Err(ProgramError::MissingRequiredSignature.into());
}
drop(old_data);

let space = ANCHOR_DISCRIMINATOR_BYTES + Storage::SERIALIZED_LEN;
ctx.accounts.storage.realloc(space, false)?;
let min_lamports = Rent::get()?.minimum_balance(space);
if ctx.accounts.storage.lamports() < min_lamports {
return Err(ProgramError::AccountNotRentExempt.into());
}

let mut new_storage = Storage {
top_authority: old_storage.top_authority,
treasury,
single_update_fee_in_lamports: 1,
num_trusted_signers: old_storage.num_trusted_signers,
trusted_signers: Default::default(),
_extra_space: [0; EXTRA_SPACE],
};
new_storage.trusted_signers[..old_storage.trusted_signers.len()]
.copy_from_slice(&old_storage.trusted_signers);
new_storage.try_serialize(&mut Cursor::new(
&mut **ctx.accounts.storage.data.borrow_mut(),
))?;
Ok(())
}

Expand All @@ -66,6 +137,9 @@ pub mod pyth_lazer_solana_contract {
if num_trusted_signers > ctx.accounts.storage.trusted_signers.len() {
return Err(ProgramError::InvalidAccountData.into());
}
if num_trusted_signers > MAX_NUM_TRUSTED_SIGNERS {
return Err(ProgramError::InvalidAccountData.into());
}
let mut trusted_signers =
ctx.accounts.storage.trusted_signers[..num_trusted_signers].to_vec();
if expires_at == 0 {
Expand All @@ -92,6 +166,9 @@ pub mod pyth_lazer_solana_contract {
if trusted_signers.len() > ctx.accounts.storage.trusted_signers.len() {
return Err(ProgramError::AccountDataTooSmall.into());
}
if trusted_signers.len() > MAX_NUM_TRUSTED_SIGNERS {
return Err(ProgramError::InvalidInstructionData.into());
}

ctx.accounts.storage.trusted_signers = Default::default();
ctx.accounts.storage.trusted_signers[..trusted_signers.len()]
Expand All @@ -102,6 +179,47 @@ pub mod pyth_lazer_solana_contract {
.expect("num signers overflow");
Ok(())
}

/// Verifies a ed25519 signature on Solana by checking that the transaction contains
/// a correct call to the built-in `ed25519_program`.
///
/// - `message_data` is the signed message that is being verified.
/// - `ed25519_instruction_index` is the index of the `ed25519_program` instruction
/// within the transaction. This instruction must precede the current instruction.
/// - `signature_index` is the index of the signature within the inputs to the `ed25519_program`.
/// - `message_offset` is the offset of the signed message within the
/// input data for the current instruction.
pub fn verify_message(
ctx: Context<VerifyMessage>,
message_data: Vec<u8>,
ed25519_instruction_index: u16,
signature_index: u8,
message_offset: u16,
) -> Result<VerifiedMessage> {
system_program::transfer(
CpiContext::new(
ctx.accounts.system_program.to_account_info(),
system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.treasury.to_account_info(),
},
),
ctx.accounts.storage.single_update_fee_in_lamports,
)?;

signature::verify_message(
&ctx.accounts.storage,
&ctx.accounts.instructions_sysvar,
&message_data,
ed25519_instruction_index,
signature_index,
message_offset,
)
.map_err(|err| {
msg!("signature verification error: {:?}", err);
err.into()
})
}
}

#[derive(Accounts)]
Expand All @@ -111,14 +229,27 @@ pub struct Initialize<'info> {
#[account(
init,
payer = payer,
space = 8 + Storage::SERIALIZED_LEN,
space = ANCHOR_DISCRIMINATOR_BYTES + Storage::SERIALIZED_LEN,
seeds = [STORAGE_SEED],
bump,
)]
pub storage: Account<'info, Storage>,
pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct MigrateFrom010<'info> {
pub top_authority: Signer<'info>,
#[account(
mut,
seeds = [STORAGE_SEED],
bump,
)]
/// CHECK: top_authority in storage must match top_authority account.
pub storage: AccountInfo<'info>,
pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct Update<'info> {
pub top_authority: Signer<'info>,
Expand All @@ -130,3 +261,22 @@ pub struct Update<'info> {
)]
pub storage: Account<'info, Storage>,
}

#[derive(Accounts)]
pub struct VerifyMessage<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
seeds = [STORAGE_SEED],
bump,
has_one = treasury
)]
pub storage: Account<'info, Storage>,
/// CHECK: this account doesn't need additional constraints.
pub treasury: AccountInfo<'info>,
pub system_program: Program<'info, System>,
/// CHECK: account ID is checked in Solana SDK during calls
/// (e.g. in `sysvar::instructions::load_instruction_at_checked`).
/// This account is not usable with anchor's `Program` account type because it's not executable.
pub instructions_sysvar: AccountInfo<'info>,
}
Loading

0 comments on commit 9c49daf

Please sign in to comment.