diff --git a/engine/execution/state/bootstrap/bootstrap_test.go b/engine/execution/state/bootstrap/bootstrap_test.go index 5a9d394a5ac..cfeb7088d77 100644 --- a/engine/execution/state/bootstrap/bootstrap_test.go +++ b/engine/execution/state/bootstrap/bootstrap_test.go @@ -53,7 +53,7 @@ func TestBootstrapLedger(t *testing.T) { } func TestBootstrapLedger_ZeroTokenSupply(t *testing.T) { - expectedStateCommitmentBytes, _ := hex.DecodeString("6e70a1ff40e4312a547d588a4355a538610bc22844a1faa907b4ec333ff1eca9") + expectedStateCommitmentBytes, _ := hex.DecodeString("6feac3d05e40ac944e690655b1016cc2f96f7a1c58c0a235fdeaa6601f540dad") expectedStateCommitment, err := flow.ToStateCommitment(expectedStateCommitmentBytes) require.NoError(t, err) diff --git a/fvm/evm/evm_test.go b/fvm/evm/evm_test.go index 4857b532a33..580c60a17f5 100644 --- a/fvm/evm/evm_test.go +++ b/fvm/evm/evm_test.go @@ -1550,6 +1550,149 @@ func TestCadenceOwnedAccountFunctionalities(t *testing.T) { require.Equal(t, testContract.ByteCode[17:], []byte(res.ReturnedData)) }) }) + + t.Run("test coa dryCall", func(t *testing.T) { + RunWithNewEnvironment(t, + chain, func( + ctx fvm.Context, + vm fvm.VM, + snapshot snapshot.SnapshotTree, + testContract *TestContract, + testAccount *EOATestAccount, + ) { + sc := systemcontracts.SystemContractsForChain(chain.ChainID()) + code := []byte(fmt.Sprintf( + ` + import EVM from %s + + transaction(tx: [UInt8], coinbaseBytes: [UInt8; 20]){ + prepare(account: auth(Storage) &Account ) { + let cadenceOwnedAccount <- EVM.createCadenceOwnedAccount() + account.storage.save(<- cadenceOwnedAccount, to: /storage/evmCOA) + + let coinbase = EVM.EVMAddress(bytes: coinbaseBytes) + let res = EVM.run(tx: tx, coinbase: coinbase) + + assert(res.status == EVM.Status.successful, message: "unexpected status") + assert(res.errorCode == 0, message: "unexpected error code") + } + } + `, + sc.EVMContract.Address.HexWithPrefix(), + )) + + num := int64(42) + innerTxBytes := testAccount.PrepareSignAndEncodeTx(t, + testContract.DeployedAt.ToCommon(), + testContract.MakeCallData(t, "store", big.NewInt(num)), + big.NewInt(0), + uint64(50_000), + big.NewInt(0), + ) + + innerTx := cadence.NewArray( + ConvertToCadence(innerTxBytes), + ).WithType(stdlib.EVMTransactionBytesCadenceType) + + coinbase := cadence.NewArray( + ConvertToCadence(testAccount.Address().Bytes()), + ).WithType(stdlib.EVMAddressBytesCadenceType) + + tx := fvm.Transaction( + flow.NewTransactionBody(). + SetScript(code). + AddAuthorizer(sc.FlowServiceAccount.Address). + AddArgument(json.MustEncode(innerTx)). + AddArgument(json.MustEncode(coinbase)), + 0) + + state, output, err := vm.Run( + ctx, + tx, + snapshot, + ) + require.NoError(t, err) + require.NoError(t, output.Err) + assert.Len(t, output.Events, 3) + assert.Len(t, state.UpdatedRegisterIDs(), 12) + assert.Equal( + t, + flow.EventType("A.f8d6e0586b0a20c7.EVM.TransactionExecuted"), + output.Events[0].Type, + ) + assert.Equal( + t, + flow.EventType("A.f8d6e0586b0a20c7.EVM.CadenceOwnedAccountCreated"), + output.Events[1].Type, + ) + assert.Equal( + t, + flow.EventType("A.f8d6e0586b0a20c7.EVM.TransactionExecuted"), + output.Events[2].Type, + ) + snapshot = snapshot.Append(state) + + code = []byte(fmt.Sprintf( + ` + import EVM from %s + + transaction(data: [UInt8], to: String, gasLimit: UInt64, value: UInt){ + prepare(account: auth(Storage) &Account) { + let coa = account.storage.borrow<&EVM.CadenceOwnedAccount>( + from: /storage/evmCOA + ) ?? panic("could not borrow COA reference!") + let res = coa.dryCall( + to: EVM.addressFromString(to), + data: data, + gasLimit: gasLimit, + value: EVM.Balance(attoflow: value) + ) + + assert(res.status == EVM.Status.successful, message: "unexpected status") + assert(res.errorCode == 0, message: "unexpected error code") + + let values = EVM.decodeABI(types: [Type()], data: res.data) + assert(values.length == 1) + + let number = values[0] as! UInt256 + assert(number == 42, message: String.encodeHex(res.data)) + } + } + `, + sc.EVMContract.Address.HexWithPrefix(), + )) + + data := json.MustEncode( + cadence.NewArray( + ConvertToCadence(testContract.MakeCallData(t, "retrieve")), + ).WithType(stdlib.EVMTransactionBytesCadenceType), + ) + toAddress, err := cadence.NewString(testContract.DeployedAt.ToCommon().Hex()) + require.NoError(t, err) + to := json.MustEncode(toAddress) + + tx = fvm.Transaction( + flow.NewTransactionBody(). + SetScript(code). + AddAuthorizer(sc.FlowServiceAccount.Address). + AddArgument(data). + AddArgument(to). + AddArgument(json.MustEncode(cadence.NewUInt64(50_000))). + AddArgument(json.MustEncode(cadence.NewUInt(0))), + 0, + ) + + state, output, err = vm.Run( + ctx, + tx, + snapshot, + ) + require.NoError(t, err) + require.NoError(t, output.Err) + assert.Len(t, output.Events, 0) + assert.Len(t, state.UpdatedRegisterIDs(), 0) + }) + }) } func TestDryRun(t *testing.T) { @@ -1564,7 +1707,6 @@ func TestDryRun(t *testing.T) { ctx fvm.Context, vm fvm.VM, snapshot snapshot.SnapshotTree, - testContract *TestContract, ) *types.ResultSummary { code := []byte(fmt.Sprintf(` import EVM from %s @@ -1622,7 +1764,7 @@ func TestDryRun(t *testing.T) { big.NewInt(0), data, ) - result := dryRunTx(t, tx, ctx, vm, snapshot, testContract) + result := dryRunTx(t, tx, ctx, vm, snapshot) require.Equal(t, types.ErrCodeNoError, result.ErrorCode) require.Equal(t, types.StatusSuccessful, result.Status) require.Greater(t, result.GasConsumed, uint64(0)) @@ -1638,7 +1780,7 @@ func TestDryRun(t *testing.T) { big.NewInt(0), data, ) - result = dryRunTx(t, tx, ctx, vm, snapshot, testContract) + result = dryRunTx(t, tx, ctx, vm, snapshot) require.Equal(t, types.ExecutionErrCodeOutOfGas, result.ErrorCode) require.Equal(t, types.StatusFailed, result.Status) require.Equal(t, result.GasConsumed, limit) // burn it all!!! @@ -1663,7 +1805,7 @@ func TestDryRun(t *testing.T) { big.NewInt(0), data, ) - dryRunResult := dryRunTx(t, tx, ctx, vm, snapshot, testContract) + dryRunResult := dryRunTx(t, tx, ctx, vm, snapshot) require.Equal(t, types.ErrCodeNoError, dryRunResult.ErrorCode) require.Equal(t, types.StatusSuccessful, dryRunResult.Status) @@ -1794,7 +1936,7 @@ func TestDryRun(t *testing.T) { big.NewInt(0), data, ) - dryRunResult := dryRunTx(t, tx1, ctx, vm, snapshot, testContract) + dryRunResult := dryRunTx(t, tx1, ctx, vm, snapshot) require.Equal(t, types.ErrCodeNoError, dryRunResult.ErrorCode) require.Equal(t, types.StatusSuccessful, dryRunResult.Status) @@ -1929,7 +2071,7 @@ func TestDryRun(t *testing.T) { big.NewInt(0), data, ) - dryRunResult := dryRunTx(t, tx1, ctx, vm, snapshot, testContract) + dryRunResult := dryRunTx(t, tx1, ctx, vm, snapshot) require.Equal(t, types.ErrCodeNoError, dryRunResult.ErrorCode) require.Equal(t, types.StatusSuccessful, dryRunResult.Status) @@ -2013,7 +2155,7 @@ func TestDryRun(t *testing.T) { data, ) - result := dryRunTx(t, tx, ctx, vm, snapshot, testContract) + result := dryRunTx(t, tx, ctx, vm, snapshot) require.Equal(t, types.ErrCodeNoError, result.ErrorCode) require.Equal(t, types.StatusSuccessful, result.Status) require.Greater(t, result.GasConsumed, uint64(0)) @@ -2085,7 +2227,7 @@ func TestDryRun(t *testing.T) { testContract.ByteCode, ) - result := dryRunTx(t, tx, ctx, vm, snapshot, testContract) + result := dryRunTx(t, tx, ctx, vm, snapshot) require.Equal(t, types.ErrCodeNoError, result.ErrorCode) require.Equal(t, types.StatusSuccessful, result.Status) require.Greater(t, result.GasConsumed, uint64(0)) @@ -2112,7 +2254,7 @@ func TestDryRun(t *testing.T) { nil, ) - result := dryRunTx(t, tx, ctx, vm, snapshot, testContract) + result := dryRunTx(t, tx, ctx, vm, snapshot) assert.Equal(t, types.ValidationErrCodeInsufficientFunds, result.ErrorCode) assert.Equal(t, types.StatusInvalid, result.Status) assert.Equal(t, types.InvalidTransactionGasCost, int(result.GasConsumed)) @@ -2120,6 +2262,360 @@ func TestDryRun(t *testing.T) { }) } +func TestDryCall(t *testing.T) { + t.Parallel() + + chain := flow.Emulator.Chain() + sc := systemcontracts.SystemContractsForChain(chain.ChainID()) + evmAddress := sc.EVMContract.Address.HexWithPrefix() + + dryCall := func( + t *testing.T, + tx *gethTypes.Transaction, + ctx fvm.Context, + vm fvm.VM, + snapshot snapshot.SnapshotTree, + ) (*types.ResultSummary, *snapshot.ExecutionSnapshot) { + code := []byte(fmt.Sprintf(` + import EVM from %s + + access(all) + fun main(data: [UInt8], to: String, gasLimit: UInt64, value: UInt): EVM.Result { + return EVM.dryCall( + from: EVM.EVMAddress(bytes: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 15]), + to: EVM.addressFromString(to), + data: data, + gasLimit: gasLimit, + value: EVM.Balance(attoflow: value) + ) + }`, + evmAddress, + )) + + require.NotNil(t, tx.To()) + to := tx.To().Hex() + toAddress, err := cadence.NewString(to) + require.NoError(t, err) + + script := fvm.Script(code).WithArguments( + json.MustEncode( + cadence.NewArray( + ConvertToCadence(tx.Data()), + ).WithType(stdlib.EVMTransactionBytesCadenceType), + ), + json.MustEncode(toAddress), + json.MustEncode(cadence.NewUInt64(tx.Gas())), + json.MustEncode(cadence.NewUInt(uint(tx.Value().Uint64()))), + ) + execSnapshot, output, err := vm.Run( + ctx, + script, + snapshot, + ) + require.NoError(t, err) + require.NoError(t, output.Err) + require.Len(t, output.Events, 0) + + result, err := impl.ResultSummaryFromEVMResultValue(output.Value) + require.NoError(t, err) + return result, execSnapshot + } + + // this test checks that gas limit is correctly used and gas usage correctly reported + t.Run("test dryCall with different gas limits", func(t *testing.T) { + RunWithNewEnvironment(t, + chain, func( + ctx fvm.Context, + vm fvm.VM, + snapshot snapshot.SnapshotTree, + testContract *TestContract, + testAccount *EOATestAccount, + ) { + data := testContract.MakeCallData(t, "store", big.NewInt(1337)) + + limit := uint64(50_000) + tx := gethTypes.NewTransaction( + 0, + testContract.DeployedAt.ToCommon(), + big.NewInt(0), + limit, + big.NewInt(0), + data, + ) + result, _ := dryCall(t, tx, ctx, vm, snapshot) + require.Equal(t, types.ErrCodeNoError, result.ErrorCode) + require.Equal(t, types.StatusSuccessful, result.Status) + require.Greater(t, result.GasConsumed, uint64(0)) + require.Less(t, result.GasConsumed, limit) + + // gas limit too low, but still bigger than intrinsic gas value + limit = uint64(21216) + tx = gethTypes.NewTransaction( + 0, + testContract.DeployedAt.ToCommon(), + big.NewInt(0), + limit, + big.NewInt(0), + data, + ) + result, _ = dryCall(t, tx, ctx, vm, snapshot) + require.Equal(t, types.ExecutionErrCodeOutOfGas, result.ErrorCode) + require.Equal(t, types.StatusFailed, result.Status) + require.Equal(t, result.GasConsumed, limit) + }) + }) + + t.Run("test dryCall does not form EVM transactions", func(t *testing.T) { + RunWithNewEnvironment(t, + chain, func( + ctx fvm.Context, + vm fvm.VM, + snapshot snapshot.SnapshotTree, + testContract *TestContract, + testAccount *EOATestAccount, + ) { + sc := systemcontracts.SystemContractsForChain(chain.ChainID()) + code := []byte(fmt.Sprintf( + ` + import EVM from %s + + transaction(tx: [UInt8], coinbaseBytes: [UInt8; 20]){ + prepare(account: &Account) { + let coinbase = EVM.EVMAddress(bytes: coinbaseBytes) + let res = EVM.run(tx: tx, coinbase: coinbase) + + assert(res.status == EVM.Status.successful, message: "unexpected status") + assert(res.errorCode == 0, message: "unexpected error code") + } + } + `, + sc.EVMContract.Address.HexWithPrefix(), + )) + + num := int64(42) + innerTxBytes := testAccount.PrepareSignAndEncodeTx(t, + testContract.DeployedAt.ToCommon(), + testContract.MakeCallData(t, "store", big.NewInt(num)), + big.NewInt(0), + uint64(50_000), + big.NewInt(0), + ) + + innerTx := cadence.NewArray( + ConvertToCadence(innerTxBytes), + ).WithType(stdlib.EVMTransactionBytesCadenceType) + + coinbase := cadence.NewArray( + ConvertToCadence(testAccount.Address().Bytes()), + ).WithType(stdlib.EVMAddressBytesCadenceType) + + tx := fvm.Transaction( + flow.NewTransactionBody(). + SetScript(code). + AddAuthorizer(sc.FlowServiceAccount.Address). + AddArgument(json.MustEncode(innerTx)). + AddArgument(json.MustEncode(coinbase)), + 0) + + state, output, err := vm.Run( + ctx, + tx, + snapshot, + ) + require.NoError(t, err) + require.NoError(t, output.Err) + assert.Len(t, output.Events, 1) + assert.Len(t, state.UpdatedRegisterIDs(), 4) + assert.Equal( + t, + flow.EventType("A.f8d6e0586b0a20c7.EVM.TransactionExecuted"), + output.Events[0].Type, + ) + snapshot = snapshot.Append(state) + + code = []byte(fmt.Sprintf( + ` + import EVM from %s + + transaction(data: [UInt8], to: String, gasLimit: UInt64, value: UInt){ + prepare(account: &Account) { + let res = EVM.dryCall( + from: EVM.EVMAddress(bytes: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 15]), + to: EVM.addressFromString(to), + data: data, + gasLimit: gasLimit, + value: EVM.Balance(attoflow: value) + ) + + assert(res.status == EVM.Status.successful, message: "unexpected status") + assert(res.errorCode == 0, message: "unexpected error code") + + let values = EVM.decodeABI(types: [Type()], data: res.data) + assert(values.length == 1) + + let number = values[0] as! UInt256 + assert(number == 42, message: String.encodeHex(res.data)) + } + } + `, + sc.EVMContract.Address.HexWithPrefix(), + )) + + data := json.MustEncode( + cadence.NewArray( + ConvertToCadence(testContract.MakeCallData(t, "retrieve")), + ).WithType(stdlib.EVMTransactionBytesCadenceType), + ) + toAddress, err := cadence.NewString(testContract.DeployedAt.ToCommon().Hex()) + require.NoError(t, err) + to := json.MustEncode(toAddress) + + tx = fvm.Transaction( + flow.NewTransactionBody(). + SetScript(code). + AddAuthorizer(sc.FlowServiceAccount.Address). + AddArgument(data). + AddArgument(to). + AddArgument(json.MustEncode(cadence.NewUInt64(50_000))). + AddArgument(json.MustEncode(cadence.NewUInt(0))), + 0, + ) + + state, output, err = vm.Run( + ctx, + tx, + snapshot, + ) + require.NoError(t, err) + require.NoError(t, output.Err) + assert.Len(t, output.Events, 0) + assert.Len(t, state.UpdatedRegisterIDs(), 0) + }) + }) + + // this test makes sure the dryCall that updates the value on the contract + // doesn't persist the change, and after when the value is read it isn't updated. + t.Run("test dryCall has no side-effects", func(t *testing.T) { + RunWithNewEnvironment(t, + chain, func( + ctx fvm.Context, + vm fvm.VM, + snapshot snapshot.SnapshotTree, + testContract *TestContract, + testAccount *EOATestAccount, + ) { + updatedValue := int64(1337) + data := testContract.MakeCallData(t, "store", big.NewInt(updatedValue)) + tx := gethTypes.NewTransaction( + 0, + testContract.DeployedAt.ToCommon(), + big.NewInt(0), + uint64(1000000), + big.NewInt(0), + data, + ) + + result, state := dryCall(t, tx, ctx, vm, snapshot) + require.Len(t, state.UpdatedRegisterIDs(), 0) + require.Equal(t, types.ErrCodeNoError, result.ErrorCode) + require.Equal(t, types.StatusSuccessful, result.Status) + require.Greater(t, result.GasConsumed, uint64(0)) + + // query the value make sure it's not updated + code := []byte(fmt.Sprintf( + ` + import EVM from %s + access(all) + fun main(tx: [UInt8], coinbaseBytes: [UInt8; 20]): EVM.Result { + let coinbase = EVM.EVMAddress(bytes: coinbaseBytes) + return EVM.run(tx: tx, coinbase: coinbase) + } + `, + evmAddress, + )) + + innerTxBytes := testAccount.PrepareSignAndEncodeTx(t, + testContract.DeployedAt.ToCommon(), + testContract.MakeCallData(t, "retrieve"), + big.NewInt(0), + uint64(100_000), + big.NewInt(0), + ) + + innerTx := cadence.NewArray( + ConvertToCadence(innerTxBytes), + ).WithType(stdlib.EVMTransactionBytesCadenceType) + + coinbase := cadence.NewArray( + ConvertToCadence(testAccount.Address().Bytes()), + ).WithType(stdlib.EVMAddressBytesCadenceType) + + script := fvm.Script(code).WithArguments( + json.MustEncode(innerTx), + json.MustEncode(coinbase), + ) + + state, output, err := vm.Run( + ctx, + script, + snapshot, + ) + require.NoError(t, err) + require.NoError(t, output.Err) + require.Len(t, state.UpdatedRegisterIDs(), 0) + + res, err := impl.ResultSummaryFromEVMResultValue(output.Value) + require.NoError(t, err) + require.Equal(t, types.StatusSuccessful, res.Status) + require.Equal(t, types.ErrCodeNoError, res.ErrorCode) + // make sure the value we used in the dryCall is not the same as the value stored in contract + require.NotEqual(t, updatedValue, new(big.Int).SetBytes(res.ReturnedData).Int64()) + }) + }) + + t.Run("test dryCall validation error", func(t *testing.T) { + RunWithNewEnvironment(t, + chain, func( + ctx fvm.Context, + vm fvm.VM, + snapshot snapshot.SnapshotTree, + testContract *TestContract, + testAccount *EOATestAccount, + ) { + data := testContract.MakeCallData(t, "store", big.NewInt(10337)) + tx := gethTypes.NewTransaction( + 0, + testContract.DeployedAt.ToCommon(), + big.NewInt(1000), // more than available + uint64(35_000), + big.NewInt(0), + data, + ) + + result, _ := dryCall(t, tx, ctx, vm, snapshot) + assert.Equal(t, types.ValidationErrCodeInsufficientFunds, result.ErrorCode) + assert.Equal(t, types.StatusInvalid, result.Status) + assert.Equal(t, types.InvalidTransactionGasCost, int(result.GasConsumed)) + + // random function selector + data = []byte{254, 234, 101, 199} + tx = gethTypes.NewTransaction( + 0, + testContract.DeployedAt.ToCommon(), + big.NewInt(0), + uint64(25_000), + big.NewInt(0), + data, + ) + + result, _ = dryCall(t, tx, ctx, vm, snapshot) + assert.Equal(t, types.ExecutionErrCodeExecutionReverted, result.ErrorCode) + assert.Equal(t, types.StatusFailed, result.Status) + assert.Equal(t, uint64(21331), result.GasConsumed) + }) + }) +} + func TestCadenceArch(t *testing.T) { t.Parallel() diff --git a/fvm/evm/impl/impl.go b/fvm/evm/impl/impl.go index 1412158a976..fff2b0c3b89 100644 --- a/fvm/evm/impl/impl.go +++ b/fvm/evm/impl/impl.go @@ -13,6 +13,8 @@ import ( "github.com/onflow/flow-go/fvm/evm/stdlib" "github.com/onflow/flow-go/fvm/evm/types" "github.com/onflow/flow-go/model/flow" + + gethTypes "github.com/onflow/go-ethereum/core/types" ) var internalEVMContractStaticType = interpreter.ConvertSemaCompositeTypeToStaticCompositeType( @@ -50,6 +52,7 @@ func NewInternalEVMContractValue( stdlib.InternalEVMTypeCastToFLOWFunctionName: newInternalEVMTypeCastToFLOWFunction(gauge), stdlib.InternalEVMTypeGetLatestBlockFunctionName: newInternalEVMTypeGetLatestBlockFunction(gauge, handler), stdlib.InternalEVMTypeDryRunFunctionName: newInternalEVMTypeDryRunFunction(gauge, handler), + stdlib.InternalEVMTypeDryCallFunctionName: newInternalEVMTypeDryCallFunction(gauge, handler), stdlib.InternalEVMTypeCommitBlockProposalFunctionName: newInternalEVMTypeCommitBlockProposalFunction(gauge, handler), }, nil, @@ -432,69 +435,59 @@ func newInternalEVMTypeCallFunction( gauge, stdlib.InternalEVMTypeCallFunctionType, func(invocation interpreter.Invocation) interpreter.Value { - inter := invocation.Interpreter - locationRange := invocation.LocationRange - - // Get from address - - fromAddressValue, ok := invocation.Arguments[0].(*interpreter.ArrayValue) - if !ok { - panic(errors.NewUnreachableError()) - } - - fromAddress, err := AddressBytesArrayValueToEVMAddress(inter, locationRange, fromAddressValue) + callArgs, err := parseCallArguments(invocation) if err != nil { panic(err) } - // Get to address - - toAddressValue, ok := invocation.Arguments[1].(*interpreter.ArrayValue) - if !ok { - panic(errors.NewUnreachableError()) - } + inter := invocation.Interpreter + locationRange := invocation.LocationRange - toAddress, err := AddressBytesArrayValueToEVMAddress(inter, locationRange, toAddressValue) - if err != nil { - panic(err) - } + // Call - // Get data + const isAuthorized = true + account := handler.AccountByAddress(callArgs.from, isAuthorized) + result := account.Call(callArgs.to, callArgs.data, callArgs.gasLimit, callArgs.balance) - dataValue, ok := invocation.Arguments[2].(*interpreter.ArrayValue) - if !ok { - panic(errors.NewUnreachableError()) - } + return NewResultValue(handler, gauge, inter, locationRange, result) + }, + ) +} - data, err := interpreter.ByteArrayValueToByteSlice(inter, dataValue, locationRange) +func newInternalEVMTypeDryCallFunction( + gauge common.MemoryGauge, + handler types.ContractHandler, +) *interpreter.HostFunctionValue { + return interpreter.NewStaticHostFunctionValue( + gauge, + stdlib.InternalEVMTypeDryCallFunctionType, + func(invocation interpreter.Invocation) interpreter.Value { + callArgs, err := parseCallArguments(invocation) if err != nil { panic(err) } + to := callArgs.to.ToCommon() + + tx := gethTypes.NewTx(&gethTypes.LegacyTx{ + Nonce: 0, + To: &to, + Gas: uint64(callArgs.gasLimit), + Data: callArgs.data, + GasPrice: big.NewInt(0), + Value: callArgs.balance, + }) - // Get gas limit - - gasLimitValue, ok := invocation.Arguments[3].(interpreter.UInt64Value) - if !ok { - panic(errors.NewUnreachableError()) - } - - gasLimit := types.GasLimit(gasLimitValue) - - // Get balance - - balanceValue, ok := invocation.Arguments[4].(interpreter.UIntValue) - if !ok { - panic(errors.NewUnreachableError()) + txPayload, err := tx.MarshalBinary() + if err != nil { + panic(err) } - balance := types.NewBalance(balanceValue.BigInt) - // Call - - const isAuthorized = true - account := handler.AccountByAddress(fromAddress, isAuthorized) - result := account.Call(toAddress, data, gasLimit, balance) + // call contract function + inter := invocation.Interpreter + locationRange := invocation.LocationRange - return NewResultValue(handler, gauge, inter, locationRange, result) + res := handler.DryRun(txPayload, callArgs.from) + return NewResultValue(handler, gauge, inter, locationRange, res) }, ) } @@ -1161,3 +1154,81 @@ func ResultSummaryFromEVMResultValue(val cadence.Value) (*types.ResultSummary, e }, nil } + +type callArguments struct { + from types.Address + to types.Address + data []byte + gasLimit types.GasLimit + balance types.Balance +} + +func parseCallArguments(invocation interpreter.Invocation) ( + *callArguments, + error, +) { + inter := invocation.Interpreter + locationRange := invocation.LocationRange + + // Get from address + + fromAddressValue, ok := invocation.Arguments[0].(*interpreter.ArrayValue) + if !ok { + return nil, errors.NewUnreachableError() + } + + fromAddress, err := AddressBytesArrayValueToEVMAddress(inter, locationRange, fromAddressValue) + if err != nil { + return nil, err + } + + // Get to address + + toAddressValue, ok := invocation.Arguments[1].(*interpreter.ArrayValue) + if !ok { + return nil, errors.NewUnreachableError() + } + + toAddress, err := AddressBytesArrayValueToEVMAddress(inter, locationRange, toAddressValue) + if err != nil { + return nil, err + } + + // Get data + + dataValue, ok := invocation.Arguments[2].(*interpreter.ArrayValue) + if !ok { + return nil, errors.NewUnreachableError() + } + + data, err := interpreter.ByteArrayValueToByteSlice(inter, dataValue, locationRange) + if err != nil { + return nil, err + } + + // Get gas limit + + gasLimitValue, ok := invocation.Arguments[3].(interpreter.UInt64Value) + if !ok { + panic(errors.NewUnreachableError()) + } + + gasLimit := types.GasLimit(gasLimitValue) + + // Get balance + + balanceValue, ok := invocation.Arguments[4].(interpreter.UIntValue) + if !ok { + return nil, errors.NewUnreachableError() + } + + balance := types.NewBalance(balanceValue.BigInt) + + return &callArguments{ + from: fromAddress, + to: toAddress, + data: data, + gasLimit: gasLimit, + balance: balance, + }, nil +} diff --git a/fvm/evm/stdlib/contract.cdc b/fvm/evm/stdlib/contract.cdc index a547768362e..8f21c3544a6 100644 --- a/fvm/evm/stdlib/contract.cdc +++ b/fvm/evm/stdlib/contract.cdc @@ -499,6 +499,25 @@ contract EVM { ) as! Result } + /// Calls a contract function with the given data. + /// The execution is limited by the given amount of gas. + /// The transaction state changes are not persisted. + access(all) + fun dryCall( + to: EVMAddress, + data: [UInt8], + gasLimit: UInt64, + value: Balance, + ): Result { + return InternalEVM.dryCall( + from: self.addressBytes, + to: to.bytes, + data: data, + gasLimit: gasLimit, + value: value.attoflow + ) as! Result + } + /// Bridges the given NFT to the EVM environment, requiring a Provider from which to withdraw a fee to fulfill /// the bridge request access(all) @@ -601,6 +620,26 @@ contract EVM { ) as! Result } + /// Calls a contract function with the given data. + /// The execution is limited by the given amount of gas. + /// The transaction state changes are not persisted. + access(all) + fun dryCall( + from: EVMAddress, + to: EVMAddress, + data: [UInt8], + gasLimit: UInt64, + value: Balance, + ): Result { + return InternalEVM.dryCall( + from: from.bytes, + to: to.bytes, + data: data, + gasLimit: gasLimit, + value: value.attoflow + ) as! Result + } + /// Runs a batch of RLP-encoded EVM transactions, deducts the gas fees, /// and deposits the gas fees into the provided coinbase address. /// An invalid transaction is not executed and not included in the block. diff --git a/fvm/evm/stdlib/contract.go b/fvm/evm/stdlib/contract.go index df53623e2d8..74ea2c5dfb4 100644 --- a/fvm/evm/stdlib/contract.go +++ b/fvm/evm/stdlib/contract.go @@ -228,6 +228,37 @@ var InternalEVMTypeCallFunctionType = &sema.FunctionType{ ReturnTypeAnnotation: sema.NewTypeAnnotation(sema.AnyStructType), } +// InternalEVM.dryCall + +const InternalEVMTypeDryCallFunctionName = "dryCall" + +var InternalEVMTypeDryCallFunctionType = &sema.FunctionType{ + Parameters: []sema.Parameter{ + { + Label: "from", + TypeAnnotation: sema.NewTypeAnnotation(EVMAddressBytesType), + }, + { + Label: "to", + TypeAnnotation: sema.NewTypeAnnotation(EVMAddressBytesType), + }, + { + Label: "data", + TypeAnnotation: sema.NewTypeAnnotation(sema.ByteArrayType), + }, + { + Label: "gasLimit", + TypeAnnotation: sema.NewTypeAnnotation(sema.UInt64Type), + }, + { + Label: "value", + TypeAnnotation: sema.NewTypeAnnotation(sema.UIntType), + }, + }, + // Actually EVM.Result, but cannot refer to it here + ReturnTypeAnnotation: sema.NewTypeAnnotation(sema.AnyStructType), +} + // InternalEVM.createCadenceOwnedAccount const InternalEVMTypeCreateCadenceOwnedAccountFunctionName = "createCadenceOwnedAccount" @@ -452,6 +483,12 @@ var InternalEVMContractType = func() *sema.CompositeType { InternalEVMTypeCallFunctionType, "", ), + sema.NewUnmeteredPublicFunctionMember( + ty, + InternalEVMTypeDryCallFunctionName, + InternalEVMTypeDryCallFunctionType, + "", + ), sema.NewUnmeteredPublicFunctionMember( ty, InternalEVMTypeDepositFunctionName, diff --git a/fvm/evm/stdlib/contract_test.go b/fvm/evm/stdlib/contract_test.go index 9bfdc02a495..5d5ba9ff1ab 100644 --- a/fvm/evm/stdlib/contract_test.go +++ b/fvm/evm/stdlib/contract_test.go @@ -18,6 +18,7 @@ import ( . "github.com/onflow/cadence/test_utils/runtime_utils" coreContracts "github.com/onflow/flow-core-contracts/lib/go/contracts" coreContractstemplates "github.com/onflow/flow-core-contracts/lib/go/templates" + gethTypes "github.com/onflow/go-ethereum/core/types" "github.com/onflow/go-ethereum/crypto" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -4287,6 +4288,124 @@ func TestEVMDryRun(t *testing.T) { assert.True(t, dryRunCalled) } +func TestEVMDryCall(t *testing.T) { + + t.Parallel() + + dryCallCalled := false + + contractsAddress := flow.BytesToAddress([]byte{0x1}) + handler := &testContractHandler{ + evmContractAddress: common.Address(contractsAddress), + dryRun: func(tx []byte, from types.Address) *types.ResultSummary { + dryCallCalled = true + gethTx := &gethTypes.Transaction{} + if err := gethTx.UnmarshalBinary(tx); err != nil { + require.Fail(t, err.Error()) + } + + require.NotNil(t, gethTx.To()) + + assert.Equal( + t, + types.Address{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 10}, + from, + ) + assert.Equal( + t, + types.Address{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 15}, + types.NewAddress(*gethTx.To()), + ) + assert.Equal(t, []byte{255, 107, 204, 122}, gethTx.Data()) + assert.Equal(t, uint64(33_000), gethTx.Gas()) + assert.Equal(t, big.NewInt(150), gethTx.Value()) + + return &types.ResultSummary{ + Status: types.StatusSuccessful, + } + }, + } + + transactionEnvironment := newEVMTransactionEnvironment(handler, contractsAddress) + scriptEnvironment := newEVMScriptEnvironment(handler, contractsAddress) + + rt := runtime.NewInterpreterRuntime(runtime.Config{}) + + 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: newLocationResolver(contractsAddress), + 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, + ) + + // Run script + + script := []byte(` + import EVM from 0x1 + + access(all) + fun main(): EVM.Result { + return EVM.dryCall( + from: EVM.EVMAddress(bytes: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 10]), + to: EVM.EVMAddress(bytes: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 15]), + data: [255, 107, 204, 122], + gasLimit: 33000, + value: EVM.Balance(attoflow: 150) + ) + } + `) + + val, err := rt.ExecuteScript( + runtime.Script{ + Source: script, + Arguments: nil, + }, + runtime.Context{ + Interface: runtimeInterface, + Environment: scriptEnvironment, + Location: nextScriptLocation(), + }, + ) + require.NoError(t, err) + res, err := impl.ResultSummaryFromEVMResultValue(val) + require.NoError(t, err) + assert.Equal(t, types.StatusSuccessful, res.Status) + assert.True(t, dryCallCalled) +} + func TestEVMBatchRun(t *testing.T) { t.Parallel() @@ -4673,6 +4792,136 @@ func TestCadenceOwnedAccountCall(t *testing.T) { require.Equal(t, expected, actual) } +func TestCadenceOwnedAccountDryCall(t *testing.T) { + + t.Parallel() + + dryCallCalled := false + + contractsAddress := flow.BytesToAddress([]byte{0x1}) + + handler := &testContractHandler{ + evmContractAddress: common.Address(contractsAddress), + dryRun: func(tx []byte, from types.Address) *types.ResultSummary { + dryCallCalled = true + gethTx := &gethTypes.Transaction{} + if err := gethTx.UnmarshalBinary(tx); err != nil { + require.Fail(t, err.Error()) + } + + require.NotNil(t, gethTx.To()) + + assert.Equal( + t, + types.Address{4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + from, + ) + assert.Equal( + t, + types.Address{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 15}, + types.NewAddress(*gethTx.To()), + ) + assert.Equal(t, []byte{4, 5, 6}, gethTx.Data()) + assert.Equal(t, uint64(33_000), gethTx.Gas()) + assert.Equal(t, big.NewInt(1230000000000000000), gethTx.Value()) + + return &types.ResultSummary{ + Status: types.StatusSuccessful, + ReturnedData: []byte{3, 1, 4}, + } + }, + } + + transactionEnvironment := newEVMTransactionEnvironment(handler, contractsAddress) + scriptEnvironment := newEVMScriptEnvironment(handler, contractsAddress) + + rt := runtime.NewInterpreterRuntime(runtime.Config{}) + + script := []byte(` + import EVM from 0x1 + + access(all) + fun main(): [UInt8] { + let cadenceOwnedAccount <- EVM.createCadenceOwnedAccount() + let bal = EVM.Balance(attoflow: 0) + bal.setFLOW(flow: 1.23) + let response = cadenceOwnedAccount.dryCall( + to: EVM.EVMAddress( + bytes: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 15] + ), + data: [4, 5, 6], + gasLimit: 33000, + value: bal + ) + destroy cadenceOwnedAccount + return response.data + } + `) + + 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: newLocationResolver(contractsAddress), + 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, + ) + + // Run script + + actual, err := rt.ExecuteScript( + runtime.Script{ + Source: script, + }, + runtime.Context{ + Interface: runtimeInterface, + Environment: scriptEnvironment, + Location: nextScriptLocation(), + }, + ) + require.NoError(t, err) + + expected := cadence.NewArray([]cadence.Value{ + cadence.UInt8(3), + cadence.UInt8(1), + cadence.UInt8(4), + }).WithType(cadence.NewVariableSizedArrayType(cadence.UInt8Type)) + + require.Equal(t, expected, actual) + require.True(t, dryCallCalled) +} + func TestEVMAddressDeposit(t *testing.T) { t.Parallel() diff --git a/utils/unittest/execution_state.go b/utils/unittest/execution_state.go index a5e911b3771..d8a9995519b 100644 --- a/utils/unittest/execution_state.go +++ b/utils/unittest/execution_state.go @@ -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 = "c42fc978c2702793d2640e3ed8644ba54db4e92aa5d0501234dfbb9bbc5784fd" +const GenesisStateCommitmentHex = "5538a445b456f0762979aef17de82b0119c03560e39e8854d366a1d6a98cd988" var GenesisStateCommitment flow.StateCommitment @@ -87,10 +87,10 @@ func genesisCommitHexByChainID(chainID flow.ChainID) string { return GenesisStateCommitmentHex } if chainID == flow.Testnet { - return "e29456decb9ee90ad3ed1e1239383c18897b031ea851ff07f5f616657df4d4a0" + return "a77b4580da5c4844115912ce4494c021c423574a25c3b2ff9623c4447367a1cf" } if chainID == flow.Sandboxnet { return "e1c08b17f9e5896f03fe28dd37ca396c19b26628161506924fbf785834646ea1" } - return "e1989abf50fba23015251a313eefe2ceff45639a75252f4da5970dcda32dd95e" + return "a4fd537dcfd9c599cd99d507574f9527c950dc7ccd7ccfa7853074a5216c3f33" }