rewriting proposal with examples

This commit is contained in:
Godmode Galactus 2023-04-25 10:21:41 +02:00
parent 3680b81cdf
commit e30cf9bb4f
No known key found for this signature in database
GPG Key ID: A04142C71ABB0DEA
1 changed files with 289 additions and 425 deletions

View File

@ -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