Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5cdc0ba
Restrict access to known EOAs that have proven malicious activity
m-Peter Jan 1, 2026
a797953
Introduce the EVM.reclaimFundsFromAttackerEOAs function to perform re…
m-Peter Jan 1, 2026
04a0f1c
Update error message for restricted EOAs
m-Peter Jan 1, 2026
4eb1bbc
Update state commitment expected values
m-Peter Jan 1, 2026
5354ae7
Update fvm/evm/stdlib/contract.cdc
j1010001 Jan 1, 2026
aaefd3d
Apply suggestion from @j1010001
j1010001 Jan 1, 2026
f67ee68
move the msg.From check to proc.run
zhangchiqing Jan 2, 2026
4959dbb
update execution state
zhangchiqing Jan 2, 2026
046b973
Merge pull request #8286 from onflow/leo/check-msg-sender
zhangchiqing Jan 2, 2026
2ea4aab
Apply suggestions from code review
zhangchiqing Jan 2, 2026
23f7cd7
add test case for restricted eoa
zhangchiqing Jan 2, 2026
0b3de6a
add test case for TestIsRestrictedEOA
zhangchiqing Jan 2, 2026
2b970ea
add test case for TestIsRestrictedEOA
zhangchiqing Jan 2, 2026
71f46f0
add AN compatibility for v0.44.13
j1010001 Jan 2, 2026
2dd0141
remove redundent check
zhangchiqing Jan 2, 2026
39cfed4
fix lint
zhangchiqing Jan 2, 2026
31c420c
add restrict check to proc.deployAt
zhangchiqing Jan 2, 2026
a5ef155
add test case to verify restrict checks has been added to proc.deployAt
zhangchiqing Jan 2, 2026
979c7b0
fix the bootstrap state
zhangchiqing Jan 2, 2026
ec83075
fix the bootstrap state
zhangchiqing Jan 2, 2026
51e3bdd
Merge branch 'mpeter/disable-evm-state-mutation' into jan/pr-8272-wit…
j1010001 Jan 2, 2026
c3c07a7
Restrict access to EOAs only
m-Peter Jan 2, 2026
00880a9
Merge branch 'mpeter/disable-evm-state-mutation' into jan/pr-8272-wit…
j1010001 Jan 2, 2026
381c278
add AN compatibility for v0.44.14 - restrict only EOAs
j1010001 Jan 2, 2026
ee6c8e9
fix script execution for scripts that invokes system contract functio…
zhangchiqing Jan 2, 2026
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
2 changes: 2 additions & 0 deletions engine/common/version/version_control.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ var defaultCompatibilityOverrides = map[string]struct{}{
"0.44.0": {}, // mainnet, testnet
"0.44.1": {}, // mainnet, testnet
"0.44.7": {}, // mainnet, testnet
"0.44.10": {}, // mainnet, testnet
"0.44.14": {}, // mainnet, testnet
}

// VersionControl manages the version control system for the node.
Expand Down
4 changes: 2 additions & 2 deletions engine/execution/state/bootstrap/bootstrap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func TestBootstrapLedger(t *testing.T) {
}

func TestBootstrapLedger_ZeroTokenSupply(t *testing.T) {
expectedStateCommitmentBytes, _ := hex.DecodeString("8d73f47bfde633129a974c8e7ddf920d58ba1e5b7b0f2ca72e0cce85b3991b83")
expectedStateCommitmentBytes, _ := hex.DecodeString("33c80faa1cf40865168552e63744538dcc754234ab1a7b31ffb2e2272baaf524")
expectedStateCommitment, err := flow.ToStateCommitment(expectedStateCommitmentBytes)
require.NoError(t, err)

Expand Down Expand Up @@ -104,7 +104,7 @@ func TestBootstrapLedger_ZeroTokenSupply(t *testing.T) {
// - transaction fee deduction
// This tests that the state commitment has not changed for the bookkeeping parts of the transaction.
func TestBootstrapLedger_EmptyTransaction(t *testing.T) {
expectedStateCommitmentBytes, _ := hex.DecodeString("21a8e0c4542dac3655a5aa3587a99257546efa23ea288fed07aa07e0211302b6")
expectedStateCommitmentBytes, _ := hex.DecodeString("5c3dcf747d788225731e6df6bb5e32550a2a59eaf9b6e1458f4ee850778e42f2")
expectedStateCommitment, err := flow.ToStateCommitment(expectedStateCommitmentBytes)
require.NoError(t, err)

Expand Down
43 changes: 37 additions & 6 deletions fvm/evm/emulator/emulator.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"math/big"
"slices"

gethCommon "github.com/ethereum/go-ethereum/common"
gethCore "github.com/ethereum/go-ethereum/core"
Expand All @@ -21,6 +22,21 @@ import (
"github.com/onflow/flow-go/model/flow"
)

// List of EOAs with restricted access to EVM, due to malicious activity.
var restrictedEOAs = []gethCommon.Address{
gethCommon.HexToAddress("0x2e7C4b71397f10c93dC0C2ba6f8f179A47F994e1"),
gethCommon.HexToAddress("0x9D9247F5C3F3B78F7EE2C480B9CDaB91393Bf4D6"),
}

var restrictedEOAError = errors.New(
"this account has been restricted by the Community Governance Council in connection to a protocol exploit, please reach out to [email protected] for inquiries or information related to the attack",
)

// isRestrictedEOA checks if the given address is in the restricted EOAs list
func isRestrictedEOA(addr gethCommon.Address) bool {
return slices.Contains(restrictedEOAs, addr)
}

// Emulator wraps an EVM runtime where evm transactions
// and direct calls are accepted.
type Emulator struct {
Expand Down Expand Up @@ -189,7 +205,12 @@ func (bl *BlockView) RunTransaction(
if err != nil {
// this is not a fatal error (e.g. due to bad signature)
// not a valid transaction
return types.NewInvalidResult(tx, err), nil
return types.NewInvalidResult(tx.Type(), tx.Hash(), err), nil
}

// Restrict access to EVM, for EOAs with proven malicious activity
if isRestrictedEOA(msg.From) {
return types.NewInvalidResult(tx.Type(), tx.Hash(), restrictedEOAError), nil
}

// call tracer
Expand Down Expand Up @@ -244,7 +265,13 @@ func (bl *BlockView) BatchRunTransactions(txs []*gethTypes.Transaction) ([]*type
GetSigner(bl.config),
proc.config.BlockContext.BaseFee)
if err != nil {
batchResults[i] = types.NewInvalidResult(tx, err)
batchResults[i] = types.NewInvalidResult(tx.Type(), tx.Hash(), err)
continue
}

// Restrict access to EVM, for EOAs with proven malicious activity
if isRestrictedEOA(msg.From) {
batchResults[i] = types.NewInvalidResult(tx.Type(), tx.Hash(), restrictedEOAError)
continue
}

Expand Down Expand Up @@ -385,7 +412,8 @@ func (proc *procedure) mintTo(
value, isValid := checkAndConvertValue(call.Value)
if !isValid {
return types.NewInvalidResult(
call.Transaction(),
call.Type,
call.Hash(),
types.ErrInvalidBalance,
), nil
}
Expand Down Expand Up @@ -430,15 +458,17 @@ func (proc *procedure) withdrawFrom(
value, isValid := checkAndConvertValue(call.Value)
if !isValid {
return types.NewInvalidResult(
call.Transaction(),
call.Type,
call.Hash(),
types.ErrInvalidBalance,
), nil
}

// check balance is not prone to rounding error
if !types.AttoFlowBalanceIsValidForFlowVault(call.Value) {
return types.NewInvalidResult(
call.Transaction(),
call.Type,
call.Hash(),
types.ErrWithdrawBalanceRounding,
), nil
}
Expand Down Expand Up @@ -486,7 +516,8 @@ func (proc *procedure) deployAt(
castedValue, isValid := checkAndConvertValue(call.Value)
if !isValid {
return types.NewInvalidResult(
call.Transaction(),
call.Type,
call.Hash(),
types.ErrInvalidBalance,
), nil
}
Expand Down
82 changes: 82 additions & 0 deletions fvm/evm/emulator/restricted_eoa_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package emulator

import (
"testing"

gethCommon "github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestIsRestrictedEOA(t *testing.T) {
t.Run("restricted address 1 should return true", func(t *testing.T) {
addr := gethCommon.HexToAddress("0x2e7C4b71397f10c93dC0C2ba6f8f179A47F994e1")
result := isRestrictedEOA(addr)
assert.True(t, result, "first restricted address should be detected as restricted")
})

t.Run("restricted address 2 should return true", func(t *testing.T) {
addr := gethCommon.HexToAddress("0x9D9247F5C3F3B78F7EE2C480B9CDaB91393Bf4D6")
result := isRestrictedEOA(addr)
assert.True(t, result, "second restricted address should be detected as restricted")
})

t.Run("non-restricted address should return false", func(t *testing.T) {
addr := gethCommon.HexToAddress("0x1234567890123456789012345678901234567890")
result := isRestrictedEOA(addr)
assert.False(t, result, "non-restricted address should return false")
})

t.Run("empty address should return false", func(t *testing.T) {
addr := gethCommon.Address{}
result := isRestrictedEOA(addr)
assert.False(t, result, "empty address should return false")
})

t.Run("case sensitivity - same address with different case should match", func(t *testing.T) {
// Ethereum addresses are case-insensitive in hex representation
// but gethCommon.HexToAddress normalizes to lowercase
addr1 := gethCommon.HexToAddress("0x2e7C4b71397f10c93dC0C2ba6f8f179A47F994e1")
addr2 := gethCommon.HexToAddress("0x2e7c4b71397f10c93dc0c2ba6f8f179a47f994e1")
require.Equal(t, addr1, addr2, "addresses should be equal regardless of case")

result1 := isRestrictedEOA(addr1)
result2 := isRestrictedEOA(addr2)
assert.Equal(t, result1, result2, "results should be the same for same address")
assert.True(t, result1, "both should be detected as restricted")
})

t.Run("all addresses in restrictedEOAs list should be detected", func(t *testing.T) {
for _, addr := range restrictedEOAs {
result := isRestrictedEOA(addr)
assert.True(t, result, "address %s should be detected as restricted", addr.Hex())
}
})

t.Run("address not in list should return false", func(t *testing.T) {
// Test with addresses that are similar but not in the list
testAddresses := []gethCommon.Address{
gethCommon.HexToAddress("0x2e7C4b71397f10c93dC0C2ba6f8f179A47F994e0"), // one byte different
gethCommon.HexToAddress("0x9D9247F5C3F3B78F7EE2C480B9CDaB91393Bf4D7"), // one byte different
gethCommon.HexToAddress("0x0000000000000000000000000000000000000001"),
gethCommon.HexToAddress("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"),
}

for _, addr := range testAddresses {
// Skip if the address is actually in the restricted list
isInList := false
for _, restrictedAddr := range restrictedEOAs {
if addr == restrictedAddr {
isInList = true
break
}
}
if isInList {
continue
}

result := isRestrictedEOA(addr)
assert.False(t, result, "address %s should not be detected as restricted", addr.Hex())
}
})
}
20 changes: 18 additions & 2 deletions fvm/evm/stdlib/contract.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -690,8 +690,8 @@ access(all) contract EVM {
access(all)
fun run(tx: [UInt8], coinbase: EVMAddress): Result {
return InternalEVM.run(
tx: tx,
coinbase: coinbase.bytes
tx: tx,
coinbase: coinbase.bytes
) as! Result
}

Expand Down Expand Up @@ -1028,6 +1028,22 @@ access(all) contract EVM {
self.account.storage.save(<-create Heartbeat(), to: /storage/EVMHeartbeat)
}

/// This is only a temporary measure and will be removed immediately
/// after the remediation of the illicit tokens
// in the Dec 2025 security incident is complete.
/// This function can only be called from the `FlowServiceAccount` contract,
/// and only from the holder of `FlowServiceAccount.Administrator` resource.
access(account)
fun reclaimFundsFromAttackerEOAs(from: String, to: String, amount: UInt): Result {
return InternalEVM.call(
from: EVM.addressFromString(from).bytes,
to: EVM.addressFromString(to).bytes,
data: [],
gasLimit: 1_000_000,
value: amount
) as! Result
}

init() {
self.setupHeartbeat()
}
Expand Down
6 changes: 3 additions & 3 deletions fvm/evm/types/result.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@ type ResultSummary struct {

// NewInvalidResult creates a new result that hold transaction validation
// error as well as the defined gas cost for validation.
func NewInvalidResult(tx *gethTypes.Transaction, err error) *Result {
func NewInvalidResult(txType uint8, txHash gethCommon.Hash, err error) *Result {
return &Result{
TxType: tx.Type(),
TxHash: tx.Hash(),
TxType: txType,
TxHash: txHash,
ValidationError: err,
GasConsumed: InvalidTransactionGasCost,
}
Expand Down
20 changes: 20 additions & 0 deletions fvm/script.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,11 @@ func (executor *scriptExecutor) executeScript() error {
chainID := executor.ctx.Chain.ChainID()

if executor.ctx.EVMEnabled {
// Setup InternalEVM in both environments because:
// - Scripts execute in ScriptRuntimeEnv
// - But system contract invocations (e.g., getAccount().balance) use TxRuntimeEnv

// Setup InternalEVM in ScriptRuntimeEnv (for script execution)
err := evm.SetupEnvironment(
chainID,
executor.env,
Expand All @@ -211,6 +216,21 @@ func (executor *scriptExecutor) executeScript() error {
if err != nil {
return err
}

// Setup InternalEVM in TxRuntimeEnv (for system contract invocations)
// This solves the problem where FlowServiceAccount (which imports EVM) needs
// InternalEVM to be available during dependency checking when invoked via
// system contracts (e.g., getAccount().balance). Without this, type checking
// fails with "cannot find variable in this scope: `InternalEVM`" because
// system contract invocations use TxRuntimeEnv, not ScriptRuntimeEnv.
err = evm.SetupEnvironment(
chainID,
executor.env,
rt.TxRuntimeEnv,
)
if err != nil {
return err
}
}

value, err := rt.ExecuteScript(
Expand Down
6 changes: 3 additions & 3 deletions utils/unittest/execution_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const ServiceAccountPrivateKeySignAlgo = crypto.ECDSAP256
const ServiceAccountPrivateKeyHashAlgo = hash.SHA2_256

// Pre-calculated state commitment with root account with the above private key
const GenesisStateCommitmentHex = "035698fda15a78e87fc7920f4b8084606b689012864f90b26bb65fefa4adf9fe"
const GenesisStateCommitmentHex = "976de8ab609fcf1d7d9d9536527593aa28a0dd1e18eb58158ca3e1644e730329"

var GenesisStateCommitment flow.StateCommitment

Expand Down Expand Up @@ -87,10 +87,10 @@ func genesisCommitHexByChainID(chainID flow.ChainID) string {
return GenesisStateCommitmentHex
}
if chainID == flow.Testnet {
return "0a8012fdf9a6bfc31d8949ce76340f3059903fd508deac0638fa30dbd206e252"
return "8b0f83512ccbca71ca85447f788506181eadb83744c0f49bd0f3bd389ac555f8"
}
if chainID == flow.Sandboxnet {
return "e1c08b17f9e5896f03fe28dd37ca396c19b26628161506924fbf785834646ea1"
}
return "f4595c2d492bd930b2b37c3bf731d758a4a32874e41c1e0388d1091a633dd3d2"
return "a01cb6d7b23beb2cc8c03a31f2c536ab3aa1382d5f484c4276055e59023bfc8c"
}
Loading