lido 1.1.0-patch.1

Lido for Solana is a Lido DAO-governed liquid staking protocol for the Solana blockchain.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
// SPDX-FileCopyrightText: 2021 Chorus One AG
// SPDX-License-Identifier: GPL-3.0

#![allow(clippy::too_many_arguments)]

use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};

use solana_program::{
    account_info::AccountInfo,
    instruction::{AccountMeta, Instruction},
    program_error::ProgramError,
    pubkey::Pubkey,
    stake as stake_program, system_program,
    sysvar::{self, stake_history},
    vote,
};

use crate::{
    accounts_struct, accounts_struct_meta,
    error::LidoError,
    state::RewardDistribution,
    token::{Lamports, StLamports},
};

#[repr(C)]
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, BorshSchema)]
pub enum LidoInstruction {
    Initialize {
        #[allow(dead_code)] // but it's not
        reward_distribution: RewardDistribution,
        #[allow(dead_code)] // but it's not
        max_validators: u32,
        #[allow(dead_code)] // but it's not
        max_maintainers: u32,
    },

    /// Deposit a given amount of SOL.
    ///
    /// This can be called by anybody.
    Deposit {
        #[allow(dead_code)] // but it's not
        amount: Lamports,
    },

    /// Withdraw a given amount of stSOL.
    ///
    /// Caller provides some `amount` of StLamports that are to be burned in
    /// order to withdraw SOL.
    Withdraw {
        #[allow(dead_code)] // but it's not
        amount: StLamports,
    },

    /// Move deposits from the reserve into a stake account and delegate it to a member validator.
    StakeDeposit {
        #[allow(dead_code)] // but it's not
        amount: Lamports,
    },
    /// Unstake from a validator to a new stake account.
    Unstake {
        #[allow(dead_code)] // but it's not
        amount: Lamports,
    },
    /// Update the exchange rate, at the beginning of the epoch.
    ///
    /// This can be called by anybody.
    UpdateExchangeRate,

    /// Observe any external changes in the balances of a validator's stake accounts.
    ///
    /// If there is inactive balance in stake accounts, withdraw this back to the reserve.
    WithdrawInactiveStake,

    /// Claim rewards from the validator account and distribute rewards.
    CollectValidatorFee,
    ClaimValidatorFee,
    ChangeRewardDistribution {
        #[allow(dead_code)] // but it's not
        new_reward_distribution: RewardDistribution,
    },

    /// Add a new validator to the validator set.
    ///
    /// Requires the manager to sign.
    AddValidator,

    /// Set the `active` flag to false for a given validator.
    ///
    /// Requires the manager to sign.
    ///
    /// Deactivation initiates the validator removal process:
    ///
    /// * It prevents new funds from being staked with the validator.
    /// * It signals to the maintainer bot to start unstaking from this validator.
    ///
    /// Once there are no more delegations to this validator, and it has no
    /// unclaimed fee credits, then the validator can be removed.
    DeactivateValidator,

    RemoveValidator,
    AddMaintainer,
    RemoveMaintainer,
    MergeStake,
}

impl LidoInstruction {
    pub fn to_vec(&self) -> Vec<u8> {
        // `BorshSerialize::try_to_vec` returns a Result, because it uses
        // `Borsh::serialize`, which takes an arbitrary writer, and which can
        // therefore return an IoError. But when serializing to a vec, there
        // is no IO, so for this particular writer, it should never fail.
        self.try_to_vec()
            .expect("Serializing an Instruction to Vec<u8> does not fail.")
    }
}

accounts_struct! {
    InitializeAccountsMeta, InitializeAccountsInfo {
        pub lido {
            is_signer: false,
            is_writable: true,
        },
        pub manager {
            is_signer: false,
            is_writable: false,
        },
        pub st_sol_mint {
            is_signer: false,
            is_writable: false,
        },
        pub treasury_account {
            is_signer: false,
            is_writable: false,
        },
        pub developer_account {
            is_signer: false,
            is_writable: false,
        },
        pub reserve_account {
            is_signer: false,
            is_writable: false,
        },
        const sysvar_rent = sysvar::rent::id(),
        const spl_token = spl_token::id(),
    }
}

pub fn initialize(
    program_id: &Pubkey,
    reward_distribution: RewardDistribution,
    max_validators: u32,
    max_maintainers: u32,
    accounts: &InitializeAccountsMeta,
) -> Instruction {
    let data = LidoInstruction::Initialize {
        reward_distribution,
        max_validators,
        max_maintainers,
    };
    Instruction {
        program_id: *program_id,
        accounts: accounts.to_vec(),
        data: data.to_vec(),
    }
}

