jito_tip_distribution/
lib.rs

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    // Required fields
13    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    // Optional Fields
18    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    /// Initialize a singleton instance of the [Config] account.
38    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    /// Initialize a new [TipDistributionAccount] associated with the given validator vote key
58    /// and current epoch.
59    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    /// Update config fields. Only the [Config] authority can invoke this.
97    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    /// Uploads a merkle root to the provided [TipDistributionAccount].
115    ///
116    /// This instruction may be invoked many times as long as the account is at least one epoch old and not expired; and
117    /// no funds have already been claimed. Only the `merkle_root_upload_authority` has the
118    /// authority to invoke.
119    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    /// Anyone can invoke this only after the [TipDistributionAccount] has expired.
161    /// This instruction will return any rent back to `claimant` and close the account
162    pub fn close_claim_status(ctx: Context<CloseClaimStatus>) -> Result<()> {
163        let claim_status = &ctx.accounts.claim_status;
164
165        // can only claim after claim_status has expired to prevent draining.
166        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    /// Anyone can invoke this only after the [TipDistributionAccount] has expired.
179    /// This instruction will send any unclaimed funds to the designated `expired_funds_account`
180    /// before closing and returning the rent exempt funds to the validator.
181    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    /// Claims tokens from the [TipDistributionAccount].
209    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        // Redundant check since we shouldn't be able to init a claim status account using the same seeds.
224        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        // Verify the merkle proof.
236        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        // Mark it claimed.
256        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        // Call the authorize function
297        InitializeMerkleRootUploadConfig::auth(&ctx)?;
298
299        // Set the bump and override authority
300        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        // Call the authorize function
313        UpdateMerkleRootUploadConfig::auth(&ctx)?;
314
315        // Update override authority
316        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        // Validate TDA has no MerkleRoot uploaded to it
328        if distribution_account.merkle_root.is_some() {
329            return Err(InvalidTdaForMigration.into());
330        }
331        // Validate the TDA key is the acceptable original authority (i.e. the original Jito Lab's authority)
332        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        // Change the TDA's root upload authority
342        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    // bypass seed check since owner check prevents attacker from passing in invalid data
406    // account can only be transferred to us if it is zeroed, failing the deserialization check
407    #[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    /// CHECK: This is checked against claim_status in the constraint
415    /// Receiver of the funds.
416    #[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    /// CHECK: Safe because we check the vote program is the owner before deserialization.
462    /// The validator's vote account is used to check this transaction's signer is also the authorized withdrawer.
463    pub validator_vote_account: AccountInfo<'info>,
464
465    /// Must be equal to the supplied validator vote account's authorized withdrawer.
466    #[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    /// CHECK: safe see auth fn
497    #[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    /// CHECK: safe see auth fn
513    #[account(mut)]
514    pub validator_vote_account: AccountInfo<'info>,
515
516    /// Anyone can crank this instruction.
517    #[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    /// Status of the claim. Used to prevent the same party from claiming multiple times.
542    #[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    /// CHECK: This is safe.
557    /// Receiver of the funds.
558    #[account(mut)]
559    pub claimant: AccountInfo<'info>,
560
561    /// Who is paying for the claim.
562    #[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// Events
685
686#[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    /// Who updated it.
707    authority: Pubkey,
708}
709
710#[event]
711pub struct ClaimedEvent {
712    /// [TipDistributionAccount] claimed from.
713    pub tip_distribution_account: Pubkey,
714
715    /// User that paid for the claim, may or may not be the same as claimant.
716    pub payer: Pubkey,
717
718    /// Account that received the funds.
719    pub claimant: Pubkey,
720
721    /// Amount of funds to distribute.
722    pub amount: u64,
723}
724
725#[event]
726pub struct MerkleRootUploadedEvent {
727    /// Who uploaded the root.
728    pub merkle_root_upload_authority: Pubkey,
729
730    /// Where the root was uploaded to.
731    pub tip_distribution_account: Pubkey,
732}
733
734#[event]
735pub struct TipDistributionAccountClosedEvent {
736    /// Account where unclaimed funds were transferred to.
737    pub expired_funds_account: Pubkey,
738
739    /// [TipDistributionAccount] closed.
740    pub tip_distribution_account: Pubkey,
741
742    /// Unclaimed amount transferred.
743    pub expired_amount: u64,
744}
745
746#[event]
747pub struct ClaimStatusClosedEvent {
748    /// Account where funds were transferred to.
749    pub claim_status_payer: Pubkey,
750
751    /// [ClaimStatus] account that was closed.
752    pub claim_status_account: Pubkey,
753}