Skip to content

Commit

Permalink
Merge pull request #22 from danhper/trace-contract
Browse files Browse the repository at this point in the history
Trace contract
  • Loading branch information
danhper authored Sep 16, 2024
2 parents 7b2fde3 + c2e18ff commit 5395097
Show file tree
Hide file tree
Showing 12 changed files with 458 additions and 49 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

* Add `repl.fork` to start run and use an Anvil instance as a fork of the current URL
* Add `repl.startPrank` / `repl.stopPrank` to start/stop impersonating an address
* Add `FUNC.trace_call` method to contract functions

### Other changes

Expand Down
100 changes: 91 additions & 9 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ alloy = { version = "0.2.0", features = [
"signer-keystore",
"node-bindings",
"provider-anvil-api",
"provider-debug-api",
] }
itertools = "0.13.0"
rpassword = "7.3.1"
Expand All @@ -37,6 +38,7 @@ semver = "1.0.23"
shellexpand = { version = "3.1.0", features = ["path"] }
indexmap = "2.2.6"
lazy_static = "1.5.0"
textwrap = { version = "0.16.1", features = ["terminal_size"] }

[build-dependencies]
git2-rs = { version = "0.19.0", package = "git2", default-features = false }
Expand Down
1 change: 1 addition & 0 deletions docs/src/interacting_with_contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Sending a transaction requires an [account to be loaded](./account_management.md
The behavior can be changed by using one of the following method on the returned function object:

* `call`: Call the function and return the result
* `trace_call`: Same as call but also prints the trace of the call (also potentially shows better error messages)
* `send`: Sends a transaction to the function and return the result
* `encode`: ABI-encodes the function call

Expand Down
73 changes: 62 additions & 11 deletions src/interpreter/builtins/abi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,85 @@ use crate::interpreter::{
functions::{FunctionDef, FunctionParam, SyncMethod},
ContractInfo, Env, Type, Value,
};
use alloy::dyn_abi::{DynSolType, DynSolValue, JsonAbiExt};
use anyhow::{bail, Result};
use alloy::{
dyn_abi::{DynSolType, DynSolValue, JsonAbiExt},
json_abi::{self, JsonAbi},
primitives::FixedBytes,
};
use anyhow::{anyhow, bail, Result};
use lazy_static::lazy_static;

fn abi_decode_calldata(_env: &mut Env, receiver: &Value, args: &[Value]) -> Result<Value> {
trait Decodable: JsonAbiExt {
fn signature(&self) -> String;
fn selector(&self) -> FixedBytes<4>;
}

impl Decodable for json_abi::Function {
fn signature(&self) -> String {
json_abi::Function::signature(self)
}

fn selector(&self) -> FixedBytes<4> {
json_abi::Function::selector(self)
}
}
impl Decodable for json_abi::Error {
fn signature(&self) -> String {
json_abi::Error::signature(self)
}

fn selector(&self) -> FixedBytes<4> {
json_abi::Error::selector(self)
}
}

fn _generic_abi_decode<D: Decodable, F>(
receiver: &Value,
args: &[Value],
type_: &str,
get_options: F,
) -> Result<Value>
where
F: Fn(&JsonAbi) -> Vec<&D>,
{
let (name, abi) = match receiver {
Value::TypeObject(Type::Contract(ContractInfo(name, abi))) => (name, abi),
_ => bail!("decode function expects contract type as first argument"),
_ => bail!("decode {} expects contract type as first argument", type_),
};
let data = match args.first() {
Some(Value::Bytes(bytes)) => bytes,
_ => bail!("decode function expects bytes as argument"),
_ => bail!("decode {} expects bytes as argument", type_),
};
let selector = alloy::primitives::FixedBytes::<4>::from_slice(&data[..4]);
let function = abi
.functions()
let options = get_options(abi);
let error = options
.iter()
.find(|f| f.selector() == selector)
.ok_or(anyhow::anyhow!(
"function with selector {} not found for {}",
.ok_or(anyhow!(
"{} with selector {} not found for {}",
type_,
selector,
name
))?;
let decoded = function.abi_decode_input(&data[4..], true)?;
let decoded = error.abi_decode_input(&data[4..], true)?;
let values = decoded
.into_iter()
.map(Value::try_from)
.collect::<Result<Vec<_>>>()?;
Ok(Value::Tuple(vec![
Value::Str(function.signature()),
Value::Str(error.signature()),
Value::Tuple(values),
]))
}

fn abi_decode_calldata(_env: &mut Env, receiver: &Value, args: &[Value]) -> Result<Value> {
_generic_abi_decode(receiver, args, "function", |abi| abi.functions().collect())
}

fn abi_decode_error(_env: &mut Env, receiver: &Value, args: &[Value]) -> Result<Value> {
_generic_abi_decode(receiver, args, "error", |abi| abi.errors().collect())
}

fn value_to_soltype(value: &Value) -> Result<DynSolType> {
match value {
Value::TypeObject(ty) => Ok(DynSolType::try_from(ty.clone())?),
Expand Down Expand Up @@ -101,6 +147,11 @@ lazy_static! {
abi_decode_calldata,
vec![vec![FunctionParam::new("calldata", Type::Bytes)]]
);
pub static ref ABI_DECODE_ERROR: Arc<dyn FunctionDef> = SyncMethod::arc(
"decode_error",
abi_decode_error,
vec![vec![FunctionParam::new("data", Type::Bytes)]]
);
}

#[cfg(test)]
Expand Down
1 change: 1 addition & 0 deletions src/interpreter/builtins/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ lazy_static! {

let mut contract_methods = HashMap::new();
contract_methods.insert("decode".to_string(), abi::ABI_DECODE_CALLDATA.clone());
contract_methods.insert("decode_error".to_string(), abi::ABI_DECODE_ERROR.clone());
m.insert(NonParametricType::Contract, contract_methods);

let mut abi_methods = HashMap::new();
Expand Down
Loading

0 comments on commit 5395097

Please sign in to comment.