Skip to content

Latest commit

 

History

History
169 lines (109 loc) · 13.1 KB

cip-0066.md

File metadata and controls

169 lines (109 loc) · 13.1 KB
cip title author discussions-to status type category created license
66
New Transaction Type: Celo Denominated Fees
Karl Bartel (@karlb)
Draft
Standards Track
Ring 0
2024-03-05
Apache 2.0

Simple Summary

Create a new EIP-2718 Transaction Type that will act as an EIP-1559 transaction but includes the feeCurrency field for paying in a different currency. Contrary to previous fee currency tx types, the fee amounts are denominated in the native Celo token and not in the fee currency. This makes the tx type more similar to standard EIP-1559 txs and thereby increases compatibility with unmodified Ethereum tooling.

Abstract

This CIP proposes to create a new EIP-2718 Transaction Type 0x7a. This new transaction is encoded as follows: 0x7a || rlp([chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, accessList, feeCurrency, maxFeeInFeeCurrency, signatureYParity, signatureR, signatureS]).

Compared to EIP-1559, the fields feeCurrency and maxFeeInFeeCurrency are added.

Compared to CIP-64, the field maxFeeInFeeCurrency is added, and the unit for the fields maxPriorityFeePerGas, maxFeePerGas is changed from the fee currency to the native CELO token.

Motivation

While CIP-66 txs will work like CIP-64 txs for end users in most cases, they provide multiple benefits.

Fee Accuracy

Specifying the fees in CELO provides the same fee price accuracy for all fee currencies, even those with a low number of decimals. Some of the most popular ERC-20 tokens on Ethereum have only 6 decimals. That is enough to specify the total costs for a tx, but not sufficient to accurately specify the cost per unit of gas. E.g., for USDC, even the smallest possible maxFeePerGas of 0.00001 USDC without a tip would make a native transfer cost 21000 * 0.00001 = 0.21 USDC.

Increased Ethereum Compatibility

The fields maxPriorityFeePerGas, maxFeePerGas and effectiveGasPrice are specified in the native CELO token, just like it is done for EIP-1559 txs. In many cases, this allows unmodified Ethereum tooling to work on these txs without knowing about CIP-66 and still getting the correct result. In contracts, CIP-64 txs would be misinterpreted by the exchange rate between CELO and the fee currency. Creating CIP-66 txs still requires explicit tooling support, though.

Simplified Data Analytics

Data analysis queries, indexers and other tools often ask questions like "How much fees have been paid by account X", "What was the average tip?" or "What was the average cost of a tx?". When only looking at EIP-1559 (and to a certain degree legacy Ethereum-compatible txs) and CIP-66 txs, these questions can be answered without making any distinction between the different tx types, since they share the same fields with the same meaning. Although other tx types are in use now, this will likely change and make this a clear advantage in the long run.

Staying Closer to Upstream Code

Keeping the tx field meaning consistent with EIP-1559 also allows us to reduce the necessary changes in celo-blockchain compared to geth. This is most relevant in two places:

  • The tx pool, where txs are sorted, replaced or discarded based on their fee values.
  • baseFee handling and effectiveGasPrice calculation (see example diff)

Specification

As of FORK_BLOCK_NUMBER, a new EIP-2718 transaction is introduced with TransactionType 122.

Definitions

  • TransactionType 1220x7a. See EIP-2718
  • || is the byte/byte-array concatenation operator
  • // is integer division (discarding the remainder)
  • feeCurrency is the address of the whitelisted currency to be used to pay for gas. In contrast to older tx types, this field can not be omitted to mean "native CELO token".
  • The exchange rate from CELO to feeCurrency is specified as a rational number consisting of two uint256: rateNum / rateDen
  • feeInFeeCurrency is the total amount of fees paid by the user denominated in feeCurrency, so feeInFeeCurrency = gasUsed * (baseFeePerGas + tipPerGas) * rateNum // rateDen.
  • debitInFeeCurrency is the amount of tokens that would be spent if the whole gasLimit is used up, thus debitInFeeCurrency = maxFeePerGas * gasLimit * rateNum // rateDen.
  • maxFeeInFeeCurrency is chosen by the tx sender to limit the exchange rate risk. Txs must only be included if maxFeeInFeeCurrency >= debitInFeeCurrency.

