Skip to content

Commit 994b6a0

Browse files
cryptoAtwillcryptoAtwillkarlemdrahnr
authored
feat(node): parse contract revert (#1311)
Co-Authored-By: cryptoAtwill <willes.lau@protocol.ai> Co-Authored-By: Karel Moravec <moravec.wdd@gmail.com> Co-Authored-By: Bernhard Schuster <bernhard@ahoi.io>
1 parent 89b0c90 commit 994b6a0

14 files changed

Lines changed: 243 additions & 5795 deletions

File tree

.github/workflows/build.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ jobs:
5151
with:
5252
path: |
5353
./contracts/out
54-
./contract-bindings
5554
./contracts/cache
5655
## TODO maybe add the rust version and solc version to the key
5756
key: v2-contracts-abi-${{ hashFiles('./contracts/**/*.sol') }}
@@ -75,6 +74,9 @@ jobs:
7574
cargo -V
7675
echo "glibc version"
7776
ldd --version
77+
echo "contract-bindings/src"
78+
ls contract-bindings/src
79+
cat contract-bindings/src/lib.rs
7880
7981
- name: Check fmt (nightly)
8082
run: cargo +nightly-2024-07-05 fmt --check --all

.github/workflows/tests-e2e.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ jobs:
4040
with:
4141
path: |
4242
./contracts/out
43-
./contract-bindings
4443
./contracts/cache
4544
key: v2-contracts-abi-${{ hashFiles('./contracts/**/*.sol') }}
4645

.github/workflows/tests-unit.yaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,7 @@ jobs:
2828
uses: actions/cache@v4
2929
with:
3030
path: |
31-
./contracts/out
32-
./contract-bindings
31+
./contracts/out
3332
./contracts/cache
3433
key: v2-contracts-abi-${{ hashFiles('./contracts/**/*.sol') }}
3534

Cargo.lock

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ clap = { version = "4.1", features = ["derive", "env", "string"] }
7474
color-eyre = "0.5.11"
7575
byteorder = "1.5.0"
7676
config = "0.13"
77+
const-hex = "1.14.0"
7778
dirs = "5.0"
7879
dircpy = "0.3.19"
7980
either = "1.10"
@@ -192,7 +193,9 @@ openssl = { version = "0.10", features = ["vendored"] }
192193
# pull in crates as transitive dependencies that do not support Wasm architector. If this
193194
# happens, try removing "crypto" feature from fvm_shared dependency in contract-bindings/Cargo.toml
194195
# and run `cargo build`. Then add the "crypto" feature back and run `cargo build` again.
195-
fvm = { version = "4.4.0", features = ["verify-signature"], default-features = false } # no opencl feature or it fails on CI
196+
fvm = { version = "4.4.0", features = [
197+
"verify-signature",
198+
], default-features = false } # no opencl feature or it fails on CI
196199
fvm_shared = { version = "4.4.0" }
197200
fvm_sdk = { version = "4.4.0" }
198201

contract-bindings/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ ethers = { workspace = true, features = ["abigen", "ws"] }
1111
fvm_shared = { workspace = true }
1212
anyhow = { workspace = true }
1313
fs-err = { workspace = true }
14+
lazy_static = { workspace = true }
15+
tracing = { workspace = true }
16+
const-hex = { workspace = true }
17+
thiserror = { workspace = true }
1418

1519
[build-dependencies]
1620
ethers = { workspace = true, features = ["abigen", "ws"] }

contract-bindings/build.rs

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ fn main() -> color_eyre::Result<()> {
3030
// PathBuf::from(std::env::var("OUT_DIR")?);
3131
let gen_dir = crate_dir.join("src").join("gen");
3232
let mod_path = gen_dir.join("mod.rs");
33-
println!("cargo:rerun-if-changed={}", mod_path.display());
33+
34+
for entry in (fs_err::read_dir("src")?).flatten() {
35+
println!("cargo:rerun-if-changed={}", entry.path().display());
36+
}
3437

3538
// Maybe we want to skip the build and use the files as-is, could be imported as crate.
3639
// Enabled by default so that in the monorepo we don't have to worry about stale code.
@@ -63,7 +66,7 @@ fn main() -> color_eyre::Result<()> {
6366
// The list of actors we need contract-bindings for, based on how the ipc-actor uses `abigen!`.
6467
// With the diamond pattern, there is a contract that holds state, and there are these facets which have the code,
6568
// so we need contract-bindings for the facets, but well (I think) use the same address with all of them.
66-
for contract_name in [
69+
let all_contracts = [
6770
"IDiamond",
6871
"DiamondLoupeFacet",
6972
"DiamondCutFacet",
@@ -89,7 +92,9 @@ fn main() -> color_eyre::Result<()> {
8992
"LibPowerChangeLog",
9093
"LibGateway",
9194
"LibQuorum",
92-
] {
95+
];
96+
97+
for contract_name in all_contracts {
9398
let contract_name_path = PathBuf::from(contract_name);
9499
let module_name = camel_to_snake(contract_name);
95100

@@ -151,6 +156,9 @@ fn main() -> color_eyre::Result<()> {
151156
)?;
152157
}
153158

159+
writeln!(mod_f, "\n\n")?;
160+
error_mapping_gen(&mut mod_f, &all_contracts)?;
161+
154162
mod_f.flush()?;
155163
mod_f.sync_all()?;
156164

@@ -185,3 +193,25 @@ fn camel_to_snake(name: &str) -> String {
185193
}
186194
out
187195
}
196+
197+
/// generate the mapping between contract error selectors to the ethers abi error type for parsing
198+
/// potential contract errors.
199+
/// This function generates a rust file that creates the error mapping, see [`ipc_actors_abis::extend_contract_error_mapping`]
200+
/// macro for how it works internally.
201+
/// This function will write the macro call [`ipc_actors_abis::extend_contract_error_mapping`] and loops all the contract names to watch and
202+
/// fill the macro rule.
203+
fn error_mapping_gen(mod_f: &mut impl Write, all_contracts: &[&str]) -> color_eyre::Result<()> {
204+
writeln!(mod_f, "crate::extend_contract_error_mapping!(")?;
205+
206+
let map_name_to_macro_rule = |s| format!("[{}, {}_ABI]", camel_to_snake(s), s.to_uppercase());
207+
208+
let extend_map_code = all_contracts
209+
.iter()
210+
.map(|s| map_name_to_macro_rule(s))
211+
.collect::<Vec<_>>()
212+
.join(",\n");
213+
214+
writeln!(mod_f, "{}", extend_map_code)?;
215+
writeln!(mod_f, ");")?;
216+
Ok(())
217+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
use ethers::abi::ethabi::AbiError;
2+
use std::collections::BTreeMap;
3+
4+
/// Macro to extend the contract error mapping by aggregating errors from multiple contracts.
5+
/// Each contract is specified as a pair: `[snake_case, ABI_IDENTIFIER]` where:
6+
/// - `snake_case` is the contract name converted to snake case.
7+
/// - `ABI_IDENTIFIER` is the contract name in uppercase with `_ABI` appended.
8+
///
9+
/// This macro builds a lazily initialized static map (`MAP`) that associates Solidity error selectors
10+
/// (first 4 bytes of an error signature, hex-encoded) with their corresponding `AbiError`.
11+
#[macro_export]
12+
macro_rules! extend_contract_error_mapping {
13+
($([$snake_case:ident, $abi:ident]),* $(,)?) => {
14+
lazy_static::lazy_static! {
15+
pub(crate) static ref MAP: ::std::collections::BTreeMap<String, ethers::abi::ethabi::AbiError> = {
16+
let mut errors = ::std::collections::BTreeMap::default();
17+
18+
$(
19+
$crate::error_parser::extend_errors(&mut errors, $crate::gen::$snake_case::$snake_case::$abi.errors.clone());
20+
)*
21+
22+
errors
23+
};
24+
}
25+
}
26+
}
27+
28+
const SOLIDITY_SELECTOR_BYTE_SIZE: usize = 4;
29+
30+
/// Extends the provided error map with errors from a contract’s error collection.
31+
/// For each error, it extracts the Solidity selector (first 4 bytes of the error signature),
32+
/// hex-encodes it, and maps that selector to a clone of the `AbiError`.
33+
///
34+
/// If a selector already exists in the map, a warning is logged.
35+
pub fn extend_errors(
36+
map: &mut BTreeMap<String, AbiError>,
37+
contract_errors: BTreeMap<String, Vec<AbiError>>,
38+
) {
39+
for (_, v) in contract_errors.iter() {
40+
for e in v {
41+
// solidity selector is only the first 4 bytes of the signature
42+
let selector = const_hex::hex::encode(&e.signature().0[0..SOLIDITY_SELECTOR_BYTE_SIZE]);
43+
map.insert(selector, e.clone());
44+
}
45+
}
46+
}
47+
48+
#[derive(Debug, PartialEq, thiserror::Error)]
49+
pub enum ParseContractError {
50+
#[error("error bytes shorter than 4 bytes for solidity contract selector")]
51+
ErrorBytesTooShort,
52+
#[error("error string not hex format: {0}")]
53+
ErrorNotHexStr(String),
54+
#[error("error selector not found in contract error map: {selector}")]
55+
ErrorNotFound { selector: String },
56+
}
57+
58+
pub struct ContractErrorParser {}
59+
60+
impl ContractErrorParser {
61+
pub fn parse_from_bytes(bytes: &[u8]) -> Result<String, ParseContractError> {
62+
if bytes.len() < SOLIDITY_SELECTOR_BYTE_SIZE {
63+
return Err(ParseContractError::ErrorBytesTooShort);
64+
}
65+
66+
let selector = const_hex::hex::encode(&bytes[0..4]);
67+
68+
let Some(error) = crate::gen::MAP.get(&selector) else {
69+
return Err(ParseContractError::ErrorNotFound { selector });
70+
};
71+
if let Err(e) = error.decode(bytes) {
72+
tracing::warn!("contract error selector found: {selector}, but decode failed: {e}");
73+
}
74+
75+
Ok(error.name.clone())
76+
}
77+
78+
pub fn parse_from_hex_str(err: &str) -> Result<String, ParseContractError> {
79+
let bytes = const_hex::hex::decode(err)
80+
.map_err(|e| ParseContractError::ErrorNotHexStr(e.to_string()))?;
81+
Self::parse_from_bytes(bytes.as_slice())
82+
}
83+
}
84+
85+
#[cfg(test)]
86+
mod tests {
87+
use crate::error_parser::{ContractErrorParser, ParseContractError};
88+
use const_hex::hex;
89+
90+
#[test]
91+
fn test_parse_error_ok() {
92+
// selector for "BottomUpCheckpointAlreadySubmitted" error
93+
let err_bytes = hex::decode("d6bb62dd").unwrap();
94+
95+
assert_eq!(
96+
ContractErrorParser::parse_from_bytes(err_bytes.as_ref()).unwrap(),
97+
"BottomUpCheckpointAlreadySubmitted".to_string()
98+
);
99+
100+
// selector for "FunctionNotFound" error
101+
let err_bytes =
102+
hex::decode("5416eb98611941f900000000000000000000000000000000000000000000000000000000")
103+
.unwrap();
104+
105+
assert_eq!(
106+
ContractErrorParser::parse_from_bytes(err_bytes.as_ref()).unwrap(),
107+
"FunctionNotFound".to_string()
108+
);
109+
}
110+
111+
#[test]
112+
fn test_parse_error_not_found() {
113+
// a random error selector
114+
let err_bytes = hex::decode("a6bb62dd").unwrap();
115+
116+
assert_eq!(
117+
ContractErrorParser::parse_from_bytes(err_bytes.as_ref()),
118+
Err(ParseContractError::ErrorNotFound {
119+
selector: "a6bb62dd".to_string()
120+
})
121+
);
122+
}
123+
}

contract-bindings/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#[macro_use]
22
mod convert;
33

4+
pub mod error_parser;
45
mod gen;
6+
57
pub use self::gen::*;

contracts/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ compile-abi: node_modules | forge
8585
./ops/compile-abi.sh $(OUTPUT)
8686

8787
rust-binding:
88-
OUTPUT=$(OUTPUT) cargo build --locked --release --manifest-path ../contract-bindings/Cargo.toml -p ipc_actors_abis
88+
OUTPUT=$(OUTPUT) cargo build --release --manifest-path ../contract-bindings/Cargo.toml -p ipc_actors_abis
8989

9090
# ==============================================================================
9191
# Running security checks within the local computer

0 commit comments

Comments
 (0)