diff --git a/client-sdk/go/modules/evm/signed_calls.go b/client-sdk/go/modules/evm/signed_calls.go index 38715a1572..77fdbaa7f2 100644 --- a/client-sdk/go/modules/evm/signed_calls.go +++ b/client-sdk/go/modules/evm/signed_calls.go @@ -5,6 +5,7 @@ import ( "fmt" "math/big" + ethCommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/signer/core/apitypes" @@ -52,6 +53,11 @@ func NewSignedCallDataPack(signer RSVSigner, chainID uint64, caller, callee []by } func makeSignableCall(chainID uint64, caller, callee []byte, gasLimit uint64, gasPrice *big.Int, value *big.Int, data []byte, leash Leash) apitypes.TypedData { + if callee == nil { + var zeroAddress ethCommon.Address + callee = zeroAddress.Bytes() + } + if value == nil { value = big.NewInt(0) } diff --git a/client-sdk/go/modules/evm/types.go b/client-sdk/go/modules/evm/types.go index fec075b99a..fd8aed3f18 100644 --- a/client-sdk/go/modules/evm/types.go +++ b/client-sdk/go/modules/evm/types.go @@ -37,7 +37,7 @@ type SimulateCallQuery struct { GasPrice []byte `json:"gas_price"` GasLimit uint64 `json:"gas_limit"` Caller []byte `json:"caller"` - Address []byte `json:"address"` + Address []byte `json:"address,omitempty"` Value []byte `json:"value"` Data []byte `json:"data"` } diff --git a/runtime-sdk/modules/evm/src/lib.rs b/runtime-sdk/modules/evm/src/lib.rs index ae92b1811c..a066622ec8 100644 --- a/runtime-sdk/modules/evm/src/lib.rs +++ b/runtime-sdk/modules/evm/src/lib.rs @@ -316,27 +316,12 @@ impl API for Module { Self::decode_call_data(ctx, init_code, tx_call_format, tx_index, true)? .expect("processing always proceeds"); - Self::do_evm( - caller, - ctx, - |exec, gas_limit| { - let address = exec.create_address(evm::CreateScheme::Legacy { - caller: caller.into(), - }); - let (exit_reason, exit_value) = - exec.transact_create(caller.into(), value.into(), init_code, gas_limit, vec![]); - if exit_reason.is_succeed() { - // If successful return the contract deployed address. - (exit_reason, address.as_bytes().to_vec()) - } else { - // Otherwise propagate the exit value. - (exit_reason, exit_value) - } - }, - // If in simulation, this must be EstimateGas query. - // Use estimate mode if not doing binary search for exact gas costs. - is_simulation && ::Core::estimate_gas_search_max_iters(ctx) == 0, - ) + // If in simulation, this must be EstimateGas query. + // Use estimate mode if not doing binary search for exact gas costs. + let estimate_gas = + is_simulation && ::Core::estimate_gas_search_max_iters(ctx) == 0; + + Self::evm_create(ctx, caller, value, init_code, estimate_gas) } fn call( @@ -360,23 +345,12 @@ impl API for Module { Self::decode_call_data(ctx, data, tx_call_format, tx_index, true)? .expect("processing always proceeds"); - let evm_result = Self::do_evm( - caller, - ctx, - |exec, gas_limit| { - exec.transact_call( - caller.into(), - address.into(), - value.into(), - data, - gas_limit, - vec![], - ) - }, - // If in simulation, this must be EstimateGas query. - // Use estimate mode if not doing binary search for exact gas costs. - is_simulation && ::Core::estimate_gas_search_max_iters(ctx) == 0, - ); + // If in simulation, this must be EstimateGas query. + // Use estimate mode if not doing binary search for exact gas costs. + let estimate_gas = + is_simulation && ::Core::estimate_gas_search_max_iters(ctx) == 0; + + let evm_result = Self::evm_call(ctx, caller, address, value, data, estimate_gas); Self::encode_evm_result(ctx, evm_result, tx_metadata) } @@ -415,20 +389,46 @@ impl API for Module { tx_metadata, ) = Self::decode_simulate_call_query(ctx, call)?; - let call_tx = transaction::Transaction { + let (method, body, exec): (_, _, Box Result<_, _>>) = match address { + Some(address) => { + // Address is set, this is a simulated `evm.Call`. + ( + "evm.Call", + cbor::to_value(types::Call { + address, + value, + data: data.clone(), + }), + Box::new(move || Self::evm_call(ctx, caller, address, value, data, false)), + ) + } + None => { + // Address is not set, this is a simulated `evm.Create`. + ( + "evm.Create", + cbor::to_value(types::Create { + value, + init_code: data.clone(), + }), + Box::new(|| Self::evm_create(ctx, caller, value, data, false)), + ) + } + }; + let tx = transaction::Transaction { version: 1, call: transaction::Call { format: transaction::CallFormat::Plain, - method: "evm.Call".to_owned(), - body: cbor::to_value(types::Call { - address, - value, - data: data.clone(), - }), + method: method.to_owned(), + body, ..Default::default() }, auth_info: transaction::AuthInfo { - signer_info: vec![], + signer_info: vec![transaction::SignerInfo { + address_spec: transaction::AddressSpec::Internal( + transaction::CallerAddress::EthAddress(caller.into()), + ), + nonce: 0, + }], fee: transaction::Fee { amount: token::BaseUnits::new( gas_price @@ -443,38 +443,76 @@ impl API for Module { ..Default::default() }, }; + let evm_result = CurrentState::with_transaction_opts( Options::new() - .with_tx(TransactionWithMeta::internal(call_tx)) + .with_tx(TransactionWithMeta::internal(tx)) .with_mode(Mode::Simulate), - || { - let result = Self::do_evm( - caller, - ctx, - |exec, gas_limit| { - exec.transact_call( - caller.into(), - address.into(), - value.into(), - data, - gas_limit, - vec![], - ) - }, - // Simulate call is never called from EstimateGas. - false, - ); - - TransactionResult::Rollback(result) - }, + || TransactionResult::Rollback(exec()), ); Self::encode_evm_result(ctx, evm_result, tx_metadata) } } impl Module { - fn do_evm(source: H160, ctx: &C, f: F, estimate_gas: bool) -> Result, Error> + fn evm_call( + ctx: &C, + caller: H160, + address: H160, + value: U256, + data: Vec, + estimate_gas: bool, + ) -> Result, Error> { + Self::evm_execute( + ctx, + caller, + |exec, gas_limit| { + exec.transact_call( + caller.into(), + address.into(), + value.into(), + data, + gas_limit, + vec![], + ) + }, + estimate_gas, + ) + } + + fn evm_create( + ctx: &C, + caller: H160, + value: U256, + init_code: Vec, + estimate_gas: bool, + ) -> Result, Error> { + Self::evm_execute( + ctx, + caller, + |exec, gas_limit| { + // Precompute the address as execution can modify state such that the subsequent + // address derivation would be incorrect (e.g. due to nonce increments). + let address = exec.create_address(evm::CreateScheme::Legacy { + caller: caller.into(), + }); + let (exit_reason, exit_value) = + exec.transact_create(caller.into(), value.into(), init_code, gas_limit, vec![]); + if exit_reason.is_succeed() { + // If successful return the contract deployed address. + (exit_reason, address.as_bytes().to_vec()) + } else { + // Otherwise propagate the exit value. + (exit_reason, exit_value) + } + }, + estimate_gas, + ) + } + + fn evm_execute(ctx: &C, source: H160, f: F, estimate_gas: bool) -> Result, Error> where + C: Context, F: FnOnce( &mut StackExecutor< 'static, @@ -484,7 +522,6 @@ impl Module { >, u64, ) -> (evm::ExitReason, Vec), - C: Context, { let is_query = CurrentState::with_env(|env| !env.is_execute()); let cfg = Cfg::evm_config(estimate_gas); diff --git a/runtime-sdk/modules/evm/src/mock.rs b/runtime-sdk/modules/evm/src/mock.rs index ec0fbc56d0..265e1dd948 100644 --- a/runtime-sdk/modules/evm/src/mock.rs +++ b/runtime-sdk/modules/evm/src/mock.rs @@ -78,7 +78,7 @@ impl EvmSigner { } /// Dispatch a query to the given EVM contract method. - pub fn query_evm( + pub fn query_evm_call( &self, ctx: &C, address: H160, @@ -89,11 +89,11 @@ impl EvmSigner { where C: Context, { - self.query_evm_opts(ctx, address, name, param_types, params, Default::default()) + self.query_evm_call_opts(ctx, address, name, param_types, params, Default::default()) } /// Dispatch a query to the given EVM contract method. - pub fn query_evm_opts( + pub fn query_evm_call_opts( &self, ctx: &C, address: H160, @@ -105,12 +105,47 @@ impl EvmSigner { where C: Context, { - let mut data = [ + let data = [ ethabi::short_signature(name, param_types).to_vec(), ethabi::encode(params), ] .concat(); + self.query_evm_opts(ctx, Some(address), data, opts) + } + + /// Dispatch a query to simulate EVM contract creation. + pub fn query_evm_create(&self, ctx: &C, init_code: Vec) -> Result, RuntimeError> + where + C: Context, + { + self.query_evm_opts(ctx, None, init_code, Default::default()) + } + + /// Dispatch a query to simulate EVM contract creation. + pub fn query_evm_create_opts( + &self, + ctx: &C, + init_code: Vec, + opts: QueryOptions, + ) -> Result, RuntimeError> + where + C: Context, + { + self.query_evm_opts(ctx, None, init_code, opts) + } + + /// Dispatch a query to the EVM. + pub fn query_evm_opts( + &self, + ctx: &C, + address: Option, + mut data: Vec, + opts: QueryOptions, + ) -> Result, RuntimeError> + where + C: Context, + { // Handle optional encryption. let client_keypair = deoxysii::generate_key_pair(); if opts.encrypt { diff --git a/runtime-sdk/modules/evm/src/signed_call.rs b/runtime-sdk/modules/evm/src/signed_call.rs index 9af3527b1f..8cb2034d84 100644 --- a/runtime-sdk/modules/evm/src/signed_call.rs +++ b/runtime-sdk/modules/evm/src/signed_call.rs @@ -114,7 +114,7 @@ fn hash_call(query: &SimulateCallQuery, leash: &Leash) -> [u8; 32] { hash_encoded(&[ encode_bytes(CALL_TYPE_STR), Token::Address(query.caller.0.into()), - Token::Address(query.address.0.into()), + Token::Address(query.address.unwrap_or_default().0.into()), Token::Uint(query.gas_limit.into()), Token::Uint(ethabi::ethereum_types::U256(query.gas_price.0)), Token::Uint(ethabi::ethereum_types::U256(query.value.0)), @@ -180,9 +180,11 @@ mod test { caller: "0x11e244400Cf165ade687077984F09c3A037b868F" .parse() .unwrap(), - address: "0xb5ed90452AAC09f294a0BE877CBf2Dc4D55e096f" - .parse() - .unwrap(), + address: Some( + "0xb5ed90452AAC09f294a0BE877CBf2Dc4D55e096f" + .parse() + .unwrap(), + ), value: 42u64.into(), data: cbor::from_value(data_pack.data.body.clone()).unwrap(), }, diff --git a/runtime-sdk/modules/evm/src/test.rs b/runtime-sdk/modules/evm/src/test.rs index 07c2a0c897..998c09aa0e 100644 --- a/runtime-sdk/modules/evm/src/test.rs +++ b/runtime-sdk/modules/evm/src/test.rs @@ -817,6 +817,26 @@ fn test_c10l_queries() { static QUERY_CONTRACT_CODE_HEX: &str = include_str!("../../../../tests/e2e/contracts/query/query.hex"); + // Simulate contract creation. + let result = signer + .query_evm_create(&ctx, load_contract_bytecode(QUERY_CONTRACT_CODE_HEX)) + .expect("query should succeed"); + let contract_address1 = H160::from_slice(&result); + + let result = signer + .query_evm_create_opts( + &ctx, + load_contract_bytecode(QUERY_CONTRACT_CODE_HEX), + QueryOptions { + encrypt: true, + ..Default::default() + }, + ) + .expect("query should succeed"); + let contract_address2 = H160::from_slice(&result); + + assert_eq!(contract_address1, contract_address2); + // Create contract. let dispatch_result = signer.call( &ctx, @@ -830,11 +850,9 @@ fn test_c10l_queries() { let result: Vec = cbor::from_value(result).unwrap(); let contract_address = H160::from_slice(&result); - let ctx = mock.create_ctx_for_runtime::>(true); - // Call the `test` method on the contract via a query. let result = signer - .query_evm(&ctx, contract_address, "test", &[], &[]) + .query_evm_call(&ctx, contract_address, "test", &[], &[]) .expect("query should succeed"); let mut result = @@ -845,7 +863,7 @@ fn test_c10l_queries() { // Test call with confidential envelope. let result = signer - .query_evm_opts( + .query_evm_call_opts( &ctx, contract_address, "test", @@ -1170,7 +1188,7 @@ fn test_return_value_limits() { let ctx = mock.create_ctx_for_runtime::>(true); let result = signer - .query_evm_opts( + .query_evm_call_opts( &ctx, contract_address, "testSuccess", diff --git a/runtime-sdk/modules/evm/src/types.rs b/runtime-sdk/modules/evm/src/types.rs index a12932da01..bd954c0544 100644 --- a/runtime-sdk/modules/evm/src/types.rs +++ b/runtime-sdk/modules/evm/src/types.rs @@ -41,7 +41,8 @@ pub struct SimulateCallQuery { pub gas_price: U256, pub gas_limit: u64, pub caller: H160, - pub address: H160, + #[cbor(optional)] + pub address: Option, pub value: U256, pub data: Vec, }