mirror of
https://github.com/serai-dex/serai.git
synced 2025-04-22 22:18:15 +00:00
Test Execute result decoding, reentrancy
This commit is contained in:
parent
7e01589fba
commit
17cc10b3f7
3 changed files with 87 additions and 14 deletions
processor/ethereum/router
|
@ -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))) { }
|
||||
|
||||
|
|
17
processor/ethereum/router/contracts/tests/Reentrancy.sol
Normal file
17
processor/ethereum/router/contracts/tests/Reentrancy.sol
Normal 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)));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue