rewriting proposal with examples
This commit is contained in:
parent
3680b81cdf
commit
e30cf9bb4f
|
@ -15,80 +15,59 @@ feature:
|
|||
## Summary
|
||||
|
||||
This SIMD will discuss additional fees called Application Fees. These fees are
|
||||
decided and set by the dapp developer to interact with the dapp. Dapp
|
||||
developers then can decide to rebate these fees if a user interacts with the
|
||||
dapp as intended and disincentivize the users which do not. These fees will be
|
||||
applied even if the transaction eventually fails and collected on the same
|
||||
writable account. Owner of the account can do lamport transfers to recover
|
||||
these fees. So instead of fees going to the validator these fees go to the
|
||||
**dapp developers**. It will be dapp developer's responsibility to advertise
|
||||
the required application fees to its users.
|
||||
decided and set by the dapp developer to interact with the dapp. Dapp developers
|
||||
then can decide to rebate these fees if a user interacts with the dapp as
|
||||
intended and disincentivize the users which do not. These fees will be applied
|
||||
even if the transaction eventually fails and are collected on the writable
|
||||
account. The owner of the account can do lamport transfer to recover these
|
||||
fees. So instead of fees going to the validator, these fees go to the **dapp
|
||||
developers**. It will be dapp developer's responsibility to advertise the
|
||||
required application fees to its users.
|
||||
|
||||
Discussion for the issue : <https://github.com/solana-labs/solana/issues/21883>
|
||||
Discussion for the issue: <https://github.com/solana-labs/solana/issues/21883>
|
||||
|
||||
## Motivation
|
||||
|
||||
During network congestion, many transactions in the network cannot be
|
||||
processed because they all want to write-lock same accounts. When a write lock
|
||||
on an account is taken by a transaction batch no other batch can use this
|
||||
account in parallel, so only transactions belonging to a single batch are
|
||||
processed correctly and all others are retried again or forwarded to the
|
||||
leader of the next slot. With all the forwarding and retrying the validator is
|
||||
choked by the transactions write-locking the same accounts and effectively
|
||||
processing valid transactions sequentially.
|
||||
Currently, there is no way for dapp developers to enforce appropriate behavior
|
||||
and the way their contracts should be used. Bots spamming on dapps make them
|
||||
unusable and dapps lose their users/clients because the UX is laggy and
|
||||
inefficient. Unsuccessful transactions deny block space to potentially valid
|
||||
transactions which reduces activity on the dapp. For dapps like mango or
|
||||
openbook increasing fees without any rebates or dynamically based other proposed
|
||||
mechanisms will punish potentially thousands of users because of a handful of
|
||||
malicious users. Giving dapp's authority more control to incentivize proper
|
||||
utilization of its contract is the primary motivation for this proposal.
|
||||
|
||||
During network congestion, many transactions in the network cannot be processed
|
||||
because they all want to write-lock the same accounts. When a write lock on an
|
||||
account is taken by a transaction batch no other batch can use this account in
|
||||
parallel, so only transactions belonging to a single batch are processed
|
||||
correctly and all others are retried again or forwarded to the leader of the
|
||||
next slot. With all the forwarding and retrying the validator is choked by the
|
||||
transactions write-locking the same accounts and effectively processing valid
|
||||
transactions sequentially.
|
||||
|
||||
There are multiple accounts of OpenBook (formerly serum), mango markets, etc
|
||||
which are used in high-frequency trading. During the event of extreme
|
||||
congestion, we observe that specialized trading programs write lock these
|
||||
accounts but never CPI into the respective programs unless they can extract
|
||||
profit, effectively starving the actual users for access. Penalizing this
|
||||
behavior with runtime fees collected by validators can create a perverse
|
||||
incentive to artificially delay HFT transactions, cause them to fail, and
|
||||
charge penalty fees.
|
||||
profit, effectively starving the actual users for access. With current low
|
||||
cluster fees, it incentivizes spammers to spam the network with transactions.
|
||||
|
||||
Currently, there is no way for dapp developers to enforce appropriate behavior
|
||||
and the way their contracts should be used. Bots spamming on dapps make them
|
||||
unusable and dapps lose their users/clients because the UX is laggy and
|
||||
inefficient. Validators gain base fees and prioritization fees even if the
|
||||
transactions are executed unsuccessfully but unsuccessful transactions deny
|
||||
block space to potentially valid transactions which reduces activity on the
|
||||
dapp. For dapps like mango or openbook increasing fees without any rebates or
|
||||
dynamically based other proposed mechanisms will punish potentially thousands
|
||||
of users because of a handful of malicious users. Giving dapp's authority more
|
||||
control to incentivize proper utilization of its contract is the primary
|
||||
motivation for this proposal.
|
||||
|
||||
Adding dynamic fees per account penalizes the dapp and its user. Without
|
||||
proper rebates to users Solana gas fees will increase and it will lose the
|
||||
edge for being low fees cluster. If the dynamic gas fees are low then it won't
|
||||
solve the spamming issues. Either way, dynamic fees will not be covered by
|
||||
this proposal because for users can't tell beforehand how much fees will be
|
||||
paid for the transaction they have sent. This will make the cluster not
|
||||
interesting for required players like market makers for whom profit margins
|
||||
are thin and a dynamic cluster will make it impossible to predict the outcome.
|
||||
|
||||
The cluster is currently vulnerable to a different kind of attack where an
|
||||
adversary with malicious intent can block its competitors by writing or
|
||||
read-locking their accounts through a transaction. This attack involves
|
||||
carrying out intensive calculations that consume a large number of
|
||||
computational units, thereby blocking competitors from performing MEV during
|
||||
that particular block, and giving the attacker an unfair advantage. We have
|
||||
identified specific transactions that perpetrate this attack and waste
|
||||
valuable block space. The malicious transaction write locks multiple token
|
||||
accounts and consumes 11.7 million CU i.e around 1/4 the block space. As a
|
||||
result, such attacks can prevent users from using their own token accounts,
|
||||
vote accounts, or stake accounts, and dapps from utilizing the required
|
||||
accounts. With the proposed solution, every program, such as the token
|
||||
program, stake program, and vote program, can include instructions to employ
|
||||
the application fees feature on their accounts and rebate the fees if the user
|
||||
initiates the transaction. The attacker will find this option unfeasible as
|
||||
they will consume their SOL tokens more rapidly to maintain the attack.
|
||||
|
||||
We are motivated to improve the user experience, and user activity and keep
|
||||
Solana a low gas fee blockchain. Keeping gas fees low and increasing user
|
||||
Without any proper rebate mechanism for users, Solana gas fees will increase and
|
||||
it will lose the edge for being low fees cluster. For entities like market
|
||||
makers, Solana must remain a low-fee cluster as they have a thin profit margin
|
||||
and have to quote quite often. The goal of this proposal is to reduce spamming
|
||||
without really increasing fees for everyone. Encourage users to create proper
|
||||
transactions. Read the cluster state before creating transactions instead of
|
||||
spamming. We are motivated to improve the user experience, and user activity and
|
||||
keep Solana a low gas fee blockchain. Keeping gas fees low and increasing user
|
||||
activity on the chain will help all the communities based on Solana grow. This
|
||||
will also help users to gain more control over accounts owned by them.
|
||||
|
||||
Note that this proposal does not aim to increase gas fees for all transactions
|
||||
or add any kind of congestion control mechanism.
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
* Having a fixed write lock fee and a read lock fee.
|
||||
|
@ -97,23 +76,25 @@ Pros: Simpler to implement, Simple to calculate fees.
|
|||
|
||||
Cons: Will increase fees for everyone, dapp cannot decide to rebate fees, (or
|
||||
not all existing apps have to develop a rebate system). Fees cannot be decided
|
||||
by the dapp developer.
|
||||
by the dapp developer. Penalizes everyone for a few bad actors.
|
||||
|
||||
* Passing application fees for each account by instruction and validating them
|
||||
during the execution
|
||||
during the execution inside user application code
|
||||
|
||||
Pros: High efficiency, minimal performance impact, more generic, no need to
|
||||
change the account structure.
|
||||
|
||||
Cons: Cannot prevent denial of service attacks, or account blocking attacks.
|
||||
Cons: Cannot prevent denial of service attacks, or account blocking
|
||||
attacks that do not call into the respective program
|
||||
|
||||
## New Terminology
|
||||
|
||||
Application Fees: Fees that are decided by the dapp developer will be charged
|
||||
if a user wants to use the dapp. They will be applied even if the transaction
|
||||
fails contrary to lamport transfers.
|
||||
Application Fees: Fees that are decided by the dapp developer will be charged if
|
||||
a user wants to use the dapp. They will be applied even if the transaction fails
|
||||
contrary to lamport transfers. The program can decide to rebate these fees back
|
||||
to the user if certain conditions decided by dapp developers are met.
|
||||
|
||||
## Other terminology
|
||||
### Other terminology
|
||||
|
||||
`Owner` of an account: It is the account owner as specified in the `owner`
|
||||
field in the account structure. In the case of an externally owned account
|
||||
|
@ -129,308 +110,252 @@ withdrawing tokens, the authority of the associated token account has to be
|
|||
the signer for the transaction containing transfer instruction. To receive
|
||||
tokens from another account, the authority's signature is not required.
|
||||
|
||||
`Other Fees`: Fees other than application fees
|
||||
`base fees + prioritization fees`.
|
||||
|
||||
## Detailed Design
|
||||
|
||||
As a high-performance cluster, we want to incentivize players which feed the
|
||||
network with responsibly behaved transactions instead of spamming the network.
|
||||
The introduction of application fees would be an interesting way to penalize
|
||||
the bad actors and dapps can rebate these fees to the good actors. This means
|
||||
the dapp developer has to decide who to rebate and who to penalize special
|
||||
instructions. In particular, it needs to be able to penalize, even if the
|
||||
application is not CPI'd to. There were multiple possible approaches discussed
|
||||
to provide the application with access to transfer lamports outside of the
|
||||
regular cpi execution context. The following approach seems the best.
|
||||
Application fees enable dapp developer to decide who to rebate and who to
|
||||
penalize through runtime features. In particular, programs needs to be able
|
||||
to penalize, even if the application is not CPI'd to.
|
||||
|
||||
*Updating core account structure to store application fees in ledger*. A
|
||||
`PayApplicationFee` instruction is will be used by solana runtime to calculate
|
||||
how much application fees are being paid by the transaction. A
|
||||
`UpdateApplicationFees` instruction will update the application fees for an
|
||||
account. A `Rebate` instruction will be used to rebate the application fees
|
||||
back to the payer.
|
||||
The checks on the application fees will be taken care of by Solana runtime.
|
||||
Total application fees paid will be included in the transaction through a
|
||||
special `PayApplicationFees` similar to the `ComputeBudget` program so that it
|
||||
is easier to calculate the total amount of fees that will be paid. In case of
|
||||
nonpayment or partial payment, the program is never executed. This instruction
|
||||
cannot be CPI'ed into.
|
||||
|
||||
1. High degree of flexibility for programs (**++**)
|
||||
2. Each transaction has to include additional instructions so it will
|
||||
break existing dapp interfaces (**-**)
|
||||
3. This implementation will prevent DOS on dapps and account blocking
|
||||
attacks (**+++**).
|
||||
4. Account structure needs to be modified, lot of changes in core Solana
|
||||
code (**---**)
|
||||
5. Application fees are checked before executing the dapps (**++**)
|
||||
6. Easier to calculate total fees to be paid by the payer (**+**)
|
||||
7. Application fees cannot be dynamic (**-**)
|
||||
8. Will be used to disable read locks on any accounts (**+**)
|
||||
Application developers will be able to rebate these fees through a new runtime
|
||||
mechanism, but only if the transaction actually succeeds, this way CU heavy
|
||||
simulation of the program while write-locking the respective accounts will
|
||||
require full fee payment.
|
||||
|
||||
An additional option will be added to disable read-locking of accounts so that
|
||||
an account having application fees could not be read-locked by any
|
||||
transaction. This will disable attacks where transaction read locks an account
|
||||
and prevents other transactions from write-locking the account.
|
||||
### Payment
|
||||
|
||||
If existing dapps like openbook want to implement application fees then all
|
||||
the dapps depending on openbook also have to do additional development. The
|
||||
checks on the application fees will be taken care by solana runtime. Total
|
||||
application fees paid will be included in the transaction so that it is easier
|
||||
to calculate the total amount of fees that will be paid and less scope for
|
||||
fraud. The maximum amount of application fees that can be set for an account
|
||||
will be limited to a predecided number of SOLs recommended (100 SOLs) so that
|
||||
account does not become inaccessible.
|
||||
|
||||
All other programs have to implement required instructions so that the
|
||||
authority of the accounts can cyclically sign to update application fees on
|
||||
the accounts they own.
|
||||
|
||||
### A new application fee program
|
||||
|
||||
We add a new native solana program called application fees program with
|
||||
program id.
|
||||
|
||||
```
|
||||
App1icationFees1111111111111111111111111111
|
||||
```
|
||||
|
||||
This native program will be used to update application fee for an account, to
|
||||
initialize rebates initaited by the account authority and special instruction
|
||||
by which fee payer accepts the maximum amount of lamports they are willing to
|
||||
pay as application fees for the transaction.
|
||||
|
||||
#### PayApplicationFees Instruction
|
||||
|
||||
With this instruction, the fee payer accepts to pay application fees
|
||||
specifying the maximum amount. This instruction **MUST** be included in the
|
||||
transaction that interacts with dapps having application fees. This
|
||||
instruction is like an infallible transfer if the payer has enough funds i.e,
|
||||
even if the transaction fails, the payer will end up paying the required
|
||||
amount of application fees. If the payer does not have enough balance, the
|
||||
transaction fails with the error `InsufficientFunds`. This instruction is
|
||||
decoded in the `calculate_fee` method of `bank.rs` where other fees are
|
||||
calculated, and the sum of all the fees is deducted from the payer. The
|
||||
(Accounts -> Fees) map will be created from loaded accounts and passed into
|
||||
invoke context and execution results. Before executing the transaction, we
|
||||
will check if enough application fees is paid to coverall loaded accounts. In
|
||||
case of missing instruction or insufficient fees paid, the transaction will
|
||||
fail, citing `ApplicationFeesNotPaid` error. If the transaction read locks an
|
||||
account on which read lock has been disabled then the transaction fails with
|
||||
an error `ReadLockDisabled`. If the transaction fails at this stage the payer
|
||||
will end up paying `base fees + prioritization fees + application fees`. If
|
||||
the transaction is successfully executed, then rebates are returned to the
|
||||
payer and the remaining fees are transferred to respective accounts. If the
|
||||
transaction fails to execute, the fees are transferred to respective accounts
|
||||
without any rebates.
|
||||
|
||||
If payer has overpaid the application fees then after paying all application
|
||||
fees the remaining amount will be returned to the payer even if the
|
||||
transaction fails. In case of partial payment, the user will lose the
|
||||
partially paid amount. To avoid burning application fees or creating new
|
||||
accounts, we add a constraint that accounts on which application fees are paid
|
||||
must exist and write-locked. Suppose we pay application fees on an account
|
||||
that does not exist. In that case, the payer will end up paying
|
||||
`base fees + prioritization fees + application fees on existing accounts`, and
|
||||
the transaction will fail. This instruction cannot be CPI'ed into.
|
||||
|
||||
It requires:
|
||||
|
||||
Accounts :
|
||||
|
||||
* None
|
||||
Argument: Maximum application fees to be paid in lamports as `u64`
|
||||
|
||||
#### UpdateApplicationFees Instruction
|
||||
|
||||
This instruction will update application fees for an account.
|
||||
It requires :
|
||||
|
||||
* Writable account as (writable)
|
||||
* Owner of the writable account as (signer)
|
||||
|
||||
Argument: fees in lamport (u64), disable read lock (boolean) by default false.
|
||||
|
||||
This instruction will set the application fees for an account in the `Account`
|
||||
structure. It will also update if the read locks on the account should be
|
||||
disabled in the `Account` structure. Before executing transactions Solana
|
||||
runtime will check the data set by this instruction for an account against
|
||||
application fees paid by the instruction. The account must already exist and
|
||||
should be rent-free to change its application fees. Application fees cannot be
|
||||
updated on externally owned accounts, i.e accounts where system program is the
|
||||
owner of the account.
|
||||
|
||||
#### Rebate Instruction
|
||||
|
||||
This instruction should be called by the dapp using CPI or by the owner of the
|
||||
account. It requires :
|
||||
|
||||
* Account on which a fee was paid
|
||||
* Owner of the account (signer)
|
||||
|
||||
Argument: Number of lamports to rebate (u64) can be u64::MAX to rebate all the
|
||||
fees.
|
||||
|
||||
The owner could be easily deduced from the `AccountMeta`. In case of PDA's
|
||||
usually account and owner are the same (if it was not changed), then
|
||||
`invoke_signed` can be used to issue a rebate. In case of multiple rebate
|
||||
instructions, only the maximum rebate will one will be issued. Rebates on the
|
||||
same accounts can be done in multiple instructions only the maximum one will
|
||||
be issued. Payer has to pay full application fees initially even if they are
|
||||
eligible for a rebate. There will be no rebates if the transaction fails even
|
||||
if the authority had rebated the fees back. If there is no application fee
|
||||
associated with the account rebate instruction does not do anything.
|
||||
|
||||
The existing programs could integrate cyclic signing to implement this
|
||||
feature. For instance, token program can include a rebate instruction that
|
||||
necessitates the token account authority's signature. Therefore, while
|
||||
write-locking their token account to perform fund transfers, the authority can
|
||||
add rebate instruction to initiate a rebate to itself.
|
||||
|
||||
### Changes in the core Solana code
|
||||
|
||||
These are following changes that we have identified as required to implement
|
||||
this feature.
|
||||
|
||||
Currently account structure is defined as follows:
|
||||
|
||||
```Rust
|
||||
#[repr(C)]
|
||||
pub struct Account {
|
||||
/// lamports in the account
|
||||
pub lamports: u64, // size = 8, align = 8, offset = 0
|
||||
/// data held in this account
|
||||
#[serde(with = "serde_bytes")]
|
||||
pub data: Vec<u8>, // size = 24, align = 8, offset = 8
|
||||
/// the program that owns this account. If executable,
|
||||
/// the program that loads this account.
|
||||
pub owner: Pubkey, // size = 32, align = 1, offset = 32
|
||||
/// this account's data contains a loaded program (and is now read-only)
|
||||
pub executable: bool, // size = 1, align = 1, offset = 64
|
||||
/// the epoch at which this account will next owe rent
|
||||
pub rent_epoch: Epoch, // size = 8, align = 8, offset = 72
|
||||
}
|
||||
```
|
||||
|
||||
Here we can see that we have 7 bytes of space between executable and
|
||||
rent_epoch. The rent_epoch is being deprecated and will be eventually removed.
|
||||
So we can reuse the rent epoch to store application fees as both of them store
|
||||
value as u64. We also add a new field called `has_application_fees` and rename
|
||||
`rent_epoch` to `rent_epoch_or_application_fees`. If `has_application_fees` is
|
||||
true then rent_epoch for the account is rent exempt i.e u64::MAX and the
|
||||
application fee is decided by value of `rent_epoch_or_application_fees`. And
|
||||
if `has_application_fees` is false then `rent_epoch` is
|
||||
`rent_epoch_or_application_fees` and the application fee is 0.
|
||||
|
||||
So we cannot have both the rent epoch and application fees in the same space.
|
||||
We cannot set application fees for accounts that are not rent-free. As in two
|
||||
years, we won't have any account which is not rent-free I guess that won't be
|
||||
an issue.
|
||||
|
||||
We will also add a boolean `disable_read_locks` after `has_application_fees`
|
||||
boolean. This boolean can also be set by the `UpdateApplicationFees`
|
||||
instruction. If true then no transaction can take a read lock on the account.
|
||||
If any transaction takes the read lock then it will fail with
|
||||
`ReadLockDisabled` error before the execution.
|
||||
|
||||
In append_vec.rs AccountMeta is the way an account is stored physically on the
|
||||
disk. We use a similar concept as above but we do not have extra space to add
|
||||
the `has_application_fees` boolean. Here we have decided to reuse the space in
|
||||
the `executable` byte to store the value of `has_application_fees` and
|
||||
`disable_read_locks`. So `executable` will be changed to `account_flags` where
|
||||
1 LSB is `executable` and 2nd LSB is `has_application_fees` and 3rd LSB is
|
||||
`disable_read_locks`. This change does not impact a lot of code and is very
|
||||
localized to file append_vec.
|
||||
|
||||
The new account structure will look like this:
|
||||
|
||||
```Rust
|
||||
#[repr(C)]
|
||||
pub struct Account {
|
||||
/// lamports in the account
|
||||
pub lamports: u64, // size = 8, align = 8, offset = 0
|
||||
/// data held in this account
|
||||
#[serde(with = "serde_bytes")]
|
||||
pub data: Vec<u8>, // size = 24, align = 8, offset = 8
|
||||
/// the program that owns this account. If executable,
|
||||
/// the program that loads this account.
|
||||
pub owner: Pubkey, // size = 32, align = 1, offset = 32
|
||||
/// this account's data contains a loaded program (and is now read-only)
|
||||
pub executable: bool, // size = 1, align = 1, offset = 64
|
||||
/// this account's has application fees,
|
||||
/// if true value of the application fees is rent_epoch_or_application_fees
|
||||
/// if true the account is rent free
|
||||
pub has_application_fees: bool // size = 1, align = 1, offset = 65
|
||||
/// transactions cannot take read lock on this account.
|
||||
/// They have to take write locks.
|
||||
pub disable_read_locks: bool // size = 1, align = 1, offset = 66
|
||||
/// the epoch at which this account will next owe rent
|
||||
pub rent_epoch_or_application_fees: Epoch,// size = 8,align = 8,offset = 72
|
||||
}
|
||||
```
|
||||
|
||||
#### Changes in `MessageProcessor::process_message`
|
||||
|
||||
Before processing the message, we check if a loaded account has associated
|
||||
application fees. For all accounts with application fees, the fees paid should
|
||||
be greater than the required fees. In case of overpayment, the difference is
|
||||
stored in a variable. In case the application fees are insufficiently paid or
|
||||
not paid, then we set the transaction status as errored. If there was a read
|
||||
lock taken on an account where the read lock has been disabled, then we set
|
||||
the transaction status as errored.
|
||||
|
||||
#### Changes in `load_transaction_accounts`
|
||||
|
||||
When we load transaction accounts we have to calculate the application fees by
|
||||
decoding the `PayApplicationFees` instruction. Then we verify that fee-payer
|
||||
has minimum balance of:
|
||||
`per-transaction base fees` +
|
||||
`prioritization fees` +
|
||||
When the cluster receives a new transaction, `PayApplicationFees` instruction is
|
||||
decoded to calculate the total fees required for the transaction message. Then
|
||||
we verify that the fee-payer has a minimum balance of:
|
||||
`per-transaction base fees` + `prioritization fees` +
|
||||
`maximum application fees to be paid`
|
||||
|
||||
If the payer has a sufficient balance then we continue loading other accounts.
|
||||
If `PayApplicationFees` is missing then application fees is 0. If payer has
|
||||
insufficient balance transaction fails with error `Insufficient Balance`.
|
||||
If the payer does not have enough balance, the transaction is not scheduled
|
||||
and fails with an error `InsufficientFunds`. Consider this case the same as if
|
||||
the payer does not have enough funds to pay `other fees`.
|
||||
|
||||
#### Changes in invoke context
|
||||
Before processing the message, we check if any loaded account has associated
|
||||
application fees. For all accounts with application fees, the fees paid should
|
||||
be greater than the required fees. In case of overpayment, the difference is
|
||||
stored in a variable and will be paid back to the payer in any case. In case the
|
||||
application fees are insufficiently paid or not paid then the transaction fails
|
||||
with an error `ApplicationFeesNotPaid`. The payer will pay just `other fees`.
|
||||
This failure is before the execution of the transaction.
|
||||
|
||||
The structure `invoke context` is passed to all the native solana program
|
||||
while execution. We create a new structure called `ApplicationFeeChanges`
|
||||
which contains one hashmap mapping application fees (`Pubkey` ->
|
||||
`application fees(u64)`), and another containing rebates (`Pubkey` ->
|
||||
`amount rebated (u64)`). The `ApplicationFeeChanges` structure will be filled
|
||||
by iterating over all accounts and finding which accounts require application
|
||||
fees.
|
||||
The application fees minus rebates and minus overpayment are transferred to
|
||||
the respecitve accounts lamport balance, from where the owner can collect them.
|
||||
|
||||
```rust
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Default)]
|
||||
pub struct ApplicationFeeChanges {
|
||||
/// To store application fees by account
|
||||
pub application_fees: HashMap<Pubkey, u64>,
|
||||
/// to store rebates by account
|
||||
pub rebated: HashMap<Pubkey, u64>,
|
||||
}
|
||||
```
|
||||
### Configuration
|
||||
|
||||
Accounts having application fees will be added in the `application_fees` field
|
||||
of this structure with corresponding application fees. On each `Rebate`
|
||||
instruction we find the minimum between `application_fees` and the rebate
|
||||
amount for the account. If there is already a rebate in the `rebated` map then
|
||||
we update it by `max(rebate amount in map, rebate amount in instruction)`, if
|
||||
the map does not have any value for the account, then we add the rebated
|
||||
amount in the map.
|
||||
Application fees should be considered as set and forget kind of fees by dapp
|
||||
developers. They are not aimed to control congestion over the network instead
|
||||
they aim to reduce spamming.
|
||||
|
||||
In verify stage we verify that `Application Fees` >= `Rebates` +
|
||||
`Overpaid amount` for each account and return `UnbalancedInstruction` on
|
||||
failure.
|
||||
The ledger will need to store the amount of lamports required for every account.
|
||||
Hence changes should be constrained like any account write to the owner of the
|
||||
account. All programs will need to implement special instructions so that the
|
||||
authority of the accounts can sign to update application fees on the accounts.
|
||||
|
||||
#### Changes in Bank
|
||||
Setting the amount to `0` disables application fees. The maximum amount of
|
||||
application fees that can be set for an account will be limited to a constant
|
||||
amount of SOL (e.g. 20 SOL) so that accounts cannot become inaccessible through
|
||||
simple programming errors.
|
||||
|
||||
In method `filter_program_errors_and_collect_fee` we will add logic to deposit
|
||||
application fees to the respective mutable accounts and to reimburse the
|
||||
rebates to the payer in case the transaction was successful. If the
|
||||
transaction fails then we withdraw
|
||||
`per-transaction base fees` +
|
||||
`prioritization fees` +
|
||||
`sum of application fees on all accounts`
|
||||
from the payer. We return the overpaid amount in case the transaction was
|
||||
successful or unsuccessful. We also transfer the application fees to the
|
||||
respective accounts at this stage.
|
||||
### Rebate
|
||||
|
||||
The owner of the account can issue a rebate of the application fees paid to
|
||||
write lock a specific account. Similar to the configuration this will need to be
|
||||
exposed through special instructions so that programs can implement custom
|
||||
authorization and delegate the decision to composing programs.
|
||||
|
||||
Simple rebate schemes will verify merely signers, e.g an oracle preventing 3rd
|
||||
parties from write-locking their price feed. Dexes will need more complex rebate
|
||||
schemes based on instruction sysvar introspection.
|
||||
|
||||
Rebate takes the amount of lamports to be rebated, and account on which rebate
|
||||
is issued as input. In case of multiple rebates from the same account only the
|
||||
highest amount of rebate will be taken into account. The rebated amount is
|
||||
always the minimum of rebate issued by the program and the application fees on
|
||||
the account. If program rebates `U64::MAX` it means all the application fees on
|
||||
the account are rebated. The rebate amount cannot be negative.
|
||||
|
||||
### Looking at common cases
|
||||
|
||||
#### No application fees enabled
|
||||
|
||||
* A payer does not include `PayApplicationFees` in the transaction. The
|
||||
transaction does not write lock any accounts with application fees. Then the
|
||||
transaction is executed without the application fee feature. The payer ends up
|
||||
paying other fees.
|
||||
|
||||
* A payer includes `PayApplicationFees(app fees)` in the transaction but none of
|
||||
the accounts have any application fees. This case is considered an overpay
|
||||
case. The payer balance is checked for `other fees + app fees`.
|
||||
1. The payer does not have enough balance: Transaction fails with an error
|
||||
`Insufficient Balance` and the transaction is not even scheduled for
|
||||
execution.
|
||||
2. The payer has enough balance then the transaction is executed and application
|
||||
fees paid are transferred back to the payer in any case.
|
||||
|
||||
Note in this case
|
||||
even if there are no application fees involved the payer balance is checked
|
||||
against application fees.
|
||||
|
||||
#### Application fees are enabled
|
||||
|
||||
* Fees not paid case:
|
||||
|
||||
A payer does not include `PayApplicationFees` in the transaction. The
|
||||
transaction includes one or more accounts with application fees. Then the
|
||||
transaction is failed with an error `ApplicationFeesNotPaid`. The program is
|
||||
not executed at all. The payer ends up paying only other fees.
|
||||
|
||||
* Fees paid no rebates case:
|
||||
|
||||
A payer includes instruction `PayApplicationFees(100)` in the transaction.
|
||||
There is an account `accA` which is write-locked by the transaction and it has
|
||||
an application fee of `100` lamports. Consider that the program does not have
|
||||
any rebate mechanism. Then in any case (execution fails or succeeds) `accA`
|
||||
will receive `100` lamports. The payer will end up paying `other fees` + `100`
|
||||
lamports.
|
||||
|
||||
* Fees paid full rebates case:
|
||||
|
||||
A payer includes instruction `PayApplicationFees(100)` in the transaction.
|
||||
There is an accounts `accA` which is write-locked by the transaction and it
|
||||
has an application fee of `100` lamports. Consider during execution the
|
||||
program will rebate the application fee on the account. Then payer should have
|
||||
a minimum balance of `other fees` + `100` lamports to execute the transaction.
|
||||
After successful execution of the transaction, the `100` lamports will be
|
||||
rebated by the program and then Solana runtime will transfer them back to the
|
||||
payer. So the payer will finally end up paying `other fees` only.
|
||||
|
||||
* Fees paid multiple partial rebates case:
|
||||
|
||||
A payer includes instruction `PayApplicationFees(200)` in the transaction. The
|
||||
transaction has three instructions (`Ix1`, `Ix2`, `Ix3`). There are accounts
|
||||
(`accA`, `accB`) that are write-locked by the transaction and each of them has
|
||||
an application fee of `100` lamports. Lets consider `Ix1` rebates 25 lamports
|
||||
on both accounts, `Ix2` rebates 75 lamports on `accA` and `Ix3` rebates 10
|
||||
lamports on `accB`. In the case of multiple rebates only the maximum of all
|
||||
the rebates is applied. Consider the transaction is executed successfully. The
|
||||
maximum of all the rebates for `accA` is 75 lamports and `accB` is 25
|
||||
lamports. So a total of 100 lamports are rebated back to the payer, `accA`
|
||||
gets 25 lamports and `accB` gets 75 lamports. The payer will end up paying
|
||||
`other fees` + `100` lamports.
|
||||
|
||||
* Fees paid full rebates but the execution failed case:
|
||||
|
||||
A payer includes instruction `PayApplicationFees(100)` in the transaction.
|
||||
There is an account `accA` which is write-locked by the transaction and it has
|
||||
an application fee of `100` lamports. Consider during execution the program
|
||||
will rebate all the application fees on the account but later the execution
|
||||
failed. Then payer should have a minimum balance of `other fees` + `100`
|
||||
lamports to execute the transaction. The program rebated application fees but
|
||||
as executing the transaction failed, no rebate will be issued. The application
|
||||
fees will be transferred to respective accounts, and the payer will finally
|
||||
end up paying `other fees` + `100` lamports as application fees.
|
||||
|
||||
* Fees are over paid case:
|
||||
|
||||
A payer includes instruction `PayApplicationFees(1000)` in the transaction.
|
||||
There is an account `accA` that is write-locked by the transaction and it has
|
||||
an application fee of `100` lamports. The minimum balance required by payer
|
||||
will be `other fees` + `1000` lamports as application fees. So the payer pays
|
||||
100 lamports for the account as application fees and 900 lamports is an
|
||||
overpayment. The 900 lamports will be transferred back to the user even if the
|
||||
transaction succeeds or fails. The 100 lamports will be transferred to `accA`
|
||||
in all cases except if the transaction is successful and the program issued
|
||||
a rebate.
|
||||
|
||||
* Fees underpaid case:
|
||||
|
||||
A payer includes instruction `PayApplicationFees(150)` in the transaction.
|
||||
There is an accounts `accA` that is write-locked by the transaction and it has
|
||||
an application fees of `300` lamports. The minimum balance required by payer
|
||||
will be `other fees` + `150` lamports as application fees to load accounts and
|
||||
schedule transactions for execution. Here payer has insufficiently paid the
|
||||
application fees paying 150 lamports instead of 300 lamports. So before
|
||||
program execution, we detect that the application fees are not sufficiently
|
||||
paid and execution fails with the error `ApplicationFeesNotPaid` and the
|
||||
partially paid amount is transferred back to the payer. So the payer pays only
|
||||
`base fees` in the end but the transaction is unsuccessful.
|
||||
|
||||
## Impact
|
||||
|
||||
Overall this feature will incentivize the creation of proper transactions and
|
||||
spammers would have to pay much more fees reducing congestion in the cluster.
|
||||
This will add very low calculation overhead on the validators. It will also
|
||||
enable users to protect their accounts against malicious read and write locks.
|
||||
This feature will encourage everyone to write better-quality code to help
|
||||
avoid congestion.
|
||||
|
||||
It is the DApp's responsibility to publish the application fee required for each
|
||||
account and instruction. They should also add appropriate `PayApplicationFee`
|
||||
instruction in their client library while creating transactions or provide
|
||||
visible API to get these application fees. We expect these fees to be set and
|
||||
forget kind of fees and do not expect them to be changed frequently. Some
|
||||
changes have to be done in web3.js client library to get application fees when
|
||||
we request the account. Additional instructions should be added to the known
|
||||
programs like Token Program, to enable this feature on the TokenAccounts. The
|
||||
DApp developer have to also take into account application fees on the programs
|
||||
they are dependent on.
|
||||
|
||||
The cluster is currently vulnerable to a different kind of attack where an
|
||||
adversary with malicious intent can block its competitors by writing or
|
||||
read-locking their accounts through a transaction. This attack involves
|
||||
carrying out intensive calculations that consume a large number of
|
||||
computational units, thereby blocking competitors from performing MEV during
|
||||
that particular block, and giving the attacker an unfair advantage. We have
|
||||
identified specific transactions that perpetrate this attack and waste
|
||||
valuable block space. The malicious transaction write locks multiple token
|
||||
accounts and consumes 11.7 million CU i.e around 1/4 the block space. As a
|
||||
result, such attacks can prevent users from using their token accounts,
|
||||
vote accounts, or stake accounts, and dapps from utilizing the required
|
||||
accounts. With the proposed solution, every program, such as the token
|
||||
program, stake program, and vote program, can include instructions to employ
|
||||
the application fees feature on their accounts and rebate the fees if the user
|
||||
initiates the transaction. The attacker will find this option unfeasible as
|
||||
they will consume their SOL tokens more rapidly to maintain the attack.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
If the application fee for an account is set too high then we cannot ever
|
||||
mutate that account anymore. Even updating the application fees for the
|
||||
account will need a very high amount of balance. This issue can be easily
|
||||
solved by setting a maximum limit to the application fees.
|
||||
|
||||
For an account that has collected application fees, to transfer these fees
|
||||
collected to another account we have to pay application fees to write lock the
|
||||
account, we can include a rebate in the transaction. In case of
|
||||
any bugs, while transferring application fees from the account to the
|
||||
authority, there can be an endless loop where the authority creates a
|
||||
transaction to recover collected application fees, with an instruction to pay
|
||||
application fees to modify the account and an instruction to rebate. If the
|
||||
transaction fails because of the bug, the user fails to recover collected
|
||||
fees, in turn increasing application fees collected on the account.
|
||||
|
||||
## Backwards Compatibility
|
||||
|
||||
This feature does not introduce any breaking changes. The transaction without
|
||||
using this feature should work as it is. To use this feature supermajority of
|
||||
the validators should move to a branch that implements this feature.
|
||||
Validators that do not implement this feature cannot replay the blocks with
|
||||
transactions using application fees they also could not validate the block
|
||||
including transactions with application fees.
|
||||
|
||||
|
||||
## Additional Notes
|
||||
|
||||
If the dapps want to rebate application fees they have to implement very
|
||||
carefully the logic of rebate. They should be very meticulous before calling
|
||||
rebate so that a malicious user could not use this feature to bypass
|
||||
|
@ -448,20 +373,6 @@ they use IxB to issue a rebate for the application fees. So DApp developers
|
|||
have to be sure when to do rebates usually white listing and black listing
|
||||
instruction sequence would be ideal.
|
||||
|
||||
A dapp can break the cpi interface with other dapps if it implements this
|
||||
feature. This is because it will require an additional account for the
|
||||
application fees program to all the instructions which calls `Rebate`. The
|
||||
client library also has to add the correct amount of fees in instruction
|
||||
`PayApplicationFees` while creating the transaction.
|
||||
|
||||
It is the DApp's responsibility to publish the application fee required for
|
||||
each account and instruction. They should also add appropriate
|
||||
`PayApplicationFee` instruction in their client library while creating
|
||||
transactions or provide visible API to get these application fees. As DApp has
|
||||
to be redeployed to change application fees, we do not expect to change it
|
||||
often. We can also add additional methods in JsonRpc and solana web3 library
|
||||
to get application fees for an account.
|
||||
|
||||
Dapp should be careful before rolling out this feature. Because the
|
||||
transaction would start failing if the rollout is sudden. It is preferable to
|
||||
implement rebate, and add pay application fees in their APIs, so that the user
|
||||
|
@ -469,27 +380,14 @@ pays full application fee but is then rebated if the transaction succeeds.
|
|||
Then once everyone starts using the new API they can add the check on
|
||||
application fees.
|
||||
|
||||
Additional instructions should be added to the known programs like Token
|
||||
Program, Vote Program, Stake Program, etc. to enable this feature on the
|
||||
TokenAccount, VoteAccount, and other accounts. Take an example of the token
|
||||
program; we can let the user change application fees to its token account. For
|
||||
that, we have to add an instruction `UpdateApplicationFees` instruction to the
|
||||
token program, which will cpi `UpdateApplicationFees` for the token account
|
||||
PDA. Same goes for `Rebate.` In this way, any malicious access to the user's
|
||||
token account will fail without paying application fees. These token program
|
||||
also have to find a way for a user to get back the application fees collected
|
||||
on their accounts.
|
||||
Another proposal will also introduce protection against unwanted read-locking
|
||||
of the accounts. Many accounts like token account rarely need to be read-locked
|
||||
this proposal will force these accounts to be write-locked instead and pay
|
||||
application fees if needed. This feature is out of the scope of this proposal.
|
||||
|
||||
Overall this feature will incentivize the creation of proper transactions and
|
||||
spammers would have to pay much more fees reducing congestion in the cluster.
|
||||
This will add very low calculation overhead on the validators. It will also
|
||||
enable users to protect their accounts against malicious read and write locks.
|
||||
This feature will encourage everyone to write better-quality code to help
|
||||
avoid congestion.
|
||||
### Calculating Application Fees for a dapp
|
||||
|
||||
## Calculating Application Fees for a dapp
|
||||
|
||||
Lets consider setting application fees for Openbook DEX. We can set fees
|
||||
Let us consider setting application fees for Openbook DEX. We can set fees
|
||||
comparable to the rent of the accounts involved or something fixed. Setting
|
||||
application fees too high means dapp users need more balance to interact with
|
||||
the dapps and if they are too low then it won't prevent spamming or malicious
|
||||
|
@ -508,48 +406,14 @@ fees are rebated. This will make spammers spend their SOLs 2000 times more
|
|||
rapidly than before. The thumb rule for dapps to set application fees on their
|
||||
accounts is `More important the account = Higher application fees`.
|
||||
|
||||
Suppose a user or entity desires to safeguard their token account from
|
||||
potentially malicious read/write locking that obstructs their ability to
|
||||
perform MEV. In that case, they can set the highest possible application fees
|
||||
on their token account, rendering it impossible to extract profit by blocking
|
||||
their account. Even if the transaction fails, and they have to pay the
|
||||
application fees, they can recover the fees as they own the account. A general
|
||||
guideline for users is that they should possess at least N times (where N=10
|
||||
is recommended) the SOLs required to pay application fees on their accounts so
|
||||
that they are not locked out. The user has to pay application fees to transfer
|
||||
all collected application fees.
|
||||
### Pyth Usecase
|
||||
|
||||
## Security Considerations
|
||||
Pyth's price feeds currently serve around 33M CU peak read load. An attacker
|
||||
could write lock those for 12M CU and cause scheduling issues by crowding out
|
||||
the majority of the block. A high application fee of 10 SOL could prevent anyone
|
||||
except price feed publishers from write locking a price feed account.
|
||||
|
||||
If the application fee for an account is set too high then we cannot ever
|
||||
mutate that account anymore. Even updating the application fees for the
|
||||
account will need a very high amount of balance. This issue can be easily
|
||||
solved by setting a maximum limit to the application fees. We suggest setting
|
||||
this limit to 100 SOLs.
|
||||
|
||||
For a dapp it is better to set very low application fees at the beginning so
|
||||
that it could be rolled out easier.
|
||||
|
||||
For an account that has collected application fees, to transfer these fees
|
||||
collected to another account we have to pay application fees to write lock the
|
||||
account, we can include a rebate instruction in the transaction. In case of
|
||||
any bugs, while transferring application fees from the account to the
|
||||
authority, there can be an endless loop where the authority creates a
|
||||
transaction to recover collected application fees, with an instruction to pay
|
||||
application fees to modify the account and an instruction to rebate. If the
|
||||
transaction fails because of the bug, the user fails to recover collected
|
||||
fees, inturn increasing application fees collected on the account.
|
||||
|
||||
## Backwards Compatibility
|
||||
|
||||
This feature does not introduce any breaking changes. The transaction without
|
||||
using this feature should work as it is. To use this feature supermajority of
|
||||
the validators should move to a branch that implements this feature.
|
||||
Validators that do not implement this feature cannot replay the blocks with
|
||||
transactions using application fees they also could not validate the block
|
||||
including transactions with application fees.
|
||||
|
||||
## Mango V4 Usecase
|
||||
### Mango V4 Usecase
|
||||
|
||||
With this feature implemented Mango-V4 will be able to charge users who spam
|
||||
risk-free arbitrage or spam liquidations by increasing application fees on
|
||||
|
|
Loading…
Reference in New Issue