diff --git a/e2e_test/debug-fee-currency/lib.sh b/e2e_test/debug-fee-currency/lib.sh index a7077c774e..ba34eec7ca 100755 --- a/e2e_test/debug-fee-currency/lib.sh +++ b/e2e_test/debug-fee-currency/lib.sh @@ -10,10 +10,14 @@ set -xeo pipefail # $3: highGasOnCredit (bool) # if true, this will make the DebugFeeCurrenc.CreditFees() call use # a high amount of gas +# $4: intrinsicGas (num): +# intrinsic gas set for the fee currency. If it's not set, it will +# default to 60000. # returns: # deployed fee-currency address function deploy_fee_currency() { ( + DEFAULT_INTRINSIC_GAS=60000 local fee_currency=$( forge create --root "$SCRIPT_DIR/debug-fee-currency" --contracts "$SCRIPT_DIR/debug-fee-currency" --private-key $ACC_PRIVKEY DebugFeeCurrency.sol:DebugFeeCurrency --constructor-args '100000000000000000000000000' $1 $2 $3 --json | jq .deployedTo -r ) @@ -22,7 +26,7 @@ function deploy_fee_currency() { fi # this always resets the token address for the predeployed oracle3 cast send --private-key $ACC_PRIVKEY $ORACLE3 'setExchangeRate(address, uint256, uint256)' $fee_currency 2ether 1ether > /dev/null - cast send --private-key $ACC_PRIVKEY $FEE_CURRENCY_DIRECTORY_ADDR 'setCurrencyConfig(address, address, uint256)' $fee_currency $ORACLE3 60000 > /dev/null + cast send --private-key $ACC_PRIVKEY $FEE_CURRENCY_DIRECTORY_ADDR 'setCurrencyConfig(address, address, uint256)' $fee_currency $ORACLE3 ${4:-$DEFAULT_INTRINSIC_GAS} > /dev/null echo "$fee_currency" ) } @@ -76,3 +80,12 @@ function assert_cip_64_tx() { fi echo "$value" | jq .error | grep -qE "$expected_error" } + +# args: +# $1: value (num): +# value to send in the transaction +# $2: feeCurrencyAddress (string): +# which fee-currency address to use for the default CIP-64 transaction +function estimate_tx() { + $SCRIPT_DIR/js-tests/estimate_tx.mjs "$(cast chain-id)" $1 $2 +} \ No newline at end of file diff --git a/e2e_test/js-tests/estimate_tx.mjs b/e2e_test/js-tests/estimate_tx.mjs new file mode 100755 index 0000000000..bf781cec6b --- /dev/null +++ b/e2e_test/js-tests/estimate_tx.mjs @@ -0,0 +1,21 @@ +#!/usr/bin/env node +import { publicClient, account } from "./viem_setup.mjs" + +const [chainId, celoValue, feeCurrency] = process.argv.slice(2); + +async function main() { + let bigCeloValue = BigInt(celoValue) + + let result = await publicClient.estimateGas({ + account, + to: "0x00000000000000000000000000000000DeaDBeef", + value: bigCeloValue, + feeCurrency + }); + console.log(result.toString()) + + return result; +} + +await main(); +process.exit(0); diff --git a/e2e_test/js-tests/test_ethers_tx.mjs b/e2e_test/js-tests/test_ethers_tx.mjs index 979150af9f..b65274994f 100644 --- a/e2e_test/js-tests/test_ethers_tx.mjs +++ b/e2e_test/js-tests/test_ethers_tx.mjs @@ -2,7 +2,12 @@ import { assert } from "chai"; import "mocha"; import { ethers } from "ethers"; -const provider = new ethers.JsonRpcProvider(process.env.ETH_RPC_URL); +let provider +if (process.env.ETH_RPC_URL.startsWith("ws")) { + provider = new ethers.WebSocketProvider(process.env.ETH_RPC_URL); +} else { + provider = new ethers.JsonRpcProvider(process.env.ETH_RPC_URL); +} const signer = new ethers.Wallet(process.env.ACC_PRIVKEY, provider); describe("ethers.js send tx", () => { @@ -50,3 +55,10 @@ describe("ethers.js compatibility tests with state", () => { assert.isTrue(fullBlock.hasOwnProperty("baseFeePerGas")); }); }); + +// Close the WebSocket connection after all tests +after(async () => { + if (provider instanceof ethers.WebSocketProvider) { + await provider.destroy(); // Close the WebSocket connection + } +}); \ No newline at end of file diff --git a/e2e_test/js-tests/viem_setup.mjs b/e2e_test/js-tests/viem_setup.mjs index cf32258dbc..7868806ec3 100644 --- a/e2e_test/js-tests/viem_setup.mjs +++ b/e2e_test/js-tests/viem_setup.mjs @@ -4,6 +4,7 @@ import { createPublicClient, createWalletClient, http, + webSocket, defineChain, } from "viem"; import { celo, celoAlfajores } from "viem/chains"; @@ -17,6 +18,7 @@ const devChain = defineChain({ rpcUrls: { default: { http: [process.env.ETH_RPC_URL], + webSocket: [process.env.ETH_RPC_URL], }, }, }); @@ -28,6 +30,7 @@ const celoBaklava = defineChain({ rpcUrls: { default: { http: [process.env.ETH_RPC_URL], + webSocket: [process.env.ETH_RPC_URL], }, }, }); @@ -37,6 +40,7 @@ const celoMainnet = defineChain({ rpcUrls: { default: { http: [process.env.ETH_RPC_URL], + webSocket: [process.env.ETH_RPC_URL], }, }, }); @@ -54,14 +58,25 @@ const chain = (() => { }; })(); +const transportForNetwork = (() => { + switch (process.env.NETWORK) { + case 'alfajores': + case 'baklava': + case 'mainnet': + return webSocket(process.env.ETH_RPC_URL); + default: + return http(process.env.ETH_RPC_URL); + }; +}) + // Set up clients/wallet export const publicClient = createPublicClient({ chain: chain, - transport: http(), + transport: transportForNetwork(), }); export const account = privateKeyToAccount(process.env.ACC_PRIVKEY); export const walletClient = createWalletClient({ account, chain: chain, - transport: http(), + transport: transportForNetwork(), }); diff --git a/e2e_test/run_all_tests.sh b/e2e_test/run_all_tests.sh index 9d0675abd1..fc9e06fe9b 100755 --- a/e2e_test/run_all_tests.sh +++ b/e2e_test/run_all_tests.sh @@ -39,7 +39,7 @@ for f in test_*"$TEST_GLOB"*; do if [[ -n $NETWORK ]]; then case $f in # Skip tests that require a local network. - test_fee_currency_fails_on_credit.sh|test_fee_currency_fails_on_debit.sh|test_fee_currency_fails_intrinsic.sh|test_value_and_fee_currency_balance_check.sh) + test_fee_currency_fails_on_credit.sh|test_fee_currency_fails_on_debit.sh|test_fee_currency_fails_intrinsic.sh|test_value_and_fee_currency_balance_check.sh|test_fee_currency_gas_estimation.sh) echo "skipping file $f" continue ;; diff --git a/e2e_test/shared.sh b/e2e_test/shared.sh index c9a04bf29e..2e2d85a633 100644 --- a/e2e_test/shared.sh +++ b/e2e_test/shared.sh @@ -10,7 +10,7 @@ case $NETWORK in # cast call 0x000000000000000000000000000000000000ce10 "getAddressForStringOrDie(string calldata identifier) returns (address)" $contract # end mainnet) - export ETH_RPC_URL=https://forno.celo.org + export ETH_RPC_URL=wss://forno.celo.org/ws export TOKEN_ADDR=0x471EcE3750Da237f93B8E339c536989b8978a438 export FEE_HANDLER=0xcD437749E43A154C07F3553504c68fBfD56B8778 export FEE_CURRENCY=0xD8763CBa276a3738E6DE85b4b3bF5FDed6D6cA73 @@ -18,7 +18,7 @@ case $NETWORK in echo "Using mainnet network" ;; alfajores) - export ETH_RPC_URL=https://alfajores-forno.celo-testnet.org + export ETH_RPC_URL=wss://alfajores-forno.celo-testnet.org/ws export TOKEN_ADDR=0xF194afDf50B03e69Bd7D057c1Aa9e10c9954E4C9 export FEE_HANDLER=0xEAaFf71AB67B5d0eF34ba62Ea06Ac3d3E2dAAA38 export FEE_CURRENCY=0x4822e58de6f5e485eF90df51C41CE01721331dC0 @@ -26,7 +26,7 @@ case $NETWORK in echo "Using Alfajores network" ;; baklava) - export ETH_RPC_URL=https://baklava-forno.celo-testnet.org + export ETH_RPC_URL=wss://baklava-forno.celo-testnet.org/ws export TOKEN_ADDR=0xdDc9bE57f553fe75752D61606B94CBD7e0264eF8 export FEE_HANDLER=0xeed0A69c51079114C280f7b936C79e24bD94013e export FEE_CURRENCY=0x62492A644A588FD904270BeD06ad52B9abfEA1aE diff --git a/e2e_test/test_fee_currency_gas_estimation.sh b/e2e_test/test_fee_currency_gas_estimation.sh new file mode 100755 index 0000000000..23e90f6a4d --- /dev/null +++ b/e2e_test/test_fee_currency_gas_estimation.sh @@ -0,0 +1,17 @@ +#!/bin/bash +#shellcheck disable=SC2086 +set -eo pipefail + +source shared.sh +source debug-fee-currency/lib.sh + +fee_currency=$(deploy_fee_currency false false false 70000) +gas=$(estimate_tx 20 $fee_currency) + +cleanup_fee_currency $fee_currency + +# intrinsic of fee_currency: 70000 +# intrinsic of tx: 21000 +# total: 91000 +if [ $gas -ne 91000 ]; then exit 1; fi + diff --git a/eth/gasestimator/gasestimator.go b/eth/gasestimator/gasestimator.go index 27c79b8c44..d54b4f7f2e 100644 --- a/eth/gasestimator/gasestimator.go +++ b/eth/gasestimator/gasestimator.go @@ -25,6 +25,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/exchange" + "github.com/ethereum/go-ethereum/contracts" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" @@ -52,7 +53,17 @@ type Options struct { // Estimate returns the lowest possible gas limit that allows the transaction to // run successfully with the provided context options. It returns an error if the // transaction would always revert, or if there are unexpected failures. -func Estimate(ctx context.Context, call *core.Message, opts *Options, gasCap uint64, exchangeRates common.ExchangeRates, feeCurrencyBalance *big.Int) (uint64, []byte, error) { +func Estimate(ctx context.Context, call *core.Message, opts *Options, gasCap uint64) (uint64, []byte, error) { + // Celo specific: get balance of fee currency if fee currency is specified + feeCurrencyBalance := new(big.Int) + if call.FeeCurrency != nil { + feeCurrencyBalance = getFeeBalance(call, opts) + } + + feeCurrencyContext := core.GetFeeCurrencyContext(opts.Header, opts.Config, opts.State) + // currency intrinsic gas for celo = 0 + extraIntrinsicGas, _ := common.CurrencyIntrinsicGasCost(feeCurrencyContext.IntrinsicGasCosts, call.FeeCurrency) + // Binary search the gas limit, as it may need to be higher than the amount used var ( lo uint64 // lowest-known gas limit where tx execution fails @@ -60,7 +71,7 @@ func Estimate(ctx context.Context, call *core.Message, opts *Options, gasCap uin ) // Determine the highest gas limit can be used during the estimation. hi = opts.Header.GasLimit - if call.GasLimit >= params.TxGas { + if call.GasLimit >= (params.TxGas + extraIntrinsicGas) { hi = call.GasLimit } // Normalize the max fee per gas the call is willing to spend. @@ -81,7 +92,7 @@ func Estimate(ctx context.Context, call *core.Message, opts *Options, gasCap uin // CIP-66, prices are given in native token. // We need to check the allowance in the converted feeCurrency var err error - feeCap, err = exchange.ConvertCeloToCurrency(exchangeRates, call.FeeCurrency, feeCap) + feeCap, err = exchange.ConvertCeloToCurrency(feeCurrencyContext.ExchangeRates, call.FeeCurrency, feeCap) if err != nil { return 0, nil, err } @@ -140,15 +151,20 @@ func Estimate(ctx context.Context, call *core.Message, opts *Options, gasCap uin // unused access list items). Ever so slightly wasteful, but safer overall. if len(call.Data) == 0 { if call.To != nil && opts.State.GetCodeSize(*call.To) == 0 { - failed, _, err := execute(ctx, call, opts, params.TxGas) + gasLimit := params.TxGas + if call.FeeCurrency != nil { + // if the feeCurrency is not supported, the returned extraIntrinsicGas is 0, which will end up failing before the balance check + gasLimit += extraIntrinsicGas + } + failed, _, err := execute(ctx, call, opts, gasLimit, feeCurrencyContext) if !failed && err == nil { - return params.TxGas, nil, nil + return gasLimit, nil, nil } } } // We first execute the transaction at the highest allowable gas limit, since if this fails we // can return error immediately. - failed, result, err := execute(ctx, call, opts, hi) + failed, result, err := execute(ctx, call, opts, hi, feeCurrencyContext) if err != nil { return 0, nil, err } @@ -170,7 +186,7 @@ func Estimate(ctx context.Context, call *core.Message, opts *Options, gasCap uin // check that gas amount and use as a limit for the binary search. optimisticGasLimit := (result.UsedGas + result.RefundedGas + params.CallStipend) * 64 / 63 if optimisticGasLimit < hi { - failed, _, err = execute(ctx, call, opts, optimisticGasLimit) + failed, _, err = execute(ctx, call, opts, optimisticGasLimit, feeCurrencyContext) if err != nil { // This should not happen under normal conditions since if we make it this far the // transaction had run without error at least once before. @@ -201,7 +217,7 @@ func Estimate(ctx context.Context, call *core.Message, opts *Options, gasCap uin // range here is skewed to favor the low side. mid = lo * 2 } - failed, _, err = execute(ctx, call, opts, mid) + failed, _, err = execute(ctx, call, opts, mid, feeCurrencyContext) if err != nil { // This should not happen under normal conditions since if we make it this far the // transaction had run without error at least once before. @@ -221,14 +237,14 @@ func Estimate(ctx context.Context, call *core.Message, opts *Options, gasCap uin // returns true if the transaction fails for a reason that might be related to // not enough gas. A non-nil error means execution failed due to reasons unrelated // to the gas limit. -func execute(ctx context.Context, call *core.Message, opts *Options, gasLimit uint64) (bool, *core.ExecutionResult, error) { +func execute(ctx context.Context, call *core.Message, opts *Options, gasLimit uint64, feeCurrencyContext *common.FeeCurrencyContext) (bool, *core.ExecutionResult, error) { // Configure the call for this specific execution (and revert the change after) defer func(gas uint64) { call.GasLimit = gas }(call.GasLimit) call.GasLimit = gasLimit // Execute the call and separate execution faults caused by a lack of gas or // other non-fixable conditions - result, err := run(ctx, call, opts) + result, err := run(ctx, call, opts, feeCurrencyContext) if err != nil { if errors.Is(err, core.ErrIntrinsicGas) { return true, nil, nil // Special case, raise gas limit @@ -240,12 +256,11 @@ func execute(ctx context.Context, call *core.Message, opts *Options, gasLimit ui // run assembles the EVM as defined by the consensus rules and runs the requested // call invocation. -func run(ctx context.Context, call *core.Message, opts *Options) (*core.ExecutionResult, error) { +func run(ctx context.Context, call *core.Message, opts *Options, feeCurrencyContext *common.FeeCurrencyContext) (*core.ExecutionResult, error) { // Assemble the call and the call context var ( - feeCurrencyContext = core.GetFeeCurrencyContext(opts.Header, opts.Config, opts.State) - evmContext = core.NewEVMBlockContext(opts.Header, opts.Chain, nil, opts.Config, opts.State, feeCurrencyContext) - dirtyState = opts.State.Copy() + evmContext = core.NewEVMBlockContext(opts.Header, opts.Chain, nil, opts.Config, opts.State, feeCurrencyContext) + dirtyState = opts.State.Copy() ) if opts.BlockOverrides != nil { opts.BlockOverrides.Apply(&evmContext) @@ -280,3 +295,11 @@ func run(ctx context.Context, call *core.Message, opts *Options) (*core.Executio } return result, nil } + +func getFeeBalance(call *core.Message, opts *Options) *big.Int { + cb := &contracts.CeloBackend{ + ChainConfig: opts.Config, + State: opts.State, + } + return contracts.GetFeeBalance(cb, call.From, call.FeeCurrency) +} diff --git a/ethclient/ethclient.go b/ethclient/ethclient.go index 1863a83985..9bb174bc8c 100644 --- a/ethclient/ethclient.go +++ b/ethclient/ethclient.go @@ -717,6 +717,9 @@ func toCallArg(msg ethereum.CallMsg) interface{} { if msg.BlobHashes != nil { arg["blobVersionedHashes"] = msg.BlobHashes } + if msg.FeeCurrency != nil { + arg["feeCurrency"] = msg.FeeCurrency + } return arg } diff --git a/interfaces.go b/interfaces.go index 53e2e3ae16..e4d87db18f 100644 --- a/interfaces.go +++ b/interfaces.go @@ -156,6 +156,9 @@ type CallMsg struct { // For BlobTxType BlobGasFeeCap *big.Int BlobHashes []common.Hash + + // For CeloDynamicFeeTxType + FeeCurrency *common.Address } // A ContractCaller provides contract calls, essentially transactions that are executed by diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index ab88d04a0b..96b909062d 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -963,17 +963,8 @@ func DoEstimateGas(ctx context.Context, b CeloBackend, args TransactionArgs, blo call := args.ToMessage(header.BaseFee, true, true, exchangeRates) - // Celo specific: get balance of fee currency if fee currency is specified - feeCurrencyBalance := new(big.Int) - if args.FeeCurrency != nil { - feeCurrencyBalance, err = b.GetFeeBalance(ctx, blockNrOrHash, call.From, args.FeeCurrency) - if err != nil { - return 0, err - } - } - // Run the gas estimation and wrap any revertals into a custom return - estimate, revert, err := gasestimator.Estimate(ctx, call, opts, gasCap, exchangeRates, feeCurrencyBalance) + estimate, revert, err := gasestimator.Estimate(ctx, call, opts, gasCap) if err != nil { if len(revert) > 0 { return 0, newRevertError(revert)