diff --git a/frame/evm/src/runner/stack.rs b/frame/evm/src/runner/stack.rs index 82926bf2c4..73a3779e3b 100644 --- a/frame/evm/src/runner/stack.rs +++ b/frame/evm/src/runner/stack.rs @@ -130,18 +130,23 @@ where >, ) -> (ExitReason, R), { - // EIP-3607: https://eips.ethereum.org/EIPS/eip-3607 - // Do not allow transactions for which `tx.sender` has any code deployed. - // - // We extend the principle of this EIP to also prevent `tx.sender` to be the address - // of a precompile. While mainnet Ethereum currently only has stateless precompiles, - // projects using Frontier can have stateful precompiles that can manage funds or - // which calls other contracts that expects this precompile address to be trustworthy. - if !>::get(source).is_empty() || precompiles.is_precompile(source) { - return Err(RunnerError { - error: Error::::TransactionMustComeFromEOA, - weight, - }); + // Only check the restrictions of EIP-3607 if the source of the EVM operation is from an external transaction. + // If the source of this EVM operation is from an internal call, like from `eth_call` or `eth_estimateGas` RPC, + // we will skip the checks for the EIP-3607. + if is_transactional { + // EIP-3607: https://eips.ethereum.org/EIPS/eip-3607 + // Do not allow transactions for which `tx.sender` has any code deployed. + // + // We extend the principle of this EIP to also prevent `tx.sender` to be the address + // of a precompile. While mainnet Ethereum currently only has stateless precompiles, + // projects using Frontier can have stateful precompiles that can manage funds or + // which calls other contracts that expects this precompile address to be trustworthy. + if !>::get(source).is_empty() || precompiles.is_precompile(source) { + return Err(RunnerError { + error: Error::::TransactionMustComeFromEOA, + weight, + }); + } } let (total_fee_per_gas, _actual_priority_fee_per_gas) = diff --git a/frame/evm/src/tests.rs b/frame/evm/src/tests.rs index 1e8202ea43..c38c47c0e7 100644 --- a/frame/evm/src/tests.rs +++ b/frame/evm/src/tests.rs @@ -572,8 +572,8 @@ fn eip3607_transaction_from_contract_should_fail() { None, None, Vec::new(), - false, // non-transactional - true, // must be validated + true, // transactional + false, // may not be validated &::config().clone(), ) { Err(RunnerError { @@ -585,6 +585,29 @@ fn eip3607_transaction_from_contract_should_fail() { }); } +#[test] +fn eip3607_non_transaction_from_contract_should_succeed() { + new_test_ext().execute_with(|| { + let r = ::Runner::call( + // Contract address. + H160::from_str("1000000000000000000000000000000000000001").unwrap(), + H160::from_str("1000000000000000000000000000000000000001").unwrap(), + Vec::new(), + U256::from(1u32), + 1000000, + None, + None, + None, + Vec::new(), + false, // non-transactional + true, // must be validated + &::config().clone(), + ); + + assert!(r.is_ok()); + }); +} + #[test] fn eip3607_transaction_from_precompile_should_fail() { new_test_ext().execute_with(|| { @@ -599,8 +622,8 @@ fn eip3607_transaction_from_precompile_should_fail() { None, None, Vec::new(), - false, // non-transactional - true, // must be validated + true, // transactional + false, // may be validated &::config().clone(), ) { Err(RunnerError { @@ -611,3 +634,26 @@ fn eip3607_transaction_from_precompile_should_fail() { } }); } + +#[test] +fn eip3607_non_transaction_from_precompile_should_succeed() { + new_test_ext().execute_with(|| { + let r = ::Runner::call( + // Precompile address. + H160::from_str("0000000000000000000000000000000000000001").unwrap(), + H160::from_str("1000000000000000000000000000000000000001").unwrap(), + Vec::new(), + U256::from(1u32), + 1000000, + None, + None, + None, + Vec::new(), + false, // non-transactional + true, // must be validated + &::config().clone(), + ); + + assert!(r.is_ok()); + }); +} diff --git a/ts-tests/tests/test-eth-call.ts b/ts-tests/tests/test-eth-call.ts new file mode 100644 index 0000000000..da9613ce2e --- /dev/null +++ b/ts-tests/tests/test-eth-call.ts @@ -0,0 +1,77 @@ +import { expect, use as chaiUse } from "chai"; +import chaiAsPromised from "chai-as-promised"; +import Web3 from "web3"; +import { Contract } from "web3-eth-contract"; +import { AbiItem } from "web3-utils"; + +import Test from "../build/contracts/Test.json"; +import { GENESIS_ACCOUNT, GENESIS_ACCOUNT_PRIVATE_KEY } from "./config"; +import { createAndFinalizeBlock, customRequest, describeWithFrontier } from "./util"; + +chaiUse(chaiAsPromised); + +async function deployContract( + web3: Web3, + bytecode: string, + abi: AbiItem[] +): Promise<{ + contract: Contract; + contractAddress: string; +}> { + const contract = new web3.eth.Contract(abi); + const data = contract.deploy({ data: bytecode }).encodeABI(); + const tx = await web3.eth.accounts.signTransaction( + { + from: GENESIS_ACCOUNT, + data, + value: "0x00", + gas: "0x100000", + }, + GENESIS_ACCOUNT_PRIVATE_KEY + ); + const { result } = await customRequest(web3, "eth_sendRawTransaction", [tx.rawTransaction]); + await createAndFinalizeBlock(web3); + const receipt = await web3.eth.getTransactionReceipt(result); + const contractAddress = receipt.contractAddress; + + return { contract, contractAddress }; +} + +describeWithFrontier("Frontier RPC (EthCall)", (context) => { + let contract: Contract; + let contractAddress: string; + let contractAddress2: string; + + before("deploy contract to two addresses", async function () { + this.timeout(15000); + const firstDeploy = await deployContract(context.web3, Test.bytecode, Test.abi as AbiItem[]); + const secondDeploy = await deployContract(context.web3, Test.bytecode, Test.abi as AbiItem[]); + contract = firstDeploy.contract; + contractAddress = firstDeploy.contractAddress; + contractAddress2 = secondDeploy.contractAddress; + }); + + it("should be able to call eth_call from address with no code", async function () { + const r = await customRequest(context.web3, "eth_call", [ + { + from: GENESIS_ACCOUNT, + to: contractAddress, + data: await contract.methods.multiply(5).encodeABI(), + }, + ]); + expect(r.error?.message).to.be.undefined; + expect(Web3.utils.hexToNumberString(r.result)).to.equal("35"); + }); + + it("should be able to call eth_call from address with code", async function () { + const r = await customRequest(context.web3, "eth_call", [ + { + from: contractAddress2, + to: contractAddress, + data: await contract.methods.multiply(5).encodeABI(), + }, + ]); + expect(r.error?.message).to.be.undefined; + expect(Web3.utils.hexToNumberString(r.result)).to.equal("35"); + }); +});