accounts_struct! {
    DepositAccountsMeta, DepositAccountsInfo {
        pub lido {
            is_signer: false,
            // Needs to be writable for us to update the metrics.
            is_writable: true,
        },
        pub user {
            is_signer: true,
            // Is writable due to transfer (system_instruction::transfer) from user to
            // reserve_account
            is_writable: true,
        },
        pub recipient {
            is_signer: false,
            // Is writable due to mint to (spl_token::instruction::mint_to) recipient from
            // st_sol_mint
            is_writable: true,
        },
        pub st_sol_mint {
            is_signer: false,
            // Is writable due to mint to (spl_token::instruction::mint_to) recipient from
            // st_sol_mint
            is_writable: true,
        },
        pub reserve_account {
            is_signer: false,
            // Is writable due to transfer (system_instruction::transfer) from user to
            // reserve_account
            is_writable: true,
        },
        pub mint_authority {
            is_signer: false,
            is_writable: false,
        },
        const spl_token = spl_token::id(),
        const system_program = system_program::id(),
    }
}

pub fn deposit(
    program_id: &Pubkey,
    accounts: &DepositAccountsMeta,
    amount: Lamports,
) -> Instruction {
    let data = LidoInstruction::Deposit { amount };
    Instruction {
        program_id: *program_id,
        accounts: accounts.to_vec(),
        data: data.to_vec(),
    }
}

accounts_struct! {
    WithdrawAccountsMeta, WithdrawAccountsInfo {
        pub lido {
            is_signer: false,
            // Needs to be writable for us to update the metrics.
            is_writable: true,
        },
        pub st_sol_account_owner {
            is_signer: true,
            is_writable: false,
        },
        // This should be owned by the user.
        pub st_sol_account {
            is_signer: false,
            // Is writable due to st_sol burn (spl_token::instruction::burn)
            is_writable: true,
        },
        pub st_sol_mint {
            is_signer: false,
            // Is writable due to st_sol burn (spl_token::instruction::burn)
            is_writable: true,
        },
        pub validator_vote_account {
            is_signer: false,
            is_writable: false,
        },
        // Stake account to withdraw from.
        pub source_stake_account {
            is_signer: false,
            // Is writable due to spliti stake (solana_program::stake::instruction::split)
            is_writable: true,
        },
        // Stake where the withdrawn amounts will go.
        pub destination_stake_account {
            is_signer: true,
            // Is writable due to split stake (solana_program::stake::instruction::split) and
            // transfer of stake authority (solana_program::stake::instruction::authorize
            is_writable: true,
        },
        // Used to split stake accounts and burn tokens.
        pub stake_authority {
            is_signer: false,
            is_writable: false,
        },
        const spl_token = spl_token::id(),
        const sysvar_clock = sysvar::clock::id(),
        const system_program = system_program::id(),
        const stake_program = stake_program::program::id(),
    }
}

pub fn withdraw(
    program_id: &Pubkey,
    accounts: &WithdrawAccountsMeta,
    amount: StLamports,
) -> Instruction {
    let data = LidoInstruction::Withdraw { amount };
    Instruction {
        program_id: *program_id,
        accounts: accounts.to_vec(),
        data: data.to_vec(),
    }
}

accounts_struct! {
    StakeDepositAccountsMeta, StakeDepositAccountsInfo {
        pub lido {
            is_signer: false,
            is_writable: true,
        },
        pub maintainer {
            is_signer: true,
            is_writable: false,
        },
        pub reserve {
            is_signer: false,
            // Is writable due to transfer (system_instruction::transfer) from reserve_account to
            // stake_account_end
            is_writable: true,
        },
        pub validator_vote_account {
            is_signer: false,
            is_writable: false,
        },
        // For a `StakeDeposit` where we temporarily create an undelegated
        // account at `stake_account_end`, but immediately merge it into
        // `stake_account_merge_into`, this must be set to the program-derived
        // stake account for the validator, with seed `stake_seed.end
        // - 1`. For a `StakeDeposit` where we create a new stake account, this
        // should be set to the same value as `stake_account_end`.
        pub stake_account_merge_into {
            is_signer: false,
            // Is writable due to merge (stake_program::intruction::merge) of stake_account_end
            // into stake_account_merge_into under the condition that they are not equal
            is_writable: true,
        },
        // Must be set to the program-derived stake account for the given
        // validator, with seed `stake_seeds.end`.
        pub stake_account_end {
            is_signer: false,
            // Is writable due to transfer (system_instruction::transfer) from reserve_account to
            // stake_account_end and stake program being initialized
            // (stake_program::instruction::initialize)
            is_writable: true,
        },
        pub stake_authority {
            is_signer: false,
            is_writable: false,
        },
        const sysvar_clock = sysvar::clock::id(),
        const system_program = system_program::id(),
        const sysvar_rent = sysvar::rent::id(),
        const stake_program = stake_program::program::id(),
        const stake_history = stake_history::id(),
        const stake_program_config = stake_program::config::id(),
    }
}

