Skip to content

chore(forge): revert diagnostic inspector #10446

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: master
Choose a base branch
from

Conversation

0xrusowsky
Copy link

@0xrusowsky 0xrusowsky commented May 6, 2025

PoC for a revert diagnostic inspector that would tackle:

the idea is that this inspector could be enhanced to track other contextual information, and provide support for other "undiagnosed" reverts. For the time being though, it has been kept quite minimal (just to target the linked issue).

design

created a new inspector:

/// An inspector that tracks call context to enhances revert diagnostics.
/// Useful for understanding reverts that are not linked to custom errors or revert strings.
#[derive(Clone, Debug, Default)]
pub struct RevertDiagnostic {
    /// Tracks calls with calldata that target an address without executable code
    pub non_contract_call: Option<(Address, CallScheme)>,

    /// Tracks whether a failed call has been spotted or not.
    pub reverted: bool,
}

and a new enum:

#[derive(Debug, Clone, Copy)]
pub enum DetailedRevertReason {
    CallToNonContract(Address),
    DelegateCallToNonContract(Address),
}

which provide extra content to the call traces, so that RevertDecoder can provide extra context when it would otherwise report EvmError: Revert .

output

these would be the new error messages that users would see:

Ran 1 test for test/NonContractCallRevertTest.t.sol:NonContractCallRevertTest
[FAIL: EvmError: call to non-contract address `0xdEADBEeF00000000000000000000000000000000`] test_non_contract_call_failure() ([GAS])
// rest of the logs

Ran 1 test for test/NonContractDelegateCallRevertTest.t.sol:NonContractDelegateCallRevertTest
[FAIL: EvmError: delegatecall to non-contract address `0xdEADBEeF00000000000000000000000000000000` (usually an unliked library)] test_unlinked_library_call_failure() ([GAS])
// rest of the logs

despite the nature of these error messages is of last-resort, maybe we can't know for certain that they were the root cause, as the calls themselves don't fail.

an alternative approach would be to add them as call ctx regardless of the test outcome, so that users are aware that their tests had a call (with calldata) to a non-contract address.

@0xrusowsky 0xrusowsky linked an issue May 6, 2025 that may be closed by this pull request
Copy link
Collaborator

@grandizzy grandizzy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO on the right track, @klkvr pls share your thoughts re the approach.
left some minor comments / nits.
To note that such default option could add perf penalty for invariant tests running with fail_on_revert = false but probably bearable.

}

impl RevertDiagnostic {
fn is_delegatecall(&self, scheme: CallScheme) -> bool {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these can be all helper functions, not RevertDiagnostic specific

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would u keep them in the inspector file? or should i place them somewhere else (like utils or similar)?

return None;
}

let reason = match self.non_contract_call {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can use self.non_contract_call.map

@@ -61,6 +61,8 @@ pub struct InspectorStackBuilder {
pub wallets: Option<Wallets>,
/// The CREATE2 deployer address.
pub create2_deployer: Address,
/// Whether to provide diagnostics for EVM reverts.
pub revert_diag: Option<bool>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it makes sense to always provide such, wdyt?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just wanted to keep the existing pattern, but i'd do the same 👍


Failing tests:
Encountered 1 failing test in test/NonContractCallRevertTest.t.sol:NonContractCallRevertTest
[FAIL: EvmError: call to non-contract address `0xdEADBEeF00000000000000000000000000000000`] test_non_contract_call_failure() ([GAS])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!

outcome: CallOutcome,
) -> CallOutcome {
if outcome.result.result == InstructionResult::Revert {
self.reverted = true
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe here we could directly set the reason? we can also do more checks if target address is actually a contract, (like checking if selector available, etc. some other similar hh checks https://github.com/NomicFoundation/hardhat/blob/67f1e95e1f3904f7b2e8a5560115c1551e899f64/packages/hardhat-core/src/internal/hardhat-network/stack-traces/solidity-errors.ts#L218-L341)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i thought that storing a bool could be more efficient + fn reason() would allow us to isolate the logic, but totally fine for me to do it directly

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need some smarter strategy here to ensure that call we've caught in call hook is indeed the one that caused revert (e.g compare depth, ensure there were no calls after it)

Comment on lines 3612 to 3614
interface ICounter {
function number() external returns (uint256);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that in cases of calling functions returning data Solidity's approach is to revert if data is empty.

However, if method does not return anything (e.g like increment()) then Solidity would firstly check before the call whether the address has any code, and revert if it's not which I believe is not being caught by our inspector rn

@0xrusowsky 0xrusowsky requested review from grandizzy and klkvr May 15, 2025 15:04
@0xrusowsky
Copy link
Author

addresses the PR feedback by checking the call stack depth + tracking EXTCODESIZE calls to empty addresses (confirmed that the initial impl wouldn't diagnose those).

the only downside is extra performance overhead, but i tried to make the fn setp as efficient as possible (improvement suggestions are welcome)

@0xrusowsky 0xrusowsky marked this pull request as ready for review May 16, 2025 10:48
@0xrusowsky
Copy link
Author

i also enhanced the trace decoder to identify function calls for a selector that is not supported by the called contract (if the tracer has access to its abi)

if EXTCODESIZE == interpreter.current_opcode() {
if let Ok(word) = interpreter.stack().peek(0) {
let addr = Address::from_word(word.into());
if let Ok(state) = ctx.code(addr) {
Copy link
Collaborator

@grandizzy grandizzy May 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this warms the account (load_account) hence the gas changes in tests, we should probably just record these addresses and their depth and check for the code in call_end, if call reverted

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: No status
Development

Successfully merging this pull request may close these issues.

Better error reporting when interacting with undeployed contracts
3 participants