7 Commits

Author SHA1 Message Date
Luke Parker
604a4b2442 Add execute_tx to fill in missing test cases reliant on it 2025-01-24 07:33:36 -05:00
Luke Parker
977dcad86d Test the Router rejects invalid signatures 2025-01-24 07:22:43 -05:00
Luke Parker
cefc542744 Test SeraiKeyWasNone 2025-01-24 06:58:54 -05:00
Luke Parker
164fe9a14f Test Router's InvalidSeraiKey error 2025-01-24 06:41:24 -05:00
Luke Parker
f948881eba Simplify async code in in_instructions_unordered
Outsources fetching the ERC20 events to top_level_transfers_unordered.
2025-01-24 05:43:04 -05:00
Luke Parker
201b675031 Test ERC20 InInstructions 2025-01-24 03:45:04 -05:00
Luke Parker
3d44766eff Add ERC20 InInstruction test 2025-01-24 03:23:58 -05:00
11 changed files with 630 additions and 390 deletions

View File

@@ -52,7 +52,7 @@ impl Deployer {
/// funded for this transaction to be submitted. This account has no known private key to anyone /// funded for this transaction to be submitted. This account has no known private key to anyone
/// so ETH sent can be neither misappropriated nor returned. /// so ETH sent can be neither misappropriated nor returned.
pub fn deployment_tx() -> Signed<TxLegacy> { pub fn deployment_tx() -> Signed<TxLegacy> {
let bytecode = Bytes::from(BYTECODE); let bytecode = Bytes::from_static(BYTECODE);
// Legacy transactions are used to ensure the widest possible degree of support across EVMs // Legacy transactions are used to ensure the widest possible degree of support across EVMs
let tx = TxLegacy { let tx = TxLegacy {

View File

@@ -2,8 +2,7 @@
#![doc = include_str!("../README.md")] #![doc = include_str!("../README.md")]
#![deny(missing_docs)] #![deny(missing_docs)]
use core::borrow::Borrow; use std::collections::HashMap;
use std::{sync::Arc, collections::HashMap};
use alloy_core::primitives::{Address, U256}; use alloy_core::primitives::{Address, U256};
@@ -57,20 +56,27 @@ pub struct TopLevelTransfer {
pub data: Vec<u8>, pub data: Vec<u8>,
} }
/// A view for an ERC20 contract. /// The result of `Erc20::top_level_transfers_unordered`.
#[derive(Clone, Debug)] pub struct TopLevelTransfers {
pub struct Erc20 { /// Every `Transfer` log of the contextual ERC20 to the contextual account, indexed by
provider: Arc<RootProvider<SimpleRequest>>, /// their transaction.
address: Address, ///
} /// The ERC20/account is labelled contextual as it isn't directly named here. Instead, they're
impl Erc20 { /// assumed contextual to how this was created.
/// Construct a new view of the specified ERC20 contract. pub logs: HashMap<[u8; 32], Vec<Log>>,
pub fn new(provider: Arc<RootProvider<SimpleRequest>>, address: Address) -> Self { /// All of the top-level transfers of the contextual ERC20 to the contextual account.
Self { provider, address } ///
/// The ERC20/account is labelled contextual as it isn't directly named here. Instead, they're
/// assumed contextual to how this was created.
pub transfers: Vec<TopLevelTransfer>,
} }
/// A view for an ERC20 contract.
#[derive(Clone, Debug)]
pub struct Erc20;
impl Erc20 {
/// The filter for transfer logs of the specified ERC20, to the specified recipient. /// The filter for transfer logs of the specified ERC20, to the specified recipient.
pub fn transfer_filter(from_block: u64, to_block: u64, erc20: Address, to: Address) -> Filter { 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); let filter = Filter::new().from_block(from_block).to_block(to_block);
filter.address(erc20).event_signature(Transfer::SIGNATURE_HASH).topic2(to.into_word()) filter.address(erc20).event_signature(Transfer::SIGNATURE_HASH).topic2(to.into_word())
} }
@@ -78,32 +84,35 @@ impl Erc20 {
/// Yield the top-level transfer for the specified transaction (if one exists). /// Yield the top-level transfer for the specified transaction (if one exists).
/// ///
/// The passed-in logs MUST be the logs for this transaction. The logs MUST be filtered to the /// The passed-in logs MUST be the logs for this transaction. The logs MUST be filtered to the
/// `Transfer` events of the intended token(s) and the intended `to` transferred to. These /// `Transfer` events of the intended token and the intended `to` transferred to. These
/// properties are completely unchecked and assumed to be the case. /// properties are completely unchecked and assumed to be the case.
/// ///
/// This does NOT yield THE top-level transfer. If multiple `Transfer` events have identical /// This does NOT yield THE top-level transfer. If multiple `Transfer` events have identical
/// structure to the top-level transfer call, the earliest `Transfer` event present in the logs /// structure to the top-level transfer call, the first `Transfer` event present in the logs is
/// is considered the top-level transfer. /// considered the top-level transfer.
// Yielding THE top-level transfer would require tracing the transaction execution and isn't // Yielding THE top-level transfer would require tracing the transaction execution and isn't
// worth the effort. // worth the effort.
pub async fn top_level_transfer( async fn top_level_transfer(
provider: impl AsRef<RootProvider<SimpleRequest>>, provider: &RootProvider<SimpleRequest>,
erc20: Address,
transaction_hash: [u8; 32], transaction_hash: [u8; 32],
mut transfer_logs: Vec<impl Borrow<Log>>, transfer_logs: &[Log],
) -> Result<Option<TopLevelTransfer>, RpcError<TransportErrorKind>> { ) -> Result<Option<TopLevelTransfer>, RpcError<TransportErrorKind>> {
// Fetch the transaction // Fetch the transaction
let transaction = let transaction =
provider.as_ref().get_transaction_by_hash(transaction_hash.into()).await?.ok_or_else( provider.get_transaction_by_hash(transaction_hash.into()).await?.ok_or_else(|| {
|| {
TransportErrorKind::Custom( TransportErrorKind::Custom(
"node didn't have the transaction which emitted a log it had".to_string().into(), "node didn't have the transaction which emitted a log it had".to_string().into(),
) )
}, })?;
)?;
// If this transaction didn't call this ERC20 at a top-level, return
if transaction.inner.to() != Some(erc20) {
return Ok(None);
}
// If this is a top-level call...
// Don't validate the encoding as this can't be re-encoded to an identical bytestring due // Don't validate the encoding as this can't be re-encoded to an identical bytestring due
// to the `InInstruction` appended after the call itself // to the additional data appended after the call itself
let Ok(call) = IERC20Calls::abi_decode(transaction.inner.input(), false) else { let Ok(call) = IERC20Calls::abi_decode(transaction.inner.input(), false) else {
return Ok(None); return Ok(None);
}; };
@@ -116,21 +125,12 @@ impl Erc20 {
_ => return Ok(None), _ => return Ok(None),
}; };
// Sort the logs to ensure the the earliest logs are first
transfer_logs.sort_by_key(|log| log.borrow().log_index);
// Find the log for this top-level transfer // Find the log for this top-level transfer
for log in transfer_logs { for log in transfer_logs {
// Check the log is for the called contract
// This handles the edge case where we're checking if transfers of token X were top-level and
// a transfer of token Y (with equivalent structure) was top-level
if Some(log.borrow().address()) != transaction.inner.to() {
continue;
}
// Since the caller is responsible for filtering these to `Transfer` events, we can assume // Since the caller is responsible for filtering these to `Transfer` events, we can assume
// this is a non-compliant ERC20 or an error with the logs fetched. We assume ERC20 // this is a non-compliant ERC20 or an error with the logs fetched. We assume ERC20
// compliance here, making this an RPC error // compliance here, making this an RPC error
let log = log.borrow().log_decode::<Transfer>().map_err(|_| { let log = log.log_decode::<Transfer>().map_err(|_| {
TransportErrorKind::Custom("log didn't include a valid transfer event".to_string().into()) TransportErrorKind::Custom("log didn't include a valid transfer event".to_string().into())
})?; })?;
@@ -158,8 +158,8 @@ impl Erc20 {
) => Vec::from(inInstruction), ) => Vec::from(inInstruction),
} }
} else { } else {
// We don't error here so this transfer is propagated up the stack, even without the // If there was no additional data appended, use an empty Vec (which has no data)
// InInstruction. In practice, Serai should acknowledge this and return it to the sender // This has a slight information loss in that it's None -> Some(vec![]), but it's fine
vec![] vec![]
}; };
@@ -177,25 +177,24 @@ impl Erc20 {
/// Fetch all top-level transfers to the specified address for this token. /// Fetch all top-level transfers to the specified address for this token.
/// ///
/// The result of this function is unordered. /// The `transfers` in the result are unordered. The `logs` are sorted by index.
pub async fn top_level_transfers_unordered( pub async fn top_level_transfers_unordered(
&self, provider: &RootProvider<SimpleRequest>,
from_block: u64, from_block: u64,
to_block: u64, to_block: u64,
erc20: Address,
to: Address, to: Address,
) -> Result<Vec<TopLevelTransfer>, RpcError<TransportErrorKind>> { ) -> Result<TopLevelTransfers, RpcError<TransportErrorKind>> {
let mut logs = {
// Get all transfers within these blocks // Get all transfers within these blocks
let logs = self let logs = provider.get_logs(&Self::transfer_filter(from_block, to_block, erc20, to)).await?;
.provider
.get_logs(&Self::transfer_filter(from_block, to_block, self.address, to))
.await?;
// The logs, indexed by their transactions // The logs, indexed by their transactions
let mut transaction_logs = HashMap::new(); let mut transaction_logs = HashMap::new();
// Index the logs by their transactions // Index the logs by their transactions
for log in logs { for log in logs {
// Double check the address which emitted this log // Double check the address which emitted this log
if log.address() != self.address { if log.address() != erc20 {
Err(TransportErrorKind::Custom( Err(TransportErrorKind::Custom(
"node returned logs for a different address than requested".to_string().into(), "node returned logs for a different address than requested".to_string().into(),
))?; ))?;
@@ -223,23 +222,31 @@ impl Erc20 {
transaction_logs.entry(tx_id).or_insert_with(|| Vec::with_capacity(1)).push(log); transaction_logs.entry(tx_id).or_insert_with(|| Vec::with_capacity(1)).push(log);
} }
transaction_logs
};
let mut transfers = vec![];
{
// Use `FuturesUnordered` so these RPC calls run in parallel // Use `FuturesUnordered` so these RPC calls run in parallel
let mut futures = FuturesUnordered::new(); let mut futures = FuturesUnordered::new();
for (tx_id, transfer_logs) in transaction_logs { for (tx_id, transfer_logs) in &mut logs {
futures.push(Self::top_level_transfer(&self.provider, tx_id, transfer_logs)); // Sort the logs to ensure the the earliest logs are first
transfer_logs.sort_by_key(|log| log.log_index);
futures.push(Self::top_level_transfer(provider, erc20, *tx_id, transfer_logs));
} }
let mut top_level_transfers = vec![]; while let Some(transfer) = futures.next().await {
while let Some(top_level_transfer) = futures.next().await { match transfer {
match top_level_transfer {
// Top-level transfer // Top-level transfer
Ok(Some(top_level_transfer)) => top_level_transfers.push(top_level_transfer), Ok(Some(transfer)) => transfers.push(transfer),
// Not a top-level transfer // Not a top-level transfer
Ok(None) => continue, Ok(None) => continue,
// Failed to get this transaction's information so abort // Failed to get this transaction's information so abort
Err(e) => Err(e)?, Err(e) => Err(e)?,
} }
} }
Ok(top_level_transfers) }
Ok(TopLevelTransfers { logs, transfers })
} }
} }

View File

@@ -14,7 +14,7 @@ mod borsh;
pub use borsh::*; pub use borsh::*;
/// An index of a log within a block. /// An index of a log within a block.
#[derive(Clone, Copy, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, BorshSerialize, BorshDeserialize)]
#[borsh(crate = "::borsh")] #[borsh(crate = "::borsh")]
pub struct LogIndex { pub struct LogIndex {
/// The hash of the block which produced this log. /// The hash of the block which produced this log.

View File

@@ -41,6 +41,9 @@ fn main() {
"contracts/IRouter.sol", "contracts/IRouter.sol",
"contracts/Router.sol", "contracts/Router.sol",
], ],
&(artifacts_path + "/router.rs"), &(artifacts_path.clone() + "/router.rs"),
); );
// Build the test contracts
build_solidity_contracts::build(&[], "contracts/tests", &(artifacts_path + "/tests")).unwrap();
} }

View File

@@ -45,6 +45,8 @@ interface IRouterWithoutCollisions {
/// @param amount The amount which escaped /// @param amount The amount which escaped
event Escaped(address indexed coin, uint256 amount); event Escaped(address indexed coin, uint256 amount);
/// @notice The Serai key verifying the signature wasn't set
error SeraiKeyWasNone();
/// @notice The key for Serai was invalid /// @notice The key for Serai was invalid
/// @dev This is incomplete and not always guaranteed to be thrown upon an invalid key /// @dev This is incomplete and not always guaranteed to be thrown upon an invalid key
error InvalidSeraiKey(); error InvalidSeraiKey();

View File

@@ -137,7 +137,7 @@ contract Router is IRouterWithoutCollisions {
The Schnorr contract should already reject this public key yet it's best to be explicit. The Schnorr contract should already reject this public key yet it's best to be explicit.
*/ */
if (key == bytes32(0)) { if (key == bytes32(0)) {
revert InvalidSignature(); revert SeraiKeyWasNone();
} }
message = msg.data; message = msg.data;
@@ -266,7 +266,7 @@ contract Router is IRouterWithoutCollisions {
function inInstruction(address coin, uint256 amount, bytes memory instruction) external payable { function inInstruction(address coin, uint256 amount, bytes memory instruction) external payable {
// Check there is an active key // Check there is an active key
if (_seraiKey == bytes32(0)) { if (_seraiKey == bytes32(0)) {
revert InvalidSeraiKey(); revert SeraiKeyWasNone();
} }
// Don't allow further InInstructions once the escape hatch has been invoked // Don't allow further InInstructions once the escape hatch has been invoked

View File

@@ -17,17 +17,11 @@ contract TestERC20 {
return 18; return 18;
} }
function totalSupply() public pure returns (uint256) { uint256 public totalSupply;
return 1_000_000 * 10e18;
}
mapping(address => uint256) balances; mapping(address => uint256) balances;
mapping(address => mapping(address => uint256)) allowances; mapping(address => mapping(address => uint256)) allowances;
constructor() {
balances[msg.sender] = totalSupply();
}
function balanceOf(address owner) public view returns (uint256) { function balanceOf(address owner) public view returns (uint256) {
return balances[owner]; return balances[owner];
} }
@@ -35,6 +29,7 @@ contract TestERC20 {
function transfer(address to, uint256 value) public returns (bool) { function transfer(address to, uint256 value) public returns (bool) {
balances[msg.sender] -= value; balances[msg.sender] -= value;
balances[to] += value; balances[to] += value;
emit Transfer(msg.sender, to, value);
return true; return true;
} }
@@ -42,15 +37,28 @@ contract TestERC20 {
allowances[from][msg.sender] -= value; allowances[from][msg.sender] -= value;
balances[from] -= value; balances[from] -= value;
balances[to] += value; balances[to] += value;
emit Transfer(from, to, value);
return true; return true;
} }
function approve(address spender, uint256 value) public returns (bool) { function approve(address spender, uint256 value) public returns (bool) {
allowances[msg.sender][spender] = value; allowances[msg.sender][spender] = value;
emit Approval(msg.sender, spender, value);
return true; return true;
} }
function allowance(address owner, address spender) public view returns (uint256) { function allowance(address owner, address spender) public view returns (uint256) {
return allowances[owner][spender]; return allowances[owner][spender];
} }
function mint(address owner, uint256 value) external {
balances[owner] += value;
totalSupply += value;
emit Transfer(address(0), owner, value);
}
function magicApprove(address owner, address spender, uint256 value) external {
allowances[owner][spender] = value;
emit Approval(owner, spender, value);
}
} }

View File

@@ -11,10 +11,7 @@ use borsh::{BorshSerialize, BorshDeserialize};
use group::ff::PrimeField; use group::ff::PrimeField;
use alloy_core::primitives::{ use alloy_core::primitives::{hex, Address, U256, TxKind};
hex::{self, FromHex},
Address, U256, Bytes, TxKind,
};
use alloy_sol_types::{SolValue, SolConstructor, SolCall, SolEvent}; use alloy_sol_types::{SolValue, SolConstructor, SolCall, SolEvent};
use alloy_consensus::TxLegacy; use alloy_consensus::TxLegacy;
@@ -32,7 +29,7 @@ use serai_client::{
use ethereum_primitives::LogIndex; use ethereum_primitives::LogIndex;
use ethereum_schnorr::{PublicKey, Signature}; use ethereum_schnorr::{PublicKey, Signature};
use ethereum_deployer::Deployer; use ethereum_deployer::Deployer;
use erc20::{Transfer, Erc20}; use erc20::{Transfer, TopLevelTransfer, TopLevelTransfers, Erc20};
use futures_util::stream::{StreamExt, FuturesUnordered}; use futures_util::stream::{StreamExt, FuturesUnordered};
@@ -257,9 +254,18 @@ impl Router {
const ESCAPE_HATCH_GAS: u64 = 61_238; const ESCAPE_HATCH_GAS: u64 = 61_238;
fn code() -> Vec<u8> { fn code() -> Vec<u8> {
const BYTECODE: &[u8] = const BYTECODE: &[u8] = {
const BYTECODE_HEX: &[u8] =
include_bytes!(concat!(env!("OUT_DIR"), "/serai-processor-ethereum-router/Router.bin")); include_bytes!(concat!(env!("OUT_DIR"), "/serai-processor-ethereum-router/Router.bin"));
Bytes::from_hex(BYTECODE).expect("compiled-in Router bytecode wasn't valid hex").to_vec() const BYTECODE: [u8; BYTECODE_HEX.len() / 2] =
match hex::const_decode_to_array::<{ BYTECODE_HEX.len() / 2 }>(BYTECODE_HEX) {
Ok(bytecode) => bytecode,
Err(_) => panic!("Router.bin did not contain valid hex"),
};
&BYTECODE
};
BYTECODE.to_vec()
} }
fn init_code(key: &PublicKey) -> Vec<u8> { fn init_code(key: &PublicKey) -> Vec<u8> {
@@ -445,35 +451,66 @@ impl Router {
} }
} }
/// Fetch the `InInstruction`s emitted by the Router from this block. /// Fetch the `InInstruction`s for the Router for the specified inclusive range of blocks.
///
/// This includes all `InInstruction` events from the Router and all top-level transfers to the
/// Router.
/// ///
/// This is not guaranteed to return them in any order. /// This is not guaranteed to return them in any order.
pub async fn in_instructions_unordered( pub async fn in_instructions_unordered(
&self, &self,
from_block: u64, from_block: u64,
to_block: u64, to_block: u64,
allowed_tokens: &HashSet<Address>, allowed_erc20s: &HashSet<Address>,
) -> Result<Vec<InInstruction>, RpcError<TransportErrorKind>> { ) -> Result<Vec<InInstruction>, RpcError<TransportErrorKind>> {
// The InInstruction events for this block // The InInstruction events for this block
let logs = { let in_instruction_logs = {
let filter = Filter::new().from_block(from_block).to_block(to_block).address(self.address); let filter = Filter::new().from_block(from_block).to_block(to_block).address(self.address);
let filter = filter.event_signature(InInstructionEvent::SIGNATURE_HASH); let filter = filter.event_signature(InInstructionEvent::SIGNATURE_HASH);
self.provider.get_logs(&filter).await? self.provider.get_logs(&filter).await?
}; };
let mut in_instructions = Vec::with_capacity(logs.len()); // Define the Vec for the result now that we have the logs as a size hint
/* let mut in_instructions = Vec::with_capacity(in_instruction_logs.len());
We check that for all InInstructions for ERC20s emitted, a corresponding transfer occurred.
On this initial loop, we just queue the ERC20 InInstructions for later verification.
We don't do this for ETH as it'd require tracing the transaction, which is non-trivial. It // Handle the top-level transfers for this block
also isn't necessary as all of this is solely defense in depth. let mut justifying_erc20_transfer_logs = HashSet::new();
*/ let erc20_transfer_logs = {
let mut erc20s = HashSet::new(); let mut transfers = FuturesUnordered::new();
let mut erc20_transfer_logs = FuturesUnordered::new(); for erc20 in allowed_erc20s {
let mut erc20_transactions = HashSet::new(); transfers.push(async move {
let mut erc20_in_instructions = vec![]; (
for log in logs { erc20,
Erc20::top_level_transfers_unordered(
&self.provider,
from_block,
to_block,
*erc20,
self.address,
)
.await,
)
});
}
let mut logs = HashMap::with_capacity(allowed_erc20s.len());
while let Some((token, transfers)) = transfers.next().await {
let TopLevelTransfers { logs: token_logs, transfers } = transfers?;
logs.insert(token, token_logs);
// Map the top-level transfer to an InInstruction
for transfer in transfers {
let TopLevelTransfer { id, transaction_hash, from, amount, data } = transfer;
justifying_erc20_transfer_logs.insert(transfer.id);
let in_instruction =
InInstruction { id, transaction_hash, from, coin: Coin::Erc20(*token), amount, data };
in_instructions.push(in_instruction);
}
}
logs
};
// Now handle the InInstruction events
for log in in_instruction_logs {
// Double check the address which emitted this log // Double check the address which emitted this log
if log.address() != self.address { if log.address() != self.address {
Err(TransportErrorKind::Custom( Err(TransportErrorKind::Custom(
@@ -485,7 +522,8 @@ impl Router {
continue; continue;
} }
let id = LogIndex { let log_index = |log: &Log| -> Result<LogIndex, TransportErrorKind> {
Ok(LogIndex {
block_hash: log block_hash: log
.block_hash .block_hash
.ok_or_else(|| { .ok_or_else(|| {
@@ -495,8 +533,11 @@ impl Router {
index_within_block: log.log_index.ok_or_else(|| { index_within_block: log.log_index.ok_or_else(|| {
TransportErrorKind::Custom("log didn't have its index set".to_string().into()) TransportErrorKind::Custom("log didn't have its index set".to_string().into())
})?, })?,
})
}; };
let id = log_index(&log)?;
let transaction_hash = log.transaction_hash.ok_or_else(|| { let transaction_hash = log.transaction_hash.ok_or_else(|| {
TransportErrorKind::Custom("log didn't have its transaction hash set".to_string().into()) TransportErrorKind::Custom("log didn't have its transaction hash set".to_string().into())
})?; })?;
@@ -524,108 +565,28 @@ impl Router {
}; };
match coin { match coin {
Coin::Ether => in_instructions.push(in_instruction), Coin::Ether => {}
Coin::Erc20(token) => { Coin::Erc20(token) => {
if !allowed_tokens.contains(&token) { // Check this is an allowed token
if !allowed_erc20s.contains(&token) {
continue; continue;
} }
// Fetch the ERC20 transfer events necessary to verify this InInstruction has a matching
// transfer
if !erc20s.contains(&token) {
erc20s.insert(token);
erc20_transfer_logs.push(async move {
let filter = Erc20::transfer_filter(from_block, to_block, token, self.address);
self.provider.get_logs(&filter).await.map(|logs| (token, logs))
});
}
erc20_transactions.insert(transaction_hash);
erc20_in_instructions.push((transaction_hash, in_instruction))
}
}
}
// Collect the ERC20 transfer logs
let erc20_transfer_logs = {
let mut collected = HashMap::with_capacity(erc20s.len());
while let Some(token_and_logs) = erc20_transfer_logs.next().await {
let (token, logs) = token_and_logs?;
collected.insert(token, logs);
}
collected
};
/* /*
For each transaction, it may have a top-level ERC20 transfer. That top-level transfer won't We check that for all InInstructions for ERC20s emitted, a corresponding transfer
be the transfer caused by the call to `inInstruction`, so we shouldn't consider it occurred.
justification for this `InInstruction` event.
Fetch all top-level transfers here so we can ignore them. We don't do this for ETH as it'd require tracing the transaction, which is non-trivial.
It also isn't necessary as all of this is solely defense in depth.
*/ */
let mut erc20_top_level_transfers = FuturesUnordered::new();
let mut transaction_transfer_logs = HashMap::new();
for transaction in erc20_transactions {
// Filter to the logs for this specific transaction
let logs = erc20_transfer_logs
.values()
.flat_map(|logs_per_token| logs_per_token.iter())
.filter_map(|log| {
let log_transaction_hash = log.transaction_hash.ok_or_else(|| {
TransportErrorKind::Custom(
"log didn't have its transaction hash set".to_string().into(),
)
});
match log_transaction_hash {
Ok(log_transaction_hash) => {
if log_transaction_hash == transaction {
Some(Ok(log))
} else {
None
}
}
Err(e) => Some(Err(e)),
}
})
.collect::<Result<Vec<_>, _>>()?;
// Find the top-level transfer
erc20_top_level_transfers.push(Erc20::top_level_transfer(
&self.provider,
transaction,
logs.clone(),
));
// Keep the transaction-indexed logs for the actual justifying
transaction_transfer_logs.insert(transaction, logs);
}
/*
In order to prevent a single transfer from being used to justify multiple distinct
InInstructions, we insert the transfer's log index into this HashSet.
*/
let mut already_used_to_justify = HashSet::new();
// Collect the top-level transfers
while let Some(erc20_top_level_transfer) = erc20_top_level_transfers.next().await {
let erc20_top_level_transfer = erc20_top_level_transfer?;
// If this transaction had a top-level transfer...
if let Some(erc20_top_level_transfer) = erc20_top_level_transfer {
// Mark this log index as used so it isn't used again
already_used_to_justify.insert(erc20_top_level_transfer.id.index_within_block);
}
}
// Now, for each ERC20 InInstruction, find a justifying transfer log
for (transaction_hash, in_instruction) in erc20_in_instructions {
let mut justified = false; let mut justified = false;
for log in &transaction_transfer_logs[&transaction_hash] { // These logs are returned from `top_level_transfers_unordered` and we don't require any
let log_index = log.log_index.ok_or_else(|| { // ordering of them
TransportErrorKind::Custom( for log in erc20_transfer_logs[&token].get(&transaction_hash).unwrap_or(&vec![]) {
"log in transaction receipt didn't have its log index set".to_string().into(), let log_index = log_index(log)?;
)
})?;
// Ensure we didn't already use this transfer to check a distinct InInstruction event // Ensure we didn't already use this transfer to justify a distinct InInstruction
if already_used_to_justify.contains(&log_index) { if justifying_erc20_transfer_logs.contains(&log_index) {
continue; continue;
} }
@@ -643,7 +604,7 @@ impl Router {
(transfer.to == self.address) && (transfer.to == self.address) &&
(transfer.value == in_instruction.amount) (transfer.value == in_instruction.amount)
{ {
already_used_to_justify.insert(log_index); justifying_erc20_transfer_logs.insert(log_index);
justified = true; justified = true;
break; break;
} }
@@ -654,13 +615,15 @@ impl Router {
"ERC20 InInstruction with no matching transfer log".to_string().into(), "ERC20 InInstruction with no matching transfer log".to_string().into(),
))?; ))?;
} }
}
}
in_instructions.push(in_instruction); in_instructions.push(in_instruction);
} }
Ok(in_instructions) Ok(in_instructions)
} }
/// Fetch the executed actions from this block. /// Fetch the executed actions for the specified range of blocks.
pub async fn executed( pub async fn executed(
&self, &self,
from_block: u64, from_block: u64,

View File

@@ -0,0 +1,89 @@
use alloy_core::primitives::{hex, Address, U256, Bytes, TxKind};
use alloy_sol_types::{SolValue, SolCall};
use alloy_consensus::TxLegacy;
use alloy_rpc_types_eth::{TransactionInput, TransactionRequest};
use alloy_provider::Provider;
use crate::tests::Test;
#[rustfmt::skip]
#[expect(warnings)]
#[expect(needless_pass_by_value)]
#[expect(clippy::all)]
#[expect(clippy::ignored_unit_patterns)]
#[expect(clippy::redundant_closure_for_method_calls)]
mod abi {
alloy_sol_macro::sol!("contracts/tests/ERC20.sol");
}
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: [u8; BYTECODE_HEX.len() / 2] =
match hex::const_decode_to_array::<{ BYTECODE_HEX.len() / 2 }>(BYTECODE_HEX) {
Ok(bytecode) => bytecode,
Err(_) => panic!("TestERC20.bin did not contain valid hex"),
};
&BYTECODE
};
let tx = TxLegacy {
chain_id: None,
nonce: 0,
gas_price: 100_000_000_000u128,
gas_limit: 1_000_000,
to: TxKind::Create,
value: U256::ZERO,
input: Bytes::from_static(BYTECODE),
};
let tx = ethereum_primitives::deterministically_sign(tx);
let receipt = ethereum_test_primitives::publish_tx(&test.provider, tx).await;
Self(receipt.contract_address.unwrap())
}
pub(crate) fn address(&self) -> Address {
self.0
}
pub(crate) async fn approve(&self, test: &Test, owner: Address, spender: Address, amount: U256) {
let tx = TxLegacy {
chain_id: None,
nonce: 0,
gas_price: 100_000_000_000u128,
gas_limit: 1_000_000,
to: self.0.into(),
value: U256::ZERO,
input: abi::TestERC20::magicApproveCall::new((owner, spender, amount)).abi_encode().into(),
};
let tx = ethereum_primitives::deterministically_sign(tx);
let receipt = ethereum_test_primitives::publish_tx(&test.provider, tx).await;
assert!(receipt.status());
}
pub(crate) async fn mint(&self, test: &Test, account: Address, amount: U256) {
let tx = TxLegacy {
chain_id: None,
nonce: 0,
gas_price: 100_000_000_000u128,
gas_limit: 1_000_000,
to: self.0.into(),
value: U256::ZERO,
input: abi::TestERC20::mintCall::new((account, amount)).abi_encode().into(),
};
let tx = ethereum_primitives::deterministically_sign(tx);
let receipt = ethereum_test_primitives::publish_tx(&test.provider, tx).await;
assert!(receipt.status());
}
pub(crate) async fn balance_of(&self, test: &Test, account: Address) -> U256 {
let call = TransactionRequest::default().to(self.0).input(TransactionInput::new(
abi::TestERC20::balanceOfCall::new((account,)).abi_encode().into(),
));
U256::abi_decode(&test.provider.call(&call).await.unwrap(), true).unwrap()
}
}

View File

@@ -5,13 +5,12 @@ use rand_core::{RngCore, OsRng};
use group::ff::Field; use group::ff::Field;
use k256::{Scalar, ProjectivePoint}; use k256::{Scalar, ProjectivePoint};
use alloy_core::primitives::{Address, U256, TxKind}; use alloy_core::primitives::{Address, U256};
use alloy_sol_types::SolCall; use alloy_sol_types::{SolCall, SolEvent};
use alloy_consensus::TxLegacy; use alloy_consensus::{TxLegacy, Signed};
#[rustfmt::skip] use alloy_rpc_types_eth::{BlockNumberOrTag, TransactionInput, TransactionRequest};
use alloy_rpc_types_eth::{BlockNumberOrTag, TransactionInput, TransactionRequest, TransactionReceipt};
use alloy_simple_request_transport::SimpleRequest; use alloy_simple_request_transport::SimpleRequest;
use alloy_rpc_client::ClientBuilder; use alloy_rpc_client::ClientBuilder;
use alloy_provider::{Provider, RootProvider}; use alloy_provider::{Provider, RootProvider};
@@ -20,6 +19,7 @@ use alloy_node_bindings::{Anvil, AnvilInstance};
use scale::Encode; use scale::Encode;
use serai_client::{ use serai_client::{
networks::ethereum::Address as SeraiEthereumAddress,
primitives::SeraiAddress, primitives::SeraiAddress,
in_instructions::primitives::{ in_instructions::primitives::{
InInstruction as SeraiInInstruction, RefundableInInstruction, Shorthand, InInstruction as SeraiInInstruction, RefundableInInstruction, Shorthand,
@@ -38,6 +38,8 @@ use crate::{
}; };
mod constants; mod constants;
mod erc20;
use erc20::Erc20;
pub(crate) fn test_key() -> (Scalar, PublicKey) { pub(crate) fn test_key() -> (Scalar, PublicKey) {
loop { loop {
@@ -221,10 +223,16 @@ impl Test {
let tx = ethereum_primitives::deterministically_sign(tx); let tx = ethereum_primitives::deterministically_sign(tx);
let receipt = ethereum_test_primitives::publish_tx(&self.provider, tx.clone()).await; let receipt = ethereum_test_primitives::publish_tx(&self.provider, tx.clone()).await;
assert!(receipt.status()); assert!(receipt.status());
if self.state.next_key.is_none() {
assert_eq!( assert_eq!(
CalldataAgnosticGas::calculate(tx.tx(), receipt.gas_used), CalldataAgnosticGas::calculate(tx.tx(), receipt.gas_used),
Router::UPDATE_SERAI_KEY_GAS, Router::UPDATE_SERAI_KEY_GAS,
); );
} else {
assert!(
CalldataAgnosticGas::calculate(tx.tx(), receipt.gas_used) < Router::UPDATE_SERAI_KEY_GAS
);
}
{ {
let block = receipt.block_number.unwrap(); let block = receipt.block_number.unwrap();
@@ -241,13 +249,17 @@ impl Test {
self.verify_state().await; self.verify_state().await;
} }
fn in_instruction() -> Shorthand {
Shorthand::Raw(RefundableInInstruction {
origin: None,
instruction: SeraiInInstruction::Transfer(SeraiAddress([0xff; 32])),
})
}
fn eth_in_instruction_tx(&self) -> (Coin, U256, Shorthand, TxLegacy) { fn eth_in_instruction_tx(&self) -> (Coin, U256, Shorthand, TxLegacy) {
let coin = Coin::Ether; let coin = Coin::Ether;
let amount = U256::from(1); let amount = U256::from(1);
let shorthand = Shorthand::Raw(RefundableInInstruction { let shorthand = Self::in_instruction();
origin: None,
instruction: SeraiInInstruction::Transfer(SeraiAddress([0xff; 32])),
});
let mut tx = self.router.in_instruction(coin, amount, &shorthand); let mut tx = self.router.in_instruction(coin, amount, &shorthand);
tx.gas_limit = 1_000_000; tx.gas_limit = 1_000_000;
@@ -256,6 +268,74 @@ impl Test {
(coin, amount, shorthand, tx) (coin, amount, shorthand, tx)
} }
async fn publish_in_instruction_tx(
&self,
tx: Signed<TxLegacy>,
coin: Coin,
amount: U256,
shorthand: &Shorthand,
) {
let receipt = ethereum_test_primitives::publish_tx(&self.provider, tx.clone()).await;
assert!(receipt.status());
let block = receipt.block_number.unwrap();
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();
assert!(in_instructions.is_empty());
}
let in_instructions = self
.router
.in_instructions_unordered(
block,
block,
&if let Coin::Erc20(token) = coin { HashSet::from([token]) } else { HashSet::new() },
)
.await
.unwrap();
assert_eq!(in_instructions.len(), 1);
let in_instruction_log_index = receipt.inner.logs().iter().find_map(|log| {
(log.topics().first() == Some(&crate::InInstructionEvent::SIGNATURE_HASH))
.then(|| log.log_index.unwrap())
});
// If this isn't an InInstruction event, it'll be a top-level transfer event
let log_index = in_instruction_log_index.unwrap_or(0);
assert_eq!(
in_instructions[0],
InInstruction {
id: LogIndex { block_hash: *receipt.block_hash.unwrap(), index_within_block: log_index },
transaction_hash: **tx.hash(),
from: tx.recover_signer().unwrap(),
coin,
amount,
data: shorthand.encode(),
}
);
}
fn execute_tx(
&self,
coin: Coin,
fee: U256,
out_instructions: &[(SeraiEthereumAddress, U256)],
) -> TxLegacy {
let out_instructions = OutInstructions::from(out_instructions);
let msg = Router::execute_message(
self.chain_id,
self.state.next_nonce,
coin,
fee,
out_instructions.clone(),
);
let sig = sign(self.state.key.unwrap(), &msg);
self.router.execute(coin, fee, out_instructions, &sig)
}
fn escape_hatch_tx(&self, escape_to: Address) -> TxLegacy { fn escape_hatch_tx(&self, escape_to: Address) -> TxLegacy {
let msg = Router::escape_hatch_message(self.chain_id, self.state.next_nonce, escape_to); let msg = Router::escape_hatch_message(self.chain_id, self.state.next_nonce, escape_to);
let sig = sign(self.state.key.unwrap(), &msg); let sig = sign(self.state.key.unwrap(), &msg);
@@ -306,20 +386,139 @@ async fn test_constructor() {
#[tokio::test] #[tokio::test]
async fn test_confirm_next_serai_key() { async fn test_confirm_next_serai_key() {
let mut test = Test::new().await; let mut test = Test::new().await;
// TODO: Check all calls fail at this time, including inInstruction
test.confirm_next_serai_key().await; test.confirm_next_serai_key().await;
} }
#[tokio::test]
async fn test_no_serai_key() {
// Before we confirm a key, any operations requiring a signature shouldn't work
{
let mut test = Test::new().await;
// Corrupt the test's state so we can obtain signed TXs
test.state.key = Some(test_key());
assert!(matches!(
test.call_and_decode_err(test.update_serai_key_tx().1).await,
IRouterErrors::SeraiKeyWasNone(IRouter::SeraiKeyWasNone {})
));
assert!(matches!(
test.call_and_decode_err(test.execute_tx(Coin::Ether, U256::from(0), &[])).await,
IRouterErrors::SeraiKeyWasNone(IRouter::SeraiKeyWasNone {})
));
assert!(matches!(
test.call_and_decode_err(test.escape_hatch_tx(Address::ZERO)).await,
IRouterErrors::SeraiKeyWasNone(IRouter::SeraiKeyWasNone {})
));
}
// And if there's no key to confirm, any operations requiring a signature shouldn't work
{
let mut test = Test::new().await;
test.confirm_next_serai_key().await;
test.state.next_key = Some(test_key());
assert!(matches!(
test.call_and_decode_err(test.confirm_next_serai_key_tx()).await,
IRouterErrors::SeraiKeyWasNone(IRouter::SeraiKeyWasNone {})
));
}
}
#[tokio::test]
async fn test_invalid_signature() {
let mut test = Test::new().await;
{
let mut tx = test.confirm_next_serai_key_tx();
// Cut it down to the function signature
tx.input = tx.input.as_ref()[.. 4].to_vec().into();
assert!(matches!(
test.call_and_decode_err(tx).await,
IRouterErrors::InvalidSignature(IRouter::InvalidSignature {})
));
}
{
let mut tx = test.confirm_next_serai_key_tx();
// Mutate the signature
let mut input = Vec::<u8>::from(tx.input);
*input.last_mut().unwrap() = input.last().unwrap().wrapping_add(1);
tx.input = input.into();
assert!(matches!(
test.call_and_decode_err(tx).await,
IRouterErrors::InvalidSignature(IRouter::InvalidSignature {})
));
}
test.confirm_next_serai_key().await;
{
let mut tx = test.update_serai_key_tx().1;
// Mutate the message
let mut input = Vec::<u8>::from(tx.input);
*input.last_mut().unwrap() = input.last().unwrap().wrapping_add(1);
tx.input = input.into();
assert!(matches!(
test.call_and_decode_err(tx).await,
IRouterErrors::InvalidSignature(IRouter::InvalidSignature {})
));
}
}
#[tokio::test] #[tokio::test]
async fn test_update_serai_key() { async fn test_update_serai_key() {
let mut test = Test::new().await; let mut test = Test::new().await;
test.confirm_next_serai_key().await; test.confirm_next_serai_key().await;
test.update_serai_key().await; test.update_serai_key().await;
// We should be able to update while an update is pending as well (in case the new key never
// confirms)
test.update_serai_key().await;
// But we shouldn't be able to update the key to None
{
let msg = crate::abi::updateSeraiKeyCall::new((
crate::abi::Signature {
c: test.chain_id.into(),
s: U256::try_from(test.state.next_nonce).unwrap().into(),
},
[0; 32].into(),
))
.abi_encode();
let sig = sign(test.state.key.unwrap(), &msg);
assert!(matches!(
test
.call_and_decode_err(TxLegacy {
input: crate::abi::updateSeraiKeyCall::new((
crate::abi::Signature::from(&sig),
[0; 32].into(),
))
.abi_encode()
.into(),
..Default::default()
})
.await,
IRouterErrors::InvalidSeraiKey(IRouter::InvalidSeraiKey {})
));
}
// Once we update to a new key, we should, of course, be able to continue to rotate keys // Once we update to a new key, we should, of course, be able to continue to rotate keys
test.confirm_next_serai_key().await; test.confirm_next_serai_key().await;
} }
#[tokio::test]
async fn test_no_in_instruction_before_key() {
let test = Test::new().await;
// We shouldn't be able to publish `InInstruction`s before publishing a key
let (_coin, _amount, _shorthand, tx) = test.eth_in_instruction_tx();
assert!(matches!(
test.call_and_decode_err(tx).await,
IRouterErrors::SeraiKeyWasNone(IRouter::SeraiKeyWasNone {})
));
}
#[tokio::test] #[tokio::test]
async fn test_eth_in_instruction() { async fn test_eth_in_instruction() {
let mut test = Test::new().await; let mut test = Test::new().await;
@@ -338,32 +537,68 @@ async fn test_eth_in_instruction() {
} }
let tx = ethereum_primitives::deterministically_sign(tx); let tx = ethereum_primitives::deterministically_sign(tx);
let receipt = ethereum_test_primitives::publish_tx(&test.provider, tx.clone()).await; test.publish_in_instruction_tx(tx, coin, amount, &shorthand).await;
assert!(receipt.status());
let block = receipt.block_number.unwrap();
let in_instructions =
test.router.in_instructions_unordered(block, block, &HashSet::new()).await.unwrap();
assert_eq!(in_instructions.len(), 1);
assert_eq!(
in_instructions[0],
InInstruction {
id: LogIndex {
block_hash: *receipt.block_hash.unwrap(),
index_within_block: receipt.inner.logs()[0].log_index.unwrap(),
},
transaction_hash: **tx.hash(),
from: tx.recover_signer().unwrap(),
coin,
amount,
data: shorthand.encode(),
}
);
} }
#[tokio::test] #[tokio::test]
async fn test_erc20_in_instruction() { async fn test_erc20_router_in_instruction() {
todo!("TODO") let mut test = Test::new().await;
test.confirm_next_serai_key().await;
let erc20 = Erc20::deploy(&test).await;
let coin = Coin::Erc20(erc20.address());
let amount = U256::from(1);
let shorthand = Test::in_instruction();
// The provided `in_instruction` function will use a top-level transfer for ERC20 InInstructions,
// so we have to manually write this call
let tx = TxLegacy {
chain_id: None,
nonce: 0,
gas_price: 100_000_000_000u128,
gas_limit: 1_000_000,
to: test.router.address().into(),
value: U256::ZERO,
input: crate::abi::inInstructionCall::new((coin.into(), amount, shorthand.encode().into()))
.abi_encode()
.into(),
};
// If no `approve` was granted, this should fail
assert!(matches!(
test.call_and_decode_err(tx.clone()).await,
IRouterErrors::TransferFromFailed(IRouter::TransferFromFailed {})
));
let tx = ethereum_primitives::deterministically_sign(tx);
{
let signer = tx.recover_signer().unwrap();
erc20.mint(&test, signer, amount).await;
erc20.approve(&test, signer, test.router.address(), amount).await;
}
test.publish_in_instruction_tx(tx, coin, amount, &shorthand).await;
}
#[tokio::test]
async fn test_erc20_top_level_transfer_in_instruction() {
let mut test = Test::new().await;
test.confirm_next_serai_key().await;
let erc20 = Erc20::deploy(&test).await;
let coin = Coin::Erc20(erc20.address());
let amount = U256::from(1);
let shorthand = Test::in_instruction();
let mut tx = test.router.in_instruction(coin, amount, &shorthand);
tx.gas_price = 100_000_000_000u128;
tx.gas_limit = 1_000_000;
let tx = ethereum_primitives::deterministically_sign(tx);
erc20.mint(&test, tx.recover_signer().unwrap(), amount).await;
test.publish_in_instruction_tx(tx, coin, amount, &shorthand).await;
} }
#[tokio::test] #[tokio::test]
@@ -444,7 +679,10 @@ async fn test_escape_hatch() {
test.call_and_decode_err(test.eth_in_instruction_tx().3).await, test.call_and_decode_err(test.eth_in_instruction_tx().3).await,
IRouterErrors::EscapeHatchInvoked(IRouter::EscapeHatchInvoked {}) IRouterErrors::EscapeHatchInvoked(IRouter::EscapeHatchInvoked {})
)); ));
// TODO execute assert!(matches!(
test.call_and_decode_err(test.execute_tx(Coin::Ether, U256::from(0), &[])).await,
IRouterErrors::EscapeHatchInvoked(IRouter::EscapeHatchInvoked {})
));
// We reject further attempts to update the escape hatch to prevent the last key from being // We reject further attempts to update the escape hatch to prevent the last key from being
// able to switch from the honest escape hatch to siphoning via a malicious escape hatch (such // able to switch from the honest escape hatch to siphoning via a malicious escape hatch (such
// as after the validators represented unstake) // as after the validators represented unstake)
@@ -473,31 +711,36 @@ async fn test_escape_hatch() {
vec![Escape { coin: Coin::Ether, amount: U256::from(1) }], vec![Escape { coin: Coin::Ether, amount: U256::from(1) }],
); );
assert!(test.provider.get_balance(test.router.address()).await.unwrap() == U256::from(0)); assert_eq!(test.provider.get_balance(test.router.address()).await.unwrap(), U256::from(0));
assert!( assert_eq!(
test.provider.get_balance(test.state.escaped_to.unwrap()).await.unwrap() == U256::from(1) test.provider.get_balance(test.state.escaped_to.unwrap()).await.unwrap(),
U256::from(1)
); );
} }
// TODO ERC20 escape // ERC20
{
let erc20 = Erc20::deploy(&test).await;
let coin = Coin::Erc20(erc20.address());
let amount = U256::from(1);
erc20.mint(&test, test.router.address(), amount).await;
let tx = ethereum_primitives::deterministically_sign(test.escape_tx(coin));
let receipt = ethereum_test_primitives::publish_tx(&test.provider, tx.clone()).await;
assert!(receipt.status());
let block = receipt.block_number.unwrap();
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);
}
} }
/* /* TODO
event InInstruction(
address indexed from, address indexed coin, uint256 amount, bytes instruction
);
event Batch(uint256 indexed nonce, bytes32 indexed messageHash, bytes results); event Batch(uint256 indexed nonce, bytes32 indexed messageHash, bytes results);
error InvalidSeraiKey();
error InvalidSignature();
error AmountMismatchesMsgValue();
error TransferFromFailed();
error Reentered(); error Reentered();
error EscapeFailed(); error EscapeFailed();
function executeArbitraryCode(bytes memory code) external payable; function executeArbitraryCode(bytes memory code) external payable;
struct Signature {
bytes32 c;
bytes32 s;
}
enum DestinationType { enum DestinationType {
Address, Address,
Code Code
@@ -519,61 +762,6 @@ async fn test_escape_hatch() {
) external; ) external;
} }
#[tokio::test]
async fn test_eth_in_instruction() {
let (_anvil, provider, router, key) = setup_test().await;
confirm_next_serai_key(&provider, &router, 1, key).await;
let amount = U256::try_from(OsRng.next_u64()).unwrap();
let mut in_instruction = vec![0; usize::try_from(OsRng.next_u64() % 256).unwrap()];
OsRng.fill_bytes(&mut in_instruction);
let tx = TxLegacy {
chain_id: None,
nonce: 0,
// 100 gwei
gas_price: 100_000_000_000,
gas_limit: 1_000_000,
to: TxKind::Call(router.address()),
value: amount,
input: crate::_irouter_abi::inInstructionCall::new((
[0; 20].into(),
amount,
in_instruction.clone().into(),
))
.abi_encode()
.into(),
};
let tx = ethereum_primitives::deterministically_sign(tx);
let signer = tx.recover_signer().unwrap();
let receipt = ethereum_test_primitives::publish_tx(&provider, tx).await;
assert!(receipt.status());
assert_eq!(receipt.inner.logs().len(), 1);
let parsed_log =
receipt.inner.logs()[0].log_decode::<crate::InInstructionEvent>().unwrap().inner.data;
assert_eq!(parsed_log.from, signer);
assert_eq!(parsed_log.coin, Address::from([0; 20]));
assert_eq!(parsed_log.amount, amount);
assert_eq!(parsed_log.instruction.as_ref(), &in_instruction);
let parsed_in_instructions =
router.in_instructions(receipt.block_number.unwrap(), &HashSet::new()).await.unwrap();
assert_eq!(parsed_in_instructions.len(), 1);
assert_eq!(
parsed_in_instructions[0].id,
LogIndex {
block_hash: *receipt.block_hash.unwrap(),
index_within_block: receipt.inner.logs()[0].log_index.unwrap(),
},
);
assert_eq!(parsed_in_instructions[0].from, signer);
assert_eq!(parsed_in_instructions[0].coin, Coin::Ether);
assert_eq!(parsed_in_instructions[0].amount, amount);
assert_eq!(parsed_in_instructions[0].data, in_instruction);
}
async fn publish_outs( async fn publish_outs(
provider: &RootProvider<SimpleRequest>, provider: &RootProvider<SimpleRequest>,
router: &Router, router: &Router,

View File

@@ -16,9 +16,7 @@ use serai_db::Db;
use scanner::ScannerFeed; use scanner::ScannerFeed;
use ethereum_schnorr::PublicKey; use ethereum_schnorr::PublicKey;
use ethereum_erc20::{TopLevelTransfer, Erc20}; use ethereum_router::{InInstruction as EthereumInInstruction, Executed, Router};
#[rustfmt::skip]
use ethereum_router::{Coin as EthereumCoin, InInstruction as EthereumInInstruction, Executed, Router};
use crate::{ use crate::{
TOKENS, ETHER_DUST, DAI_DUST, InitialSeraiKey, TOKENS, ETHER_DUST, DAI_DUST, InitialSeraiKey,
@@ -158,31 +156,13 @@ impl<D: Db> ScannerFeed for Rpc<D> {
}; };
async fn sync_block( async fn sync_block(
provider: Arc<RootProvider<SimpleRequest>>,
router: Router, router: Router,
block: Header, block: Header,
) -> Result<(Vec<EthereumInInstruction>, Vec<Executed>), RpcError<TransportErrorKind>> { ) -> Result<(Vec<EthereumInInstruction>, Vec<Executed>), RpcError<TransportErrorKind>> {
let mut instructions = router let instructions = router
.in_instructions_unordered(block.number, block.number, &HashSet::from(TOKENS)) .in_instructions_unordered(block.number, block.number, &HashSet::from(TOKENS))
.await?; .await?;
for token in TOKENS {
for TopLevelTransfer { id, transaction_hash, from, amount, data } in
Erc20::new(provider.clone(), token)
.top_level_transfers_unordered(block.number, block.number, router.address())
.await?
{
instructions.push(EthereumInInstruction {
id,
transaction_hash,
from,
coin: EthereumCoin::Erc20(token),
amount,
data,
});
}
}
let executed = router.executed(block.number, block.number).await?; let executed = router.executed(block.number, block.number).await?;
Ok((instructions, executed)) Ok((instructions, executed))
@@ -214,7 +194,7 @@ impl<D: Db> ScannerFeed for Rpc<D> {
to_check = *to_check_block.parent_hash; to_check = *to_check_block.parent_hash;
// Spawn a task to sync this block // Spawn a task to sync this block
join_set.spawn(sync_block(self.provider.clone(), router.clone(), to_check_block)); join_set.spawn(sync_block(router.clone(), to_check_block));
} }
let mut instructions = vec![]; let mut instructions = vec![];