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:
parent
2caec406bc
commit
2d770628ef
|
@ -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
|
||||
|
|
|
@ -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!
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)) => {
|
||||
|
|
Loading…
Reference in New Issue