mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 12:19:24 +00:00
alloy-core 1.0, alloy 0.14, revm 0.22 (001)
This moves to Rust 1.86 as were prior on Rust 1.81, and the new alloy dependencies require 1.82. The revm API changes were notable for us. Instead of relying on a modified call instruction (with deep introspection into the EVM design), we now use the more recent and now more prominent Inspector API. This: 1) Lets us perform far less introspection 2) Forces us to rewrite the gas estimation code we just had audited Thankfully, it itself should be much easier to read/review, and our existing test suite has extensively validated it. This resolves 001 which was a concern for if/when this upgrade occurs. By doing it now, with a dedicated test case ensuring the issue we would have had with alloy-core 0.8 and `validate=false` isn't actively an issue, we resolve it.
This commit is contained in:
1528
Cargo.lock
generated
1528
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -21,8 +21,8 @@ tower = "0.5"
|
||||
serde_json = { version = "1", default-features = false }
|
||||
simple-request = { path = "../../../common/request", version = "0.1", default-features = false }
|
||||
|
||||
alloy-json-rpc = { version = "0.9", default-features = false }
|
||||
alloy-transport = { version = "0.9", default-features = false }
|
||||
alloy-json-rpc = { version = "0.14", default-features = false }
|
||||
alloy-transport = { version = "0.14", default-features = false }
|
||||
|
||||
[features]
|
||||
default = ["tls"]
|
||||
|
||||
@@ -29,14 +29,14 @@ rand_core = { version = "0.6", default-features = false, features = ["std"] }
|
||||
|
||||
k256 = { version = "^0.13.1", default-features = false, features = ["ecdsa"] }
|
||||
|
||||
alloy-core = { version = "0.8", default-features = false }
|
||||
alloy-sol-types = { version = "0.8", default-features = false }
|
||||
alloy-core = { version = "1", default-features = false }
|
||||
alloy-sol-types = { version = "1", default-features = false }
|
||||
|
||||
alloy-simple-request-transport = { path = "../../../networks/ethereum/alloy-simple-request-transport", default-features = false }
|
||||
alloy-rpc-types-eth = { version = "0.9", default-features = false }
|
||||
alloy-rpc-client = { version = "0.9", default-features = false }
|
||||
alloy-provider = { version = "0.9", default-features = false }
|
||||
alloy-rpc-types-eth = { version = "0.14", default-features = false }
|
||||
alloy-rpc-client = { version = "0.14", default-features = false }
|
||||
alloy-provider = { version = "0.14", default-features = false }
|
||||
|
||||
alloy-node-bindings = { version = "0.9", default-features = false }
|
||||
alloy-node-bindings = { version = "0.14", default-features = false }
|
||||
|
||||
tokio = { version = "1", default-features = false, features = ["macros"] }
|
||||
|
||||
@@ -32,7 +32,7 @@ mod abi {
|
||||
pub(crate) use TestSchnorr::*;
|
||||
}
|
||||
|
||||
async fn setup_test() -> (AnvilInstance, Arc<RootProvider<SimpleRequest>>, Address) {
|
||||
async fn setup_test() -> (AnvilInstance, Arc<RootProvider>, Address) {
|
||||
let anvil = Anvil::new().spawn();
|
||||
|
||||
let provider = Arc::new(RootProvider::new(
|
||||
@@ -61,7 +61,7 @@ async fn setup_test() -> (AnvilInstance, Arc<RootProvider<SimpleRequest>>, Addre
|
||||
}
|
||||
|
||||
async fn call_verify(
|
||||
provider: &RootProvider<SimpleRequest>,
|
||||
provider: &RootProvider,
|
||||
address: Address,
|
||||
public_key: &PublicKey,
|
||||
message: &[u8],
|
||||
@@ -80,10 +80,8 @@ async fn call_verify(
|
||||
.abi_encode()
|
||||
.into(),
|
||||
));
|
||||
let bytes = provider.call(&call).await.unwrap();
|
||||
let res = abi::verifyCall::abi_decode_returns(&bytes, true).unwrap();
|
||||
|
||||
res._0
|
||||
let bytes = provider.call(call).await.unwrap();
|
||||
abi::verifyCall::abi_decode_returns(&bytes).unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -18,7 +18,7 @@ use crate::{Signature, tests::test_key};
|
||||
fn ecrecover(message: Scalar, odd_y: bool, r: Scalar, s: Scalar) -> Option<[u8; 20]> {
|
||||
let sig = ecdsa::Signature::from_scalars(r, s).ok()?;
|
||||
let message: [u8; 32] = message.to_repr().into();
|
||||
alloy_core::primitives::PrimitiveSignature::from_signature_and_parity(sig, odd_y)
|
||||
alloy_core::primitives::Signature::from_signature_and_parity(sig, odd_y)
|
||||
.recover_address_from_prehash(&alloy_core::primitives::B256::from(message))
|
||||
.ok()
|
||||
.map(Into::into)
|
||||
|
||||
@@ -31,14 +31,14 @@ frost = { package = "modular-frost", path = "../../crypto/frost", default-featur
|
||||
|
||||
k256 = { version = "^0.13.1", default-features = false, features = ["std"] }
|
||||
|
||||
alloy-core = { version = "0.8", default-features = false }
|
||||
alloy-core = { version = "1", default-features = false }
|
||||
alloy-rlp = { version = "0.3", default-features = false }
|
||||
|
||||
alloy-rpc-types-eth = { version = "0.9", default-features = false }
|
||||
alloy-transport = { version = "0.9", default-features = false }
|
||||
alloy-rpc-types-eth = { version = "0.14", default-features = false }
|
||||
alloy-transport = { version = "0.14", default-features = false }
|
||||
alloy-simple-request-transport = { path = "../../networks/ethereum/alloy-simple-request-transport", default-features = false }
|
||||
alloy-rpc-client = { version = "0.9", default-features = false }
|
||||
alloy-provider = { version = "0.9", default-features = false }
|
||||
alloy-rpc-client = { version = "0.14", default-features = false }
|
||||
alloy-provider = { version = "0.14", default-features = false }
|
||||
|
||||
serai-client = { path = "../../substrate/client", default-features = false, features = ["ethereum"] }
|
||||
|
||||
|
||||
@@ -17,17 +17,16 @@ rustdoc-args = ["--cfg", "docsrs"]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
alloy-core = { version = "0.8", default-features = false }
|
||||
alloy-core = { version = "1", default-features = false }
|
||||
|
||||
alloy-sol-types = { version = "0.8", default-features = false }
|
||||
alloy-sol-macro = { version = "0.8", default-features = false }
|
||||
alloy-sol-types = { version = "1", default-features = false }
|
||||
alloy-sol-macro = { version = "1", default-features = false }
|
||||
|
||||
alloy-consensus = { version = "0.9", default-features = false }
|
||||
alloy-consensus = { version = "0.14", default-features = false }
|
||||
|
||||
alloy-rpc-types-eth = { version = "0.9", default-features = false }
|
||||
alloy-transport = { version = "0.9", default-features = false }
|
||||
alloy-simple-request-transport = { path = "../../../networks/ethereum/alloy-simple-request-transport", default-features = false }
|
||||
alloy-provider = { version = "0.9", default-features = false }
|
||||
alloy-rpc-types-eth = { version = "0.14", default-features = false }
|
||||
alloy-transport = { version = "0.14", default-features = false }
|
||||
alloy-provider = { version = "0.14", default-features = false }
|
||||
|
||||
ethereum-primitives = { package = "serai-processor-ethereum-primitives", path = "../primitives", default-features = false }
|
||||
|
||||
@@ -35,8 +34,9 @@ ethereum-primitives = { package = "serai-processor-ethereum-primitives", path =
|
||||
build-solidity-contracts = { path = "../../../networks/ethereum/build-contracts", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
alloy-rpc-client = { version = "0.9", default-features = false }
|
||||
alloy-node-bindings = { version = "0.9", default-features = false }
|
||||
alloy-simple-request-transport = { path = "../../../networks/ethereum/alloy-simple-request-transport", default-features = false }
|
||||
alloy-rpc-client = { version = "0.14", default-features = false }
|
||||
alloy-node-bindings = { version = "0.14", default-features = false }
|
||||
|
||||
tokio = { version = "1.0", default-features = false, features = ["rt-multi-thread", "macros"] }
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ use alloy_sol_types::SolCall;
|
||||
|
||||
use alloy_rpc_types_eth::{TransactionInput, TransactionRequest};
|
||||
use alloy_transport::{TransportErrorKind, RpcError};
|
||||
use alloy_simple_request_transport::SimpleRequest;
|
||||
use alloy_provider::{Provider, RootProvider};
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -44,7 +43,7 @@ const INITCODE: &[u8] = {
|
||||
/// of the EVM. It then supports retrieving the deployed contracts addresses (which aren't
|
||||
/// deterministic) using a single call.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Deployer(Arc<RootProvider<SimpleRequest>>);
|
||||
pub struct Deployer(Arc<RootProvider>);
|
||||
impl Deployer {
|
||||
/// Obtain the transaction to deploy this contract, already signed.
|
||||
///
|
||||
@@ -119,7 +118,7 @@ impl Deployer {
|
||||
///
|
||||
/// This will return `None` if the Deployer has yet to be deployed on-chain.
|
||||
pub async fn new(
|
||||
provider: Arc<RootProvider<SimpleRequest>>,
|
||||
provider: Arc<RootProvider>,
|
||||
) -> Result<Option<Self>, RpcError<TransportErrorKind>> {
|
||||
let address = Self::address();
|
||||
let code = provider.get_code_at(address).await?;
|
||||
@@ -138,16 +137,14 @@ impl Deployer {
|
||||
let call = TransactionRequest::default().to(Self::address()).input(TransactionInput::new(
|
||||
abi::Deployer::deploymentsCall::new((init_code_hash.into(),)).abi_encode().into(),
|
||||
));
|
||||
let bytes = self.0.call(&call).await?;
|
||||
let deployment = abi::Deployer::deploymentsCall::abi_decode_returns(&bytes, true)
|
||||
.map_err(|e| {
|
||||
TransportErrorKind::Custom(
|
||||
format!("node returned a non-address for function returning address: {e:?}").into(),
|
||||
)
|
||||
})?
|
||||
._0;
|
||||
let bytes = self.0.call(call).await?;
|
||||
let deployment = abi::Deployer::deploymentsCall::abi_decode_returns(&bytes).map_err(|e| {
|
||||
TransportErrorKind::Custom(
|
||||
format!("node returned a non-address for function returning address: {e:?}").into(),
|
||||
)
|
||||
})?;
|
||||
|
||||
if **deployment == [0; 20] {
|
||||
if deployment == Address::ZERO {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(deployment))
|
||||
|
||||
@@ -76,9 +76,9 @@ async fn test_deployer() {
|
||||
let call = TransactionRequest::default()
|
||||
.to(Deployer::address())
|
||||
.input(TransactionInput::new(deploy_tx.tx().input.clone()));
|
||||
let call_err = provider.call(&call).await.unwrap_err();
|
||||
let call_err = provider.call(call).await.unwrap_err();
|
||||
assert!(matches!(
|
||||
call_err.as_error_resp().unwrap().as_decoded_error::<DeployerErrors>(true).unwrap(),
|
||||
call_err.as_error_resp().unwrap().as_decoded_interface_error::<DeployerErrors>().unwrap(),
|
||||
DeployerErrors::PriorDeployed(PriorDeployed {}),
|
||||
));
|
||||
}
|
||||
@@ -97,9 +97,9 @@ async fn test_deployer() {
|
||||
let call = TransactionRequest::default()
|
||||
.to(Deployer::address())
|
||||
.input(TransactionInput::new(deploy_tx.tx().input.clone()));
|
||||
let call_err = provider.call(&call).await.unwrap_err();
|
||||
let call_err = provider.call(call).await.unwrap_err();
|
||||
assert!(matches!(
|
||||
call_err.as_error_resp().unwrap().as_decoded_error::<DeployerErrors>(true).unwrap(),
|
||||
call_err.as_error_resp().unwrap().as_decoded_interface_error::<DeployerErrors>().unwrap(),
|
||||
DeployerErrors::DeploymentFailed(DeploymentFailed {}),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -17,15 +17,14 @@ rustdoc-args = ["--cfg", "docsrs"]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
alloy-core = { version = "0.8", default-features = false }
|
||||
alloy-core = { version = "1", default-features = false }
|
||||
|
||||
alloy-sol-types = { version = "0.8", default-features = false }
|
||||
alloy-sol-macro = { version = "0.8", default-features = false }
|
||||
alloy-sol-types = { version = "1", default-features = false }
|
||||
alloy-sol-macro = { version = "1", default-features = false }
|
||||
|
||||
alloy-rpc-types-eth = { version = "0.9", default-features = false }
|
||||
alloy-transport = { version = "0.9", default-features = false }
|
||||
alloy-simple-request-transport = { path = "../../../networks/ethereum/alloy-simple-request-transport", default-features = false }
|
||||
alloy-provider = { version = "0.9", default-features = false }
|
||||
alloy-rpc-types-eth = { version = "0.14", default-features = false }
|
||||
alloy-transport = { version = "0.14", default-features = false }
|
||||
alloy-provider = { version = "0.14", default-features = false }
|
||||
|
||||
ethereum-primitives = { package = "serai-processor-ethereum-primitives", path = "../primitives", default-features = false }
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ use alloy_sol_types::{SolInterface, SolEvent};
|
||||
|
||||
use alloy_rpc_types_eth::{Log, Filter, TransactionTrait};
|
||||
use alloy_transport::{TransportErrorKind, RpcError};
|
||||
use alloy_simple_request_transport::SimpleRequest;
|
||||
use alloy_provider::{Provider, RootProvider};
|
||||
|
||||
use ethereum_primitives::LogIndex;
|
||||
@@ -94,7 +93,7 @@ impl Erc20 {
|
||||
// Yielding THE top-level transfer would require tracing the transaction execution and isn't
|
||||
// worth the effort.
|
||||
async fn top_level_transfer(
|
||||
provider: &RootProvider<SimpleRequest>,
|
||||
provider: &RootProvider,
|
||||
erc20: Address,
|
||||
transaction_hash: [u8; 32],
|
||||
transfer_logs: &[Log],
|
||||
@@ -112,15 +111,13 @@ impl Erc20 {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Don't validate the encoding as this can't be re-encoded to an identical bytestring due
|
||||
// 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()) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// Extract the top-level call's from/to/value
|
||||
let (from, to, value) = match call {
|
||||
IERC20Calls::transfer(transferCall { to, value }) => (transaction.from, to, value),
|
||||
IERC20Calls::transfer(transferCall { to, value }) => (transaction.inner.signer(), to, value),
|
||||
IERC20Calls::transferFrom(transferFromCall { from, to, value }) => (from, to, value),
|
||||
// Treat any other function selectors as unrecognized
|
||||
_ => return Ok(None),
|
||||
@@ -149,7 +146,7 @@ impl Erc20 {
|
||||
}
|
||||
|
||||
// Read the data appended after
|
||||
let data = if let Ok(call) = SeraiIERC20Calls::abi_decode(transaction.inner.input(), true) {
|
||||
let data = if let Ok(call) = SeraiIERC20Calls::abi_decode(transaction.inner.input()) {
|
||||
match call {
|
||||
SeraiIERC20Calls::transferWithInInstruction01BB244A8A(
|
||||
transferWithInInstructionCall { inInstruction, .. },
|
||||
@@ -180,7 +177,7 @@ impl Erc20 {
|
||||
///
|
||||
/// The `transfers` in the result are unordered. The `logs` are sorted by index.
|
||||
pub async fn top_level_transfers_unordered(
|
||||
provider: &RootProvider<SimpleRequest>,
|
||||
provider: &RootProvider,
|
||||
blocks: RangeInclusive<u64>,
|
||||
erc20: Address,
|
||||
to: Address,
|
||||
|
||||
@@ -12,4 +12,30 @@ fn selector_collisions() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn abi_decode_panic() {
|
||||
use alloy_sol_types::SolInterface;
|
||||
|
||||
/*
|
||||
The following code panics with alloy-core 0.8, when the validate flag (commented out) is set to
|
||||
`false`. This flag was removed with alloy-core 1.0, leaving the default behavior of
|
||||
`abi_decode` to be `validate = false`. This test was added to ensure when we removed our
|
||||
practice of `validate = true`, we didn't open ourselves up this as a DoS risk.
|
||||
*/
|
||||
assert!(crate::SeraiIERC20Calls::abi_decode(
|
||||
&alloy_core::primitives::hex::decode(concat!(
|
||||
"a9059cbb",
|
||||
"0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"000000000000000000000000000000000000000000000000000000000000006f",
|
||||
"ffffffffff000000000000000000000000000000000000000000000000000023",
|
||||
"000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
"ffffff0000000000000000000000000000000000000000000000000000000000",
|
||||
))
|
||||
.unwrap(),
|
||||
// false
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
// This is primarily tested via serai-processor-ethereum-router
|
||||
|
||||
@@ -22,5 +22,5 @@ borsh = { version = "1", default-features = false, features = ["std", "derive",
|
||||
group = { version = "0.13", default-features = false }
|
||||
k256 = { version = "^0.13.1", default-features = false, features = ["std", "arithmetic"] }
|
||||
|
||||
alloy-primitives = { version = "0.8", default-features = false }
|
||||
alloy-consensus = { version = "0.9", default-features = false, features = ["k256"] }
|
||||
alloy-primitives = { version = "1", default-features = false }
|
||||
alloy-consensus = { version = "0.14", default-features = false, features = ["k256"] }
|
||||
|
||||
@@ -7,7 +7,7 @@ use ::borsh::{BorshSerialize, BorshDeserialize};
|
||||
use group::ff::PrimeField;
|
||||
use k256::Scalar;
|
||||
|
||||
use alloy_primitives::PrimitiveSignature;
|
||||
use alloy_primitives::Signature;
|
||||
use alloy_consensus::{SignableTransaction, Signed, TxLegacy};
|
||||
|
||||
mod borsh;
|
||||
@@ -68,8 +68,7 @@ pub fn deterministically_sign(tx: TxLegacy) -> Signed<TxLegacy> {
|
||||
let s = Scalar::ONE;
|
||||
let r_bytes: [u8; 32] = r.to_repr().into();
|
||||
let s_bytes: [u8; 32] = s.to_repr().into();
|
||||
let signature =
|
||||
PrimitiveSignature::from_scalars_and_parity(r_bytes.into(), s_bytes.into(), false);
|
||||
let signature = Signature::from_scalars_and_parity(r_bytes.into(), s_bytes.into(), false);
|
||||
|
||||
let res = tx.into_signed(signature);
|
||||
debug_assert!(res.recover_signer().is_ok());
|
||||
|
||||
@@ -22,19 +22,18 @@ borsh = { version = "1", default-features = false, features = ["std", "derive",
|
||||
group = { version = "0.13", default-features = false }
|
||||
k256 = { version = "0.13", default-features = false, features = ["std", "arithmetic"] }
|
||||
|
||||
alloy-core = { version = "0.8", default-features = false }
|
||||
alloy-core = { version = "1", default-features = false }
|
||||
|
||||
alloy-sol-types = { version = "0.8", default-features = false }
|
||||
alloy-sol-macro = { version = "0.8", default-features = false }
|
||||
alloy-sol-types = { version = "1", default-features = false }
|
||||
alloy-sol-macro = { version = "1", default-features = false }
|
||||
|
||||
alloy-consensus = { version = "0.9", default-features = false }
|
||||
alloy-consensus = { version = "0.14", default-features = false }
|
||||
|
||||
alloy-rpc-types-eth = { version = "0.9", default-features = false }
|
||||
alloy-transport = { version = "0.9", default-features = false }
|
||||
alloy-simple-request-transport = { path = "../../../networks/ethereum/alloy-simple-request-transport", default-features = false }
|
||||
alloy-provider = { version = "0.9", default-features = false }
|
||||
alloy-rpc-types-eth = { version = "0.14", default-features = false }
|
||||
alloy-transport = { version = "0.14", default-features = false }
|
||||
alloy-provider = { version = "0.14", default-features = false }
|
||||
|
||||
revm = { version = "19", default-features = false, features = ["std"] }
|
||||
revm = { version = "22", default-features = false, features = ["std"] }
|
||||
|
||||
ethereum-schnorr = { package = "ethereum-schnorr-contract", path = "../../../networks/ethereum/schnorr", default-features = false }
|
||||
|
||||
@@ -52,18 +51,19 @@ build-solidity-contracts = { path = "../../../networks/ethereum/build-contracts"
|
||||
|
||||
syn = { version = "2", default-features = false, features = ["proc-macro"] }
|
||||
|
||||
syn-solidity = { version = "0.8", default-features = false }
|
||||
alloy-sol-macro-input = { version = "0.8", default-features = false }
|
||||
alloy-sol-macro-expander = { version = "0.8", default-features = false }
|
||||
syn-solidity = { version = "1", default-features = false }
|
||||
alloy-sol-macro-input = { version = "1", default-features = false }
|
||||
alloy-sol-macro-expander = { version = "1", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
rand_core = { version = "0.6", default-features = false, features = ["std"] }
|
||||
|
||||
k256 = { version = "0.13", default-features = false, features = ["std"] }
|
||||
|
||||
alloy-provider = { version = "0.9", default-features = false, features = ["debug-api", "trace-api"] }
|
||||
alloy-rpc-client = { version = "0.9", default-features = false }
|
||||
alloy-node-bindings = { version = "0.9", default-features = false }
|
||||
alloy-simple-request-transport = { path = "../../../networks/ethereum/alloy-simple-request-transport", default-features = false }
|
||||
alloy-provider = { version = "0.14", default-features = false, features = ["debug-api", "trace-api"] }
|
||||
alloy-rpc-client = { version = "0.14", default-features = false }
|
||||
alloy-node-bindings = { version = "0.14", default-features = false }
|
||||
|
||||
tokio = { version = "1.0", default-features = false, features = ["rt-multi-thread", "macros"] }
|
||||
|
||||
|
||||
@@ -1,26 +1,130 @@
|
||||
use core::convert::Infallible;
|
||||
|
||||
use k256::{Scalar, ProjectivePoint};
|
||||
|
||||
use alloy_core::primitives::{Address, U160, U256};
|
||||
use alloy_core::primitives::{Address, U256, Bytes};
|
||||
use alloy_sol_types::SolCall;
|
||||
|
||||
use revm::{
|
||||
primitives::*,
|
||||
interpreter::{gas::*, opcode::InstructionTables, *},
|
||||
db::{emptydb::EmptyDB, in_memory_db::InMemoryDB},
|
||||
Handler, Context, EvmBuilder, Evm,
|
||||
primitives::hardfork::SpecId,
|
||||
bytecode::Bytecode,
|
||||
state::AccountInfo,
|
||||
database::{empty_db::EmptyDB, in_memory_db::InMemoryDB},
|
||||
interpreter::{
|
||||
gas::calculate_initial_tx_gas,
|
||||
interpreter_action::{CallInputs, CallOutcome},
|
||||
interpreter::EthInterpreter,
|
||||
Interpreter,
|
||||
},
|
||||
handler::{
|
||||
instructions::EthInstructions, PrecompileProvider, EthPrecompiles, EthFrame, MainnetHandler,
|
||||
},
|
||||
context::{
|
||||
result::{EVMError, InvalidTransaction, ExecutionResult},
|
||||
evm::{EvmData, Evm},
|
||||
context::Context,
|
||||
*,
|
||||
},
|
||||
inspector::{Inspector, InspectorHandler},
|
||||
};
|
||||
|
||||
use ethereum_schnorr::{PublicKey, Signature};
|
||||
|
||||
use crate::*;
|
||||
|
||||
// The specification this uses
|
||||
const SPEC_ID: SpecId = SpecId::CANCUN;
|
||||
|
||||
// The chain ID used for gas estimation
|
||||
const CHAIN_ID: U256 = U256::from_be_slice(&[1]);
|
||||
|
||||
type RevmContext = Context<BlockEnv, TxEnv, CfgEnv, InMemoryDB, Journal<InMemoryDB>, ()>;
|
||||
|
||||
fn precompiles() -> EthPrecompiles {
|
||||
let mut precompiles = EthPrecompiles::default();
|
||||
PrecompileProvider::<RevmContext>::set_spec(&mut precompiles, SPEC_ID);
|
||||
precompiles
|
||||
}
|
||||
|
||||
/*
|
||||
Instead of attempting to solve the halting problem, we assume all CALLs take the worst-case
|
||||
amount of gas (as we do have bounds on the gas they're allowed to take). This assumption is
|
||||
implemented via an revm Inspector.
|
||||
|
||||
The Inspector is allowed to override the CALL directly. We don't do this due to the amount of
|
||||
side effects a CALL has. Instead, we override the result.
|
||||
|
||||
In the case the ERC20 is called, we additionally have it return `true` (as expected for compliant
|
||||
ERC20s, and as will trigger the worst-case gas consumption by the Router itself). This is done by
|
||||
hooking `call_end`.
|
||||
*/
|
||||
pub(crate) struct WorstCaseCallInspector {
|
||||
erc20: Option<Address>,
|
||||
call_depth: usize,
|
||||
unused_gas: u64,
|
||||
override_immediate_call_return_value: bool,
|
||||
}
|
||||
impl Inspector<RevmContext> for WorstCaseCallInspector {
|
||||
fn call(&mut self, _context: &mut RevmContext, _inputs: &mut CallInputs) -> Option<CallOutcome> {
|
||||
self.call_depth += 1;
|
||||
// Don't override the CALL immediately for prior described reasons
|
||||
None
|
||||
}
|
||||
|
||||
fn call_end(
|
||||
&mut self,
|
||||
_context: &mut RevmContext,
|
||||
inputs: &CallInputs,
|
||||
outcome: &mut CallOutcome,
|
||||
) {
|
||||
self.call_depth -= 1;
|
||||
|
||||
/*
|
||||
Mark the amount of gas left unused, for us to later assume will be used in practice.
|
||||
|
||||
This only runs if the call-depth is 1 (so only the Router-made calls have their gas so
|
||||
tracked), and if it's not to a precompile. This latter condition isn't solely because we can
|
||||
perfectly model precompiles (which wouldn't be worth the complexity) yet because the Router
|
||||
does call precompiles (ecrecover) and accordingly has to model the gas of that correctly.
|
||||
*/
|
||||
if (self.call_depth == 1) && (!precompiles().contains(&inputs.target_address)) {
|
||||
let unused_gas = inputs.gas_limit - outcome.result.gas.spent();
|
||||
self.unused_gas += unused_gas;
|
||||
|
||||
// Now that the CALL is over, flag we should normalize the values on the stack
|
||||
self.override_immediate_call_return_value = true;
|
||||
}
|
||||
|
||||
// If ERC20, provide the expected ERC20 return data
|
||||
if Some(inputs.target_address) == self.erc20 {
|
||||
outcome.result.output = true.abi_encode().into();
|
||||
}
|
||||
}
|
||||
|
||||
fn step(&mut self, interpreter: &mut Interpreter, _context: &mut RevmContext) {
|
||||
if self.override_immediate_call_return_value {
|
||||
// We fix this result to having succeeded, which triggers the most-expensive pathing within
|
||||
// the Router contract itself (some paths return early if a CALL fails)
|
||||
let return_value = interpreter.stack.pop().unwrap();
|
||||
assert!((return_value == U256::ZERO) || (return_value == U256::ONE));
|
||||
assert!(
|
||||
interpreter.stack.push(U256::ONE),
|
||||
"stack capacity couldn't fit item after popping an item"
|
||||
);
|
||||
self.override_immediate_call_return_value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The object used for estimating gas.
|
||||
///
|
||||
/// Due to `execute` heavily branching, we locally simulate calls with revm.
|
||||
pub(crate) type GasEstimator = Evm<'static, (), InMemoryDB>;
|
||||
pub(crate) type GasEstimator = Evm<
|
||||
RevmContext,
|
||||
WorstCaseCallInspector,
|
||||
EthInstructions<EthInterpreter, RevmContext>,
|
||||
EthPrecompiles,
|
||||
>;
|
||||
|
||||
impl Router {
|
||||
const SMART_CONTRACT_NONCE_STORAGE_SLOT: U256 = U256::from_be_slice(&[0]);
|
||||
@@ -114,114 +218,26 @@ impl Router {
|
||||
db
|
||||
};
|
||||
|
||||
// Create a custom handler so we can assume every CALL is the worst-case
|
||||
let handler = {
|
||||
let mut instructions = InstructionTables::<'_, _>::new_plain::<CancunSpec>();
|
||||
instructions.update_boxed(revm::interpreter::opcode::CALL, {
|
||||
move |call_op, interpreter, host: &mut Context<_, _>| {
|
||||
let (address_called, value, return_addr, return_len) = {
|
||||
let stack = &mut interpreter.stack;
|
||||
|
||||
let address = stack.peek(1).unwrap();
|
||||
let value = stack.peek(2).unwrap();
|
||||
let return_addr = stack.peek(5).unwrap();
|
||||
let return_len = stack.peek(6).unwrap();
|
||||
|
||||
(
|
||||
address,
|
||||
value,
|
||||
usize::try_from(return_addr).unwrap(),
|
||||
usize::try_from(return_len).unwrap(),
|
||||
)
|
||||
};
|
||||
let address_called =
|
||||
Address::from(U160::from_be_slice(&address_called.to_be_bytes::<32>()[12 ..]));
|
||||
|
||||
// Have the original call op incur its costs as programmed
|
||||
call_op(interpreter, host);
|
||||
|
||||
/*
|
||||
Unfortunately, the call opcode executed only sets itself up, it doesn't handle the
|
||||
entire inner call for us. We manually do so here by shimming the intended result. The
|
||||
other option, on this path chosen, would be to shim the call-frame execution ourselves
|
||||
and only then manipulate the result.
|
||||
|
||||
Ideally, we wouldn't override CALL, yet STOP/RETURN (the tail of the CALL) to avoid all
|
||||
of this. Those overrides weren't being successfully hit in initial experiments, and
|
||||
while this solution does appear overly complicated, it's sufficiently tested to justify
|
||||
itself.
|
||||
|
||||
revm does cost the entire gas limit during the call setup. After the call completes,
|
||||
it refunds whatever was unused. Since we manually complete the call here ourselves,
|
||||
but don't implement that refund logic as we want the worst-case scenario, we do
|
||||
successfully implement complete costing of the gas limit.
|
||||
*/
|
||||
|
||||
// Perform the call value transfer, which also marks the recipient as warm
|
||||
assert!(host
|
||||
.evm
|
||||
.inner
|
||||
.journaled_state
|
||||
.transfer(
|
||||
&interpreter.contract.target_address,
|
||||
&address_called,
|
||||
value,
|
||||
&mut host.evm.inner.db
|
||||
)
|
||||
.unwrap()
|
||||
.is_none());
|
||||
|
||||
// Clear the call-to-be
|
||||
debug_assert!(matches!(interpreter.next_action, InterpreterAction::Call { .. }));
|
||||
interpreter.next_action = InterpreterAction::None;
|
||||
interpreter.instruction_result = InstructionResult::Continue;
|
||||
|
||||
// Clear the existing return data
|
||||
interpreter.return_data_buffer.clear();
|
||||
|
||||
/*
|
||||
If calling an ERC20, trigger the return data's worst-case by returning `true`
|
||||
(as expected by compliant ERC20s). Else return none, as we expect none or won't bother
|
||||
copying/decoding the return data.
|
||||
|
||||
This doesn't affect calls to ecrecover as those use STATICCALL and this overrides CALL
|
||||
alone.
|
||||
*/
|
||||
if Some(address_called) == erc20 {
|
||||
interpreter.return_data_buffer = true.abi_encode().into();
|
||||
}
|
||||
// Also copy the return data into memory
|
||||
let return_len = return_len.min(interpreter.return_data_buffer.len());
|
||||
let needed_memory_size = return_addr + return_len;
|
||||
if interpreter.shared_memory.len() < needed_memory_size {
|
||||
assert!(interpreter.resize_memory(needed_memory_size));
|
||||
}
|
||||
interpreter
|
||||
.shared_memory
|
||||
.slice_mut(return_addr, return_len)
|
||||
.copy_from_slice(&interpreter.return_data_buffer[.. return_len]);
|
||||
|
||||
// Finally, push the result of the call onto the stack
|
||||
interpreter.stack.push(U256::from(1)).unwrap();
|
||||
}
|
||||
});
|
||||
let mut handler = Handler::mainnet::<CancunSpec>();
|
||||
handler.set_instruction_table(instructions);
|
||||
|
||||
handler
|
||||
};
|
||||
|
||||
EvmBuilder::default()
|
||||
.with_db(db)
|
||||
.with_handler(handler)
|
||||
.modify_cfg_env(|cfg| {
|
||||
cfg.chain_id = CHAIN_ID.try_into().unwrap();
|
||||
})
|
||||
.modify_tx_env(|tx| {
|
||||
tx.gas_limit = u64::MAX;
|
||||
tx.transact_to = self.address.into();
|
||||
})
|
||||
.build()
|
||||
Evm {
|
||||
data: EvmData {
|
||||
ctx: RevmContext::new(db, SPEC_ID)
|
||||
.modify_cfg_chained(|cfg| {
|
||||
cfg.chain_id = CHAIN_ID.try_into().unwrap();
|
||||
})
|
||||
.modify_tx_chained(|tx: &mut TxEnv| {
|
||||
tx.gas_limit = u64::MAX;
|
||||
tx.kind = self.address.into();
|
||||
}),
|
||||
inspector: WorstCaseCallInspector {
|
||||
erc20,
|
||||
call_depth: 0,
|
||||
unused_gas: 0,
|
||||
override_immediate_call_return_value: false,
|
||||
},
|
||||
},
|
||||
instruction: EthInstructions::default(),
|
||||
precompiles: precompiles(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The worst-case gas cost for a legacy transaction which executes this batch.
|
||||
@@ -242,11 +258,10 @@ impl Router {
|
||||
let fee = U256::from(1);
|
||||
|
||||
// Set a balance of the amount sent out to ensure we don't error on that premise
|
||||
{
|
||||
let db = gas_estimator.db_mut();
|
||||
gas_estimator.data.ctx.modify_db(|db| {
|
||||
let account = db.load_account(self.address).unwrap();
|
||||
account.info.balance = fee + outs.0.iter().map(|out| out.amount).sum::<U256>();
|
||||
}
|
||||
});
|
||||
|
||||
fee
|
||||
}
|
||||
@@ -271,8 +286,7 @@ impl Router {
|
||||
consistent use of nonce #1 shows storage read/writes aren't being persisted. They're solely
|
||||
returned upon execution in a `state` field we ignore.
|
||||
*/
|
||||
{
|
||||
let tx = gas_estimator.tx_mut();
|
||||
gas_estimator.data.ctx.modify_tx(|tx| {
|
||||
tx.caller = Address::from({
|
||||
/*
|
||||
We assume the transaction sender is not the destination of any `OutInstruction`, making
|
||||
@@ -296,25 +310,35 @@ impl Router {
|
||||
))
|
||||
.abi_encode()
|
||||
.into();
|
||||
}
|
||||
});
|
||||
|
||||
// Execute the transaction
|
||||
let mut gas = match gas_estimator.transact().unwrap().result {
|
||||
let mut gas = match MainnetHandler::<
|
||||
_,
|
||||
EVMError<Infallible, InvalidTransaction>,
|
||||
EthFrame<_, _, _>,
|
||||
>::default()
|
||||
.inspect_run(&mut gas_estimator)
|
||||
.unwrap()
|
||||
.result
|
||||
{
|
||||
ExecutionResult::Success { gas_used, gas_refunded, .. } => {
|
||||
assert_eq!(gas_refunded, 0);
|
||||
gas_used
|
||||
}
|
||||
res => panic!("estimated execute transaction failed: {res:?}"),
|
||||
};
|
||||
gas += gas_estimator.into_inspector().unused_gas;
|
||||
|
||||
// The transaction uses gas based on the amount of non-zero bytes in the calldata, which is
|
||||
// variable to the fee, which is variable to the gad used. This iterates until parity
|
||||
// variable to the fee, which is variable to the gas used. This iterates until parity
|
||||
let initial_gas = |fee, sig| {
|
||||
let gas = calculate_initial_tx_gas(
|
||||
SpecId::CANCUN,
|
||||
SPEC_ID,
|
||||
&abi::executeCall::new((sig, Address::from(coin), fee, outs.0.clone())).abi_encode(),
|
||||
false,
|
||||
&[],
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
assert_eq!(gas.floor_gas, 0);
|
||||
|
||||
@@ -19,7 +19,6 @@ use alloy_consensus::TxLegacy;
|
||||
|
||||
use alloy_rpc_types_eth::{BlockId, Log, Filter, TransactionInput, TransactionRequest};
|
||||
use alloy_transport::{TransportErrorKind, RpcError};
|
||||
use alloy_simple_request_transport::SimpleRequest;
|
||||
use alloy_provider::{Provider, RootProvider};
|
||||
|
||||
use scale::Encode;
|
||||
@@ -48,6 +47,7 @@ mod _irouter_abi {
|
||||
#[expect(warnings)]
|
||||
#[expect(needless_pass_by_value)]
|
||||
#[expect(clippy::all)]
|
||||
#[expect(clippy::unused_self)]
|
||||
#[expect(clippy::ignored_unit_patterns)]
|
||||
#[expect(clippy::redundant_closure_for_method_calls)]
|
||||
mod _router_abi {
|
||||
@@ -236,7 +236,7 @@ pub struct Escape {
|
||||
/// A view of the Router for Serai.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Router {
|
||||
provider: Arc<RootProvider<SimpleRequest>>,
|
||||
provider: Arc<RootProvider>,
|
||||
address: Address,
|
||||
empty_execute_gas: HashMap<Coin, u64>,
|
||||
}
|
||||
@@ -272,7 +272,7 @@ impl 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>>,
|
||||
provider: Arc<RootProvider>,
|
||||
initial_serai_key: &PublicKey,
|
||||
) -> Result<Option<Self>, RpcError<TransportErrorKind>> {
|
||||
let Some(deployer) = Deployer::new(provider.clone()).await? else {
|
||||
@@ -573,7 +573,7 @@ impl Router {
|
||||
if log.topics().first() != Some(&Transfer::SIGNATURE_HASH) {
|
||||
continue;
|
||||
}
|
||||
let Ok(transfer) = Transfer::decode_log(&log.inner.clone(), true) else { continue };
|
||||
let Ok(transfer) = Transfer::decode_log(&log.inner.clone()) else { continue };
|
||||
// Check if this aligns with the InInstruction
|
||||
if (transfer.from == in_instruction.from) &&
|
||||
(transfer.to == self.address) &&
|
||||
@@ -743,11 +743,11 @@ impl Router {
|
||||
) -> 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?;
|
||||
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)
|
||||
let res = abi::nextSeraiKeyCall::abi_decode_returns(&bytes)
|
||||
.map_err(|e| TransportErrorKind::Custom(format!("failed to decode key: {e:?}").into()))?;
|
||||
let eth_repr = <[u8; 32]>::from(res._0);
|
||||
let eth_repr = <[u8; 32]>::from(res);
|
||||
Ok(if eth_repr == [0; 32] {
|
||||
None
|
||||
} else {
|
||||
@@ -778,10 +778,10 @@ impl Router {
|
||||
let call = TransactionRequest::default()
|
||||
.to(self.address)
|
||||
.input(TransactionInput::new(abi::nextNonceCall::new(()).abi_encode().into()));
|
||||
let bytes = self.provider.call(&call).block(block).await?;
|
||||
let res = abi::nextNonceCall::abi_decode_returns(&bytes, true)
|
||||
let bytes = self.provider.call(call).block(block).await?;
|
||||
let res = abi::nextNonceCall::abi_decode_returns(&bytes)
|
||||
.map_err(|e| TransportErrorKind::Custom(format!("failed to decode nonce: {e:?}").into()))?;
|
||||
Ok(u64::try_from(res._0).map_err(|_| {
|
||||
Ok(u64::try_from(res).map_err(|_| {
|
||||
TransportErrorKind::Custom("nonce returned exceeded 2**64".to_string().into())
|
||||
})?)
|
||||
}
|
||||
@@ -794,10 +794,10 @@ impl Router {
|
||||
let call = TransactionRequest::default()
|
||||
.to(self.address)
|
||||
.input(TransactionInput::new(abi::escapedToCall::new(()).abi_encode().into()));
|
||||
let bytes = self.provider.call(&call).block(block).await?;
|
||||
let res = abi::escapedToCall::abi_decode_returns(&bytes, true).map_err(|e| {
|
||||
let bytes = self.provider.call(call).block(block).await?;
|
||||
let res = abi::escapedToCall::abi_decode_returns(&bytes).map_err(|e| {
|
||||
TransportErrorKind::Custom(format!("failed to decode the address escaped to: {e:?}").into())
|
||||
})?;
|
||||
Ok(if res._0 == Address([0; 20].into()) { None } else { Some(res._0) })
|
||||
Ok(if res == Address::ZERO { None } else { Some(res) })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use alloy_consensus::TxLegacy;
|
||||
use alloy_rpc_types_eth::{TransactionInput, TransactionRequest};
|
||||
use alloy_provider::Provider;
|
||||
|
||||
use revm::{primitives::SpecId, interpreter::gas::calculate_initial_tx_gas};
|
||||
use revm::{primitives::hardfork::SpecId, interpreter::gas::calculate_initial_tx_gas};
|
||||
|
||||
use crate::tests::Test;
|
||||
|
||||
@@ -65,13 +65,13 @@ async fn test_create_address() {
|
||||
let call =
|
||||
TransactionRequest::default().to(address).input(TransactionInput::new(input.clone().into()));
|
||||
assert_eq!(
|
||||
&test.provider.call(&call).await.unwrap().as_ref()[12 ..],
|
||||
&test.provider.call(call.clone()).await.unwrap().as_ref()[12 ..],
|
||||
address.create(nonce).as_slice(),
|
||||
);
|
||||
|
||||
// Check the function is constant-gas
|
||||
let gas_used = test.provider.estimate_gas(&call).await.unwrap();
|
||||
let initial_gas = calculate_initial_tx_gas(SpecId::CANCUN, &input, false, &[], 0).initial_gas;
|
||||
let gas_used = test.provider.estimate_gas(call).await.unwrap();
|
||||
let initial_gas = calculate_initial_tx_gas(SpecId::CANCUN, &input, false, 0, 0, 0).initial_gas;
|
||||
let this_call = gas_used - initial_gas;
|
||||
if gas.is_none() {
|
||||
gas = Some(this_call);
|
||||
|
||||
@@ -86,13 +86,13 @@ impl Erc20 {
|
||||
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()
|
||||
U256::abi_decode(&test.provider.call(call).await.unwrap()).unwrap()
|
||||
}
|
||||
|
||||
pub(crate) async fn router_approval(&self, test: &Test, account: Address) -> U256 {
|
||||
let call = TransactionRequest::default().to(self.0).input(TransactionInput::new(
|
||||
abi::TestERC20::allowanceCall::new((test.router.address(), account)).abi_encode().into(),
|
||||
));
|
||||
U256::abi_decode(&test.provider.call(&call).await.unwrap(), true).unwrap()
|
||||
U256::abi_decode(&test.provider.call(call).await.unwrap()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ struct CalldataAgnosticGas;
|
||||
impl CalldataAgnosticGas {
|
||||
#[must_use]
|
||||
fn calculate(input: &[u8], mut constant_zero_bytes: usize, gas_used: u64) -> u64 {
|
||||
use revm::{primitives::SpecId, interpreter::gas::calculate_initial_tx_gas};
|
||||
use revm::{primitives::hardfork::SpecId, interpreter::gas::calculate_initial_tx_gas};
|
||||
|
||||
let mut without_variable_zero_bytes = Vec::with_capacity(input.len());
|
||||
for byte in input {
|
||||
@@ -76,9 +76,9 @@ impl CalldataAgnosticGas {
|
||||
}
|
||||
}
|
||||
gas_used +
|
||||
(calculate_initial_tx_gas(SpecId::CANCUN, &without_variable_zero_bytes, false, &[], 0)
|
||||
(calculate_initial_tx_gas(SpecId::CANCUN, &without_variable_zero_bytes, false, 0, 0, 0)
|
||||
.initial_gas -
|
||||
calculate_initial_tx_gas(SpecId::CANCUN, input, false, &[], 0).initial_gas)
|
||||
calculate_initial_tx_gas(SpecId::CANCUN, input, false, 0, 0, 0).initial_gas)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ struct RouterState {
|
||||
struct Test {
|
||||
#[allow(unused)]
|
||||
anvil: AnvilInstance,
|
||||
provider: Arc<RootProvider<SimpleRequest>>,
|
||||
provider: Arc<RootProvider>,
|
||||
chain_id: U256,
|
||||
router: Router,
|
||||
state: RouterState,
|
||||
@@ -173,8 +173,8 @@ impl Test {
|
||||
let call = TransactionRequest::default()
|
||||
.to(self.router.address())
|
||||
.input(TransactionInput::new(tx.input));
|
||||
let call_err = self.provider.call(&call).await.unwrap_err();
|
||||
call_err.as_error_resp().unwrap().as_decoded_error::<IRouterErrors>(true).unwrap()
|
||||
let call_err = self.provider.call(call).await.unwrap_err();
|
||||
call_err.as_error_resp().unwrap().as_decoded_interface_error::<IRouterErrors>().unwrap()
|
||||
}
|
||||
|
||||
fn confirm_next_serai_key_tx(&self) -> TxLegacy {
|
||||
@@ -574,11 +574,11 @@ async fn test_empty_execute() {
|
||||
TransactionRequest::default().to(token).input(TransactionInput::new(vec![].into()));
|
||||
// Check it returns the expected result
|
||||
assert_eq!(
|
||||
test.provider.call(&call).await.unwrap().as_ref(),
|
||||
test.provider.call(call.clone()).await.unwrap().as_ref(),
|
||||
U256::from(1).abi_encode().as_slice()
|
||||
);
|
||||
// Check it has the expected gas cost (16 is documented in `return_true_code`)
|
||||
assert_eq!(test.provider.estimate_gas(&call).await.unwrap(), 21_000 + 16);
|
||||
assert_eq!(test.provider.estimate_gas(call).await.unwrap(), 21_000 + 16);
|
||||
}
|
||||
|
||||
let gas = test.router.execute_gas(Coin::Erc20(token), U256::from(0), &[].as_slice().into());
|
||||
@@ -700,6 +700,35 @@ async fn test_eth_code_out_instruction() {
|
||||
assert_eq!(test.provider.get_code_at(deployed).await.unwrap().to_vec(), true.abi_encode());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_eth_code_out_instruction_reverts() {
|
||||
let mut test = Test::new().await;
|
||||
test.confirm_next_serai_key().await;
|
||||
let () = test
|
||||
.provider
|
||||
.raw_request("anvil_setBalance".into(), (test.router.address(), 1_000_000))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// [REVERT], which will cause `executeArbitraryCode`'s call to CREATE to fail
|
||||
let code = vec![0xfd];
|
||||
let amount_out = U256::from(0);
|
||||
let out_instructions = OutInstructions::from(
|
||||
[(
|
||||
SeraiEthereumAddress::Contract(ContractDeployment::new(50_000, code.clone()).unwrap()),
|
||||
amount_out,
|
||||
)]
|
||||
.as_slice(),
|
||||
);
|
||||
|
||||
let gas = test.router.execute_gas(Coin::Ether, U256::from(1), &out_instructions);
|
||||
let fee = U256::from(gas);
|
||||
let (tx, gas_used) = test.execute(Coin::Ether, fee, out_instructions, vec![true]).await;
|
||||
|
||||
let unused_gas = test.gas_unused_by_calls(&tx).await;
|
||||
assert_eq!(gas_used + unused_gas, gas);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_erc20_code_out_instruction() {
|
||||
let mut test = Test::new().await;
|
||||
|
||||
@@ -4,7 +4,6 @@ use std::sync::Arc;
|
||||
use alloy_rlp::Encodable;
|
||||
|
||||
use alloy_transport::{TransportErrorKind, RpcError};
|
||||
use alloy_simple_request_transport::SimpleRequest;
|
||||
use alloy_provider::RootProvider;
|
||||
|
||||
use tokio::{
|
||||
@@ -26,13 +25,13 @@ use crate::{
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct TransactionPublisher<D: Db> {
|
||||
db: D,
|
||||
rpc: Arc<RootProvider<SimpleRequest>>,
|
||||
rpc: Arc<RootProvider>,
|
||||
router: Arc<RwLock<Option<Router>>>,
|
||||
relayer_url: String,
|
||||
}
|
||||
|
||||
impl<D: Db> TransactionPublisher<D> {
|
||||
pub(crate) fn new(db: D, rpc: Arc<RootProvider<SimpleRequest>>, relayer_url: String) -> Self {
|
||||
pub(crate) fn new(db: D, rpc: Arc<RootProvider>, relayer_url: String) -> Self {
|
||||
Self { db, rpc, router: Arc::new(RwLock::new(None)), relayer_url }
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,8 @@ use core::future::Future;
|
||||
use std::{sync::Arc, collections::HashSet};
|
||||
|
||||
use alloy_core::primitives::B256;
|
||||
use alloy_rpc_types_eth::{Header, BlockTransactionsKind, BlockNumberOrTag};
|
||||
use alloy_rpc_types_eth::{Header, BlockNumberOrTag};
|
||||
use alloy_transport::{RpcError, TransportErrorKind};
|
||||
use alloy_simple_request_transport::SimpleRequest;
|
||||
use alloy_provider::{Provider, RootProvider};
|
||||
|
||||
use serai_client::primitives::{ExternalNetworkId, ExternalCoin, Amount};
|
||||
@@ -26,7 +25,7 @@ use crate::{
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Rpc<D: Db> {
|
||||
pub(crate) db: D,
|
||||
pub(crate) provider: Arc<RootProvider<SimpleRequest>>,
|
||||
pub(crate) provider: Arc<RootProvider>,
|
||||
}
|
||||
|
||||
impl<D: Db> ScannerFeed for Rpc<D> {
|
||||
@@ -49,7 +48,7 @@ impl<D: Db> ScannerFeed for Rpc<D> {
|
||||
async move {
|
||||
let actual_number = self
|
||||
.provider
|
||||
.get_block(BlockNumberOrTag::Finalized.into(), BlockTransactionsKind::Hashes)
|
||||
.get_block(BlockNumberOrTag::Finalized.into())
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
TransportErrorKind::Custom("there was no finalized block".to_string().into())
|
||||
@@ -77,7 +76,7 @@ impl<D: Db> ScannerFeed for Rpc<D> {
|
||||
async move {
|
||||
let header = self
|
||||
.provider
|
||||
.get_block(BlockNumberOrTag::Number(number).into(), BlockTransactionsKind::Hashes)
|
||||
.get_block(BlockNumberOrTag::Number(number).into())
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
TransportErrorKind::Custom(
|
||||
@@ -105,7 +104,7 @@ impl<D: Db> ScannerFeed for Rpc<D> {
|
||||
} else {
|
||||
self
|
||||
.provider
|
||||
.get_block((start - 1).into(), BlockTransactionsKind::Hashes)
|
||||
.get_block((start - 1).into())
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
TransportErrorKind::Custom(
|
||||
@@ -120,7 +119,7 @@ impl<D: Db> ScannerFeed for Rpc<D> {
|
||||
|
||||
let end_header = self
|
||||
.provider
|
||||
.get_block((start + 31).into(), BlockTransactionsKind::Hashes)
|
||||
.get_block((start + 31).into())
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
TransportErrorKind::Custom(
|
||||
@@ -177,7 +176,7 @@ impl<D: Db> ScannerFeed for Rpc<D> {
|
||||
while to_check != epoch.prior_end_hash {
|
||||
let to_check_block = self
|
||||
.provider
|
||||
.get_block(B256::from(to_check).into(), BlockTransactionsKind::Hashes)
|
||||
.get_block(B256::from(to_check).into())
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
TransportErrorKind::Custom(
|
||||
|
||||
@@ -19,11 +19,10 @@ workspace = true
|
||||
[dependencies]
|
||||
k256 = { version = "0.13", default-features = false, features = ["std"] }
|
||||
|
||||
alloy-core = { version = "0.8", default-features = false }
|
||||
alloy-consensus = { version = "0.9", default-features = false, features = ["std"] }
|
||||
alloy-core = { version = "1", default-features = false }
|
||||
alloy-consensus = { version = "0.14", default-features = false, features = ["std"] }
|
||||
|
||||
alloy-rpc-types-eth = { version = "0.9", default-features = false }
|
||||
alloy-simple-request-transport = { path = "../../../networks/ethereum/alloy-simple-request-transport", default-features = false }
|
||||
alloy-provider = { version = "0.9", default-features = false }
|
||||
alloy-rpc-types-eth = { version = "0.14", default-features = false }
|
||||
alloy-provider = { version = "0.14", default-features = false }
|
||||
|
||||
ethereum-primitives = { package = "serai-processor-ethereum-primitives", path = "../primitives", default-features = false }
|
||||
|
||||
@@ -5,13 +5,12 @@
|
||||
use k256::{elliptic_curve::sec1::ToEncodedPoint, ProjectivePoint};
|
||||
|
||||
use alloy_core::{
|
||||
primitives::{Address, U256, Bytes, PrimitiveSignature, TxKind},
|
||||
primitives::{Address, U256, Bytes, Signature, TxKind},
|
||||
hex::FromHex,
|
||||
};
|
||||
use alloy_consensus::{SignableTransaction, TxLegacy, Signed};
|
||||
|
||||
use alloy_rpc_types_eth::TransactionReceipt;
|
||||
use alloy_simple_request_transport::SimpleRequest;
|
||||
use alloy_provider::{Provider, RootProvider};
|
||||
|
||||
use ethereum_primitives::{keccak256, deterministically_sign};
|
||||
@@ -24,7 +23,7 @@ fn address(point: &ProjectivePoint) -> [u8; 20] {
|
||||
}
|
||||
|
||||
/// Fund an account.
|
||||
pub async fn fund_account(provider: &RootProvider<SimpleRequest>, address: Address, value: U256) {
|
||||
pub async fn fund_account(provider: &RootProvider, address: Address, value: U256) {
|
||||
let _: () = provider
|
||||
.raw_request("anvil_setBalance".into(), [address.to_string(), value.to_string()])
|
||||
.await
|
||||
@@ -32,10 +31,7 @@ pub async fn fund_account(provider: &RootProvider<SimpleRequest>, address: Addre
|
||||
}
|
||||
|
||||
/// Publish an already-signed transaction.
|
||||
pub async fn publish_tx(
|
||||
provider: &RootProvider<SimpleRequest>,
|
||||
tx: Signed<TxLegacy>,
|
||||
) -> TransactionReceipt {
|
||||
pub async fn publish_tx(provider: &RootProvider, tx: Signed<TxLegacy>) -> TransactionReceipt {
|
||||
// Fund the sender's address
|
||||
fund_account(
|
||||
provider,
|
||||
@@ -55,7 +51,7 @@ pub async fn publish_tx(
|
||||
///
|
||||
/// The contract deployment will be done by a random account.
|
||||
pub async fn deploy_contract(
|
||||
provider: &RootProvider<SimpleRequest>,
|
||||
provider: &RootProvider,
|
||||
file_path: &str,
|
||||
constructor_arguments: &[u8],
|
||||
) -> Address {
|
||||
@@ -88,7 +84,7 @@ pub async fn deploy_contract(
|
||||
///
|
||||
/// This assumes the wallet is funded.
|
||||
pub async fn send(
|
||||
provider: &RootProvider<SimpleRequest>,
|
||||
provider: &RootProvider,
|
||||
wallet: &k256::ecdsa::SigningKey,
|
||||
mut tx: TxLegacy,
|
||||
) -> TransactionReceipt {
|
||||
@@ -111,7 +107,7 @@ pub async fn send(
|
||||
);
|
||||
|
||||
let mut bytes = vec![];
|
||||
tx.into_signed(PrimitiveSignature::from(sig)).eip2718_encode(&mut bytes);
|
||||
tx.into_signed(Signature::from(sig)).eip2718_encode(&mut bytes);
|
||||
let pending_tx = provider.send_raw_transaction(&bytes).await.unwrap();
|
||||
pending_tx.get_receipt().await.unwrap()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[toolchain]
|
||||
channel = "1.81"
|
||||
channel = "1.86"
|
||||
targets = ["wasm32-unknown-unknown"]
|
||||
profile = "minimal"
|
||||
components = ["rust-src", "rustfmt", "clippy"]
|
||||
|
||||
Reference in New Issue
Block a user