Skip to content

Commit

Permalink
Read IntrinsicGas from FeeCurrencyDirectory (#178)
Browse files Browse the repository at this point in the history
* Add FeeCurrencyContext object to BlockContext

This replaces the `BlockContext.ExchangeRates` with a wrapper object
`BlockContext.FeeCurrencyContext`.

This enables easier addition of fee-currency related information
to the block context and passing around that information.
The exchange-rates are now part of the `FeeCurrencyContext`

* Add intrinsic gas retrieval from FeeCurrencyDirectory

* Use intrinsic gas from FeeCurrencyDirectory in STF

* Add test for intrinsic gas too high in fee currency

* Add empty fee-currency context to tools and tests

* Add minor code improvements

Co-authored-by: Paul Lange <[email protected]>

* Add comment for nil fee-currency code path

* Add fee-currency-context to CeloBackend.NewEVM constructor

---------

Co-authored-by: Paul Lange <[email protected]>
  • Loading branch information
2 people authored and karlb committed Dec 13, 2024
1 parent 7557b5b commit f1fb53c
Show file tree
Hide file tree
Showing 35 changed files with 323 additions and 121 deletions.
7 changes: 6 additions & 1 deletion cmd/evm/internal/t8ntool/execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,12 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
rejectedTxs = append(rejectedTxs, &rejectedTx{i, errMsg})
continue
}
msg, err := core.TransactionToMessage(tx, signer, pre.Env.BaseFee, vmContext.ExchangeRates)
// NOTE: we can't provide exchange rates
// for fee-currencies here, since those are dynamically changing
// based on the oracle's exchange rates.
// When a Celo transaction with specified fee-currency is validated with this tool,
// this will thus result in a ErrUnregisteredFeeCurrency error for now.
msg, err := core.TransactionToMessage(tx, signer, pre.Env.BaseFee, vmContext.FeeCurrencyContext.ExchangeRates)
if err != nil {
log.Warn("rejected tx", "index", i, "hash", tx.Hash(), "error", err)
rejectedTxs = append(rejectedTxs, &rejectedTx{i, err.Error()})
Expand Down
8 changes: 7 additions & 1 deletion cmd/evm/internal/t8ntool/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,14 @@ func Transaction(ctx *cli.Context) error {
r.Address = sender
}
// Check intrinsic gas
// NOTE: we can't provide specific intrinsic gas costs
// for fee-currencies here, since those are written to the
// FeeCurrencyDirectory contract and are chain-specific.
// When a Celo transaction with specified fee-currency is validated with this tool,
// this will thus result in a ErrUnregisteredFeeCurrency error for now.
var feeIntrinsic common.IntrinsicGasCosts
if gas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.To() == nil,
chainConfig.IsHomestead(new(big.Int)), chainConfig.IsIstanbul(new(big.Int)), chainConfig.IsShanghai(new(big.Int), 0), tx.FeeCurrency()); err != nil {
chainConfig.IsHomestead(new(big.Int)), chainConfig.IsIstanbul(new(big.Int)), chainConfig.IsShanghai(new(big.Int), 0), tx.FeeCurrency(), feeIntrinsic); err != nil {
r.Error = err
results = append(results, r)
continue
Expand Down
31 changes: 31 additions & 0 deletions common/celo_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,37 @@ var (
)

type ExchangeRates = map[Address]*big.Rat
type IntrinsicGasCosts = map[Address]uint64

type FeeCurrencyContext struct {
ExchangeRates ExchangeRates
IntrinsicGasCosts IntrinsicGasCosts
}

func MaxAllowedIntrinsicGasCost(i IntrinsicGasCosts, feeCurrency *Address) (uint64, bool) {
intrinsicGas, ok := CurrencyIntrinsicGasCost(i, feeCurrency)
if !ok {
return 0, false
}
// Allow the contract to overshoot 2 times the deducted intrinsic gas
// during execution.
// If the feeCurrency is nil, then the max allowed intrinsic gas cost
// is 0 (i.e. not allowed) for a fee-currency specific EVM call within the STF.
return intrinsicGas * 3, true
}

func CurrencyIntrinsicGasCost(i IntrinsicGasCosts, feeCurrency *Address) (uint64, bool) {
// the additional intrinsic gas cost for a non fee-currency
// transaction is 0
if feeCurrency == nil {
return 0, true
}
gasCost, ok := i[*feeCurrency]
if !ok {
return 0, false
}
return gasCost, true
}

func CurrencyAllowlist(exchangeRates ExchangeRates) []Address {
addrs := make([]Address, len(exchangeRates))
Expand Down
9 changes: 7 additions & 2 deletions contracts/celo_backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,19 @@ func (b *CeloBackend) CallContract(ctx context.Context, call ethereum.CallMsg, b

// Get a vm.EVM object of you can't use the abi wrapper via the ContractCaller interface
// This is usually the case when executing functions that modify state.
func (b *CeloBackend) NewEVM() *vm.EVM {
blockCtx := vm.BlockContext{BlockNumber: new(big.Int), Time: 0,
func (b *CeloBackend) NewEVM(feeCurrencyContext *common.FeeCurrencyContext) *vm.EVM {
blockCtx := vm.BlockContext{
BlockNumber: new(big.Int),
Time: 0,
Transfer: func(state vm.StateDB, from common.Address, to common.Address, value *uint256.Int) {
if value.Cmp(common.U2560) != 0 {
panic("Non-zero transfers not implemented, yet.")
}
},
}
if feeCurrencyContext != nil {
blockCtx.FeeCurrencyContext = *feeCurrencyContext
}
txCtx := vm.TxContext{}
vmConfig := vm.Config{}
return vm.NewEVM(blockCtx, txCtx, b.State, b.ChainConfig, vmConfig)
Expand Down
153 changes: 115 additions & 38 deletions contracts/fee_currencies.go
Original file line number Diff line number Diff line change
@@ -1,28 +1,22 @@
package contracts

import (
"errors"
"fmt"
"math"
"math/big"

"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/addresses"
"github.com/ethereum/go-ethereum/contracts/celo/abigen"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/log"
)

const (
Thousand = 1000

// Default intrinsic gas cost of transactions paying for gas in alternative currencies.
// Calculated to estimate 1 balance read, 1 debit, and 4 credit transactions.
IntrinsicGasForAlternativeFeeCurrency uint64 = 50 * Thousand
maxAllowedGasForDebitAndCredit uint64 = 3 * IntrinsicGasForAlternativeFeeCurrency
)

var feeCurrencyABI *abi.ABI

func init() {
Expand All @@ -34,31 +28,42 @@ 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, feeContext common.FeeCurrencyContext) error {
amount := new(big.Int).SetUint64(tx.Gas())
amount.Mul(amount, tx.GasFeeCap())

snapshot := backend.State.Snapshot()
err := DebitFees(backend.NewEVM(), tx.FeeCurrency(), from, amount)
evm := backend.NewEVM(&feeContext)
_, err := DebitFees(evm, tx.FeeCurrency(), from, amount)
backend.State.RevertToSnapshot(snapshot)
return err
}

// Debits transaction fees from the transaction sender and stores them in the temporary address
func DebitFees(evm *vm.EVM, feeCurrency *common.Address, address common.Address, amount *big.Int) error {
func DebitFees(evm *vm.EVM, feeCurrency *common.Address, address common.Address, amount *big.Int) (uint64, error) {
if amount.Cmp(big.NewInt(0)) == 0 {
return nil
return 0, nil
}

maxIntrinsicGasCost, ok := common.MaxAllowedIntrinsicGasCost(evm.Context.FeeCurrencyContext.IntrinsicGasCosts, feeCurrency)
if !ok {
return 0, fmt.Errorf("%w: %x", exchange.ErrUnregisteredFeeCurrency, feeCurrency)
}

leftoverGas, err := evm.CallWithABI(
feeCurrencyABI, "debitGasFees", *feeCurrency, maxAllowedGasForDebitAndCredit,
feeCurrencyABI, "debitGasFees", *feeCurrency, maxIntrinsicGasCost,
// debitGasFees(address from, uint256 value) parameters
address, amount,
)
gasUsed := maxAllowedGasForDebitAndCredit - leftoverGas
evm.Context.GasUsedForDebit = gasUsed
if errors.Is(err, vm.ErrOutOfGas) {
// This basically is a configuration / contract error, since
// the contract itself used way more gas than was expected (including grace limit)
return 0, fmt.Errorf("surpassed maximum allowed intrinsic gas for fee currency: %w", err)
}

gasUsed := maxIntrinsicGasCost - leftoverGas
log.Trace("DebitFees called", "feeCurrency", *feeCurrency, "gasUsed", gasUsed)
return err
return gasUsed, err
}

// Credits fees to the respective parties
Expand All @@ -71,6 +76,7 @@ func CreditFees(
feeCurrency *common.Address,
txSender, tipReceiver, baseFeeReceiver, l1DataFeeReceiver common.Address,
refund, feeTip, baseFee, l1DataFee *big.Int,
gasUsedDebit uint64,
) error {
// Our old `creditGasFees` function does not accept an l1DataFee and
// the fee currencies do not implement the new interface yet. Since tip
Expand All @@ -85,8 +91,12 @@ func CreditFees(
if tipReceiver.Cmp(common.ZeroAddress) == 0 {
tipReceiver = baseFeeReceiver
}
maxAllowedGasForDebitAndCredit, ok := common.MaxAllowedIntrinsicGasCost(evm.Context.FeeCurrencyContext.IntrinsicGasCosts, feeCurrency)
if !ok {
return fmt.Errorf("%w: %x", exchange.ErrUnregisteredFeeCurrency, feeCurrency)
}

maxAllowedGasForCredit := maxAllowedGasForDebitAndCredit - evm.Context.GasUsedForDebit
maxAllowedGasForCredit := maxAllowedGasForDebitAndCredit - gasUsedDebit
leftoverGas, err := evm.CallWithABI(
feeCurrencyABI, "creditGasFees", *feeCurrency, maxAllowedGasForCredit,
// function creditGasFees(
Expand All @@ -101,43 +111,72 @@ func CreditFees(
// )
txSender, tipReceiver, common.ZeroAddress, baseFeeReceiver, refund, feeTip, common.Big0, baseFee,
)
if errors.Is(err, vm.ErrOutOfGas) {
// This is a configuration / contract error, since
// the contract itself used way more gas than was expected (including grace limit)
return fmt.Errorf("surpassed maximum allowed intrinsic gas for fee currency: %w", err)
}

gasUsed := maxAllowedGasForCredit - leftoverGas
log.Trace("CreditFees called", "feeCurrency", *feeCurrency, "gasUsed", gasUsed)

gasUsedForDebitAndCredit := evm.Context.GasUsedForDebit + gasUsed
if gasUsedForDebitAndCredit > IntrinsicGasForAlternativeFeeCurrency {
log.Info("Gas usage for debit+credit exceeds intrinsic gas!", "gasUsed", gasUsedForDebitAndCredit, "intrinsicGas", IntrinsicGasForAlternativeFeeCurrency, "feeCurrency", feeCurrency)
intrinsicGas, ok := common.CurrencyIntrinsicGasCost(evm.Context.FeeCurrencyContext.IntrinsicGasCosts, feeCurrency)
if !ok {
// this will never happen
return fmt.Errorf("%w: %x", exchange.ErrUnregisteredFeeCurrency, feeCurrency)
}
gasUsedForDebitAndCredit := gasUsedDebit + gasUsed
if gasUsedForDebitAndCredit > intrinsicGas {
log.Info("Gas usage for debit+credit exceeds intrinsic gas!", "gasUsed", gasUsedForDebitAndCredit, "intrinsicGas", intrinsicGas, "feeCurrency", feeCurrency)
}
return err
}

// GetExchangeRates returns the exchange rates for all gas currencies from CELO
func GetRegisteredCurrencies(caller *abigen.FeeCurrencyDirectoryCaller) ([]common.Address, error) {
currencies, err := caller.GetCurrencies(&bind.CallOpts{})
if err != nil {
return currencies, fmt.Errorf("failed to get registered tokens: %w", err)
}
return currencies, nil
}

// GetExchangeRates returns the exchange rates for the provided gas currencies
func GetExchangeRates(caller *CeloBackend) (common.ExchangeRates, error) {
exchangeRates := map[common.Address]*big.Rat{}
directory, err := abigen.NewFeeCurrencyDirectoryCaller(addresses.GetAddresses(caller.ChainConfig.ChainID).FeeCurrencyDirectory, caller)
if err != nil {
return common.ExchangeRates{}, fmt.Errorf("failed to access FeeCurrencyDirectory: %w", err)
}

registeredTokens, err := directory.GetCurrencies(&bind.CallOpts{})
currencies, err := GetRegisteredCurrencies(directory)
if err != nil {
return exchangeRates, fmt.Errorf("Failed to get whitelisted tokens: %w", err)
return common.ExchangeRates{}, err
}
for _, tokenAddress := range registeredTokens {
rate, err := directory.GetExchangeRate(&bind.CallOpts{}, tokenAddress)
if err != nil {
log.Error("Failed to get medianRate for gas currency!", "err", err, "tokenAddress", tokenAddress.Hex())
continue
}
if rate.Numerator.Sign() <= 0 || rate.Denominator.Sign() <= 0 {
log.Error("Bad exchange rate for fee currency", "tokenAddress", tokenAddress.Hex(), "numerator", rate.Numerator, "denominator", rate.Denominator)
continue
}
exchangeRates[tokenAddress] = new(big.Rat).SetFrac(rate.Numerator, rate.Denominator)
return getExchangeRatesForTokens(directory, currencies)
}

// GetFeeCurrencyContext returns the fee currency block context for all registered gas currencies from CELO
func GetFeeCurrencyContext(caller *CeloBackend) (common.FeeCurrencyContext, error) {
var feeContext common.FeeCurrencyContext
directory, err := abigen.NewFeeCurrencyDirectoryCaller(addresses.GetAddresses(caller.ChainConfig.ChainID).FeeCurrencyDirectory, caller)
if err != nil {
return feeContext, fmt.Errorf("failed to access FeeCurrencyDirectory: %w", err)
}

return exchangeRates, nil
currencies, err := GetRegisteredCurrencies(directory)
if err != nil {
return feeContext, err
}
rates, err := getExchangeRatesForTokens(directory, currencies)
if err != nil {
return feeContext, err
}
intrinsicGas, err := getIntrinsicGasForTokens(directory, currencies)
if err != nil {
return feeContext, err
}
return common.FeeCurrencyContext{
ExchangeRates: rates,
IntrinsicGasCosts: intrinsicGas,
}, nil
}

// GetBalanceERC20 returns an account's balance on a given ERC20 currency
Expand Down Expand Up @@ -167,3 +206,41 @@ func GetFeeBalance(backend *CeloBackend, account common.Address, feeCurrency *co
}
return balance
}

// getIntrinsicGasForTokens returns the intrinsic gas costs for the provided gas currencies from CELO
func getIntrinsicGasForTokens(caller *abigen.FeeCurrencyDirectoryCaller, tokens []common.Address) (common.IntrinsicGasCosts, error) {
gasCosts := common.IntrinsicGasCosts{}
for _, tokenAddress := range tokens {
config, err := caller.GetCurrencyConfig(&bind.CallOpts{}, tokenAddress)
if err != nil {
log.Error("Failed to get intrinsic gas cost for gas currency!", "err", err, "tokenAddress", tokenAddress.Hex())
continue
}
if !config.IntrinsicGas.IsUint64() {
log.Error("Intrinsic gas cost exceeds MaxUint64 limit, capping at MaxUint64", "err", err, "tokenAddress", tokenAddress.Hex())
gasCosts[tokenAddress] = math.MaxUint64
} else {
gasCosts[tokenAddress] = config.IntrinsicGas.Uint64()
}
}
return gasCosts, nil
}

// getExchangeRatesForTokens returns the exchange rates for the provided gas currencies from CELO
func getExchangeRatesForTokens(caller *abigen.FeeCurrencyDirectoryCaller, tokens []common.Address) (common.ExchangeRates, error) {
exchangeRates := common.ExchangeRates{}
for _, tokenAddress := range tokens {
rate, err := caller.GetExchangeRate(&bind.CallOpts{}, tokenAddress)
if err != nil {
log.Error("Failed to get medianRate for gas currency!", "err", err, "tokenAddress", tokenAddress.Hex())
continue
}
if rate.Numerator.Sign() <= 0 || rate.Denominator.Sign() <= 0 {
log.Error("Bad exchange rate for fee currency", "tokenAddress", tokenAddress.Hex(), "numerator", rate.Numerator, "denominator", rate.Denominator)
continue
}
exchangeRates[tokenAddress] = new(big.Rat).SetFrac(rate.Numerator, rate.Denominator)
}

return exchangeRates, nil
}
2 changes: 1 addition & 1 deletion core/bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func genValueTx(nbytes int) func(int, *BlockGen) {
return func(i int, gen *BlockGen) {
toaddr := common.Address{}
data := make([]byte, nbytes)
gas, _ := IntrinsicGas(data, nil, false, false, false, false, nil)
gas, _ := IntrinsicGas(data, nil, false, false, false, false, nil, common.IntrinsicGasCosts{})
signer := gen.Signer()
gasPrice := big.NewInt(0)
if gen.header.BaseFee != nil {
Expand Down
18 changes: 14 additions & 4 deletions core/celo_evm.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,19 @@ import (
"github.com/ethereum/go-ethereum/params"
)

// XXYXX
func setCeloFieldsInBlockContext(blockContext *vm.BlockContext, header *types.Header, config *params.ChainConfig, statedb vm.StateDB) {
blockContext.ExchangeRates = GetExchangeRates(header, config, statedb)
if !config.IsCel2(header.Time) {
return
}

caller := &contracts.CeloBackend{ChainConfig: config, State: statedb}

feeCurrencyContext, err := contracts.GetFeeCurrencyContext(caller)
if err != nil {
log.Error("Error fetching exchange rates!", "err", err)
}
blockContext.FeeCurrencyContext = feeCurrencyContext
}

func GetExchangeRates(header *types.Header, config *params.ChainConfig, statedb vm.StateDB) common.ExchangeRates {
Expand All @@ -20,10 +31,9 @@ func GetExchangeRates(header *types.Header, config *params.ChainConfig, statedb

caller := &contracts.CeloBackend{ChainConfig: config, State: statedb}

// Add fee currency exchange rates
exchangeRates, err := contracts.GetExchangeRates(caller)
feeCurrencyContext, err := contracts.GetFeeCurrencyContext(caller)
if err != nil {
log.Error("Error fetching exchange rates!", "err", err)
}
return exchangeRates
return feeCurrencyContext.ExchangeRates
}
2 changes: 1 addition & 1 deletion core/state_prefetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func (p *statePrefetcher) Prefetch(block *types.Block, statedb *state.StateDB, c
return
}
// Convert the transaction into an executable message and pre-cache its sender
msg, err := TransactionToMessage(tx, signer, header.BaseFee, blockContext.ExchangeRates)
msg, err := TransactionToMessage(tx, signer, header.BaseFee, blockContext.FeeCurrencyContext.ExchangeRates)
if err != nil {
return // Also invalid block, bail out
}
Expand Down
Loading

0 comments on commit f1fb53c

Please sign in to comment.