mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-13 06:29:25 +00:00
Compare commits
7 Commits
a63a86ba79
...
604a4b2442
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
604a4b2442 | ||
|
|
977dcad86d | ||
|
|
cefc542744 | ||
|
|
164fe9a14f | ||
|
|
f948881eba | ||
|
|
201b675031 | ||
|
|
3d44766eff |
@@ -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 {
|
||||||
|
|||||||
@@ -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>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The result of `Erc20::top_level_transfers_unordered`.
|
||||||
|
pub struct TopLevelTransfers {
|
||||||
|
/// Every `Transfer` log of the contextual ERC20 to the contextual account, indexed by
|
||||||
|
/// their transaction.
|
||||||
|
///
|
||||||
|
/// The ERC20/account is labelled contextual as it isn't directly named here. Instead, they're
|
||||||
|
/// assumed contextual to how this was created.
|
||||||
|
pub logs: HashMap<[u8; 32], Vec<Log>>,
|
||||||
|
/// All of the top-level transfers of the contextual ERC20 to the contextual account.
|
||||||
|
///
|
||||||
|
/// 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.
|
/// A view for an ERC20 contract.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Erc20 {
|
pub struct Erc20;
|
||||||
provider: Arc<RootProvider<SimpleRequest>>,
|
|
||||||
address: Address,
|
|
||||||
}
|
|
||||||
impl Erc20 {
|
impl Erc20 {
|
||||||
/// Construct a new view of the specified ERC20 contract.
|
|
||||||
pub fn new(provider: Arc<RootProvider<SimpleRequest>>, address: Address) -> Self {
|
|
||||||
Self { provider, address }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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,69 +177,76 @@ 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>> {
|
||||||
// Get all transfers within these blocks
|
let mut logs = {
|
||||||
let logs = self
|
// Get all transfers within these blocks
|
||||||
.provider
|
let logs = provider.get_logs(&Self::transfer_filter(from_block, to_block, erc20, to)).await?;
|
||||||
.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(),
|
||||||
))?;
|
))?;
|
||||||
}
|
}
|
||||||
// Double check the event signature for this log
|
// Double check the event signature for this log
|
||||||
if log.topics().first() != Some(&Transfer::SIGNATURE_HASH) {
|
if log.topics().first() != Some(&Transfer::SIGNATURE_HASH) {
|
||||||
Err(TransportErrorKind::Custom(
|
Err(TransportErrorKind::Custom(
|
||||||
"node returned a log for a different topic than filtered to".to_string().into(),
|
"node returned a log for a different topic than filtered to".to_string().into(),
|
||||||
))?;
|
))?;
|
||||||
}
|
}
|
||||||
// Double check the `to` topic
|
// Double check the `to` topic
|
||||||
if log.topics().get(2) != Some(&to.into_word()) {
|
if log.topics().get(2) != Some(&to.into_word()) {
|
||||||
Err(TransportErrorKind::Custom(
|
Err(TransportErrorKind::Custom(
|
||||||
"node returned a transfer for a different `to` than filtered to".to_string().into(),
|
"node returned a transfer for a different `to` than filtered to".to_string().into(),
|
||||||
))?;
|
))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let tx_id = log
|
||||||
|
.transaction_hash
|
||||||
|
.ok_or_else(|| {
|
||||||
|
TransportErrorKind::Custom("log didn't specify its transaction hash".to_string().into())
|
||||||
|
})?
|
||||||
|
.0;
|
||||||
|
|
||||||
|
transaction_logs.entry(tx_id).or_insert_with(|| Vec::with_capacity(1)).push(log);
|
||||||
}
|
}
|
||||||
|
|
||||||
let tx_id = log
|
transaction_logs
|
||||||
.transaction_hash
|
};
|
||||||
.ok_or_else(|| {
|
|
||||||
TransportErrorKind::Custom("log didn't specify its transaction hash".to_string().into())
|
|
||||||
})?
|
|
||||||
.0;
|
|
||||||
|
|
||||||
transaction_logs.entry(tx_id).or_insert_with(|| Vec::with_capacity(1)).push(log);
|
let mut transfers = vec![];
|
||||||
}
|
{
|
||||||
|
// Use `FuturesUnordered` so these RPC calls run in parallel
|
||||||
|
let mut futures = FuturesUnordered::new();
|
||||||
|
for (tx_id, transfer_logs) in &mut 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));
|
||||||
|
}
|
||||||
|
|
||||||
// Use `FuturesUnordered` so these RPC calls run in parallel
|
while let Some(transfer) = futures.next().await {
|
||||||
let mut futures = FuturesUnordered::new();
|
match transfer {
|
||||||
for (tx_id, transfer_logs) in transaction_logs {
|
// Top-level transfer
|
||||||
futures.push(Self::top_level_transfer(&self.provider, tx_id, transfer_logs));
|
Ok(Some(transfer)) => transfers.push(transfer),
|
||||||
}
|
// Not a top-level transfer
|
||||||
|
Ok(None) => continue,
|
||||||
let mut top_level_transfers = vec![];
|
// Failed to get this transaction's information so abort
|
||||||
while let Some(top_level_transfer) = futures.next().await {
|
Err(e) => Err(e)?,
|
||||||
match top_level_transfer {
|
}
|
||||||
// Top-level transfer
|
|
||||||
Ok(Some(top_level_transfer)) => top_level_transfers.push(top_level_transfer),
|
|
||||||
// Not a top-level transfer
|
|
||||||
Ok(None) => continue,
|
|
||||||
// Failed to get this transaction's information so abort
|
|
||||||
Err(e) => Err(e)?,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(top_level_transfers)
|
|
||||||
|
Ok(TopLevelTransfers { logs, transfers })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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] = {
|
||||||
include_bytes!(concat!(env!("OUT_DIR"), "/serai-processor-ethereum-router/Router.bin"));
|
const BYTECODE_HEX: &[u8] =
|
||||||
Bytes::from_hex(BYTECODE).expect("compiled-in Router bytecode wasn't valid hex").to_vec()
|
include_bytes!(concat!(env!("OUT_DIR"), "/serai-processor-ethereum-router/Router.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!("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,18 +522,22 @@ impl Router {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let id = LogIndex {
|
let log_index = |log: &Log| -> Result<LogIndex, TransportErrorKind> {
|
||||||
block_hash: log
|
Ok(LogIndex {
|
||||||
.block_hash
|
block_hash: log
|
||||||
.ok_or_else(|| {
|
.block_hash
|
||||||
TransportErrorKind::Custom("log didn't have its block hash set".to_string().into())
|
.ok_or_else(|| {
|
||||||
})?
|
TransportErrorKind::Custom("log didn't have its block hash set".to_string().into())
|
||||||
.into(),
|
})?
|
||||||
index_within_block: log.log_index.ok_or_else(|| {
|
.into(),
|
||||||
TransportErrorKind::Custom("log didn't have its index set".to_string().into())
|
index_within_block: log.log_index.ok_or_else(|| {
|
||||||
})?,
|
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,135 +565,57 @@ 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
|
We check that for all InInstructions for ERC20s emitted, a corresponding transfer
|
||||||
if !erc20s.contains(&token) {
|
occurred.
|
||||||
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
|
We don't do this for ETH as it'd require tracing the transaction, which is non-trivial.
|
||||||
let erc20_transfer_logs = {
|
It also isn't necessary as all of this is solely defense in depth.
|
||||||
let mut collected = HashMap::with_capacity(erc20s.len());
|
*/
|
||||||
while let Some(token_and_logs) = erc20_transfer_logs.next().await {
|
let mut justified = false;
|
||||||
let (token, logs) = token_and_logs?;
|
// These logs are returned from `top_level_transfers_unordered` and we don't require any
|
||||||
collected.insert(token, logs);
|
// ordering of them
|
||||||
}
|
for log in erc20_transfer_logs[&token].get(&transaction_hash).unwrap_or(&vec![]) {
|
||||||
collected
|
let log_index = log_index(log)?;
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
// Ensure we didn't already use this transfer to justify a distinct InInstruction
|
||||||
For each transaction, it may have a top-level ERC20 transfer. That top-level transfer won't
|
if justifying_erc20_transfer_logs.contains(&log_index) {
|
||||||
be the transfer caused by the call to `inInstruction`, so we shouldn't consider it
|
continue;
|
||||||
justification for this `InInstruction` event.
|
}
|
||||||
|
|
||||||
Fetch all top-level transfers here so we can ignore them.
|
// Check if this log is from the token we expected to be transferred
|
||||||
*/
|
if log.address() != Address::from(in_instruction.coin) {
|
||||||
let mut erc20_top_level_transfers = FuturesUnordered::new();
|
continue;
|
||||||
let mut transaction_transfer_logs = HashMap::new();
|
}
|
||||||
for transaction in erc20_transactions {
|
// Check if this is a transfer log
|
||||||
// Filter to the logs for this specific transaction
|
if log.topics().first() != Some(&Transfer::SIGNATURE_HASH) {
|
||||||
let logs = erc20_transfer_logs
|
continue;
|
||||||
.values()
|
}
|
||||||
.flat_map(|logs_per_token| logs_per_token.iter())
|
let Ok(transfer) = Transfer::decode_log(&log.inner.clone(), true) else { continue };
|
||||||
.filter_map(|log| {
|
// Check if this aligns with the InInstruction
|
||||||
let log_transaction_hash = log.transaction_hash.ok_or_else(|| {
|
if (transfer.from == in_instruction.from) &&
|
||||||
TransportErrorKind::Custom(
|
(transfer.to == self.address) &&
|
||||||
"log didn't have its transaction hash set".to_string().into(),
|
(transfer.value == in_instruction.amount)
|
||||||
)
|
{
|
||||||
});
|
justifying_erc20_transfer_logs.insert(log_index);
|
||||||
match log_transaction_hash {
|
justified = true;
|
||||||
Ok(log_transaction_hash) => {
|
break;
|
||||||
if log_transaction_hash == transaction {
|
|
||||||
Some(Ok(log))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Err(e) => Some(Err(e)),
|
|
||||||
}
|
}
|
||||||
})
|
if !justified {
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
// This is an exploit, a non-conforming ERC20, or an invalid connection
|
||||||
|
Err(TransportErrorKind::Custom(
|
||||||
// Find the top-level transfer
|
"ERC20 InInstruction with no matching transfer log".to_string().into(),
|
||||||
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;
|
|
||||||
for log in &transaction_transfer_logs[&transaction_hash] {
|
|
||||||
let log_index = log.log_index.ok_or_else(|| {
|
|
||||||
TransportErrorKind::Custom(
|
|
||||||
"log in transaction receipt didn't have its log index set".to_string().into(),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Ensure we didn't already use this transfer to check a distinct InInstruction event
|
|
||||||
if already_used_to_justify.contains(&log_index) {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this log is from the token we expected to be transferred
|
|
||||||
if log.address() != Address::from(in_instruction.coin) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Check if this is a transfer log
|
|
||||||
if log.topics().first() != Some(&Transfer::SIGNATURE_HASH) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let Ok(transfer) = Transfer::decode_log(&log.inner.clone(), true) else { continue };
|
|
||||||
// Check if this aligns with the InInstruction
|
|
||||||
if (transfer.from == in_instruction.from) &&
|
|
||||||
(transfer.to == self.address) &&
|
|
||||||
(transfer.value == in_instruction.amount)
|
|
||||||
{
|
|
||||||
already_used_to_justify.insert(log_index);
|
|
||||||
justified = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !justified {
|
|
||||||
// This is an exploit, a non-conforming ERC20, or an invalid connection
|
|
||||||
Err(TransportErrorKind::Custom(
|
|
||||||
"ERC20 InInstruction with no matching transfer log".to_string().into(),
|
|
||||||
))?;
|
|
||||||
}
|
}
|
||||||
in_instructions.push(in_instruction);
|
in_instructions.push(in_instruction);
|
||||||
}
|
}
|
||||||
@@ -660,7 +623,7 @@ impl Router {
|
|||||||
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,
|
||||||
|
|||||||
89
processor/ethereum/router/src/tests/erc20.rs
Normal file
89
processor/ethereum/router/src/tests/erc20.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
assert_eq!(
|
if self.state.next_key.is_none() {
|
||||||
CalldataAgnosticGas::calculate(tx.tx(), receipt.gas_used),
|
assert_eq!(
|
||||||
Router::UPDATE_SERAI_KEY_GAS,
|
CalldataAgnosticGas::calculate(tx.tx(), receipt.gas_used),
|
||||||
);
|
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,
|
||||||
|
|||||||
@@ -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![];
|
||||||
|
|||||||
Reference in New Issue
Block a user