From 17cc10b3f7bf8c853c8d9866d27ac57e6c3f94ef Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Mon, 27 Jan 2025 13:01:52 -0500 Subject: [PATCH] Test Execute result decoding, reentrancy --- .../router/contracts/tests/CreateAddress.sol | 2 +- .../router/contracts/tests/Reentrancy.sol | 17 ++++ processor/ethereum/router/src/tests/mod.rs | 82 ++++++++++++++++--- 3 files changed, 87 insertions(+), 14 deletions(-) create mode 100644 processor/ethereum/router/contracts/tests/Reentrancy.sol diff --git a/processor/ethereum/router/contracts/tests/CreateAddress.sol b/processor/ethereum/router/contracts/tests/CreateAddress.sol index 2d092449..6aa57629 100644 --- a/processor/ethereum/router/contracts/tests/CreateAddress.sol +++ b/processor/ethereum/router/contracts/tests/CreateAddress.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.26; import "Router.sol"; -// Wrap the Router with a contract which exposes the address +// Wrap the Router with a contract which exposes the createAddress function contract CreateAddress is Router { constructor() Router(bytes32(uint256(1))) { } diff --git a/processor/ethereum/router/contracts/tests/Reentrancy.sol b/processor/ethereum/router/contracts/tests/Reentrancy.sol new file mode 100644 index 00000000..979fd74d --- /dev/null +++ b/processor/ethereum/router/contracts/tests/Reentrancy.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.26; + +import "Router.sol"; + +// This inherits from the Router for visibility over Reentered +contract Reentrancy { + error Reentered(); + + constructor() { + (bool success, bytes memory res) = + msg.sender.call(abi.encodeWithSelector(Router.execute4DE42904.selector, "")); + require(!success); + // We can't compare `bytes memory` so we hash them and compare the hashes + require(keccak256(res) == keccak256(abi.encode(Reentered.selector))); + } +} diff --git a/processor/ethereum/router/src/tests/mod.rs b/processor/ethereum/router/src/tests/mod.rs index f879f181..4c21aeaf 100644 --- a/processor/ethereum/router/src/tests/mod.rs +++ b/processor/ethereum/router/src/tests/mod.rs @@ -456,9 +456,12 @@ impl Test { let gas_provided = trace.action.as_call().as_ref().unwrap().gas; let gas_spent = trace.result.as_ref().unwrap().gas_used(); unused_gas += gas_provided - gas_spent; - for _ in 0 .. trace.subtraces { - // Skip the subtraces for this call (such as CREATE) - traces.next().unwrap(); + + let mut subtraces = trace.subtraces; + while subtraces != 0 { + // Skip the subtraces (and their subtraces) for this call (such as CREATE) + subtraces += traces.next().unwrap().trace.subtraces; + subtraces -= 1; } } @@ -774,9 +777,6 @@ async fn test_empty_execute() { } } -// TODO: Test order, length of results -// TODO: Test reentrancy - #[tokio::test] async fn test_eth_address_out_instruction() { let mut test = Test::new().await; @@ -921,6 +921,31 @@ async fn test_erc20_code_out_instruction() { assert_eq!(test.provider.get_code_at(deployed).await.unwrap().to_vec(), true.abi_encode()); } +#[tokio::test] +async fn test_result_decoding() { + let mut test = Test::new().await; + test.confirm_next_serai_key().await; + + // Create three OutInstructions, where the last one errors + let out_instructions = OutInstructions::from( + [ + (SeraiEthereumAddress::Address([0; 20]), U256::from(0)), + (SeraiEthereumAddress::Address([0; 20]), U256::from(0)), + (SeraiEthereumAddress::Contract(ContractDeployment::new(0, vec![]).unwrap()), U256::from(0)), + ] + .as_slice(), + ); + + let gas = test.router.execute_gas(Coin::Ether, U256::from(0), &out_instructions); + + // We should decode these in the correct order (not `false, true, true`) + let (_tx, gas_used) = + test.execute(Coin::Ether, U256::from(0), out_instructions, vec![true, true, false]).await; + // We don't check strict equality as we don't know how much gas was used by the reverted call + // (even with the trace), solely that it used less than or equal to the limit + assert!(gas_used <= gas); +} + #[tokio::test] async fn test_escape_hatch() { let mut test = Test::new().await; @@ -1038,10 +1063,41 @@ async fn test_escape_hatch() { } } -/* TODO - event Batch(uint256 indexed nonce, bytes32 indexed messageHash, bytes results); - error Reentered(); - error EscapeFailed(); - function executeArbitraryCode(bytes memory code) external payable; - function createAddress(uint256 nonce) private view returns (address); -*/ +#[tokio::test] +async fn test_reentrancy() { + let mut test = Test::new().await; + test.confirm_next_serai_key().await; + + const BYTECODE: &[u8] = { + const BYTECODE_HEX: &[u8] = include_bytes!(concat!( + env!("OUT_DIR"), + "/serai-processor-ethereum-router/tests/Reentrancy.bin" + )); + const BYTECODE: [u8; BYTECODE_HEX.len() / 2] = + match alloy_core::primitives::hex::const_decode_to_array::<{ BYTECODE_HEX.len() / 2 }>( + BYTECODE_HEX, + ) { + Ok(bytecode) => bytecode, + Err(_) => panic!("Reentrancy.bin did not contain valid hex"), + }; + &BYTECODE + }; + + let out_instructions = OutInstructions::from( + [( + // The Reentrancy contract, in its constructor, will re-enter and verify the proper error is + // returned + SeraiEthereumAddress::Contract(ContractDeployment::new(50_000, BYTECODE.to_vec()).unwrap()), + U256::from(0), + )] + .as_slice(), + ); + + let gas = test.router.execute_gas(Coin::Ether, U256::from(0), &out_instructions); + let (_tx, gas_used) = + test.execute(Coin::Ether, U256::from(0), out_instructions, vec![true]).await; + // Even though this doesn't have failed `OutInstruction`s, our logic is incomplete upon any + // failed internal calls for some reason. That's fine, as the gas yielded is still the worst-case + // (which this isn't a counter-example to) and is validated to be the worst-case, but is peculiar + assert!(gas_used <= gas); +}