Tx and Receipt Encoding

The EIP-2718 TransactionPayload for this transaction is 0x7a || rlp([chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, accessList, feeCurrency, maxFeeInFeeCurrency, signatureYParity, signatureR, signatureS]).

The signatureYParity, signatureR, signatureS elements of this transaction represent a secp256k1 signature over keccak256(0x7a || rlp([chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, accessList, feeCurrency, maxFeeInFeeCurrency])).

The EIP-2718 ReceiptPayload for this transaction is rlp([status, cumulativeGasUsed, logsBloom, logs, feeInFeeCurrency]).

The new feeInFeeCurrency receipt field is also included in RPC responses for CIP-66 txs.

Tx Handling in the Blockchain Client

In the tx pool, CIP-66 txs are rejected if the CELO/feeCurrency exchange rate exceeds the limit implied by the maxFeeInFeeCurrency. The exchange rate is acceptable if maxFeeInFeeCurrency >= debitInFeeCurrency. The maxFeeInFeeCurrency field is only used for this purpose, not for any other calculations.

As for all tx types, the tx pool also ensures that the sender can be debited successfully. For CIP-66 txs, this means ensuring that debitGasFees(debitInFeeCurrency) can be successfully executed.

During the state transition, a CIP-66 tx is processed in the usual steps of debit gas fees, main tx execution and credit gas fees. While the main tx execution goes unchanged, the debiting and crediting are adapted to the CIP-66 fee requirements as shown in the following pseudocode:

# CIP-66 fee debit
debitInFeeCurrency = maxFeePerGas * gasLimit * rateNum // rateDen
assert maxFeeInFeeCurrency >= debitInFeeCurrency  # ensured by tx pool check
feeCurrency.debitGasFees(debitInFeeCurrency)

# Normal tx handling, all values in native CELO token
gasUsed = evm.Call(sender, to, data, gasLimit, value)
tipPerGas = min(maxFeePerGas - baseFeePerGas, maxPriorityFeePerGas)
effectiveGasPrice = baseFeePerGas + tipPerGas

# CIP-66 specific fee credit
baseFeeInFeeCurrency = baseFeePerGas * gasUsed * rateNum // rateDen
tipInFeeCurrency = tipPerGas * gasUsed * rateNum // rateDen
feeInFeeCurrency = baseFeeInFeeCurrency + tipInFeeCurrency
refundInFeeCurrency = debitInFeeCurrency - feeInFeeCurrency
assert refundInFeeCurrency >= 0  # because `debitInFeeCurrency >= feeInFeeCurrency`, see "Invariants"
feeCurrency.creditGasFees(refundInFeeCurrency, tipInFeeCurrency, baseFeeInFeeCurrency)

Invariants

maxFeeInFeeCurrency >= debitInFeeCurrency >= feeInFeeCurrency

maxFeeInFeeCurrency >= debitInFeeCurrency is explicitly checked and failing transactions are discarded.

debitInFeeCurrency >= feeInFeeCurrency is true because gasLimit >= gasUsed, maxFeePerGas >= baseFeePerGas + tipPerGas and doing the integer division twice instead of once can only lower the resulting fees. Here's the full proof:

  debitInFeeCurrency  = maxFeePerGas * gasLimit * rateNum // rateDen