pub fn stake_deposit(
    program_id: &Pubkey,
    accounts: &StakeDepositAccountsMeta,
    amount: Lamports,
) -> Instruction {
    let data = LidoInstruction::StakeDeposit { amount };
    Instruction {
        program_id: *program_id,
        accounts: accounts.to_vec(),
        data: data.to_vec(),
    }
}

accounts_struct! {
    UnstakeAccountsMeta, UnstakeAccountsInfo {
        pub lido {
            is_signer: false,
            is_writable: true,
        },
        pub maintainer {
            is_signer: true,
            is_writable: false,
        },
        pub validator_vote_account {
            is_signer: false,
            is_writable: false,
        },
        // Source stake account is the oldest active stake account that we'll try
        // to unstake from.  Determined by the program-derived stake account for
        // the given validator, with seed `stake_seeds.begin`.
        pub source_stake_account {
            is_signer: false,
            // Is writable due to split (`stake_program::intruction::split`).
            is_writable: true,
        },
        // Destination stake account is the oldest unstake stake account that will
        // receive the split of the funds. Determined by the program-derived
        // stake account for the given validator, with seed `unstake_seeds.end`.
        pub destination_unstake_account {
            is_signer: false,
            // Is writable due to the first two instructions from split.
            is_writable: true,
        },
        // Stake authority, to be able to split the stake.
        pub stake_authority {
            is_signer: false,
            is_writable: false,
        },
        // Required to call `solana_program::stake::instruction::deactivate_stake`.
        const sysvar_clock = sysvar::clock::id(),
        // Required to call cross-program.
        const system_program = system_program::id(),
        // Required to call `stake_program::intruction::split`.
        const stake_program = stake_program::program::id(),
    }
}

pub fn unstake(
    program_id: &Pubkey,
    accounts: &UnstakeAccountsMeta,
    amount: Lamports,
) -> Instruction {
    let data = LidoInstruction::Unstake { amount };
    Instruction {
        program_id: *program_id,
        accounts: accounts.to_vec(),
        data: data.to_vec(),
    }
}

accounts_struct! {
    UpdateExchangeRateAccountsMeta, UpdateExchangeRateAccountsInfo {
        pub lido {
            is_signer: false,
            is_writable: true,
        },
        pub reserve {
            is_signer: false,
            is_writable: false,
        },
        pub st_sol_mint {
            is_signer: false,
            is_writable: false,
        },
        const sysvar_clock = sysvar::clock::id(),
        const sysvar_rent = sysvar::rent::id(),
    }
}

pub fn update_exchange_rate(
    program_id: &Pubkey,
    accounts: &UpdateExchangeRateAccountsMeta,
) -> Instruction {
    Instruction {
        program_id: *program_id,
        accounts: accounts.to_vec(),
        data: LidoInstruction::UpdateExchangeRate.to_vec(),
    }
}

accounts_struct! {
    // Note: there are no signers among these accounts, updating validator
    // balance is permissionless, anybody can do it.
    WithdrawInactiveStakeMeta, WithdrawInactiveStakeInfo {
        pub lido {
            is_signer: false,
            is_writable: true,
        },
        // The validator to update the balance for.
        pub validator_vote_account {
            is_signer: false,
            is_writable: false,
        },

        // This instruction withdraws any excess stake from the stake accounts
        // back to the reserve. The stake authority needs to sign off on those
        // (but program-derived, so it is not a signer here), and we need access
        // to the reserve.
        pub stake_authority {
            is_signer: false,
            is_writable: false,
        },
        pub reserve {
            is_signer: false,
            // Is writable due to withdraw from stake account to reserve (StakeAccount::stake_account_withdraw)
            is_writable: true,
        },

        // We only allow updating balances if the exchange rate is up to date,
        // so we need to know the current epoch.
        const sysvar_clock = sysvar::clock::id(),

        // Needed to determine if there is excess balance in a stake account.
        const sysvar_rent = sysvar::rent::id(),

        // Needed for the stake program, to withdraw from stake accounts.
        const sysvar_stake_history = sysvar::stake_history::id(),

        // Needed to withdraw from stake accounts.
        const stake_program = stake_program::program::id(),

        // The validator's stake accounts, from the begin seed until (but
        // excluding) the end seed.
        pub ...stake_accounts {
            is_signer: false,
            is_writable: true,
        },
    }
}

