diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 382d9a2f..f08b457b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -55,6 +55,7 @@ jobs: -p serai-processor-ethereum-contracts \ -p serai-processor-ethereum-primitives \ -p serai-processor-ethereum-deployer \ + -p serai-processor-ethereum-erc20 \ -p ethereum-serai \ -p serai-ethereum-processor \ -p serai-monero-processor \ diff --git a/Cargo.lock b/Cargo.lock index 0253cf32..b52aca05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8737,6 +8737,19 @@ dependencies = [ "serai-processor-ethereum-primitives", ] +[[package]] +name = "serai-processor-ethereum-erc20" +version = "0.1.0" +dependencies = [ + "alloy-core", + "alloy-provider", + "alloy-rpc-types-eth", + "alloy-simple-request-transport", + "alloy-sol-macro", + "alloy-sol-types", + "alloy-transport", +] + [[package]] name = "serai-processor-ethereum-primitives" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index c0010659..e2de489d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,6 +90,7 @@ members = [ "processor/ethereum/contracts", "processor/ethereum/primitives", "processor/ethereum/deployer", + "processor/ethereum/erc20", "processor/ethereum/ethereum-serai", "processor/ethereum", "processor/monero", diff --git a/deny.toml b/deny.toml index 8b630fb9..1091d103 100644 --- a/deny.toml +++ b/deny.toml @@ -62,6 +62,7 @@ exceptions = [ { allow = ["AGPL-3.0"], name = "serai-processor-ethereum-contracts" }, { allow = ["AGPL-3.0"], name = "serai-processor-ethereum-primitives" }, { allow = ["AGPL-3.0"], name = "serai-processor-ethereum-deployer" }, + { allow = ["AGPL-3.0"], name = "serai-processor-ethereum-erc20" }, { allow = ["AGPL-3.0"], name = "ethereum-serai" }, { allow = ["AGPL-3.0"], name = "serai-ethereum-processor" }, { allow = ["AGPL-3.0"], name = "serai-monero-processor" }, diff --git a/processor/ethereum/erc20/Cargo.toml b/processor/ethereum/erc20/Cargo.toml new file mode 100644 index 00000000..85bc83c3 --- /dev/null +++ b/processor/ethereum/erc20/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "serai-processor-ethereum-erc20" +version = "0.1.0" +description = "A library for the Serai Processor to interact with ERC20s" +license = "AGPL-3.0-only" +repository = "https://github.com/serai-dex/serai/tree/develop/processor/ethereum/erc20" +authors = ["Luke Parker "] +edition = "2021" +publish = false +rust-version = "1.79" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[lints] +workspace = true + +[dependencies] +alloy-core = { version = "0.8", default-features = false } + +alloy-sol-types = { version = "0.8", default-features = false } +alloy-sol-macro = { version = "0.8", default-features = false } + +alloy-rpc-types-eth = { version = "0.3", default-features = false } +alloy-transport = { version = "0.3", default-features = false } +alloy-simple-request-transport = { path = "../../../networks/ethereum/alloy-simple-request-transport", default-features = false } +alloy-provider = { version = "0.3", default-features = false } diff --git a/processor/ethereum/erc20/LICENSE b/processor/ethereum/erc20/LICENSE new file mode 100644 index 00000000..41d5a261 --- /dev/null +++ b/processor/ethereum/erc20/LICENSE @@ -0,0 +1,15 @@ +AGPL-3.0-only license + +Copyright (c) 2022-2024 Luke Parker + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License Version 3 as +published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . diff --git a/processor/ethereum/erc20/README.md b/processor/ethereum/erc20/README.md new file mode 100644 index 00000000..f1e447b0 --- /dev/null +++ b/processor/ethereum/erc20/README.md @@ -0,0 +1,3 @@ +# ERC20 + +A library for the Serai Processor to interact with ERC20s. diff --git a/processor/ethereum/contracts/contracts/IERC20.sol b/processor/ethereum/erc20/contracts/IERC20.sol similarity index 100% rename from processor/ethereum/contracts/contracts/IERC20.sol rename to processor/ethereum/erc20/contracts/IERC20.sol diff --git a/processor/ethereum/ethereum-serai/src/erc20.rs b/processor/ethereum/erc20/src/lib.rs similarity index 61% rename from processor/ethereum/ethereum-serai/src/erc20.rs rename to processor/ethereum/erc20/src/lib.rs index 6a32f7cc..560ea86c 100644 --- a/processor/ethereum/ethereum-serai/src/erc20.rs +++ b/processor/ethereum/erc20/src/lib.rs @@ -1,3 +1,7 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc = include_str!("../README.md")] +#![deny(missing_docs)] + use std::{sync::Arc, collections::HashSet}; use alloy_core::primitives::{Address, B256, U256}; @@ -5,18 +9,31 @@ use alloy_core::primitives::{Address, B256, U256}; use alloy_sol_types::{SolInterface, SolEvent}; use alloy_rpc_types_eth::Filter; +use alloy_transport::{TransportErrorKind, RpcError}; use alloy_simple_request_transport::SimpleRequest; use alloy_provider::{Provider, RootProvider}; -use crate::Error; -pub use crate::abi::erc20 as abi; -use abi::{IERC20Calls, Transfer, transferCall, transferFromCall}; +#[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/IERC20.sol"); +} +use abi::IERC20::{IERC20Calls, Transfer, transferCall, transferFromCall}; +/// A top-level ERC20 transfer #[derive(Clone, Debug)] pub struct TopLevelErc20Transfer { + /// The transaction ID which effected this transfer. pub id: [u8; 32], + /// The address which made the transfer. pub from: [u8; 20], + /// The amount transferred. pub amount: U256, + /// The data appended after the call itself. pub data: Vec, } @@ -29,30 +46,43 @@ impl Erc20 { Self(provider, Address::from(&address)) } + /// Fetch all top-level transfers to the specified ERC20. pub async fn top_level_transfers( &self, block: u64, to: [u8; 20], - ) -> Result, Error> { + ) -> Result, RpcError> { let filter = Filter::new().from_block(block).to_block(block).address(self.1); let filter = filter.event_signature(Transfer::SIGNATURE_HASH); let mut to_topic = [0; 32]; to_topic[12 ..].copy_from_slice(&to); let filter = filter.topic2(B256::from(to_topic)); - let logs = self.0.get_logs(&filter).await.map_err(|_| Error::ConnectionError)?; + let logs = self.0.get_logs(&filter).await?; + /* + A set of all transactions we've handled a transfer from. This handles the edge case where a + top-level transfer T somehow triggers another transfer T', with equivalent contents, within + the same transaction. We only want to report one transfer as only one is top-level. + */ let mut handled = HashSet::new(); let mut top_level_transfers = vec![]; for log in logs { // Double check the address which emitted this log if log.address() != self.1 { - Err(Error::ConnectionError)?; + Err(TransportErrorKind::Custom( + "node returned logs for a different address than requested".to_string().into(), + ))?; } - let tx_id = log.transaction_hash.ok_or(Error::ConnectionError)?; - let tx = - self.0.get_transaction_by_hash(tx_id).await.ok().flatten().ok_or(Error::ConnectionError)?; + let tx_id = log.transaction_hash.ok_or_else(|| { + TransportErrorKind::Custom("log didn't specify its transaction hash".to_string().into()) + })?; + let tx = self.0.get_transaction_by_hash(tx_id).await?.ok_or_else(|| { + TransportErrorKind::Custom( + "node didn't have the transaction which emitted a log it had".to_string().into(), + ) + })?; // If this is a top-level call... if tx.to == Some(self.1) { @@ -70,7 +100,13 @@ impl Erc20 { _ => continue, }; - let log = log.log_decode::().map_err(|_| Error::ConnectionError)?.inner.data; + let log = log + .log_decode::() + .map_err(|e| { + TransportErrorKind::Custom(format!("failed to decode Transfer log: {e:?}").into()) + })? + .inner + .data; // Ensure the top-level transfer is equivalent, and this presumably isn't a log for an // internal transfer