Skip to content

Commit 085e95a

Browse files
authored
feat(assex): add calloutputat interface (#50)
* feat: add callOutputAt precompile interface * test: cover reverted callOutputAt cases
1 parent 7d75436 commit 085e95a

File tree

3 files changed

+106
-0
lines changed

3 files changed

+106
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ Access these via the `ph` instance inherited from `Credible`:
154154
| `getDelegateCallInputs(address, bytes4)` | Get DELEGATECALL inputs |
155155
| `getAllCallInputs(address, bytes4)` | Get all call types |
156156
| `callinputAt(uint256)` | Get full recorded calldata for one call id |
157+
| `callOutputAt(uint256)` | Get full recorded return or revert bytes for one call id |
157158
| `getStateChanges(address, bytes32)` | Get state changes for a slot |
158159
| `getAssertionAdopter()` | Get the adopter contract address |
159160

src/PhEvm.sol

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,11 @@ interface PhEvm {
219219
/// @return calls Array of CallInputs from CALLCODE opcodes
220220
function getCallCodeInputs(address target, bytes4 selector) external view returns (CallInputs[] memory calls);
221221

222+
/// @notice Returns the raw return or revert bytes for a traced call.
223+
/// @param callId The call identifier from CallInputs.id.
224+
/// @return output The raw ABI-encoded return bytes or revert bytes.
225+
function callOutputAt(uint256 callId) external view returns (bytes memory output);
226+
222227
/// @notice Returns the calldata of a specific call.
223228
/// @param callId The call ID to read input from.
224229
/// @return input The raw calldata bytes (selector + ABI-encoded arguments).
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.13;
3+
4+
import {Assertion} from "../../Assertion.sol";
5+
import {PhEvm} from "../../PhEvm.sol";
6+
import {Target, TARGET} from "../common/Target.sol";
7+
8+
contract TestCallOutputAt is Assertion {
9+
constructor() payable {}
10+
11+
function callOutputAtReturnsEncodedReturnData() external view {
12+
PhEvm.CallInputs[] memory readCalls = ph.getStaticCallInputs(address(TARGET), Target.readStorage.selector);
13+
require(readCalls.length == 1, "expected one readStorage call");
14+
15+
bytes memory output = ph.callOutputAt(readCalls[0].id);
16+
require(abi.decode(output, (uint256)) == 7, "unexpected readStorage output");
17+
}
18+
19+
function callOutputAtReturnsEmptyBytesForVoidCall() external view {
20+
bytes memory output = ph.callOutputAt(_successfulNestedWriteId());
21+
require(output.length == 0, "void call should return empty bytes");
22+
}
23+
24+
function callOutputAtReturnsRevertData() external view {
25+
bytes memory output = ph.callOutputAt(_revertedWriteStorageAndRevertId());
26+
require(output.length >= 4, "reverting call should return revert bytes");
27+
}
28+
29+
function callOutputAtRejectsRevertedSubtreeCallId() external view {
30+
uint256 revertedCallId = _revertedSubtreeChildCallId();
31+
bytes memory calldata_ = abi.encodeWithSelector(PhEvm.callOutputAt.selector, revertedCallId);
32+
(bool success,) = address(ph).staticcall(calldata_);
33+
require(!success, "reverted subtree call should revert");
34+
}
35+
36+
function _successfulNestedWriteId() internal view returns (uint256 successfulNestedWriteId) {
37+
PhEvm.CallInputs[] memory writeCalls = ph.getCallInputs(address(TARGET), Target.writeStorage.selector);
38+
39+
bool found;
40+
for (uint256 i = 0; i < writeCalls.length; i++) {
41+
uint256 param = abi.decode(writeCalls[i].input, (uint256));
42+
if (param == 7) {
43+
successfulNestedWriteId = writeCalls[i].id;
44+
found = true;
45+
break;
46+
}
47+
}
48+
49+
require(found, "expected nested writeStorage call");
50+
}
51+
52+
function _revertedWriteStorageAndRevertId() internal view returns (uint256) {
53+
return _successfulNestedWriteId() + 1;
54+
}
55+
56+
function _revertedSubtreeChildCallId() internal view returns (uint256) {
57+
return _revertedWriteStorageAndRevertId() + 2;
58+
}
59+
60+
function triggers() external view override {
61+
registerCallTrigger(this.callOutputAtReturnsEncodedReturnData.selector);
62+
registerCallTrigger(this.callOutputAtReturnsEmptyBytesForVoidCall.selector);
63+
registerCallTrigger(this.callOutputAtReturnsRevertData.selector);
64+
registerCallTrigger(this.callOutputAtRejectsRevertedSubtreeCallId.selector);
65+
}
66+
}
67+
68+
contract TriggeringTx {
69+
constructor() payable {
70+
TARGET.writeStorage(5);
71+
72+
CallFrameTrigger callFrameTrigger = new CallFrameTrigger();
73+
callFrameTrigger.trigger();
74+
75+
RevertingSubtreeTrigger revertingSubtreeTrigger = new RevertingSubtreeTrigger();
76+
try revertingSubtreeTrigger.trigger() {
77+
revert("expected reverting subtree to revert");
78+
} catch {}
79+
80+
uint256 value = TARGET.readStorage();
81+
require(value == 7, "readStorage call failed");
82+
}
83+
}
84+
85+
contract CallFrameTrigger {
86+
function trigger() external {
87+
TARGET.writeStorage(7);
88+
89+
try TARGET.writeStorageAndRevert(11) {
90+
revert("expected writeStorageAndRevert to revert");
91+
} catch {}
92+
}
93+
}
94+
95+
contract RevertingSubtreeTrigger {
96+
function trigger() external {
97+
TARGET.writeStorage(9);
98+
revert("reverted subtree");
99+
}
100+
}

0 commit comments

Comments
 (0)