diff --git a/solana/Cargo.lock b/solana/Cargo.lock index 97e1756..5995265 100644 --- a/solana/Cargo.lock +++ b/solana/Cargo.lock @@ -735,6 +735,21 @@ dependencies = [ "udigest", ] +[[package]] +name = "axelar-solana-gas-service" +version = "0.1.0" +dependencies = [ + "axelar-solana-gateway-test-fixtures", + "borsh 1.5.3", + "bytemuck", + "gateway-event-stack", + "program-utils", + "solana-program", + "solana-program-test", + "solana-sdk", + "thiserror", +] + [[package]] name = "axelar-solana-gateway" version = "0.1.0" @@ -773,6 +788,7 @@ version = "0.1.0" dependencies = [ "axelar-executable", "axelar-solana-encoding", + "axelar-solana-gas-service", "axelar-solana-gateway", "bincode", "ed25519-dalek 2.1.1", @@ -2921,6 +2937,7 @@ dependencies = [ name = "gateway-event-stack" version = "0.1.0" dependencies = [ + "axelar-solana-gas-service", "axelar-solana-gateway", "base64 0.21.7", "pretty_assertions", @@ -4783,6 +4800,7 @@ name = "program-utils" version = "0.1.0" dependencies = [ "borsh 1.5.3", + "bytemuck", "rkyv", "solana-program", ] diff --git a/solana/Cargo.toml b/solana/Cargo.toml index 2e7b15e..b8241a8 100644 --- a/solana/Cargo.toml +++ b/solana/Cargo.toml @@ -128,6 +128,7 @@ async-recursion = "1" # solana programs axelar-solana-its = { path = "programs/axelar-solana-its" } +axelar-solana-gas-service = { path = "programs/axelar-solana-gas-service" } axelar-solana-memo-program = { path = "programs/axelar-solana-memo-program" } axelar-solana-memo-program-old = { path = "programs/axelar-solana-memo-program-old" } axelar-solana-multicall = { path = "programs/axelar-solana-multicall" } diff --git a/solana/crates/axelar-executable/src/lib.rs b/solana/crates/axelar-executable/src/lib.rs index 8a12ab4..ccfc21c 100644 --- a/solana/crates/axelar-executable/src/lib.rs +++ b/solana/crates/axelar-executable/src/lib.rs @@ -4,9 +4,9 @@ use core::borrow::Borrow; use core::str::FromStr; use axelar_solana_encoding::types::messages::Message; -use axelar_solana_gateway::get_validate_message_signing_pda; +use axelar_solana_gateway::error::GatewayError; use axelar_solana_gateway::state::incoming_message::{command_id, IncomingMessage}; -use axelar_solana_gateway::state::BytemuckedPda; +use axelar_solana_gateway::{get_validate_message_signing_pda, BytemuckedPda}; use num_traits::{FromPrimitive, ToPrimitive}; use rkyv::bytecheck::{self, CheckBytes}; use rkyv::{Deserialize, Infallible}; @@ -56,7 +56,8 @@ pub fn validate_message( let accounts_iter = &mut relayer_prepended_accs.iter(); let incoming_message_pda = next_account_info(accounts_iter)?; let incoming_message_data = incoming_message_pda.try_borrow_data()?; - let incoming_message = IncomingMessage::read(&incoming_message_data)?; + let incoming_message = IncomingMessage::read(&incoming_message_data) + .ok_or(GatewayError::BytemuckDataLenInvalid)?; incoming_message.signing_pda_bump }; @@ -106,7 +107,8 @@ pub fn validate_with_gmp_metadata( let accounts_iter = &mut accounts.iter(); let incoming_message_pda = next_account_info(accounts_iter)?; let incoming_message_data = incoming_message_pda.try_borrow_data()?; - let incoming_message = IncomingMessage::read(&incoming_message_data)?; + let incoming_message = IncomingMessage::read(&incoming_message_data) + .ok_or(GatewayError::BytemuckDataLenInvalid)?; incoming_message.signing_pda_bump }; diff --git a/solana/crates/axelar-solana-gateway-test-fixtures/Cargo.toml b/solana/crates/axelar-solana-gateway-test-fixtures/Cargo.toml index fe6227b..063bd4d 100644 --- a/solana/crates/axelar-solana-gateway-test-fixtures/Cargo.toml +++ b/solana/crates/axelar-solana-gateway-test-fixtures/Cargo.toml @@ -18,6 +18,7 @@ libsecp256k1.workspace = true rand.workspace = true libsecp-rand.workspace = true gateway-event-stack.workspace = true +axelar-solana-gas-service.workspace = true [lints] workspace = true diff --git a/solana/crates/axelar-solana-gateway-test-fixtures/src/base.rs b/solana/crates/axelar-solana-gateway-test-fixtures/src/base.rs index ea68646..5aa9706 100644 --- a/solana/crates/axelar-solana-gateway-test-fixtures/src/base.rs +++ b/solana/crates/axelar-solana-gateway-test-fixtures/src/base.rs @@ -16,6 +16,7 @@ use solana_sdk::instruction::Instruction; use solana_sdk::signature::Keypair; use solana_sdk::signer::Signer as _; use solana_sdk::signers::Signers; +use solana_sdk::system_instruction; use solana_sdk::transaction::Transaction; /// Base test fixture wrapper that's agnostic to the Axelar Solana Gateway, it @@ -268,6 +269,13 @@ impl TestFixture { Some(_) => Err(BanksClientError::ClientError("unexpected account owner")), } } + + /// Funds the account using the `self.payer` as the bank + pub async fn fund_account(&mut self, to: &Pubkey, amount: u64) { + let from = self.payer.pubkey(); + let ix = system_instruction::transfer(&from, to, amount); + self.send_tx(&[ix]).await.expect("failed to fund account"); + } } /// Utility triat to find a specific log within the diff --git a/solana/crates/axelar-solana-gateway-test-fixtures/src/gas_service.rs b/solana/crates/axelar-solana-gateway-test-fixtures/src/gas_service.rs new file mode 100644 index 0000000..1a49da2 --- /dev/null +++ b/solana/crates/axelar-solana-gateway-test-fixtures/src/gas_service.rs @@ -0,0 +1,114 @@ +//! Utilities for working with the Axelar gas service + +use crate::base::TestFixture; +use axelar_solana_gas_service::processor::GasServiceEvent; +use axelar_solana_gateway::BytemuckedPda; +use gateway_event_stack::{MatchContext, ProgramInvocationState}; +use solana_program_test::{tokio, BanksTransactionResultWithMetadata}; +use solana_sdk::{ + account::ReadableAccount, keccak, pubkey::Pubkey, signature::Keypair, signer::Signer, +}; + +/// Utility structure for keeping gas service related state +pub struct GasServiceUtils { + /// upgrade authority of the program + pub upgrade_authority: Keypair, + /// the config authorty + pub config_authority: Keypair, + /// PDA of the gas service config + pub config_pda: Pubkey, + /// salt to derive the config pda + pub salt: [u8; 32], +} + +impl TestFixture { + /// Deploy the gas service program and construct a pre-emptive + pub async fn deploy_gas_service(&mut self) -> GasServiceUtils { + // deploy gas service + let gas_service_bytecode = + tokio::fs::read("../../target/deploy/axelar_solana_gas_service.so") + .await + .unwrap(); + + // Generate a new keypair for the upgrade authority + let upgrade_authority = Keypair::new(); + + self.register_upgradeable_program( + &gas_service_bytecode, + &upgrade_authority.pubkey(), + &axelar_solana_gas_service::id(), + ) + .await; + + let config_authority = Keypair::new(); + let salt = keccak::hash(b"my gas service").0; + let (config_pda, ..) = axelar_solana_gas_service::get_config_pda( + &axelar_solana_gas_service::ID, + &salt, + &config_authority.pubkey(), + ); + + GasServiceUtils { + upgrade_authority, + config_authority, + config_pda, + salt, + } + } + + /// init the gas service + pub async fn init_gas_config( + &mut self, + utils: &GasServiceUtils, + ) -> Result { + self.init_gas_config_with_params( + utils.config_authority.pubkey(), + utils.config_pda, + utils.salt, + ) + .await + } + + /// init the gas service with raw params + pub async fn init_gas_config_with_params( + &mut self, + config_authority: Pubkey, + config_pda: Pubkey, + salt: [u8; 32], + ) -> Result { + let ix = axelar_solana_gas_service::instructions::init_config( + &axelar_solana_gas_service::ID, + &self.payer.pubkey(), + &config_authority, + &config_pda, + salt, + ) + .unwrap(); + self.send_tx(&[ix]).await + } + + /// get the gas service config pda state + pub async fn gas_service_config_state( + &mut self, + config_pda: Pubkey, + ) -> axelar_solana_gas_service::state::Config { + let acc = self + .get_account(&config_pda, &axelar_solana_gas_service::ID) + .await; + let config = axelar_solana_gas_service::state::Config::read(acc.data()).unwrap(); + *config + } +} + +/// Get events emitted by the `GasService` +#[must_use] +pub fn get_gas_service_events( + tx: &solana_program_test::BanksTransactionResultWithMetadata, +) -> Vec> { + let match_context = MatchContext::new(&axelar_solana_gas_service::ID.to_string()); + gateway_event_stack::build_program_event_stack( + &match_context, + tx.metadata.as_ref().unwrap().log_messages.as_slice(), + gateway_event_stack::parse_gas_service_log, + ) +} diff --git a/solana/crates/axelar-solana-gateway-test-fixtures/src/gateway.rs b/solana/crates/axelar-solana-gateway-test-fixtures/src/gateway.rs index 1a98e31..a4dae63 100644 --- a/solana/crates/axelar-solana-gateway-test-fixtures/src/gateway.rs +++ b/solana/crates/axelar-solana-gateway-test-fixtures/src/gateway.rs @@ -18,9 +18,10 @@ use axelar_solana_gateway::processor::GatewayEvent; use axelar_solana_gateway::state::incoming_message::{command_id, IncomingMessage}; use axelar_solana_gateway::state::signature_verification_pda::SignatureVerificationSessionData; use axelar_solana_gateway::state::verifier_set_tracker::VerifierSetTracker; -use axelar_solana_gateway::state::{BytemuckedPda, GatewayConfig}; +use axelar_solana_gateway::state::GatewayConfig; use axelar_solana_gateway::{ get_gateway_root_config_pda, get_incoming_message_pda, get_verifier_set_tracker_pda, + BytemuckedPda, }; pub use gateway_event_stack::{MatchContext, ProgramInvocationState}; use rand::Rng as _; diff --git a/solana/crates/axelar-solana-gateway-test-fixtures/src/lib.rs b/solana/crates/axelar-solana-gateway-test-fixtures/src/lib.rs index 2f37c74..3f3e484 100644 --- a/solana/crates/axelar-solana-gateway-test-fixtures/src/lib.rs +++ b/solana/crates/axelar-solana-gateway-test-fixtures/src/lib.rs @@ -3,8 +3,10 @@ #![allow(clippy::expect_used)] #![allow(clippy::missing_panics_doc)] #![allow(clippy::missing_errors_doc)] +#![allow(clippy::multiple_inherent_impl)] pub mod base; +pub mod gas_service; pub mod gateway; pub mod test_signer; diff --git a/solana/crates/gateway-event-stack/Cargo.toml b/solana/crates/gateway-event-stack/Cargo.toml index c1ceeff..5427160 100644 --- a/solana/crates/gateway-event-stack/Cargo.toml +++ b/solana/crates/gateway-event-stack/Cargo.toml @@ -9,6 +9,7 @@ edition.workspace = true [dependencies] axelar-solana-gateway.workspace = true +axelar-solana-gas-service.workspace = true tracing.workspace = true solana-sdk.workspace = true base64.workspace = true diff --git a/solana/crates/gateway-event-stack/src/lib.rs b/solana/crates/gateway-event-stack/src/lib.rs index 85883c4..4b15ee9 100644 --- a/solana/crates/gateway-event-stack/src/lib.rs +++ b/solana/crates/gateway-event-stack/src/lib.rs @@ -1,6 +1,7 @@ //! Parse Solana events from transaction data -use axelar_solana_gateway::processor::{EventParseError, GatewayEvent}; +use axelar_solana_gas_service::processor::GasServiceEvent; +use axelar_solana_gateway::processor::GatewayEvent; use base64::{engine::general_purpose, Engine}; /// Represents the state of a program invocation along with associated events. @@ -66,7 +67,7 @@ pub fn build_program_event_stack( ) -> Vec> where T: AsRef, - F: Fn(&mut [ProgramInvocationState], &T, usize) -> Result<(), Err>, + F: Fn(&T) -> Result, { let logs = logs.iter().enumerate(); let mut program_stack: Vec> = Vec::new(); @@ -82,8 +83,16 @@ where handle_failure_log(&mut program_stack); } else { // Process logs if inside a program invocation + let Some(&mut ProgramInvocationState::InProgress(ref mut events)) = + program_stack.last_mut() + else { + continue; + }; #[allow(clippy::let_underscore_must_use, clippy::let_underscore_untyped)] - let _ = transformer(&mut program_stack, log, idx); + let Ok(event) = transformer(log) else { + continue; + }; + events.push((idx, event)); } } program_stack @@ -100,31 +109,20 @@ pub fn decode_base64(input: &str) -> Option> { /// /// # Arguments /// -/// * `program_stack` - A mutable slice of program invocation states to update. /// * `log` - The log entry to parse. -/// * `idx` - The index of the log entry in the logs slice. -/// -/// # Returns -/// -/// `Ok(())` if parsing is successful, or an `EventParseError` if parsing fails. /// /// # Errors /// /// - if the discrimintant for the event is not present -/// - if the event was detected via the discriminant but the data does not match +/// - if the event was detected via the discriminant but the data does not match the discriminant type pub fn parse_gateway_logs( - program_stack: &mut [ProgramInvocationState], log: &T, - idx: usize, -) -> Result<(), EventParseError> +) -> Result where T: AsRef, { use axelar_solana_gateway::event_prefixes::*; - let Some(&mut ProgramInvocationState::InProgress(ref mut events)) = program_stack.last_mut() - else { - return Ok(()); - }; + use axelar_solana_gateway::processor::EventParseError; let mut logs = log .as_ref() @@ -166,11 +164,61 @@ where let event = axelar_solana_gateway::processor::VerifierSetRotated::new(logs)?; GatewayEvent::VerifierSetRotated(event) } - _ => return Ok(()), + _ => return Err(EventParseError::Other("unsupported discrimintant")), + }; + + Ok(gateway_event) +} + +/// Parses gas service logs and extracts events. +/// +/// # Arguments +/// +/// * `log` - The log entry to parse. +/// +/// # Errors +/// +/// - if the discrimintant for the event is not present +/// - if the event was detected via the discriminant but the data does not match the discriminant type +pub fn parse_gas_service_log( + log: &T, +) -> Result +where + T: AsRef, +{ + use axelar_solana_gas_service::event_prefixes::*; + use axelar_solana_gas_service::event_utils::EventParseError; + use axelar_solana_gas_service::processor::{ + NativeGasAddedEvent, NativeGasPaidForContractCallEvent, NativeGasRefundedEvent, + }; + + let mut logs = log + .as_ref() + .trim() + .trim_start_matches("Program data:") + .split_whitespace() + .filter_map(decode_base64); + let disc = logs + .next() + .ok_or(EventParseError::MissingData("discriminant"))?; + let disc = disc.as_slice(); + let gas_service_event = match disc { + NATIVE_GAS_PAID_FOR_CONTRACT_CALL => { + let event = NativeGasPaidForContractCallEvent::new(logs)?; + GasServiceEvent::NativeGasPaidForncontractCall(event) + } + NATIVE_GAS_ADDED => { + let event = NativeGasAddedEvent::new(logs)?; + GasServiceEvent::NativeGasAdded(event) + } + NATIVE_GAS_REFUNDED => { + let event = NativeGasRefundedEvent::new(logs)?; + GasServiceEvent::NativeGasRefunded(event) + } + _ => return Err(EventParseError::Other("unsupported discrimintant")), }; - events.push((idx, gateway_event)); - Ok(()) + Ok(gas_service_event) } /// Handles a failure log by marking the current program invocation as failed. diff --git a/solana/helpers/program-utils/Cargo.toml b/solana/helpers/program-utils/Cargo.toml index ea53e4a..631ec63 100644 --- a/solana/helpers/program-utils/Cargo.toml +++ b/solana/helpers/program-utils/Cargo.toml @@ -9,6 +9,7 @@ crate-type = ["cdylib", "lib"] [dependencies] rkyv = { workspace = true, features = ["validation"] } borsh.workspace = true +bytemuck.workspace = true solana-program.workspace = true [dev-dependencies] diff --git a/solana/helpers/program-utils/src/lib.rs b/solana/helpers/program-utils/src/lib.rs index 902d0d8..07228e6 100644 --- a/solana/helpers/program-utils/src/lib.rs +++ b/solana/helpers/program-utils/src/lib.rs @@ -6,6 +6,7 @@ use std::borrow::Borrow; use std::io::Write; use borsh::{BorshDeserialize, BorshSerialize}; +use bytemuck::{AnyBitPattern, NoUninit}; use rkyv::de::deserializers::SharedDeserializeMap; use rkyv::ser::serializers::AllocSerializer; use rkyv::validation::validators::DefaultValidator; @@ -448,3 +449,48 @@ where Self::unpack_from_slice(&account_data) } } + +/// A trait for types that can be safely converted to and from byte slices using `bytemuck`. +pub trait BytemuckedPda: Sized + NoUninit + AnyBitPattern { + /// Reads an immutable reference to `Self` from a byte slice. + /// + /// This method attempts to interpret the provided byte slice as an instance of `Self`. + /// It checks that the length of the slice matches the size of `Self` to ensure safety. + fn read(data: &[u8]) -> Option<&Self> { + let result: &Self = bytemuck::try_from_bytes(data) + .map_err(|err| { + msg!("bytemuck error {:?}", err); + err + }) + .ok()?; + Some(result) + } + + /// Reads a mutable reference to `Self` from a mutable byte slice. + /// + /// Similar to [`read`], but allows for mutation of the underlying data. + /// This is useful when you need to modify the data in place. + fn read_mut(data: &mut [u8]) -> Option<&mut Self> { + let result: &mut Self = bytemuck::try_from_bytes_mut(data) + .map_err(|err| { + msg!("bytemuck error {:?}", err); + err + }) + .ok()?; + Some(result) + } + + /// Writes the instance of `Self` into a mutable byte slice. + /// + /// This method serializes `self` into its byte representation and copies it into the + /// provided mutable byte slice. It ensures that the destination slice is of the correct + /// length to hold the data. + fn write(&self, data: &mut [u8]) -> Option<()> { + let self_bytes = bytemuck::bytes_of(self); + if data.len() != self_bytes.len() { + return None; + } + data.copy_from_slice(self_bytes); + Some(()) + } +} diff --git a/solana/helpers/test-fixtures/Cargo.toml b/solana/helpers/test-fixtures/Cargo.toml index b92dc55..eac60e7 100644 --- a/solana/helpers/test-fixtures/Cargo.toml +++ b/solana/helpers/test-fixtures/Cargo.toml @@ -18,8 +18,8 @@ axelar-rkyv-encoding = { workspace = true, features = ["test-fixtures"] } interchain-token-transfer-gmp = { workspace = true } solana-program-test = { workspace = true } -gas-service = { workspace = true, features = ["no-entrypoint"] } gateway = { workspace = true, features = ["no-entrypoint"] } +gas-service = { workspace = true, features = ["no-entrypoint"] } spl-token-2022 = { workspace = true, features = ["no-entrypoint"] } diff --git a/solana/programs/axelar-solana-gas-service/Cargo.toml b/solana/programs/axelar-solana-gas-service/Cargo.toml new file mode 100644 index 0000000..93eefb3 --- /dev/null +++ b/solana/programs/axelar-solana-gas-service/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "axelar-solana-gas-service" +version.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +license.workspace = true +edition.workspace = true + +[dependencies] +solana-program.workspace = true +bytemuck.workspace = true +borsh.workspace = true +program-utils.workspace = true +thiserror.workspace = true + +[dev-dependencies] +solana-sdk.workspace = true +solana-program-test.workspace = true +axelar-solana-gateway-test-fixtures.workspace = true +gateway-event-stack.workspace = true + +[lints] +workspace = true + +[lib] +crate-type = ["cdylib", "lib"] + +[features] +no-entrypoint = [] diff --git a/solana/programs/axelar-solana-gas-service/src/entrypoint.rs b/solana/programs/axelar-solana-gas-service/src/entrypoint.rs new file mode 100644 index 0000000..d02bd82 --- /dev/null +++ b/solana/programs/axelar-solana-gas-service/src/entrypoint.rs @@ -0,0 +1,7 @@ +//! Program entrypoint + +#![cfg(all(target_os = "solana", not(feature = "no-entrypoint")))] + +use crate::processor::process_instruction; + +solana_program::entrypoint!(process_instruction); diff --git a/solana/programs/axelar-solana-gas-service/src/instructions.rs b/solana/programs/axelar-solana-gas-service/src/instructions.rs new file mode 100644 index 0000000..c852867 --- /dev/null +++ b/solana/programs/axelar-solana-gas-service/src/instructions.rs @@ -0,0 +1,316 @@ +//! # Instruction Module +//! +//! This module provides constructors and definitions for all instructions that can be issued to the + +use borsh::{BorshDeserialize, BorshSerialize}; +use solana_program::program_error::ProgramError; +use solana_program::system_program; +use solana_program::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, +}; + +/// Top-level instructions supported by the Axelar Solana Gas Service program. +#[repr(u8)] +#[derive(Clone, Debug, PartialEq, Eq, BorshDeserialize, BorshSerialize)] +pub enum GasServiceInstruction { + /// Initialize the configuration PDA. + /// + /// Accounts expected: + /// 0. `[signer, writable]` The account (`payer`) paying for PDA creation + /// 1. `[]` The `authority` account of this PDA. + /// 1. `[writable]` The `config_pda` account to be created. + /// 2. `[]` The `system_program` account. + Initialize { + /// A unique 32-byte array used as a seed in deriving the config PDA. + salt: [u8; 32], + }, + + /// Use SPL tokens to pay for gas-related operations. + SplToken(PayWithSplToken), + + /// Use native SOL to pay for gas-related operations. + Native(PayWithNativeToken), +} + +/// Instructions related to paying gas fees with SPL tokens. +#[repr(u8)] +#[derive(Clone, Debug, PartialEq, Eq, BorshDeserialize, BorshSerialize)] +pub enum PayWithSplToken { + /// Pay gas fees for a contract call using SPL tokens. + ForContractCall { + /// The account paying the gas fee. + sender: Pubkey, + /// The target blockchain (e.g., "ethereum") for the contract call. + destination_chain: String, + /// The recipient address on the destination chain. + destination_address: String, + /// A 32-byte hash representing the payload. + payload_hash: [u8; 32], + /// The SPL token mint used for the gas fee. + gas_token: Pubkey, + /// The amount of tokens to be paid as gas fees. + gas_fee_amount: u64, + /// Where refunds should be sent + refund_address: Pubkey, + }, + + /// Add more gas (SPL tokens) to an existing contract call. + AddGas { + /// A 64-byte unique transaction identifier. + tx_hash: [u8; 64], + /// The index of the log entry in the transaction. + log_index: u64, + /// The account paying the additional gas fee. + pubkey: Pubkey, + /// The additional SPL tokens to add as gas. + gas_fee_amount: u64, + /// Where refunds should be sent. + refund_address: Pubkey, + }, + + /// Collect fees that have accrued in SPL tokens (authority only). + CollectFees { + /// The amount of SPL tokens to be collected as fees. + amount: u64, + }, + + /// Refund previously collected SPL token fees (authority only). + Refund { + /// A 64-byte unique transaction identifier + tx_hash: [u8; 64], + /// The index of the log entry in the transaction + log_index: u64, + /// The amount of SPL tokens to be refunded + fees: u64, + }, +} + +/// Instructions related to paying gas fees with native SOL. +#[repr(u8)] +#[derive(Clone, Debug, PartialEq, Eq, BorshDeserialize, BorshSerialize)] +pub enum PayWithNativeToken { + /// Pay gas fees for a contract call using native SOL. + /// + /// Accounts expected: + /// 0. `[signer, writable]` The account (`payer`) paying the gas fee in lamports. + /// 1. `[writable]` The `config_pda` account that receives the lamports. + /// 2. `[]` The `system_program` account. + ForContractCall { + /// The target blockchain for the contract call. + destination_chain: String, + /// The destination address on the target chain. + destination_address: String, + /// A 32-byte hash representing the payload. + payload_hash: [u8; 32], + /// Where refunds should be sent. + refund_address: Pubkey, + /// Additional parameters for the contract call. + params: Vec, + /// The amount of SOL to pay as gas fees. + gas_fee_amount: u64, + }, + + /// Add more native SOL gas to an existing transaction. + /// + /// Accounts expected: + /// 1. `[signer, writable]` The account (`sender`) providing the additional lamports. + /// 2. `[writable]` The `config_pda` account that receives the additional lamports. + /// 3. `[]` The `system_program` account. + AddGas { + /// A 64-byte unique transaction identifier. + tx_hash: [u8; 64], + /// The index of the log entry in the transaction. + log_index: u64, + /// The additional SOL to add as gas. + gas_fee_amount: u64, + /// Where refunds should be sent. + refund_address: Pubkey, + }, + + /// Collect accrued native SOL fees (authority only). + /// + /// Accounts expected: + /// 1. `[signer, read-only]` The `authority` account authorized to collect fees. + /// 2. `[writable]` The `config_pda` account holding the accrued lamports to collect. + /// 3. `[writable]` The `receiver` account where the collected lamports will be sent. + CollectFees { + /// The amount of SOL to collect as fees. + amount: u64, + }, + + /// Refund previously collected native SOL fees (authority only). + /// + /// Accounts expected: + /// 1. `[signer, read-only]` The `authority` account authorized to issue refunds. + /// 2. `[writable]` The `receiver` account that will receive the refunded lamports. + /// 3. `[writable]` The `config_pda` account from which lamports are refunded. + Refund { + /// A 64-byte unique transaction identifier. + tx_hash: [u8; 64], + /// The index of the log entry in the transaction. + log_index: u64, + /// The amount of SOL to be refunded. + fees: u64, + }, +} + +/// Builds an instruction to initialize the configuration PDA. +/// +/// # Errors +/// - ix data cannot be serialized +pub fn init_config( + program_id: &Pubkey, + payer: &Pubkey, + authority: &Pubkey, + config_pda: &Pubkey, + salt: [u8; 32], +) -> Result { + let ix_data = borsh::to_vec(&GasServiceInstruction::Initialize { salt })?; + + let accounts = vec![ + AccountMeta::new(*payer, true), + AccountMeta::new_readonly(*authority, false), + AccountMeta::new(*config_pda, false), + AccountMeta::new(system_program::ID, false), + ]; + + Ok(Instruction { + program_id: *program_id, + accounts, + data: ix_data, + }) +} + +/// Builds an instruction to pay native SOL for a contract call. +/// +/// # Errors +/// - ix data cannot be serialized +#[allow(clippy::too_many_arguments)] +pub fn pay_native_for_contract_call_instruction( + program_id: &Pubkey, + payer: &Pubkey, + config_pda: &Pubkey, + destination_chain: String, + destination_address: String, + payload_hash: [u8; 32], + refund_address: Pubkey, + params: Vec, + gas_fee_amount: u64, +) -> Result { + let ix_data = borsh::to_vec(&GasServiceInstruction::Native( + PayWithNativeToken::ForContractCall { + destination_chain, + destination_address, + payload_hash, + refund_address, + params, + gas_fee_amount, + }, + ))?; + + let accounts = vec![ + AccountMeta::new(*payer, true), + AccountMeta::new(*config_pda, false), + AccountMeta::new(system_program::ID, false), + ]; + + Ok(Instruction { + program_id: *program_id, + accounts, + data: ix_data, + }) +} + +/// Builds an instruction to add native SOL gas. +/// +/// # Errors +/// - ix data cannot be serialized +pub fn add_native_gas_instruction( + program_id: &Pubkey, + sender: &Pubkey, + config_pda: &Pubkey, + tx_hash: [u8; 64], + log_index: u64, + gas_fee_amount: u64, + refund_address: Pubkey, +) -> Result { + let ix_data = borsh::to_vec(&GasServiceInstruction::Native(PayWithNativeToken::AddGas { + tx_hash, + log_index, + gas_fee_amount, + refund_address, + }))?; + + let accounts = vec![ + AccountMeta::new(*sender, true), + AccountMeta::new(*config_pda, false), + AccountMeta::new(system_program::ID, false), + ]; + + Ok(Instruction { + program_id: *program_id, + accounts, + data: ix_data, + }) +} + +/// Builds an instruction for the authority to collect native SOL fees. +/// +/// # Errors +/// - ix data cannot be serialized +pub fn collect_native_fees_instruction( + program_id: &Pubkey, + authority: &Pubkey, + config_pda: &Pubkey, + receiver: &Pubkey, + amount: u64, +) -> Result { + let ix_data = borsh::to_vec(&GasServiceInstruction::Native( + PayWithNativeToken::CollectFees { amount }, + ))?; + + let accounts = vec![ + AccountMeta::new_readonly(*authority, true), + AccountMeta::new(*config_pda, false), + AccountMeta::new(*receiver, false), + ]; + + Ok(Instruction { + program_id: *program_id, + accounts, + data: ix_data, + }) +} + +/// Builds an instruction for the authority to refund previously collected native SOL fees. +/// +/// # Errors +/// - ix data cannot be serialized +pub fn refund_native_fees_instruction( + program_id: &Pubkey, + authority: &Pubkey, + receiver: &Pubkey, + config_pda: &Pubkey, + tx_hash: [u8; 64], + log_index: u64, + fees: u64, +) -> Result { + let ix_data = borsh::to_vec(&GasServiceInstruction::Native(PayWithNativeToken::Refund { + tx_hash, + log_index, + fees, + }))?; + + let accounts = vec![ + AccountMeta::new_readonly(*authority, true), + AccountMeta::new(*receiver, false), + AccountMeta::new(*config_pda, false), + ]; + + Ok(Instruction { + program_id: *program_id, + accounts, + data: ix_data, + }) +} diff --git a/solana/programs/axelar-solana-gas-service/src/lib.rs b/solana/programs/axelar-solana-gas-service/src/lib.rs new file mode 100644 index 0000000..c759374 --- /dev/null +++ b/solana/programs/axelar-solana-gas-service/src/lib.rs @@ -0,0 +1,169 @@ +//! Axelar Gas Service program for the Solana blockchain +#![allow(clippy::little_endian_bytes)] +pub mod entrypoint; +pub mod instructions; +pub mod processor; +pub mod state; + +// Export current sdk types for downstream users building with a different sdk +// version. +pub use solana_program; +use solana_program::msg; +use solana_program::program_error::ProgramError; +use solana_program::pubkey::Pubkey; + +solana_program::declare_id!("gasHQkvaC4jTD2MQpAuEN3RdNwde2Ym5E5QNDoh6m6G"); + +/// Seed prefixes for PDAs created by this program +pub mod seed_prefixes { + /// The seed used when deriving the configuration PDA. + pub const CONFIG_SEED: &[u8] = b"gas-service"; +} + +/// Event discriminators (prefixes) used to identify logged events related to native gas operations. +pub mod event_prefixes { + /// Prefix emitted when native gas is paid for a contract call. + pub const NATIVE_GAS_PAID_FOR_CONTRACT_CALL: &[u8] = b"native gas paid for contract call"; + /// Prefix emitted when native gas is added to an already emtted contract call. + pub const NATIVE_GAS_ADDED: &[u8] = b"native gas added"; + /// Prefix emitted when native gas is refunded. + pub const NATIVE_GAS_REFUNDED: &[u8] = b"native gas refunded"; +} + +/// Checks that the provided `program_id` matches the current program’s ID. +/// +/// # Errors +/// +/// - if the provided `program_id` does not match. +#[inline] +pub fn check_program_account(program_id: Pubkey) -> Result<(), ProgramError> { + if program_id != crate::ID { + return Err(ProgramError::IncorrectProgramId); + } + Ok(()) +} + +/// Derives the configuration PDA for this program. +/// +/// Given a `program_id`, a `salt` (32-byte array), and an `authority` (`Pubkey`), this function +/// uses [`Pubkey::find_program_address`] to return the derived PDA and its associated bump seed. +#[inline] +#[must_use] +pub fn get_config_pda(program_id: &Pubkey, salt: &[u8; 32], authority: &Pubkey) -> (Pubkey, u8) { + Pubkey::find_program_address( + &[seed_prefixes::CONFIG_SEED, salt, authority.as_ref()], + program_id, + ) +} + +/// Checks that the given `expected_pubkey` matches the derived PDA for the provided parameters. +/// +/// # Panics +/// - if the seeds + bump don't result in a valid PDA +/// +/// # Errors +/// +/// - if the derived PDA does not match the `expected_pubkey`. +#[inline] +#[track_caller] +pub fn assert_valid_config_pda( + bump: u8, + salt: &[u8; 32], + authority: &Pubkey, + expected_pubkey: &Pubkey, +) -> Result<(), ProgramError> { + let derived_pubkey = Pubkey::create_program_address( + &[ + seed_prefixes::CONFIG_SEED, + salt, + authority.as_ref(), + &[bump], + ], + &crate::ID, + ) + .expect("invalid bump for the config pda"); + + if &derived_pubkey == expected_pubkey { + Ok(()) + } else { + msg!("Error: Invalid Config PDA "); + Err(ProgramError::IncorrectProgramId) + } +} + +/// Utilities for working with gas service events +pub mod event_utils { + + /// Errors that may occur while parsing a `MessageEvent`. + #[derive(Debug, thiserror::Error)] + pub enum EventParseError { + /// Occurs when a required field is missing in the event data. + #[error("Missing data: {0}")] + MissingData(&'static str), + + /// The data is there but it's not of valid format + #[error("Invalid data: {0}")] + InvalidData(&'static str), + + /// Occurs when the length of a field does not match the expected length. + #[error("Invalid length for {field}: expected {expected}, got {actual}")] + InvalidLength { + /// the field that we're trying to parse + field: &'static str, + /// the desired length + expected: usize, + /// the actual length + actual: usize, + }, + + /// Occurs when a field contains invalid UTF-8 data. + #[error("Invalid UTF-8 in {field}: {source}")] + InvalidUtf8 { + /// the field we're trying to parse + field: &'static str, + /// underlying error + #[source] + source: std::string::FromUtf8Error, + }, + + /// Generic error for any other parsing issues. + #[error("Other error: {0}")] + Other(&'static str), + } + + pub(crate) fn read_array( + field: &'static str, + data: &[u8], + ) -> Result<[u8; N], EventParseError> { + if data.len() != N { + return Err(EventParseError::InvalidLength { + field, + expected: N, + actual: data.len(), + }); + } + let array = data + .try_into() + .map_err(|_err| EventParseError::InvalidLength { + field, + expected: N, + actual: data.len(), + })?; + Ok(array) + } + + pub(crate) fn read_string( + field: &'static str, + data: Vec, + ) -> Result { + String::from_utf8(data).map_err(|err| EventParseError::InvalidUtf8 { field, source: err }) + } + + #[allow(clippy::little_endian_bytes)] + pub(crate) fn parse_u64_le(field: &'static str, data: &[u8]) -> Result { + if data.len() != 8 { + return Err(EventParseError::InvalidData(field)); + } + Ok(u64::from_le_bytes(data.try_into().expect("length checked"))) + } +} diff --git a/solana/programs/axelar-solana-gas-service/src/processor.rs b/solana/programs/axelar-solana-gas-service/src/processor.rs new file mode 100644 index 0000000..65a5010 --- /dev/null +++ b/solana/programs/axelar-solana-gas-service/src/processor.rs @@ -0,0 +1,94 @@ +//! Processor for the Solana gas service + +use borsh::BorshDeserialize; +use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey}; + +use crate::{ + check_program_account, + instructions::{GasServiceInstruction, PayWithNativeToken}, +}; + +pub use self::native::{ + NativeGasAddedEvent, NativeGasPaidForContractCallEvent, NativeGasRefundedEvent, +}; +use self::{ + initialize::process_initialize_config, + native::{ + add_native_gas, collect_fees_native, process_pay_native_for_contract_call, refund_native, + }, +}; + +mod initialize; +mod native; + +/// Processes an instruction. +/// +/// # Errors +/// - if the ix processing resulted in an error +#[allow(clippy::todo)] +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo<'_>], + input: &[u8], +) -> ProgramResult { + let instruction = GasServiceInstruction::try_from_slice(input)?; + check_program_account(*program_id)?; + + match instruction { + GasServiceInstruction::Initialize { salt } => { + process_initialize_config(program_id, accounts, salt) + } + GasServiceInstruction::SplToken(_) => todo!("we do need this one"), + GasServiceInstruction::Native(ix) => match ix { + PayWithNativeToken::ForContractCall { + destination_chain, + destination_address, + payload_hash, + refund_address, + params, + gas_fee_amount, + } => process_pay_native_for_contract_call( + program_id, + accounts, + destination_chain, + destination_address, + payload_hash, + refund_address, + ¶ms, + gas_fee_amount, + ), + PayWithNativeToken::AddGas { + tx_hash, + log_index, + gas_fee_amount, + refund_address, + } => add_native_gas( + program_id, + accounts, + tx_hash, + log_index, + gas_fee_amount, + refund_address, + ), + PayWithNativeToken::CollectFees { amount } => { + collect_fees_native(program_id, accounts, amount) + } + PayWithNativeToken::Refund { + tx_hash, + log_index, + fees, + } => refund_native(program_id, accounts, tx_hash, log_index, fees), + }, + } +} + +/// Even emitted by the Axelar Solana Gas service +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum GasServiceEvent { + /// Event when SOL was used to pay for a contract call + NativeGasPaidForncontractCall(NativeGasPaidForContractCallEvent), + /// Event when SOL was add to fund an already emitted contract call + NativeGasAdded(NativeGasAddedEvent), + /// Event when SOL was refunded + NativeGasRefunded(NativeGasRefundedEvent), +} diff --git a/solana/programs/axelar-solana-gas-service/src/processor/initialize.rs b/solana/programs/axelar-solana-gas-service/src/processor/initialize.rs new file mode 100644 index 0000000..0f53ca8 --- /dev/null +++ b/solana/programs/axelar-solana-gas-service/src/processor/initialize.rs @@ -0,0 +1,59 @@ +use core::mem::size_of; + +use program_utils::BytemuckedPda; +use solana_program::account_info::{next_account_info, AccountInfo}; +use solana_program::entrypoint::ProgramResult; +use solana_program::program_error::ProgramError; +use solana_program::pubkey::Pubkey; +use solana_program::system_program; + +use crate::state::Config; +use crate::{assert_valid_config_pda, get_config_pda, seed_prefixes}; + +/// This function is used to initialize a config on the program +pub(crate) fn process_initialize_config( + program_id: &Pubkey, + accounts: &[AccountInfo<'_>], + salt: [u8; 32], +) -> ProgramResult { + let accounts = &mut accounts.iter(); + let payer = next_account_info(accounts)?; + let authority = next_account_info(accounts)?; + let config_pda = next_account_info(accounts)?; + let system_account = next_account_info(accounts)?; + + // Check: System Program Account + if !system_program::check_id(system_account.key) { + return Err(ProgramError::InvalidInstructionData); + } + + let (_, bump) = get_config_pda(program_id, &salt, authority.key); + + // Check: Gateway Config account uses the canonical bump. + assert_valid_config_pda(bump, &salt, authority.key, config_pda.key)?; + + // Initialize the account + program_utils::init_pda_raw( + payer, + config_pda, + program_id, + system_account, + size_of::().try_into().expect("must be valid u64"), + &[ + seed_prefixes::CONFIG_SEED, + &salt, + authority.key.as_ref(), + &[bump], + ], + )?; + let mut data = config_pda.try_borrow_mut_data()?; + let gateway_config = Config::read_mut(&mut data).ok_or(ProgramError::InvalidAccountData)?; + + *gateway_config = Config { + bump, + authority: *authority.key, + salt, + }; + + Ok(()) +} diff --git a/solana/programs/axelar-solana-gas-service/src/processor/native.rs b/solana/programs/axelar-solana-gas-service/src/processor/native.rs new file mode 100644 index 0000000..f283fb9 --- /dev/null +++ b/solana/programs/axelar-solana-gas-service/src/processor/native.rs @@ -0,0 +1,348 @@ +use program_utils::{transfer_lamports, BytemuckedPda, ValidPDA}; +use solana_program::account_info::{next_account_info, AccountInfo}; +use solana_program::entrypoint::ProgramResult; +use solana_program::log::sol_log_data; +use solana_program::program::invoke; +use solana_program::program_error::ProgramError; +use solana_program::pubkey::Pubkey; +use solana_program::system_instruction; + +use crate::event_utils::{parse_u64_le, read_array, read_string, EventParseError}; +use crate::state::Config; +use crate::{assert_valid_config_pda, event_prefixes}; + +#[allow(clippy::too_many_arguments)] +pub(crate) fn process_pay_native_for_contract_call( + program_id: &Pubkey, + accounts: &[AccountInfo<'_>], + destination_chain: String, + destination_address: String, + payload_hash: [u8; 32], + refund_address: Pubkey, + params: &[u8], + gas_fee_amount: u64, +) -> ProgramResult { + let accounts = &mut accounts.iter(); + let sender = next_account_info(accounts)?; + let config_pda = next_account_info(accounts)?; + let system_program = next_account_info(accounts)?; + + { + config_pda.check_initialized_pda_without_deserialization(program_id)?; + let data = config_pda.try_borrow_data()?; + let config = Config::read(&data).ok_or(ProgramError::InvalidAccountData)?; + assert_valid_config_pda(config.bump, &config.salt, &config.authority, config_pda.key)?; + } + + invoke( + &system_instruction::transfer(sender.key, config_pda.key, gas_fee_amount), + &[sender.clone(), config_pda.clone(), system_program.clone()], + )?; + + // Emit an event + sol_log_data(&[ + event_prefixes::NATIVE_GAS_PAID_FOR_CONTRACT_CALL, + &config_pda.key.to_bytes(), + &destination_chain.into_bytes(), + &destination_address.into_bytes(), + &payload_hash, + &refund_address.to_bytes(), + params, + &gas_fee_amount.to_le_bytes(), + ]); + + Ok(()) +} + +pub(crate) fn add_native_gas( + program_id: &Pubkey, + accounts: &[AccountInfo<'_>], + tx_hash: [u8; 64], + log_index: u64, + gas_fee_amount: u64, + refund_address: Pubkey, +) -> ProgramResult { + let accounts = &mut accounts.iter(); + let sender = next_account_info(accounts)?; + let config_pda = next_account_info(accounts)?; + let system_program = next_account_info(accounts)?; + + { + config_pda.check_initialized_pda_without_deserialization(program_id)?; + let data = config_pda.try_borrow_data()?; + let config = Config::read(&data).ok_or(ProgramError::InvalidAccountData)?; + assert_valid_config_pda(config.bump, &config.salt, &config.authority, config_pda.key)?; + } + + invoke( + &system_instruction::transfer(sender.key, config_pda.key, gas_fee_amount), + &[sender.clone(), config_pda.clone(), system_program.clone()], + )?; + + // Emit an event + sol_log_data(&[ + event_prefixes::NATIVE_GAS_ADDED, + &config_pda.key.to_bytes(), + &tx_hash, + &log_index.to_le_bytes(), + &refund_address.to_bytes(), + &gas_fee_amount.to_le_bytes(), + ]); + + Ok(()) +} + +pub(crate) fn collect_fees_native( + program_id: &Pubkey, + accounts: &[AccountInfo<'_>], + amount: u64, +) -> ProgramResult { + let accounts = &mut accounts.iter(); + let authority = next_account_info(accounts)?; + let config_pda = next_account_info(accounts)?; + let receiver = next_account_info(accounts)?; + + { + // Check: Valid Config PDA + config_pda.check_initialized_pda_without_deserialization(program_id)?; + let data = config_pda.try_borrow_data()?; + let config = Config::read(&data).ok_or(ProgramError::InvalidAccountData)?; + assert_valid_config_pda(config.bump, &config.salt, &config.authority, config_pda.key)?; + + // Check: Authority mtaches + if authority.key != &config.authority { + return Err(ProgramError::InvalidAccountOwner); + } + } + + // Check: Authority is signer + if !authority.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } + + transfer_lamports(config_pda, receiver, amount)?; + + Ok(()) +} + +pub(crate) fn refund_native( + program_id: &Pubkey, + accounts: &[AccountInfo<'_>], + tx_hash: [u8; 64], + log_index: u64, + fees: u64, +) -> ProgramResult { + let accounts = &mut accounts.iter(); + let authority = next_account_info(accounts)?; + let receiver = next_account_info(accounts)?; + let config_pda = next_account_info(accounts)?; + + { + // Check: Valid Config PDA + config_pda.check_initialized_pda_without_deserialization(program_id)?; + let data = config_pda.try_borrow_data()?; + let config = Config::read(&data).ok_or(ProgramError::InvalidAccountData)?; + assert_valid_config_pda(config.bump, &config.salt, &config.authority, config_pda.key)?; + + // Check: Authority mtaches + if authority.key != &config.authority { + return Err(ProgramError::InvalidAccountOwner); + } + } + + // Check: Authority is signer + if !authority.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } + + transfer_lamports(config_pda, receiver, fees)?; + + // Emit an event + sol_log_data(&[ + event_prefixes::NATIVE_GAS_REFUNDED, + &tx_hash, + &config_pda.key.to_bytes(), + &log_index.to_le_bytes(), + &receiver.key.to_bytes(), + &fees.to_le_bytes(), + ]); + + Ok(()) +} + +/// Represents the event emitted when native gas is paid for a contract call. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct NativeGasPaidForContractCallEvent { + /// The Gas service config PDA + pub config_pda: Pubkey, + /// Destination chain on the Axelar network + pub destination_chain: String, + /// Destination address on the Axelar network + pub destination_address: String, + /// The payload hash for the event we're paying for + pub payload_hash: [u8; 32], + /// The refund address + pub refund_address: Pubkey, + /// Extra parameters to be passed + pub params: Vec, + /// The amount of SOL to send + pub gas_fee_amount: u64, +} + +impl NativeGasPaidForContractCallEvent { + /// Construct a new event from byte slices + /// + /// # Errors + /// - if the data could not be parsed into an event + pub fn new>>(mut data: I) -> Result { + let config_pda_data = data + .next() + .ok_or(EventParseError::MissingData("config_pda"))?; + let config_pda = Pubkey::new_from_array(read_array::<32>("config_pda", &config_pda_data)?); + + let destination_chain_data = data + .next() + .ok_or(EventParseError::MissingData("destination_chain"))?; + let destination_chain = read_string("destination_chain", destination_chain_data)?; + + let destination_address_data = data + .next() + .ok_or(EventParseError::MissingData("destination_address"))?; + let destination_address = read_string("destination_address", destination_address_data)?; + + let payload_hash_data = data + .next() + .ok_or(EventParseError::MissingData("payload_hash"))?; + let payload_hash = read_array::<32>("payload_hash", &payload_hash_data)?; + + let refund_address_data = data + .next() + .ok_or(EventParseError::MissingData("refund_address"))?; + let refund_address = + Pubkey::new_from_array(read_array::<32>("refund_address", &refund_address_data)?); + + let params = data.next().ok_or(EventParseError::MissingData("params"))?; + + let gas_fee_amount_data = data + .next() + .ok_or(EventParseError::MissingData("gas_fee_amount"))?; + let gas_fee_amount = parse_u64_le("gas_fee_amount", &gas_fee_amount_data)?; + + Ok(Self { + config_pda, + destination_chain, + destination_address, + payload_hash, + refund_address, + params, + gas_fee_amount, + }) + } +} + +/// Represents the event emitted when native gas is added. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct NativeGasAddedEvent { + /// The Gas service config PDA + pub config_pda: Pubkey, + /// Solana transaction signature + pub tx_hash: [u8; 64], + /// index of the log + pub log_index: u64, + /// The refund address + pub refund_address: Pubkey, + /// amount of SOL + pub gas_fee_amount: u64, +} + +impl NativeGasAddedEvent { + /// Construct a new event from byte slices + /// + /// # Errors + /// - if the data could not be parsed into an event + pub fn new>>(mut data: I) -> Result { + let config_pda_data = data + .next() + .ok_or(EventParseError::MissingData("config_pda"))?; + let config_pda = Pubkey::new_from_array(read_array::<32>("config_pda", &config_pda_data)?); + + let tx_hash_data = data.next().ok_or(EventParseError::MissingData("tx_hash"))?; + let tx_hash = read_array::<64>("tx_hash", &tx_hash_data)?; + + let log_index_data = data + .next() + .ok_or(EventParseError::MissingData("log_index"))?; + let log_index = parse_u64_le("log_index", &log_index_data)?; + + let refund_address_data = data + .next() + .ok_or(EventParseError::MissingData("refund_address"))?; + let refund_address = + Pubkey::new_from_array(read_array::<32>("refund_address", &refund_address_data)?); + + let gas_fee_amount_data = data + .next() + .ok_or(EventParseError::MissingData("gas_fee_amount"))?; + let gas_fee_amount = parse_u64_le("gas_fee_amount", &gas_fee_amount_data)?; + + Ok(Self { + config_pda, + tx_hash, + log_index, + refund_address, + gas_fee_amount, + }) + } +} + +/// Represents the event emitted when native gas is refunded. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct NativeGasRefundedEvent { + /// Solana transaction signature + pub tx_hash: [u8; 64], + /// The Gas service config PDA + pub config_pda: Pubkey, + /// The log index + pub log_index: u64, + /// The receiver of the refund + pub receiver: Pubkey, + /// amount of SOL + pub fees: u64, +} + +impl NativeGasRefundedEvent { + /// Construct a new event from byte slices + /// + /// # Errors + /// - if the data could not be parsed into an event + pub fn new>>(mut data: I) -> Result { + let tx_hash_data = data.next().ok_or(EventParseError::MissingData("tx_hash"))?; + let tx_hash = read_array::<64>("tx_hash", &tx_hash_data)?; + + let config_pda_data = data + .next() + .ok_or(EventParseError::MissingData("config_pda"))?; + let config_pda = Pubkey::new_from_array(read_array::<32>("config_pda", &config_pda_data)?); + + let log_index_data = data + .next() + .ok_or(EventParseError::MissingData("log_index"))?; + let log_index = parse_u64_le("log_index", &log_index_data)?; + + let receiver_data = data + .next() + .ok_or(EventParseError::MissingData("receiver"))?; + let receiver = Pubkey::new_from_array(read_array::<32>("receiver", &receiver_data)?); + + let fees_data = data.next().ok_or(EventParseError::MissingData("fees"))?; + let fees = parse_u64_le("fees", &fees_data)?; + + Ok(Self { + tx_hash, + config_pda, + log_index, + receiver, + fees, + }) + } +} diff --git a/solana/programs/axelar-solana-gas-service/src/state.rs b/solana/programs/axelar-solana-gas-service/src/state.rs new file mode 100644 index 0000000..3062cdd --- /dev/null +++ b/solana/programs/axelar-solana-gas-service/src/state.rs @@ -0,0 +1,19 @@ +//! State module for the Axelar Solana Gas Service + +use bytemuck::{Pod, Zeroable}; +use program_utils::BytemuckedPda; +use solana_program::pubkey::Pubkey; + +/// Keep track of the authority for aggregating gas payments +#[repr(C)] +#[derive(Zeroable, Pod, Clone, Copy, PartialEq, Eq, Debug)] +pub struct Config { + /// The authority with permission give refunds & withdraw funds + pub authority: Pubkey, + /// A 32-byte "salt" to ensure uniqueness in PDA derivation. + pub salt: [u8; 32], + /// The bump seed used to derive the PDA, ensuring the address is valid. + pub bump: u8, +} + +impl BytemuckedPda for Config {} diff --git a/solana/programs/axelar-solana-gas-service/tests/module/collect_fees_native.rs b/solana/programs/axelar-solana-gas-service/tests/module/collect_fees_native.rs new file mode 100644 index 0000000..a2adcc6 --- /dev/null +++ b/solana/programs/axelar-solana-gas-service/tests/module/collect_fees_native.rs @@ -0,0 +1,110 @@ +use axelar_solana_gateway_test_fixtures::base::TestFixture; +use solana_program_test::{tokio, ProgramTest}; +use solana_sdk::{signature::Keypair, signer::Signer}; + +#[tokio::test] +async fn test_receive_funds() { + // Setup + let pt = ProgramTest::default(); + let mut test_fixture = TestFixture::new(pt).await; + let gas_utils = test_fixture.deploy_gas_service().await; + test_fixture.init_gas_config(&gas_utils).await.unwrap(); + + // Record balances before the transaction + test_fixture + .fund_account(&gas_utils.config_pda, 1_000_000_000) + .await; + let receiver = Keypair::new(); + let receiver_balance_before = 0; + let config_pda_balance_before = test_fixture + .banks_client + .get_account(gas_utils.config_pda) + .await + .unwrap() + .unwrap() + .lamports; + + // Action + let sol_amount = 1_000_000; + let ix = axelar_solana_gas_service::instructions::collect_native_fees_instruction( + &axelar_solana_gas_service::ID, + &gas_utils.config_authority.pubkey(), + &gas_utils.config_pda, + &receiver.pubkey(), + sol_amount, + ) + .unwrap(); + + test_fixture + .send_tx_with_custom_signers( + &[ix], + &[ + // pays for tx + &test_fixture.payer.insecure_clone(), + // authority for config pda deduction + &gas_utils.config_authority, + ], + ) + .await + .unwrap(); + + // assert that SOL gets transferred + let receiver_balance_after = test_fixture + .banks_client + .get_account(receiver.pubkey()) + .await + .unwrap() + .unwrap() + .lamports; + let config_pda_balance_after = test_fixture + .banks_client + .get_account(gas_utils.config_pda) + .await + .unwrap() + .unwrap() + .lamports; + + assert_eq!( + config_pda_balance_after, + config_pda_balance_before - sol_amount + ); + assert_eq!(receiver_balance_after, receiver_balance_before + sol_amount); +} + +#[tokio::test] +async fn test_refund_native_fails_if_not_signed_by_authority() { + // Setup + let pt = ProgramTest::default(); + let mut test_fixture = TestFixture::new(pt).await; + let gas_utils = test_fixture.deploy_gas_service().await; + test_fixture.init_gas_config(&gas_utils).await.unwrap(); + test_fixture + .fund_account(&gas_utils.config_pda, 1_000_000_000) + .await; + + // Action + let receiver = Keypair::new(); + let sol_amount = 1_000_000; + let mut ix = axelar_solana_gas_service::instructions::collect_native_fees_instruction( + &axelar_solana_gas_service::ID, + &gas_utils.config_authority.pubkey(), + &gas_utils.config_pda, + &receiver.pubkey(), + sol_amount, + ) + .unwrap(); + // mark that authority does not need to be a signer + ix.accounts[0].is_signer = false; + + let res = test_fixture + .send_tx_with_custom_signers( + &[ix], + &[ + // pays for tx + &test_fixture.payer.insecure_clone(), + ], + ) + .await; + + assert!(res.is_err()); +} diff --git a/solana/programs/axelar-solana-gas-service/tests/module/initialize.rs b/solana/programs/axelar-solana-gas-service/tests/module/initialize.rs new file mode 100644 index 0000000..605ba9d --- /dev/null +++ b/solana/programs/axelar-solana-gas-service/tests/module/initialize.rs @@ -0,0 +1,75 @@ +use axelar_solana_gas_service::state::Config; +use axelar_solana_gateway_test_fixtures::base::TestFixture; +use solana_program_test::{tokio, ProgramTest}; +use solana_sdk::{keccak::hashv, signer::Signer}; + +#[tokio::test] +async fn test_successfylly_initialize_config() { + // Setup + let pt = ProgramTest::default(); + let mut test_fixture = TestFixture::new(pt).await; + let gas_utils = test_fixture.deploy_gas_service().await; + + // Action + let _res = test_fixture.init_gas_config(&gas_utils).await.unwrap(); + + // Assert + let config = test_fixture + .gas_service_config_state(gas_utils.config_pda) + .await; + assert_eq!( + config, + Config { + authority: gas_utils.config_authority.pubkey(), + salt: gas_utils.salt, + bump: config.bump + } + ); +} + +#[tokio::test] +async fn test_different_salts_give_new_configs() { + // Setup + let pt = ProgramTest::default(); + let mut test_fixture = TestFixture::new(pt).await; + let gas_utils = test_fixture.deploy_gas_service().await; + + // Action + let salt_seeds = b"abc"; + for salt_seed in salt_seeds { + let salt = hashv(&[&[*salt_seed]]).0; + let (config_pda, bump) = axelar_solana_gas_service::get_config_pda( + &axelar_solana_gas_service::ID, + &salt, + &gas_utils.config_authority.pubkey(), + ); + let _res = test_fixture + .init_gas_config_with_params(gas_utils.config_authority.pubkey(), config_pda, salt) + .await + .unwrap(); + // Assert + let config = test_fixture.gas_service_config_state(config_pda).await; + assert_eq!( + config, + Config { + authority: gas_utils.config_authority.pubkey(), + salt, + bump + } + ); + } + + // assert -- subsequent initializations will revert the tx + for salt_seed in salt_seeds { + let salt = hashv(&[&[*salt_seed]]).0; + let (config_pda, _bump) = axelar_solana_gas_service::get_config_pda( + &axelar_solana_gas_service::ID, + &salt, + &gas_utils.config_authority.pubkey(), + ); + let res = test_fixture + .init_gas_config_with_params(gas_utils.config_authority.pubkey(), config_pda, salt) + .await; + assert!(res.is_err()); + } +} diff --git a/solana/programs/axelar-solana-gas-service/tests/module/main.rs b/solana/programs/axelar-solana-gas-service/tests/module/main.rs new file mode 100644 index 0000000..3dd0e4a --- /dev/null +++ b/solana/programs/axelar-solana-gas-service/tests/module/main.rs @@ -0,0 +1,16 @@ +#![allow( + clippy::expect_used, + clippy::indexing_slicing, + clippy::missing_errors_doc, + clippy::str_to_string, + clippy::tests_outside_test_module, + clippy::unwrap_used, + clippy::panic, + unused_must_use +)] + +mod collect_fees_native; +mod initialize; +mod native_add_gas; +mod pay_native_for_contract_call; +mod refund_native_gas; diff --git a/solana/programs/axelar-solana-gas-service/tests/module/native_add_gas.rs b/solana/programs/axelar-solana-gas-service/tests/module/native_add_gas.rs new file mode 100644 index 0000000..d5148e1 --- /dev/null +++ b/solana/programs/axelar-solana-gas-service/tests/module/native_add_gas.rs @@ -0,0 +1,147 @@ +use axelar_solana_gas_service::processor::{GasServiceEvent, NativeGasAddedEvent}; +use axelar_solana_gateway_test_fixtures::{base::TestFixture, gas_service::get_gas_service_events}; +use gateway_event_stack::ProgramInvocationState; +use solana_program_test::{tokio, ProgramTest}; +use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer}; + +#[tokio::test] +async fn test_add_native_gas() { + // Setup + let pt = ProgramTest::default(); + let mut test_fixture = TestFixture::new(pt).await; + let gas_utils = test_fixture.deploy_gas_service().await; + test_fixture.init_gas_config(&gas_utils).await.unwrap(); + + // Record balances before the transaction + let payer = Keypair::new(); + test_fixture + .fund_account(&payer.pubkey(), 1_000_000_000) + .await; + let payer_balance_before = test_fixture + .banks_client + .get_account(payer.pubkey()) + .await + .unwrap() + .unwrap() + .lamports; + let config_pda_balance_before = test_fixture + .banks_client + .get_account(gas_utils.config_pda) + .await + .unwrap() + .unwrap() + .lamports; + + // Action + let refund_address = Pubkey::new_unique(); + let gas_amount = 1_000_000; + let tx_hash = [42; 64]; + let log_index = 1232; + let ix = axelar_solana_gas_service::instructions::add_native_gas_instruction( + &axelar_solana_gas_service::ID, + &payer.pubkey(), + &gas_utils.config_pda, + tx_hash, + log_index, + gas_amount, + refund_address, + ) + .unwrap(); + + let res = test_fixture + .send_tx_with_custom_signers( + &[ix], + &[ + // pays for tx + &test_fixture.payer.insecure_clone(), + // pays for gas deduction + &payer, + ], + ) + .await + .unwrap(); + + // assert event + let emitted_events = get_gas_service_events(&res).into_iter().next().unwrap(); + let ProgramInvocationState::Succeeded(vec_events) = emitted_events else { + panic!("unexpected event") + }; + let [(_, GasServiceEvent::NativeGasAdded(emitted_event))] = vec_events.as_slice() else { + panic!("unexpected event") + }; + assert_eq!( + emitted_event, + &NativeGasAddedEvent { + config_pda: gas_utils.config_pda, + tx_hash, + log_index, + refund_address, + gas_fee_amount: gas_amount, + } + ); + + // assert that SOL gets transferred + let payer_balance_after = test_fixture + .banks_client + .get_account(payer.pubkey()) + .await + .unwrap() + .unwrap() + .lamports; + let config_pda_balance_after = test_fixture + .banks_client + .get_account(gas_utils.config_pda) + .await + .unwrap() + .unwrap() + .lamports; + + assert_eq!( + config_pda_balance_after, + config_pda_balance_before + gas_amount + ); + assert_eq!(payer_balance_after, payer_balance_before - gas_amount); +} + +#[tokio::test] +async fn fails_if_payer_not_signer() { + // Setup + let pt = ProgramTest::default(); + let mut test_fixture = TestFixture::new(pt).await; + let gas_utils = test_fixture.deploy_gas_service().await; + test_fixture.init_gas_config(&gas_utils).await.unwrap(); + + // Record balances before the transaction + let payer = Keypair::new(); + test_fixture + .fund_account(&payer.pubkey(), 1_000_000_000) + .await; + + // Action + let refund_address = Pubkey::new_unique(); + let gas_amount = 1_000_000; + let tx_hash = [42; 64]; + let log_index = 1232; + let mut ix = axelar_solana_gas_service::instructions::add_native_gas_instruction( + &axelar_solana_gas_service::ID, + &payer.pubkey(), + &gas_utils.config_pda, + tx_hash, + log_index, + gas_amount, + refund_address, + ) + .unwrap(); + ix.accounts[0].is_signer = false; + + let res = test_fixture + .send_tx_with_custom_signers( + &[ix], + &[ + // pays for tx + &test_fixture.payer.insecure_clone(), + ], + ) + .await; + assert!(res.is_err()); +} diff --git a/solana/programs/axelar-solana-gas-service/tests/module/pay_native_for_contract_call.rs b/solana/programs/axelar-solana-gas-service/tests/module/pay_native_for_contract_call.rs new file mode 100644 index 0000000..7b9f9d9 --- /dev/null +++ b/solana/programs/axelar-solana-gas-service/tests/module/pay_native_for_contract_call.rs @@ -0,0 +1,163 @@ +use axelar_solana_gas_service::processor::{GasServiceEvent, NativeGasPaidForContractCallEvent}; +use axelar_solana_gateway_test_fixtures::{base::TestFixture, gas_service::get_gas_service_events}; +use gateway_event_stack::ProgramInvocationState; +use solana_program_test::{tokio, ProgramTest}; +use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer}; + +#[tokio::test] +async fn test_pay_native_for_contract_call() { + // Setup + let pt = ProgramTest::default(); + let mut test_fixture = TestFixture::new(pt).await; + let gas_utils = test_fixture.deploy_gas_service().await; + test_fixture.init_gas_config(&gas_utils).await.unwrap(); + + // Record balances before the transaction + let payer = Keypair::new(); + test_fixture + .fund_account(&payer.pubkey(), 1_000_000_000) + .await; + let payer_balance_before = test_fixture + .banks_client + .get_account(payer.pubkey()) + .await + .unwrap() + .unwrap() + .lamports; + let config_pda_balance_before = test_fixture + .banks_client + .get_account(gas_utils.config_pda) + .await + .unwrap() + .unwrap() + .lamports; + + // Action + let refund_address = Pubkey::new_unique(); + let gas_amount = 1_000_000; + let destination_chain = "ethereum".to_owned(); + let destination_addr = "destination addr 123".to_owned(); + let payload_hash = [42; 32]; + let params = "\u{1f42a}\u{1f42a}\u{1f42a}\u{1f42a}" + .to_string() + .into_bytes(); + let ix = axelar_solana_gas_service::instructions::pay_native_for_contract_call_instruction( + &axelar_solana_gas_service::ID, + &payer.pubkey(), + &gas_utils.config_pda, + destination_chain.clone(), + destination_addr.clone(), + payload_hash, + refund_address, + params.clone(), + gas_amount, + ) + .unwrap(); + + let res = test_fixture + .send_tx_with_custom_signers( + &[ix], + &[ + // pays for tx + &test_fixture.payer.insecure_clone(), + // pays for gas deduction + &payer, + ], + ) + .await + .unwrap(); + + // assert event + let emitted_events = get_gas_service_events(&res).into_iter().next().unwrap(); + let ProgramInvocationState::Succeeded(vec_events) = emitted_events else { + panic!("unexpected event") + }; + let [(_, GasServiceEvent::NativeGasPaidForncontractCall(emitted_event))] = + vec_events.as_slice() + else { + panic!("unexpected event") + }; + assert_eq!( + emitted_event, + &NativeGasPaidForContractCallEvent { + config_pda: gas_utils.config_pda, + destination_chain, + destination_address: destination_addr, + payload_hash, + refund_address, + params, + gas_fee_amount: gas_amount, + } + ); + + // assert that SOL gets transferred + let payer_balance_after = test_fixture + .banks_client + .get_account(payer.pubkey()) + .await + .unwrap() + .unwrap() + .lamports; + let config_pda_balance_after = test_fixture + .banks_client + .get_account(gas_utils.config_pda) + .await + .unwrap() + .unwrap() + .lamports; + + assert_eq!( + config_pda_balance_after, + config_pda_balance_before + gas_amount + ); + assert_eq!(payer_balance_after, payer_balance_before - gas_amount); +} + +#[tokio::test] +async fn fails_if_payer_not_signer() { + // Setup + let pt = ProgramTest::default(); + let mut test_fixture = TestFixture::new(pt).await; + let gas_utils = test_fixture.deploy_gas_service().await; + test_fixture.init_gas_config(&gas_utils).await.unwrap(); + + // Record balances before the transaction + let payer = Keypair::new(); + test_fixture + .fund_account(&payer.pubkey(), 1_000_000_000) + .await; + + // Action + let refund_address = Pubkey::new_unique(); + let gas_amount = 1_000_000; + let destination_chain = "ethereum".to_owned(); + let destination_addr = "destination addr 123".to_owned(); + let payload_hash = [42; 32]; + let params = "\u{1f42a}\u{1f42a}\u{1f42a}\u{1f42a}" + .to_string() + .into_bytes(); + let mut ix = axelar_solana_gas_service::instructions::pay_native_for_contract_call_instruction( + &axelar_solana_gas_service::ID, + &payer.pubkey(), + &gas_utils.config_pda, + destination_chain.clone(), + destination_addr.clone(), + payload_hash, + refund_address, + params.clone(), + gas_amount, + ) + .unwrap(); + ix.accounts[0].is_signer = false; + + let res = test_fixture + .send_tx_with_custom_signers( + &[ix], + &[ + // pays for tx + &test_fixture.payer.insecure_clone(), + ], + ) + .await; + assert!(res.is_err()); +} diff --git a/solana/programs/axelar-solana-gas-service/tests/module/refund_native_gas.rs b/solana/programs/axelar-solana-gas-service/tests/module/refund_native_gas.rs new file mode 100644 index 0000000..26665ef --- /dev/null +++ b/solana/programs/axelar-solana-gas-service/tests/module/refund_native_gas.rs @@ -0,0 +1,139 @@ +use axelar_solana_gas_service::processor::{GasServiceEvent, NativeGasRefundedEvent}; +use axelar_solana_gateway_test_fixtures::{base::TestFixture, gas_service::get_gas_service_events}; +use gateway_event_stack::ProgramInvocationState; +use solana_program_test::{tokio, ProgramTest}; +use solana_sdk::{signature::Keypair, signer::Signer}; + +#[tokio::test] +async fn test_refund_native() { + // Setup + let pt = ProgramTest::default(); + let mut test_fixture = TestFixture::new(pt).await; + let gas_utils = test_fixture.deploy_gas_service().await; + test_fixture.init_gas_config(&gas_utils).await.unwrap(); + + // Record balances before the transaction + test_fixture + .fund_account(&gas_utils.config_pda, 1_000_000_000) + .await; + let refunded_user = Keypair::new(); + let refunder_balance_before = 0; + let config_pda_balance_before = test_fixture + .banks_client + .get_account(gas_utils.config_pda) + .await + .unwrap() + .unwrap() + .lamports; + + // Action + let gas_amount = 1_000_000; + let tx_hash = [42; 64]; + let log_index = 1232; + let ix = axelar_solana_gas_service::instructions::refund_native_fees_instruction( + &axelar_solana_gas_service::ID, + &gas_utils.config_authority.pubkey(), + &refunded_user.pubkey(), + &gas_utils.config_pda, + tx_hash, + log_index, + gas_amount, + ) + .unwrap(); + + let res = test_fixture + .send_tx_with_custom_signers( + &[ix], + &[ + // pays for tx + &test_fixture.payer.insecure_clone(), + // authority for config pda deduction + &gas_utils.config_authority, + ], + ) + .await + .unwrap(); + + // assert event + let emitted_events = get_gas_service_events(&res).into_iter().next().unwrap(); + let ProgramInvocationState::Succeeded(vec_events) = emitted_events else { + panic!("unexpected event") + }; + let [(_, GasServiceEvent::NativeGasRefunded(emitted_event))] = vec_events.as_slice() else { + panic!("unexpected event") + }; + assert_eq!( + emitted_event, + &NativeGasRefundedEvent { + config_pda: gas_utils.config_pda, + tx_hash, + log_index, + receiver: refunded_user.pubkey(), + fees: gas_amount + } + ); + + // assert that SOL gets transferred + let refunder_balance_after = test_fixture + .banks_client + .get_account(refunded_user.pubkey()) + .await + .unwrap() + .unwrap() + .lamports; + let config_pda_balance_after = test_fixture + .banks_client + .get_account(gas_utils.config_pda) + .await + .unwrap() + .unwrap() + .lamports; + + assert_eq!( + config_pda_balance_after, + config_pda_balance_before - gas_amount + ); + assert_eq!(refunder_balance_after, refunder_balance_before + gas_amount); +} + +#[tokio::test] +async fn test_refund_native_fails_if_not_signed_by_authority() { + // Setup + let pt = ProgramTest::default(); + let mut test_fixture = TestFixture::new(pt).await; + let gas_utils = test_fixture.deploy_gas_service().await; + test_fixture.init_gas_config(&gas_utils).await.unwrap(); + test_fixture + .fund_account(&gas_utils.config_pda, 1_000_000_000) + .await; + + // Action + let refunded_user = Keypair::new(); + let gas_amount = 1_000_000; + let tx_hash = [42; 64]; + let log_index = 1232; + let mut ix = axelar_solana_gas_service::instructions::refund_native_fees_instruction( + &axelar_solana_gas_service::ID, + &gas_utils.config_authority.pubkey(), + &refunded_user.pubkey(), + &gas_utils.config_pda, + tx_hash, + log_index, + gas_amount, + ) + .unwrap(); + // mark that authority does not need to be a signer + ix.accounts[0].is_signer = false; + + let res = test_fixture + .send_tx_with_custom_signers( + &[ix], + &[ + // pays for tx + &test_fixture.payer.insecure_clone(), + // note -- missing authortiy signature here + ], + ) + .await; + assert!(res.is_err()); +} diff --git a/solana/programs/axelar-solana-gateway/src/error.rs b/solana/programs/axelar-solana-gateway/src/error.rs index 234f12b..20d9605 100644 --- a/solana/programs/axelar-solana-gateway/src/error.rs +++ b/solana/programs/axelar-solana-gateway/src/error.rs @@ -38,6 +38,26 @@ pub enum GatewayError { #[error("Verifier set tracker PDA already initialized")] VerifierSetTrackerAlreadyInitialised, + /// Used when a signature index is too high. + #[error("Slot is out of bounds")] + SlotIsOutOfBounds, + + /// Used when the internal digital signature verification fails. + #[error("Digital signature verification failed")] + InvalidDigitalSignature, + + /// Leaf node is not part of the Merkle root. + #[error("Leaf node not part of Merkle root")] + LeafNodeNotPartOfMerkleRoot, + + /// Used when the Merkle inclusion proof fails to verify against the given root. + #[error("Signer is not a member of the active verifier set")] + InvalidMerkleProof, + + /// Invalid destination address. + #[error("Invalid destination address")] + InvalidDestinationAddress, + /// Message Payload PDA was already initialized. #[error("Message Payload PDA was already initialized")] MessagePayloadAlreadyInitialized, @@ -53,38 +73,14 @@ pub enum GatewayError { #[error("Verifier set too old")] VerifierSetTooOld, - /// Invalid weight threshold. - #[error("Invalid weight threshold")] - InvalidWeightThreshold, - /// Data length mismatch when trying to read bytemucked data. #[error("Invalid bytemucked data length")] BytemuckDataLenInvalid, - /// Used when a signature index is too high. - #[error("Slot is out of bounds")] - SlotIsOutOfBounds, - - /// Used when the Merkle inclusion proof fails to verify against the given root. - #[error("Signer is not a member of the active verifier set")] - InvalidMerkleProof, - - /// Used when the internal digital signature verification fails. - #[error("Digital signature verification failed")] - InvalidDigitalSignature, - /// The signing session is not valid. #[error("Signing session not valid")] SigningSessionNotValid, - /// Leaf node is not part of the Merkle root. - #[error("Leaf node not part of Merkle root")] - LeafNodeNotPartOfMerkleRoot, - - /// Invalid destination address. - #[error("Invalid destination address")] - InvalidDestinationAddress, - /// Invalid verification session PDA. #[error("Invalid verification session PDA")] InvalidVerificationSessionPDA, @@ -172,8 +168,8 @@ mod tests { .collect_vec(); // confidence check that we derived the errors correctly - assert_eq!(errors_to_proceed.len(), 6); - assert_eq!(errors_to_not_proceed.len(), 23); + assert_eq!(errors_to_proceed.len(), 11); + assert_eq!(errors_to_not_proceed.len(), 17); // Errors that should cause the relayer to proceed (error numbers < 500) for error in errors_to_proceed { diff --git a/solana/programs/axelar-solana-gateway/src/lib.rs b/solana/programs/axelar-solana-gateway/src/lib.rs index 92c182f..deb02b8 100644 --- a/solana/programs/axelar-solana-gateway/src/lib.rs +++ b/solana/programs/axelar-solana-gateway/src/lib.rs @@ -7,6 +7,7 @@ pub mod state; pub use bytemuck; pub use num_traits; +pub use program_utils::BytemuckedPda; // Export current sdk types for downstream users building with a different sdk // version. diff --git a/solana/programs/axelar-solana-gateway/src/processor/approve_message.rs b/solana/programs/axelar-solana-gateway/src/processor/approve_message.rs index abc0c9f..f54b2f7 100644 --- a/solana/programs/axelar-solana-gateway/src/processor/approve_message.rs +++ b/solana/programs/axelar-solana-gateway/src/processor/approve_message.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use axelar_solana_encoding::hasher::SolanaSyscallHasher; use axelar_solana_encoding::types::execute_data::MerkleisedMessage; use axelar_solana_encoding::{rs_merkle, LeafHash}; -use program_utils::ValidPDA; +use program_utils::{BytemuckedPda, ValidPDA}; use solana_program::account_info::{next_account_info, AccountInfo}; use solana_program::entrypoint::ProgramResult; use solana_program::log::sol_log_data; @@ -13,7 +13,6 @@ use super::Processor; use crate::error::GatewayError; use crate::state::incoming_message::{command_id, IncomingMessage, MessageStatus}; use crate::state::signature_verification_pda::SignatureVerificationSessionData; -use crate::state::BytemuckedPda; use crate::{ assert_valid_incoming_message_pda, assert_valid_signature_verification_pda, event_prefixes, get_incoming_message_pda, get_validate_message_signing_pda, seed_prefixes, @@ -43,7 +42,8 @@ impl Processor { // Check: Verification session PDA is initialized. verification_session_account.check_initialized_pda_without_deserialization(program_id)?; let data = verification_session_account.try_borrow_data()?; - let session = SignatureVerificationSessionData::read(&data)?; + let session = SignatureVerificationSessionData::read(&data) + .ok_or(GatewayError::BytemuckDataLenInvalid)?; assert_valid_signature_verification_pda( gateway_root_pda.key, &payload_merkle_root, @@ -112,7 +112,8 @@ impl Processor { // Persist a new incoming message with "in progress" status in the PDA data. let mut data = incoming_message_pda.try_borrow_mut_data()?; - let incoming_message_data = IncomingMessage::read_mut(&mut data)?; + let incoming_message_data = + IncomingMessage::read_mut(&mut data).ok_or(GatewayError::BytemuckDataLenInvalid)?; *incoming_message_data = IncomingMessage::new( incoming_message_pda_bump, signing_pda_bump, diff --git a/solana/programs/axelar-solana-gateway/src/processor/call_contract.rs b/solana/programs/axelar-solana-gateway/src/processor/call_contract.rs index 98aaecc..4e86504 100644 --- a/solana/programs/axelar-solana-gateway/src/processor/call_contract.rs +++ b/solana/programs/axelar-solana-gateway/src/processor/call_contract.rs @@ -1,4 +1,4 @@ -use program_utils::ValidPDA; +use program_utils::{BytemuckedPda, ValidPDA}; use solana_program::account_info::{next_account_info, AccountInfo}; use solana_program::entrypoint::ProgramResult; use solana_program::log::sol_log_data; @@ -6,7 +6,8 @@ use solana_program::pubkey::Pubkey; use super::event_utils::{read_array, read_string, EventParseError}; use super::Processor; -use crate::state::{BytemuckedPda, GatewayConfig}; +use crate::error::GatewayError; +use crate::state::GatewayConfig; use crate::{assert_valid_gateway_root_pda, event_prefixes}; impl Processor { @@ -25,7 +26,8 @@ impl Processor { // Check: Gateway Root PDA is initialized. gateway_root_pda.check_initialized_pda_without_deserialization(program_id)?; let data = gateway_root_pda.try_borrow_data()?; - let gateway_config = GatewayConfig::read(&data)?; + let gateway_config = + GatewayConfig::read(&data).ok_or(GatewayError::BytemuckDataLenInvalid)?; assert_valid_gateway_root_pda(gateway_config.bump, gateway_root_pda.key)?; // compute the payload hash diff --git a/solana/programs/axelar-solana-gateway/src/processor/call_contract_offchain_data.rs b/solana/programs/axelar-solana-gateway/src/processor/call_contract_offchain_data.rs index 94ce945..0907c47 100644 --- a/solana/programs/axelar-solana-gateway/src/processor/call_contract_offchain_data.rs +++ b/solana/programs/axelar-solana-gateway/src/processor/call_contract_offchain_data.rs @@ -1,4 +1,4 @@ -use program_utils::ValidPDA; +use program_utils::{BytemuckedPda, ValidPDA}; use solana_program::account_info::{next_account_info, AccountInfo}; use solana_program::entrypoint::ProgramResult; use solana_program::log::sol_log_data; @@ -6,7 +6,8 @@ use solana_program::pubkey::Pubkey; use super::event_utils::{read_array, read_string, EventParseError}; use super::Processor; -use crate::state::{BytemuckedPda, GatewayConfig}; +use crate::error::GatewayError; +use crate::state::GatewayConfig; use crate::{assert_valid_gateway_root_pda, event_prefixes}; impl Processor { @@ -25,7 +26,8 @@ impl Processor { // Check: Gateway Root PDA is initialized. gateway_root_pda.check_initialized_pda_without_deserialization(program_id)?; let data = gateway_root_pda.try_borrow_data()?; - let gateway_config = GatewayConfig::read(&data)?; + let gateway_config = + GatewayConfig::read(&data).ok_or(GatewayError::BytemuckDataLenInvalid)?; assert_valid_gateway_root_pda(gateway_config.bump, gateway_root_pda.key)?; // Check: sender is signer diff --git a/solana/programs/axelar-solana-gateway/src/processor/initialize_config.rs b/solana/programs/axelar-solana-gateway/src/processor/initialize_config.rs index e822833..2da685a 100644 --- a/solana/programs/axelar-solana-gateway/src/processor/initialize_config.rs +++ b/solana/programs/axelar-solana-gateway/src/processor/initialize_config.rs @@ -2,7 +2,7 @@ use std::mem::size_of; use axelar_message_primitives::U256; use itertools::Itertools; -use program_utils::ValidPDA; +use program_utils::{BytemuckedPda, ValidPDA}; use solana_program::account_info::{next_account_info, AccountInfo}; use solana_program::clock::Clock; use solana_program::entrypoint::ProgramResult; @@ -12,9 +12,10 @@ use solana_program::system_program; use solana_program::sysvar::Sysvar; use super::Processor; +use crate::error::GatewayError; use crate::instructions::InitializeConfig; use crate::state::verifier_set_tracker::VerifierSetTracker; -use crate::state::{BytemuckedPda, GatewayConfig}; +use crate::state::GatewayConfig; use crate::{ assert_valid_gateway_root_pda, assert_valid_verifier_set_tracker_pda, get_gateway_root_config_internal, get_verifier_set_tracker_pda, seed_prefixes, @@ -71,7 +72,8 @@ impl Processor { // store account data let mut data = verifier_set_pda.try_borrow_mut_data()?; - let tracker = VerifierSetTracker::read_mut(&mut data)?; + let tracker = VerifierSetTracker::read_mut(&mut data) + .ok_or(GatewayError::BytemuckDataLenInvalid)?; *tracker = VerifierSetTracker { bump: pda_bump, _padding: [0; 7], @@ -98,7 +100,8 @@ impl Processor { &[seed_prefixes::GATEWAY_SEED, &[bump]], )?; let mut data = gateway_root_pda.try_borrow_mut_data()?; - let gateway_config = GatewayConfig::read_mut(&mut data)?; + let gateway_config = + GatewayConfig::read_mut(&mut data).ok_or(GatewayError::BytemuckDataLenInvalid)?; let clock = Clock::get()?; let current_timestamp = clock.unix_timestamp.try_into().expect("invalid timestamp"); diff --git a/solana/programs/axelar-solana-gateway/src/processor/initialize_payload_verification_session.rs b/solana/programs/axelar-solana-gateway/src/processor/initialize_payload_verification_session.rs index 7b174c7..27359d9 100644 --- a/solana/programs/axelar-solana-gateway/src/processor/initialize_payload_verification_session.rs +++ b/solana/programs/axelar-solana-gateway/src/processor/initialize_payload_verification_session.rs @@ -1,6 +1,6 @@ use std::mem::size_of; -use program_utils::ValidPDA; +use program_utils::{BytemuckedPda, ValidPDA}; use solana_program::account_info::{next_account_info, AccountInfo}; use solana_program::entrypoint::ProgramResult; use solana_program::pubkey::Pubkey; @@ -9,7 +9,7 @@ use solana_program::system_program; use super::Processor; use crate::error::GatewayError; use crate::state::signature_verification_pda::SignatureVerificationSessionData; -use crate::state::{BytemuckedPda, GatewayConfig}; +use crate::state::GatewayConfig; use crate::{assert_valid_gateway_root_pda, seed_prefixes}; impl Processor { @@ -38,7 +38,8 @@ impl Processor { // Check: Gateway Root PDA is initialized. gateway_root_pda.check_initialized_pda_without_deserialization(program_id)?; let data = gateway_root_pda.try_borrow_data()?; - let gateway_config = GatewayConfig::read(&data)?; + let gateway_config = + GatewayConfig::read(&data).ok_or(GatewayError::BytemuckDataLenInvalid)?; assert_valid_gateway_root_pda(gateway_config.bump, gateway_root_pda.key)?; // Check: Verification PDA can be derived from provided seeds. @@ -73,7 +74,8 @@ impl Processor { signers_seeds, )?; let mut data = verification_session_account.try_borrow_mut_data()?; - let session = SignatureVerificationSessionData::read_mut(&mut data)?; + let session = SignatureVerificationSessionData::read_mut(&mut data) + .ok_or(GatewayError::BytemuckDataLenInvalid)?; session.bump = bump; Ok(()) diff --git a/solana/programs/axelar-solana-gateway/src/processor/rotate_signers.rs b/solana/programs/axelar-solana-gateway/src/processor/rotate_signers.rs index eb073b0..b2fa6d6 100644 --- a/solana/programs/axelar-solana-gateway/src/processor/rotate_signers.rs +++ b/solana/programs/axelar-solana-gateway/src/processor/rotate_signers.rs @@ -3,7 +3,7 @@ use std::mem::size_of; use axelar_message_primitives::U256; use axelar_solana_encoding::hasher::SolanaSyscallHasher; -use program_utils::ValidPDA; +use program_utils::{BytemuckedPda, ValidPDA}; use solana_program::account_info::{next_account_info, AccountInfo}; use solana_program::entrypoint::ProgramResult; use solana_program::log::sol_log_data; @@ -16,7 +16,7 @@ use super::Processor; use crate::error::GatewayError; use crate::state::signature_verification_pda::SignatureVerificationSessionData; use crate::state::verifier_set_tracker::VerifierSetTracker; -use crate::state::{BytemuckedPda, GatewayConfig}; +use crate::state::GatewayConfig; use crate::{ assert_valid_gateway_root_pda, assert_valid_signature_verification_pda, assert_valid_verifier_set_tracker_pda, event_prefixes, get_verifier_set_tracker_pda, @@ -52,13 +52,15 @@ impl Processor { // Check: Gateway Root PDA is initialized. gateway_root_pda.check_initialized_pda_without_deserialization(program_id)?; let mut data = gateway_root_pda.try_borrow_mut_data()?; - let gateway_config = GatewayConfig::read_mut(&mut data)?; + let gateway_config = + GatewayConfig::read_mut(&mut data).ok_or(GatewayError::BytemuckDataLenInvalid)?; assert_valid_gateway_root_pda(gateway_config.bump, gateway_root_pda.key)?; // Check: Verification session PDA is initialized. verification_session_account.check_initialized_pda_without_deserialization(program_id)?; let mut data = verification_session_account.try_borrow_mut_data()?; - let session = SignatureVerificationSessionData::read_mut(&mut data)?; + let session = SignatureVerificationSessionData::read_mut(&mut data) + .ok_or(GatewayError::BytemuckDataLenInvalid)?; // New verifier set merkle root can be transformed into the payload hash let payload_merkle_root = @@ -85,7 +87,8 @@ impl Processor { // Check: Active verifier set tracker PDA is initialized. verifier_set_tracker_account.check_initialized_pda_without_deserialization(program_id)?; let data = verifier_set_tracker_account.try_borrow_data()?; - let verifier_set_tracker = VerifierSetTracker::read(&data)?; + let verifier_set_tracker = + VerifierSetTracker::read(&data).ok_or(GatewayError::BytemuckDataLenInvalid)?; assert_valid_verifier_set_tracker_pda( verifier_set_tracker, verifier_set_tracker_account.key, @@ -155,7 +158,8 @@ impl Processor { // store account data let mut data = new_empty_verifier_set.try_borrow_mut_data()?; - let new_verifier_set_tracker = VerifierSetTracker::read_mut(&mut data)?; + let new_verifier_set_tracker = + VerifierSetTracker::read_mut(&mut data).ok_or(GatewayError::BytemuckDataLenInvalid)?; *new_verifier_set_tracker = VerifierSetTracker { bump: new_verifier_set_bump, _padding: [0; 7], diff --git a/solana/programs/axelar-solana-gateway/src/processor/transfer_operatorship.rs b/solana/programs/axelar-solana-gateway/src/processor/transfer_operatorship.rs index 649a4c9..2499210 100644 --- a/solana/programs/axelar-solana-gateway/src/processor/transfer_operatorship.rs +++ b/solana/programs/axelar-solana-gateway/src/processor/transfer_operatorship.rs @@ -1,4 +1,4 @@ -use program_utils::ValidPDA; +use program_utils::{BytemuckedPda, ValidPDA}; use solana_program::account_info::{next_account_info, AccountInfo}; use solana_program::bpf_loader_upgradeable::{self, UpgradeableLoaderState}; use solana_program::entrypoint::ProgramResult; @@ -8,7 +8,7 @@ use solana_program::pubkey::Pubkey; use super::event_utils::{read_array, EventParseError}; use super::Processor; use crate::error::GatewayError; -use crate::state::{BytemuckedPda, GatewayConfig}; +use crate::state::GatewayConfig; use crate::{assert_valid_gateway_root_pda, event_prefixes}; impl Processor { @@ -27,7 +27,8 @@ impl Processor { // Check: Gateway Root PDA is initialized. gateway_root_pda.check_initialized_pda_without_deserialization(program_id)?; let mut data = gateway_root_pda.try_borrow_mut_data()?; - let gateway_config = GatewayConfig::read_mut(&mut data)?; + let gateway_config = + GatewayConfig::read_mut(&mut data).ok_or(GatewayError::BytemuckDataLenInvalid)?; assert_valid_gateway_root_pda(gateway_config.bump, gateway_root_pda.key)?; // Check: programdata account derived correctly (it holds the upgrade authority diff --git a/solana/programs/axelar-solana-gateway/src/processor/validate_message.rs b/solana/programs/axelar-solana-gateway/src/processor/validate_message.rs index 3539d5f..69ffd85 100644 --- a/solana/programs/axelar-solana-gateway/src/processor/validate_message.rs +++ b/solana/programs/axelar-solana-gateway/src/processor/validate_message.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use axelar_solana_encoding::hasher::SolanaSyscallHasher; use axelar_solana_encoding::types::messages::Message; use axelar_solana_encoding::LeafHash; -use program_utils::ValidPDA; +use program_utils::{BytemuckedPda, ValidPDA}; use solana_program::account_info::{next_account_info, AccountInfo}; use solana_program::log::sol_log_data; use solana_program::msg; @@ -14,7 +14,6 @@ use super::event_utils::{read_array, read_string, EventParseError}; use super::Processor; use crate::error::GatewayError; use crate::state::incoming_message::{command_id, IncomingMessage, MessageStatus}; -use crate::state::BytemuckedPda; use crate::{ assert_valid_incoming_message_pda, create_validate_message_signing_pda, event_prefixes, }; @@ -39,7 +38,8 @@ impl Processor { // Check: Gateway Root PDA is initialized. incoming_message_pda.check_initialized_pda_without_deserialization(program_id)?; let mut data = incoming_message_pda.try_borrow_mut_data()?; - let incoming_message = IncomingMessage::read_mut(&mut data)?; + let incoming_message = + IncomingMessage::read_mut(&mut data).ok_or(GatewayError::BytemuckDataLenInvalid)?; assert_valid_incoming_message_pda( &command_id, incoming_message.bump, diff --git a/solana/programs/axelar-solana-gateway/src/processor/verify_signature.rs b/solana/programs/axelar-solana-gateway/src/processor/verify_signature.rs index 377e200..1448346 100644 --- a/solana/programs/axelar-solana-gateway/src/processor/verify_signature.rs +++ b/solana/programs/axelar-solana-gateway/src/processor/verify_signature.rs @@ -1,14 +1,15 @@ use axelar_solana_encoding::types::execute_data::SigningVerifierSetInfo; -use program_utils::ValidPDA; +use program_utils::{BytemuckedPda, ValidPDA}; use solana_program::account_info::{next_account_info, AccountInfo}; use solana_program::entrypoint::ProgramResult; use solana_program::program_error::ProgramError; use solana_program::pubkey::Pubkey; use super::Processor; +use crate::error::GatewayError; use crate::state::signature_verification_pda::SignatureVerificationSessionData; use crate::state::verifier_set_tracker::VerifierSetTracker; -use crate::state::{BytemuckedPda, GatewayConfig}; +use crate::state::GatewayConfig; use crate::{ assert_valid_gateway_root_pda, assert_valid_signature_verification_pda, assert_valid_verifier_set_tracker_pda, @@ -33,13 +34,15 @@ impl Processor { // Check: Gateway Root PDA is initialized. gateway_root_pda.check_initialized_pda_without_deserialization(program_id)?; let data = gateway_root_pda.try_borrow_data()?; - let gateway_config = GatewayConfig::read(&data)?; + let gateway_config = + GatewayConfig::read(&data).ok_or(GatewayError::BytemuckDataLenInvalid)?; assert_valid_gateway_root_pda(gateway_config.bump, gateway_root_pda.key)?; // Check: Verification session PDA is initialized. verification_session_account.check_initialized_pda_without_deserialization(program_id)?; let mut data = verification_session_account.try_borrow_mut_data()?; - let session = SignatureVerificationSessionData::read_mut(&mut data)?; + let session = SignatureVerificationSessionData::read_mut(&mut data) + .ok_or(GatewayError::BytemuckDataLenInvalid)?; assert_valid_signature_verification_pda( gateway_root_pda.key, &payload_merkle_root, @@ -50,7 +53,8 @@ impl Processor { // Check: Active verifier set tracker PDA is initialized. verifier_set_tracker_account.check_initialized_pda_without_deserialization(program_id)?; let data = verifier_set_tracker_account.try_borrow_data()?; - let verifier_set_tracker = VerifierSetTracker::read(&data)?; + let verifier_set_tracker = + VerifierSetTracker::read(&data).ok_or(GatewayError::BytemuckDataLenInvalid)?; assert_valid_verifier_set_tracker_pda( verifier_set_tracker, verifier_set_tracker_account.key, diff --git a/solana/programs/axelar-solana-gateway/src/state.rs b/solana/programs/axelar-solana-gateway/src/state.rs index 12427e0..3cd0363 100644 --- a/solana/programs/axelar-solana-gateway/src/state.rs +++ b/solana/programs/axelar-solana-gateway/src/state.rs @@ -7,49 +7,4 @@ pub mod signature_verification; pub mod signature_verification_pda; pub mod verifier_set_tracker; -use bytemuck::{AnyBitPattern, NoUninit}; pub use config::GatewayConfig; -use solana_program::msg; - -use crate::error::GatewayError; - -/// A trait for types that can be safely converted to and from byte slices using `bytemuck`. -pub trait BytemuckedPda: Sized + NoUninit + AnyBitPattern { - /// Reads an immutable reference to `Self` from a byte slice. - /// - /// This method attempts to interpret the provided byte slice as an instance of `Self`. - /// It checks that the length of the slice matches the size of `Self` to ensure safety. - fn read(data: &[u8]) -> Result<&Self, GatewayError> { - let result: &Self = bytemuck::try_from_bytes(data).map_err(|err| { - msg!("bytemuck error {:?}", err); - GatewayError::BytemuckDataLenInvalid - })?; - Ok(result) - } - - /// Reads a mutable reference to `Self` from a mutable byte slice. - /// - /// Similar to [`read`], but allows for mutation of the underlying data. - /// This is useful when you need to modify the data in place. - fn read_mut(data: &mut [u8]) -> Result<&mut Self, GatewayError> { - let result: &mut Self = bytemuck::try_from_bytes_mut(data).map_err(|err| { - msg!("bytemuck error {:?}", err); - GatewayError::BytemuckDataLenInvalid - })?; - Ok(result) - } - - /// Writes the instance of `Self` into a mutable byte slice. - /// - /// This method serializes `self` into its byte representation and copies it into the - /// provided mutable byte slice. It ensures that the destination slice is of the correct - /// length to hold the data. - fn write(&self, data: &mut [u8]) -> Result<(), GatewayError> { - let self_bytes = bytemuck::bytes_of(self); - if data.len() != self_bytes.len() { - return Err(GatewayError::BytemuckDataLenInvalid); - } - data.copy_from_slice(self_bytes); - Ok(()) - } -} diff --git a/solana/programs/axelar-solana-gateway/src/state/config.rs b/solana/programs/axelar-solana-gateway/src/state/config.rs index fa36809..1695614 100644 --- a/solana/programs/axelar-solana-gateway/src/state/config.rs +++ b/solana/programs/axelar-solana-gateway/src/state/config.rs @@ -2,12 +2,11 @@ use axelar_message_primitives::U256; use bytemuck::{Pod, Zeroable}; +use program_utils::BytemuckedPda; use solana_program::pubkey::Pubkey; use crate::error::GatewayError; -use super::BytemuckedPda; - /// Timestamp alias for when the last signer rotation happened pub type Timestamp = u64; /// Seconds that need to pass between signer rotations diff --git a/solana/programs/axelar-solana-gateway/src/state/incoming_message.rs b/solana/programs/axelar-solana-gateway/src/state/incoming_message.rs index 06b01ce..eb0b7db 100644 --- a/solana/programs/axelar-solana-gateway/src/state/incoming_message.rs +++ b/solana/programs/axelar-solana-gateway/src/state/incoming_message.rs @@ -1,8 +1,7 @@ //! Module for the IncomingMessage account type. use bytemuck::{Pod, Zeroable}; - -use super::BytemuckedPda; +use program_utils::BytemuckedPda; /// Data for the incoming message (from Axelar to Solana) PDA. #[repr(C)] diff --git a/solana/programs/axelar-solana-gateway/src/state/signature_verification.rs b/solana/programs/axelar-solana-gateway/src/state/signature_verification.rs index cf742e4..178a665 100644 --- a/solana/programs/axelar-solana-gateway/src/state/signature_verification.rs +++ b/solana/programs/axelar-solana-gateway/src/state/signature_verification.rs @@ -9,11 +9,11 @@ use bitvec::order::Lsb0; use bitvec::slice::BitSlice; use bitvec::view::BitView; use bytemuck::{Pod, Zeroable}; +use program_utils::BytemuckedPda; use crate::error::GatewayError; use super::verifier_set_tracker::VerifierSetHash; -use super::BytemuckedPda; /// Controls the signature verification session for a given payload. #[repr(C)] diff --git a/solana/programs/axelar-solana-gateway/src/state/signature_verification_pda.rs b/solana/programs/axelar-solana-gateway/src/state/signature_verification_pda.rs index 69a86c0..3a9c565 100644 --- a/solana/programs/axelar-solana-gateway/src/state/signature_verification_pda.rs +++ b/solana/programs/axelar-solana-gateway/src/state/signature_verification_pda.rs @@ -1,8 +1,9 @@ //! Module for the signature verification session PDA data layout type. use bytemuck::{Pod, Zeroable}; +use program_utils::BytemuckedPda; -use super::{signature_verification::SignatureVerification, BytemuckedPda}; +use super::signature_verification::SignatureVerification; /// The data layout of a signature verification PDA /// diff --git a/solana/programs/axelar-solana-gateway/src/state/verifier_set_tracker.rs b/solana/programs/axelar-solana-gateway/src/state/verifier_set_tracker.rs index 9332484..5feacde 100644 --- a/solana/programs/axelar-solana-gateway/src/state/verifier_set_tracker.rs +++ b/solana/programs/axelar-solana-gateway/src/state/verifier_set_tracker.rs @@ -2,8 +2,7 @@ use axelar_message_primitives::U256; use bytemuck::{Pod, Zeroable}; - -use super::BytemuckedPda; +use program_utils::BytemuckedPda; /// Ever-incrementing counter for keeping track of the sequence of signer sets pub type Epoch = U256; diff --git a/solana/programs/axelar-solana-its/src/processor/mod.rs b/solana/programs/axelar-solana-its/src/processor/mod.rs index d67dc37..1e8dbb1 100644 --- a/solana/programs/axelar-solana-its/src/processor/mod.rs +++ b/solana/programs/axelar-solana-its/src/processor/mod.rs @@ -3,10 +3,11 @@ use alloy_primitives::U256; use axelar_executable::{validate_with_gmp_metadata, PROGRAM_ACCOUNTS_START_INDEX}; use axelar_solana_encoding::types::messages::Message; -use axelar_solana_gateway::state::{BytemuckedPda, GatewayConfig}; +use axelar_solana_gateway::error::GatewayError; +use axelar_solana_gateway::state::GatewayConfig; use borsh::BorshDeserialize; use interchain_token_transfer_gmp::{GMPPayload, SendToHub}; -use program_utils::{BorshPda, ValidPDA}; +use program_utils::{BorshPda, BytemuckedPda, ValidPDA}; use role_management::processor::{ ensure_signer_roles, ensure_upgrade_authority, RoleManagementAccounts, }; @@ -176,7 +177,8 @@ fn process_initialize(program_id: &Pubkey, accounts: &[AccountInfo<'_>]) -> Prog // Check: Gateway Root PDA Account is valid. let gateway_config_data = gateway_root_pda_account.try_borrow_data()?; - let gateway_config = GatewayConfig::read(&gateway_config_data)?; + let gateway_config = + GatewayConfig::read(&gateway_config_data).ok_or(GatewayError::BytemuckDataLenInvalid)?; axelar_solana_gateway::assert_valid_gateway_root_pda( gateway_config.bump, gateway_root_pda_account.key,