Test Execute result decoding, reentrancy

This commit is contained in:
Luke Parker 2025-01-27 13:01:52 -05:00
parent 7e01589fba
commit 17cc10b3f7
No known key found for this signature in database
3 changed files with 87 additions and 14 deletions
processor/ethereum/router

View file

@ -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))) { }

View file

@ -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)));
}
}

View file

@ -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);
}