Skip to content

Commit 5717d63

Browse files
authored
fix: contracts errors parsing (#1409)
1 parent f09848c commit 5717d63

11 files changed

Lines changed: 1279 additions & 54 deletions

File tree

Cargo.lock

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

Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,6 @@ fvm_ipld_hamt = { git = "https://github.com/consensus-shipyard/ref-fvm.git" }
254254
fvm_ipld_amt = { git = "https://github.com/consensus-shipyard/ref-fvm.git" }
255255
yamux = { git = "https://github.com/paritytech/yamux", tag = "yamux-v0.13.4" }
256256

257-
258257
[profile.wasm]
259258
inherits = "release"
260259
panic = "abort"

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ For further documentation, see:
7272
- [docs/contracts.md](./docs/ipc/contracts.md) for instructions on how to deploy FEVM actors on subnets.
7373
- [docs/usage.md](./docs/ipc/usage.md) for instructions on how to use the `ipc-cli` to interact with subnets (from managing your identities, to sending funds to a subnet).
7474
- [docs/deploying-hierarchy.md](./docs/ipc/deploying-hierarchy.md) for instructions on how to deploy your own instance of IPC on a network.
75+
- [docs/contract-errors.md](./docs/ipc/contract-errors.md) for a comprehensive reference of all possible contract errors and how to resolve them.
7576

7677
If you are a developer, see:
7778

contract-bindings/src/error_parser.rs

Lines changed: 191 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@ macro_rules! extend_contract_error_mapping {
2727

2828
const SOLIDITY_SELECTOR_BYTE_SIZE: usize = 4;
2929

30-
/// Extends the provided error map with errors from a contract’s error collection.
30+
/// Standard Solidity error selectors
31+
const ERROR_STRING_SELECTOR: &str = "08c379a0"; // Error(string)
32+
const PANIC_SELECTOR: &str = "4e487b71"; // Panic(uint256)
33+
34+
/// Extends the provided error map with errors from a contract's error collection.
3135
/// For each error, it extracts the Solidity selector (first 4 bytes of the error signature),
3236
/// hex-encodes it, and maps that selector to a clone of the `AbiError`.
3337
///
@@ -55,31 +59,155 @@ pub enum ParseContractError {
5559
ErrorNotFound { selector: String },
5660
}
5761

62+
#[derive(Debug, Clone, PartialEq)]
63+
pub struct ParsedError {
64+
pub error_type: ErrorType,
65+
pub name: String,
66+
pub message: Option<String>,
67+
pub parameters: Option<Vec<String>>,
68+
}
69+
70+
#[derive(Debug, Clone, PartialEq)]
71+
pub enum ErrorType {
72+
IpcContract,
73+
StandardRevert,
74+
Panic,
75+
Unknown,
76+
}
77+
5878
pub struct ContractErrorParser {}
5979

6080
impl ContractErrorParser {
61-
pub fn parse_from_bytes(bytes: &[u8]) -> Result<String, ParseContractError> {
81+
pub fn parse_from_bytes(bytes: &[u8]) -> Result<ParsedError, ParseContractError> {
6282
if bytes.len() < SOLIDITY_SELECTOR_BYTE_SIZE {
6383
return Err(ParseContractError::ErrorBytesTooShort);
6484
}
6585

6686
let selector = const_hex::hex::encode(&bytes[0..4]);
6787

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-
}
88+
// Check for standard Solidity errors first
89+
match selector.as_str() {
90+
ERROR_STRING_SELECTOR => {
91+
// Handle Error(string)
92+
if bytes.len() >= 68 {
93+
// 4 bytes selector + 32 bytes offset + 32 bytes length
94+
let length_start = 36; // 4 + 32
95+
let length_end = 68; // 4 + 32 + 32
96+
97+
if bytes.len() >= length_end {
98+
let length_bytes = &bytes[length_start..length_end];
99+
let length_u256 = ethers::types::U256::from_big_endian(length_bytes);
100+
let length = length_u256.as_u64();
101+
102+
let message_start = length_end;
103+
let message_end = message_start + length as usize;
104+
105+
if bytes.len() >= message_end {
106+
let message_bytes = &bytes[message_start..message_end];
107+
if let Ok(message) = String::from_utf8(message_bytes.to_vec()) {
108+
return Ok(ParsedError {
109+
error_type: ErrorType::StandardRevert,
110+
name: "RevertString".to_string(),
111+
message: Some(message),
112+
parameters: None,
113+
});
114+
}
115+
}
116+
}
117+
}
74118

75-
Ok(error.name.clone())
119+
// Fallback if parsing fails
120+
Ok(ParsedError {
121+
error_type: ErrorType::StandardRevert,
122+
name: "RevertString".to_string(),
123+
message: None,
124+
parameters: None,
125+
})
126+
}
127+
PANIC_SELECTOR => {
128+
// Handle Panic(uint256)
129+
if bytes.len() >= 36 {
130+
// 4 bytes selector + 32 bytes panic code
131+
let panic_code_bytes = &bytes[4..36];
132+
let panic_code_u256 = ethers::types::U256::from_big_endian(panic_code_bytes);
133+
let panic_code = panic_code_u256.as_u64();
134+
135+
let panic_message = match panic_code {
136+
0x00 => "Generic panic",
137+
0x01 => "Assertion failed",
138+
0x11 => "Arithmetic overflow/underflow",
139+
0x12 => "Division by zero",
140+
0x21 => "Invalid enum value",
141+
0x22 => "Invalid encoded storage byte array",
142+
0x31 => "Pop on empty array",
143+
0x32 => "Array index out of bounds",
144+
0x41 => "Memory allocation overflow",
145+
0x51 => "Zero-initialized variable of internal function type",
146+
_ => "Unknown panic",
147+
};
148+
149+
return Ok(ParsedError {
150+
error_type: ErrorType::Panic,
151+
name: "Panic".to_string(),
152+
message: Some(panic_message.to_string()),
153+
parameters: Some(vec![format!("0x{:x}", panic_code)]),
154+
});
155+
}
156+
157+
Ok(ParsedError {
158+
error_type: ErrorType::Panic,
159+
name: "Panic".to_string(),
160+
message: None,
161+
parameters: None,
162+
})
163+
}
164+
_ => {
165+
// Check for IPC contract errors
166+
let Some(error) = crate::gen::MAP.get(&selector) else {
167+
// Instead of returning ErrorNotFound, treat as Unknown error
168+
return Ok(ParsedError {
169+
error_type: ErrorType::Unknown,
170+
name: format!("UnknownError_{}", selector),
171+
message: None,
172+
parameters: None,
173+
});
174+
};
175+
176+
// Try to decode the error with parameters
177+
let mut parameters = None;
178+
if let Ok(decoded) = error.decode(bytes) {
179+
// Only show parameters if they contain meaningful data
180+
let decoded_str = format!("{:?}", decoded);
181+
if !decoded_str.contains("[]") && !decoded_str.is_empty() {
182+
parameters = Some(vec![decoded_str]);
183+
}
184+
}
185+
186+
Ok(ParsedError {
187+
error_type: ErrorType::IpcContract,
188+
name: error.name.clone(),
189+
message: None,
190+
parameters,
191+
})
192+
}
193+
}
76194
}
77195

78-
pub fn parse_from_hex_str(err: &str) -> Result<String, ParseContractError> {
196+
pub fn parse_from_hex_str(err: &str) -> Result<ParsedError, ParseContractError> {
79197
let bytes = const_hex::hex::decode(err)
80198
.map_err(|e| ParseContractError::ErrorNotHexStr(e.to_string()))?;
81199
Self::parse_from_bytes(bytes.as_slice())
82200
}
201+
202+
/// Legacy method for backward compatibility
203+
pub fn parse_from_bytes_legacy(bytes: &[u8]) -> Result<String, ParseContractError> {
204+
Self::parse_from_bytes(bytes).map(|parsed| parsed.name)
205+
}
206+
207+
/// Legacy method for backward compatibility
208+
pub fn parse_from_hex_str_legacy(err: &str) -> Result<String, ParseContractError> {
209+
Self::parse_from_hex_str(err).map(|parsed| parsed.name)
210+
}
83211
}
84212

85213
#[cfg(test)]
@@ -92,32 +220,70 @@ mod tests {
92220
// selector for "BottomUpCheckpointAlreadySubmitted" error
93221
let err_bytes = hex::decode("d6bb62dd").unwrap();
94222

95-
assert_eq!(
96-
ContractErrorParser::parse_from_bytes(err_bytes.as_ref()).unwrap(),
97-
"BottomUpCheckpointAlreadySubmitted".to_string()
98-
);
223+
let result = ContractErrorParser::parse_from_bytes(err_bytes.as_ref()).unwrap();
224+
assert_eq!(result.name, "BottomUpCheckpointAlreadySubmitted");
225+
assert!(matches!(
226+
result.error_type,
227+
crate::error_parser::ErrorType::IpcContract
228+
));
229+
}
230+
231+
#[test]
232+
fn test_parse_standard_revert() {
233+
// Standard Solidity Error(string) with message "This is a test error message"
234+
let revert_string = "08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001a5468697320697320612074657374206572726f72206d6573736167650000000000";
235+
236+
let result = ContractErrorParser::parse_from_hex_str(revert_string).unwrap();
237+
assert_eq!(result.name, "RevertString");
238+
assert!(matches!(
239+
result.error_type,
240+
crate::error_parser::ErrorType::StandardRevert
241+
));
242+
// The message should contain "This is a test error" (may be truncated due to padding)
243+
assert!(result.message.is_some());
244+
let message = result.message.unwrap();
245+
assert!(message.contains("This is a test error"));
246+
}
99247

100-
// selector for "FunctionNotFound" error
101-
let err_bytes =
102-
hex::decode("5416eb98611941f900000000000000000000000000000000000000000000000000000000")
248+
#[test]
249+
fn test_parse_panic() {
250+
// Panic with code 0x11 (arithmetic overflow)
251+
let panic_bytes =
252+
hex::decode("4e487b710000000000000000000000000000000000000000000000000000000000000011")
103253
.unwrap();
104254

255+
let result = ContractErrorParser::parse_from_bytes(panic_bytes.as_ref()).unwrap();
256+
assert_eq!(result.name, "Panic");
257+
assert!(matches!(
258+
result.error_type,
259+
crate::error_parser::ErrorType::Panic
260+
));
105261
assert_eq!(
106-
ContractErrorParser::parse_from_bytes(err_bytes.as_ref()).unwrap(),
107-
"FunctionNotFound".to_string()
262+
result.message,
263+
Some("Arithmetic overflow/underflow".to_string())
108264
);
265+
assert_eq!(result.parameters, Some(vec!["0x11".to_string()]));
109266
}
110267

111268
#[test]
112269
fn test_parse_error_not_found() {
113270
// a random error selector
114271
let err_bytes = hex::decode("a6bb62dd").unwrap();
115272

116-
assert_eq!(
117-
ContractErrorParser::parse_from_bytes(err_bytes.as_ref()),
118-
Err(ParseContractError::ErrorNotFound {
119-
selector: "a6bb62dd".to_string()
120-
})
121-
);
273+
let result = ContractErrorParser::parse_from_bytes(err_bytes.as_ref());
274+
assert!(result.is_err());
275+
if let Err(ParseContractError::ErrorNotFound { selector }) = result {
276+
assert_eq!(selector, "a6bb62dd");
277+
} else {
278+
panic!("Expected ErrorNotFound error");
279+
}
280+
}
281+
282+
#[test]
283+
fn test_legacy_compatibility() {
284+
// Test that legacy methods still work
285+
let test_error = "d6bb62dd";
286+
let legacy_result = ContractErrorParser::parse_from_hex_str_legacy(test_error).unwrap();
287+
assert_eq!(legacy_result, "BottomUpCheckpointAlreadySubmitted");
122288
}
123289
}

contracts/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ The original idea of IPC is presented in these [paper](https://research.protocol
2020

2121
The current specification draft is available [here](https://github.com/consensus-shipyard/IPC-design-reference-spec/blob/main/main.pdf).
2222

23+
## Contract Errors Reference
24+
25+
For a comprehensive reference of all possible contract errors and how to resolve them, see [docs/ipc/contract-errors.md](../docs/ipc/contract-errors.md). This document provides detailed information about error types, when they occur, and how to fix them.
26+
2327
# Deploying IPC Solidity contracts
2428

2529
Before deploying the contract, you'll need to configure the `RPC_URL` and `PRIVATE_KEY` environmental variables

0 commit comments

Comments
 (0)