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..3fdbadd998 100644 --- a/compiler/tests/runtime.rs +++ b/compiler/tests/runtime.rs @@ -23,6 +23,24 @@ macro_rules! assert_eq { }; } +#[test] +fn test_revert() { + with_executor(&|mut executor| { + test_runtime_functions_revert( + &mut executor, + functions::std(), + statements! { + (mstore(0, 5)) + (revert(0, 32)) + }, + &[ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 5, + ], + ); + }) +} + #[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..38ce2de385 --- /dev/null +++ b/compiler/tests/solidity.rs @@ -0,0 +1,36 @@ +//! Solidity tests 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_revert_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..d573f51474 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(&format!("No valid hex: {}", &all)) +} + +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)) @@ -246,6 +331,34 @@ pub fn test_runtime_functions( functions: Vec, test_statements: Vec, ) { + let (reason, _) = execute_runtime_functions(executor, functions, test_statements); + if !matches!(reason, ExitReason::Succeed(_)) { + panic!("Runtime function test failed: {:?}", reason) + } +} + +#[allow(dead_code)] +pub fn test_runtime_functions_revert( + executor: &mut Executor, + functions: Vec, + test_statements: Vec, + expected_output: &[u8], +) { + let (reason, output) = execute_runtime_functions(executor, functions, test_statements); + if output != expected_output { + panic!("Runtime function test failed (wrong output): {:?}", output) + } + + if !matches!(reason, ExitReason::Revert(_)) { + panic!("Runtime function did not revert: {:?}", reason) + } +} + +fn execute_runtime_functions( + executor: &mut Executor, + functions: Vec, + test_statements: Vec, +) -> (ExitReason, Vec) { let all_statements = [functions, test_statements].concat(); let yul_code = yul::Object { name: identifier! { Contract }, @@ -259,7 +372,7 @@ pub fn test_runtime_functions( .expect("failed to compile Yul"); let bytecode = hex::decode(&bytecode).expect("failed to decode bytecode"); - if let evm::Capture::Exit((reason, _, _)) = executor.create( + if let evm::Capture::Exit((reason, _, output)) = executor.create( address(DEFAULT_CALLER), evm_runtime::CreateScheme::Legacy { caller: address(DEFAULT_CALLER), @@ -268,9 +381,7 @@ pub fn test_runtime_functions( bytecode, None, ) { - if !matches!(reason, ExitReason::Succeed(_)) { - panic!("Runtime function test failed: {:?}", reason) - } + (reason, output) } else { panic!("EVM trap during test") } diff --git a/newsfragments/342.internal.md b/newsfragments/342.internal.md new file mode 100644 index 0000000000..5997affe1c --- /dev/null +++ b/newsfragments/342.internal.md @@ -0,0 +1,2 @@ +Added support for running tests against solidity fixtures. +Also added tests that cover how solidity encodes revert reason strings. \ No newline at end of file