Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fee implementation passing all tests #94

Merged
merged 4 commits into from
May 21, 2021

Conversation

enriquefynn
Copy link
Member

@enriquefynn enriquefynn commented May 20, 2021

  • Implements Ruud's proposal for fee management.
  • Moves the variadic accounts to Lido's account, simplifying handling.
  • Modifies the distribution of the validator's fees, the new approach is done in one transaction per validator
  • Modifies tests for the new structures

@enriquefynn enriquefynn requested review from naomijub, ruuda and malikankit and removed request for naomijub May 20, 2021 19:41
@naomijub
Copy link
Contributor

Why CI did not run for this branch?

assert_eq!(lido.owner, id());
// let lido = get_account(&mut banks_client, &lido_accounts.lido.pubkey()).await;
// assert_eq!(lido.data.len(), get_packed_len::<state::Lido>());
// assert_eq!(lido.owner, id());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are these asserts supposed to be commented?

@ruuda
Copy link
Contributor

ruuda commented May 21, 2021

Why CI did not run for this branch?

It’s targeted at fee-management-initial, but our config only lists main:

pull_request:
branches: [ main ]

The docs for pull_request.branches are a bit cryptic:

For a pull_request event, only branches and tags on the base are evaluated.

I think this means that if the base branch for the pull request is listed in branches, it will get evaluated. In this case the base branch is not main but fee-management-initial. I ran into this at #90 (comment) as well so I fixed it there.

@ruuda
Copy link
Contributor

ruuda commented May 21, 2021

$ git diff --shortstat origin/fee-management-initial..origin/fee-management-new-struct
 11 files changed, 506 insertions(+), 521 deletions(-)

It’s a net negative, nice! 😎

Copy link
Contributor

@ruuda ruuda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice! This makes everything simpler, more readable, and safer 🎉

My only substantial comment is that we should not mix stSOL and stake pool tokens in the code, if only to not confuse the reader. Let's add a new type for stake pool tokens as well. Aside from that I only have a few minor comments.

program/src/instruction.rs Show resolved Hide resolved
if insurance_account.key != &self.insurance_account
|| treasury_account.key != &self.treasury_account
|| manager_accounts.key != &self.manager_account
if &spl_token::state::Account::unpack_from_slice(&insurance_account_info.data.borrow())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of repeating the check three times, we could make the signature

pub fn check_recipient_valid(&self, minter_program: &Pubkey, recipient: &AccountInfo) -> Result<(), LidoError>;

Then at the call site it would look almost the same:

new_fee.check_recipient_valid(&lido.st_sol_mint_program, accounts.insurance_account)?;
new_fee.check_recipient_valid(&lido.st_sol_mint_program, accounts.treasury_account)?;
new_fee.check_recipient_valid(&lido.st_sol_mint_program, accounts.manager_accounts)?;

and this makes it a lot more obvious that these accounts are all treated in the same way, and it makes it harder to have copy-paste bugs such as accidentally checking one account twice and leaving one unchecked in the code below. (I didn’t see such copy-paste mistake here, but steering clear of them is nicer than carefully checking for them.)

