Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Read IntrinsicGas from FeeCurrencyDirectory #178

Merged
merged 8 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion cmd/evm/internal/t8ntool/execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,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 ErrNonWhitelistedFeeCurrency error for now.
msg, err := core.TransactionToMessage(tx, signer, pre.Env.BaseFee, vmContext.FeeCurrencyContext.ExchangeRates)
palango marked this conversation as resolved.
Show resolved Hide resolved
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 ErrNonWhitelistedFeeCurrency error for now.
var feeIntrinsic common.IntrinsicGasCosts
if gas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.To() == nil,
ezdac marked this conversation as resolved.
Show resolved Hide resolved
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
}
ezdac marked this conversation as resolved.
Show resolved Hide resolved

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
}
Comment on lines +34 to +36
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to look up why feeCurrency cannot be nil here. I think a comment would be helpful. Or maybe we can even panic.

Copy link
Author

@ezdac ezdac Aug 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason it is there is to guard the pointer deref just below and because I thought it would make sense that the additional intrinsic gas cost of a nil currency is 0.
This also translates to an additional fee-currency specific max gas allowance for EVM calls as used by the MaxAllowedIntrinsicGasCost for the credit/debit EVM calls.
This then will be 0 and will cause the EVM calls to revert with out of gas, if ever called with a nil currency.

However this is all theoretical and currently there is no code-path where it comes to this, since the CreditFees and DebitFees are only called in non-nil fee-currency code-branches.

I will put the above in a comment.

gasCost, ok := i[*feeCurrency]
if !ok {
return 0, false
}
return gasCost, true
}

func CurrencyWhitelist(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 @@ -57,14 +57,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
152 changes: 115 additions & 37 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, exchange.ErrNonWhitelistedFeeCurrency
}

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 exchange.ErrNonWhitelistedFeeCurrency
}

maxAllowedGasForCredit := maxAllowedGasForDebitAndCredit - evm.Context.GasUsedForDebit
maxAllowedGasForCredit := maxAllowedGasForDebitAndCredit - gasUsedDebit
leftoverGas, err := evm.CallWithABI(
feeCurrencyABI, "creditGasFees", *feeCurrency, maxAllowedGasForCredit,
// function creditGasFees(
Expand All @@ -101,43 +111,73 @@ 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 exchange.ErrNonWhitelistedFeeCurrency
}
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 bind.ContractCaller) (common.ExchangeRates, error) {
exchangeRates := map[common.Address]*big.Rat{}
directory, err := abigen.NewFeeCurrencyDirectoryCaller(addresses.FeeCurrencyDirectoryAddress, caller)
if err != nil {
return 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 bind.ContractCaller) (common.FeeCurrencyContext, error) {
var feeContext common.FeeCurrencyContext
directory, err := abigen.NewFeeCurrencyDirectoryCaller(addresses.FeeCurrencyDirectoryAddress, 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 +207,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
4 changes: 2 additions & 2 deletions core/celo_evm.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ func setCeloFieldsInBlockContext(blockContext *vm.BlockContext, header *types.He

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

// Add fee currency exchange rates
// Add fee currency context
var err error
blockContext.ExchangeRates, err = contracts.GetExchangeRates(caller)
blockContext.FeeCurrencyContext, err = contracts.GetFeeCurrencyContext(caller)
if err != nil {
log.Error("Error fetching exchange rates!", "err", err)
}
Expand Down
2 changes: 1 addition & 1 deletion core/state_prefetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,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
6 changes: 3 additions & 3 deletions core/state_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg
}
// Iterate over and process the individual transactions
for i, tx := range block.Transactions() {
msg, err := TransactionToMessage(tx, signer, header.BaseFee, context.ExchangeRates)
msg, err := TransactionToMessage(tx, signer, header.BaseFee, context.FeeCurrencyContext.ExchangeRates)
if err != nil {
return nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err)
}
Expand Down Expand Up @@ -156,7 +156,7 @@ func applyTransaction(msg *Message, config *params.ChainConfig, gp *GasPool, sta
if tx.Type() == types.CeloDynamicFeeTxV2Type {
alternativeBaseFee := evm.Context.BaseFee
if msg.FeeCurrency != nil {
alternativeBaseFee, err = exchange.ConvertCeloToCurrency(evm.Context.ExchangeRates, msg.FeeCurrency, evm.Context.BaseFee)
alternativeBaseFee, err = exchange.ConvertCeloToCurrency(evm.Context.FeeCurrencyContext.ExchangeRates, msg.FeeCurrency, evm.Context.BaseFee)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -189,7 +189,7 @@ func applyTransaction(msg *Message, config *params.ChainConfig, gp *GasPool, sta
func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *common.Address, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction, usedGas *uint64, cfg vm.Config) (*types.Receipt, error) {
// Create a new context to be used in the EVM environment
blockContext := NewEVMBlockContext(header, bc, author, config, statedb)
msg, err := TransactionToMessage(tx, types.MakeSigner(config, header.Number, header.Time), header.BaseFee, blockContext.ExchangeRates)
msg, err := TransactionToMessage(tx, types.MakeSigner(config, header.Number, header.Time), header.BaseFee, blockContext.FeeCurrencyContext.ExchangeRates)
if err != nil {
return nil, err
}
Expand Down
Loading