Files
serai/processor/ethereum/router/src/lib.rs

813 lines
27 KiB
Rust
Raw Normal View History

2024-09-17 01:04:08 -04:00
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc = include_str!("../README.md")]
#![deny(missing_docs)]
use std::{
sync::Arc,
collections::{HashSet, HashMap},
};
use borsh::{BorshSerialize, BorshDeserialize};
2024-09-17 01:04:08 -04:00
use group::ff::PrimeField;
2025-01-24 03:23:58 -05:00
use alloy_core::primitives::{hex, Address, U256, TxKind};
2024-09-17 01:04:08 -04:00
use alloy_sol_types::{SolValue, SolConstructor, SolCall, SolEvent};
2024-10-31 02:23:59 -04:00
use alloy_consensus::TxLegacy;
use alloy_rpc_types_eth::{BlockId, Log, Filter, TransactionInput, TransactionRequest};
2024-09-17 01:04:08 -04:00
use alloy_transport::{TransportErrorKind, RpcError};
use alloy_simple_request_transport::SimpleRequest;
use alloy_provider::{Provider, RootProvider};
2025-01-23 09:30:54 -05:00
use scale::Encode;
use serai_client::{
in_instructions::primitives::Shorthand, networks::ethereum::Address as SeraiAddress,
};
use ethereum_primitives::LogIndex;
2024-09-17 01:04:08 -04:00
use ethereum_schnorr::{PublicKey, Signature};
use ethereum_deployer::Deployer;
use erc20::{Transfer, TopLevelTransfer, TopLevelTransfers, Erc20};
2024-09-17 01:04:08 -04:00
use futures_util::stream::{StreamExt, FuturesUnordered};
2024-09-17 01:04:08 -04:00
#[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 _irouter_abi {
alloy_sol_macro::sol!("contracts/IRouter.sol");
}
#[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 _router_abi {
2024-09-17 01:04:08 -04:00
include!(concat!(env!("OUT_DIR"), "/serai-processor-ethereum-router/router.rs"));
}
2024-11-02 13:19:07 -04:00
mod abi {
pub use super::_router_abi::IRouterWithoutCollisions::*;
pub use super::_router_abi::IRouter::*;
pub use super::_router_abi::Router::constructorCall;
2024-11-02 13:19:07 -04:00
}
2024-09-17 01:04:08 -04:00
use abi::{
NextSeraiKeySet as NextSeraiKeySetEvent, SeraiKeyUpdated as SeraiKeyUpdatedEvent,
InInstruction as InInstructionEvent, Batch as BatchEvent, EscapeHatch as EscapeHatchEvent,
Escaped as EscapedEvent,
2024-09-17 01:04:08 -04:00
};
2024-10-31 02:23:59 -04:00
#[cfg(test)]
mod tests;
2024-09-17 01:04:08 -04:00
impl From<&Signature> for abi::Signature {
fn from(signature: &Signature) -> Self {
Self {
c: <[u8; 32]>::from(signature.c().to_repr()).into(),
s: <[u8; 32]>::from(signature.s().to_repr()).into(),
}
}
}
/// A coin on Ethereum.
#[derive(Clone, Copy, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)]
2024-09-17 01:04:08 -04:00
pub enum Coin {
/// Ether, the native coin of Ethereum.
Ether,
/// An ERC20 token.
Erc20(
#[borsh(
serialize_with = "ethereum_primitives::serialize_address",
deserialize_with = "ethereum_primitives::deserialize_address"
)]
Address,
),
2024-09-17 01:04:08 -04:00
}
impl From<Coin> for Address {
fn from(coin: Coin) -> Address {
match coin {
Coin::Ether => Address::ZERO,
Coin::Erc20(address) => address,
}
}
}
impl From<Address> for Coin {
fn from(address: Address) -> Coin {
if address == Address::ZERO {
Coin::Ether
} else {
Coin::Erc20(address)
2024-12-09 02:00:17 -05:00
}
}
2024-09-17 01:04:08 -04:00
}
/// An InInstruction from the Router.
#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)]
2024-09-17 01:04:08 -04:00
pub struct InInstruction {
/// The ID for this `InInstruction`.
pub id: LogIndex,
/// The hash of the transaction which caused this.
pub transaction_hash: [u8; 32],
2024-09-17 01:04:08 -04:00
/// The address which transferred these coins to Serai.
#[borsh(
serialize_with = "ethereum_primitives::serialize_address",
deserialize_with = "ethereum_primitives::deserialize_address"
)]
pub from: Address,
2024-09-17 01:04:08 -04:00
/// The coin transferred.
pub coin: Coin,
/// The amount transferred.
#[borsh(
serialize_with = "ethereum_primitives::serialize_u256",
deserialize_with = "ethereum_primitives::deserialize_u256"
)]
2024-09-17 01:04:08 -04:00
pub amount: U256,
/// The data associated with the transfer.
pub data: Vec<u8>,
}
/// A list of `OutInstruction`s.
#[derive(Clone)]
pub struct OutInstructions(Vec<abi::OutInstruction>);
impl From<&[(SeraiAddress, U256)]> for OutInstructions {
fn from(outs: &[(SeraiAddress, U256)]) -> Self {
Self(
outs
.iter()
.map(|(address, amount)| {
#[allow(non_snake_case)]
let (destinationType, destination) = match address {
SeraiAddress::Address(address) => {
// Per the documentation, `DestinationType::Address`'s value is an ABI-encoded
// address
(abi::DestinationType::Address, (Address::from(address)).abi_encode())
}
SeraiAddress::Contract(contract) => (
abi::DestinationType::Code,
(abi::CodeDestination {
gasLimit: contract.gas_limit(),
code: contract.code().to_vec().into(),
})
.abi_encode(),
),
};
abi::OutInstruction { destinationType, destination: destination.into(), amount: *amount }
})
.collect(),
)
}
}
/// An action which was executed by the Router.
#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)]
2024-09-17 01:04:08 -04:00
pub enum Executed {
/// Next key was set.
NextSeraiKeySet {
/// The nonce this was done with.
nonce: u64,
/// The key set.
key: [u8; 32],
},
/// The next key was updated to.
SeraiKeyUpdated {
2024-09-17 01:04:08 -04:00
/// The nonce this was done with.
nonce: u64,
/// The key set.
key: [u8; 32],
},
/// Executed batch of `OutInstruction`s.
2024-09-17 01:04:08 -04:00
Batch {
/// The nonce this was done with.
nonce: u64,
/// The hash of the signed message for the Batch executed.
message_hash: [u8; 32],
},
/// The escape hatch was set.
EscapeHatch {
/// The nonce this was done with.
nonce: u64,
/// The address set to escape to.
#[borsh(
serialize_with = "ethereum_primitives::serialize_address",
deserialize_with = "ethereum_primitives::deserialize_address"
)]
escape_to: Address,
},
2024-09-17 01:04:08 -04:00
}
impl Executed {
/// The nonce consumed by this executed event.
///
/// This is a `u64` despite the contract defining the nonce as a `u256`. Since the nonce is
/// incremental, the u64 will never be exhausted.
2024-09-17 01:04:08 -04:00
pub fn nonce(&self) -> u64 {
match self {
Executed::NextSeraiKeySet { nonce, .. } |
Executed::SeraiKeyUpdated { nonce, .. } |
Executed::Batch { nonce, .. } |
Executed::EscapeHatch { nonce, .. } => *nonce,
2024-09-17 01:04:08 -04:00
}
}
}
/// An Escape from the Router.
#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)]
pub struct Escape {
/// The coin escaped.
pub coin: Coin,
/// The amount escaped.
#[borsh(
serialize_with = "ethereum_primitives::serialize_u256",
deserialize_with = "ethereum_primitives::deserialize_u256"
)]
pub amount: U256,
}
2024-09-17 01:04:08 -04:00
/// A view of the Router for Serai.
#[derive(Clone, Debug)]
pub struct Router {
provider: Arc<RootProvider<SimpleRequest>>,
address: Address,
}
2024-09-17 01:04:08 -04:00
impl Router {
/*
The gas limits to use for transactions.
These are expected to be constant as a distributed group signs the transactions invoking these
calls. Having the gas be constant prevents needing to run a protocol to determine what gas to
use.
These gas limits may break if/when gas opcodes undergo repricing. In that case, this library is
expected to be modified with these made parameters. The caller would then be expected to pass
the correct set of prices for the network they're operating on.
*/
const CONFIRM_NEXT_SERAI_KEY_GAS: u64 = 57_736;
const UPDATE_SERAI_KEY_GAS: u64 = 60_045;
const EXECUTE_BASE_GAS: u64 = 48_000;
const ESCAPE_HATCH_GAS: u64 = 61_238;
fn code() -> Vec<u8> {
2025-01-24 03:23:58 -05:00
const BYTECODE: &[u8] = {
const BYTECODE_HEX: &[u8] =
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()
2024-09-17 01:04:08 -04:00
}
fn init_code(key: &PublicKey) -> Vec<u8> {
2024-09-17 01:04:08 -04:00
let mut bytecode = Self::code();
// Append the constructor arguments
bytecode.extend((abi::constructorCall { initialSeraiKey: key.eth_repr().into() }).abi_encode());
bytecode
}
2024-10-31 02:23:59 -04:00
/// Obtain the transaction to deploy this contract.
///
/// This transaction assumes the `Deployer` has already been deployed. The gas limit and gas
/// price are not set and are left to the caller.
2024-10-31 02:23:59 -04:00
pub fn deployment_tx(initial_serai_key: &PublicKey) -> TxLegacy {
Deployer::deploy_tx(Self::init_code(initial_serai_key))
2024-10-31 02:23:59 -04:00
}
2024-09-17 01:04:08 -04:00
/// Create a new view of the Router.
///
/// This performs an on-chain lookup for the first deployed Router constructed with this public
/// key. This lookup is of a constant amount of calls and does not read any logs.
pub async fn new(
provider: Arc<RootProvider<SimpleRequest>>,
initial_serai_key: &PublicKey,
) -> Result<Option<Self>, RpcError<TransportErrorKind>> {
let Some(deployer) = Deployer::new(provider.clone()).await? else {
return Ok(None);
};
let Some(address) = deployer
2024-09-17 01:04:08 -04:00
.find_deployment(ethereum_primitives::keccak256(Self::init_code(initial_serai_key)))
.await?
else {
return Ok(None);
};
Ok(Some(Self { provider, address }))
2024-09-17 01:04:08 -04:00
}
/// The address of the router.
pub fn address(&self) -> Address {
self.address
2024-09-17 01:04:08 -04:00
}
/// Get the message to be signed in order to confirm the next key for Serai.
pub fn confirm_next_serai_key_message(chain_id: U256, nonce: u64) -> Vec<u8> {
abi::confirmNextSeraiKeyCall::new((abi::Signature {
c: chain_id.into(),
s: U256::try_from(nonce).unwrap().into(),
},))
.abi_encode()
}
/// Construct a transaction to confirm the next key representing Serai.
2025-01-23 09:30:54 -05:00
///
/// The gas price is not set and is left to the caller.
pub fn confirm_next_serai_key(&self, sig: &Signature) -> TxLegacy {
TxLegacy {
to: TxKind::Call(self.address),
input: abi::confirmNextSeraiKeyCall::new((abi::Signature::from(sig),)).abi_encode().into(),
gas_limit: Self::CONFIRM_NEXT_SERAI_KEY_GAS * 120 / 100,
..Default::default()
}
}
/// Get the message to be signed in order to update the key for Serai.
pub fn update_serai_key_message(chain_id: U256, nonce: u64, key: &PublicKey) -> Vec<u8> {
abi::updateSeraiKeyCall::new((
abi::Signature { c: chain_id.into(), s: U256::try_from(nonce).unwrap().into() },
key.eth_repr().into(),
))
.abi_encode()
}
2024-09-17 01:04:08 -04:00
/// Construct a transaction to update the key representing Serai.
2025-01-23 09:30:54 -05:00
///
/// The gas price is not set and is left to the caller.
2024-09-17 01:04:08 -04:00
pub fn update_serai_key(&self, public_key: &PublicKey, sig: &Signature) -> TxLegacy {
TxLegacy {
to: TxKind::Call(self.address),
input: abi::updateSeraiKeyCall::new((
abi::Signature::from(sig),
public_key.eth_repr().into(),
))
.abi_encode()
.into(),
gas_limit: Self::UPDATE_SERAI_KEY_GAS * 120 / 100,
2024-09-17 01:04:08 -04:00
..Default::default()
}
}
2025-01-23 09:30:54 -05:00
/// Construct a transaction to send coins with an InInstruction to Serai.
///
/// If coin is an ERC20, this will not create a transaction calling the Router but will create a
/// top-level transfer of the ERC20 to the Router. This avoids needing to call `approve` before
/// publishing the transaction calling the Router.
///
/// The gas limit and gas price are not set and are left to the caller.
pub fn in_instruction(&self, coin: Coin, amount: U256, in_instruction: &Shorthand) -> TxLegacy {
match coin {
Coin::Ether => TxLegacy {
to: self.address.into(),
input: abi::inInstructionCall::new((coin.into(), amount, in_instruction.encode().into()))
.abi_encode()
.into(),
value: amount,
..Default::default()
},
Coin::Erc20(erc20) => TxLegacy {
to: erc20.into(),
input: erc20::transferWithInInstructionCall::new((
self.address,
amount,
in_instruction.encode().into(),
))
.abi_encode()
.into(),
..Default::default()
},
}
}
/// Get the message to be signed in order to execute a series of `OutInstruction`s.
pub fn execute_message(
chain_id: U256,
nonce: u64,
coin: Coin,
fee: U256,
outs: OutInstructions,
) -> Vec<u8> {
abi::executeCall::new((
abi::Signature { c: chain_id.into(), s: U256::try_from(nonce).unwrap().into() },
Address::from(coin),
fee,
outs.0,
))
.abi_encode()
}
2024-09-17 01:04:08 -04:00
/// Construct a transaction to execute a batch of `OutInstruction`s.
2025-01-23 09:30:54 -05:00
///
/// The gas limit and gas price are not set and are left to the caller.
pub fn execute(&self, coin: Coin, fee: U256, outs: OutInstructions, sig: &Signature) -> TxLegacy {
// TODO
let gas_limit = Self::EXECUTE_BASE_GAS + outs.0.iter().map(|_| 200_000 + 10_000).sum::<u64>();
2024-09-17 01:04:08 -04:00
TxLegacy {
to: TxKind::Call(self.address),
input: abi::executeCall::new((abi::Signature::from(sig), Address::from(coin), fee, outs.0))
.abi_encode()
.into(),
gas_limit: gas_limit * 120 / 100,
2024-09-17 01:04:08 -04:00
..Default::default()
}
}
2024-12-09 02:00:17 -05:00
/// Get the message to be signed in order to trigger the escape hatch.
pub fn escape_hatch_message(chain_id: U256, nonce: u64, escape_to: Address) -> Vec<u8> {
2024-12-09 02:00:17 -05:00
abi::escapeHatchCall::new((
abi::Signature { c: chain_id.into(), s: U256::try_from(nonce).unwrap().into() },
2024-12-09 02:00:17 -05:00
escape_to,
))
.abi_encode()
}
/// Construct a transaction to trigger the escape hatch.
2025-01-23 09:30:54 -05:00
///
/// The gas price is not set and is left to the caller.
2024-12-09 02:00:17 -05:00
pub fn escape_hatch(&self, escape_to: Address, sig: &Signature) -> TxLegacy {
TxLegacy {
to: TxKind::Call(self.address),
2024-12-09 02:00:17 -05:00
input: abi::escapeHatchCall::new((abi::Signature::from(sig), escape_to)).abi_encode().into(),
gas_limit: Self::ESCAPE_HATCH_GAS * 120 / 100,
..Default::default()
}
}
/// Construct a transaction to escape coins via the escape hatch.
2025-01-23 09:30:54 -05:00
///
/// The gas limit and gas price are not set and are left to the caller.
pub fn escape(&self, coin: Coin) -> TxLegacy {
2024-12-09 02:00:17 -05:00
TxLegacy {
to: TxKind::Call(self.address),
input: abi::escapeCall::new((Address::from(coin),)).abi_encode().into(),
2024-12-09 02:00:17 -05:00
..Default::default()
}
}
/// 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.
pub async fn in_instructions_unordered(
2024-09-17 01:04:08 -04:00
&self,
from_block: u64,
to_block: u64,
allowed_erc20s: &HashSet<Address>,
2024-09-17 01:04:08 -04:00
) -> Result<Vec<InInstruction>, RpcError<TransportErrorKind>> {
// The InInstruction events for this block
let in_instruction_logs = {
let filter = Filter::new().from_block(from_block).to_block(to_block).address(self.address);
let filter = filter.event_signature(InInstructionEvent::SIGNATURE_HASH);
self.provider.get_logs(&filter).await?
};
2024-09-17 01:04:08 -04:00
// 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());
// Handle the top-level transfers for this block
let mut justifying_erc20_transfer_logs = HashSet::new();
let erc20_transfer_logs = {
let mut transfers = FuturesUnordered::new();
for erc20 in allowed_erc20s {
transfers.push(async move {
(
erc20,
Erc20::top_level_transfers_unordered(
&self.provider,
from_block,
to_block,
*erc20,
self.address,
)
.await,
)
});
}
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 {
2024-09-17 01:04:08 -04:00
// Double check the address which emitted this log
if log.address() != self.address {
2024-09-17 01:04:08 -04:00
Err(TransportErrorKind::Custom(
"node returned a log from a different address than requested".to_string().into(),
))?;
}
// Double check this is a InInstruction log
if log.topics().first() != Some(&InInstructionEvent::SIGNATURE_HASH) {
continue;
}
2024-09-17 01:04:08 -04:00
let log_index = |log: &Log| -> Result<LogIndex, TransportErrorKind> {
Ok(LogIndex {
block_hash: log
.block_hash
.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(|| {
TransportErrorKind::Custom("log didn't have its index set".to_string().into())
})?,
})
};
2024-09-17 01:04:08 -04:00
let id = log_index(&log)?;
let transaction_hash = log.transaction_hash.ok_or_else(|| {
2024-09-17 01:04:08 -04:00
TransportErrorKind::Custom("log didn't have its transaction hash set".to_string().into())
})?;
let transaction_hash = *transaction_hash;
2024-09-17 01:04:08 -04:00
let log = log
.log_decode::<InInstructionEvent>()
.map_err(|e| {
TransportErrorKind::Custom(
format!("filtered to InInstructionEvent yet couldn't decode log: {e:?}").into(),
)
})?
.inner
.data;
let coin = Coin::from(log.coin);
2024-09-17 01:04:08 -04:00
let in_instruction = InInstruction {
id,
transaction_hash,
from: log.from,
coin,
amount: log.amount,
data: log.instruction.as_ref().to_vec(),
};
match coin {
Coin::Ether => {}
Coin::Erc20(token) => {
// Check this is an allowed token
if !allowed_erc20s.contains(&token) {
2024-09-17 01:04:08 -04:00
continue;
}
/*
We check that for all InInstructions for ERC20s emitted, a corresponding transfer
occurred.
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 justified = false;
// These logs are returned from `top_level_transfers_unordered` and we don't require any
// ordering of them
for log in erc20_transfer_logs[&token].get(&transaction_hash).unwrap_or(&vec![]) {
let log_index = log_index(log)?;
// Ensure we didn't already use this transfer to justify a distinct InInstruction
if justifying_erc20_transfer_logs.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)
{
justifying_erc20_transfer_logs.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);
2024-09-17 01:04:08 -04:00
}
Ok(in_instructions)
}
/// Fetch the executed actions for the specified range of blocks.
pub async fn executed(
&self,
from_block: u64,
to_block: u64,
) -> Result<Vec<Executed>, RpcError<TransportErrorKind>> {
fn decode<E: SolEvent>(log: &Log) -> Result<E, RpcError<TransportErrorKind>> {
Ok(
log
.log_decode::<E>()
2024-09-17 01:04:08 -04:00
.map_err(|e| {
TransportErrorKind::Custom(
format!("filtered to event yet couldn't decode log: {e:?}").into(),
2024-09-17 01:04:08 -04:00
)
})?
.inner
.data,
)
2024-09-17 01:04:08 -04:00
}
let filter = Filter::new().from_block(from_block).to_block(to_block).address(self.address);
let mut logs = self.provider.get_logs(&filter).await?;
logs.sort_by_key(|log| (log.block_number, log.log_index));
2024-09-17 01:04:08 -04:00
let mut res = vec![];
for log in logs {
// Double check the address which emitted this log
if log.address() != self.address {
Err(TransportErrorKind::Custom(
"node returned a log from a different address than requested".to_string().into(),
))?;
}
2024-09-17 01:04:08 -04:00
match log.topics().first() {
Some(&NextSeraiKeySetEvent::SIGNATURE_HASH) => {
let event = decode::<NextSeraiKeySetEvent>(&log)?;
res.push(Executed::NextSeraiKeySet {
nonce: event.nonce.try_into().map_err(|e| {
TransportErrorKind::Custom(format!("failed to convert nonce to u64: {e:?}").into())
})?,
key: event.key.into(),
});
}
Some(&SeraiKeyUpdatedEvent::SIGNATURE_HASH) => {
let event = decode::<SeraiKeyUpdatedEvent>(&log)?;
res.push(Executed::SeraiKeyUpdated {
nonce: event.nonce.try_into().map_err(|e| {
TransportErrorKind::Custom(format!("failed to convert nonce to u64: {e:?}").into())
})?,
key: event.key.into(),
});
}
Some(&BatchEvent::SIGNATURE_HASH) => {
let event = decode::<BatchEvent>(&log)?;
res.push(Executed::Batch {
nonce: event.nonce.try_into().map_err(|e| {
TransportErrorKind::Custom(format!("failed to convert nonce to u64: {e:?}").into())
})?,
message_hash: event.messageHash.into(),
});
}
Some(&EscapeHatchEvent::SIGNATURE_HASH) => {
let event = decode::<EscapeHatchEvent>(&log)?;
res.push(Executed::EscapeHatch {
nonce: event.nonce.try_into().map_err(|e| {
TransportErrorKind::Custom(format!("failed to convert nonce to u64: {e:?}").into())
})?,
escape_to: event.escapeTo,
});
}
Some(&InInstructionEvent::SIGNATURE_HASH | &EscapedEvent::SIGNATURE_HASH) => {}
unrecognized => Err(TransportErrorKind::Custom(
format!("unrecognized event yielded by the Router: {:?}", unrecognized.map(hex::encode))
.into(),
))?,
2024-09-17 01:04:08 -04:00
}
}
Ok(res)
}
/// Fetch the `Escape`s from the smart contract through the escape hatch.
pub async fn escapes(
&self,
from_block: u64,
to_block: u64,
) -> Result<Vec<Escape>, RpcError<TransportErrorKind>> {
let filter = Filter::new().from_block(from_block).to_block(to_block).address(self.address);
let mut logs =
self.provider.get_logs(&filter.event_signature(EscapedEvent::SIGNATURE_HASH)).await?;
logs.sort_by_key(|log| (log.block_number, log.log_index));
let mut res = vec![];
for log in logs {
// Double check the address which emitted this log
if log.address() != self.address {
Err(TransportErrorKind::Custom(
"node returned a log from a different address than requested".to_string().into(),
))?;
}
// Double check the topic
if log.topics().first() != Some(&EscapedEvent::SIGNATURE_HASH) {
Err(TransportErrorKind::Custom(
"node returned a log for a different topic than filtered to".to_string().into(),
))?;
}
let log = log
.log_decode::<EscapedEvent>()
.map_err(|e| {
TransportErrorKind::Custom(
format!("filtered to event yet couldn't decode log: {e:?}").into(),
)
})?
.inner
.data;
res.push(Escape { coin: Coin::from(log.coin), amount: log.amount });
}
2024-09-17 01:04:08 -04:00
Ok(res)
}
2024-10-31 02:23:59 -04:00
async fn fetch_key(
&self,
block: BlockId,
call: Vec<u8>,
) -> Result<Option<PublicKey>, RpcError<TransportErrorKind>> {
let call =
TransactionRequest::default().to(self.address).input(TransactionInput::new(call.into()));
let bytes = self.provider.call(&call).block(block).await?;
// This is fine as both key calls share a return type
let res = abi::nextSeraiKeyCall::abi_decode_returns(&bytes, true)
.map_err(|e| TransportErrorKind::Custom(format!("failed to decode key: {e:?}").into()))?;
let eth_repr = <[u8; 32]>::from(res._0);
Ok(if eth_repr == [0; 32] {
None
} else {
Some(PublicKey::from_eth_repr(eth_repr).ok_or_else(|| {
2024-10-31 02:23:59 -04:00
TransportErrorKind::Custom("invalid key set on router".to_string().into())
})?)
})
}
/// Fetch the next key for Serai's Ethereum validators
pub async fn next_key(
&self,
block: BlockId,
) -> Result<Option<PublicKey>, RpcError<TransportErrorKind>> {
self.fetch_key(block, abi::nextSeraiKeyCall::new(()).abi_encode()).await
}
/// Fetch the current key for Serai's Ethereum validators
pub async fn key(
&self,
block: BlockId,
) -> Result<Option<PublicKey>, RpcError<TransportErrorKind>> {
self.fetch_key(block, abi::seraiKeyCall::new(()).abi_encode()).await
2024-10-31 02:23:59 -04:00
}
/// Fetch the nonce of the next action to execute
pub async fn next_nonce(&self, block: BlockId) -> Result<u64, RpcError<TransportErrorKind>> {
let call = TransactionRequest::default()
.to(self.address)
2024-10-31 02:23:59 -04:00
.input(TransactionInput::new(abi::nextNonceCall::new(()).abi_encode().into()));
let bytes = self.provider.call(&call).block(block).await?;
2024-10-31 02:23:59 -04:00
let res = abi::nextNonceCall::abi_decode_returns(&bytes, true)
.map_err(|e| TransportErrorKind::Custom(format!("failed to decode nonce: {e:?}").into()))?;
2024-10-31 02:23:59 -04:00
Ok(u64::try_from(res._0).map_err(|_| {
TransportErrorKind::Custom("nonce returned exceeded 2**64".to_string().into())
})?)
}
/// Fetch the address the escape hatch was set to
pub async fn escaped_to(
&self,
block: BlockId,
) -> Result<Option<Address>, RpcError<TransportErrorKind>> {
2024-10-31 02:23:59 -04:00
let call = TransactionRequest::default()
.to(self.address)
2024-10-31 02:23:59 -04:00
.input(TransactionInput::new(abi::escapedToCall::new(()).abi_encode().into()));
let bytes = self.provider.call(&call).block(block).await?;
2024-10-31 02:23:59 -04:00
let res = abi::escapedToCall::abi_decode_returns(&bytes, true).map_err(|e| {
TransportErrorKind::Custom(format!("failed to decode the address escaped to: {e:?}").into())
2024-10-31 02:23:59 -04:00
})?;
Ok(if res._0 == Address([0; 20].into()) { None } else { Some(res._0) })
2024-10-31 02:23:59 -04:00
}
2024-09-17 01:04:08 -04:00
}