⇒                    >= (baseFeePerGas + tipPerGas) * gasLimit * rateNum // rateDen
⇒                    >= (baseFeePerGas + tipPerGas) * gasUsed * rateNum // rateDen
⇒                    >= (baseFeePerGas * gasUsed * rateNum // rateDen) + (tipPerGas * gasUsed * rateNum // rateDen)
⇔                    >= baseFeeInFeeCurrency + tipInFeeCurrency
⇔                    >= feeInFeeCurrency

Rationale

Rates vs. absolute values

The fields maxFeeInFeeCurrency and feeInFeeCurrency are meant to convey information about conversion rates (allowed conversion rates for the former and used rate for the latter). Actually encoding this information as conversion rate fields would not be straightforward, as only integer numbers are supported natively by the EVM. There are many ways to encode such information, but none is as accurate and easy to use as plain integer values for the resulting totals.

Including feeInFeeCurrency in the receipt

It must be obvious exactly how much has been paid in fees for a tx and since the fees are paid in feeCurrency, the total fees for a tx must also be available denominated in that fee currency. Without modifying the receipt, the feeInFeeCurrency could be derived from the tx, the base fee and the conversion rate at the beginning of the block.

But the conversion rate is stored in the block state, which gets pruned from non-archive nodes after a while. This would mean that feeInFeeCurrency is returned via JSON RPC after the tx was executed, but then "disappears" from the JSON receipt after a while. This is both surprising (receipt responses should not change over time) and problematic, since it makes the exact fee amount unavailable. To avoid this, we chose to include the feeInFeeCurrency in the RLP-encoded receipt.

While this introduces a difference to EIP-1559, the downsides are limited because

  • Most tools access the receipts via JSON RPC, where the additional field won't do any harm.
  • The field is hidden inside the EIP-2718 compliant receipt payload.

Why feeInFeeCurrency instead of effectiveGasPriceInFeeCurrency?

Considering that JSON RPC responses contain an effectiveGasPrice field, it would be natural to add an effectiveGasPriceInFeeCurrency field instead of the suggested feeInFeeCurrency field. This does not work due to the limited accuracy of defining prices per gas in fee currency. For tokens with a low number of decimals, this could not accurately represent all relevant prices, thereby either limiting the allowed prices or misrepresenting the actually paid price.

Why maxFeeInFeeCurrency instead of maxFeePerGasInFeeCurrency?

This is analogous to the feeInFeeCurrency decision in the previous section. Since txs already contain a maxFeePerGas field, it would be nice to limit the exchange rate risk by specifying a maxFeePerGasInFeeCurrency field. But per-gas values denominated in the fee currency won't be precise enough for fee currencies with a low number of decimals. Therefore, we use a maxFeeInFeeCurrency field.

Disallowing the omitted feeCurrency

Previous fee currency tx types allowed omitting the feeCurrency field, causing the fees to be paid in CELO like for an EIP-1559 tx. Disallowing this has the following advantages:

  • Txs which don't need to specify a feeCurrency will be sent as EIP-1559 tx, staying closer to Ethereum
  • Simpler code for both celo-blockchain and all kinds of tooling, since less cases have to be handled

Debiting debitInFeeCurrency, not maxFeeInFeeCurrency

The maxFeeInFeeCurrency value is only intended to limit the exchange rate risk and should not have any other effects. If the tx sender decides to fully accept all exchange rate risk and sends maxFeeInFeeCurrency = maxInt (which is strongly discouraged!) the tx should still be able to succeed. We already know that debitInFeeCurrency = maxFeePerGas * gasLimit * rateNum // rateDen will be sufficient to pay for the tx, so debiting that amount is enough and there is no reason to increase the debit to maxFeeInFeeCurrency.

Backwards Compatibility

As with every new transaction type, this needs to be added as part of a fork, and as it is a new transaction type, it shouldn't change any actual behaviour.

Implementation

TODO

Security Considerations

Exchange Rate Variability

The exchange rate might change from the moment the user signed a transaction to the moment the transaction is included in the block. This means that the user might end up paying a different fee than what they were expecting (in terms of feeCurrency, but not in terms of CELO). The risk is limited by the maxFeeInFeeCurrency value, so the trade-off between exchange rate risk and chances of tx rejection is fully under the user's control.

With CIP-64, the same risk already exists, it is just less clearly visible. The fee currency's baseFeePerGas is calculated for each currency by converting the CELO baseFeePerGas to the respective currency based on the Oracle. The oracle result can change just before it is used to calculate the base fee. In this case, the risk is not limited by maxFeeInFeeCurrency, but instead by maxFeePerGas which can be exceeded if the oracle price changed strongly. So this CIP does not create a new risk, it just makes the already existing exchange rate risk more explicit.

License

This work is licensed under the Apache License, Version 2.0.