-
Notifications
You must be signed in to change notification settings - Fork 44
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
Fee implementation passing all tests #94
Conversation
enriquefynn
commented
May 20, 2021
•
edited
Loading
edited
- 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
Why CI did not run for this branch? |
program/tests/initialize.rs
Outdated
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()); |
There was a problem hiding this comment.
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?
It’s targeted at solido/.github/workflows/rust.yml Lines 6 to 7 in c798716
The docs for
I think this means that if the base branch for the pull request is listed in |
$ 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! 😎 |
There was a problem hiding this 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/state.rs
Outdated
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()) |
There was a problem hiding this comment.
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.)
program/src/process_management.rs
Outdated
let token_shares = distribute_fees( | ||
&lido.fee_spec, | ||
lido.validator_credit_accounts.validator_accounts.len() as u64, | ||
StLamports(stake_pool_fee_account.amount), |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added it ^^
Line 317 in c07cd2f
fn test_validator_credit_size() { |
program/src/state.rs
Outdated
|
||
// 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. |
There was a problem hiding this comment.
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.
program/src/state.rs
Outdated
}, | ||
distribute_fees(&spec_equal, 1, StLamports(1_000)).unwrap() | ||
); | ||
// Validation fee of 250 μstSOL is not a multiple of the number of |
There was a problem hiding this comment.
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.
program/src/state.rs
Outdated
manager_fee_numerator: 1, | ||
denominator: 9, | ||
|
||
let spec_equal = FeeSpec { |
There was a problem hiding this comment.
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
There was a problem hiding this 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.
program/src/processor.rs
Outdated
)?; | ||
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)?; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
program/src/process_management.rs
Outdated
)?; | ||
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)?; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!