Merge pull request #5 from godmodegalactus/application_fees_mvines_comments

nit based on comments
This commit is contained in:
galactus 2023-05-16 22:32:58 +02:00 committed by GitHub
commit cdd91d295d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 566 additions and 438 deletions

View File

@ -1,438 +0,0 @@
---
simd: '0016'
title: Application Fees
authors:
- Godmode Galactus (Mango Markets)
- Maximilian Schneider (Mango Markets)
category: Standard
type: Core, Fees
status: Draft
created: 2022-12-23
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 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>
## Motivation
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. With current low
cluster fees, it incentivizes spammers to spam the network with transactions.
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.
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. Penalizes everyone for a few bad actors.
* Passing application fees for each account by instruction and validating them
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 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. The program can decide to rebate these fees back
to the user if certain conditions decided by dapp developers are met.
### 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
(i.e, keypair) owner is a `system program` and in case of a Program derived
address owner is usually the program.
Account `Authority`: The true authority for the account. Let's take an example
of associated token account. A associated token account is a `PDA` derived
from associated token program, where the owner is token program. But the
program internally saves who has the authority over the account. Only token
program can change the data of a token account. For operations like
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
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.
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.
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.
### Payment
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 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`.
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 application fees minus rebates and minus overpayment are transferred to
the respecitve accounts lamport balance, from where the owner can collect them.
### Configuration
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.
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.
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.
### 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
application fees. Dapp developers also have to implement additional
instruction to collect these fees using lamport transfers.
DApp developers have to consider the following way to bypass application fees
is possible: A Defi smart contract with two instructions IxA and IxB. Both IxA
and IxB issue a rebate. IxA is an instruction that places an order on the
market which can be used to extract profit. IxB is an instruction that just
does some bookkeeping like settling funds overall harmless instruction.
Malicious users then can create a custom smart contract to bypass the
application fees where it CPI's IxA only if they can extract profit or else
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.
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
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.
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.
### Calculating Application Fees for a dapp
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
use. In case of openbook the intent is to avoid spamming.
Most of the OpenBook accounts like asks, bids and event queues are used in
write mode only we can disable read-locks on these accounts. Now we can
consider there are 48 M CU's per block and 2.5 blocks per second. Considering
each instruction takes 200 CUs so around 600 transactions per second.
Currently, base fees are around 0.00005 SOLs, with so low gas fees spammers
have the advantage to spam the cluster. A reasonable application fee could be
around 0.1 SOLs per transaction that could be distributed among different
accounts. For a user interacting with dapp with 0.1 SOLs in the account seems
reasonable assuming that the transactions are executed successfully and the
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`.
### Pyth Usecase
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.
### 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
perp-markets, token banks and mango-user accounts.
#### Perp markets
Application fees on perp liquidations, perp place order, perp cancel, perp
consume, perp settle fees. Rebates on: successful liquidations, consume
events, HFT market making refresh (cancel all, N* place POST, no IOC).
#### Token vaults
Application fees on open order book liquidations, deposit, withdrawals. Rebate
on successful liquidations, place IOC & fill in isolation, HFT marketmaking
refresh (cancel all, N* place POST, no IOC).
#### Mango accounts
Application fees on all liquidity transactions, consume events, settle pnl,
all user signed transactions. Rebate on transaction signed by owner or
delegate, successful liquidations, settlements, consume events.

View File

