Smash ERC20 into its own library

This commit is contained in:
Luke Parker 2024-09-16 21:59:12 -04:00
parent ae61f3d359
commit a7d5640642
9 changed files with 108 additions and 10 deletions

View file

@ -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 \

13
Cargo.lock generated
View file

@ -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"

View file

@ -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",

View file

@ -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" },

View file

@ -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 <lukeparker5132@gmail.com>"]
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 }

View file

@ -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 <http://www.gnu.org/licenses/>.

View file

@ -0,0 +1,3 @@
# ERC20
A library for the Serai Processor to interact with ERC20s.

View file

@ -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<u8>,
}
@ -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<Vec<TopLevelErc20Transfer>, Error> {
) -> Result<Vec<TopLevelErc20Transfer>, RpcError<TransportErrorKind>> {
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::<Transfer>().map_err(|_| Error::ConnectionError)?.inner.data;
let log = log
.log_decode::<Transfer>()
.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