Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ Access these via the `ph` instance inherited from `Credible`:
| `getDelegateCallInputs(address, bytes4)` | Get DELEGATECALL inputs |
| `getAllCallInputs(address, bytes4)` | Get all call types |
| `callinputAt(uint256)` | Get full recorded calldata for one call id |
| `callOutputAt(uint256)` | Get full recorded return or revert bytes for one call id |
| `getStateChanges(address, bytes32)` | Get state changes for a slot |
| `getAssertionAdopter()` | Get the adopter contract address |

Expand Down
5 changes: 5 additions & 0 deletions src/PhEvm.sol
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,11 @@ interface PhEvm {
/// @return calls Array of CallInputs from CALLCODE opcodes
function getCallCodeInputs(address target, bytes4 selector) external view returns (CallInputs[] memory calls);

/// @notice Returns the raw return or revert bytes for a traced call.
/// @param callId The call identifier from CallInputs.id.
/// @return output The raw ABI-encoded return bytes or revert bytes.
function callOutputAt(uint256 callId) external view returns (bytes memory output);

/// @notice Returns the calldata of a specific call.
/// @param callId The call ID to read input from.
/// @return input The raw calldata bytes (selector + ABI-encoded arguments).
Expand Down
100 changes: 100 additions & 0 deletions src/test-cases/precompiles/CallOutputAt.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import {Assertion} from "../../Assertion.sol";
import {PhEvm} from "../../PhEvm.sol";
import {Target, TARGET} from "../common/Target.sol";

contract TestCallOutputAt is Assertion {
constructor() payable {}

function callOutputAtReturnsEncodedReturnData() external view {
PhEvm.CallInputs[] memory readCalls = ph.getStaticCallInputs(address(TARGET), Target.readStorage.selector);
require(readCalls.length == 1, "expected one readStorage call");

bytes memory output = ph.callOutputAt(readCalls[0].id);
require(abi.decode(output, (uint256)) == 7, "unexpected readStorage output");
}

function callOutputAtReturnsEmptyBytesForVoidCall() external view {
bytes memory output = ph.callOutputAt(_successfulNestedWriteId());
require(output.length == 0, "void call should return empty bytes");
}

function callOutputAtReturnsRevertData() external view {
bytes memory output = ph.callOutputAt(_revertedWriteStorageAndRevertId());
require(output.length >= 4, "reverting call should return revert bytes");
}

function callOutputAtRejectsRevertedSubtreeCallId() external view {
uint256 revertedCallId = _revertedSubtreeChildCallId();
bytes memory calldata_ = abi.encodeWithSelector(PhEvm.callOutputAt.selector, revertedCallId);
(bool success,) = address(ph).staticcall(calldata_);
require(!success, "reverted subtree call should revert");
}

function _successfulNestedWriteId() internal view returns (uint256 successfulNestedWriteId) {
PhEvm.CallInputs[] memory writeCalls = ph.getCallInputs(address(TARGET), Target.writeStorage.selector);

bool found;
for (uint256 i = 0; i < writeCalls.length; i++) {
uint256 param = abi.decode(writeCalls[i].input, (uint256));
if (param == 7) {
successfulNestedWriteId = writeCalls[i].id;
found = true;
break;
}
}

require(found, "expected nested writeStorage call");
}

function _revertedWriteStorageAndRevertId() internal view returns (uint256) {
return _successfulNestedWriteId() + 1;
}

function _revertedSubtreeChildCallId() internal view returns (uint256) {
return _revertedWriteStorageAndRevertId() + 2;
}

function triggers() external view override {
registerCallTrigger(this.callOutputAtReturnsEncodedReturnData.selector);
registerCallTrigger(this.callOutputAtReturnsEmptyBytesForVoidCall.selector);
registerCallTrigger(this.callOutputAtReturnsRevertData.selector);
registerCallTrigger(this.callOutputAtRejectsRevertedSubtreeCallId.selector);
}
}

contract TriggeringTx {
constructor() payable {
TARGET.writeStorage(5);

CallFrameTrigger callFrameTrigger = new CallFrameTrigger();
callFrameTrigger.trigger();

RevertingSubtreeTrigger revertingSubtreeTrigger = new RevertingSubtreeTrigger();
try revertingSubtreeTrigger.trigger() {
revert("expected reverting subtree to revert");
} catch {}

uint256 value = TARGET.readStorage();
require(value == 7, "readStorage call failed");
}
}

contract CallFrameTrigger {
function trigger() external {
TARGET.writeStorage(7);

try TARGET.writeStorageAndRevert(11) {
revert("expected writeStorageAndRevert to revert");
} catch {}
}
}

contract RevertingSubtreeTrigger {
function trigger() external {
TARGET.writeStorage(9);
revert("reverted subtree");
}
}