pub fn withdraw_inactive_stake(
    program_id: &Pubkey,
    accounts: &WithdrawInactiveStakeMeta,
) -> Instruction {
    Instruction {
        program_id: *program_id,
        accounts: accounts.to_vec(),
        data: LidoInstruction::WithdrawInactiveStake.to_vec(),
    }
}

accounts_struct! {
    // Note: there are no signers among these accounts, updating a validator
    // account is permissionless, anybody can do it.
    CollectValidatorFeeMeta, CollectValidatorFeeInfo {
        pub lido {
            is_signer: false,
            is_writable: true,
        },
        // The validator to update the balance for.
        // Needs to be writable so we withdraw from it.
        pub validator_vote_account {
            is_signer: false,
            // Is writable due to withdraw to reserve (vote_instruction::withdraw)
            is_writable: true,
        },

        // Updating balances also immediately mints rewards, so we need the stSOL
        // mint, and the fee accounts to deposit the stSOL into.
        pub st_sol_mint {
            is_signer: false,
            // Is writable due to fee mint (spl_token::instruction::mint_to)
            is_writable: true,
        },

        // Mint authority is required to mint tokens.
        pub mint_authority {
            is_signer: false,
            is_writable: false,
        },

        pub treasury_st_sol_account {
            is_signer: false,
            // Is writable due to fee mint (spl_token::instruction::mint_to) to treasury
            is_writable: true,
        },
        pub developer_st_sol_account {
            is_signer: false,
            // Is writable due to fee mint (spl_token::instruction::mint_to) to developer
            is_writable: true,
        },

        pub reserve {
            is_signer: false,
            // Is writable due to withdraw to reserve (vote_instruction::withdraw)
            is_writable: true,
        },
        // Used to get the rewards out of the validator vote account.
        pub rewards_withdraw_authority {
            is_signer: false,
            is_writable: false,
        },

        // We only allow updating balances if the exchange rate is up to date,
        // so we need to know the current epoch.
        const sysvar_clock = sysvar::clock::id(),

        // Needed for minting rewards.
        const spl_token_program = spl_token::id(),

        // Needed to calculate the validator's vote account rent exempt, so it
        // can subtracted from the rewards.
        const sysvar_rent = sysvar::rent::id(),

        // Needed to withdraw from the vote account.
        const vote_program = vote::program::id(),
    }
}

pub fn collect_validator_fee(
    program_id: &Pubkey,
    accounts: &CollectValidatorFeeMeta,
) -> Instruction {
    Instruction {
        program_id: *program_id,
        accounts: accounts.to_vec(),
        data: LidoInstruction::CollectValidatorFee.to_vec(),
    }
}

// Changes the Fee spec
// The new Fee structure is passed by argument and the recipients are passed here
accounts_struct! {
    ChangeRewardDistributionMeta, ChangeRewardDistributionInfo {
        pub lido {
            is_signer: false,
            is_writable: true,
        },
        pub manager {
            is_signer: true,
            is_writable: false,
        },
        pub treasury_account {
            is_signer: false,
            is_writable: false,
        },
        pub developer_account {
            is_signer: false,
            is_writable: false,
        },
    }
}

pub fn change_reward_distribution(
    program_id: &Pubkey,
    new_reward_distribution: RewardDistribution,
    accounts: &ChangeRewardDistributionMeta,
) -> Instruction {
    Instruction {
        program_id: *program_id,
        accounts: accounts.to_vec(),
        data: LidoInstruction::ChangeRewardDistribution {
            new_reward_distribution,
        }
        // Serializing the instruction should never fail.
        .try_to_vec()
        .unwrap(),
    }
}

accounts_struct! {
    AddValidatorMeta, AddValidatorInfo {
        pub lido {
            is_signer: false,
            is_writable: true,
        },
        pub manager {
            is_signer: true,
            is_writable: false,
        },
        pub validator_vote_account {
            is_signer: false,
            is_writable: false,
        },
        pub validator_fee_st_sol_account {
            is_signer: false,
            is_writable: false,
        },
        const sysvar_rent = sysvar::rent::id(),
    }
}

pub fn add_validator(program_id: &Pubkey, accounts: &AddValidatorMeta) -> Instruction {
    Instruction {
        program_id: *program_id,
        accounts: accounts.to_vec(),
        data: LidoInstruction::AddValidator.to_vec(),
    }
}

