Rewriting the proposal using only implementation 3

This commit is contained in:
Godmode Galactus 2023-02-16 20:32:45 +01:00
parent 98a73eb308
commit d7e201c928
No known key found for this signature in database
GPG Key ID: A04142C71ABB0DEA
1 changed files with 126 additions and 203 deletions

View File

@ -15,12 +15,13 @@ feature:
## Summary
This SIMD will discuss additional fees called Application Fees or write lock fees.
These fees are decided and set by the dapp developer to interact with the dapp usually by
write-locking one of the accounts. 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
interact with the app as intentended. These fees will be applied even if the transaction
eventually fails. These fees will be collected on the same writable account and the
authority (i.e owner of the account) can do lamport transfers to recover these 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.
Account authority (i.e 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.
Discussion for the issue : <https://github.com/solana-labs/solana/issues/21883>
@ -33,6 +34,7 @@ transactions belonging to a single batch are processed correctly and all others
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
@ -42,22 +44,25 @@ incentive to artificially delay HFT transactions, cause them to fail, and charge
## Alternatives Considered
Having a fixed write lock fee and a read lock fee.
* 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.
* Extend existing account structure in ledger to store fee settings and collect lamports.
Pros: High efficiency, minimal performance impact, no need to add additional instruction in transaction,
helps to avoid denial of service attack on the smart contract.
Cons: Modification of account structure which impacts a lot of code. Needs to load all accounts to calculate
total fees to charge the payer, to avoid that have to implement multiple level of fees.
## New Terminology
Application Fees: Fees that are decided by the dapp developer that will be charged if a user
successfully write locks an account.
Base Fee Surcharge: An extra fee is applied depending on the number of accounts that were loaded for
a transaction. This fee is only charged after loading multiple accounts and then failing because
the payer did not have enough lamports to pay application fees or the limit set by `LimitApplicationFees`
instruction is not enough described below.
Application Fees: Fees that are decided by the dapp developer that will be charged if a user wants
to use the dapp. They will be applied even if transaction fails contrary to lamport transfers.
## Detailed Design
@ -66,103 +71,25 @@ with correctly formed transactions instead of spamming the network. The introduc
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 are multiple possible approaches
to provide the application with access to transfer lamports outside of the regular cpi
execution context:
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.
1. A new specialized fee collection mechanism that uses per-account meta-data to encode
additional fees. Lamports should be collected on the actual accounts until claimed by
their owner. Account owners can trigger a per account rebate on the fee collected
during a transaction. Two strategies have been proposed:
1. Create PDAs of the application program to store fee settings
1. Concern is well encapsulated and implementation can be easily modified (**+**)
2. Overhead for loading a lot of PDAs might be high (**---**)
3. Hard to calculate fee for a message (**-**)
2. **Extend existing account structure in ledger to store fee settings and collect lamports**
1. High efficiency, minimal performance impact (**++**)
2. Accounts structure is difficult to modify (**---**)
3. Easier to calculate fees for a message (**+**)
4. If application fees are high can prevent denial of service attack on smart contract. (**+**)
POC is implemented using this method.
2. A generic execution phase that gets invoked for every program account passed to a
transaction. Programs would optionally contain a fee entry-point in their binary
code that gets invoked with a list of all accessed account keys and their lock
status. Programs would need access to a new sysvar identifying the transaction fee
payer to rebate lamports to, to prevent breaking API changes for clients.
1. High degree of flexibility for programs (**++**)
2. Least data structures modified in runtime (**++**)
3. Does not allow guarding non-pda accounts (e.g. an end-users signer key) (**-**)
4. Allowing program execution that continues on failure might invite users to
implement unforseen side-effects inside the fee entry-point (**---**)
5. Very hard to calculate fees for a message (**--**)
3. *Passing the application fees in the instruction for each account and validating in the dapp*. A `PayApplicationFee`
*Passing the application fees in the instruction for each account and validating in the dapp*. A `PayApplicationFee`
instruction is like irrevocable transfer instruction and will do the transfer even if the transaction fails. A new
instruction `CheckApplicationFee` will check if the application fee for an account has been paid.
1. High degree of flexibility for programs (**++**)
2. No need to save the application fees on the ledger. (**++**)
3. Each transaction has to include additional instructions so it will break existing dapp interfaces (**--**)
3. Each transaction has to include additional instructions so it will break existing dapp interfaces (**-**)
4. Account structure does not need to be modified (**++**)
5. Does not prevent any denial of service attack on a smart contract (**-**)
6. Each dapp has to implement a cpi to check if a transaction has paid app fees. (**-**)
6. Each dapp has to implement a cpi to check if a transaction has paid app fees
After discussion with the internal team and Solana teams, we have decided to go ahead with
implementation **1.2** i.e extending the account structure in the ledger to store fee setting.
This decision was made because it will make the application fee a core feature of Solana.
The downside is that we have to change one of the core structures which makes a lot of code changes
but the rent epoch is being deprecated which we can reuse to store application fees.
Recently we have also been discussing **3** which could be easier to implement than **1.2** the only
catch is that each dapp that wants to use this feature has to do additional development. 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 move to dapp side. The plus point is that we
do not have to touch the account structure, and do not have to implement the base fee surcharge stage.
It is also more easier to calculate total fees. I will write more about **3** at the end of the document.
### Base fee surcharge
The main issue with the approach is that the validator will have to load all the accounts for
a transaction before it can decide how much the payer has to pay as fees. And if the fee payer
has insufficient lamports, then the work for loading of accounts is a waste of resources.
Previously with fixed base fees and priority fees the total fees were already detemined, we just
load the payer account and check if it has a sufficient balance. But now we can imagine a
transaction with many accounts and a payer having sufficient amount to pay base fees but not the
application fees, if the account with application fees was set at the end then the validator loads
all the accounts till the end and then realize that payer is unable to pay application fees.
This can be used by an attacker to slow the cluster.
To address this issue we will add additional surcharge for base fees if the transaction uses
any account with application fees but was not available to pay them. This is just a recommendatation
and is subjected to change to something static instead of dynamic. It will work as follows.
1. Before loading accounts we check that payer has minimum balance
`balance >= per-signature transaction base fees + priority fees + (per account fee) * number of accounts` in the transaction.
2. If payer does not have this balance minimum transaction fails.
3. If payer has this balance then we start loading accounts and checking if there are any application fees.
4. If payer does not have enough balance to pay application fees then we charge payer
`total fees = per-signature transaction base fees + priority fees + (per account fee) * accounts loaded`.
5. If payer has enough balance to pay application fees but `LimitApplicationFees` instruction set amount too low.
`total fees = per-signature transaction base fees + priority fees + (per account fee) * accounts loaded`.
6. If payer has enough balance then to pay application fee and has included the instruction `LimitApplicationFees`
with sufficient limit in the instruction.
`total fees = per-signature transaction base fees + priority fees + application fees`.
7. If there is no application fees involved then the payer pays.
`total fees = per-signature transaction base fees + priority fees`
So this method adds the requirement that payer **MUST** have additional balance of number of accounts * base fees.
With base fees so low we hope that wont be an issue for the user and this additional fees will goes to
the validators and not the dapp account.
The overall fees for transactions without any accounts using this feature **WONT** change.
We start describing the design with a new solana native program.
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 responsibility of dapp developers.
The plus point is that we do not have to touch the core solana structure to store the application fees.
It is also more easier to calculate total fees.
### A new application fee program
@ -172,146 +99,109 @@ We add a new native solana program called application fees program with program
App1icationFees1111111111111111111111111111
```
This program will be used to change application fee for an account, to intialize rebates
This native program will be used to check application fee for an account, to intialize rebates
initaited by the account authority, and a special instruction by which fee payer accepts
amount of lamports they are willing to pay as application fees.
amount of lamports they are willing to pay as application fees per account.
#### LimitApplicationFees Instruction
#### PayApplicationFees Instruction
With this instruction, the fee payer limits to pay application fees specifying the maximum amount.
If the application fee required for the account is more than specified, then the payer has to pay
a base fee surcharge. This instruction **OPTIONAL** be included in the transaction that writes locks
accounts that have implemented this feature.
With this instruction, the fee payer accepts to pay application fees specifying the amount.
This instruction **MUST** be included in the transaction that interacts with dapps having application fees.
This instruction is like an irrevokable transfer if the payer has enough funds
i.e even if the transaction fails the payer will end up paying.
If this instruction is added then even if the dapp does not check the application fees the payer ends
up paying.
If the payer does not have enough balance the transaction fails with error `InsufficientFunds`.
It requires:
Argument : Maximum application fees intented to pay in lamports (u64).
#### UpdateFees Instruction
Accounts :
* List of accounts that need an application fees.
This instruction will update application fees for an account.
Argument : Corresponding list of application fees for each account in `Accounts`.
The arguments list must be of same length as number of accounts.
The index of fees and account should match.
The account fees for each account is 8 bytes represented in rust type `u64`
#### CheckApplicationFees Instruction
This instruction will check if an application fee for an account is paid.
It requires :
* authority (i.e owner) of the writable account as (signer)
* Writable account as (writable)
* Account where fees are paid.
Argument: updated fees in lamport (u64).
Argument: required fees in lamport (u64).
If application fees are not paid or are paid insufficiently this instruction will return
an error. The idea is dapp developer uses this instruction to check if the required fees
are paid and fail the transaction if they are not paid or partially paid.
In case of partial payment, the user will lose the partially paid amount.
A payer may overpay for the fees. This instruction can be called multiple times across multiple instructions.
#### Rebate Instruction
This instruction should be called by the dapp using CPI or by the owner of the account.
It requires :
* Authority of the writable account (signer)
* Writable account (writable)
* Account on which a fee was paid
* Authority (owner) of the account (signer)
Argument: Number of lamports to rebate (u64) can be u64::MAX to rebate all the fees.
The authority or 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.
Payer has to pay full application fees even if they are eligible for a rebate.
If there is no application fee associated with the account we ignore the rebate instruction.
### Changes in the core solana code
These are following changes that we have identified as required to implement this feature.
#### Account structure
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.
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`.
So `executable` will be changed to `account_flags` where 1 LSB is `executable` and 2nd LSB is `has_application_fees`.
This change does not impact a lot of code and is very localized to file append_vec.
#### Changes in `load_transaction_accounts`
When we load transaction accounts we have to calculate the application fees, decode the `LimitApplicationFees`
instruction and implement the logic described in the base fee surcharge part above.
When we load transaction accounts we have to calculate the application fees by decoding the `PayApplicationFees`
instruction. Then we verify that fee payer has miminum balance of:
`per-transaction base fees + prioritization fees + sum of application fees on all accounts`
If the payer has 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`.
#### Changes in invoke context
The structure `invoke context` is passed to all the native solana program while execution. We create
a new structure called `application fee changes` which contains one hashmap mapping application fees
(`Pubkey` -> `application fees(u64)`), another containing rebates (`Pubkey` -> `amount rebated (u64)`)
and a third to store all the updates in application fees (`Pubkey` -> `New application fees (u64)`).
The `application fee changes` structure is already filled with application fees that were decided
while we were loading all the accounts. This new structure we add as a field in invoke structure so
that it can be used by native program `App1icationFees1111111111111111111111111111`.
a new structure called `ApplicationFeeChanges` which contains one hashmap mapping application fees
(`Pubkey` -> `application fees(u64)`), another containing rebates (`Pubkey` -> `amount rebated (u64)`).
The `ApplicationFeeChanges` structure is already filled with application fees in the stage `load_transaction_accounts`.
```Rust
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct ApplicationFeeChanges {
pub application_fees: HashMap<Pubkey, u64>, // To store application fees by account
pub rebated: HashMap<Pubkey, u64>, // to store rebates by account
pub updated: Vec<(Pubkey, u64)>, // to store updates by account
}
```
In the application fees program we will just add the new values of application fees in case of
`UpdateFees` instruction. In case of `Rebate` instruction add the rebated value in the relevant
hashmap and remove the same amount from the application fees hash map.
`PayApplicationFees` will add the accounts specified in the `application_fees` field of this structure.
Each time `CheckApplicationFees` is called we just check that the account is present in the map `application_fees`
and that the amount passed in the instruction is `<=` value in the `application_fees` map.
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.
In verify stage we verify that `Old Application Fees` = `New Application Fees` + `Rebates` for each
account.
In verify stage we verify that `Application Fees` >= `Rebates` for each account and return `UnbalancedInstruction` on failure.
#### Changes in Bank
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 base fees with application fees.
The updates in the application fees will be stored in a special hashmap and the changes will be
applied at the end of the slot when the bank is freezing. This will effectively make any updates to
the application fees valid from the next slot and not in the current slot.
## Implementation of application fees without saving data in the ledger
This section describes an alternate implementation of application fees noted in the third implementation
here [3](#detailed-design). It is very similar to the implementation described above, we do not save the
application fees data on the ledger so no need to make changes in the account struct. Instead of `LimitApplicationFees`
we will have `PayApplicationFees` for each account we have to pay the application fee. No need for `UpdateFees`
instruction, instead we can have `CheckApplicationFeesArePaid` which dapp can cpi into to check if the
application fee for the account is paid. The application fee paid will be valid for the whole transaction,
so if multiple instructions are calling `CheckApplicationFeesArePaid` will pass if the payer has paid
application fees once for that account. The rebate is exactly as described above.
In `load_transaction_accounts` section we iterate through all the instructions and find any instruction for
program id `App1icationFees1111111111111111111111111111`. If there are any instructions for the application
fees program then we will decode them and check if it is `PayApplicationFees` instruction. We will collect
all `PayApplicationFees` instructions to calculate the total application fees. To validate the payer we will
check if the payer has other fees + application fees to pay. There is no more need to implement a base fee surcharge
section as we already know what are total fees required before loading all the accounts.
The changes in `invoke_context` and `bank` will remain as described above. This implementation is more elegant,
but setting application fees for existing dapps like openbook means we have to move the checking application fee
part to the dapp. The dapps interfacing with openbook have to do additional development of this feature to support
their cpi interface.
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 also transfer the application fees to the repective accounts in this stage.
## Impact
@ -321,20 +211,53 @@ They should be very meticoulous before calling rebate so that a malicious user c
feature to bypass application fees. Dapp developer also have to implement additional instruction
to collect these fees using lamport transfers.
This could also add additional fees collection for the validator if the transactions are not correctly
formed, like missing `PayApplicationFee` instruction or insuffucient payer balance.
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.
A dapp can break the cpi interface with other dapps if it implements this feature. This is because
it will require additional account for application fees program to all the instructions which calls
`CheckApplicationFees` or `Rebate`, interface also has to add an additional instruction `PayApplicationFees`
to the correct account.
Overall this feature will incentivise creation of proper transaction and spammers would have to pay
much more fees reducing congestion in the cluster.
## 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.
User could overpay (more than required by dapp) application fees, if the `CheckApplicationFees` instruction
has amount more than the application fee required by dapp.
Denial of service attack for a dapp is possible by flooding the cluster with a lot of transactions write-locking
an account used by the dapp. This attack is already possible on the network.
This implementation of application fees does not protect the dapp from denial of service
attacks. An attacker can always flood the network with the transactions without the `CheckApplicationFees` instruction.
None of these transactions will be executed successfully but the write lock on the account was taken without any
payment of application fees.
To solve this kind of attack the application fees should be stored in the ledger but this involves a lot of changes in
the solana core code. And this kind of attack can only affect one dapp at a time, attacker has to burn a lot of gas
fees to sustain for a very long time.
## 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 which implements this feature.
Validators which do not implement this feature cannot replay the blocks with transaction using application fees.
## Mango V4 Usecase
With this feature implemented Mango-V4 will be able to charge users who spam risk-free aribitrage
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.