@ -0,0 +1,566 @@
---
simd: '0016'
title: Program Rebatable Account Write Fees
authors:
- Godmode Galactus (Mango Markets)
- Maximilian Schneider (Mango Markets)
category: Standard
type: Core, Fees
status: Draft
created: 2022-12-23
feature:
---
## Summary
This SIMD will discuss additional fees called Program Rebatable Account Write
Fees or PRAW 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 are
collected on the account on which they were set. These fees would be only
applied if the account is write locked by the transaction. 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 PRAW fees to its users.
These fees will depend on the CU requested by the transaction.
## Motivation
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 markets 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 dapps 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, 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. With current low cluster fees, it incentivizes spammers
to spam the network with transactions.
Without any proper rebate mechanism for users, Solana 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.
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. Penalizes everyone for a few bad actors.
* Passing fees for each account by instruction and validating them
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 that do not call into the respective program
## New Terminology
Program Rebatable Account Write Fees or PRAW 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.
Millilamports: 1/1000 lamports or 10^-3 lamports. PRAW fees are set and used in
milli-lamports per CU.
### Other terminology
Account `Authority`: The true authority for the account. Let's take an example
of associated token account. A associated token account is a `PDA` derived from
associated token program, where the owner is token program. But the program
internally saves who has the authority over the account. Only token program can
change the data of a token account. For operations like 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 PRAW fees `base fees + prioritization fees`.
## Detailed Design
PRAW 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.
The checks on the PRAW fees will be taken care of by Solana runtime. Total PRAW
fees paid will be included in the transaction through a special
`SetProgramRebatableAccountWriteFees` 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.
Addition of new syscalls to get, set and rebate PRAW fees during program
runtime. Application developers will be able to rebate these fees through the
new syscall, but only if the transaction succeeds, this way transactions
write-locking the respective accounts will require full fee payment before
program is executed.
Once PRAW fees are paid on an account by the payer they are valid for the whole
transaction, even if the same account is write-locked in multiple instructions.
### Payment
`SetProgramRebatableAccountWriteFees` will be part of the updated compute budget
program. `SetProgramRebatableAccountWriteFees` will take a u64 parameter to set
millilamports per requested CU as the PRAW fees. Instead of having separate
instructions for each compute budget parameter we will have a single instruction
that will set all the compute budget parameters. Compute budget parameters could
be considered as transaction header that sets some parameters for the
transaction. We create a new instruction that takes a vector of parameters. If
similar transaction header parameters appear multiple times then we just take
the first one and ignore the other occurrences. In case of multiple transaction
headers in the transaction, we take the first occurrence and ignore the others.
```
enum TransactionHeaderParameter {
// existing compute budget parameters
SetComputeUnitLimit(u32),
SetComputeUnitPrice(u64),
SetLoadedAccountsDataSizeLimit(u32),
// Limit on maximum of PRAW fees to be paid
// Fees is set in milli lamports per CU
SetProgramRebatableAccountWriteFees(u64),
}
enum TransactionHeaderInstruction {
parameter: Vec<TransactionHeaderParameter>
}
```
When the cluster receives a new transaction, with `TransactionHeaderParameter`
instruction is decoded to get the `SetProgramRebatableAccountWriteFees` and then
calculates 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` +
`Millilamports per CU set by SetProgramRebatableAccountWriteFees` *
`requested CUs set by SetComputeUnitLimit`.
Where PRAW fees is :
`Millilamports per CU set by SetProgramRebatableAccountWriteFees` *
`requested CUs set by SetComputeUnitLimit`.
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`.
Before processing the message, we check if any loaded account has associated
PRAW fees. For all accounts with PRAW 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 PRAW fees
are insufficiently paid or not paid then the transaction fails with an error
`ProgramRebatableAccountWriteFeesNotPaid`. The payer will pay just `other fees`.
This failure is before the execution of the transaction.
The PRAW fees minus rebates and minus overpayment are transferred to the
respective accounts lamport balance, from where the owner can collect them and
then the rest would be transferred back to the payer.
### Syscall to change PRAW fees
PRAW 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.
A new syscall will be added to update the PRAW fees:
```
fn update_program_rebatable_account_write_fees(
account: Pubkey,
milli_lamports_per_cu: u32
) -> bool;
```
The syscall will take as arguments the account on which fee should be updated
and the millilamports per cu that should be charged and return a true if the
changes were successful else will return false.
The accounts database will need to store the value of millilamports per
requested CU for the account. Changes to update the PRAW fees could be done only
by the authority program. All programs will need to implement special
instructions so that the program admin can use this syscall to update PRAW fees
on the required accounts. Program admin should always be signer for these
instructions.
Setting the millilamports per CU to `0` disables PRAW fees. The maximum amount
of millilamports per CU that could be set will be u32::MAX. Lets assume that the
maximum CU required to update these fees on an account is 1,000 CU. So the
maximum PRAW fees required to update an account (`M`) is
```
M = u32::MAX * 1000 CU * (1/1000) lamports
M = 4.294967296×10^9 * 1000 / 1000
M = 4.294967296 SOLs
```
So this makes sure that admin does not lock itself out from changing the PRAW
fees.
### Syscall to read PRAW fees
A new syscall will be added to read PRAW fees on an account.
```
fn get_program_rebatable_account_write_fees(
account: Pubkey
) -> u32;
```
The syscall will take writable account as import and return millilamports per CU.
### Syscall to Rebate
The owner of the account can issue a rebate of the PRAW fees paid to write lock
a specific account. Similar to the update and read PRAW fees this will need to
be exposed through a syscall 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.
Syscall definition of the rebate mechanism is:
```
fn rebate_program_rebatable_account_write_fees(
account: Pubkey,
milli_lamports_per_cu: u32
) -> bool;
```
Rebate takes account on which rebate is issued and the amount of millilamports
per CU that needs to be rebated as input. It returns true of rebate was
successful false otherwise. In case of multiple rebates on 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 PRAW fees on the
account. If program rebates `U64::MAX` it means all the PRAW fees on the account
are rebated. The rebate amount cannot be negative.
### Calculation of PRAW Fees and rebates.
User creates a transaction by setting maximum PRAW fees in terms of
millilamports per requested CUs (`m`) and requested CUs (`C`). Solana runtime
will multiply `m` and `C` to calculate total maximum PRAW fees (`M`) payer is
willing to pay, then check if user has enough balance to pay all the fees
including PRAW fees then it will load the accounts required by the transaction.
Then we iterate on accounts to calculate total PRAW fees required (`T`).
Let (`Pi`) be PRAW fee set for ith account.
Let number of accounts loaded be (`L`)
```
T = sum(Pi * C) for i in 0 to L.
```
If M>=T we continue to execute else the transaction fails with an error
`ProgramRebatableAccountWriteFeesNotPaid`.
Let Overpaid amount (`O = M - T`)
After execution, we iterate on the rebates issued to calculate total rebated
amount (`R`), For an account there could be multiple rebates during a
transaction. Consider that there are `N` rebates on ith account. Each rebate
will be represented as `Rin`.
```
R = Sum( min(Pi, max(Rin where n=0 to N)) * C )
```
We then transfer `(Pi - Ri) * C` into each writable account and `O + R` back to
the payer.
### Looking at common cases
For simplicity, we have written examples in total PRAW fees instead of
millilamports per requested CUs.
#### No PRAW fees enabled
* A payer does not include `SetProgramRebatableAccountWriteFees` in the
transaction. The transaction does not write lock any accounts with PRAW fees.
Then the transaction is executed without the PRAW fee feature. The payer ends
up paying other fees.
* A payer includes `SetProgramRebatableAccountWriteFees(PRAW fees)` in the
transaction but none of the accounts have any PRAW fees. This case is
considered an overpay case. The payer balance is checked for
`other fees + PRAW 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 PRAW
fees paid are transferred back to the payer in any case.
Note in this case
even if there are no PRAW fees involved the payer balance is checked
against PRAW fees.
#### PRAW fees are enabled
* Fees not paid case:
A payer does not include `SetProgramRebatableAccountWriteFees` in the
transaction. The transaction includes one or more accounts with PRAW fees.
Then the transaction is failed with an error
`ProgramRebatableAccountWriteFeesNotPaid`. The program is not executed at all.
The payer ends up paying only other fees.
* Fees paid no rebates case:
A payer includes instruction `SetProgramRebatableAccountWriteFees(100)` in the
transaction. There is an account `accA` which is write-locked by the
transaction and it has an PRAW 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 `SetProgramRebatableAccountWriteFees(100)` in the
transaction. There is an accounts `accA` which is write-locked by the
transaction and it has an PRAW fees fee of `100` lamports. Consider during
execution the program will rebate the PRAW fees 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 `SetProgramRebatableAccountWriteFees(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 PRAW fees 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 `SetProgramRebatableAccountWriteFees(100)` in the
transaction. There is an account `accA` which is write-locked by the
transaction and it has an PRAW fees fee of `100` lamports. Consider during
execution the program will rebate all the PRAW 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
PRAW fees but as executing the transaction failed, no rebate will be issued.
The PRAW fees fees will be transferred to respective accounts, and the payer
will finally end up paying `other fees` + `100` lamports as PRAW fees.
* Fees are over paid case:
A payer includes instruction `SetProgramRebatableAccountWriteFees(1000)` in
the transaction. There is an account `accA` that is write-locked by the
transaction and it has an PRAW fees fee of `100` lamports. The minimum balance
required by payer will be `other fees` + `1000` lamports as PRAW fees. So the
payer pays 100 lamports for the account as PRAW 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 `SetProgramRebatableAccountWriteFees(150)` in the
transaction. There is an accounts `accA` that is write-locked by the
transaction and it has an PRAW fees of `300` lamports. The minimum balance
required by payer will be `other fees` + `150` lamports as PRAW fees to load
accounts and schedule transactions for execution. Here payer has
insufficiently paid the PRAW fees paying 150 lamports instead of 300 lamports.
So before program execution, we detect that the PRAW fees are not sufficiently
paid and execution fails with the error
`ProgramRebatableAccountWriteFeesNotPaid` 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 PRAW fees fee required for each
account and instruction. They should also add appropriate
`SetProgramRebatableAccountWriteFees` instruction in their client library while
creating transactions or provide visible API to get these PRAW 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 PRAW 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 PRAW 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 PRAW 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 PRAW fees fee for an account is set too high then we cannot ever mutate
that account anymore. Maximum fees can be set to around 4.2949 * 10^9 milli
lamports per CU, assuming that instruction that updates the PRAW fees on the
accounts will use around 1000 CUs, program owner will need roughly 4.2949 SOLs
to reupdate the PRAW fees. Optimizing the instruction to update the PRAW fees is
a must for dapp developers.
For an account that has collected PRAW fees, to transfer these fees
collected to another account we have to pay PRAW fees to write lock the
account, we can include a rebate in the transaction. In case of
any bugs, while transferring PRAW fees from the account to the
authority, there can be an endless loop where the authority creates a
transaction to recover collected PRAW fees, with an instruction to pay
PRAW 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 PRAW 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 PRAW fees they also could not validate the block including transactions
with PRAW fees.
## Additional Notes
If the dapps want to rebate PRAW 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
PRAW fees. Dapp developers also have to implement additional
instruction to collect these fees using lamport transfers.
Dapp developers have to consider the following way to bypass PRAW fees
is possible: A Defi smart contract with two instructions IxA and IxB. Both IxA
and IxB issue a rebate. IxA is an instruction that places an order on the
market which can be used to extract profit. IxB is an instruction that just
does some bookkeeping like settling funds overall harmless instruction.
Malicious users then can create a custom smart contract to bypass the
PRAW fees where it CPI's IxA only if they can extract profit or else
they use IxB to issue a rebate for the PRAW fees. So dapp developers
have to be sure when to do rebates usually white listing and black listing
instruction sequence would be ideal.
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 PRAW fees in their APIs, so that the user
pays full PRAW fees fee but is then rebated if the transaction succeeds.
Then once everyone starts using the new API they can add the check on
PRAW fees.
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
PRAW fees if needed. This feature is out of the scope of this proposal.
### Calculating PRAW fees for a dapp
Let us consider setting PRAW fees for Openbook DEX. We can set fees
comparable to the rent of the accounts involved or something fixed. Setting
PRAW 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
use. In case of openbook the intent is to avoid spamming.
Most of the OpenBook accounts like asks, bids and event queues are used in write
mode only we can disable read-locks on these accounts. Now we can consider there
are 48 M CUs per block and 2.5 blocks per second. Considering each instruction
takes 200K CUs so around 600 transactions per second. Currently, base fees are
around 0.00005 SOLs, with so low gas fees spammers have the advantage to spam
the cluster. A reasonable PRAW fees fee could be around 0.1 SOLs per transaction
that could be distributed among different accounts. For a user interacting with
dapp with 0.1 SOLs in the account seems reasonable assuming that the
transactions are executed successfully and the fees are rebated. This will make
spammers spend their SOLs 2000 times more rapidly than before. The thumb rule
for dapps to set PRAW fees on their accounts is
`More important the account = Higher PRAW fees`.
### Pyth Usecase
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 PRAW fees fee of u32::MAX could prevent anyone
except price feed publishers from write-locking a price feed account.
To effectively block DeFi protocols from using Pyth oracles a malicious attacker
needs to write lock Pyth oracles with a large amount of CUs. With current limits
of CUs per writable account of around 10 million CUs the attacker has to spend
around 4294 SOLs to block PYTH oracle for a block.
### 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 PRAW fees on
perp-markets, token banks and mango-user accounts.
#### Perp markets
PRAW fees on perp liquidations, perp place order, perp cancel, perp
consume, perp settle fees. Rebates on: successful liquidations, consume
events, HFT market making refresh (cancel all, N* place POST, no IOC).
#### Token vaults
PRAW fees on open order book liquidations, deposit, withdrawals. Rebate
on successful liquidations, place IOC & fill in isolation, HFT marketmaking
refresh (cancel all, N* place POST, no IOC).
#### Mango accounts
PRAW fees on all liquidity transactions, consume events, settle pnl,
all user signed transactions. Rebate on transaction signed by owner or
delegate, successful liquidations, settlements, consume events.