stake-pool-cli: Add best practices for fees, prevent zero fees (#2670)

* stake-pool-cli: Add best practices for fees, prevent zero fees

* Address feedback
This commit is contained in:
Jon Cinque 2022-01-11 01:24:16 +01:00 committed by GitHub
parent 2caec406bc
commit 2d770628ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 218 additions and 63 deletions

View File

@ -18,6 +18,7 @@ To get started with stake pools:
- [Install the Stake Pool CLI](stake-pool/cli.md)
- [Step through the quick start guide](stake-pool/quickstart.md)
- [Learn more about stake pools](stake-pool/overview.md)
- [Learn more about fees and monetization](stake-pool/fees.md)
## Source
@ -26,3 +27,20 @@ The Stake Pool Program's source is available on
For information about the types and instructions, the Stake Pool Rust docs are
available at [docs.rs](https://docs.rs/spl-stake-pool/0.6.3/spl_stake_pool/).
## Security Audits
Multiple security firms have audited the stake pool program to ensure total
safety of funds. The audit reports are available for reading, presented in descending
chronological order, and the commit hash that each was reviewed at:
* Quantstamp
- Initial review commit hash [`99914c9`](https://github.com/solana-labs/solana-program-library/tree/99914c9fc7246b22ef04416586ab1722c89576de)
- Re-review commit hash [`3b48fa0`](https://github.com/solana-labs/solana-program-library/tree/3b48fa09d38d1b66ffb4fef186b606f1bc4fdb31)
- Final report https://solana.com/SolanaQuantstampStakePoolAudit.pdf
* Neodyme
- Review commit hash [`0a85a9a`](https://github.com/solana-labs/solana-program-library/tree/0a85a9a533795b6338ea144e433893c6c0056210)
- Report https://solana.com/SolanaNeodymeStakePoolAudit.pdf
* Kudelski
- Review commit hash [`3dd6767`](https://github.com/solana-labs/solana-program-library/tree/3dd67672974f92d3b648bb50ee74f4747a5f8973)
- Report https://solana.com/SolanaKudelskiStakePoolAudit.pdf

146
docs/src/stake-pool/fees.md Normal file
View File

@ -0,0 +1,146 @@
---
title: Fees
---
Operators of stake pools should take time to understand the purpose of each fee
and think about them carefully to ensure that the pool cannot be abused.
There are five different sources of fees.
### Epoch Fee
Every epoch (roughly 2 days), the stake accounts in the pool earn
inflation rewards, so the stake pool mints pool tokens into the manager's fee
account as a proportion of the earned rewards.
For example, if the pool earns 10 SOL in rewards, and the fee is set to 2%, the
manager will earn pool tokens worth 0.2 SOL.
Note that the epoch fee is charged after normal validator
commissions are assessed. For example, if a validator charges 8% commission,
and the stake pool charges 2%, and a stake in the pool earns 100 SOL pre-commission,
then that stake will actually enrich the pool by 90.16 SOL. The total rewards
on that validator will be reduced by ~9.84%.
### SOL Withdraw Fee
Sends a proportion of the desired withdrawal amount to the manager.
For example, if a user wishes to withdraw 100 pool tokens, and the fee is set
to 3%, 3 pool tokens go to the manager, and the remaining 97 tokens are converted
to SOL and sent to the user.
### Stake Withdraw Fee
Sends a proportion of the desired withdrawal amount to the manager before
creating a new stake for the user.
For example, if a user wishes to withdraw 100 pool tokens, and the fee is set
to 0.5%, 0.5 pool tokens go to the manager, and the remaining 99.5 tokens are
converted to SOL then sent to the user as an activated stake account.
### SOL Deposit Fee
Converts the entire SOL deposit into pool tokens, then sends a proportion of
the pool tokens to the manager, and the rest to the user.
For example, if a user deposits 100 SOL, which converts to 90 pool tokens,
and the fee is 1%, then the user receives 89.1 pool tokens, and the manager receives
0.9 pool tokens.
### Stake Deposit Fee
Converts the stake account's delegation plus rent-exemption to pool tokens,
sends a proportion of those to the manager, and the rest to the user. The rent-
exempt portion of the stake account is converted at the SOL deposit rate, and
the stake is converted at the stake deposit rate.
For example, let's say the pool token to SOL exchange rate is 1:1, the SOL deposit rate
is 10%, and the stake deposit rate is 5%. If a user deposits a stake account with
100 SOL staked and 0.00228288 SOL for the rent exemption. The fee from the stake
is worth 5 pool tokens, and the fee from the rent exemption is worth 0.000228288
pool tokens, so the user receives 95.002054592 pool tokens, and the manager
receives 5.000228288 pool tokens.
## Referral Fees
For partner applications, the manager may set a referral fee on deposits.
During SOL or stake deposits, the stake pool redistributes a percentage of
the pool token fees to another address as a referral fee.
This option is particularly attractive for wallet providers. When a wallet
integrates a stake pool, the wallet developer will have the option to earn
additional tokens anytime a user deposits into the stake pool. Stake pool
managers can use this feature to create strategic partnerships and entice
greater adoption of stake pools!
## Best Practices
Outside of monetization, fees are a crucial tool for avoiding economic attacks
on the stake pool and keeping it running. For this reason, the stake pool CLI
will prevent managers from creating a pool with no fees, unless they also provide
the `--yolo` flag.
### Epoch
If a stake pool with 1000 validators runs a rebalancing script every epoch, the
staker needs to send roughly 200 transactions to update the stake pool balances,
followed by up to 1000 transactions to increase or decrease the stake on every
validator.
At the time of writing, the current transaction fee is 5,000 lamports per signature,
so the minimum cost for these 1,200 transactions is 6,000,000 lamports, or 0.006 SOL.
For the stake pool manager to break even, they must earn 0.006 SOL per epoch in
fees.
For example, let's say we have a stake pool with 10,000 SOL staked, whose stakes
are earning 6% APY / ~3.3 basis points per epoch, yielding roughly 3.3 SOL per epoch
in rewards. The minimal break-even epoch fee for this stake pool is 0.18%.
### Stake Deposit / Withdraw
If a stake pool has no deposit or withdraw fees, a malicious pool token holder
can easily leech value from the stake pool.
In the simplest attack, right before the end of every epoch, the malicious pool
token holder finds the highest performing validator in the pool for that epoch,
withdraws an active stake worth all of their pool tokens, waits until the epoch
rolls over, earns the maximum stake rewards, and then deposits right back into
the stake pool.
Practically speaking, the malicious depositor is always delegated to the best
performing validator in the stake pool, without ever actually committing a stake
to that validator. On top of that, the malicious depositor goes around any
epoch fees.
To render this attack unviable, the stake pool manager can set a deposit or withdraw
fee. If the stake pool has an overall performance of 6% APY / ~3.3 basis points
per epoch, and the best validator has a performance of 6.15% APY / ~3.37 basis
points per epoch, then the minimum stake deposit / withdrawal fee would be
0.07 basis points.
For total safety, in case a delinquent validator in the pool brings down
performance, a manager may want to go much higher.
### SOL Deposit / Withdrawal
If a stake pool has 0 SOL deposit / withdrawal fee, then a malicious SOL holder
can perform a similar attack to extract even more value from the pool.
If they deposit SOL into a stake pool, withdraw a stake account on the top
validator in the pool, wait until the epoch rolls over, then deposit that stake
back into the pool, then withdraw SOL, they have essentially earned free instant
rewards without any time commitment of their SOL. In the meantime, the stake
pool performance has decreased because the deposited liquid SOL does not earn
rewards.
For example, if the best performing validator in the stake pool earns 6.15%
APY / ~3.37 basis points per epoch, then the minimum SOL deposit / withdrawal
fee should be 3.37 basis points.
## Final thoughts
The attacks outlined in the previous sections are the simplest attacks that anyone
can easily perform with a couple of scripts running a few times per epoch. There are
likely more complex attacks possible for zero or very low fee stake pools, so be
sure to protect your depositors with fees!

View File

@ -106,43 +106,6 @@ the stake account on a validator, so the stake pool staker will need liquidity
on hand to fully manage the pool stakes. The SOL used to add a new validator
is recovered when removing the validator.
### Fees
The stake pool program provides managers many options for making the pool
financially viable, predominantly through fees. There are five different sources
of fees:
* Epoch: every epoch (roughly 2 days), the stake accounts in the pool earn
inflation rewards, so the stake pool mints pool tokens into the manager's fee
account as a proportion of the earned rewards. For example, if the pool earns
10 SOL in rewards, and the fee is set to 2%, the manager will earn pool tokens
worth 0.2 SOL. Note that the epoch fee is charged after normal validator
commissions are assessed. For example, if a validator charges 8% commission,
and the stake pool charges 2%, and a stake in the pool earns 100 SOL pre-commission,
then that stake will actually enrich the pool by 90.16 SOL. The total rewards
on that validator will be reduced by ~9.84%.
* SOL withdraw: sends a proportion of the desired withdrawal amount to the manager
For example, if a user wishes to withdraw 100 pool tokens, and the fee is set
to 3%, 3 pool tokens go to the manager, and the remaining 97 tokens go to the
user in the form of a SOL.
* Stake withdraw: sends a proportion of the desired withdrawal amount to the manager
before creating a new stake for the user.
* SOL deposit: converts the entire SOL deposit into pool tokens, then sends a
proportion of those to the manager, and the rest to the user
* Stake deposit: converts the stake account's delegation plus rent-exemption
to pool tokens, sends a proportion of those to the manager, and the rest to
the user
For partner applications, there's the option of a referral fee on deposits.
During SOL or stake deposits, the stake pool can redistribute a percentage of
the fees to another address as a referral fee.
This option is particularly attractive for wallet providers. When a wallet
integrates a stake pool, the wallet developer will have the option to earn
additional tokens anytime a user deposits into the stake pool. Stake pool
managers can use this feature to create strategic partnerships and entice
greater adoption of stake pools!
### Funding restrictions
To give the manager more control over funds entering the pool, stake pools allow
@ -169,23 +132,6 @@ This can also be useful in a few situations:
Note: in order to keep user funds safe, stake withdrawals are always permitted.
## Security Audits
Multiple security firms have audited the stake pool program to ensure total
safety of funds. The audit reports are available for reading, presented in descending
chronological order, and the commit hash that each was reviewed at:
* Quantstamp
- Initial review commit hash [`99914c9`](https://github.com/solana-labs/solana-program-library/tree/99914c9fc7246b22ef04416586ab1722c89576de)
- Re-review commit hash [`3b48fa0`](https://github.com/solana-labs/solana-program-library/tree/3b48fa09d38d1b66ffb4fef186b606f1bc4fdb31)
- Final report https://solana.com/SolanaQuantstampStakePoolAudit.pdf
* Neodyme
- Review commit hash [`0a85a9a`](https://github.com/solana-labs/solana-program-library/tree/0a85a9a533795b6338ea144e433893c6c0056210)
- Report https://solana.com/SolanaNeodymeStakePoolAudit.pdf
* Kudelski
- Review commit hash [`3dd6767`](https://github.com/solana-labs/solana-program-library/tree/3dd67672974f92d3b648bb50ee74f4747a5f8973)
- Report https://solana.com/SolanaKudelskiStakePoolAudit.pdf
## Safety of Funds
One of the primary aims of the stake pool program is to always allow pool token

View File

@ -109,9 +109,16 @@ if it has no fees.
Each of these parameters is modifiable after pool creation, so there's no need
to worry about being locked in to any choices.
Modify the parameters to suit your needs. In our example, we will use fees
of 0.3%, a referral fee of 50%, opt to *not* set a deposit authority, and have
the maximum number of validators (2,950). Next, run the script:
Modify the parameters to suit your needs. The fees are especially important to
avoid abuse, so please take the time to review and calculate fees that work best
for your pool.
Carefully read through the [Fees](fees.md) for more information about fees and
best practices.
In our example, we will use fees of 0.3%, a referral fee of 50%, opt to *not*
set a deposit authority, and have the maximum number of validators (2,950). Next,
run the script:
```bash
$ ./setup-stake-pool.sh

View File

@ -89,6 +89,32 @@ fn check_fee_payer_balance(config: &Config, required_balance: u64) -> Result<(),
}
}
const FEES_REFERENCE: &str = "Consider setting a minimal fee. \
See https://spl.solana.com/stake-pool/fees for more \
information about fees and best practices. If you are \
aware of the possible risks of a stake pool with no fees, \
you may force pool creation with the --unsafe-fees flag.";
fn check_stake_pool_fees(
epoch_fee: &Fee,
withdrawal_fee: &Fee,
deposit_fee: &Fee,
) -> Result<(), Error> {
if epoch_fee.numerator == 0 || epoch_fee.denominator == 0 {
return Err(format!("Epoch fee should not be 0. {}", FEES_REFERENCE,).into());
}
let is_withdrawal_fee_zero = withdrawal_fee.numerator == 0 || withdrawal_fee.denominator == 0;
let is_deposit_fee_zero = deposit_fee.numerator == 0 || deposit_fee.denominator == 0;
if is_withdrawal_fee_zero && is_deposit_fee_zero {
return Err(format!(
"Withdrawal and deposit fee should not both be 0. {}",
FEES_REFERENCE,
)
.into());
}
Ok(())
}
fn get_signer(
matches: &ArgMatches<'_>,
keypair_name: &str,
@ -192,15 +218,19 @@ fn command_create_pool(
config: &Config,
deposit_authority: Option<Keypair>,
epoch_fee: Fee,
stake_withdrawal_fee: Fee,
stake_deposit_fee: Fee,
stake_referral_fee: u8,
withdrawal_fee: Fee,
deposit_fee: Fee,
referral_fee: u8,
max_validators: u32,
stake_pool_keypair: Option<Keypair>,
validator_list_keypair: Option<Keypair>,
mint_keypair: Option<Keypair>,
reserve_keypair: Option<Keypair>,
unsafe_fees: bool,
) -> CommandResult {
if !unsafe_fees {
check_stake_pool_fees(&epoch_fee, &withdrawal_fee, &deposit_fee)?;
}
let reserve_keypair = reserve_keypair.unwrap_or_else(Keypair::new);
println!("Creating reserve stake {}", reserve_keypair.pubkey());
@ -326,9 +356,9 @@ fn command_create_pool(
&spl_token::id(),
deposit_authority.as_ref().map(|x| x.pubkey()),
epoch_fee,
stake_withdrawal_fee,
stake_deposit_fee,
stake_referral_fee,
withdrawal_fee,
deposit_fee,
referral_fee,
max_validators,
),
],
@ -1958,6 +1988,12 @@ fn main() {
.takes_value(true)
.help("Stake pool reserve keypair [default: new keypair]"),
)
.arg(
Arg::with_name("unsafe_fees")
.long("unsafe-fees")
.takes_value(false)
.help("Bypass fee checks, allowing pool to be created with unsafe fees"),
)
)
.subcommand(SubCommand::with_name("add-validator")
.about("Add validator account to the stake pool. Must be signed by the pool staker.")
@ -2657,6 +2693,7 @@ fn main() {
let validator_list_keypair = keypair_of(arg_matches, "validator_list_keypair");
let mint_keypair = keypair_of(arg_matches, "mint_keypair");
let reserve_keypair = keypair_of(arg_matches, "reserve_keypair");
let unsafe_fees = arg_matches.is_present("unsafe_fees");
command_create_pool(
&config,
deposit_authority,
@ -2678,6 +2715,7 @@ fn main() {
validator_list_keypair,
mint_keypair,
reserve_keypair,
unsafe_fees,
)
}
("add-validator", Some(arg_matches)) => {