use std::ops::Range;
use serde::Serialize;
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
use solana_program::borsh::get_instance_packed_len;
use solana_program::clock::Clock;
use solana_program::{
account_info::AccountInfo, clock::Epoch, entrypoint::ProgramResult, msg,
program_error::ProgramError, program_pack::Pack, pubkey::Pubkey, rent::Rent, sysvar::Sysvar,
};
use spl_token::state::Mint;
use crate::error::LidoError;
use crate::logic::get_reserve_available_balance;
use crate::metrics::Metrics;
use crate::processor::StakeType;
use crate::token::{self, Lamports, Rational, StLamports};
use crate::util::serialize_b58;
use crate::{
account_map::{AccountMap, AccountSet, EntryConstantSize, PubkeyAndEntry},
MINIMUM_STAKE_ACCOUNT_BALANCE, MINT_AUTHORITY, RESERVE_ACCOUNT, STAKE_AUTHORITY,
};
use crate::{REWARDS_WITHDRAW_AUTHORITY, VALIDATOR_STAKE_ACCOUNT, VALIDATOR_UNSTAKE_ACCOUNT};
pub const LIDO_VERSION: u8 = 0;
pub const LIDO_CONSTANT_SIZE: usize = 357;
pub const VALIDATOR_CONSTANT_SIZE: usize = 89;
pub type Validators = AccountMap<Validator>;
impl Validators {
pub fn iter_active(&self) -> impl Iterator<Item = &Validator> {
self.iter_entries().filter(|&v| v.active)
}
pub fn iter_active_entries(&self) -> impl Iterator<Item = &PubkeyAndEntry<Validator>> {
self.entries.iter().filter(|&v| v.entry.active)
}
}
pub type Maintainers = AccountSet;
impl EntryConstantSize for Validator {
const SIZE: usize = VALIDATOR_CONSTANT_SIZE;
}
impl EntryConstantSize for () {
const SIZE: usize = 0;
}
#[repr(C)]
#[derive(
Clone, Debug, Default, BorshDeserialize, BorshSerialize, BorshSchema, Eq, PartialEq, Serialize,
)]
pub struct ExchangeRate {
pub computed_in_epoch: Epoch,
pub st_sol_supply: StLamports,
pub sol_balance: Lamports,
}
impl ExchangeRate {
pub fn exchange_sol(&self, amount: Lamports) -> token::Result<StLamports> {
if self.st_sol_supply == StLamports(0) || self.sol_balance == Lamports(0) {
return Ok(StLamports(amount.0));
}
let rate = Rational {
numerator: self.st_sol_supply.0,
denominator: self.sol_balance.0,
};
(amount * rate).map(|x| StLamports(x.0))
}
pub fn exchange_st_sol(&self, amount: StLamports) -> Result<Lamports, LidoError> {
if self.st_sol_supply == StLamports(0) {
msg!("Cannot exchange stSOL for SOL, because no stSTOL has been minted.");
return Err(LidoError::InvalidAmount);
}
let rate = Rational {
numerator: self.sol_balance.0,
denominator: self.st_sol_supply.0,
};
Ok((amount * rate).map(|x| Lamports(x.0))?)
}
}
#[repr(C)]
#[derive(
Clone, Debug, Default, BorshDeserialize, BorshSerialize, BorshSchema, Eq, PartialEq, Serialize,
)]
pub struct Lido {
pub lido_version: u8,
#[serde(serialize_with = "serialize_b58")]
pub manager: Pubkey,
#[serde(serialize_with = "serialize_b58")]
pub st_sol_mint: Pubkey,
pub exchange_rate: ExchangeRate,
pub sol_reserve_account_bump_seed: u8,
pub stake_authority_bump_seed: u8,
pub mint_authority_bump_seed: u8,
pub rewards_withdraw_authority_bump_seed: u8,
pub reward_distribution: RewardDistribution,
pub fee_recipients: FeeRecipients,
pub metrics: Metrics,
pub validators: Validators,
pub maintainers: Maintainers,
}
impl Lido {
pub fn calculate_size(max_validators: u32, max_maintainers: u32) -> usize {
let lido_instance = Lido {
validators: Validators::new_fill_default(max_validators),
maintainers: Maintainers::new_fill_default(max_maintainers),
..Default::default()
};
get_instance_packed_len(&lido_instance).unwrap()
}
pub fn check_mint_is_st_sol_mint(&self, mint_account_info: &AccountInfo) -> ProgramResult {
if &self.st_sol_mint != mint_account_info.key {
msg!(
"Expected to find our stSOL mint ({}), but got {} instead.",
self.st_sol_mint,
mint_account_info.key
);
return Err(LidoError::InvalidStSolAccount.into());
}
Ok(())
}
pub fn check_is_st_sol_account(&self, token_account_info: &AccountInfo) -> ProgramResult {
if token_account_info.owner != &spl_token::id() {
msg!(
"Expected SPL token account to be owned by {}, but it's owned by {} instead.",
spl_token::id(),
token_account_info.owner
);
return Err(LidoError::InvalidStSolAccountOwner.into());
}
let token_account =
match spl_token::state::Account::unpack_from_slice(&token_account_info.data.borrow()) {
Ok(account) => account,
Err(..) => {
msg!(
"Expected an SPL token account at {}.",
token_account_info.key
);
return Err(LidoError::InvalidStSolAccount.into());
}
};
if token_account.mint != self.st_sol_mint {
msg!(
"Expected mint of {} to be our stSOL mint ({}), but found {}.",
token_account_info.key,
self.st_sol_mint,
token_account.mint,
);
return Err(LidoError::InvalidFeeRecipient.into());
}
Ok(())
}
pub fn check_manager(&self, manager: &AccountInfo) -> ProgramResult {
if &self.manager != manager.key {
msg!("Invalid manager, not the same as the one stored in state");
return Err(LidoError::InvalidManager.into());
}
Ok(())
}
pub fn check_maintainer(&self, maintainer: &AccountInfo) -> ProgramResult {
if !&self.maintainers.entries.contains(&PubkeyAndEntry {
pubkey: *maintainer.key,
entry: (),
}) {
msg!(
"Invalid maintainer, account {} is not present in the maintainers list.",
maintainer.key
);
return Err(LidoError::InvalidMaintainer.into());
}
Ok(())
}
pub fn check_treasury_fee_st_sol_account(&self, st_sol_account: &AccountInfo) -> ProgramResult {
if &self.fee_recipients.treasury_account != st_sol_account.key {
msg!("Invalid treasury fee stSOL account, not the same as the one stored in state.");
return Err(LidoError::InvalidFeeRecipient.into());
}
self.check_is_st_sol_account(st_sol_account)
}
pub fn check_developer_fee_st_sol_account(
&self,
st_sol_account: &AccountInfo,
) -> ProgramResult {
if &self.fee_recipients.developer_account != st_sol_account.key {
msg!("Invalid developer fee stSOL account, not the same as the one stored in state.");
return Err(LidoError::InvalidFeeRecipient.into());
}
self.check_is_st_sol_account(st_sol_account)
}
pub fn get_reserve_account(
&self,
program_id: &Pubkey,
solido_address: &Pubkey,
) -> Result<Pubkey, ProgramError> {
Pubkey::create_program_address(
&[
&solido_address.to_bytes()[..],
RESERVE_ACCOUNT,
&[self.sol_reserve_account_bump_seed],
],
program_id,
)
.map_err(|_| LidoError::InvalidReserveAccount.into())
}
pub fn check_reserve_account(
&self,
program_id: &Pubkey,
solido_address: &Pubkey,
reserve_account_info: &AccountInfo,
) -> Result<Pubkey, ProgramError> {
let reserve_id = self.get_reserve_account(program_id, solido_address)?;
if reserve_id != *reserve_account_info.key {
msg!("Invalid reserve account");
return Err(LidoError::InvalidReserveAccount.into());
}
Ok(reserve_id)
}
pub fn get_stake_authority(
&self,
program_id: &Pubkey,
solido_address: &Pubkey,
) -> Result<Pubkey, ProgramError> {
Pubkey::create_program_address(
&[
&solido_address.to_bytes()[..],
STAKE_AUTHORITY,
&[self.stake_authority_bump_seed],
],
program_id,
)
.map_err(|_| ProgramError::InvalidSeeds)
}
pub fn check_stake_authority(
&self,
program_id: &Pubkey,
solido_address: &Pubkey,
stake_authority_account_info: &AccountInfo,
) -> Result<Pubkey, ProgramError> {
let authority = self.get_stake_authority(program_id, solido_address)?;
if &authority != stake_authority_account_info.key {
msg!(
"Invalid stake authority, expected {} but got {}.",
authority,
stake_authority_account_info.key
);
return Err(LidoError::InvalidStakeAuthority.into());
}
Ok(authority)
}
pub fn get_rewards_withdraw_authority(
&self,
program_id: &Pubkey,
solido_address: &Pubkey,
) -> Result<Pubkey, ProgramError> {
Pubkey::create_program_address(
&[
&solido_address.to_bytes()[..],
REWARDS_WITHDRAW_AUTHORITY,
&[self.rewards_withdraw_authority_bump_seed],
],
program_id,
)
.map_err(|_| ProgramError::InvalidSeeds)
}
pub fn check_rewards_withdraw_authority(
&self,
program_id: &Pubkey,
solido_address: &Pubkey,
rewards_withdraw_authority_account_info: &AccountInfo,
) -> Result<Pubkey, ProgramError> {
let authority = self.get_rewards_withdraw_authority(program_id, solido_address)?;
if &authority != rewards_withdraw_authority_account_info.key {
msg!(
"Invalid rewards withdraw authority, expected {} but got {}.",
authority,
rewards_withdraw_authority_account_info.key
);
return Err(LidoError::InvalidRewardsWithdrawAuthority.into());
}
Ok(authority)
}
pub fn get_mint_authority(
&self,
program_id: &Pubkey,
solido_address: &Pubkey,
) -> Result<Pubkey, ProgramError> {
Pubkey::create_program_address(
&[
&solido_address.to_bytes()[..],
MINT_AUTHORITY,
&[self.mint_authority_bump_seed],
],
program_id,
)
.map_err(|_| ProgramError::InvalidSeeds)
}
pub fn check_can_stake_amount(
&self,
reserve: &AccountInfo,
sysvar_rent: &AccountInfo,
amount: Lamports,
) -> Result<(), ProgramError> {
if amount < MINIMUM_STAKE_ACCOUNT_BALANCE {
msg!("Trying to stake less than the minimum stake account balance.");
msg!(
"Need as least {} but got {}.",
MINIMUM_STAKE_ACCOUNT_BALANCE,
amount
);
return Err(LidoError::InvalidAmount.into());
}
let rent: Rent = Rent::from_account_info(sysvar_rent)?;
let available_reserve_amount = get_reserve_available_balance(&rent, reserve)?;
if amount > available_reserve_amount {
msg!(
"The requested amount {} is greater than the available amount {}, \
considering rent-exemption",
amount,
available_reserve_amount
);
return Err(LidoError::AmountExceedsReserve.into());
}
Ok(())
}
pub fn check_stake_account(
program_id: &Pubkey,
solido_address: &Pubkey,
validator: &PubkeyAndEntry<Validator>,
stake_account_seed: u64,
stake_account: &AccountInfo,
authority: &[u8],
) -> Result<u8, ProgramError> {
let (stake_addr, stake_addr_bump_seed) = validator
.find_stake_account_address_with_authority(
program_id,
solido_address,
authority,
stake_account_seed,
);
if &stake_addr != stake_account.key {
msg!(
"The derived stake address for seed {} is {}, \
but the instruction received {} instead.",
stake_account_seed,
stake_addr,
stake_account.key,
);
msg!(
"Note: this can happen during normal operation when instructions \
race, and one updates the validator's seeds before the other executes."
);
return Err(LidoError::InvalidStakeAccount.into());
}
Ok(stake_addr_bump_seed)
}
pub fn save(&self, account: &AccountInfo) -> ProgramResult {
BorshSerialize::serialize(self, &mut *account.data.borrow_mut())?;
Ok(())
}
pub fn get_sol_balance(
&self,
rent: &Rent,
reserve: &AccountInfo,
) -> Result<Lamports, LidoError> {
let effective_reserve_balance = get_reserve_available_balance(rent, reserve)?;
let validator_balance: token::Result<Lamports> = self
.validators
.iter_entries()
.map(|v| v.stake_accounts_balance)
.sum();
let result = validator_balance.and_then(|s| s + effective_reserve_balance)?;
Ok(result)
}
pub fn get_st_sol_supply(&self, st_sol_mint: &AccountInfo) -> Result<StLamports, ProgramError> {
self.check_mint_is_st_sol_mint(st_sol_mint)?;
let st_sol_mint = Mint::unpack_from_slice(&st_sol_mint.data.borrow())?;
let minted_supply = StLamports(st_sol_mint.supply);
let credit: token::Result<StLamports> =
self.validators.iter_entries().map(|v| v.fee_credit).sum();
let result = credit.and_then(|s| s + minted_supply)?;
Ok(result)
}
pub fn check_exchange_rate_last_epoch(
&self,
clock: &Clock,
method: &str,
) -> Result<(), LidoError> {
if self.exchange_rate.computed_in_epoch < clock.epoch {
msg!(
"The exchange rate is outdated, it was last computed in epoch {}, \
but now it is epoch {}.",
self.exchange_rate.computed_in_epoch,
clock.epoch,
);
msg!("Please call UpdateExchangeRate before calling {}.", method);
return Err(LidoError::ExchangeRateNotUpdatedInThisEpoch);
}
Ok(())
}
}
#[repr(C)]
#[derive(Clone, Debug, Eq, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema, Serialize)]
pub struct Validator {
pub fee_credit: StLamports,
#[serde(serialize_with = "serialize_b58")]
pub fee_address: Pubkey,
pub stake_seeds: SeedRange,
pub unstake_seeds: SeedRange,
pub stake_accounts_balance: Lamports,
pub unstake_accounts_balance: Lamports,
pub active: bool,
}
#[repr(C)]
#[derive(
Clone, Debug, Default, Eq, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema, Serialize,
)]
pub struct SeedRange {
pub begin: u64,
pub end: u64,
}
impl IntoIterator for &SeedRange {
type Item = u64;
type IntoIter = Range<u64>;
fn into_iter(self) -> Self::IntoIter {
Range {
start: self.begin,
end: self.end,
}
}
}
impl Validator {
pub fn new(fee_address: Pubkey) -> Validator {
Validator {
fee_address,
..Default::default()
}
}
pub fn effective_stake_balance(&self) -> Lamports {
(self.stake_accounts_balance - self.unstake_accounts_balance)
.expect("Unstake balance cannot exceed the validator's total stake balance.")
}
}
impl Default for Validator {
fn default() -> Self {
Validator {
fee_address: Pubkey::default(),
fee_credit: StLamports(0),
stake_seeds: SeedRange { begin: 0, end: 0 },
unstake_seeds: SeedRange { begin: 0, end: 0 },
stake_accounts_balance: Lamports(0),
unstake_accounts_balance: Lamports(0),
active: true,
}
}
}
impl Validator {
pub fn has_stake_accounts(&self) -> bool {
self.stake_seeds.begin != self.stake_seeds.end
}
pub fn has_unstake_accounts(&self) -> bool {
self.unstake_seeds.begin != self.unstake_seeds.end
}
pub fn check_can_be_removed(&self) -> Result<(), LidoError> {
if self.active {
return Err(LidoError::ValidatorIsStillActive);
}
if self.fee_credit != StLamports(0) {
return Err(LidoError::ValidatorHasUnclaimedCredit);
}
if self.has_stake_accounts() {
return Err(LidoError::ValidatorShouldHaveNoStakeAccounts);
}
if self.has_unstake_accounts() {
return Err(LidoError::ValidatorShouldHaveNoUnstakeAccounts);
}
assert_eq!(self.stake_accounts_balance, Lamports(0));
Ok(())
}
pub fn show_removed_error_msg(error: &Result<(), LidoError>) {
if let Err(err) = error {
match err {
LidoError::ValidatorIsStillActive => {
msg!(
"Refusing to remove validator because it is still active, deactivate it first."
);
}
LidoError::ValidatorHasUnclaimedCredit => {
msg!(
"Validator still has tokens to claim. Reclaim tokens before removing the validator"
);
}
LidoError::ValidatorShouldHaveNoStakeAccounts => {
msg!("Refusing to remove validator because it still has stake accounts, unstake them first.");
}
LidoError::ValidatorShouldHaveNoUnstakeAccounts => {
msg!("Refusing to remove validator because it still has unstake accounts, withdraw them first.");
}
_ => {
msg!("Invalid error when removing a validator: shouldn't happen.");
}
}
}
}
}
impl PubkeyAndEntry<Validator> {
pub fn find_stake_account_address_with_authority(
&self,
program_id: &Pubkey,
solido_account: &Pubkey,
authority: &[u8],
seed: u64,
) -> (Pubkey, u8) {
let seeds = [
&solido_account.to_bytes(),
&self.pubkey.to_bytes(),
authority,
&seed.to_le_bytes()[..],
];
Pubkey::find_program_address(&seeds, program_id)
}
pub fn find_stake_account_address(
&self,
program_id: &Pubkey,
solido_account: &Pubkey,
seed: u64,
stake_type: StakeType,
) -> (Pubkey, u8) {
let authority = match stake_type {
StakeType::Stake => VALIDATOR_STAKE_ACCOUNT,
StakeType::Unstake => VALIDATOR_UNSTAKE_ACCOUNT,
};
self.find_stake_account_address_with_authority(program_id, solido_account, authority, seed)
}
}
#[derive(
Clone, Default, Debug, Eq, PartialEq, BorshSerialize, BorshDeserialize, BorshSchema, Serialize,
)]
pub struct RewardDistribution {
pub treasury_fee: u32,
pub validation_fee: u32,
pub developer_fee: u32,
pub st_sol_appreciation: u32,
}
#[derive(
Clone, Default, Debug, Eq, PartialEq, BorshSerialize, BorshDeserialize, BorshSchema, Serialize,
)]
pub struct FeeRecipients {
#[serde(serialize_with = "serialize_b58")]
pub treasury_account: Pubkey,
#[serde(serialize_with = "serialize_b58")]
pub developer_account: Pubkey,
}
impl RewardDistribution {
pub fn sum(&self) -> u64 {
self.treasury_fee as u64
+ self.validation_fee as u64
+ self.developer_fee as u64
+ self.st_sol_appreciation as u64
}
pub fn treasury_fraction(&self) -> Rational {
Rational {
numerator: self.treasury_fee as u64,
denominator: self.sum(),
}
}
pub fn validation_fraction(&self) -> Rational {
Rational {
numerator: self.validation_fee as u64,
denominator: self.sum(),
}
}
pub fn developer_fraction(&self) -> Rational {
Rational {
numerator: self.developer_fee as u64,
denominator: self.sum(),
}
}
pub fn split_reward(&self, amount: Lamports, num_validators: u64) -> token::Result<Fees> {
use std::ops::Add;
let treasury_amount = (amount * self.treasury_fraction())?;
let developer_amount = (amount * self.developer_fraction())?;
let validation_amount = (amount * self.validation_fraction())?;
let reward_per_validator = (validation_amount / num_validators)?;
let total_fees = Lamports(0)
.add(treasury_amount)?
.add(developer_amount)?
.add((reward_per_validator * num_validators)?)?;
assert!(total_fees <= amount);
let st_sol_appreciation_amount = (amount - total_fees)?;
let result = Fees {
treasury_amount,
reward_per_validator,
developer_amount,
st_sol_appreciation_amount,
};
Ok(result)
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct Fees {
pub treasury_amount: Lamports,
pub reward_per_validator: Lamports,
pub developer_amount: Lamports,
pub st_sol_appreciation_amount: Lamports,
}
#[cfg(test)]
mod test_lido {
use super::*;
use solana_program::program_error::ProgramError;
#[test]
fn test_account_map_required_bytes_relates_to_maximum_entries() {
for buffer_size in 0..8_000 {
let max_entries = Validators::maximum_entries(buffer_size);
let needed_size = Validators::required_bytes(max_entries);
assert!(
needed_size <= buffer_size || max_entries == 0,
"Buffer of len {} can fit {} validators which need {} bytes.",
buffer_size,
max_entries,
needed_size,
);
let max_entries = Maintainers::maximum_entries(buffer_size);
let needed_size = Maintainers::required_bytes(max_entries);
assert!(
needed_size <= buffer_size || max_entries == 0,
"Buffer of len {} can fit {} maintainers which need {} bytes.",
buffer_size,
max_entries,
needed_size,
);
}
}
#[test]
fn test_validators_size() {
let validator = get_instance_packed_len(&Validator::default()).unwrap();
assert_eq!(validator, Validator::SIZE);
let one_len = get_instance_packed_len(&Validators::new_fill_default(1)).unwrap();
let two_len = get_instance_packed_len(&Validators::new_fill_default(2)).unwrap();
assert_eq!(one_len, Validators::required_bytes(1));
assert_eq!(two_len, Validators::required_bytes(2));
assert_eq!(
two_len - one_len,
std::mem::size_of::<Pubkey>() + Validator::SIZE
);
}
#[test]
fn test_lido_constant_size() {
let minimal = Lido::default();
let mut data = Vec::new();
BorshSerialize::serialize(&minimal, &mut data).unwrap();
let num_entries = 0;
let size_validators = Validators::required_bytes(num_entries);
let size_maintainers = Maintainers::required_bytes(num_entries);
assert_eq!(
data.len() - size_validators - size_maintainers,
LIDO_CONSTANT_SIZE
);
}
#[test]
fn test_lido_serialization_roundtrips() {
use solana_sdk::borsh::try_from_slice_unchecked;
let mut validators = Validators::new(10_000);
validators
.add(Pubkey::new_unique(), Validator::new(Pubkey::new_unique()))
.unwrap();
let maintainers = Maintainers::new(1);
let lido = Lido {
lido_version: 0,
manager: Pubkey::new_unique(),
st_sol_mint: Pubkey::new_unique(),
exchange_rate: ExchangeRate {
computed_in_epoch: 11,
sol_balance: Lamports(13),
st_sol_supply: StLamports(17),
},
sol_reserve_account_bump_seed: 1,
stake_authority_bump_seed: 2,
mint_authority_bump_seed: 3,
rewards_withdraw_authority_bump_seed: 4,
reward_distribution: RewardDistribution {
treasury_fee: 2,
validation_fee: 3,
developer_fee: 4,
st_sol_appreciation: 7,
},
fee_recipients: FeeRecipients {
treasury_account: Pubkey::new_unique(),
developer_account: Pubkey::new_unique(),
},
metrics: Metrics::new(),
validators: validators,
maintainers: maintainers,
};
let mut data = Vec::new();
BorshSerialize::serialize(&lido, &mut data).unwrap();
let lido_restored = try_from_slice_unchecked(&data[..]).unwrap();
assert_eq!(lido, lido_restored);
}
#[test]
fn test_exchange_when_balance_and_supply_are_zero() {
let rate = ExchangeRate {
computed_in_epoch: 0,
sol_balance: Lamports(0),
st_sol_supply: StLamports(0),
};
assert_eq!(rate.exchange_sol(Lamports(123)), Ok(StLamports(123)));
}
#[test]
fn test_exchange_when_rate_is_one_to_two() {
let rate = ExchangeRate {
computed_in_epoch: 0,
sol_balance: Lamports(2),
st_sol_supply: StLamports(1),
};
assert_eq!(rate.exchange_sol(Lamports(44)), Ok(StLamports(22)));
}
#[test]
fn test_exchange_when_one_balance_is_zero() {
let rate = ExchangeRate {
computed_in_epoch: 0,
sol_balance: Lamports(100),
st_sol_supply: StLamports(0),
};
assert_eq!(rate.exchange_sol(Lamports(123)), Ok(StLamports(123)));
let rate = ExchangeRate {
computed_in_epoch: 0,
sol_balance: Lamports(0),
st_sol_supply: StLamports(100),
};
assert_eq!(rate.exchange_sol(Lamports(123)), Ok(StLamports(123)));
}
#[test]
fn test_exchange_sol_to_st_sol_to_sol_roundtrips() {
let rate = ExchangeRate {
computed_in_epoch: 0,
sol_balance: Lamports(100),
st_sol_supply: StLamports(50),
};
let sol_1 = Lamports(10);
let st_sol = rate.exchange_sol(sol_1).unwrap();
let sol_2 = rate.exchange_st_sol(st_sol).unwrap();
assert_eq!(sol_2, sol_1);
let rate = ExchangeRate {
computed_in_epoch: 0,
sol_balance: Lamports(110_000),
st_sol_supply: StLamports(100_000),
};
let sol_1 = Lamports(1_000);
let st_sol = rate.exchange_sol(sol_1).unwrap();
let sol_2 = rate.exchange_st_sol(st_sol).unwrap();
assert_eq!(sol_2, Lamports(999));
}
#[test]
fn test_lido_for_deposit_wrong_mint() {
let mut lido = Lido::default();
lido.st_sol_mint = Pubkey::new_unique();
let pubkey = Pubkey::new_unique();
let mut lamports = 100;
let mut data = [0_u8];
let is_signer = false;
let is_writable = false;
let owner = spl_token::id();
let executable = false;
let rent_epoch = 1;
let fake_mint_account = AccountInfo::new(
&pubkey,
is_signer,
is_writable,
&mut lamports,
&mut data,
&owner,
executable,
rent_epoch,
);
let result = lido.check_mint_is_st_sol_mint(&fake_mint_account);
let expected_error: ProgramError = LidoError::InvalidStSolAccount.into();
assert_eq!(result, Err(expected_error));
}
#[test]
fn test_get_sol_balance() {
use std::cell::RefCell;
use std::rc::Rc;
let rent = &Rent::default();
let mut lido = Lido::default();
let key = Pubkey::default();
let mut amount = rent.minimum_balance(0);
let mut reserve_account =
AccountInfo::new(&key, true, true, &mut amount, &mut [], &key, false, 0);
assert_eq!(
lido.get_sol_balance(&rent, &reserve_account),
Ok(Lamports(0))
);
let mut new_amount = rent.minimum_balance(0) + 10;
reserve_account.lamports = Rc::new(RefCell::new(&mut new_amount));
assert_eq!(
lido.get_sol_balance(&rent, &reserve_account),
Ok(Lamports(10))
);
lido.validators.maximum_entries = 1;
lido.validators
.add(Pubkey::new_unique(), Validator::new(Pubkey::new_unique()))
.unwrap();
lido.validators.entries[0].entry.stake_accounts_balance = Lamports(37);
assert_eq!(
lido.get_sol_balance(&rent, &reserve_account),
Ok(Lamports(10 + 37))
);
lido.validators.entries[0].entry.stake_accounts_balance = Lamports(u64::MAX);
assert_eq!(
lido.get_sol_balance(&rent, &reserve_account),
Err(LidoError::CalculationFailure)
);
let mut new_amount = u64::MAX;
reserve_account.lamports = Rc::new(RefCell::new(&mut new_amount));
lido.validators.entries[0].entry.stake_accounts_balance = Lamports(5_000_000);
assert_eq!(
lido.get_sol_balance(&rent, &reserve_account),
Err(LidoError::CalculationFailure)
);
}
#[test]
fn test_get_st_sol_supply() {
use solana_program::program_option::COption;
let mint = Mint {
mint_authority: COption::None,
supply: 200_000,
decimals: 9,
is_initialized: true,
freeze_authority: COption::None,
};
let mut data = [0_u8; 128];
mint.pack_into_slice(&mut data);
let mut lido = Lido::default();
let mint_address = Pubkey::default();
let mut amount = 0;
let is_signer = false;
let is_writable = false;
let executable = false;
let rent_epoch = 0;
let st_sol_mint = AccountInfo::new(
&mint_address,
is_signer,
is_writable,
&mut amount,
&mut data,
&mint_address,
executable,
rent_epoch,
);
lido.st_sol_mint = mint_address;
assert_eq!(
lido.get_st_sol_supply(&st_sol_mint),
Ok(StLamports(200_000)),
);
lido.validators.maximum_entries = 1;
lido.validators
.add(Pubkey::new_unique(), Validator::new(Pubkey::new_unique()))
.unwrap();
lido.validators.entries[0].entry.fee_credit = StLamports(37);
assert_eq!(
lido.get_st_sol_supply(&st_sol_mint),
Ok(StLamports(200_000 + 37))
);
lido.st_sol_mint = Pubkey::new_unique();
assert_eq!(
lido.get_st_sol_supply(&st_sol_mint),
Err(LidoError::InvalidStSolAccount.into())
);
}
#[test]
fn test_split_reward() {
let mut spec = RewardDistribution {
treasury_fee: 3,
validation_fee: 2,
developer_fee: 1,
st_sol_appreciation: 0,
};
assert_eq!(
spec.split_reward(Lamports(600), 1).unwrap(),
Fees {
treasury_amount: Lamports(300),
reward_per_validator: Lamports(200),
developer_amount: Lamports(100),
st_sol_appreciation_amount: Lamports(0),
},
);
assert_eq!(
spec.split_reward(Lamports(1_000), 4).unwrap(),
Fees {
treasury_amount: Lamports(500),
reward_per_validator: Lamports(83),
developer_amount: Lamports(166),
st_sol_appreciation_amount: Lamports(2),
},
);
spec.st_sol_appreciation = 94;
assert_eq!(
spec.split_reward(Lamports(100), 1).unwrap(),
Fees {
treasury_amount: Lamports(3),
reward_per_validator: Lamports(2),
developer_amount: Lamports(1),
st_sol_appreciation_amount: Lamports(94),
},
);
let spec_coprime = RewardDistribution {
treasury_fee: 17,
validation_fee: 23,
developer_fee: 19,
st_sol_appreciation: 0,
};
assert_eq!(
spec_coprime.split_reward(Lamports(1_000), 1).unwrap(),
Fees {
treasury_amount: Lamports(288),
reward_per_validator: Lamports(389),
developer_amount: Lamports(322),
st_sol_appreciation_amount: Lamports(1),
},
);
}
#[test]
fn test_n_val() {
let n_validators: u64 = 10_000;
let size =
get_instance_packed_len(&Validators::new_fill_default(n_validators as u32)).unwrap();
assert_eq!(Validators::maximum_entries(size) as u64, n_validators);
}
#[test]
fn test_version_serialise() {
use solana_sdk::borsh::try_from_slice_unchecked;
for i in 0..=255 {
let lido = Lido {
lido_version: i,
..Lido::default()
};
let mut res: Vec<u8> = Vec::new();
BorshSerialize::serialize(&lido, &mut res).unwrap();
assert_eq!(res[0], i);
let lido_recovered = try_from_slice_unchecked(&res[..]).unwrap();
assert_eq!(lido, lido_recovered);
}
}
#[test]
fn test_check_is_st_sol_account_fails_with_different_owner() {
let lido = Lido::default();
let key = Pubkey::new_unique();
let mut lamports = 0;
let mut data = [];
let owner = Pubkey::new_unique();
let token_account = &AccountInfo::new(
&key,
false,
true,
&mut lamports,
&mut data,
&owner,
false,
0,
);
let result = lido.check_is_st_sol_account(token_account);
match result {
Err(ProgramError::Custom(err_code)) => {
assert_eq!(err_code, LidoError::InvalidStSolAccountOwner as u32)
}
_ => panic!("Should be the InvalidStSolAccountOwner error"),
}
assert!(result.is_err());
}
}