3 Commits

Author SHA1 Message Date
Luke Parker
3892fa30b7 Test an empty execute 2025-01-24 17:13:36 -05:00
Luke Parker
ed599c8ab5 Have the Batch event encode the amount of results
Necessary to distinguish a bitvec with 1 results from a bitvec with 7 results.
2025-01-24 17:04:25 -05:00
Luke Parker
29bb5e21ab Take advantage of RangeInclusive for specifying filters' blocks 2025-01-24 07:44:47 -05:00
10 changed files with 280 additions and 97 deletions

View File

@@ -2,6 +2,7 @@
#![doc = include_str!("../README.md")]
#![deny(missing_docs)]
use core::ops::RangeInclusive;
use std::collections::HashMap;
use alloy_core::primitives::{Address, U256};
@@ -76,8 +77,8 @@ pub struct TopLevelTransfers {
pub struct Erc20;
impl Erc20 {
/// The filter for transfer logs of the specified ERC20, to the specified recipient.
fn transfer_filter(from_block: u64, to_block: u64, erc20: Address, to: Address) -> Filter {
let filter = Filter::new().from_block(from_block).to_block(to_block);
fn transfer_filter(blocks: RangeInclusive<u64>, erc20: Address, to: Address) -> Filter {
let filter = Filter::new().select(blocks);
filter.address(erc20).event_signature(Transfer::SIGNATURE_HASH).topic2(to.into_word())
}
@@ -180,14 +181,13 @@ impl Erc20 {
/// The `transfers` in the result are unordered. The `logs` are sorted by index.
pub async fn top_level_transfers_unordered(
provider: &RootProvider<SimpleRequest>,
from_block: u64,
to_block: u64,
blocks: RangeInclusive<u64>,
erc20: Address,
to: Address,
) -> Result<TopLevelTransfers, RpcError<TransportErrorKind>> {
let mut logs = {
// Get all transfers within these blocks
let logs = provider.get_logs(&Self::transfer_filter(from_block, to_block, erc20, to)).await?;
let logs = provider.get_logs(&Self::transfer_filter(blocks, erc20, to)).await?;
// The logs, indexed by their transactions
let mut transaction_logs = HashMap::new();

View File

@@ -27,14 +27,17 @@ interface IRouterWithoutCollisions {
/// @notice Emitted when a batch of `OutInstruction`s occurs
/// @param nonce The nonce consumed to execute this batch of transactions
/// @param messageHash The hash of the message signed for the executed batch
/// @param resultsLength The length of the results bitvec (represented as bytes)
/**
* @param results The result of each `OutInstruction` executed. This is a bitmask with true
* representing success and false representing failure. The high bit (1 << 7) in the first byte
* is used for the first `OutInstruction`, before the next bit, and so on, before the next byte.
* An `OutInstruction` is considered as having succeeded if the call transferring ETH doesn't
* fail, the ERC20 transfer doesn't fail, and any executed code doesn't revert.
* @param results The result of each `OutInstruction` executed. This is a bitvec with true
* representing success and false representing failure. The low bit in the first byte is used
* for the first `OutInstruction`, before the next bit, and so on, before the next byte. An
* `OutInstruction` is considered as having succeeded if the call transferring ETH doesn't fail,
* the ERC20 transfer doesn't fail, and any executed code doesn't revert.
*/
event Batch(uint256 indexed nonce, bytes32 indexed messageHash, bytes results);
event Batch(
uint256 indexed nonce, bytes32 indexed messageHash, uint256 resultsLength, bytes results
);
/// @notice Emitted when `escapeHatch` is invoked
/// @param escapeTo The address to escape to

View File

@@ -376,14 +376,13 @@ contract Router is IRouterWithoutCollisions {
*/
function transferOut(address to, address coin, uint256 amount) private returns (bool success) {
if (coin == address(0)) {
// Enough gas to service the transfer and a minimal amount of logic
uint256 _gas = 5_000;
// This uses assembly to prevent return bombs
// slither-disable-next-line assembly
assembly {
success :=
call(
_gas,
// explicit gas
0,
to,
amount,
// calldata
@@ -512,7 +511,7 @@ contract Router is IRouterWithoutCollisions {
}
if (success) {
results[i / 8] |= bytes1(uint8(1 << (7 - (i % 8))));
results[i / 8] |= bytes1(uint8(1 << (i % 8)));
}
}
@@ -521,7 +520,7 @@ contract Router is IRouterWithoutCollisions {
This is an effect after interactions yet we have a reentrancy guard making this safe.
*/
emit Batch(nonceUsed, message, results);
emit Batch(nonceUsed, message, outs.length, results);
// Transfer the fee to the relayer
transferOut(msg.sender, coin, fee);

View File

@@ -2,6 +2,7 @@
#![doc = include_str!("../README.md")]
#![deny(missing_docs)]
use core::ops::RangeInclusive;
use std::{
sync::Arc,
collections::{HashSet, HashMap},
@@ -67,6 +68,14 @@ use abi::{
#[cfg(test)]
mod tests;
// As per Dencun, used for estimating gas for determining relayer fees
const NON_ZERO_BYTE_GAS_COST: u64 = 16;
const MEMORY_EXPANSION_COST: u64 = 3; // Does not model the quadratic cost
const COLD_COST: u64 = 2_600;
const WARM_COST: u64 = 100;
const POSITIVE_VALUE_COST: u64 = 9_000;
const EMPTY_ACCOUNT_COST: u64 = 25_000;
impl From<&Signature> for abi::Signature {
fn from(signature: &Signature) -> Self {
Self {
@@ -133,35 +142,33 @@ pub struct InInstruction {
pub data: Vec<u8>,
}
impl From<&(SeraiAddress, U256)> for abi::OutInstruction {
fn from((address, amount): &(SeraiAddress, U256)) -> Self {
#[allow(non_snake_case)]
let (destinationType, destination) = match address {
SeraiAddress::Address(address) => {
// Per the documentation, `DestinationType::Address`'s value is an ABI-encoded address
(abi::DestinationType::Address, (Address::from(address)).abi_encode())
}
SeraiAddress::Contract(contract) => (
abi::DestinationType::Code,
(abi::CodeDestination {
gasLimit: contract.gas_limit(),
code: contract.code().to_vec().into(),
})
.abi_encode(),
),
};
abi::OutInstruction { destinationType, destination: destination.into(), amount: *amount }
}
}
/// A list of `OutInstruction`s.
#[derive(Clone)]
pub struct OutInstructions(Vec<abi::OutInstruction>);
impl From<&[(SeraiAddress, U256)]> for OutInstructions {
fn from(outs: &[(SeraiAddress, U256)]) -> Self {
Self(
outs
.iter()
.map(|(address, amount)| {
#[allow(non_snake_case)]
let (destinationType, destination) = match address {
SeraiAddress::Address(address) => {
// Per the documentation, `DestinationType::Address`'s value is an ABI-encoded
// address
(abi::DestinationType::Address, (Address::from(address)).abi_encode())
}
SeraiAddress::Contract(contract) => (
abi::DestinationType::Code,
(abi::CodeDestination {
gasLimit: contract.gas_limit(),
code: contract.code().to_vec().into(),
})
.abi_encode(),
),
};
abi::OutInstruction { destinationType, destination: destination.into(), amount: *amount }
})
.collect(),
)
Self(outs.iter().map(Into::into).collect())
}
}
@@ -188,6 +195,8 @@ pub enum Executed {
nonce: u64,
/// The hash of the signed message for the Batch executed.
message_hash: [u8; 32],
/// The results of the `OutInstruction`s executed.
results: Vec<bool>,
},
/// The escape hatch was set.
EscapeHatch {
@@ -237,12 +246,15 @@ pub struct Router {
address: Address,
}
impl Router {
// Gas allocated for ERC20 calls
const GAS_FOR_ERC20_CALL: u64 = 100_000;
/*
The gas limits to use for transactions.
These are expected to be constant as a distributed group signs the transactions invoking these
calls. Having the gas be constant prevents needing to run a protocol to determine what gas to
use.
These are expected to be constant as a distributed group may sign the transactions invoking
these calls. Having the gas be constant prevents needing to run a protocol to determine what
gas to use.
These gas limits may break if/when gas opcodes undergo repricing. In that case, this library is
expected to be modified with these made parameters. The caller would then be expected to pass
@@ -250,9 +262,18 @@ impl Router {
*/
const CONFIRM_NEXT_SERAI_KEY_GAS: u64 = 57_736;
const UPDATE_SERAI_KEY_GAS: u64 = 60_045;
const EXECUTE_BASE_GAS: u64 = 48_000;
const EXECUTE_BASE_GAS: u64 = 51_131;
const ESCAPE_HATCH_GAS: u64 = 61_238;
/*
The percentage to actually use as the gas limit, in case any opcodes are repriced or errors
occurred.
Per prior commentary, this is just intended to be best-effort. If this is unnecessary, the gas
will be unspent. If this becomes necessary, it avoids needing an update.
*/
const GAS_REPRICING_BUFFER: u64 = 120;
fn code() -> Vec<u8> {
const BYTECODE: &[u8] = {
const BYTECODE_HEX: &[u8] =
@@ -324,7 +345,7 @@ impl Router {
TxLegacy {
to: TxKind::Call(self.address),
input: abi::confirmNextSeraiKeyCall::new((abi::Signature::from(sig),)).abi_encode().into(),
gas_limit: Self::CONFIRM_NEXT_SERAI_KEY_GAS * 120 / 100,
gas_limit: Self::CONFIRM_NEXT_SERAI_KEY_GAS * Self::GAS_REPRICING_BUFFER / 100,
..Default::default()
}
}
@@ -350,7 +371,7 @@ impl Router {
))
.abi_encode()
.into(),
gas_limit: Self::UPDATE_SERAI_KEY_GAS * 120 / 100,
gas_limit: Self::UPDATE_SERAI_KEY_GAS * Self::GAS_REPRICING_BUFFER / 100,
..Default::default()
}
}
@@ -403,18 +424,103 @@ impl Router {
.abi_encode()
}
/// The estimated gas cost for this OutInstruction.
///
/// This is not guaranteed to be correct or even sufficient. It is a hint and a hint alone used
/// for determining relayer fees.
fn execute_out_instruction_gas_estimate_internal(
coin: Coin,
instruction: &abi::OutInstruction,
) -> u64 {
// The assigned cost for performing an additional iteration of the loop
const ITERATION_COST: u64 = 5_000;
// The additional cost for a `DestinationType.Code`, as an additional buffer for its complexity
const CODE_COST: u64 = 10_000;
let size = u64::try_from(instruction.abi_encoded_size()).unwrap();
let calldata_memory_cost =
(NON_ZERO_BYTE_GAS_COST * size) + (MEMORY_EXPANSION_COST * size.div_ceil(32));
ITERATION_COST +
(match coin {
Coin::Ether => match instruction.destinationType {
// We assume we're tranferring a positive value to a cold, empty account
abi::DestinationType::Address => {
calldata_memory_cost + COLD_COST + POSITIVE_VALUE_COST + EMPTY_ACCOUNT_COST
}
abi::DestinationType::Code => {
// OutInstructions can't be encoded/decoded and doesn't have pub internals, enabling it
// to be correct by construction
let code = abi::CodeDestination::abi_decode(&instruction.destination, true).unwrap();
// This performs a call to self with the value, incurring the positive-value cost before
// CREATE's
calldata_memory_cost +
CODE_COST +
(WARM_COST + POSITIVE_VALUE_COST + u64::from(code.gasLimit))
}
abi::DestinationType::__Invalid => unreachable!(),
},
Coin::Erc20(_) => {
// The ERC20 is warmed by the fee payment to the relayer
let erc20_call_gas = WARM_COST + Self::GAS_FOR_ERC20_CALL;
match instruction.destinationType {
abi::DestinationType::Address => calldata_memory_cost + erc20_call_gas,
abi::DestinationType::Code => {
let code = abi::CodeDestination::abi_decode(&instruction.destination, true).unwrap();
calldata_memory_cost +
CODE_COST +
erc20_call_gas +
// Call to self to deploy the contract
(WARM_COST + u64::from(code.gasLimit))
}
abi::DestinationType::__Invalid => unreachable!(),
}
}
})
}
/// The estimated gas cost for this OutInstruction.
///
/// This is not guaranteed to be correct or even sufficient. It is a hint and a hint alone used
/// for determining relayer fees.
pub fn execute_out_instruction_gas_estimate(coin: Coin, address: SeraiAddress) -> u64 {
Self::execute_out_instruction_gas_estimate_internal(
coin,
&abi::OutInstruction::from(&(address, U256::ZERO)),
)
}
/// The estimated gas cost for this batch.
///
/// This is not guaranteed to be correct or even sufficient. It is a hint and a hint alone used
/// for determining relayer fees.
pub fn execute_gas_estimate(coin: Coin, outs: &OutInstructions) -> u64 {
Self::EXECUTE_BASE_GAS +
(match coin {
// This is warm as it's the message sender who is called with the fee payment
Coin::Ether => WARM_COST + POSITIVE_VALUE_COST,
// This is cold as we say the fee payment is the one warming the ERC20
Coin::Erc20(_) => COLD_COST + Self::GAS_FOR_ERC20_CALL,
}) +
outs
.0
.iter()
.map(|out| Self::execute_out_instruction_gas_estimate_internal(coin, out))
.sum::<u64>()
}
/// Construct a transaction to execute a batch of `OutInstruction`s.
///
/// The gas limit and gas price are not set and are left to the caller.
/// The gas limit is set to an estimate which may or may not be sufficient. The caller is
/// expected to set a correct gas limit. The gas price is not set and is left to the caller.
pub fn execute(&self, coin: Coin, fee: U256, outs: OutInstructions, sig: &Signature) -> TxLegacy {
// TODO
let gas_limit = Self::EXECUTE_BASE_GAS + outs.0.iter().map(|_| 200_000 + 10_000).sum::<u64>();
let gas = Self::execute_gas_estimate(coin, &outs);
TxLegacy {
to: TxKind::Call(self.address),
input: abi::executeCall::new((abi::Signature::from(sig), Address::from(coin), fee, outs.0))
.abi_encode()
.into(),
gas_limit: gas_limit * 120 / 100,
gas_limit: gas * Self::GAS_REPRICING_BUFFER / 100,
..Default::default()
}
}
@@ -435,7 +541,7 @@ impl Router {
TxLegacy {
to: TxKind::Call(self.address),
input: abi::escapeHatchCall::new((abi::Signature::from(sig), escape_to)).abi_encode().into(),
gas_limit: Self::ESCAPE_HATCH_GAS * 120 / 100,
gas_limit: Self::ESCAPE_HATCH_GAS * Self::GAS_REPRICING_BUFFER / 100,
..Default::default()
}
}
@@ -459,13 +565,13 @@ impl Router {
/// This is not guaranteed to return them in any order.
pub async fn in_instructions_unordered(
&self,
from_block: u64,
to_block: u64,
blocks: RangeInclusive<u64>,
allowed_erc20s: &HashSet<Address>,
) -> Result<Vec<InInstruction>, RpcError<TransportErrorKind>> {
// The InInstruction events for this block
let in_instruction_logs = {
let filter = Filter::new().from_block(from_block).to_block(to_block).address(self.address);
// https://github.com/rust-lang/rust/issues/27186
let filter = Filter::new().select(blocks.clone()).address(self.address);
let filter = filter.event_signature(InInstructionEvent::SIGNATURE_HASH);
self.provider.get_logs(&filter).await?
};
@@ -478,18 +584,15 @@ impl Router {
let erc20_transfer_logs = {
let mut transfers = FuturesUnordered::new();
for erc20 in allowed_erc20s {
transfers.push(async move {
(
erc20,
Erc20::top_level_transfers_unordered(
&self.provider,
from_block,
to_block,
*erc20,
self.address,
)
.await,
)
transfers.push({
// https://github.com/rust-lang/rust/issues/27186
let blocks: RangeInclusive<u64> = blocks.clone();
async move {
let transfers =
Erc20::top_level_transfers_unordered(&self.provider, blocks, *erc20, self.address)
.await;
(erc20, transfers)
}
});
}
@@ -626,8 +729,7 @@ impl Router {
/// Fetch the executed actions for the specified range of blocks.
pub async fn executed(
&self,
from_block: u64,
to_block: u64,
blocks: RangeInclusive<u64>,
) -> Result<Vec<Executed>, RpcError<TransportErrorKind>> {
fn decode<E: SolEvent>(log: &Log) -> Result<E, RpcError<TransportErrorKind>> {
Ok(
@@ -643,7 +745,7 @@ impl Router {
)
}
let filter = Filter::new().from_block(from_block).to_block(to_block).address(self.address);
let filter = Filter::new().select(blocks).address(self.address);
let mut logs = self.provider.get_logs(&filter).await?;
logs.sort_by_key(|log| (log.block_number, log.log_index));
@@ -682,6 +784,24 @@ impl Router {
TransportErrorKind::Custom(format!("failed to convert nonce to u64: {e:?}").into())
})?,
message_hash: event.messageHash.into(),
results: {
let results_len = usize::try_from(event.resultsLength).map_err(|e| {
TransportErrorKind::Custom(
format!("failed to convert resultsLength to usize: {e:?}").into(),
)
})?;
if results_len.div_ceil(8) != event.results.len() {
Err(TransportErrorKind::Custom(
"resultsLength didn't align with results length".to_string().into(),
))?;
}
let mut results = Vec::with_capacity(results_len);
for b in 0 .. results_len {
let byte = event.results[b / 8];
results.push(((byte >> (b % 8)) & 1) == 1);
}
results
},
});
}
Some(&EscapeHatchEvent::SIGNATURE_HASH) => {
@@ -707,10 +827,9 @@ impl Router {
/// Fetch the `Escape`s from the smart contract through the escape hatch.
pub async fn escapes(
&self,
from_block: u64,
to_block: u64,
blocks: RangeInclusive<u64>,
) -> Result<Vec<Escape>, RpcError<TransportErrorKind>> {
let filter = Filter::new().from_block(from_block).to_block(to_block).address(self.address);
let filter = Filter::new().select(blocks).address(self.address);
let mut logs =
self.provider.get_logs(&filter.event_signature(EscapedEvent::SIGNATURE_HASH)).await?;
logs.sort_by_key(|log| (log.block_number, log.log_index));

View File

@@ -22,8 +22,10 @@ pub struct Erc20(Address);
impl Erc20 {
pub(crate) async fn deploy(test: &Test) -> Self {
const BYTECODE: &[u8] = {
const BYTECODE_HEX: &[u8] =
include_bytes!(concat!(env!("OUT_DIR"), "/serai-processor-ethereum-router/TestERC20.bin"));
const BYTECODE_HEX: &[u8] = include_bytes!(concat!(
env!("OUT_DIR"),
"/serai-processor-ethereum-router/tests/TestERC20.bin"
));
const BYTECODE: [u8; BYTECODE_HEX.len() / 2] =
match hex::const_decode_to_array::<{ BYTECODE_HEX.len() / 2 }>(BYTECODE_HEX) {
Ok(bytecode) => bytecode,

View File

@@ -144,7 +144,7 @@ impl Test {
// Confirm nonce 0 was used as such
{
let block = receipt.block_number.unwrap();
let executed = router.executed(block, block).await.unwrap();
let executed = router.executed(block ..= block).await.unwrap();
assert_eq!(executed.len(), 1);
assert_eq!(executed[0], Executed::NextSeraiKeySet { nonce: 0, key: public_key.eth_repr() });
}
@@ -191,7 +191,7 @@ impl Test {
{
let block = receipt.block_number.unwrap();
let executed = self.router.executed(block, block).await.unwrap();
let executed = self.router.executed(block ..= block).await.unwrap();
assert_eq!(executed.len(), 1);
assert_eq!(
executed[0],
@@ -236,7 +236,7 @@ impl Test {
{
let block = receipt.block_number.unwrap();
let executed = self.router.executed(block, block).await.unwrap();
let executed = self.router.executed(block ..= block).await.unwrap();
assert_eq!(executed.len(), 1);
assert_eq!(
executed[0],
@@ -283,15 +283,14 @@ impl Test {
if matches!(coin, Coin::Erc20(_)) {
// If we don't whitelist this token, we shouldn't be yielded an InInstruction
let in_instructions =
self.router.in_instructions_unordered(block, block, &HashSet::new()).await.unwrap();
self.router.in_instructions_unordered(block ..= block, &HashSet::new()).await.unwrap();
assert!(in_instructions.is_empty());
}
let in_instructions = self
.router
.in_instructions_unordered(
block,
block,
block ..= block,
&if let Coin::Erc20(token) = coin { HashSet::from([token]) } else { HashSet::new() },
)
.await
@@ -323,7 +322,7 @@ impl Test {
coin: Coin,
fee: U256,
out_instructions: &[(SeraiEthereumAddress, U256)],
) -> TxLegacy {
) -> ([u8; 32], TxLegacy) {
let out_instructions = OutInstructions::from(out_instructions);
let msg = Router::execute_message(
self.chain_id,
@@ -332,8 +331,47 @@ impl Test {
fee,
out_instructions.clone(),
);
let msg_hash = ethereum_primitives::keccak256(&msg);
let sig = sign(self.state.key.unwrap(), &msg);
self.router.execute(coin, fee, out_instructions, &sig)
let mut tx = self.router.execute(coin, fee, out_instructions, &sig);
// Restore the original estimate as the gas limit to ensure it's sufficient, at least in our
// test cases
tx.gas_limit = (tx.gas_limit * 100) / Router::GAS_REPRICING_BUFFER;
(msg_hash, tx)
}
async fn execute(
&mut self,
coin: Coin,
fee: U256,
out_instructions: &[(SeraiEthereumAddress, U256)],
results: Vec<bool>,
) -> u64 {
let (message_hash, mut tx) = self.execute_tx(coin, fee, out_instructions);
tx.gas_price = 100_000_000_000;
let tx = ethereum_primitives::deterministically_sign(tx);
let receipt = ethereum_test_primitives::publish_tx(&self.provider, tx.clone()).await;
assert!(receipt.status());
// We don't check the gas for `execute` as it's infeasible. Due to our use of account
// abstraction, it isn't a critical if we do ever under-estimate, solely an unprofitable relay
{
let block = receipt.block_number.unwrap();
let executed = self.router.executed(block ..= block).await.unwrap();
assert_eq!(executed.len(), 1);
assert_eq!(
executed[0],
Executed::Batch { nonce: self.state.next_nonce, message_hash, results }
);
}
self.state.next_nonce += 1;
self.verify_state().await;
// We do return the gas used in case a caller can benefit from it
CalldataAgnosticGas::calculate(tx.tx(), receipt.gas_used)
}
fn escape_hatch_tx(&self, escape_to: Address) -> TxLegacy {
@@ -359,7 +397,7 @@ impl Test {
{
let block = receipt.block_number.unwrap();
let executed = self.router.executed(block, block).await.unwrap();
let executed = self.router.executed(block ..= block).await.unwrap();
assert_eq!(executed.len(), 1);
assert_eq!(executed[0], Executed::EscapeHatch { nonce: self.state.next_nonce, escape_to });
}
@@ -403,7 +441,7 @@ async fn test_no_serai_key() {
IRouterErrors::SeraiKeyWasNone(IRouter::SeraiKeyWasNone {})
));
assert!(matches!(
test.call_and_decode_err(test.execute_tx(Coin::Ether, U256::from(0), &[])).await,
test.call_and_decode_err(test.execute_tx(Coin::Ether, U256::from(0), &[]).1).await,
IRouterErrors::SeraiKeyWasNone(IRouter::SeraiKeyWasNone {})
));
assert!(matches!(
@@ -556,7 +594,7 @@ async fn test_erc20_router_in_instruction() {
let tx = TxLegacy {
chain_id: None,
nonce: 0,
gas_price: 100_000_000_000u128,
gas_price: 100_000_000_000,
gas_limit: 1_000_000,
to: test.router.address().into(),
value: U256::ZERO,
@@ -593,7 +631,7 @@ async fn test_erc20_top_level_transfer_in_instruction() {
let shorthand = Test::in_instruction();
let mut tx = test.router.in_instruction(coin, amount, &shorthand);
tx.gas_price = 100_000_000_000u128;
tx.gas_price = 100_000_000_000;
tx.gas_limit = 1_000_000;
let tx = ethereum_primitives::deterministically_sign(tx);
@@ -601,6 +639,21 @@ async fn test_erc20_top_level_transfer_in_instruction() {
test.publish_in_instruction_tx(tx, coin, amount, &shorthand).await;
}
#[tokio::test]
async fn test_empty_execute() {
let mut test = Test::new().await;
test.confirm_next_serai_key().await;
let () =
test.provider.raw_request("anvil_setBalance".into(), (test.router.address(), 1)).await.unwrap();
let gas_used = test.execute(Coin::Ether, U256::from(1), &[], vec![]).await;
// For the empty ETH case, we do compare this cost to the base cost
const CALL_GAS_STIPEND: u64 = 2_300;
// We don't use the call gas stipend here
const UNUSED_GAS: u64 = CALL_GAS_STIPEND;
assert_eq!(gas_used + UNUSED_GAS, Router::EXECUTE_BASE_GAS);
}
#[tokio::test]
async fn test_eth_address_out_instruction() {
todo!("TODO")
@@ -644,7 +697,7 @@ async fn test_escape_hatch() {
let tx = ethereum_primitives::deterministically_sign(TxLegacy {
to: Address([1; 20].into()).into(),
gas_limit: 21_000,
gas_price: 100_000_000_000u128,
gas_price: 100_000_000_000,
value: U256::from(1),
..Default::default()
});
@@ -680,7 +733,7 @@ async fn test_escape_hatch() {
IRouterErrors::EscapeHatchInvoked(IRouter::EscapeHatchInvoked {})
));
assert!(matches!(
test.call_and_decode_err(test.execute_tx(Coin::Ether, U256::from(0), &[])).await,
test.call_and_decode_err(test.execute_tx(Coin::Ether, U256::from(0), &[]).1).await,
IRouterErrors::EscapeHatchInvoked(IRouter::EscapeHatchInvoked {})
));
// We reject further attempts to update the escape hatch to prevent the last key from being
@@ -707,7 +760,7 @@ async fn test_escape_hatch() {
let block = receipt.block_number.unwrap();
assert_eq!(
test.router.escapes(block, block).await.unwrap(),
test.router.escapes(block ..= block).await.unwrap(),
vec![Escape { coin: Coin::Ether, amount: U256::from(1) }],
);
@@ -730,7 +783,7 @@ async fn test_escape_hatch() {
assert!(receipt.status());
let block = receipt.block_number.unwrap();
assert_eq!(test.router.escapes(block, block).await.unwrap(), vec![Escape { coin, amount }],);
assert_eq!(test.router.escapes(block ..= block).await.unwrap(), vec![Escape { coin, amount }],);
assert_eq!(erc20.balance_of(&test, test.router.address()).await, U256::from(0));
assert_eq!(erc20.balance_of(&test, test.state.escaped_to.unwrap()).await, amount);
}

View File

@@ -97,12 +97,19 @@ impl primitives::Block for FullEpoch {
> {
let mut res = HashMap::new();
for executed in &self.executed {
let Some(expected) =
let Some(mut expected) =
eventualities.active_eventualities.remove(executed.nonce().to_le_bytes().as_slice())
else {
// TODO: Why is this a continue, not an assert?
continue;
};
// If this is a Batch Eventuality, we didn't know how the OutInstructions would resolve at
// time of creation. Copy the results from the actual transaction into the expectation
if let (Executed::Batch { results, .. }, Executed::Batch { results: expected_results, .. }) =
(executed, &mut expected.0)
{
*expected_results = results.clone();
}
assert_eq!(
executed,
&expected.0,
@@ -119,7 +126,7 @@ impl primitives::Block for FullEpoch {
Accordingly, we have free reign as to what to set the transaction ID to.
We set the ID to the nonce as it's the most helpful value and unique barring someone
finding the premise for this as a hash.
finding the preimage for this as a hash.
*/
let mut tx_id = [0; 32];
tx_id[.. 8].copy_from_slice(executed.nonce().to_le_bytes().as_slice());

View File

@@ -52,7 +52,7 @@ impl Action {
Executed::NextSeraiKeySet { nonce: *nonce, key: key.eth_repr() }
}
Self::Batch { chain_id: _, nonce, .. } => {
Executed::Batch { nonce: *nonce, message_hash: keccak256(self.message()) }
Executed::Batch { nonce: *nonce, message_hash: keccak256(self.message()), results: vec![] }
}
})
}

View File

@@ -160,10 +160,10 @@ impl<D: Db> ScannerFeed for Rpc<D> {
block: Header,
) -> Result<(Vec<EthereumInInstruction>, Vec<Executed>), RpcError<TransportErrorKind>> {
let instructions = router
.in_instructions_unordered(block.number, block.number, &HashSet::from(TOKENS))
.in_instructions_unordered(block.number ..= block.number, &HashSet::from(TOKENS))
.await?;
let executed = router.executed(block.number, block.number).await?;
let executed = router.executed(block.number ..= block.number).await?;
Ok((instructions, executed))
}

View File

@@ -14,8 +14,8 @@ pub const ADDRESS_GAS_LIMIT: u32 = 950_000;
pub struct ContractDeployment {
/// The gas limit to use for this contract's execution.
///
/// THis MUST be less than the Serai gas limit. The cost of it will be deducted from the amount
/// transferred.
/// This MUST be less than the Serai gas limit. The cost of it, and the associated costs with
/// making this transaction, will be deducted from the amount transferred.
gas_limit: u32,
/// The initialization code of the contract to deploy.
///