diff --git a/cmd/util/ledger/migrations/change_contract_code_migration.go b/cmd/util/ledger/migrations/change_contract_code_migration.go index 8471ed2c7e5..91b037c7ca3 100644 --- a/cmd/util/ledger/migrations/change_contract_code_migration.go +++ b/cmd/util/ledger/migrations/change_contract_code_migration.go @@ -328,6 +328,7 @@ func SystemContractChanges(chainID flow.ChainID) []SystemContractChange { systemContracts.EVM, evm.ContractCode( systemContracts.FlowToken.Address, + true, ), ), } diff --git a/engine/execution/computation/manager.go b/engine/execution/computation/manager.go index 946f2457a00..f4d0d2cc748 100644 --- a/engine/execution/computation/manager.go +++ b/engine/execution/computation/manager.go @@ -242,6 +242,7 @@ func DefaultFVMOptions(chainID flow.ChainID, cadenceTracing bool, extensiveTraci CapabilityControllersEnabled: chainID != flow.Mainnet, }, )), + fvm.WithEVMEnabled(true), } if extensiveTracing { diff --git a/fvm/bootstrap.go b/fvm/bootstrap.go index 7aead199904..466045a453e 100644 --- a/fvm/bootstrap.go +++ b/fvm/bootstrap.go @@ -76,7 +76,13 @@ type BootstrapParams struct { minimumStorageReservation cadence.UFix64 storagePerFlow cadence.UFix64 restrictedAccountCreationEnabled cadence.Bool - setupEVMEnabled cadence.Bool + + // `setupEVMEnabled` == true && `evmAbiOnly` == true will enable the ABI-only EVM + // `setupEVMEnabled` == true && `evmAbiOnly` == false will enable the full EVM functionality + // `setupEVMEnabled` == false will disable EVM + // This will allow to quickly disable the ABI-only EVM, in case there's a bug or something. + setupEVMEnabled cadence.Bool + evmAbiOnly cadence.Bool // versionFreezePeriod is the number of blocks in the future where the version // changes are frozen. The Node version beacon manages the freeze period, @@ -219,6 +225,13 @@ func WithSetupEVMEnabled(enabled cadence.Bool) BootstrapProcedureOption { } } +func WithEVMABIOnly(evmAbiOnly cadence.Bool) BootstrapProcedureOption { + return func(bp *BootstrapProcedure) *BootstrapProcedure { + bp.evmAbiOnly = evmAbiOnly + return bp + } +} + func WithRestrictedContractDeployment(restricted *bool) BootstrapProcedureOption { return func(bp *BootstrapProcedure) *BootstrapProcedure { bp.restrictedContractDeployment = restricted @@ -809,7 +822,7 @@ func (b *bootstrapExecutor) setupEVM(serviceAddress, fungibleTokenAddress, flowT evmAcc := b.createAccount(nil) // account for storage tx := blueprints.DeployContractTransaction( serviceAddress, - stdlib.ContractCode(flowTokenAddress), + stdlib.ContractCode(flowTokenAddress, bool(b.evmAbiOnly)), stdlib.ContractName, ) // WithEVMEnabled should only be used after we create an account for storage diff --git a/fvm/evm/stdlib/abiOnlyContract.cdc b/fvm/evm/stdlib/abiOnlyContract.cdc new file mode 100644 index 00000000000..45378726215 --- /dev/null +++ b/fvm/evm/stdlib/abiOnlyContract.cdc @@ -0,0 +1,60 @@ +access(all) +contract EVM { + + /// EVMAddress is an EVM-compatible address + access(all) + struct EVMAddress { + + /// Bytes of the address + access(all) + let bytes: [UInt8; 20] + + /// Constructs a new EVM address from the given byte representation + init(bytes: [UInt8; 20]) { + self.bytes = bytes + } + + } + + access(all) + fun encodeABI(_ values: [AnyStruct]): [UInt8] { + return InternalEVM.encodeABI(values) + } + + access(all) + fun decodeABI(types: [Type], data: [UInt8]): [AnyStruct] { + return InternalEVM.decodeABI(types: types, data: data) + } + + access(all) + fun encodeABIWithSignature( + _ signature: String, + _ values: [AnyStruct] + ): [UInt8] { + let methodID = HashAlgorithm.KECCAK_256.hash( + signature.utf8 + ).slice(from: 0, upTo: 4) + let arguments = InternalEVM.encodeABI(values) + + return methodID.concat(arguments) + } + + access(all) + fun decodeABIWithSignature( + _ signature: String, + types: [Type], + data: [UInt8] + ): [AnyStruct] { + let methodID = HashAlgorithm.KECCAK_256.hash( + signature.utf8 + ).slice(from: 0, upTo: 4) + + for byte in methodID { + if byte != data.removeFirst() { + panic("signature mismatch") + } + } + + return InternalEVM.decodeABI(types: types, data: data) + } +} diff --git a/fvm/evm/stdlib/contract.go b/fvm/evm/stdlib/contract.go index d7fe8401d59..ba62334fffc 100644 --- a/fvm/evm/stdlib/contract.go +++ b/fvm/evm/stdlib/contract.go @@ -27,9 +27,16 @@ import ( //go:embed contract.cdc var contractCode string +//go:embed abiOnlyContract.cdc +var abiOnlyContractCode string + var flowTokenImportPattern = regexp.MustCompile(`^import "FlowToken"\n`) -func ContractCode(flowTokenAddress flow.Address) []byte { +func ContractCode(flowTokenAddress flow.Address, evmAbiOnly bool) []byte { + if evmAbiOnly { + return []byte(abiOnlyContractCode) + } + return []byte(flowTokenImportPattern.ReplaceAllString( contractCode, fmt.Sprintf("import FlowToken from %s", flowTokenAddress.HexWithPrefix()), diff --git a/fvm/evm/stdlib/contract_test.go b/fvm/evm/stdlib/contract_test.go index 54a68488b6d..4e4ad1c9b06 100644 --- a/fvm/evm/stdlib/contract_test.go +++ b/fvm/evm/stdlib/contract_test.go @@ -133,6 +133,7 @@ func deployContracts( runtimeInterface *TestRuntimeInterface, transactionEnvironment runtime.Environment, nextTransactionLocation func() common.TransactionLocation, + evmAbiOnly bool, ) { contractsAddressHex := contractsAddress.Hex() @@ -185,7 +186,7 @@ func deployContracts( }, { name: stdlib.ContractName, - code: stdlib.ContractCode(contractsAddress), + code: stdlib.ContractCode(contractsAddress, evmAbiOnly), }, } @@ -306,6 +307,7 @@ func TestEVMEncodeABI(t *testing.T) { runtimeInterface, transactionEnvironment, nextTransactionLocation, + true, ) // Run script @@ -434,6 +436,7 @@ func TestEVMEncodeABIComputation(t *testing.T) { runtimeInterface, transactionEnvironment, nextTransactionLocation, + true, ) // Run script @@ -529,6 +532,7 @@ func TestEVMEncodeABIComputationEmptyDynamicVariables(t *testing.T) { runtimeInterface, transactionEnvironment, nextTransactionLocation, + true, ) // Run script @@ -633,6 +637,7 @@ func TestEVMEncodeABIComputationDynamicVariablesAboveChunkSize(t *testing.T) { runtimeInterface, transactionEnvironment, nextTransactionLocation, + true, ) // Run script @@ -731,6 +736,7 @@ func TestEVMDecodeABI(t *testing.T) { runtimeInterface, transactionEnvironment, nextTransactionLocation, + true, ) // Run script @@ -865,6 +871,7 @@ func TestEVMDecodeABIComputation(t *testing.T) { runtimeInterface, transactionEnvironment, nextTransactionLocation, + true, ) // Run script @@ -1138,6 +1145,7 @@ func TestEVMEncodeDecodeABIRoundtrip(t *testing.T) { runtimeInterface, transactionEnvironment, nextTransactionLocation, + true, ) // Run script @@ -1216,6 +1224,7 @@ func TestEVMEncodeDecodeABIErrors(t *testing.T) { runtimeInterface, transactionEnvironment, nextTransactionLocation, + true, ) // Run script @@ -1302,6 +1311,7 @@ func TestEVMEncodeDecodeABIErrors(t *testing.T) { runtimeInterface, transactionEnvironment, nextTransactionLocation, + true, ) // Run script @@ -1387,6 +1397,7 @@ func TestEVMEncodeDecodeABIErrors(t *testing.T) { runtimeInterface, transactionEnvironment, nextTransactionLocation, + true, ) // Run script @@ -1473,6 +1484,7 @@ func TestEVMEncodeDecodeABIErrors(t *testing.T) { runtimeInterface, transactionEnvironment, nextTransactionLocation, + true, ) // Run script @@ -1559,6 +1571,7 @@ func TestEVMEncodeDecodeABIErrors(t *testing.T) { runtimeInterface, transactionEnvironment, nextTransactionLocation, + true, ) // Run script @@ -1655,6 +1668,7 @@ func TestEVMEncodeDecodeABIErrors(t *testing.T) { runtimeInterface, transactionEnvironment, nextTransactionLocation, + true, ) // Run script @@ -1741,6 +1755,7 @@ func TestEVMEncodeDecodeABIErrors(t *testing.T) { runtimeInterface, transactionEnvironment, nextTransactionLocation, + true, ) // Run script @@ -1827,6 +1842,7 @@ func TestEVMEncodeDecodeABIErrors(t *testing.T) { runtimeInterface, transactionEnvironment, nextTransactionLocation, + true, ) // Run script @@ -1913,6 +1929,7 @@ func TestEVMEncodeDecodeABIErrors(t *testing.T) { runtimeInterface, transactionEnvironment, nextTransactionLocation, + true, ) // Run script @@ -1999,6 +2016,7 @@ func TestEVMEncodeDecodeABIErrors(t *testing.T) { runtimeInterface, transactionEnvironment, nextTransactionLocation, + true, ) // Run script @@ -2085,6 +2103,7 @@ func TestEVMEncodeDecodeABIErrors(t *testing.T) { runtimeInterface, transactionEnvironment, nextTransactionLocation, + true, ) // Run script @@ -2216,6 +2235,7 @@ func TestEVMEncodeABIWithSignature(t *testing.T) { runtimeInterface, transactionEnvironment, nextTransactionLocation, + true, ) // Run script @@ -2349,6 +2369,7 @@ func TestEVMDecodeABIWithSignature(t *testing.T) { runtimeInterface, transactionEnvironment, nextTransactionLocation, + true, ) // Run script @@ -2463,6 +2484,7 @@ func TestEVMDecodeABIWithSignatureMismatch(t *testing.T) { runtimeInterface, transactionEnvironment, nextTransactionLocation, + true, ) // Run script @@ -2572,6 +2594,7 @@ func TestEVMAddressConstructionAndReturn(t *testing.T) { runtimeInterface, transactionEnvironment, nextTransactionLocation, + true, ) // Run script @@ -2664,6 +2687,7 @@ func TestBalanceConstructionAndReturn(t *testing.T) { runtimeInterface, transactionEnvironment, nextTransactionLocation, + false, ) // Run script @@ -2794,6 +2818,7 @@ func TestEVMRun(t *testing.T) { runtimeInterface, transactionEnvironment, nextTransactionLocation, + false, ) // Run script @@ -2881,6 +2906,7 @@ func TestEVMCreateBridgedAccount(t *testing.T) { runtimeInterface, transactionEnvironment, nextTransactionLocation, + false, ) // Run script @@ -3011,6 +3037,7 @@ func TestBridgedAccountCall(t *testing.T) { runtimeInterface, transactionEnvironment, nextTransactionLocation, + false, ) // Run script @@ -3129,6 +3156,7 @@ func TestEVMAddressDeposit(t *testing.T) { runtimeInterface, transactionEnvironment, nextTransactionLocation, + false, ) // Run script @@ -3257,6 +3285,7 @@ func TestBridgedAccountWithdraw(t *testing.T) { runtimeInterface, transactionEnvironment, nextTransactionLocation, + false, ) // Run script @@ -3370,6 +3399,7 @@ func TestBridgedAccountDeploy(t *testing.T) { runtimeInterface, transactionEnvironment, nextTransactionLocation, + false, ) // Run script @@ -3490,6 +3520,7 @@ func TestEVMAccountBalance(t *testing.T) { runtimeInterface, transactionEnvironment, nextTransactionLocation, + false, ) // Run script @@ -3509,3 +3540,111 @@ func TestEVMAccountBalance(t *testing.T) { require.NoError(t, err) require.Equal(t, expectedBalance, actual) } + +func TestEVMAccountBalanceForABIOnlyContract(t *testing.T) { + + t.Parallel() + + contractsAddress := flow.BytesToAddress([]byte{0x1}) + + expectedBalanceValue, err := cadence.NewUFix64FromParts(1, 1337000) + require.NoError(t, err) + + handler := &testContractHandler{ + flowTokenAddress: common.Address(contractsAddress), + accountByAddress: func(fromAddress types.Address, isAuthorized bool) types.Account { + assert.Equal(t, types.Address{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, fromAddress) + assert.False(t, isAuthorized) + + return &testFlowAccount{ + address: fromAddress, + balance: func() types.Balance { + return types.Balance(expectedBalanceValue) + }, + } + }, + } + + transactionEnvironment := newEVMTransactionEnvironment(handler, contractsAddress) + scriptEnvironment := newEVMScriptEnvironment(handler, contractsAddress) + + rt := runtime.NewInterpreterRuntime(runtime.Config{}) + + script := []byte(` + import EVM from 0x1 + + access(all) + fun main(): EVM.Balance { + let bridgedAccount <- EVM.createBridgedAccount() + let balance = bridgedAccount.balance() + destroy bridgedAccount + return balance + } + `) + + accountCodes := map[common.Location][]byte{} + var events []cadence.Event + + runtimeInterface := &TestRuntimeInterface{ + Storage: NewTestLedger(nil, nil), + OnGetSigningAccounts: func() ([]runtime.Address, error) { + return []runtime.Address{runtime.Address(contractsAddress)}, nil + }, + OnResolveLocation: SingleIdentifierLocationResolver(t), + OnUpdateAccountContractCode: func(location common.AddressLocation, code []byte) error { + accountCodes[location] = code + return nil + }, + OnGetAccountContractCode: func(location common.AddressLocation) (code []byte, err error) { + code = accountCodes[location] + return code, nil + }, + OnEmitEvent: func(event cadence.Event) error { + events = append(events, event) + return nil + }, + OnDecodeArgument: func(b []byte, t cadence.Type) (cadence.Value, error) { + return json.Decode(nil, b) + }, + } + + nextTransactionLocation := NewTransactionLocationGenerator() + nextScriptLocation := NewScriptLocationGenerator() + + // Deploy contracts + + deployContracts( + t, + rt, + contractsAddress, + runtimeInterface, + transactionEnvironment, + nextTransactionLocation, + true, + ) + + // Run script + + _, err = rt.ExecuteScript( + runtime.Script{ + Source: script, + }, + runtime.Context{ + Interface: runtimeInterface, + Environment: scriptEnvironment, + Location: nextScriptLocation(), + }, + ) + require.Error(t, err) + + assert.ErrorContains( + t, + err, + "error: cannot find type in this scope: `EVM.Balance`", + ) + assert.ErrorContains( + t, + err, + "error: value of type `EVM` has no member `createBridgedAccount`", + ) +} diff --git a/fvm/fvm_test.go b/fvm/fvm_test.go index 6f1651cf49e..9cc7b66d842 100644 --- a/fvm/fvm_test.go +++ b/fvm/fvm_test.go @@ -15,6 +15,7 @@ import ( "github.com/onflow/cadence/runtime/common" cadenceErrors "github.com/onflow/cadence/runtime/errors" "github.com/onflow/cadence/runtime/tests/utils" + "github.com/stretchr/testify/assert" mockery "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -3013,6 +3014,59 @@ func TestEVM(t *testing.T) { }), ) + // this test makes sure that only ABI encoding/decoding functionality is + // available through the EVM contract, when bootstraped with `WithEVMABIOnly` + t.Run("with ABI only EVM", newVMTest(). + withBootstrapProcedureOptions( + fvm.WithSetupEVMEnabled(true), + fvm.WithEVMABIOnly(true), + ). + withContextOptions( + fvm.WithEVMEnabled(true), + ). + run(func( + t *testing.T, + vm fvm.VM, + chain flow.Chain, + ctx fvm.Context, + snapshotTree snapshot.SnapshotTree, + ) { + txBody := flow.NewTransactionBody(). + SetScript([]byte(fmt.Sprintf(` + import EVM from %s + + transaction { + execute { + let data = EVM.encodeABI(["John Doe", UInt64(33), false]) + log(data.length) + assert(data.length == 160) + + let acc <- EVM.createBridgedAccount() + destroy acc + } + } + `, chain.ServiceAddress().HexWithPrefix()))). + SetProposalKey(chain.ServiceAddress(), 0, 0). + SetPayer(chain.ServiceAddress()) + + err := testutil.SignTransactionAsServiceAccount(txBody, 0, chain) + require.NoError(t, err) + + _, output, err := vm.Run( + ctx, + fvm.Transaction(txBody, 0), + snapshotTree) + + require.NoError(t, err) + require.Error(t, output.Err) + assert.ErrorContains( + t, + output.Err, + "value of type `EVM` has no member `createBridgedAccount`", + ) + }), + ) + // this test makes sure the execution error is correctly handled and returned as a correct type t.Run("execution reverted", newVMTest(). withBootstrapProcedureOptions(fvm.WithSetupEVMEnabled(true)).