Skip to content

Commit

Permalink
feat(anvil): Fix ots_getInternalOperations (foundry-rs#6068)
Browse files Browse the repository at this point in the history
* feat(anvil): Fix `ots_getInternalOperations`

* Motivation

The otterscan `ots_getInternalOperations` was given incorrect values and
fully crashing when `SELFDESTRUCTS` where present.
The `type` field in the response for this endpoint is incorrectly serialized.

* Solution

Use the `MinedTransaction` instead of a parity traces to have more
granular controll on the specific transactions that we want to filter
out in the internal operations and the specific parameters that we want
to have access to.

Fix the serialization for the`type` field in the response.

* feat(anvil): fix create2 handling in `ots_getInternalOperations`
  • Loading branch information
ZePedroResende authored Oct 19, 2023
1 parent 490b588 commit 619f3c5
Show file tree
Hide file tree
Showing 6 changed files with 205 additions and 55 deletions.
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/anvil/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ async-trait = "0.1"

# misc
flate2 = "1.0"
serde_repr = "0.1"
serde_json.workspace = true
serde.workspace = true
thiserror = "1"
Expand Down
5 changes: 5 additions & 0 deletions crates/anvil/src/eth/backend/mem/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1813,6 +1813,11 @@ impl Backend {
self.blockchain.storage.read().transactions.get(&hash).map(|tx| tx.parity_traces())
}

/// Returns the traces for the given transaction
pub(crate) fn mined_transaction(&self, hash: H256) -> Option<MinedTransaction> {
self.blockchain.storage.read().transactions.get(&hash).cloned()
}

/// Returns the traces for the given block
pub(crate) fn mined_parity_trace_block(&self, block: u64) -> Option<Vec<Trace>> {
let block = self.get_block(block)?;
Expand Down
2 changes: 1 addition & 1 deletion crates/anvil/src/eth/otterscan/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ impl EthApi {
node_info!("ots_getInternalOperations");

self.backend
.mined_parity_trace_transaction(hash)
.mined_transaction(hash)
.map(OtsInternalOperation::batch_build)
.ok_or_else(|| BlockchainError::DataUnavailable)
}
Expand Down
91 changes: 39 additions & 52 deletions crates/anvil/src/eth/otterscan/types.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
use ethers::types::{
Action, Address, Block, Bytes, Call, CallType, Create, CreateResult, Res, Suicide, Trace,
Transaction, TransactionReceipt, H256, U256,
Action, Address, Block, Bytes, CallType, Trace, Transaction, TransactionReceipt, H256, U256,
};
use foundry_evm::{executor::InstructionResult, CallKind};
use futures::future::join_all;
use serde::{de::DeserializeOwned, Serialize};
use serde_repr::Serialize_repr;

use crate::eth::{
backend::mem::Backend,
backend::mem::{storage::MinedTransaction, Backend},
error::{BlockchainError, Result},
};

Expand Down Expand Up @@ -80,12 +81,13 @@ pub struct OtsInternalOperation {
}

/// Types of internal operations recognized by Otterscan
#[derive(Serialize, Debug, PartialEq)]
#[derive(Serialize_repr, Debug, PartialEq)]
#[repr(u8)]
pub enum OtsInternalOperationType {
Transfer = 0,
SelfDestruct = 1,
Create = 2,
// The spec asks for a Create2 entry as well, but we don't have that info
Create2 = 3,
}

/// Otterscan's representation of a trace
Expand Down Expand Up @@ -231,59 +233,44 @@ impl OtsSearchTransactions {
impl OtsInternalOperation {
/// Converts a batch of traces into a batch of internal operations, to comply with the spec for
/// [`ots_getInternalOperations`](https://github.com/otterscan/otterscan/blob/develop/docs/custom-jsonrpc.md#ots_getinternaloperations)
pub fn batch_build(traces: Vec<Trace>) -> Vec<OtsInternalOperation> {
pub fn batch_build(traces: MinedTransaction) -> Vec<OtsInternalOperation> {
traces
.info
.traces
.arena
.iter()
.filter_map(|trace| {
match (trace.action.clone(), trace.result.clone()) {
(Action::Call(Call { from, to, value, .. }), _) if !value.is_zero() => {
Some(Self { r#type: OtsInternalOperationType::Transfer, from, to, value })
}
(
Action::Create(Create { from, value, .. }),
Some(Res::Create(CreateResult { address, .. })),
) => Some(Self {
r#type: OtsInternalOperationType::Create,
from,
to: address,
value,
}),
(Action::Suicide(Suicide { address, .. }), _) => {
// this assumes a suicide trace always has a parent trace
let (from, value) =
Self::find_suicide_caller(&traces, &trace.trace_address).unwrap();

Some(Self {
r#type: OtsInternalOperationType::SelfDestruct,
from,
to: address,
value,
})
}
_ => None,
.filter_map(|node| match (node.kind(), node.status()) {
(CallKind::Call, _) if !node.trace.value.is_zero() => Some(Self {
r#type: OtsInternalOperationType::Transfer,
from: node.trace.caller,
to: node.trace.address,
value: node.trace.value,
}),
(CallKind::Create, _) => Some(Self {
r#type: OtsInternalOperationType::Create,
from: node.trace.caller,
to: node.trace.address,
value: node.trace.value,
}),
(CallKind::Create2, _) => Some(Self {
r#type: OtsInternalOperationType::Create2,
from: node.trace.caller,
to: node.trace.address,
value: node.trace.value,
}),
(_, InstructionResult::SelfDestruct) => {
Some(Self {
r#type: OtsInternalOperationType::SelfDestruct,
from: node.trace.address,
// the foundry CallTraceNode doesn't have a refund address
to: Default::default(),
value: node.trace.value,
})
}
_ => None,
})
.collect()
}

/// finds the trace that parents a given trace_address
fn find_suicide_caller(
traces: &Vec<Trace>,
suicide_address: &Vec<usize>,
) -> Option<(Address, U256)> {
traces.iter().find(|t| t.trace_address == suicide_address[..suicide_address.len() - 1]).map(
|t| match t.action {
Action::Call(Call { from, value, .. }) => (from, value),

Action::Create(Create { from, value, .. }) => (from, value),

// we assume here a suicice trace can never be parented by another suicide trace
Action::Suicide(_) => Self::find_suicide_caller(traces, &t.trace_address).unwrap(),

Action::Reward(_) => unreachable!(),
},
)
}
}

impl OtsTrace {
Expand Down
149 changes: 147 additions & 2 deletions crates/anvil/tests/it/otterscan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,153 @@ async fn can_call_ots_get_internal_operations_contract_deploy() {
);
}

#[tokio::test(flavor = "multi_thread")]
async fn can_call_ots_get_internal_operations_contract_trasfer() {
let (api, handle) = spawn(NodeConfig::test()).await;
let provider = handle.http_provider();

let accounts: Vec<_> = handle.dev_wallets().collect();
let from = accounts[0].address();
let to = accounts[1].address();
//let client = Arc::new(SignerMiddleware::new(provider, wallet));

let amount = handle.genesis_balance().checked_div(2u64.into()).unwrap();

let tx = TransactionRequest::new().to(to).value(amount).from(from);

let receipt = provider.send_transaction(tx, None).await.unwrap().await.unwrap().unwrap();

let res = api.ots_get_internal_operations(receipt.transaction_hash).await.unwrap();

assert_eq!(res.len(), 1);
assert_eq!(
res[0],
OtsInternalOperation {
r#type: OtsInternalOperationType::Transfer,
from,
to,
value: amount
}
);
}

#[tokio::test(flavor = "multi_thread")]
async fn can_call_ots_get_internal_operations_contract_create2() {
let prj = TempProject::dapptools().unwrap();
prj.add_source(
"Contract",
r"
pragma solidity 0.8.13;
contract Contract {
address constant CREATE2_DEPLOYER = 0x4e59b44847b379578588920cA78FbF26c0B4956C;
constructor() {}
function deploy() public {
uint256 salt = 0;
uint256 code = 0;
bytes memory creationCode = abi.encodePacked(code);
(bool success,) = address(CREATE2_DEPLOYER).call(abi.encodePacked(salt, creationCode));
require(success);
}
}
",
)
.unwrap();

let mut compiled = prj.compile().unwrap();
assert!(!compiled.has_compiler_errors());
let contract = compiled.remove_first("Contract").unwrap();
let (abi, bytecode, _) = contract.into_contract_bytecode().into_parts();

let (api, handle) = spawn(NodeConfig::test()).await;
let provider = handle.ws_provider().await;
let wallets = handle.dev_wallets().collect::<Vec<_>>();
let client = Arc::new(SignerMiddleware::new(provider, wallets[0].clone()));

// deploy successfully
let factory = ContractFactory::new(abi.clone().unwrap(), bytecode.unwrap(), client);
let contract = factory.deploy(()).unwrap().send().await.unwrap();

let contract = ContractInstance::new(
contract.address(),
abi.unwrap(),
SignerMiddleware::new(handle.http_provider(), wallets[1].clone()),
);
let call = contract.method::<_, ()>("deploy", ()).unwrap();

let receipt = call.send().await.unwrap().await.unwrap().unwrap();
dbg!(&receipt);

let res = api.ots_get_internal_operations(receipt.transaction_hash).await.unwrap();

assert_eq!(res.len(), 1);
assert_eq!(
res[0],
OtsInternalOperation {
r#type: OtsInternalOperationType::Create2,
from: Address::from_str("0x4e59b44847b379578588920cA78FbF26c0B4956C").unwrap(),
to: Address::from_str("0x347bcdad821abc09b8c275881b368de36476b62c").unwrap(),
value: 0.into()
}
);
}

#[tokio::test(flavor = "multi_thread")]
async fn can_call_ots_get_internal_operations_contract_selfdestruct() {
let prj = TempProject::dapptools().unwrap();
prj.add_source(
"Contract",
r"
pragma solidity 0.8.13;
contract Contract {
address payable private owner;
constructor() public {
owner = payable(msg.sender);
}
function goodbye() public {
selfdestruct(owner);
}
}
",
)
.unwrap();

let mut compiled = prj.compile().unwrap();
assert!(!compiled.has_compiler_errors());
let contract = compiled.remove_first("Contract").unwrap();
let (abi, bytecode, _) = contract.into_contract_bytecode().into_parts();

let (api, handle) = spawn(NodeConfig::test()).await;
let provider = handle.ws_provider().await;
let wallets = handle.dev_wallets().collect::<Vec<_>>();
let client = Arc::new(SignerMiddleware::new(provider, wallets[0].clone()));

// deploy successfully
let factory = ContractFactory::new(abi.clone().unwrap(), bytecode.unwrap(), client);
let contract = factory.deploy(()).unwrap().send().await.unwrap();

let contract = ContractInstance::new(
contract.address(),
abi.unwrap(),
SignerMiddleware::new(handle.http_provider(), wallets[1].clone()),
);
let call = contract.method::<_, ()>("goodbye", ()).unwrap();

let receipt = call.send().await.unwrap().await.unwrap().unwrap();

let res = api.ots_get_internal_operations(receipt.transaction_hash).await.unwrap();

assert_eq!(res.len(), 1);
assert_eq!(
res[0],
OtsInternalOperation {
r#type: OtsInternalOperationType::SelfDestruct,
from: contract.address(),
to: Default::default(),
value: 0.into()
}
);
}

#[tokio::test(flavor = "multi_thread")]
async fn can_call_ots_has_code() {
let (api, handle) = spawn(NodeConfig::test()).await;
Expand Down Expand Up @@ -238,8 +385,6 @@ contract Contract {
let call = contract.method::<_, ()>("trigger_revert", ()).unwrap().gas(150_000u64);
let receipt = call.send().await.unwrap().await.unwrap().unwrap();

let _block = api.block_by_number_full(BlockNumber::Latest).await.unwrap().unwrap();

let res = api.ots_get_transaction_error(receipt.transaction_hash).await.unwrap().unwrap();
assert_eq!(res, Bytes::from_str("0x8d6ea8be00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000012526576657274537472696e67466f6f4261720000000000000000000000000000").unwrap());
}
Expand Down

0 comments on commit 619f3c5

Please sign in to comment.