Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
32 changes: 24 additions & 8 deletions fvm/evm/emulator/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
gethVM "github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/eth/tracers"
gethParams "github.com/ethereum/go-ethereum/params"
"github.com/holiman/uint256"

"github.com/onflow/flow-go/fvm/evm/types"
)
Expand All @@ -25,6 +26,10 @@ var (
PreviewnetOsakaActivation = uint64(0) // already on Osaka for PreviewNet
TestnetOsakaActivation = uint64(1763575200) // Wednesday, November 19, 2025 18:00:00 GMT+0000
MainnetOsakaActivation = uint64(1764784800) // Wednesday, December 03, 2025 18:00:00 GMT+0000

PreviewnetAmsterdamActivation = uint64(0) // already on Amsterdam for PreviewNet
TestnetAmsterdamActivation = uint64(1798740000) // Thursday, December 31, 2026 at 18:00:00 GMT+0000
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

TestnetAmsterdamActivation & MainnetAmsterdamActivation will be updated accordingly when there's official activation times.

MainnetAmsterdamActivation = uint64(1798740000) // Thursday, December 31, 2026 at 18:00:00 GMT+0000
)

// Config aggregates all the configuration (chain, evm, block, tx, ...)
Expand Down Expand Up @@ -102,22 +107,26 @@ func MakeChainConfig(chainID *big.Int) *gethParams.ChainConfig {
MuirGlacierBlock: bigZero, // already on MuirGlacier

// Fork scheduling based on timestamps
ShanghaiTime: &zero, // already on Shanghai
CancunTime: &zero, // already on Cancun
PragueTime: nil, // this is conditionally set below
OsakaTime: nil, // this is conditionally set below
VerkleTime: nil, // not on Verkle
ShanghaiTime: &zero, // already on Shanghai
CancunTime: &zero, // already on Cancun
PragueTime: nil, // this is conditionally set below
OsakaTime: nil, // this is conditionally set below
AmsterdamTime: nil, // this is conditionally set below
UBTTime: nil, // not on Verkle (a.k.a UBT)
}

if chainID.Cmp(types.FlowEVMPreviewNetChainID) == 0 {
chainConfig.PragueTime = &PreviewnetPragueActivation
chainConfig.OsakaTime = &PreviewnetOsakaActivation
chainConfig.AmsterdamTime = &PreviewnetAmsterdamActivation
} else if chainID.Cmp(types.FlowEVMTestNetChainID) == 0 {
chainConfig.PragueTime = &TestnetPragueActivation
chainConfig.OsakaTime = &TestnetOsakaActivation
chainConfig.AmsterdamTime = &TestnetAmsterdamActivation
} else if chainID.Cmp(types.FlowEVMMainNetChainID) == 0 {
chainConfig.PragueTime = &MainnetPragueActivation
chainConfig.OsakaTime = &MainnetOsakaActivation
chainConfig.AmsterdamTime = &MainnetAmsterdamActivation
}

