Skip to content

Commit

Permalink
Add support for CELO denominated txs in state_transition
Browse files Browse the repository at this point in the history
  • Loading branch information
hbandura committed Apr 19, 2024
1 parent d1b6d12 commit 568f922
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 17 deletions.
10 changes: 9 additions & 1 deletion contracts/fee_currencies.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/exchange"
"github.com/ethereum/go-ethereum/contracts/celo/abigen"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
Expand All @@ -33,10 +34,17 @@ func init() {
}

// Returns nil if debit is possible, used in tx pool validation
func TryDebitFees(tx *types.Transaction, from common.Address, backend *CeloBackend) error {
func TryDebitFees(tx *types.Transaction, from common.Address, backend *CeloBackend, exchangeRates common.ExchangeRates) error {
amount := new(big.Int).SetUint64(tx.Gas())
amount.Mul(amount, tx.GasFeeCap())

if tx.Type() == types.CeloDenominatedTxType {
var err error
amount, err = exchange.ConvertGoldToCurrency(exchangeRates, tx.FeeCurrency(), amount)
if err != nil {
return err
}
}
snapshot := backend.State.Snapshot()
err := DebitFees(backend.NewEVM(), tx.FeeCurrency(), from, amount)
backend.State.RevertToSnapshot(snapshot)
Expand Down
10 changes: 10 additions & 0 deletions core/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,14 @@ var (

// ErrCel2NotEnabled is returned if a feature requires the Cel2 fork, but that is not enabled.
ErrCel2NotEnabled = errors.New("required cel2 fork not enabled")

// ErrDenominatedNoMax is returned when a transaction containing a fee currency has no maxFeeInFeeCurrency set.
ErrDenominatedNoMax = errors.New("CELO denominated tx has no maxFeeInFeeCurrency")

// ErrDenominatedNoCurrency is returned when a celo-denominated transaction has no fee currency set
ErrDenominatedNoCurrency = errors.New("CELO denominated tx has no fee currency")

// ErrDenominatedLowMaxFee is returned when a celo denominated transaction, with the current exchange rate,
// the MaxFeeInFeeCurrency cannot cover the tx.Fee()
ErrDenominatedLowMaxFee = errors.New("CELO denominated tx MaxFeeInCurrency cannot cover gas fee costs")
)
1 change: 1 addition & 0 deletions core/state_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ func applyTransaction(msg *Message, config *params.ChainConfig, gp *GasPool, sta
}
receipt.TxHash = tx.Hash()
receipt.GasUsed = result.UsedGas
receipt.FeeInFeeCurrency = result.FeeInFeeCurrency

if msg.IsDepositTx && config.IsOptimismRegolith(evm.Context.Time) {
// The actual nonce for deposit transactions is only recorded from Regolith onwards and
Expand Down
130 changes: 116 additions & 14 deletions core/state_transition.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ type ExecutionResult struct {
UsedGas uint64 // Total used gas but include the refunded gas
Err error // Any error encountered during the execution(listed in core/vm/errors.go)
ReturnData []byte // Returned data from evm(function result or data supplied with revert opcode)

// Celo additions
FeeInFeeCurrency *big.Int // Total fee debited in a CELO denominated tx, in FeeCurrency
}

// Unwrap returns the internal evm error which allows us for further
Expand Down Expand Up @@ -180,6 +183,20 @@ type Message struct {
// `nil` corresponds to Celo Gold (native currency).
// All other values should correspond to ERC20 contract addresses.
FeeCurrency *common.Address

// MaxFeeInFeeCurrency provides a maximum accepted value for the
// total fee of a CELO denominated transaction in the FeeCurrency.
// Note that it is not necessary that Balance > MaxFeeInFeeCurrency,
// only that ConvertGoldToCurrency(tx.Fee,FeeCurrency) <= MaxFeeInFeeCurrency.
MaxFeeInFeeCurrency *big.Int
}

// DenominatedFeeCurrency returns the currency in which all fees are expressed
func (m Message) DenominatedFeeCurrency() *common.Address {
if m.FeeCurrency != nil && m.MaxFeeInFeeCurrency != nil {
return nil
}
return m.FeeCurrency
}

// TransactionToMessage converts a transaction into a Message.
Expand All @@ -203,11 +220,21 @@ func TransactionToMessage(tx *types.Transaction, s types.Signer, baseFee *big.In
BlobHashes: tx.BlobHashes(),
BlobGasFeeCap: tx.BlobGasFeeCap(),

FeeCurrency: tx.FeeCurrency(),
FeeCurrency: tx.FeeCurrency(),
MaxFeeInFeeCurrency: tx.MaxFeeInFeeCurrency(),
}
if tx.Type() == types.CeloDenominatedTxType {
// Sanity checks for CELO denominated txs
if tx.MaxFeeInFeeCurrency() == nil {
return msg, ErrDenominatedNoMax
}
if tx.FeeCurrency() == nil {
return msg, ErrDenominatedNoCurrency
}
}
// If baseFee provided, set gasPrice to effectiveGasPrice.
if baseFee != nil {
if msg.FeeCurrency != nil {
if msg.DenominatedFeeCurrency() != nil {
var err error
baseFee, err = exchange.ConvertGoldToCurrency(exchangeRates, msg.FeeCurrency, baseFee)
if err != nil {
Expand Down Expand Up @@ -262,6 +289,10 @@ type StateTransition struct {
initialGas uint64
state vm.StateDB
evm *vm.EVM

// Celo additions
erc20FeeDebited *big.Int // Amount debited in the Debit at buyGas for a FeeCurrency tx
feeInFeeCurrency *big.Int // Total fee deducted (Debited - Credited) at the end of a CELO denominated tx
}

// NewStateTransition initialises and returns a new state transition object.
Expand Down Expand Up @@ -291,7 +322,7 @@ func (st *StateTransition) buyGas() error {
l1Cost = st.evm.Context.L1CostFunc(st.msg.RollupCostData, st.evm.Context.Time)

// L1 data fee needs to be converted in fee currency
if st.msg.FeeCurrency != nil && l1Cost != nil {
if st.msg.DenominatedFeeCurrency() != nil && l1Cost != nil {
// Existence of the fee currency has been checked in `preCheck`
l1Cost, _ = exchange.ConvertGoldToCurrency(st.evm.Context.ExchangeRates, st.msg.FeeCurrency, l1Cost)
}
Expand All @@ -304,9 +335,9 @@ func (st *StateTransition) buyGas() error {
if st.msg.GasFeeCap != nil {
balanceCheck.SetUint64(st.msg.GasLimit)
balanceCheck = balanceCheck.Mul(balanceCheck, st.msg.GasFeeCap)
balanceCheck.Add(balanceCheck, st.msg.Value)
if l1Cost != nil {
balanceCheck.Add(balanceCheck, l1Cost)
if st.msg.FeeCurrency == nil {
// Value will be checked individually for FeeCurrencies in canPayFee
balanceCheck.Add(balanceCheck, st.msg.Value)
}
}
if st.evm.ChainConfig().IsCancun(st.evm.Context.BlockNumber, st.evm.Context.Time) {
Expand Down Expand Up @@ -335,19 +366,38 @@ func (st *StateTransition) buyGas() error {
return st.subFees(mgval)
}

func (st *StateTransition) checkCanPayCELOamount(celoAmount *big.Int) error {
balance := st.state.GetBalance(st.msg.From)

if balance.Cmp(celoAmount) < 0 {
return fmt.Errorf("%w: address %v have %v want %v", ErrInsufficientFunds, st.msg.From.Hex(), balance, celoAmount)
}
return nil
}

// canPayFee checks whether accountOwner's balance can cover transaction fee.
func (st *StateTransition) canPayFee(checkAmount *big.Int) error {
if st.msg.FeeCurrency == nil {
balance := st.state.GetBalance(st.msg.From)

if balance.Cmp(checkAmount) < 0 {
return fmt.Errorf("%w: address %v have %v want %v", ErrInsufficientFunds, st.msg.From.Hex(), balance, checkAmount)
if err := st.checkCanPayCELOamount(checkAmount); err != nil {
return err
}
} else {
backend := &contracts.CeloBackend{
ChainConfig: st.evm.ChainConfig(),
State: st.state,
}
// Check first VALUE for FeeCurrencies
if err := st.checkCanPayCELOamount(st.msg.Value); err != nil {
return err
}
if st.msg.MaxFeeInFeeCurrency != nil {
// CELO denominated tx, convert the checked value to the FeeCurrency
var err error
checkAmount, err = exchange.ConvertGoldToCurrency(st.evm.Context.ExchangeRates, st.msg.FeeCurrency, checkAmount)
if err != nil {
return err
}
}
balance, err := contracts.GetBalanceERC20(backend, st.msg.From, *st.msg.FeeCurrency)
if err != nil {
return err
Expand All @@ -368,6 +418,22 @@ func (st *StateTransition) subFees(effectiveFee *big.Int) (err error) {
st.state.SubBalance(st.msg.From, effectiveFee)
return nil
} else {
st.state.SubBalance(st.msg.From, st.msg.Value)
if st.msg.MaxFeeInFeeCurrency != nil {
// CELO denominated tx. Exchange the fee
var err error
effectiveFee, err = exchange.ConvertGoldToCurrency(st.evm.Context.ExchangeRates, st.msg.FeeCurrency, effectiveFee)
if err != nil {
return err
}
// effectiveFee also includes l1cost
if effectiveFee.Cmp(st.msg.MaxFeeInFeeCurrency) > 0 {
// Fee can't be higher than MaxFeeInFeeCurrency
return fmt.Errorf("%w: Fee %v should be lower or equal than MaxFeeInFeeCurrency %v",
ErrDenominatedLowMaxFee, effectiveFee, st.msg.MaxFeeInFeeCurrency)
}
}
st.erc20FeeDebited = effectiveFee
return contracts.DebitFees(st.evm, st.msg.FeeCurrency, st.msg.From, effectiveFee)
}
}
Expand Down Expand Up @@ -422,6 +488,14 @@ func (st *StateTransition) preCheck() error {
return exchange.ErrNonWhitelistedFeeCurrency
}
}

// Verify that CELO denominated tx is valid (not including l1cost here)
if msg.MaxFeeInFeeCurrency != nil {
maxFee := new(big.Int).Mul(new(big.Int).SetUint64(st.msg.GasLimit), st.msg.GasFeeCap)
if maxFee.Cmp(st.msg.MaxFeeInFeeCurrency) > 0 {
return ErrDenominatedLowMaxFee
}
}
}

// Make sure that transaction gasFeeCap is greater than the baseFee (post london)
Expand Down Expand Up @@ -643,9 +717,10 @@ func (st *StateTransition) innerTransitionDb() (*ExecutionResult, error) {
}

return &ExecutionResult{
UsedGas: st.gasUsed(),
Err: vmerr,
ReturnData: ret,
UsedGas: st.gasUsed(),
Err: vmerr,
ReturnData: ret,
FeeInFeeCurrency: st.feeInFeeCurrency,
}, nil
}

Expand Down Expand Up @@ -710,6 +785,33 @@ func (st *StateTransition) distributeTxFees() error {
l1Cost = st.evm.Context.L1CostFunc(st.msg.RollupCostData, st.evm.Context.Time)
}

if feeCurrency != nil && st.msg.MaxFeeInFeeCurrency != nil {
// Celo Denominated

// We want to ensure that
// st.erc20FeeDebited = tipTxFee + baseTxFee + refund
// so that debit and credit totals match. Since the exchange rate
// conversions have limited accuracy, the only way to achieve this
// is to calculate one of the three credit values based on the two
// others after the exchange rate conversion.

// We have already pass the prechecks and the Debit fees so we know the
// exchange rate is whitelisted, no need to check for the error
tipTxFee, _ = exchange.ConvertGoldToCurrency(st.evm.Context.ExchangeRates, feeCurrency, tipTxFee)
baseTxFee, _ = exchange.ConvertGoldToCurrency(st.evm.Context.ExchangeRates, feeCurrency, baseTxFee)

// The l1cost is added inside of the CreditFees transaction, it should not be added here
totalTxFee.Add(tipTxFee, baseTxFee)
refund.Sub(st.erc20FeeDebited, totalTxFee) // refund = debited - tip - basefee

// Set receipt field
st.feeInFeeCurrency = totalTxFee
// Add l1cost to the fee paid
if l1Cost != nil {
st.feeInFeeCurrency.Add(st.feeInFeeCurrency, l1Cost)
}
}

if feeCurrency == nil {
st.state.AddBalance(st.evm.Context.Coinbase, tipTxFee)
st.state.AddBalance(from, refund)
Expand Down Expand Up @@ -745,7 +847,7 @@ func (st *StateTransition) calculateBaseFee() *big.Int {
baseFee = big.NewInt(0)
}

if st.msg.FeeCurrency != nil {
if st.msg.DenominatedFeeCurrency() != nil {
// Existence of the fee currency has been checked in `preCheck`
baseFee, _ = exchange.ConvertGoldToCurrency(st.evm.Context.ExchangeRates, st.msg.FeeCurrency, baseFee)
}
Expand Down
2 changes: 1 addition & 1 deletion core/txpool/legacypool/legacypool.go
Original file line number Diff line number Diff line change
Expand Up @@ -692,7 +692,7 @@ func (pool *LegacyPool) validateTx(tx *types.Transaction, local bool) error {
log.Error("Transaction sender recovery failed", "err", err)
return err
}
return contracts.TryDebitFees(tx, from, pool.celoBackend)
return contracts.TryDebitFees(tx, from, pool.celoBackend, pool.currentRates)
}
return nil
}
Expand Down
17 changes: 16 additions & 1 deletion core/types/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,22 @@ func (tx *Transaction) FeeCurrency() *common.Address {

// MaxFeeInFeeCurrency returns the max fee in the fee_currency for celo denominated txs.
func (tx *Transaction) MaxFeeInFeeCurrency() *big.Int {
return new(big.Int).Set(tx.inner.maxFeeInFeeCurrency())
maxFee := tx.inner.maxFeeInFeeCurrency()
if maxFee == nil {
return nil
}
return new(big.Int).Set(maxFee)
}

// DenominatedFeeCurrency returns in which currency the fields GasPrice, GasTipCap, GasFeeCap, etc are
// denominated in
func (tx *Transaction) DenominatedFeeCurrency() *common.Address {
// not declaring this method in TxData since it's a specific for just one type,
// to avoid cluttering it with more methods.
if tx.Type() == CeloDenominatedTxType {
return nil
}
return tx.FeeCurrency()
}

// Transactions implements DerivableList for transactions.
Expand Down

0 comments on commit 568f922

Please sign in to comment.