let token_shares = distribute_fees(
&lido.fee_spec,
lido.validator_credit_accounts.validator_accounts.len() as u64,
StLamports(stake_pool_fee_account.amount),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically, the stake pool fee account contains stake pool tokens, not stSOL, and distribute_fees converts between the two. I’m not sure how much we will be dealing with stake pool tokens in computations, but it would be good to have separate types for them, so we can prevent unit mistakes like adding stake pool tokens to stSOL. If we call them StLamports and SptLamports that’s only one character difference, that sounds like a recipe for subtle mistakes, maybe we should call the stake pool token type StakePoolTokenLamports to be sure.

To convert between the two, I think the safest thing to do would be to put that in distribute_fees. It would then look something like

pub fn distribute_fees(spec: &FeeSpec, num_validators: u64, amount_spt: StakePoolTokenLamports) -> Option<Fees> {
    // The process of distributing fees restores the SPT <-> stSOL peg,
    // we have as many new stSOL to mint, as we received in SPT fees.
    let amount = StLamports(amount_spt.0);
    // ... function body unchanged
}

The StakePoolTokenLamports type doesn’t need any of the std::ops implementations for now, because we are never doing arithmetic with it.

validator_credit_accounts.validator_accounts[idx].amount +=
token_shares.each_validator_amount;
for vc in lido.validator_credit_accounts.validator_accounts.iter_mut() {
vc.st_sol_amount = (vc.st_sol_amount + token_shares.reward_per_validator)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hah, the type system caught a hypothetical overflow bug in the old code :D

@@ -235,8 +218,6 @@ pub fn process_deposit(
.calc_pool_tokens_for_deposit(amount, total_lamports)
.ok_or(LidoError::CalculationFailure)?;

let total_st_sol = lido.st_sol_total_shares + st_sol_amount;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another hypothetical overflow prevented by the new types!

/// Constant size of fee struct: 3 public keys + 4 u32
pub const LIDO_CONSTANT_FEE_SIZE: usize = 3 * 32 + 4 * 4;
/// Constant size of Lido
pub const LIDO_CONSTANT_SIZE: usize = LIDO_CONSTANT_HEADER_SIZE + LIDO_CONSTANT_FEE_SIZE;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What we could do to make this a bit harder to go out of sync, is to define an inner struct for the constant-size part, and use mem::size_of, instead of computing it manually. The downside is that we need to touch all the places that refer to lido to reference this inner struct, so I’m not sure it’s worth it.

Copy link
Member Author

@enriquefynn enriquefynn May 21, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mem::size_of will take the vec as if it had 0 elements?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean something like this

// No variable-size things in this struct.
struct LidoFixed {
    foo: u64,
    bar: Pubkey,
}
// All of the variable-size things after the fixed-size part.
// (We should confirm that Borsh respects field order.)
struct Lido {
    fixed: LidoFixed,
    variable: Vec<Quux>,
}
// The size of the fixed-size prefix.
mem::size_of::<LidoFixed>()

return buffer_size.saturating_sub(8) / 40;
// 8 bytes: 4 bytes for `max_validators` + 4 bytes for number of validators in vec
// 40 bytes for each validator = 32 bytes + 8 bytes for st_sol_amount
// TODO: Make a test for this
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably add this test now, otherwise it will likely never happen.

Copy link
Member Author

@enriquefynn enriquefynn May 21, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added it ^^

fn test_validator_credit_size() {


// The actual amount that goes to validation can be a tiny bit lower
// than the target amount, when the number of validators does not divide
// the target amount. The loss is at most `num_validators` μstSOL.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

μstSOL should now be stLamports.

},
distribute_fees(&spec_equal, 1, StLamports(1_000)).unwrap()
);
// Validation fee of 250 μstSOL is not a multiple of the number of
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/μstSOL/stLamports/

Also, the validation fee is no longer 250 stLamports, it’s 2000/9. I think a more interesting test would be to make the reward 999 lamports, because then at least among the different fee posts the division has no rounding error (222 stLamports would go to validation fees), but the validation fees themselves are not a multiple of 4, so we get this re-distribution of the rounding error either way.

manager_fee_numerator: 1,
denominator: 9,

let spec_equal = FeeSpec {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The shares for this spec are not actually equal. I think we can just call it spec.

Corrected writable accounts
Add function to check token minter
Use StakePoolTokenLamports instead of u64 for stake pool lamports
Copy link
Contributor

@ruuda ruuda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks almost good to me, only the consistency checks for the fee accounts were lost now, so we might end up checking an account that we are never going to use 🙊

It would be good if we could somehow generate these checks automatically and make them part of from_slice, but I don't have a clear idea for how to do that yet.

)?;
Lido::check_valid_minter_program(&accounts.mint_program.key, accounts.insurance_account)?;
Lido::check_valid_minter_program(&accounts.mint_program.key, accounts.treasury_account)?;
Lido::check_valid_minter_program(&accounts.mint_program.key, accounts.manager_fee_account)?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooh, there are these &self.insurance_account == accounts.insurance_account.key checks that were present previously, but that are now lost.

What we might do is to just use the accounts from self.*. If accounts.* does not match, the runtime will throw an error for accessing an unspecified account.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's better to leave it static but add an argument for the public key and check if it's equal to the account_info one.
Since we have to pass which one is the public key anyway.

)?;
Lido::check_valid_minter_program(&lido.st_sol_mint_program, accounts.insurance_account)?;
Lido::check_valid_minter_program(&lido.st_sol_mint_program, accounts.treasury_account)?;
Lido::check_valid_minter_program(&lido.st_sol_mint_program, accounts.manager_fee_account)?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also here, see my comment below about checking the consistency of the accounts.

Copy link
Contributor

@ruuda ruuda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@enriquefynn enriquefynn merged commit b1d44d4 into fee-management-initial May 21, 2021
@enriquefynn enriquefynn deleted the fee-management-new-struct branch May 21, 2021 14:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants