Skip to content

Commit

Permalink
Add tests that cover how solidity handles revert reason strings
Browse files Browse the repository at this point in the history
  • Loading branch information
cburgdorf committed Apr 19, 2021
1 parent 32c3f00 commit b2f6347
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 7 deletions.
13 changes: 13 additions & 0 deletions compiler/tests/fixtures/solidity/revert_test.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
contract Foo {
function revert_me() public pure returns(uint){
revert("Not enough Ether provided.");
}

function revert_with_long_string() public pure returns(uint){
revert("A muuuuuch longer reason string that consumes multiple words");
}

function revert_with_empty_string() public pure returns(uint){
revert("");
}
}
44 changes: 44 additions & 0 deletions compiler/tests/solidity.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//! Solidity tests that help us prove assumptions about how Solidty handles
//! certain things

#![cfg(feature = "solc-backend")]
use rstest::rstest;

mod utils;
use utils::*;

#[rstest(
method,
reason,
case("revert_me", "Not enough Ether provided."),
case(
"revert_with_long_string",
"A muuuuuch longer reason string that consumes multiple words"
),
case("revert_with_empty_string", "")
)]
fn test_revert_string_reason(method: &str, reason: &str) {
with_executor(&|mut executor| {
let harness = deploy_solidity_contract(&mut executor, "revert_test.sol", "Foo", &[]);

let exit = harness.capture_call(&mut executor, method, &[]);

let expected_reason = format!("0x{}", hex::encode(encode_error_reason(reason)));
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"),
case("", "0x08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"),
case("foo", "0x08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003666f6f0000000000000000000000000000000000000000000000000000000000"),
)]
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);
}
125 changes: 118 additions & 7 deletions compiler/tests/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8> {
// 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()
Expand All @@ -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))
Expand All @@ -246,6 +331,34 @@ pub fn test_runtime_functions(
functions: Vec<yul::Statement>,
test_statements: Vec<yul::Statement>,
) {
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<yul::Statement>,
test_statements: Vec<yul::Statement>,
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<yul::Statement>,
test_statements: Vec<yul::Statement>,
) -> (ExitReason, Vec<u8>) {
let all_statements = [functions, test_statements].concat();
let yul_code = yul::Object {
name: identifier! { Contract },
Expand All @@ -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),
Expand All @@ -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")
}
Expand Down
2 changes: 2 additions & 0 deletions newsfragments/342.internal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Added support for running tests against solidity fixtures.
Also added tests that cover how solidity encodes revert reason strings.

0 comments on commit b2f6347

Please sign in to comment.