From 7626e6dad022223bcc198eb5fef573fa8c9e8abd Mon Sep 17 00:00:00 2001 From: Christoph Burgdorf Date: Fri, 26 Mar 2021 18:48:19 +0100 Subject: [PATCH] WIP --- .../tests/fixtures/solidity/revert_test.sol | 5 + compiler/tests/runtime.rs | 16 +++ compiler/tests/solidity.rs | 36 +++++ compiler/tests/utils.rs | 132 +++++++++++++++++- 4 files changed, 186 insertions(+), 3 deletions(-) create mode 100644 compiler/tests/fixtures/solidity/revert_test.sol create mode 100644 compiler/tests/solidity.rs diff --git a/compiler/tests/fixtures/solidity/revert_test.sol b/compiler/tests/fixtures/solidity/revert_test.sol new file mode 100644 index 0000000000..419b49a21b --- /dev/null +++ b/compiler/tests/fixtures/solidity/revert_test.sol @@ -0,0 +1,5 @@ +contract Foo { + function revert_me() public pure returns(uint){ + revert("Not enough Ether provided."); + } +} \ No newline at end of file diff --git a/compiler/tests/runtime.rs b/compiler/tests/runtime.rs index 7dc2d3db8e..cf1de9456b 100644 --- a/compiler/tests/runtime.rs +++ b/compiler/tests/runtime.rs @@ -23,6 +23,22 @@ macro_rules! assert_eq { }; } +#[test] +fn test_foobar() { + with_executor(&|mut executor| { + test_runtime_functions_revert( + &mut executor, + functions::std(), + statements! { + //(let num := 63806209331542711802848847270949280092855778197726125910674179583545433573378) + //(mstore(0, num)) + (revert(0, 1)) + }, + &[0], + ); + }) +} + #[test] fn test_runtime_alloc_and_avail() { with_executor(&|mut executor| { diff --git a/compiler/tests/solidity.rs b/compiler/tests/solidity.rs new file mode 100644 index 0000000000..bc4b1f54af --- /dev/null +++ b/compiler/tests/solidity.rs @@ -0,0 +1,36 @@ +//! Solidity test that help us prove assumptions about how Solidty handles +//! certain things + +#![cfg(feature = "solc-backend")] +use rstest::rstest; + +mod utils; +use utils::*; + +#[test] +fn test_revert_string_reason() { + with_executor(&|mut executor| { + let harness = deploy_solidity_contract(&mut executor, "revert_test.sol", "Foo", &[]); + + let exit = harness.capture_call(&mut executor, "revert_me", &[]); + + let expected_reason = format!( + "0x{}", + hex::encode(encode_error_reason("Not enough Ether provided.")) + ); + if let evm::Capture::Exit((evm::ExitReason::Revert(_), output)) = exit { + assert_eq!(format!("0x{}", hex::encode(&output)), expected_reason); + } else { + panic!("failed") + }; + }) +} + +#[rstest(reason_str, expected_encoding, + case("Not enough Ether provided.", "0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001a4e6f7420656e6f7567682045746865722070726f76696465642e000000000000"), + case("A muuuuuch longer reason string that consumes multiple words", "0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003c41206d75757575756368206c6f6e67657220726561736f6e20737472696e67207468617420636f6e73756d6573206d756c7469706c6520776f72647300000000"), +)] +fn test_reason_encoding(reason_str: &str, expected_encoding: &str) { + let encoded = encode_error_reason(reason_str); + assert_eq!(format!("0x{}", hex::encode(&encoded)), expected_encoding); +} diff --git a/compiler/tests/utils.rs b/compiler/tests/utils.rs index 1aa2c96806..4a19d8d77b 100644 --- a/compiler/tests/utils.rs +++ b/compiler/tests/utils.rs @@ -202,9 +202,64 @@ pub fn deploy_contract( .contracts .get(contract_name) .expect("could not find contract in fixture"); - let abi = ethabi::Contract::load(StringReader::new(&compiled_contract.json_abi)) - .expect("unable to load the ABI"); - let mut bytecode = hex::decode(&compiled_contract.bytecode).expect("failed to decode bytecode"); + + return _deploy_contract( + executor, + &compiled_contract.bytecode, + &compiled_contract.json_abi, + init_params, + ); +} + +#[allow(dead_code)] +pub fn deploy_solidity_contract( + executor: &mut Executor, + fixture: &str, + contract_name: &str, + init_params: &[ethabi::Token], +) -> ContractHarness { + let src = fs::read_to_string(format!("tests/fixtures/solidity/{}", fixture)) + .expect("unable to read fixture file") + .replace("\n", "") + .replace("\"", "\\\""); + + let (bytecode, abi) = + compile_solidity_contract(contract_name, &src).expect("Could not compile contract"); + + return _deploy_contract(executor, &bytecode, &abi, init_params); +} + +#[allow(dead_code)] +pub fn encode_error_reason(reason: &str) -> Vec { + // Function selector for Error(string) + const SELECTOR: &str = "08c379a0"; + // Data offset + const DATA_OFFSET: &str = "0000000000000000000000000000000000000000000000000000000000000020"; + + // Length of the string padded to 32 bit hex + let string_len = format!("{:0>64x}", reason.len()); + + let mut string_bytes = reason.as_bytes().to_vec(); + while string_bytes.len() % 32 != 0 { + string_bytes.push(0) + } + // The bytes of the string itself, right padded to consume a multiple of 32 + // bytes + let string_bytes = hex::encode(&string_bytes); + + let all = format!("{}{}{}{}", SELECTOR, DATA_OFFSET, string_len, string_bytes); + hex::decode(all).expect("meh") +} + +fn _deploy_contract( + executor: &mut Executor, + bytecode: &str, + abi: &str, + init_params: &[ethabi::Token], +) -> ContractHarness { + let abi = ethabi::Contract::load(StringReader::new(abi)).expect("unable to load the ABI"); + + let mut bytecode = hex::decode(bytecode).expect("failed to decode bytecode"); if let Some(constructor) = &abi.constructor { bytecode = constructor.encode_input(bytecode, init_params).unwrap() @@ -225,6 +280,36 @@ pub fn deploy_contract( panic!("Failed to create contract") } +pub fn compile_solidity_contract(name: &str, solidity_src: &str) -> Result<(String, String), ()> { + let solc_config = r#" + { + "language": "Solidity", + "sources": { "input.sol": { "content": "{src}" } }, + "settings": { + "outputSelection": { "*": { "*": ["*"], "": [ "*" ] } } + } + } + "#; + let solc_config = solc_config.replace("{src}", &solidity_src); + + let raw_output = solc::compile(&solc_config); + + let output: serde_json::Value = + serde_json::from_str(&raw_output).expect("Unable to compile contract"); + + let bytecode = output["contracts"]["input.sol"][name]["evm"]["bytecode"]["object"] + .to_string() + .replace("\"", ""); + + let abi = output["contracts"]["input.sol"][name]["abi"].to_string(); + + if [&bytecode, &abi].iter().any(|val| val == &"null") { + return Err(()); + } + + Ok((bytecode, abi)) +} + #[allow(dead_code)] pub fn load_contract(address: H160, fixture: &str, contract_name: &str) -> ContractHarness { let src = fs::read_to_string(format!("tests/fixtures/{}", fixture)) @@ -276,6 +361,47 @@ pub fn test_runtime_functions( } } +#[allow(dead_code)] +pub fn test_runtime_functions_revert( + executor: &mut Executor, + functions: Vec, + test_statements: Vec, + expected_output: &[u8], +) { + let all_statements = [functions, test_statements].concat(); + let yul_code = yul::Object { + name: identifier! { Contract }, + code: code! { [all_statements...] }, + objects: vec![], + data: vec![], + } + .to_string() + .replace("\"", "\\\""); + let bytecode = compiler::evm::compile_single_contract("Contract", yul_code, false) + .expect("failed to compile Yul"); + let bytecode = hex::decode(&bytecode).expect("failed to decode bytecode"); + + if let evm::Capture::Exit((reason, _, output)) = executor.create( + address(DEFAULT_CALLER), + evm_runtime::CreateScheme::Legacy { + caller: address(DEFAULT_CALLER), + }, + U256::zero(), + bytecode, + None, + ) { + if output != expected_output { + panic!("Runtime function test failed (wrong output): {:?}", output) + } + + if !matches!(reason, ExitReason::Revert(_)) { + panic!("Runtime function test failed: {:?}", reason) + } + } else { + panic!("EVM trap during test") + } +} + #[allow(dead_code)] pub fn uint_token(n: usize) -> ethabi::Token { ethabi::Token::Uint(U256::from(n))