accounts_struct! {
    RemoveValidatorMeta, RemoveValidatorInfo {
        pub lido {
            is_signer: false,
            is_writable: true,
        },
        pub validator_vote_account_to_remove {
            is_signer: false,
            is_writable: false,
        },
    }
}

pub fn remove_validator(program_id: &Pubkey, accounts: &RemoveValidatorMeta) -> Instruction {
    Instruction {
        program_id: *program_id,
        accounts: accounts.to_vec(),
        data: LidoInstruction::RemoveValidator.to_vec(),
    }
}

accounts_struct! {
    DeactivateValidatorMeta, DeactivateValidatorInfo {
        pub lido {
            is_signer: false,
            is_writable: true,
        },
        pub manager {
            is_signer: true,
            is_writable: false,
        },
        pub validator_vote_account_to_deactivate {
            is_signer: false,
            is_writable: false,
        },
    }
}

pub fn deactivate_validator(
    program_id: &Pubkey,
    accounts: &DeactivateValidatorMeta,
) -> Instruction {
    Instruction {
        program_id: *program_id,
        accounts: accounts.to_vec(),
        data: LidoInstruction::DeactivateValidator.to_vec(),
    }
}

accounts_struct! {
    ClaimValidatorFeeMeta, ClaimValidatorFeeInfo {
        pub lido {
            is_signer: false,
            is_writable: true,
        },
        pub st_sol_mint {
            is_signer: false,
            // Is writable due to fee mint (spl_token::instruction::mint_to) to validator fee
            // st_sol account
            is_writable: true,
        },
        pub mint_authority {
            is_signer: false,
            is_writable: false,
        },
        pub validator_fee_st_sol_account {
            is_signer: false,
            // Is writable due to fee mint (spl_token::instruction::mint_to) to validator fee
            // st_sol account
            is_writable: true,
        },
        const spl_token = spl_token::id(),
    }
}

pub fn claim_validator_fee(program_id: &Pubkey, accounts: &ClaimValidatorFeeMeta) -> Instruction {
    Instruction {
        program_id: *program_id,
        accounts: accounts.to_vec(),
        data: LidoInstruction::ClaimValidatorFee.to_vec(),
    }
}

accounts_struct! {
    AddMaintainerMeta, AddMaintainerInfo {
        pub lido {
            is_signer: false,
            is_writable: true,
        },
        pub manager {
            is_signer: true,
            is_writable: false,
        },
        pub maintainer {
            is_signer: false,
            is_writable: false,
        },
    }
}

pub fn add_maintainer(program_id: &Pubkey, accounts: &AddMaintainerMeta) -> Instruction {
    Instruction {
        program_id: *program_id,
        accounts: accounts.to_vec(),
        data: LidoInstruction::AddMaintainer.to_vec(),
    }
}

accounts_struct! {
    RemoveMaintainerMeta, RemoveMaintainerInfo {
        pub lido {
            is_signer: false,
            is_writable: true,
        },
        pub manager {
            is_signer: true,
            is_writable: false,
        },
        pub maintainer {
            is_signer: false,
            is_writable: false,
        },
    }
}

pub fn remove_maintainer(program_id: &Pubkey, accounts: &RemoveMaintainerMeta) -> Instruction {
    Instruction {
        program_id: *program_id,
        accounts: accounts.to_vec(),
        data: LidoInstruction::RemoveMaintainer.to_vec(),
    }
}

accounts_struct! {
    MergeStakeMeta, MergeStakeInfo {
        pub lido {
            is_signer: false,
            is_writable: true,
        },
        pub validator_vote_account {
            is_signer: false,
            is_writable: false,
        },
        pub from_stake {
            is_signer: false,
            // Is writable due to merge (solana_program::stake::instruction::merge)
            is_writable: true,
        },
        pub to_stake {
            is_signer: false,
            // Is writable due to merge (solana_program::stake::instruction::merge)
            is_writable: true,
        },
        // This instruction doesn’t reference the authority directly, but it
        // invokes one a `MergeStake` instruction that needs the deposit
        // authority to sign.
        pub stake_authority {
            is_signer: false,
            is_writable: false,
        },
        const sysvar_clock = sysvar::clock::id(),
        const stake_history = stake_history::id(),
        const stake_program = stake_program::program::id(),
    }
}

pub fn merge_stake(program_id: &Pubkey, accounts: &MergeStakeMeta) -> Instruction {
    Instruction {
        program_id: *program_id,
        accounts: accounts.to_vec(),
        data: LidoInstruction::MergeStake.try_to_vec().unwrap(), // This should never fail.
    }
}