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)
|
- [Install the Stake Pool CLI](stake-pool/cli.md)
|
||||||
- [Step through the quick start guide](stake-pool/quickstart.md)
|
- [Step through the quick start guide](stake-pool/quickstart.md)
|
||||||
- [Learn more about stake pools](stake-pool/overview.md)
|
- [Learn more about stake pools](stake-pool/overview.md)
|
||||||
|
- [Learn more about fees and monetization](stake-pool/fees.md)
|
||||||
|
|
||||||
## Source
|
## 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
|
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/).
|
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
|
on hand to fully manage the pool stakes. The SOL used to add a new validator
|
||||||
is recovered when removing the 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
|
### Funding restrictions
|
||||||
|
|
||||||
To give the manager more control over funds entering the pool, stake pools allow
|
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.
|
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
|
## Safety of Funds
|
||||||
|
|
||||||
One of the primary aims of the stake pool program is to always allow pool token
|
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
|
Each of these parameters is modifiable after pool creation, so there's no need
|
||||||
to worry about being locked in to any choices.
|
to worry about being locked in to any choices.
|
||||||
|
|
||||||
Modify the parameters to suit your needs. In our example, we will use fees
|
Modify the parameters to suit your needs. The fees are especially important to
|
||||||
of 0.3%, a referral fee of 50%, opt to *not* set a deposit authority, and have
|
avoid abuse, so please take the time to review and calculate fees that work best
|
||||||
the maximum number of validators (2,950). Next, run the script:
|
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
|
```bash
|
||||||
$ ./setup-stake-pool.sh
|
$ ./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(
|
fn get_signer(
|
||||||
matches: &ArgMatches<'_>,
|
matches: &ArgMatches<'_>,
|
||||||
keypair_name: &str,
|
keypair_name: &str,
|
||||||
|
@ -192,15 +218,19 @@ fn command_create_pool(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
deposit_authority: Option<Keypair>,
|
deposit_authority: Option<Keypair>,
|
||||||
epoch_fee: Fee,
|
epoch_fee: Fee,
|
||||||
stake_withdrawal_fee: Fee,
|
withdrawal_fee: Fee,
|
||||||
stake_deposit_fee: Fee,
|
deposit_fee: Fee,
|
||||||
stake_referral_fee: u8,
|
referral_fee: u8,
|
||||||
max_validators: u32,
|
max_validators: u32,
|
||||||
stake_pool_keypair: Option<Keypair>,
|
stake_pool_keypair: Option<Keypair>,
|
||||||
validator_list_keypair: Option<Keypair>,
|
validator_list_keypair: Option<Keypair>,
|
||||||
mint_keypair: Option<Keypair>,
|
mint_keypair: Option<Keypair>,
|
||||||
reserve_keypair: Option<Keypair>,
|
reserve_keypair: Option<Keypair>,
|
||||||
|
unsafe_fees: bool,
|
||||||
) -> CommandResult {
|
) -> CommandResult {
|
||||||
|
if !unsafe_fees {
|
||||||
|
check_stake_pool_fees(&epoch_fee, &withdrawal_fee, &deposit_fee)?;
|
||||||
|
}
|
||||||
let reserve_keypair = reserve_keypair.unwrap_or_else(Keypair::new);
|
let reserve_keypair = reserve_keypair.unwrap_or_else(Keypair::new);
|
||||||
println!("Creating reserve stake {}", reserve_keypair.pubkey());
|
println!("Creating reserve stake {}", reserve_keypair.pubkey());
|
||||||
|
|
||||||
|
@ -326,9 +356,9 @@ fn command_create_pool(
|
||||||
&spl_token::id(),
|
&spl_token::id(),
|
||||||
deposit_authority.as_ref().map(|x| x.pubkey()),
|
deposit_authority.as_ref().map(|x| x.pubkey()),
|
||||||
epoch_fee,
|
epoch_fee,
|
||||||
stake_withdrawal_fee,
|
withdrawal_fee,
|
||||||
stake_deposit_fee,
|
deposit_fee,
|
||||||
stake_referral_fee,
|
referral_fee,
|
||||||
max_validators,
|
max_validators,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -1958,6 +1988,12 @@ fn main() {
|
||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.help("Stake pool reserve keypair [default: new keypair]"),
|
.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")
|
.subcommand(SubCommand::with_name("add-validator")
|
||||||
.about("Add validator account to the stake pool. Must be signed by the pool staker.")
|
.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 validator_list_keypair = keypair_of(arg_matches, "validator_list_keypair");
|
||||||
let mint_keypair = keypair_of(arg_matches, "mint_keypair");
|
let mint_keypair = keypair_of(arg_matches, "mint_keypair");
|
||||||
let reserve_keypair = keypair_of(arg_matches, "reserve_keypair");
|
let reserve_keypair = keypair_of(arg_matches, "reserve_keypair");
|
||||||
|
let unsafe_fees = arg_matches.is_present("unsafe_fees");
|
||||||
command_create_pool(
|
command_create_pool(
|
||||||
&config,
|
&config,
|
||||||
deposit_authority,
|
deposit_authority,
|
||||||
|
@ -2678,6 +2715,7 @@ fn main() {
|
||||||
validator_list_keypair,
|
validator_list_keypair,
|
||||||
mint_keypair,
|
mint_keypair,
|
||||||
reserve_keypair,
|
reserve_keypair,
|
||||||
|
unsafe_fees,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
("add-validator", Some(arg_matches)) => {
|
("add-validator", Some(arg_matches)) => {
|
||||||
|
|
Loading…
Reference in New Issue