return chainConfig
Expand All @@ -135,8 +144,7 @@ func defaultConfig() *Config {
NoBaseFee: true,
},
TxContext: &gethVM.TxContext{
GasPrice: new(big.Int),
BlobFeeCap: new(big.Int),
GasPrice: new(uint256.Int),
},
BlockContext: &gethVM.BlockContext{
CanTransfer: gethCore.CanTransfer,
Expand Down Expand Up @@ -188,7 +196,7 @@ func WithOrigin(origin gethCommon.Address) Option {
// WithGasPrice sets the gas price for the transaction (usually the one sets by the sender)
func WithGasPrice(gasPrice *big.Int) Option {
Copy link
Copy Markdown
Collaborator Author

@m-Peter m-Peter May 11, 2026

Choose a reason for hiding this comment

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

WithGasPrice seems to be not used in flow-go or flow-evm-gateway. Seems like a relic that we could remove entirely.

return func(c *Config) *Config {
c.TxContext.GasPrice = gasPrice
c.TxContext.GasPrice = uint256.MustFromBig(gasPrice)
return c
Comment thread
m-Peter marked this conversation as resolved.
}
}
Expand Down Expand Up @@ -264,6 +272,14 @@ func WithRandom(rand *gethCommon.Hash) Option {
}
}

// WithSlotNum sets the slot number in the block context
func WithSlotNum(slotNum uint64) Option {
return func(c *Config) *Config {
c.BlockContext.SlotNum = slotNum
return c
}
}

// WithTransactionTracer sets a transaction tracer
func WithTransactionTracer(tracer *tracers.Tracer) Option {
return func(c *Config) *Config {
Expand Down
166 changes: 109 additions & 57 deletions fvm/evm/emulator/emulator.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ func newConfig(ctx types.BlockContext) *Config {
WithExtraPrecompiledContracts(ctx.ExtraPrecompiledContracts),
WithGetBlockHashFunction(ctx.GetHashFunc),
WithRandom(&ctx.Random),
WithSlotNum(ctx.SlotNum),
WithTransactionTracer(ctx.Tracer),
WithBlockTotalGasUsedSoFar(ctx.TotalGasUsedSoFar),
WithBlockTxCountSoFar(ctx.TxCountSoFar),
Expand Down Expand Up @@ -162,6 +163,9 @@ func (bl *BlockView) DirectCall(call *types.DirectCall) (res *types.Result, err
case types.WithdrawCallSubType:
return proc.withdrawFrom(call)
case types.DeployCallSubType:
// this is effectively deploying only COAs, with the `to` field
// being generated by the COA address allocator and set as the
// deployment target address
if !call.EmptyToField() {
return proc.deployAt(call)
}
Expand Down Expand Up @@ -307,23 +311,55 @@ func (bl *BlockView) DryRunTransaction(
return nil, err
}

// convert tx into message
msg, err := gethCore.TransactionToMessage(
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

We can no longer use gethCore.TransactionToMessage, as it returns a nil Message when no valid signature is provided. For EVM.dryRun we do not have such signature values, so we have to reconstruct the Message ourselves.

tx,
GetSigner(bl.config),
proc.config.BlockContext.BaseFee,
)

// we can ignore invalid signature errors since we don't expect signed transactions
if !errors.Is(err, gethTypes.ErrInvalidSig) {
return nil, err
value, overflow := uint256.FromBig(tx.Value())
if overflow {
return nil, fmt.Errorf("value exceeds 256 bits: address %v", from.Hex())
}
gasPrice, overflow := uint256.FromBig(tx.GasPrice())
if overflow {
return nil, fmt.Errorf("%w: address %v, maxFeePerGas bit length: %d", gethCore.ErrFeeCapVeryHigh,
from.Hex(), tx.GasPrice().BitLen())
}
txGasFeeCap := tx.GasFeeCap()
gasFeeCap, overflow := uint256.FromBig(txGasFeeCap)
if overflow {
return nil, fmt.Errorf("%w: address %v, maxFeePerGas bit length: %d", gethCore.ErrFeeCapVeryHigh,
from.Hex(), tx.GasFeeCap().BitLen())
}
txGasTipCap := tx.GasTipCap()
gasTipCap, overflow := uint256.FromBig(txGasTipCap)
if overflow {
return nil, fmt.Errorf("%w: address %v, maxPriorityFeePerGas bit length: %d", gethCore.ErrTipVeryHigh,
from.Hex(), tx.GasTipCap().BitLen())
}

msg := &gethCore.Message{
From: from,
To: tx.To(),
Value: value,
Data: tx.Data(),
Nonce: tx.Nonce(),
GasLimit: tx.Gas(),
GasPrice: gasPrice,
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
SetCodeAuthorizations: tx.SetCodeAuthorizations(),
AccessList: tx.AccessList(),
SkipNonceChecks: true,
SkipTransactionChecks: true,
}

// If baseFee provided, set gasPrice to effectiveGasPrice.
baseFee := proc.config.BlockContext.BaseFee
if baseFee != nil {
effectiveGasPrice := new(big.Int).Add(baseFee, txGasTipCap)
if effectiveGasPrice.Cmp(txGasFeeCap) > 0 {
effectiveGasPrice = txGasFeeCap
}
// EffectiveGasPrice is already capped by txGasFeeCap, therefore
// the overflow check is not required.
msg.GasPrice = uint256.MustFromBig(effectiveGasPrice)
}

// use the from as the signer
msg.From = from
// we need to skip nonce/transaction checks for dry run
msg.SkipNonceChecks = true
msg.SkipTransactionChecks = true

// run and return without committing the state changes
return proc.run(msg, tx.Hash(), tx.Type())
Expand Down Expand Up @@ -524,14 +560,30 @@ func (proc *procedure) deployAt(
}()
}

addr := call.To.ToCommon()
caller := call.From.ToCommon()
// pre check 1 - check balance of the source
if call.Value.Sign() != 0 &&
!proc.evm.Context.CanTransfer(proc.state, call.From.ToCommon(), castedValue) {
!proc.evm.Context.CanTransfer(proc.state, caller, castedValue) {
res.SetValidationError(gethCore.ErrInsufficientFundsForTransfer)
return res, nil
}

// increment the nonce for the caller
nonce := proc.state.GetNonce(caller)
if nonce+1 < nonce {
res.SetValidationError(gethVM.ErrNonceUintOverflow)
return res, nil
}
proc.state.SetNonce(
caller,
nonce+1,
gethTracing.NonceChangeContractCreator,
)

addr := call.To.ToCommon()
// update access list (Berlin)
proc.state.AddAddressToAccessList(addr)

// pre check 2 - ensure there's no existing eoa or contract is deployed at the address
contractHash := proc.state.GetCodeHash(addr)
if proc.state.GetNonce(addr) != 0 ||
Expand All @@ -540,48 +592,49 @@ func (proc *procedure) deployAt(
return res, nil
}

callerCommon := call.From.ToCommon()
// setup caller if doesn't exist
if !proc.state.Exist(callerCommon) {
proc.state.CreateAccount(callerCommon)
// create a new account on the state only if the object was not present.
// it might be possible the contract code is deployed to a pre-existent
// account with non-zero balance.
if !proc.state.Exist(addr) {
proc.state.CreateAccount(addr)
}
// increment the nonce for the caller
proc.state.SetNonce(
callerCommon,
proc.state.GetNonce(callerCommon)+1,
gethTracing.NonceChangeContractCreator,
)

// setup account
proc.state.CreateAccount(addr)
// CreateContract means that regardless of whether the account previously existed
// in the state trie or not, it _now_ becomes created as a _contract_ account.
// This is performed _prior_ to executing the initcode, since the initcode
// acts inside that account.
proc.state.CreateContract(addr)
proc.state.SetNonce(addr, 1, gethTracing.NonceChangeNewContract) // (EIP-158)
rules := proc.config.ChainRules()
if call.Value.Sign() > 0 {
proc.evm.Context.Transfer( // transfer value
proc.state,
callerCommon,
caller,
addr,
uint256.MustFromBig(call.Value),
&rules,
)
}

// run code through interpreter
// this would check for errors and computes the final bytes to be stored under account
var err error
// initialise a new contract and set the code that is to be used by the EVM.
// the contract is a scoped environment for this execution context only.
gasBudget := gethVM.NewGasBudget(call.GasLimit)
contract := gethVM.NewContract(
callerCommon,
caller,
addr,
castedValue,
call.GasLimit,
gasBudget,
nil,
)

contract.SetCallCode(gethCrypto.Keccak256Hash(call.Data), call.Data)
// update access list (Berlin)
proc.state.AddAddressToAccessList(addr)
// explicitly set the code to a null hash to prevent caching of jump analysis
// for the initialization code.
contract.SetCallCode(gethCommon.Hash{}, call.Data)
contract.IsDeployment = true

ret, err := proc.evm.Run(contract, nil, false)
gasCost := uint64(len(ret)) * gethParams.CreateDataGas
res.GasConsumed = gasCost
createDataGas := uint64(len(ret)) * gethParams.CreateDataGas
res.GasConsumed = createDataGas

Comment thread
m-Peter marked this conversation as resolved.
// handle errors
if err != nil {
Expand All @@ -593,34 +646,37 @@ func (proc *procedure) deployAt(
return res, nil
}

// update gas usage
if gasCost > call.GasLimit {
// check whether the max code size has been exceeded
if err := gethVM.CheckMaxCodeSize(&rules, uint64(len(ret))); err != nil {
// consume all the remaining gas (Homestead)
res.GasConsumed = call.GasLimit
res.VMError = gethVM.ErrCodeStoreOutOfGas
res.VMError = gethVM.ErrMaxCodeSizeExceeded
return res, nil
}

// check max code size (EIP-158)
if len(ret) > gethParams.MaxCodeSize {
// reject code starting with 0xEF (EIP-3541)
if len(ret) >= 1 && ret[0] == 0xEF {
// consume all the remaining gas (Homestead)
res.GasConsumed = call.GasLimit
res.VMError = gethVM.ErrMaxCodeSizeExceeded
res.VMError = gethVM.ErrInvalidCode
return res, nil
}

// reject code starting with 0xEF (EIP-3541)
if len(ret) >= 1 && ret[0] == 0xEF {
// update gas usage
if !contract.UseGas(gethVM.GasCosts{RegularGas: createDataGas}, proc.evm.Config.Tracer, gethTracing.GasChangeCallCodeStorage) {
// consume all the remaining gas (Homestead)
res.GasConsumed = call.GasLimit
res.VMError = gethVM.ErrInvalidCode
res.VMError = gethVM.ErrCodeStoreOutOfGas
return res, nil
}

res.DeployedContractAddress = &call.To
res.CumulativeGasUsed = proc.config.BlockTotalGasUsedSoFar + res.GasConsumed

proc.state.SetCode(addr, ret, gethTracing.CodeChangeContractCreation)
if len(ret) > 0 {
proc.state.SetCode(addr, ret, gethTracing.CodeChangeContractCreation)
}

res.StateChangeCommitment, err = proc.commit(true)
return res, err
}
Expand Down Expand Up @@ -676,14 +732,10 @@ func (proc *procedure) run(
// Set gas pool based on block gas limit
// if the block gas limit is set to anything than max
// we need to update this code.
gasPool := (*gethCore.GasPool)(&proc.config.BlockContext.GasLimit)
gasPool := gethCore.NewGasPool(proc.config.BlockContext.GasLimit)
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I'm not sure why the gasPool used to receive the maximum value as the available gas:

DefaultBlockLevelGasLimit = uint64(math.MaxUint64)

but given that we apply only a single EVM message, per gasPool instance, it might be safer to do what Geth does:

// Do not panic if the gas pool is nil. This is allowed when executing
// a single message via RPC invocation.
if gp == nil {
	gp = NewGasPool(msg.GasLimit)
}

and use as a sane default the GasLimit from the given EVM message.


// transit the state
execResult, err := gethCore.ApplyMessage(
proc.evm,
msg,
gasPool,
)
execResult, err := gethCore.ApplyMessage(proc.evm, msg, gasPool)
if err != nil {
// if the error is a fatal error or a non-fatal state error or a backend err return it
// this condition should never happen given all StateDB errors are withheld for the commit time.
Expand Down
9 changes: 4 additions & 5 deletions fvm/evm/emulator/emulator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"
"testing"

gethABI "github.com/ethereum/go-ethereum/accounts/abi"
gethCommon "github.com/ethereum/go-ethereum/common"
gethTypes "github.com/ethereum/go-ethereum/core/types"
gethVM "github.com/ethereum/go-ethereum/core/vm"
Expand Down Expand Up @@ -191,8 +192,7 @@ func TestNativeTokenBridging(t *testing.T) {
RunWithNewEmulator(t, backend, rootAddr, func(env *emulator.Emulator) {
RunWithNewBlockView(t, env, func(blk types.BlockView) {
// Test withdraw amounts that overflow the UInt256 range
amount := big.NewInt(1)
amount.Lsh(amount, 256)
amount := new(big.Int).Lsh(big.NewInt(1), 256)

call := types.NewWithdrawCall(bridgeAccount, testAccount, amount, testAccountNonce)
res, err := blk.DirectCall(call)
Expand All @@ -207,9 +207,8 @@ func TestNativeTokenBridging(t *testing.T) {
})
RunWithNewEmulator(t, backend, rootAddr, func(env *emulator.Emulator) {
RunWithNewBlockView(t, env, func(blk types.BlockView) {
// Test withdraw amounts within the max range of UInt256
amount := big.NewInt(1)
amount.Lsh(amount, 255)
// Test withdraw amounts within the max range of Int256
amount := gethABI.MaxInt256

call := types.NewWithdrawCall(bridgeAccount, testAccount, amount, testAccountNonce)
res, err := blk.DirectCall(call)
Expand Down
Loading
Loading