feat(sealevel): charge protocol fees (#4355)

### Description

Adds protocol fees to the Sealevel implementation, by embedding the
logic / custody into the mailbox program & PDA. The logic is very
similar to what already exists in the IGP implementation.

The instructions to interact with the protocol fees implementation are:
- `Init` (now requires specifying the `protocol_fee` and `beneficiary`
in addition to the other mailbox params)
- `ClaimProtocolFees` - invokable by anyone, and sending accumulated
lamports to the `beneficiary`
- `SetProtocolFeeConfig` - invokable by the owner, sets new values for
`protocol_fee` and / or `beneficiary`

### Drive-by changes

- The restriction applied on max message size has been removed to mirror
the Solidity V3 implementation

### Related issues

- Fixes https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/4356

### Backward compatibility

No - requires redeploying the mailbox

### Testing

Sealevel program integration tests, as is done for the other programs
pull/4372/head
Daniel Savu 3 months ago committed by GitHub
parent 3449fc76b1
commit 2909e81a55
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      rust/Cargo.lock
  2. 2
      rust/sealevel/client/Cargo.toml
  3. 60
      rust/sealevel/client/src/core.rs
  4. 21
      rust/sealevel/client/src/main.rs
  5. 5
      rust/sealevel/libraries/test-utils/src/lib.rs
  6. 57
      rust/sealevel/programs/hyperlane-sealevel-token-collateral/tests/functional.rs
  7. 57
      rust/sealevel/programs/hyperlane-sealevel-token-native/tests/functional.rs
  8. 43
      rust/sealevel/programs/hyperlane-sealevel-token/tests/functional.rs
  9. 554
      rust/sealevel/programs/mailbox-test/src/functional.rs
  10. 2
      rust/sealevel/programs/mailbox/Cargo.toml
  11. 15
      rust/sealevel/programs/mailbox/src/accounts.rs
  12. 17
      rust/sealevel/programs/mailbox/src/instruction.rs
  13. 1
      rust/sealevel/programs/mailbox/src/lib.rs
  14. 102
      rust/sealevel/programs/mailbox/src/processor.rs
  15. 15
      rust/sealevel/programs/mailbox/src/protocol_fee.rs

1
rust/Cargo.lock generated

@ -4549,6 +4549,7 @@ dependencies = [
"num-derive 0.4.1",
"num-traits",
"proc-macro-crate 1.2.1",
"serde",
"serializable-account-meta",
"solana-program",
"spl-noop",

@ -25,7 +25,7 @@ solana-transaction-status.workspace = true
account-utils = { path = "../libraries/account-utils" }
hyperlane-core = { path = "../../hyperlane-core" }
hyperlane-sealevel-connection-client = { path = "../libraries/hyperlane-sealevel-connection-client" }
hyperlane-sealevel-mailbox = { path = "../programs/mailbox", features = ["no-entrypoint"] }
hyperlane-sealevel-mailbox = { path = "../programs/mailbox", features = ["no-entrypoint", "serde"] }
hyperlane-sealevel-multisig-ism-message-id = { path = "../programs/ism/multisig-ism-message-id", features = ["no-entrypoint"] }
hyperlane-sealevel-token = { path = "../programs/hyperlane-sealevel-token", features = ["no-entrypoint"] }
hyperlane-sealevel-igp = { path = "../programs/hyperlane-sealevel-igp", features = ["no-entrypoint", "serde"] }

@ -1,3 +1,4 @@
use hyperlane_sealevel_mailbox::protocol_fee::ProtocolFee;
use serde::{Deserialize, Serialize};
use solana_program::pubkey::Pubkey;
@ -6,6 +7,7 @@ use solana_sdk::signature::Signer;
use std::collections::HashMap;
use std::{fs::File, path::Path};
use crate::ONE_SOL_IN_LAMPORTS;
use crate::{
artifacts::{read_json, write_json},
cmd_utils::{create_and_write_keypair, create_new_directory, deploy_program},
@ -15,6 +17,25 @@ use crate::{
use hyperlane_core::H256;
use hyperlane_sealevel_igp::accounts::{SOL_DECIMALS, TOKEN_EXCHANGE_RATE_SCALE};
#[derive(serde::Serialize, serde::Deserialize, PartialEq, Debug)]
#[serde(rename_all = "camelCase")]
struct ProtocolFeeConfig {
max_protocol_fee: u64,
fee: u64,
#[serde(with = "crate::serde::serde_option_pubkey")]
beneficiary: Option<Pubkey>,
}
impl Default for ProtocolFeeConfig {
fn default() -> Self {
Self {
max_protocol_fee: ONE_SOL_IN_LAMPORTS,
fee: 0,
beneficiary: None,
}
}
}
pub(crate) fn process_core_cmd(mut ctx: Context, cmd: CoreCmd) {
match cmd.cmd {
CoreSubCmd::Deploy(core) => {
@ -77,11 +98,28 @@ fn deploy_mailbox(
println!("Deployed Mailbox at program ID {}", program_id);
let protocol_fee_config = core
.protocol_fee_config_file
.as_deref()
.map(|p| {
let file = File::open(p).expect("Failed to open oracle config file");
serde_json::from_reader::<_, ProtocolFeeConfig>(file)
.expect("Failed to parse oracle config file")
})
.unwrap_or_default();
let protocol_fee_beneficiary = protocol_fee_config.beneficiary.unwrap_or(ctx.payer_pubkey);
// Initialize
let instruction = hyperlane_sealevel_mailbox::instruction::init_instruction(
program_id,
core.local_domain,
default_ism,
protocol_fee_config.max_protocol_fee,
ProtocolFee {
fee: protocol_fee_config.fee,
beneficiary: protocol_fee_beneficiary,
},
ctx.payer_pubkey,
)
.unwrap();
@ -327,3 +365,25 @@ pub(crate) fn read_core_program_ids(
.join("program-ids.json");
read_json(&path)
}
#[cfg(test)]
mod test {
use solana_program::pubkey::Pubkey;
#[test]
fn test_protocol_fee_serialization() {
let protocol_fee_config = super::ProtocolFeeConfig {
max_protocol_fee: 100,
fee: 10,
beneficiary: Some(Pubkey::new_unique()),
};
let json_serialized = serde_json::to_string(&protocol_fee_config).unwrap();
assert_eq!(
json_serialized,
r#"{"maxProtocolFee":100,"fee":10,"beneficiary":"1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM"}"#
);
let deserialized: super::ProtocolFeeConfig =
serde_json::from_str(&json_serialized).unwrap();
assert_eq!(deserialized, protocol_fee_config);
}
}

@ -31,7 +31,9 @@ use hyperlane_sealevel_mailbox::{
instruction::{Instruction as MailboxInstruction, OutboxDispatch},
mailbox_dispatched_message_pda_seeds, mailbox_inbox_pda_seeds,
mailbox_message_dispatch_authority_pda_seeds, mailbox_outbox_pda_seeds,
mailbox_processed_message_pda_seeds, spl_noop,
mailbox_processed_message_pda_seeds,
protocol_fee::ProtocolFee,
spl_noop,
};
use hyperlane_sealevel_token::{
@ -175,6 +177,8 @@ struct CoreDeploy {
#[command(flatten)]
env_args: EnvironmentArgs,
#[arg(long)]
protocol_fee_config_file: Option<PathBuf>,
#[arg(long)]
gas_oracle_config_file: Option<PathBuf>,
#[arg(long)]
overhead_config_file: Option<PathBuf>,
@ -209,6 +213,8 @@ const HYPERLANE_TOKEN_PROG_ID: Pubkey = pubkey!("3MzUPjP5LEkiHH82nEAe28Xtz9ztuMq
const MULTISIG_ISM_MESSAGE_ID_PROG_ID: Pubkey =
pubkey!("2YjtZDiUoptoSsA5eVrDCcX6wxNK6YoEVW7y82x5Z2fw");
const VALIDATOR_ANNOUNCE_PROG_ID: Pubkey = pubkey!("DH43ae1LwemXAboWwSh8zc9pG8j72gKUEXNi57w8fEnn");
pub const DEFAULT_PROTOCOL_FEE: u64 = 0;
pub const ONE_SOL_IN_LAMPORTS: u64 = 1_000_000_000;
#[derive(Args)]
struct Init {
@ -218,6 +224,12 @@ struct Init {
local_domain: u32,
#[arg(long, short, default_value_t = MULTISIG_ISM_MESSAGE_ID_PROG_ID)]
default_ism: Pubkey,
#[arg(long, short, default_value_t = ONE_SOL_IN_LAMPORTS)]
max_protocol_fee: u64,
#[arg(long, short, default_value_t = DEFAULT_PROTOCOL_FEE)]
protocol_fee: u64,
#[arg(long, short, default_value = None)]
protocol_fee_beneficiary: Option<Pubkey>,
}
#[derive(Args)]
@ -758,10 +770,17 @@ fn main() {
fn process_mailbox_cmd(ctx: Context, cmd: MailboxCmd) {
match cmd.cmd {
MailboxSubCmd::Init(init) => {
let protocol_fee_beneficiary =
init.protocol_fee_beneficiary.unwrap_or(ctx.payer_pubkey);
let instruction = hyperlane_sealevel_mailbox::instruction::init_instruction(
init.program_id,
init.local_domain,
init.default_ism,
init.max_protocol_fee,
ProtocolFee {
fee: init.protocol_fee,
beneficiary: protocol_fee_beneficiary,
},
ctx.payer_pubkey,
)
.unwrap();

@ -24,6 +24,7 @@ use hyperlane_sealevel_mailbox::{
instruction::{InboxProcess, Init as InitMailbox, Instruction as MailboxInstruction},
mailbox_inbox_pda_seeds, mailbox_outbox_pda_seeds, mailbox_process_authority_pda_seeds,
mailbox_processed_message_pda_seeds,
protocol_fee::ProtocolFee,
};
use hyperlane_sealevel_message_recipient_interface::{
HandleInstruction, MessageRecipientInstruction, HANDLE_ACCOUNT_METAS_PDA_SEEDS,
@ -55,6 +56,8 @@ pub async fn initialize_mailbox(
mailbox_program_id: &Pubkey,
payer: &Keypair,
local_domain: u32,
max_protocol_fee: u64,
protocol_fee: ProtocolFee,
) -> Result<MailboxAccounts, BanksClientError> {
let (inbox_account, inbox_bump) =
Pubkey::find_program_address(mailbox_inbox_pda_seeds!(), mailbox_program_id);
@ -66,6 +69,8 @@ pub async fn initialize_mailbox(
let ixn = MailboxInstruction::Init(InitMailbox {
local_domain,
default_ism,
max_protocol_fee,
protocol_fee,
});
let init_instruction = Instruction {
program_id: *mailbox_program_id,

@ -25,6 +25,7 @@ use hyperlane_sealevel_mailbox::{
accounts::{DispatchedMessage, DispatchedMessageAccount},
mailbox_dispatched_message_pda_seeds, mailbox_message_dispatch_authority_pda_seeds,
mailbox_process_authority_pda_seeds,
protocol_fee::ProtocolFee,
};
use hyperlane_sealevel_message_recipient_interface::{
HandleInstruction, MessageRecipientInstruction,
@ -425,10 +426,16 @@ async fn test_initialize() {
let (mut banks_client, payer) = setup_client().await;
let mailbox_accounts =
initialize_mailbox(&mut banks_client, &mailbox_program_id, &payer, LOCAL_DOMAIN)
.await
.unwrap();
let mailbox_accounts = initialize_mailbox(
&mut banks_client,
&mailbox_program_id,
&payer,
LOCAL_DOMAIN,
ONE_SOL_IN_LAMPORTS,
ProtocolFee::default(),
)
.await
.unwrap();
let igp_accounts =
initialize_igp_accounts(&mut banks_client, &igp_program_id(), &payer, REMOTE_DOMAIN)
@ -558,10 +565,16 @@ async fn test_transfer_remote(spl_token_program_id: Pubkey) {
let (mut banks_client, payer) = setup_client().await;
let mailbox_accounts =
initialize_mailbox(&mut banks_client, &mailbox_program_id, &payer, LOCAL_DOMAIN)
.await
.unwrap();
let mailbox_accounts = initialize_mailbox(
&mut banks_client,
&mailbox_program_id,
&payer,
LOCAL_DOMAIN,
ONE_SOL_IN_LAMPORTS,
ProtocolFee::default(),
)
.await
.unwrap();
let igp_accounts =
initialize_igp_accounts(&mut banks_client, &igp_program_id(), &payer, REMOTE_DOMAIN)
@ -796,10 +809,16 @@ async fn transfer_from_remote(
let (mut banks_client, payer) = setup_client().await;
let mailbox_accounts =
initialize_mailbox(&mut banks_client, &mailbox_program_id, &payer, LOCAL_DOMAIN)
.await
.unwrap();
let mailbox_accounts = initialize_mailbox(
&mut banks_client,
&mailbox_program_id,
&payer,
LOCAL_DOMAIN,
ONE_SOL_IN_LAMPORTS,
ProtocolFee::default(),
)
.await
.unwrap();
let igp_accounts =
initialize_igp_accounts(&mut banks_client, &igp_program_id(), &payer, REMOTE_DOMAIN)
@ -1023,10 +1042,16 @@ async fn test_transfer_from_remote_errors_if_process_authority_not_signer() {
let (mut banks_client, payer) = setup_client().await;
let _mailbox_accounts =
initialize_mailbox(&mut banks_client, &mailbox_program_id, &payer, LOCAL_DOMAIN)
.await
.unwrap();
let _mailbox_accounts = initialize_mailbox(
&mut banks_client,
&mailbox_program_id,
&payer,
LOCAL_DOMAIN,
ONE_SOL_IN_LAMPORTS,
ProtocolFee::default(),
)
.await
.unwrap();
let (mint, _mint_authority) = initialize_mint(
&mut banks_client,

@ -22,6 +22,7 @@ use hyperlane_sealevel_mailbox::{
accounts::{DispatchedMessage, DispatchedMessageAccount},
mailbox_dispatched_message_pda_seeds, mailbox_message_dispatch_authority_pda_seeds,
mailbox_process_authority_pda_seeds,
protocol_fee::ProtocolFee,
};
use hyperlane_sealevel_message_recipient_interface::{
HandleInstruction, MessageRecipientInstruction,
@ -263,10 +264,16 @@ async fn test_initialize() {
let (mut banks_client, payer) = setup_client().await;
let mailbox_accounts =
initialize_mailbox(&mut banks_client, &mailbox_program_id, &payer, LOCAL_DOMAIN)
.await
.unwrap();
let mailbox_accounts = initialize_mailbox(
&mut banks_client,
&mailbox_program_id,
&payer,
LOCAL_DOMAIN,
ONE_SOL_IN_LAMPORTS,
ProtocolFee::default(),
)
.await
.unwrap();
let igp_accounts =
initialize_igp_accounts(&mut banks_client, &igp_program_id(), &payer, REMOTE_DOMAIN)
@ -349,10 +356,16 @@ async fn test_transfer_remote() {
let (mut banks_client, payer) = setup_client().await;
let mailbox_accounts =
initialize_mailbox(&mut banks_client, &mailbox_program_id, &payer, LOCAL_DOMAIN)
.await
.unwrap();
let mailbox_accounts = initialize_mailbox(
&mut banks_client,
&mailbox_program_id,
&payer,
LOCAL_DOMAIN,
ONE_SOL_IN_LAMPORTS,
ProtocolFee::default(),
)
.await
.unwrap();
let igp_accounts =
initialize_igp_accounts(&mut banks_client, &igp_program_id(), &payer, REMOTE_DOMAIN)
@ -568,10 +581,16 @@ async fn transfer_from_remote(
let (mut banks_client, payer) = setup_client().await;
let mailbox_accounts =
initialize_mailbox(&mut banks_client, &mailbox_program_id, &payer, LOCAL_DOMAIN)
.await
.unwrap();
let mailbox_accounts = initialize_mailbox(
&mut banks_client,
&mailbox_program_id,
&payer,
LOCAL_DOMAIN,
ONE_SOL_IN_LAMPORTS,
ProtocolFee::default(),
)
.await
.unwrap();
let igp_accounts =
initialize_igp_accounts(&mut banks_client, &igp_program_id(), &payer, REMOTE_DOMAIN)
@ -722,10 +741,16 @@ async fn test_transfer_from_remote_errors_if_process_authority_not_signer() {
let (mut banks_client, payer) = setup_client().await;
let _mailbox_accounts =
initialize_mailbox(&mut banks_client, &mailbox_program_id, &payer, LOCAL_DOMAIN)
.await
.unwrap();
let _mailbox_accounts = initialize_mailbox(
&mut banks_client,
&mailbox_program_id,
&payer,
LOCAL_DOMAIN,
ONE_SOL_IN_LAMPORTS,
ProtocolFee::default(),
)
.await
.unwrap();
let hyperlane_token_accounts =
initialize_hyperlane_token(&program_id, &mut banks_client, &payer, None)

@ -15,6 +15,7 @@ use hyperlane_sealevel_mailbox::{
accounts::{DispatchedMessage, DispatchedMessageAccount},
mailbox_dispatched_message_pda_seeds, mailbox_message_dispatch_authority_pda_seeds,
mailbox_process_authority_pda_seeds,
protocol_fee::ProtocolFee,
};
use hyperlane_sealevel_message_recipient_interface::{
HandleInstruction, MessageRecipientInstruction,
@ -293,10 +294,16 @@ async fn test_initialize() {
let (mut banks_client, payer) = setup_client().await;
let mailbox_accounts =
initialize_mailbox(&mut banks_client, &mailbox_program_id, &payer, LOCAL_DOMAIN)
.await
.unwrap();
let mailbox_accounts = initialize_mailbox(
&mut banks_client,
&mailbox_program_id,
&payer,
LOCAL_DOMAIN,
ONE_SOL_IN_LAMPORTS,
ProtocolFee::default(),
)
.await
.unwrap();
let igp_accounts =
initialize_igp_accounts(&mut banks_client, &igp_program_id(), &payer, REMOTE_DOMAIN)
@ -404,10 +411,16 @@ async fn transfer_from_remote(
let (mut banks_client, payer) = setup_client().await;
let mailbox_accounts =
initialize_mailbox(&mut banks_client, &mailbox_program_id, &payer, LOCAL_DOMAIN)
.await
.unwrap();
let mailbox_accounts = initialize_mailbox(
&mut banks_client,
&mailbox_program_id,
&payer,
LOCAL_DOMAIN,
ONE_SOL_IN_LAMPORTS,
ProtocolFee::default(),
)
.await
.unwrap();
let igp_accounts =
initialize_igp_accounts(&mut banks_client, &igp_program_id(), &payer, REMOTE_DOMAIN)
@ -545,10 +558,16 @@ async fn test_transfer_from_remote_errors_if_process_authority_not_signer() {
let (mut banks_client, payer) = setup_client().await;
let _mailbox_accounts =
initialize_mailbox(&mut banks_client, &mailbox_program_id, &payer, LOCAL_DOMAIN)
.await
.unwrap();
let _mailbox_accounts = initialize_mailbox(
&mut banks_client,
&mailbox_program_id,
&payer,
LOCAL_DOMAIN,
ONE_SOL_IN_LAMPORTS,
ProtocolFee::default(),
)
.await
.unwrap();
let hyperlane_token_accounts =
initialize_hyperlane_token(&program_id, &mut banks_client, &payer, None)

@ -7,6 +7,7 @@ use hyperlane_sealevel_mailbox::{
error::Error as MailboxError,
instruction::{Instruction as MailboxInstruction, OutboxDispatch},
mailbox_dispatched_message_pda_seeds,
protocol_fee::ProtocolFee,
};
use hyperlane_sealevel_test_ism::{program::TestIsmError, test_client::TestIsmTestClient};
use hyperlane_sealevel_test_send_receiver::{
@ -21,6 +22,7 @@ use hyperlane_test_utils::{
use solana_program::{
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
rent::Rent,
system_program,
};
use solana_program_test::*;
@ -39,6 +41,8 @@ use crate::utils::{
const LOCAL_DOMAIN: u32 = 13775;
const REMOTE_DOMAIN: u32 = 69420;
const PROTOCOL_FEE: u64 = 1_000_000_000;
const MAX_PROTOCOL_FEE: u64 = 1_000_000_001;
async fn setup_client() -> (
BanksClient,
@ -92,14 +96,28 @@ async fn setup_client() -> (
(banks_client, payer, test_send_receiver, test_ism)
}
fn test_protocol_fee_config() -> ProtocolFee {
ProtocolFee {
fee: PROTOCOL_FEE,
beneficiary: Pubkey::new_unique(),
}
}
#[tokio::test]
async fn test_initialize() {
let program_id = mailbox_id();
let (mut banks_client, payer, _, _) = setup_client().await;
let mailbox_accounts = initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN)
.await
.unwrap();
let protocol_fee_config = test_protocol_fee_config();
let mailbox_accounts = initialize_mailbox(
&mut banks_client,
&program_id,
&payer,
LOCAL_DOMAIN,
MAX_PROTOCOL_FEE,
protocol_fee_config.clone(),
)
.await
.unwrap();
// Make sure the outbox account was created.
assert_outbox(
@ -110,6 +128,8 @@ async fn test_initialize() {
outbox_bump_seed: mailbox_accounts.outbox_bump_seed,
owner: Some(payer.pubkey()),
tree: MerkleTree::default(),
max_protocol_fee: MAX_PROTOCOL_FEE,
protocol_fee: protocol_fee_config,
},
)
.await;
@ -141,13 +161,28 @@ async fn test_initialize_errors_if_called_twice() {
let program_id = mailbox_id();
let (mut banks_client, payer, _, _) = setup_client().await;
initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN)
.await
.unwrap();
initialize_mailbox(
&mut banks_client,
&program_id,
&payer,
LOCAL_DOMAIN,
MAX_PROTOCOL_FEE,
test_protocol_fee_config(),
)
.await
.unwrap();
// Different local domain to force a different transaction signature,
// otherwise we'll get a (silent) duplicate transaction error.
let result = initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN + 1).await;
let result = initialize_mailbox(
&mut banks_client,
&program_id,
&payer,
LOCAL_DOMAIN + 1,
MAX_PROTOCOL_FEE,
test_protocol_fee_config(),
)
.await;
assert_transaction_error(
result,
TransactionError::InstructionError(0, InstructionError::AccountAlreadyInitialized),
@ -158,10 +193,18 @@ async fn test_initialize_errors_if_called_twice() {
async fn test_dispatch_from_eoa() {
let program_id = mailbox_id();
let (mut banks_client, payer, _, _) = setup_client().await;
let protocol_fee_config = test_protocol_fee_config();
let mailbox_accounts = initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN)
.await
.unwrap();
let mailbox_accounts = initialize_mailbox(
&mut banks_client,
&program_id,
&payer,
LOCAL_DOMAIN,
MAX_PROTOCOL_FEE,
protocol_fee_config.clone(),
)
.await
.unwrap();
let recipient = H256::random();
let message_body = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
@ -213,6 +256,8 @@ async fn test_dispatch_from_eoa() {
outbox_bump_seed: mailbox_accounts.outbox_bump_seed,
owner: Some(payer.pubkey()),
tree: expected_tree.clone(),
max_protocol_fee: MAX_PROTOCOL_FEE,
protocol_fee: protocol_fee_config.clone(),
},
)
.await;
@ -266,21 +311,270 @@ async fn test_dispatch_from_eoa() {
local_domain: LOCAL_DOMAIN,
outbox_bump_seed: mailbox_accounts.outbox_bump_seed,
owner: Some(payer.pubkey()),
tree: expected_tree,
tree: expected_tree.clone(),
max_protocol_fee: MAX_PROTOCOL_FEE,
protocol_fee: protocol_fee_config,
},
)
.await;
}
#[tokio::test]
async fn test_protocol_fee() {
let program_id = mailbox_id();
let (mut banks_client, payer, _, _) = setup_client().await;
let protocol_fee_config = test_protocol_fee_config();
let mailbox_accounts = initialize_mailbox(
&mut banks_client,
&program_id,
&payer,
LOCAL_DOMAIN,
MAX_PROTOCOL_FEE,
protocol_fee_config.clone(),
)
.await
.unwrap();
let recipient = H256::random();
let message_body = vec![];
let outbox_dispatch = OutboxDispatch {
sender: payer.pubkey(),
destination_domain: REMOTE_DOMAIN,
recipient,
message_body: message_body.clone(),
};
// ---- Test protocol fee payment ----
let outbox_balance_before = banks_client
.get_balance(mailbox_accounts.outbox)
.await
.unwrap();
dispatch_from_payer(
&mut banks_client,
&payer,
&mailbox_accounts,
outbox_dispatch,
)
.await
.unwrap();
let outbox_balance_after = banks_client
.get_balance(mailbox_accounts.outbox)
.await
.unwrap();
// The outbox pda has been paid `protocol_fee.fee`
assert_eq!(
outbox_balance_after - outbox_balance_before,
protocol_fee_config.fee,
);
// ---- Test protocol fee claiming ----
let non_beneficiary = new_funded_keypair(&mut banks_client, &payer, 1000000000).await;
let beneficiary_balance_before = banks_client
.get_balance(protocol_fee_config.beneficiary)
.await
.unwrap();
// Accounts:
// 0. `[writeable]` The outbox account.
// 1. `[writeable]` The protocol fees beneficiary.
process_instruction(
&mut banks_client,
Instruction::new_with_borsh(
program_id,
&MailboxInstruction::ClaimProtocolFees,
vec![
AccountMeta::new(mailbox_accounts.outbox, false),
AccountMeta::new(protocol_fee_config.beneficiary, false),
],
),
&non_beneficiary,
&[&non_beneficiary],
)
.await
.unwrap();
let beneficiary_balance_after = banks_client
.get_balance(protocol_fee_config.beneficiary)
.await
.unwrap();
assert_eq!(
beneficiary_balance_after - beneficiary_balance_before,
protocol_fee_config.fee,
);
// Make sure the outbox account is still rent exempt
let outbox_account = banks_client
.get_account(mailbox_accounts.outbox)
.await
.unwrap()
.unwrap();
let rent_exempt_balance = Rent::default().minimum_balance(outbox_account.data.len());
assert_eq!(outbox_account.lamports, rent_exempt_balance);
}
#[tokio::test]
async fn test_setting_valid_protocol_fee_config_works() {
let program_id = mailbox_id();
let (mut banks_client, payer, _, _) = setup_client().await;
let protocol_fee_config = test_protocol_fee_config();
let mailbox_accounts = initialize_mailbox(
&mut banks_client,
&program_id,
&payer,
LOCAL_DOMAIN,
MAX_PROTOCOL_FEE,
protocol_fee_config.clone(),
)
.await
.unwrap();
let new_protocol_fee = ProtocolFee {
fee: protocol_fee_config.fee + 1,
beneficiary: Pubkey::new_unique(),
};
process_instruction(
&mut banks_client,
Instruction::new_with_borsh(
program_id,
&MailboxInstruction::SetProtocolFeeConfig(new_protocol_fee.clone()),
vec![
AccountMeta::new(mailbox_accounts.outbox, false),
AccountMeta::new_readonly(payer.pubkey(), true),
],
),
&payer,
&[&payer],
)
.await
.unwrap();
// Make sure the outbox account was updated.
assert_outbox(
&mut banks_client,
mailbox_accounts.outbox,
Outbox {
local_domain: LOCAL_DOMAIN,
outbox_bump_seed: mailbox_accounts.outbox_bump_seed,
owner: Some(payer.pubkey()),
tree: MerkleTree::default(),
max_protocol_fee: MAX_PROTOCOL_FEE,
protocol_fee: new_protocol_fee,
},
)
.await;
}
#[tokio::test]
async fn test_setting_invalid_protocol_fee_config_fails() {
let program_id = mailbox_id();
let (mut banks_client, payer, _, _) = setup_client().await;
let protocol_fee_config = test_protocol_fee_config();
let mailbox_accounts = initialize_mailbox(
&mut banks_client,
&program_id,
&payer,
LOCAL_DOMAIN,
MAX_PROTOCOL_FEE,
protocol_fee_config.clone(),
)
.await
.unwrap();
let new_protocol_fee = ProtocolFee {
fee: MAX_PROTOCOL_FEE + 1,
beneficiary: Pubkey::new_unique(),
};
let result = process_instruction(
&mut banks_client,
Instruction::new_with_borsh(
program_id,
&MailboxInstruction::SetProtocolFeeConfig(new_protocol_fee.clone()),
vec![
AccountMeta::new(mailbox_accounts.outbox, false),
AccountMeta::new_readonly(payer.pubkey(), true),
],
),
&payer,
&[&payer],
)
.await;
assert_transaction_error(
result,
TransactionError::InstructionError(0, InstructionError::InvalidArgument),
)
}
#[tokio::test]
async fn test_setting_protocol_fee_config_from_unauthorized_account_fails() {
let program_id = mailbox_id();
let (mut banks_client, payer, _, _) = setup_client().await;
let protocol_fee_config = test_protocol_fee_config();
let mailbox_accounts = initialize_mailbox(
&mut banks_client,
&program_id,
&payer,
LOCAL_DOMAIN,
MAX_PROTOCOL_FEE,
protocol_fee_config.clone(),
)
.await
.unwrap();
let new_protocol_fee = ProtocolFee {
fee: MAX_PROTOCOL_FEE + 1,
beneficiary: Pubkey::new_unique(),
};
let unauthorized_account = new_funded_keypair(&mut banks_client, &payer, 1000000000).await;
let result = process_instruction(
&mut banks_client,
Instruction::new_with_borsh(
program_id,
&MailboxInstruction::SetProtocolFeeConfig(new_protocol_fee.clone()),
vec![
AccountMeta::new(mailbox_accounts.outbox, false),
AccountMeta::new_readonly(unauthorized_account.pubkey(), true),
],
),
&unauthorized_account,
&[&unauthorized_account],
)
.await;
assert_transaction_error(
result,
TransactionError::InstructionError(0, InstructionError::InvalidArgument),
)
}
#[tokio::test]
async fn test_dispatch_from_program() {
let program_id = mailbox_id();
let (mut banks_client, payer, mut test_send_receiver, _) = setup_client().await;
let test_sender_receiver_program_id = test_send_receiver.id();
let protocol_fee_config = test_protocol_fee_config();
let mailbox_accounts = initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN)
.await
.unwrap();
let mailbox_accounts = initialize_mailbox(
&mut banks_client,
&program_id,
&payer,
LOCAL_DOMAIN,
MAX_PROTOCOL_FEE,
protocol_fee_config.clone(),
)
.await
.unwrap();
let recipient = H256::random();
let message_body = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
@ -330,55 +624,29 @@ async fn test_dispatch_from_program() {
local_domain: LOCAL_DOMAIN,
outbox_bump_seed: mailbox_accounts.outbox_bump_seed,
owner: Some(payer.pubkey()),
tree: expected_tree,
tree: expected_tree.clone(),
max_protocol_fee: MAX_PROTOCOL_FEE,
protocol_fee: protocol_fee_config,
},
)
.await;
}
#[tokio::test]
async fn test_dispatch_errors_if_message_too_large() {
async fn test_dispatch_returns_message_id() {
let program_id = mailbox_id();
let (mut banks_client, payer, _, _) = setup_client().await;
let mailbox_accounts = initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN)
.await
.unwrap();
let recipient = H256::random();
let message_body = vec![1; 2049];
let outbox_dispatch = OutboxDispatch {
sender: payer.pubkey(),
destination_domain: REMOTE_DOMAIN,
recipient,
message_body,
};
let result = dispatch_from_payer(
let mailbox_accounts = initialize_mailbox(
&mut banks_client,
&program_id,
&payer,
&mailbox_accounts,
outbox_dispatch,
LOCAL_DOMAIN,
MAX_PROTOCOL_FEE,
test_protocol_fee_config(),
)
.await;
assert_transaction_error(
result,
TransactionError::InstructionError(
0,
InstructionError::Custom(MailboxError::MaxMessageSizeExceeded as u32),
),
);
}
#[tokio::test]
async fn test_dispatch_returns_message_id() {
let program_id = mailbox_id();
let (mut banks_client, payer, _, _) = setup_client().await;
let mailbox_accounts = initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN)
.await
.unwrap();
.await
.unwrap();
let recipient = H256::random();
let message_body = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
@ -453,9 +721,16 @@ async fn test_get_recipient_ism_when_specified() {
let program_id = mailbox_id();
let (mut banks_client, payer, mut test_send_receiver, _) = setup_client().await;
let mailbox_accounts = initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN)
.await
.unwrap();
let mailbox_accounts = initialize_mailbox(
&mut banks_client,
&program_id,
&payer,
LOCAL_DOMAIN,
MAX_PROTOCOL_FEE,
test_protocol_fee_config(),
)
.await
.unwrap();
let recipient_id = test_send_receiver.id();
@ -478,9 +753,16 @@ async fn test_get_recipient_ism_when_option_none_returned() {
let program_id = mailbox_id();
let (mut banks_client, payer, mut test_send_receiver, _) = setup_client().await;
let mailbox_accounts = initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN)
.await
.unwrap();
let mailbox_accounts = initialize_mailbox(
&mut banks_client,
&program_id,
&payer,
LOCAL_DOMAIN,
MAX_PROTOCOL_FEE,
test_protocol_fee_config(),
)
.await
.unwrap();
let recipient_id = test_send_receiver.id();
@ -504,9 +786,16 @@ async fn test_get_recipient_ism_when_no_return_data() {
let program_id = mailbox_id();
let (mut banks_client, payer, mut test_send_receiver, _) = setup_client().await;
let mailbox_accounts = initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN)
.await
.unwrap();
let mailbox_accounts = initialize_mailbox(
&mut banks_client,
&program_id,
&payer,
LOCAL_DOMAIN,
MAX_PROTOCOL_FEE,
test_protocol_fee_config(),
)
.await
.unwrap();
let recipient_id = test_send_receiver.id();
@ -534,9 +823,16 @@ async fn test_get_recipient_ism_errors_with_malformatted_recipient_ism_return_da
let program_id = mailbox_id();
let (mut banks_client, payer, mut test_send_receiver, _) = setup_client().await;
let mailbox_accounts = initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN)
.await
.unwrap();
let mailbox_accounts = initialize_mailbox(
&mut banks_client,
&program_id,
&payer,
LOCAL_DOMAIN,
MAX_PROTOCOL_FEE,
test_protocol_fee_config(),
)
.await
.unwrap();
let recipient_id = test_send_receiver.id();
@ -567,9 +863,16 @@ async fn test_process_successful_verify_and_handle() {
let program_id = mailbox_id();
let (mut banks_client, payer, _, _) = setup_client().await;
let mailbox_accounts = initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN)
.await
.unwrap();
let mailbox_accounts = initialize_mailbox(
&mut banks_client,
&program_id,
&payer,
LOCAL_DOMAIN,
MAX_PROTOCOL_FEE,
test_protocol_fee_config(),
)
.await
.unwrap();
let recipient_id = hyperlane_sealevel_test_send_receiver::id();
@ -640,9 +943,16 @@ async fn test_process_errors_if_message_already_processed() {
let program_id = mailbox_id();
let (mut banks_client, payer, _, _) = setup_client().await;
let mailbox_accounts = initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN)
.await
.unwrap();
let mailbox_accounts = initialize_mailbox(
&mut banks_client,
&program_id,
&payer,
LOCAL_DOMAIN,
MAX_PROTOCOL_FEE,
test_protocol_fee_config(),
)
.await
.unwrap();
let recipient_id = hyperlane_sealevel_test_send_receiver::id();
@ -688,9 +998,16 @@ async fn test_process_errors_if_ism_verify_fails() {
let program_id = mailbox_id();
let (mut banks_client, payer, _, mut test_ism) = setup_client().await;
let mailbox_accounts = initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN)
.await
.unwrap();
let mailbox_accounts = initialize_mailbox(
&mut banks_client,
&program_id,
&payer,
LOCAL_DOMAIN,
MAX_PROTOCOL_FEE,
test_protocol_fee_config(),
)
.await
.unwrap();
let recipient_id = hyperlane_sealevel_test_send_receiver::id();
@ -731,9 +1048,16 @@ async fn test_process_errors_if_recipient_handle_fails() {
let program_id = mailbox_id();
let (mut banks_client, payer, mut test_send_receiver, _) = setup_client().await;
let mailbox_accounts = initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN)
.await
.unwrap();
let mailbox_accounts = initialize_mailbox(
&mut banks_client,
&program_id,
&payer,
LOCAL_DOMAIN,
MAX_PROTOCOL_FEE,
test_protocol_fee_config(),
)
.await
.unwrap();
let recipient_id = hyperlane_sealevel_test_send_receiver::id();
@ -777,9 +1101,16 @@ async fn test_process_errors_if_incorrect_destination_domain() {
let program_id = mailbox_id();
let (mut banks_client, payer, _, _) = setup_client().await;
let mailbox_accounts = initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN)
.await
.unwrap();
let mailbox_accounts = initialize_mailbox(
&mut banks_client,
&program_id,
&payer,
LOCAL_DOMAIN,
MAX_PROTOCOL_FEE,
test_protocol_fee_config(),
)
.await
.unwrap();
let recipient_id = hyperlane_sealevel_test_send_receiver::id();
@ -819,9 +1150,16 @@ async fn test_process_errors_if_wrong_message_version() {
let program_id = mailbox_id();
let (mut banks_client, payer, _, _) = setup_client().await;
let mailbox_accounts = initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN)
.await
.unwrap();
let mailbox_accounts = initialize_mailbox(
&mut banks_client,
&program_id,
&payer,
LOCAL_DOMAIN,
MAX_PROTOCOL_FEE,
test_protocol_fee_config(),
)
.await
.unwrap();
let recipient_id = hyperlane_sealevel_test_send_receiver::id();
@ -860,9 +1198,16 @@ async fn test_process_errors_if_recipient_not_a_program() {
let program_id = mailbox_id();
let (mut banks_client, payer, _, _) = setup_client().await;
let mailbox_accounts = initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN)
.await
.unwrap();
let mailbox_accounts = initialize_mailbox(
&mut banks_client,
&program_id,
&payer,
LOCAL_DOMAIN,
MAX_PROTOCOL_FEE,
test_protocol_fee_config(),
)
.await
.unwrap();
let message = HyperlaneMessage {
version: 1,
@ -893,9 +1238,16 @@ async fn test_process_errors_if_reentrant() {
let program_id = mailbox_id();
let (mut banks_client, payer, mut test_send_receiver, _) = setup_client().await;
let mailbox_accounts = initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN)
.await
.unwrap();
let mailbox_accounts = initialize_mailbox(
&mut banks_client,
&program_id,
&payer,
LOCAL_DOMAIN,
MAX_PROTOCOL_FEE,
test_protocol_fee_config(),
)
.await
.unwrap();
test_send_receiver
.set_handle_mode(HandleMode::ReenterProcess)
@ -951,9 +1303,16 @@ async fn test_inbox_set_default_ism() {
let program_id = mailbox_id();
let (mut banks_client, payer, _, _) = setup_client().await;
let mailbox_accounts = initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN)
.await
.unwrap();
let mailbox_accounts = initialize_mailbox(
&mut banks_client,
&program_id,
&payer,
LOCAL_DOMAIN,
MAX_PROTOCOL_FEE,
test_protocol_fee_config(),
)
.await
.unwrap();
let new_default_ism = Pubkey::new_unique();
@ -996,9 +1355,16 @@ async fn test_inbox_set_default_ism_errors_if_owner_not_signer() {
let program_id = mailbox_id();
let (mut banks_client, payer, _, _) = setup_client().await;
let mailbox_accounts = initialize_mailbox(&mut banks_client, &program_id, &payer, LOCAL_DOMAIN)
.await
.unwrap();
let mailbox_accounts = initialize_mailbox(
&mut banks_client,
&program_id,
&payer,
LOCAL_DOMAIN,
MAX_PROTOCOL_FEE,
test_protocol_fee_config(),
)
.await
.unwrap();
let new_default_ism = Pubkey::new_unique();

@ -8,6 +8,7 @@ edition = "2021"
[features]
no-entrypoint = []
no-spl-noop = []
serde = ["dep:serde"]
[dependencies]
borsh.workspace = true
@ -29,6 +30,7 @@ hyperlane-core = { path = "../../../hyperlane-core" }
hyperlane-sealevel-interchain-security-module-interface = { path = "../../libraries/interchain-security-module-interface" }
hyperlane-sealevel-message-recipient-interface = { path = "../../libraries/message-recipient-interface" }
serializable-account-meta = { path = "../../libraries/serializable-account-meta" }
serde = { workspace = true, optional = true }
[dev-dependencies]
base64.workspace = true

@ -11,7 +11,7 @@ use solana_program::{
account_info::AccountInfo, clock::Slot, program_error::ProgramError, pubkey::Pubkey,
};
use crate::{mailbox_inbox_pda_seeds, mailbox_outbox_pda_seeds};
use crate::{mailbox_inbox_pda_seeds, mailbox_outbox_pda_seeds, protocol_fee::ProtocolFee};
/// The Inbox account.
pub type InboxAccount = AccountData<Inbox>;
@ -100,6 +100,10 @@ pub struct Outbox {
pub owner: Option<Pubkey>,
/// The merkle tree of dispatched messages.
pub tree: MerkleTree,
/// Max protocol fee that can be set.
pub max_protocol_fee: u64,
/// The protocol fee configuration.
pub protocol_fee: ProtocolFee,
}
impl SizedData for Outbox {
@ -108,7 +112,9 @@ impl SizedData for Outbox {
// 1 byte outbox_bump_seed
// 33 byte owner (1 byte enum variant, 32 byte pubkey)
// 1032 byte tree (32 * 32 = 1024 byte branch, 8 byte count)
4 + 1 + 33 + 1032
// 8 byte max_protocol_fee
// 40 byte protocol_fee (8 byte fee, 32 byte beneficiary)
4 + 1 + 33 + 1032 + 8 + 40
}
}
@ -328,6 +334,11 @@ mod test {
outbox_bump_seed: 69,
owner: Some(Pubkey::new_unique()),
tree: MerkleTree::default(),
max_protocol_fee: 100000000,
protocol_fee: ProtocolFee {
fee: 69696969,
beneficiary: Pubkey::new_unique(),
},
};
let mut serialized = vec![];

@ -8,14 +8,11 @@ use solana_program::{
pubkey::Pubkey,
};
use crate::{mailbox_inbox_pda_seeds, mailbox_outbox_pda_seeds};
use crate::{mailbox_inbox_pda_seeds, mailbox_outbox_pda_seeds, protocol_fee::ProtocolFee};
/// The current message version.
pub const VERSION: u8 = 3;
/// Maximum bytes per message = 2 KiB (somewhat arbitrarily set to begin).
pub const MAX_MESSAGE_BODY_BYTES: usize = 2 * 2_usize.pow(10);
/// Instructions supported by the Mailbox program.
#[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq)]
pub enum Instruction {
@ -39,6 +36,10 @@ pub enum Instruction {
GetOwner,
/// Transfers ownership of the Mailbox.
TransferOwnership(Option<Pubkey>),
/// Transfers accumulated protocol fees to the beneficiary.
ClaimProtocolFees,
/// Sets the protocol fee configuration.
SetProtocolFeeConfig(ProtocolFee),
}
impl Instruction {
@ -61,6 +62,10 @@ pub struct Init {
pub local_domain: u32,
/// The default ISM.
pub default_ism: Pubkey,
/// The maximum protocol fee that can be charged.
pub max_protocol_fee: u64,
/// The protocol fee configuration.
pub protocol_fee: ProtocolFee,
}
/// Instruction data for the OutboxDispatch instruction.
@ -94,6 +99,8 @@ pub fn init_instruction(
program_id: Pubkey,
local_domain: u32,
default_ism: Pubkey,
max_protocol_fee: u64,
protocol_fee: ProtocolFee,
payer: Pubkey,
) -> Result<SolanaInstruction, ProgramError> {
let (inbox_account, _inbox_bump) =
@ -108,6 +115,8 @@ pub fn init_instruction(
data: Instruction::Init(Init {
local_domain,
default_ism,
max_protocol_fee,
protocol_fee,
})
.into_instruction_data()?,
accounts: vec![

@ -9,5 +9,6 @@ pub mod error;
pub mod instruction;
pub mod pda_seeds;
pub mod processor;
pub mod protocol_fee;
pub use spl_noop;

@ -1,7 +1,7 @@
//! Entrypoint, dispatch, and execution for the Hyperlane Sealevel mailbox instruction.
use access_control::AccessControl;
use account_utils::SizedData;
use account_utils::{verify_rent_exempt, SizedData};
use borsh::{BorshDeserialize, BorshSerialize};
use hyperlane_core::{
accumulator::incremental::IncrementalMerkle as MerkleTree, Decode, Encode, HyperlaneMessage,
@ -10,14 +10,14 @@ use hyperlane_core::{
#[cfg(not(feature = "no-entrypoint"))]
use solana_program::entrypoint;
use solana_program::{
account_info::next_account_info,
account_info::AccountInfo,
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
instruction::{AccountMeta, Instruction},
msg,
program::{get_return_data, invoke, invoke_signed, set_return_data},
program_error::ProgramError,
pubkey::Pubkey,
system_instruction,
sysvar::{clock::Clock, rent::Rent, Sysvar},
};
@ -36,13 +36,11 @@ use crate::{
ProcessedMessage, ProcessedMessageAccount,
},
error::Error,
instruction::{
InboxProcess, Init, Instruction as MailboxIxn, OutboxDispatch, MAX_MESSAGE_BODY_BYTES,
VERSION,
},
instruction::{InboxProcess, Init, Instruction as MailboxIxn, OutboxDispatch, VERSION},
mailbox_dispatched_message_pda_seeds, mailbox_inbox_pda_seeds,
mailbox_message_dispatch_authority_pda_seeds, mailbox_outbox_pda_seeds,
mailbox_process_authority_pda_seeds, mailbox_processed_message_pda_seeds,
protocol_fee::ProtocolFee,
};
#[cfg(not(feature = "no-entrypoint"))]
@ -69,6 +67,10 @@ pub fn process_instruction(
MailboxIxn::TransferOwnership(new_owner) => {
transfer_ownership(program_id, accounts, new_owner)
}
MailboxIxn::ClaimProtocolFees => claim_protocol_fees(program_id, accounts),
MailboxIxn::SetProtocolFeeConfig(new_protocol_fee_config) => {
set_protocol_fee_config(program_id, accounts, new_protocol_fee_config)
}
}
.map_err(|err| {
msg!("{}", err);
@ -115,6 +117,10 @@ fn initialize(program_id: &Pubkey, accounts: &[AccountInfo], init: Init) -> Prog
default_ism: init.default_ism,
processed_count: 0,
});
if init.protocol_fee.fee > init.max_protocol_fee {
msg!("Invalid initialization config: Protocol fee is greater than max protocol fee",);
return Err(ProgramError::InvalidArgument);
}
// Create the inbox PDA account.
create_pda_account(
@ -143,6 +149,8 @@ fn initialize(program_id: &Pubkey, accounts: &[AccountInfo], init: Init) -> Prog
outbox_bump_seed: outbox_bump,
owner: Some(*payer_info.key),
tree: MerkleTree::default(),
max_protocol_fee: init.max_protocol_fee,
protocol_fee: init.protocol_fee,
});
// Create the outbox PDA account.
@ -637,15 +645,24 @@ fn outbox_dispatch(
return Err(ProgramError::from(Error::ExtraneousAccount));
}
if dispatch.message_body.len() > MAX_MESSAGE_BODY_BYTES {
return Err(ProgramError::from(Error::MaxMessageSizeExceeded));
}
let count = outbox
.tree
.count()
.try_into()
.expect("Too many messages in outbox tree");
let protocol_fee = outbox.protocol_fee.fee;
invoke(
&system_instruction::transfer(payer_info.key, outbox_info.key, protocol_fee),
&[payer_info.clone(), outbox_info.clone()],
)?;
msg!(
"Protocol fee of {} paid from {} to {}",
protocol_fee,
payer_info.key,
outbox_info.key
);
let message = HyperlaneMessage {
version: VERSION,
nonce: count,
@ -852,3 +869,66 @@ fn transfer_ownership(
Ok(())
}
/// Claims protocol fees
///
/// Accounts:
/// 0. `[writeable]` The Outbox PDA account.
/// 1. `[]` The beneficiary.
fn claim_protocol_fees(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
// Account 0: Outbox PDA.
let outbox_info = next_account_info(accounts_iter)?;
let outbox = Outbox::verify_account_and_fetch_inner(program_id, outbox_info)?;
// Account 1: Beneficiary
let beneficiary_info = next_account_info(accounts_iter)?;
if beneficiary_info.key != &outbox.protocol_fee.beneficiary {
return Err(ProgramError::InvalidArgument);
}
let rent = Rent::get()?;
let required_outbox_rent = rent.minimum_balance(outbox_info.data_len());
let claimable_protocol_fees = outbox_info.lamports().saturating_sub(required_outbox_rent);
**outbox_info.try_borrow_mut_lamports()? -= claimable_protocol_fees;
**beneficiary_info.try_borrow_mut_lamports()? += claimable_protocol_fees;
// For good measure...
verify_rent_exempt(outbox_info, &rent)?;
Ok(())
}
/// Sets the protocol fee configuration.
///
/// Accounts:
/// 0. `[writeable]` The Outbox PDA account.
/// 1. `[signer]` The current owner.
fn set_protocol_fee_config(
program_id: &Pubkey,
accounts: &[AccountInfo],
new_protocol_fee_config: ProtocolFee,
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
// Account 0: Outbox PDA.
let outbox_info = next_account_info(accounts_iter)?;
let mut outbox = Outbox::verify_account_and_fetch_inner(program_id, outbox_info)?;
// Account 1: Owner
let owner_info = next_account_info(accounts_iter)?;
outbox.ensure_owner_signer(owner_info)?;
if new_protocol_fee_config.fee > outbox.max_protocol_fee {
msg!("Invalid protocol fee config: Fee is greater than max protocol fee",);
return Err(ProgramError::InvalidArgument);
}
outbox.protocol_fee = new_protocol_fee_config;
// Store the updated outbox.
OutboxAccount::from(outbox).store(outbox_info, false)?;
Ok(())
}

@ -0,0 +1,15 @@
//! Data structures for the protocol fee configuration.
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::pubkey::Pubkey;
/// The Protocol Fee configuration.
#[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Eq, Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct ProtocolFee {
/// The current protocol fee, expressed in the lowest denomination.
pub fee: u64,
/// The beneficiary of protocol fees.
pub beneficiary: Pubkey,
}
Loading…
Cancel
Save