1use anchor_lang::{prelude::*, solana_program::clock::Clock};
2#[cfg(not(feature = "no-entrypoint"))]
3use solana_security_txt::security_txt;
4
5use crate::{
6 state::{ClaimStatus, Config, MerkleRoot, MerkleRootUploadConfig, TipDistributionAccount},
7 ErrorCode::Unauthorized,
8};
9
10#[cfg(not(feature = "no-entrypoint"))]
11security_txt! {
12 name: "Jito Tip Distribution Program",
14 project_url: "https://jito.network/",
15 contacts: "email:support@jito.network",
16 policy: "https://github.com/jito-foundation/jito-programs",
17 preferred_languages: "en",
19 source_code: "https://github.com/jito-foundation/jito-programs",
20 source_revision: std::env!("GIT_SHA"),
21 source_release: std::env!("GIT_REF_NAME")
22}
23
24pub mod merkle_proof;
25pub mod state;
26
27declare_id!("4R3gSG8BpU4t19KYj8CfnbtRpnT8gtk4dvTHxVRwc2r7");
28
29#[program]
30pub mod jito_tip_distribution {
31 use anchor_lang::solana_program;
32 use jito_programs_vote_state::VoteState;
33
34 use super::*;
35 use crate::ErrorCode::*;
36
37 pub fn initialize(
39 ctx: Context<Initialize>,
40 authority: Pubkey,
41 expired_funds_account: Pubkey,
42 num_epochs_valid: u64,
43 max_validator_commission_bps: u16,
44 bump: u8,
45 ) -> Result<()> {
46 let cfg = &mut ctx.accounts.config;
47 cfg.authority = authority;
48 cfg.expired_funds_account = expired_funds_account;
49 cfg.num_epochs_valid = num_epochs_valid;
50 cfg.max_validator_commission_bps = max_validator_commission_bps;
51 cfg.bump = bump;
52 cfg.validate()?;
53
54 Ok(())
55 }
56
57 pub fn initialize_tip_distribution_account(
60 ctx: Context<InitializeTipDistributionAccount>,
61 merkle_root_upload_authority: Pubkey,
62 validator_commission_bps: u16,
63 bump: u8,
64 ) -> Result<()> {
65 if validator_commission_bps > ctx.accounts.config.max_validator_commission_bps {
66 return Err(MaxValidatorCommissionFeeBpsExceeded.into());
67 }
68
69 let validator_vote_account_node_pubkey =
70 VoteState::deserialize_node_pubkey(&ctx.accounts.validator_vote_account)?;
71 if validator_vote_account_node_pubkey != *ctx.accounts.signer.key {
72 return Err(Unauthorized.into());
73 }
74
75 let current_epoch = Clock::get()?.epoch;
76
77 let distribution_acc = &mut ctx.accounts.tip_distribution_account;
78 distribution_acc.validator_vote_account = ctx.accounts.validator_vote_account.key();
79 distribution_acc.epoch_created_at = current_epoch;
80 distribution_acc.validator_commission_bps = validator_commission_bps;
81 distribution_acc.merkle_root_upload_authority = merkle_root_upload_authority;
82 distribution_acc.merkle_root = None;
83 distribution_acc.expires_at = current_epoch
84 .checked_add(ctx.accounts.config.num_epochs_valid)
85 .ok_or(ArithmeticError)?;
86 distribution_acc.bump = bump;
87 distribution_acc.validate()?;
88
89 emit!(TipDistributionAccountInitializedEvent {
90 tip_distribution_account: distribution_acc.key(),
91 });
92
93 Ok(())
94 }
95
96 pub fn update_config(ctx: Context<UpdateConfig>, new_config: Config) -> Result<()> {
98 UpdateConfig::auth(&ctx)?;
99
100 let config = &mut ctx.accounts.config;
101 config.authority = new_config.authority;
102 config.expired_funds_account = new_config.expired_funds_account;
103 config.num_epochs_valid = new_config.num_epochs_valid;
104 config.max_validator_commission_bps = new_config.max_validator_commission_bps;
105 config.validate()?;
106
107 emit!(ConfigUpdatedEvent {
108 authority: ctx.accounts.authority.key(),
109 });
110
111 Ok(())
112 }
113
114 pub fn upload_merkle_root(
120 ctx: Context<UploadMerkleRoot>,
121 root: [u8; 32],
122 max_total_claim: u64,
123 max_num_nodes: u64,
124 ) -> Result<()> {
125 UploadMerkleRoot::auth(&ctx)?;
126
127 let current_epoch = Clock::get()?.epoch;
128 let distribution_acc = &mut ctx.accounts.tip_distribution_account;
129
130 if let Some(merkle_root) = &distribution_acc.merkle_root {
131 if merkle_root.num_nodes_claimed > 0 {
132 return Err(Unauthorized.into());
133 }
134 }
135 if current_epoch <= distribution_acc.epoch_created_at {
136 return Err(PrematureMerkleRootUpload.into());
137 }
138
139 if current_epoch > distribution_acc.expires_at {
140 return Err(ExpiredTipDistributionAccount.into());
141 }
142
143 distribution_acc.merkle_root = Some(MerkleRoot {
144 root,
145 max_total_claim,
146 max_num_nodes,
147 total_funds_claimed: 0,
148 num_nodes_claimed: 0,
149 });
150 distribution_acc.validate()?;
151
152 emit!(MerkleRootUploadedEvent {
153 merkle_root_upload_authority: ctx.accounts.merkle_root_upload_authority.key(),
154 tip_distribution_account: distribution_acc.key(),
155 });
156
157 Ok(())
158 }
159
160 pub fn close_claim_status(ctx: Context<CloseClaimStatus>) -> Result<()> {
163 let claim_status = &ctx.accounts.claim_status;
164
165 if Clock::get()?.epoch <= claim_status.expires_at {
167 return Err(PrematureCloseClaimStatus.into());
168 }
169
170 emit!(ClaimStatusClosedEvent {
171 claim_status_payer: ctx.accounts.claim_status_payer.key(),
172 claim_status_account: claim_status.key(),
173 });
174
175 Ok(())
176 }
177
178 pub fn close_tip_distribution_account(
182 ctx: Context<CloseTipDistributionAccount>,
183 _epoch: u64,
184 ) -> Result<()> {
185 CloseTipDistributionAccount::auth(&ctx)?;
186
187 let tip_distribution_account = &mut ctx.accounts.tip_distribution_account;
188
189 if Clock::get()?.epoch <= tip_distribution_account.expires_at {
190 return Err(PrematureCloseTipDistributionAccount.into());
191 }
192
193 let expired_amount = TipDistributionAccount::claim_expired(
194 tip_distribution_account.to_account_info(),
195 ctx.accounts.expired_funds_account.to_account_info(),
196 )?;
197 tip_distribution_account.validate()?;
198
199 emit!(TipDistributionAccountClosedEvent {
200 expired_funds_account: ctx.accounts.expired_funds_account.key(),
201 tip_distribution_account: tip_distribution_account.key(),
202 expired_amount,
203 });
204
205 Ok(())
206 }
207
208 pub fn claim(ctx: Context<Claim>, bump: u8, amount: u64, proof: Vec<[u8; 32]>) -> Result<()> {
210 Claim::auth(&ctx)?;
211
212 let claim_status = &mut ctx.accounts.claim_status;
213 claim_status.bump = bump;
214
215 let claimant_account = &mut ctx.accounts.claimant;
216 let tip_distribution_account = &mut ctx.accounts.tip_distribution_account;
217
218 let clock = Clock::get()?;
219 if clock.epoch > tip_distribution_account.expires_at {
220 return Err(ExpiredTipDistributionAccount.into());
221 }
222
223 if claim_status.is_claimed {
225 return Err(FundsAlreadyClaimed.into());
226 }
227
228 let tip_distribution_info = tip_distribution_account.to_account_info();
229 let tip_distribution_epoch_expires_at = tip_distribution_account.expires_at;
230 let merkle_root = tip_distribution_account
231 .merkle_root
232 .as_mut()
233 .ok_or(RootNotUploaded)?;
234
235 let node = &solana_program::hash::hashv(&[
237 &[0u8],
238 &solana_program::hash::hashv(&[
239 &claimant_account.key().to_bytes(),
240 &amount.to_le_bytes(),
241 ])
242 .to_bytes(),
243 ]);
244
245 if !merkle_proof::verify(proof, merkle_root.root, node.to_bytes()) {
246 return Err(InvalidProof.into());
247 }
248
249 TipDistributionAccount::claim(
250 tip_distribution_info,
251 claimant_account.to_account_info(),
252 amount,
253 )?;
254
255 claim_status.amount = amount;
257 claim_status.is_claimed = true;
258 claim_status.slot_claimed_at = clock.slot;
259 claim_status.claimant = claimant_account.key();
260 claim_status.claim_status_payer = ctx.accounts.payer.key();
261 claim_status.expires_at = tip_distribution_epoch_expires_at;
262
263 merkle_root.total_funds_claimed = merkle_root
264 .total_funds_claimed
265 .checked_add(amount)
266 .ok_or(ArithmeticError)?;
267 if merkle_root.total_funds_claimed > merkle_root.max_total_claim {
268 return Err(ExceedsMaxClaim.into());
269 }
270
271 merkle_root.num_nodes_claimed = merkle_root
272 .num_nodes_claimed
273 .checked_add(1)
274 .ok_or(ArithmeticError)?;
275 if merkle_root.num_nodes_claimed > merkle_root.max_num_nodes {
276 return Err(ExceedsMaxNumNodes.into());
277 }
278
279 emit!(ClaimedEvent {
280 tip_distribution_account: tip_distribution_account.key(),
281 payer: ctx.accounts.payer.key(),
282 claimant: claimant_account.key(),
283 amount
284 });
285
286 tip_distribution_account.validate()?;
287
288 Ok(())
289 }
290
291 pub fn initialize_merkle_root_upload_config(
292 ctx: Context<InitializeMerkleRootUploadConfig>,
293 authority: Pubkey,
294 original_authority: Pubkey,
295 ) -> Result<()> {
296 InitializeMerkleRootUploadConfig::auth(&ctx)?;
298
299 let merkle_root_upload_config = &mut ctx.accounts.merkle_root_upload_config;
301 merkle_root_upload_config.override_authority = authority;
302 merkle_root_upload_config.original_upload_authority = original_authority;
303 merkle_root_upload_config.bump = ctx.bumps.merkle_root_upload_config;
304 Ok(())
305 }
306
307 pub fn update_merkle_root_upload_config(
308 ctx: Context<UpdateMerkleRootUploadConfig>,
309 authority: Pubkey,
310 original_authority: Pubkey,
311 ) -> Result<()> {
312 UpdateMerkleRootUploadConfig::auth(&ctx)?;
314
315 let merkle_root_upload_config = &mut ctx.accounts.merkle_root_upload_config;
317 merkle_root_upload_config.override_authority = authority;
318 merkle_root_upload_config.original_upload_authority = original_authority;
319
320 Ok(())
321 }
322
323 pub fn migrate_tda_merkle_root_upload_authority(
324 ctx: Context<MigrateTdaMerkleRootUploadAuthority>,
325 ) -> Result<()> {
326 let distribution_account = &mut ctx.accounts.tip_distribution_account;
327 if distribution_account.merkle_root.is_some() {
329 return Err(InvalidTdaForMigration.into());
330 }
331 if distribution_account.merkle_root_upload_authority
333 != ctx
334 .accounts
335 .merkle_root_upload_config
336 .original_upload_authority
337 {
338 return Err(InvalidTdaForMigration.into());
339 }
340
341 distribution_account.merkle_root_upload_authority =
343 ctx.accounts.merkle_root_upload_config.override_authority;
344
345 Ok(())
346 }
347}
348
349#[error_code]
350pub enum ErrorCode {
351 #[msg("Account failed validation.")]
352 AccountValidationFailure,
353
354 #[msg("Encountered an arithmetic under/overflow error.")]
355 ArithmeticError,
356
357 #[msg("The maximum number of funds to be claimed has been exceeded.")]
358 ExceedsMaxClaim,
359
360 #[msg("The maximum number of claims has been exceeded.")]
361 ExceedsMaxNumNodes,
362
363 #[msg("The given TipDistributionAccount has expired.")]
364 ExpiredTipDistributionAccount,
365
366 #[msg("The funds for the given index and TipDistributionAccount have already been claimed.")]
367 FundsAlreadyClaimed,
368
369 #[msg("Supplied invalid parameters.")]
370 InvalidParameters,
371
372 #[msg("The given proof is invalid.")]
373 InvalidProof,
374
375 #[msg("Failed to deserialize the supplied vote account data.")]
376 InvalidVoteAccountData,
377
378 #[msg("Validator's commission basis points must be less than or equal to the Config account's max_validator_commission_bps.")]
379 MaxValidatorCommissionFeeBpsExceeded,
380
381 #[msg("The given TipDistributionAccount is not ready to be closed.")]
382 PrematureCloseTipDistributionAccount,
383
384 #[msg("The given ClaimStatus account is not ready to be closed.")]
385 PrematureCloseClaimStatus,
386
387 #[msg("Must wait till at least one epoch after the tip distribution account was created to upload the merkle root.")]
388 PrematureMerkleRootUpload,
389
390 #[msg("No merkle root has been uploaded to the given TipDistributionAccount.")]
391 RootNotUploaded,
392
393 #[msg("Unauthorized signer.")]
394 Unauthorized,
395
396 #[msg("TDA not valid for migration.")]
397 InvalidTdaForMigration,
398}
399
400#[derive(Accounts)]
401pub struct CloseClaimStatus<'info> {
402 #[account(seeds = [Config::SEED], bump)]
403 pub config: Account<'info, Config>,
404
405 #[account(
408 mut,
409 close = claim_status_payer,
410 constraint = claim_status_payer.key() == claim_status.claim_status_payer
411 )]
412 pub claim_status: Account<'info, ClaimStatus>,
413
414 #[account(mut)]
417 pub claim_status_payer: UncheckedAccount<'info>,
418}
419
420#[derive(Accounts)]
421pub struct Initialize<'info> {
422 #[account(
423 init,
424 seeds = [Config::SEED],
425 bump,
426 payer = initializer,
427 space = Config::SIZE,
428 rent_exempt = enforce
429 )]
430 pub config: Account<'info, Config>,
431
432 pub system_program: Program<'info, System>,
433
434 #[account(mut)]
435 pub initializer: Signer<'info>,
436}
437
438#[derive(Accounts)]
439#[instruction(
440 _merkle_root_upload_authority: Pubkey,
441 _validator_commission_bps: u16,
442 _bump: u8
443)]
444pub struct InitializeTipDistributionAccount<'info> {
445 pub config: Account<'info, Config>,
446
447 #[account(
448 init,
449 seeds = [
450 TipDistributionAccount::SEED,
451 validator_vote_account.key().as_ref(),
452 Clock::get().unwrap().epoch.to_le_bytes().as_ref(),
453 ],
454 bump,
455 payer = signer,
456 space = TipDistributionAccount::SIZE,
457 rent_exempt = enforce
458 )]
459 pub tip_distribution_account: Account<'info, TipDistributionAccount>,
460
461 pub validator_vote_account: AccountInfo<'info>,
464
465 #[account(mut)]
467 pub signer: Signer<'info>,
468
469 pub system_program: Program<'info, System>,
470}
471
472#[derive(Accounts)]
473pub struct UpdateConfig<'info> {
474 #[account(mut, rent_exempt = enforce)]
475 pub config: Account<'info, Config>,
476
477 #[account(mut)]
478 pub authority: Signer<'info>,
479}
480
481impl UpdateConfig<'_> {
482 fn auth(ctx: &Context<UpdateConfig>) -> Result<()> {
483 if ctx.accounts.config.authority != ctx.accounts.authority.key() {
484 Err(Unauthorized.into())
485 } else {
486 Ok(())
487 }
488 }
489}
490
491#[derive(Accounts)]
492#[instruction(epoch: u64)]
493pub struct CloseTipDistributionAccount<'info> {
494 pub config: Account<'info, Config>,
495
496 #[account(mut)]
498 pub expired_funds_account: AccountInfo<'info>,
499
500 #[account(
501 mut,
502 close = validator_vote_account,
503 seeds = [
504 TipDistributionAccount::SEED,
505 validator_vote_account.key().as_ref(),
506 epoch.to_le_bytes().as_ref(),
507 ],
508 bump = tip_distribution_account.bump,
509 )]
510 pub tip_distribution_account: Account<'info, TipDistributionAccount>,
511
512 #[account(mut)]
514 pub validator_vote_account: AccountInfo<'info>,
515
516 #[account(mut)]
518 pub signer: Signer<'info>,
519}
520
521impl CloseTipDistributionAccount<'_> {
522 fn auth(ctx: &Context<CloseTipDistributionAccount>) -> Result<()> {
523 if ctx.accounts.config.expired_funds_account != ctx.accounts.expired_funds_account.key() {
524 Err(Unauthorized.into())
525 } else {
526 Ok(())
527 }
528 }
529}
530
531#[derive(Accounts)]
532#[instruction(_bump: u8, _amount: u64, _proof: Vec<[u8; 32]>)]
533pub struct Claim<'info> {
534 pub config: Account<'info, Config>,
535
536 #[account(mut, rent_exempt = enforce)]
537 pub tip_distribution_account: Account<'info, TipDistributionAccount>,
538
539 pub merkle_root_upload_authority: Signer<'info>,
540
541 #[account(
543 init,
544 rent_exempt = enforce,
545 seeds = [
546 ClaimStatus::SEED,
547 claimant.key().as_ref(),
548 tip_distribution_account.key().as_ref()
549 ],
550 bump,
551 space = ClaimStatus::SIZE,
552 payer = payer
553 )]
554 pub claim_status: Account<'info, ClaimStatus>,
555
556 #[account(mut)]
559 pub claimant: AccountInfo<'info>,
560
561 #[account(mut)]
563 pub payer: Signer<'info>,
564
565 pub system_program: Program<'info, System>,
566}
567impl Claim<'_> {
568 fn auth(ctx: &Context<Claim>) -> Result<()> {
569 if ctx.accounts.merkle_root_upload_authority.key()
570 != ctx
571 .accounts
572 .tip_distribution_account
573 .merkle_root_upload_authority
574 {
575 Err(Unauthorized.into())
576 } else {
577 Ok(())
578 }
579 }
580}
581
582#[derive(Accounts)]
583pub struct UploadMerkleRoot<'info> {
584 pub config: Account<'info, Config>,
585
586 #[account(mut, rent_exempt = enforce)]
587 pub tip_distribution_account: Account<'info, TipDistributionAccount>,
588
589 #[account(mut)]
590 pub merkle_root_upload_authority: Signer<'info>,
591}
592
593impl UploadMerkleRoot<'_> {
594 fn auth(ctx: &Context<UploadMerkleRoot>) -> Result<()> {
595 if ctx.accounts.merkle_root_upload_authority.key()
596 != ctx
597 .accounts
598 .tip_distribution_account
599 .merkle_root_upload_authority
600 {
601 Err(Unauthorized.into())
602 } else {
603 Ok(())
604 }
605 }
606}
607
608#[derive(Accounts)]
609pub struct InitializeMerkleRootUploadConfig<'info> {
610 #[account(mut, rent_exempt = enforce)]
611 pub config: Account<'info, Config>,
612
613 #[account(
614 init,
615 rent_exempt = enforce,
616 seeds = [
617 MerkleRootUploadConfig::SEED,
618 ],
619 bump,
620 space = MerkleRootUploadConfig::SIZE,
621 payer = payer
622 )]
623 pub merkle_root_upload_config: Account<'info, MerkleRootUploadConfig>,
624
625 pub authority: Signer<'info>,
626
627 #[account(mut)]
628 pub payer: Signer<'info>,
629
630 pub system_program: Program<'info, System>,
631}
632
633impl InitializeMerkleRootUploadConfig<'_> {
634 fn auth(ctx: &Context<InitializeMerkleRootUploadConfig>) -> Result<()> {
635 if ctx.accounts.config.authority != ctx.accounts.authority.key() {
636 Err(Unauthorized.into())
637 } else {
638 Ok(())
639 }
640 }
641}
642
643#[derive(Accounts)]
644pub struct UpdateMerkleRootUploadConfig<'info> {
645 #[account(rent_exempt = enforce)]
646 pub config: Account<'info, Config>,
647
648 #[account(
649 mut,
650 seeds = [MerkleRootUploadConfig::SEED],
651 bump,
652 rent_exempt = enforce,
653 )]
654 pub merkle_root_upload_config: Account<'info, MerkleRootUploadConfig>,
655
656 pub authority: Signer<'info>,
657
658 pub system_program: Program<'info, System>,
659}
660
661impl UpdateMerkleRootUploadConfig<'_> {
662 fn auth(ctx: &Context<UpdateMerkleRootUploadConfig>) -> Result<()> {
663 if ctx.accounts.config.authority != ctx.accounts.authority.key() {
664 Err(Unauthorized.into())
665 } else {
666 Ok(())
667 }
668 }
669}
670
671#[derive(Accounts)]
672pub struct MigrateTdaMerkleRootUploadAuthority<'info> {
673 #[account(mut, rent_exempt = enforce)]
674 pub tip_distribution_account: Account<'info, TipDistributionAccount>,
675
676 #[account(
677 seeds = [MerkleRootUploadConfig::SEED],
678 bump,
679 rent_exempt = enforce,
680 )]
681 pub merkle_root_upload_config: Account<'info, MerkleRootUploadConfig>,
682}
683
684#[event]
687pub struct TipDistributionAccountInitializedEvent {
688 pub tip_distribution_account: Pubkey,
689}
690
691#[event]
692pub struct ValidatorCommissionBpsUpdatedEvent {
693 pub tip_distribution_account: Pubkey,
694 pub old_commission_bps: u16,
695 pub new_commission_bps: u16,
696}
697
698#[event]
699pub struct MerkleRootUploadAuthorityUpdatedEvent {
700 pub old_authority: Pubkey,
701 pub new_authority: Pubkey,
702}
703
704#[event]
705pub struct ConfigUpdatedEvent {
706 authority: Pubkey,
708}
709
710#[event]
711pub struct ClaimedEvent {
712 pub tip_distribution_account: Pubkey,
714
715 pub payer: Pubkey,
717
718 pub claimant: Pubkey,
720
721 pub amount: u64,
723}
724
725#[event]
726pub struct MerkleRootUploadedEvent {
727 pub merkle_root_upload_authority: Pubkey,
729
730 pub tip_distribution_account: Pubkey,
732}
733
734#[event]
735pub struct TipDistributionAccountClosedEvent {
736 pub expired_funds_account: Pubkey,
738
739 pub tip_distribution_account: Pubkey,
741
742 pub expired_amount: u64,
744}
745
746#[event]
747pub struct ClaimStatusClosedEvent {
748 pub claim_status_payer: Pubkey,
750
751 pub claim_status_account: Pubkey,
753}