use anchor_lang::{prelude::*, solana_program::clock::Clock};
#[cfg(not(feature = "no-entrypoint"))]
use solana_security_txt::security_txt;
use crate::{
state::{
ClaimStatus, Config, MerkleRoot, MerkleRootUploadConfig, PriorityFeeDistributionAccount,
},
ErrorCode::Unauthorized,
};
#[cfg(not(feature = "no-entrypoint"))]
security_txt! {
name: "Jito Priority Fee Program",
project_url: "https://jito.network/",
contacts: "email:support@jito.network",
policy: "https://github.com/jito-foundation/jito-programs",
preferred_languages: "en",
source_code: "https://github.com/jito-foundation/jito-programs",
source_revision: std::env!("GIT_SHA"),
source_release: std::env!("GIT_REF_NAME")
}
pub mod merkle_proof;
pub mod state;
declare_id!("Priority6weCZ5HwDn29NxLFpb7TDp2iLZ6XKc5e8d3");
#[program]
pub mod jito_priority_fee_distribution {
use jito_programs_vote_state::VoteState;
use solana_program::native_token::lamports_to_sol;
use super::*;
use crate::ErrorCode::*;
pub fn initialize(
ctx: Context<Initialize>,
authority: Pubkey,
expired_funds_account: Pubkey,
num_epochs_valid: u64,
max_validator_commission_bps: u16,
bump: u8,
) -> Result<()> {
let cfg = &mut ctx.accounts.config;
cfg.authority = authority;
cfg.expired_funds_account = expired_funds_account;
cfg.num_epochs_valid = num_epochs_valid;
cfg.max_validator_commission_bps = max_validator_commission_bps;
cfg.go_live_epoch = u64::MAX;
cfg.bump = bump;
cfg.validate()?;
Ok(())
}
pub fn initialize_priority_fee_distribution_account(
ctx: Context<InitializePriorityFeeDistributionAccount>,
merkle_root_upload_authority: Pubkey,
validator_commission_bps: u16,
bump: u8,
) -> Result<()> {
if validator_commission_bps > ctx.accounts.config.max_validator_commission_bps {
return Err(MaxValidatorCommissionFeeBpsExceeded.into());
}
let validator_vote_account_node_pubkey =
VoteState::deserialize_node_pubkey(&ctx.accounts.validator_vote_account)?;
if validator_vote_account_node_pubkey != *ctx.accounts.signer.key {
return Err(Unauthorized.into());
}
let current_epoch = Clock::get()?.epoch;
let distribution_acc = &mut ctx.accounts.priority_fee_distribution_account;
distribution_acc.validator_vote_account = ctx.accounts.validator_vote_account.key();
distribution_acc.epoch_created_at = current_epoch;
distribution_acc.validator_commission_bps = validator_commission_bps;
distribution_acc.merkle_root_upload_authority = merkle_root_upload_authority;
distribution_acc.merkle_root = None;
distribution_acc.expires_at = current_epoch
.checked_add(ctx.accounts.config.num_epochs_valid)
.ok_or(ArithmeticError)?;
distribution_acc.bump = bump;
distribution_acc.validate()?;
emit!(PriorityFeeDistributionAccountInitializedEvent {
priority_fee_distribution_account: distribution_acc.key(),
});
Ok(())
}
pub fn update_config(ctx: Context<UpdateConfig>, new_config: Config) -> Result<()> {
UpdateConfig::auth(&ctx)?;
let config = &mut ctx.accounts.config;
config.authority = new_config.authority;
config.expired_funds_account = new_config.expired_funds_account;
config.num_epochs_valid = new_config.num_epochs_valid;
config.max_validator_commission_bps = new_config.max_validator_commission_bps;
config.go_live_epoch = new_config.go_live_epoch;
config.validate()?;
emit!(ConfigUpdatedEvent {
authority: ctx.accounts.authority.key(),
});
Ok(())
}
pub fn upload_merkle_root(
ctx: Context<UploadMerkleRoot>,
root: [u8; 32],
max_total_claim: u64,
max_num_nodes: u64,
) -> Result<()> {
UploadMerkleRoot::auth(&ctx)?;
let current_epoch = Clock::get()?.epoch;
let distribution_acc = &mut ctx.accounts.priority_fee_distribution_account;
if let Some(merkle_root) = &distribution_acc.merkle_root {
if merkle_root.num_nodes_claimed > 0 {
return Err(Unauthorized.into());
}
}
if current_epoch <= distribution_acc.epoch_created_at {
return Err(PrematureMerkleRootUpload.into());
}
if current_epoch > distribution_acc.expires_at {
return Err(ExpiredPriorityFeeDistributionAccount.into());
}
distribution_acc.merkle_root = Some(MerkleRoot {
root,
max_total_claim,
max_num_nodes,
total_funds_claimed: 0,
num_nodes_claimed: 0,
});
distribution_acc.validate()?;
emit!(MerkleRootUploadedEvent {
merkle_root_upload_authority: ctx.accounts.merkle_root_upload_authority.key(),
priority_fee_distribution_account: distribution_acc.key(),
});
Ok(())
}
pub fn close_claim_status(ctx: Context<CloseClaimStatus>) -> Result<()> {
let claim_status = &ctx.accounts.claim_status;
if Clock::get()?.epoch <= claim_status.expires_at {
return Err(PrematureCloseClaimStatus.into());
}
emit!(ClaimStatusClosedEvent {
claim_status_payer: ctx.accounts.claim_status_payer.key(),
claim_status_account: claim_status.key(),
});
Ok(())
}
pub fn close_priority_fee_distribution_account(
ctx: Context<ClosePriorityFeeDistributionAccount>,
_epoch: u64,
) -> Result<()> {
ClosePriorityFeeDistributionAccount::auth(&ctx)?;
let priority_fee_distribution_account = &mut ctx.accounts.priority_fee_distribution_account;
if Clock::get()?.epoch <= priority_fee_distribution_account.expires_at {
return Err(PrematureClosePriorityFeeDistributionAccount.into());
}
let expired_amount = PriorityFeeDistributionAccount::claim_expired(
priority_fee_distribution_account.to_account_info(),
ctx.accounts.expired_funds_account.to_account_info(),
)?;
priority_fee_distribution_account.validate()?;
emit!(PriorityFeeDistributionAccountClosedEvent {
expired_funds_account: ctx.accounts.expired_funds_account.key(),
priority_fee_distribution_account: priority_fee_distribution_account.key(),
expired_amount,
});
Ok(())
}
pub fn claim(ctx: Context<Claim>, _bump: u8, amount: u64, proof: Vec<[u8; 32]>) -> Result<()> {
Claim::auth(&ctx)?;
let claim_status = &mut ctx.accounts.claim_status;
let claimant_account = &mut ctx.accounts.claimant;
let priority_fee_distribution_account = &mut ctx.accounts.priority_fee_distribution_account;
let clock = Clock::get()?;
if clock.epoch > priority_fee_distribution_account.expires_at {
return Err(ExpiredPriorityFeeDistributionAccount.into());
}
let tip_distribution_info = priority_fee_distribution_account.to_account_info();
let tip_distribution_epoch_expires_at = priority_fee_distribution_account.expires_at;
let merkle_root = priority_fee_distribution_account
.merkle_root
.as_mut()
.ok_or(RootNotUploaded)?;
let node = &solana_program::hash::hashv(&[
&[0u8],
&solana_program::hash::hashv(&[
&claimant_account.key().to_bytes(),
&amount.to_le_bytes(),
])
.to_bytes(),
]);
if !merkle_proof::verify(proof, merkle_root.root, node.to_bytes()) {
return Err(InvalidProof.into());
}
PriorityFeeDistributionAccount::claim(
tip_distribution_info,
claimant_account.to_account_info(),
amount,
)?;
claim_status.claim_status_payer = ctx.accounts.payer.key();
claim_status.expires_at = tip_distribution_epoch_expires_at;
merkle_root.total_funds_claimed = merkle_root
.total_funds_claimed
.checked_add(amount)
.ok_or(ArithmeticError)?;
if merkle_root.total_funds_claimed > merkle_root.max_total_claim {
return Err(ExceedsMaxClaim.into());
}
merkle_root.num_nodes_claimed = merkle_root
.num_nodes_claimed
.checked_add(1)
.ok_or(ArithmeticError)?;
if merkle_root.num_nodes_claimed > merkle_root.max_num_nodes {
return Err(ExceedsMaxNumNodes.into());
}
emit!(ClaimedEvent {
priority_fee_distribution_account: priority_fee_distribution_account.key(),
payer: ctx.accounts.payer.key(),
claimant: claimant_account.key(),
amount
});
priority_fee_distribution_account.validate()?;
Ok(())
}
pub fn initialize_merkle_root_upload_config(
ctx: Context<InitializeMerkleRootUploadConfig>,
authority: Pubkey,
original_authority: Pubkey,
) -> Result<()> {
InitializeMerkleRootUploadConfig::auth(&ctx)?;
let merkle_root_upload_config = &mut ctx.accounts.merkle_root_upload_config;
merkle_root_upload_config.override_authority = authority;
merkle_root_upload_config.original_upload_authority = original_authority;
merkle_root_upload_config.bump = ctx.bumps.merkle_root_upload_config;
Ok(())
}
pub fn update_merkle_root_upload_config(
ctx: Context<UpdateMerkleRootUploadConfig>,
authority: Pubkey,
original_authority: Pubkey,
) -> Result<()> {
UpdateMerkleRootUploadConfig::auth(&ctx)?;
let merkle_root_upload_config = &mut ctx.accounts.merkle_root_upload_config;
merkle_root_upload_config.override_authority = authority;
merkle_root_upload_config.original_upload_authority = original_authority;
Ok(())
}
pub fn migrate_tda_merkle_root_upload_authority(
ctx: Context<MigrateTdaMerkleRootUploadAuthority>,
) -> Result<()> {
let distribution_account = &mut ctx.accounts.priority_fee_distribution_account;
if distribution_account.merkle_root.is_some() {
return Err(InvalidTdaForMigration.into());
}
if distribution_account.merkle_root_upload_authority
!= ctx
.accounts
.merkle_root_upload_config
.original_upload_authority
{
return Err(InvalidTdaForMigration.into());
}
distribution_account.merkle_root_upload_authority =
ctx.accounts.merkle_root_upload_config.override_authority;
Ok(())
}
pub fn transfer_priority_fee_tips(
ctx: Context<TransferPriorityFeeTips>,
lamports: u64,
) -> Result<()> {
let epoch = Clock::get()?.epoch;
require!(
ctx.accounts
.priority_fee_distribution_account
.epoch_created_at
== epoch,
ErrorCode::AccountValidationFailure
);
ctx.accounts
.priority_fee_distribution_account
.increment_total_lamports_transferred(lamports)?;
let go_live_epoch = ctx.accounts.config.go_live_epoch;
if go_live_epoch > epoch {
msg!(
"Priority fee transfer is not live yet. {}/{} - ({:.5})",
epoch,
go_live_epoch,
lamports_to_sol(lamports)
);
return Ok(());
}
let ix = solana_program::system_instruction::transfer(
ctx.accounts.from.key,
&ctx.accounts.priority_fee_distribution_account.key(),
lamports,
);
solana_program::program::invoke(
&ix,
&[
ctx.accounts.from.to_account_info(),
ctx.accounts
.priority_fee_distribution_account
.to_account_info(),
],
)
.map_err(Into::into)
}
}
#[error_code]
pub enum ErrorCode {
#[msg("Account failed validation.")]
AccountValidationFailure,
#[msg("Encountered an arithmetic under/overflow error.")]
ArithmeticError,
#[msg("The maximum number of funds to be claimed has been exceeded.")]
ExceedsMaxClaim,
#[msg("The maximum number of claims has been exceeded.")]
ExceedsMaxNumNodes,
#[msg("The given PriorityFeeDistributionAccount has expired.")]
ExpiredPriorityFeeDistributionAccount,
#[msg("The funds for the given index and PriorityFeeDistributionAccount have already been claimed.")]
FundsAlreadyClaimed,
#[msg("Supplied invalid parameters.")]
InvalidParameters,
#[msg("The given proof is invalid.")]
InvalidProof,
#[msg("Failed to deserialize the supplied vote account data.")]
InvalidVoteAccountData,
#[msg("Validator's commission basis points must be less than or equal to the Config account's max_validator_commission_bps.")]
MaxValidatorCommissionFeeBpsExceeded,
#[msg("The given PriorityFeeDistributionAccount is not ready to be closed.")]
PrematureClosePriorityFeeDistributionAccount,
#[msg("The given ClaimStatus account is not ready to be closed.")]
PrematureCloseClaimStatus,
#[msg("Must wait till at least one epoch after the tip distribution account was created to upload the merkle root.")]
PrematureMerkleRootUpload,
#[msg("No merkle root has been uploaded to the given PriorityFeeDistributionAccount.")]
RootNotUploaded,
#[msg("Unauthorized signer.")]
Unauthorized,
#[msg("TDA not valid for migration.")]
InvalidTdaForMigration,
}
#[derive(Accounts)]
pub struct CloseClaimStatus<'info> {
#[account(
mut,
close = claim_status_payer,
constraint = claim_status_payer.key() == claim_status.claim_status_payer
)]
pub claim_status: Account<'info, ClaimStatus>,
#[account(mut)]
pub claim_status_payer: UncheckedAccount<'info>,
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(
init,
seeds = [Config::SEED],
bump,
payer = initializer,
space = Config::SIZE,
rent_exempt = enforce
)]
pub config: Account<'info, Config>,
pub system_program: Program<'info, System>,
#[account(mut)]
pub initializer: Signer<'info>,
}
#[derive(Accounts)]
#[instruction(
_merkle_root_upload_authority: Pubkey,
_validator_commission_bps: u16,
_bump: u8
)]
pub struct InitializePriorityFeeDistributionAccount<'info> {
pub config: Account<'info, Config>,
#[account(
init,
seeds = [
PriorityFeeDistributionAccount::SEED,
validator_vote_account.key().as_ref(),
Clock::get().unwrap().epoch.to_le_bytes().as_ref(),
],
bump,
payer = signer,
space = PriorityFeeDistributionAccount::SIZE,
rent_exempt = enforce
)]
pub priority_fee_distribution_account: Account<'info, PriorityFeeDistributionAccount>,
pub validator_vote_account: AccountInfo<'info>,
#[account(mut)]
pub signer: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct UpdateConfig<'info> {
#[account(mut, rent_exempt = enforce)]
pub config: Account<'info, Config>,
#[account(mut)]
pub authority: Signer<'info>,
}
impl UpdateConfig<'_> {
fn auth(ctx: &Context<UpdateConfig>) -> Result<()> {
if ctx.accounts.config.authority != ctx.accounts.authority.key() {
Err(Unauthorized.into())
} else {
Ok(())
}
}
}
#[derive(Accounts)]
#[instruction(epoch: u64)]
pub struct ClosePriorityFeeDistributionAccount<'info> {
pub config: Account<'info, Config>,
#[account(mut)]
pub expired_funds_account: AccountInfo<'info>,
#[account(
mut,
close = validator_vote_account,
seeds = [
PriorityFeeDistributionAccount::SEED,
validator_vote_account.key().as_ref(),
epoch.to_le_bytes().as_ref(),
],
bump = priority_fee_distribution_account.bump,
)]
pub priority_fee_distribution_account: Account<'info, PriorityFeeDistributionAccount>,
#[account(mut)]
pub validator_vote_account: AccountInfo<'info>,
#[account(mut)]
pub signer: Signer<'info>,
}
impl ClosePriorityFeeDistributionAccount<'_> {
fn auth(ctx: &Context<ClosePriorityFeeDistributionAccount>) -> Result<()> {
if ctx.accounts.config.expired_funds_account != ctx.accounts.expired_funds_account.key() {
Err(Unauthorized.into())
} else {
Ok(())
}
}
}
#[derive(Accounts)]
#[instruction(_bump: u8, _amount: u64, _proof: Vec<[u8; 32]>)]
pub struct Claim<'info> {
pub config: Account<'info, Config>,
#[account(mut, rent_exempt = enforce)]
pub priority_fee_distribution_account: Account<'info, PriorityFeeDistributionAccount>,
pub merkle_root_upload_authority: Signer<'info>,
#[account(
init,
rent_exempt = enforce,
seeds = [
ClaimStatus::SEED,
claimant.key().as_ref(),
priority_fee_distribution_account.key().as_ref()
],
bump,
space = ClaimStatus::SIZE,
payer = payer
)]
pub claim_status: Account<'info, ClaimStatus>,
#[account(mut)]
pub claimant: AccountInfo<'info>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
}
impl Claim<'_> {
fn auth(ctx: &Context<Claim>) -> Result<()> {
if ctx.accounts.merkle_root_upload_authority.key()
!= ctx
.accounts
.priority_fee_distribution_account
.merkle_root_upload_authority
{
Err(Unauthorized.into())
} else {
Ok(())
}
}
}
#[derive(Accounts)]
pub struct UploadMerkleRoot<'info> {
pub config: Account<'info, Config>,
#[account(mut, rent_exempt = enforce)]
pub priority_fee_distribution_account: Account<'info, PriorityFeeDistributionAccount>,
#[account(mut)]
pub merkle_root_upload_authority: Signer<'info>,
}
impl UploadMerkleRoot<'_> {
fn auth(ctx: &Context<UploadMerkleRoot>) -> Result<()> {
if ctx.accounts.merkle_root_upload_authority.key()
!= ctx
.accounts
.priority_fee_distribution_account
.merkle_root_upload_authority
{
Err(Unauthorized.into())
} else {
Ok(())
}
}
}
#[derive(Accounts)]
pub struct InitializeMerkleRootUploadConfig<'info> {
#[account(mut, rent_exempt = enforce)]
pub config: Account<'info, Config>,
#[account(
init,
rent_exempt = enforce,
seeds = [
MerkleRootUploadConfig::SEED,
],
bump,
space = MerkleRootUploadConfig::SIZE,
payer = payer
)]
pub merkle_root_upload_config: Account<'info, MerkleRootUploadConfig>,
pub authority: Signer<'info>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
}
impl InitializeMerkleRootUploadConfig<'_> {
fn auth(ctx: &Context<InitializeMerkleRootUploadConfig>) -> Result<()> {
if ctx.accounts.config.authority != ctx.accounts.authority.key() {
Err(Unauthorized.into())
} else {
Ok(())
}
}
}
#[derive(Accounts)]
pub struct UpdateMerkleRootUploadConfig<'info> {
#[account(rent_exempt = enforce)]
pub config: Account<'info, Config>,
#[account(
mut,
seeds = [MerkleRootUploadConfig::SEED],
bump,
rent_exempt = enforce,
)]
pub merkle_root_upload_config: Account<'info, MerkleRootUploadConfig>,
pub authority: Signer<'info>,
pub system_program: Program<'info, System>,
}
impl UpdateMerkleRootUploadConfig<'_> {
fn auth(ctx: &Context<UpdateMerkleRootUploadConfig>) -> Result<()> {
if ctx.accounts.config.authority != ctx.accounts.authority.key() {
Err(Unauthorized.into())
} else {
Ok(())
}
}
}
#[derive(Accounts)]
pub struct MigrateTdaMerkleRootUploadAuthority<'info> {
#[account(mut, rent_exempt = enforce)]
pub priority_fee_distribution_account: Account<'info, PriorityFeeDistributionAccount>,
#[account(
seeds = [MerkleRootUploadConfig::SEED],
bump,
rent_exempt = enforce,
)]
pub merkle_root_upload_config: Account<'info, MerkleRootUploadConfig>,
}
#[derive(Accounts)]
pub struct TransferPriorityFeeTips<'info> {
#[account(rent_exempt = enforce)]
pub config: Account<'info, Config>,
#[account(
mut,
rent_exempt = enforce
)]
pub priority_fee_distribution_account: Account<'info, PriorityFeeDistributionAccount>,
#[account(mut)]
pub from: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[event]
pub struct PriorityFeeDistributionAccountInitializedEvent {
pub priority_fee_distribution_account: Pubkey,
}
#[event]
pub struct ValidatorCommissionBpsUpdatedEvent {
pub priority_fee_distribution_account: Pubkey,
pub old_commission_bps: u16,
pub new_commission_bps: u16,
}
#[event]
pub struct MerkleRootUploadAuthorityUpdatedEvent {
pub old_authority: Pubkey,
pub new_authority: Pubkey,
}
#[event]
pub struct ConfigUpdatedEvent {
authority: Pubkey,
}
#[event]
pub struct ClaimedEvent {
pub priority_fee_distribution_account: Pubkey,
pub payer: Pubkey,
pub claimant: Pubkey,
pub amount: u64,
}
#[event]
pub struct MerkleRootUploadedEvent {
pub merkle_root_upload_authority: Pubkey,
pub priority_fee_distribution_account: Pubkey,
}
#[event]
pub struct PriorityFeeDistributionAccountClosedEvent {
pub expired_funds_account: Pubkey,
pub priority_fee_distribution_account: Pubkey,
pub expired_amount: u64,
}
#[event]
pub struct ClaimStatusClosedEvent {
pub claim_status_payer: Pubkey,
pub claim_status_account: Pubkey,
}