diff --git a/src/PhEvm.sol b/src/PhEvm.sol index bda7c56..57dc049 100644 --- a/src/PhEvm.sol +++ b/src/PhEvm.sol @@ -253,4 +253,29 @@ interface PhEvm { /// @dev Returns the transaction envelope data for the assertion-triggering tx /// @return txObject The transaction data struct function getTxObject() external view returns (TxObject memory txObject); + + /// @notice Returns canonical Solidity key encodings h(key) for keys + /// whose mapping entry at baseSlot was written during the tx. + /// @dev Best-effort heuristic: traces KECCAK256 -> SSTORE provenance in the + /// execution trace. Custom inline assembly or precomputed hashed slots + /// can bypass the visible keccak chain and produce false negatives. + /// @param target The contract whose storage was modified. + /// @param baseSlot The Solidity mapping's base storage slot. + /// @return keys Array of encoded keys (each is the h(key) preimage). + function changedMappingKeys(address target, bytes32 baseSlot) external view returns (bytes[] memory keys); + + /// @notice Returns the pre/post values for a specific mapping entry. + /// @dev Computes slot = keccak256(key ++ baseSlot) + fieldOffset, then reads + /// pre from the PreTx fork and post from the PostTx fork. + /// @param target The contract address. + /// @param baseSlot The mapping's base slot. + /// @param key The canonical encoding h(key) of the mapping key. + /// @param fieldOffset Struct field offset (0 for the first slot of the value). + /// @return pre The PreTx value. + /// @return post The PostTx value. + /// @return changed True if pre != post. + function mappingValueDiff(address target, bytes32 baseSlot, bytes calldata key, uint256 fieldOffset) + external + view + returns (bytes32 pre, bytes32 post, bool changed); } diff --git a/src/test-cases/precompiles/MappingTracing.sol b/src/test-cases/precompiles/MappingTracing.sol new file mode 100644 index 0000000..38fdd9c --- /dev/null +++ b/src/test-cases/precompiles/MappingTracing.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {Assertion} from "../../Assertion.sol"; +import {PhEvm} from "../../PhEvm.sol"; + +contract MappingTarget { + // slot 0 + mapping(address => uint256) public balances; + + function setBalance(address user, uint256 amount) external { + balances[user] = amount; + } +} + +MappingTarget constant MAPPING_TARGET = MappingTarget(0xdCCf1eEB153eF28fdc3CF97d33f60576cF092e9c); + +contract TestChangedMappingKeys is Assertion { + constructor() payable {} + + function checkChangedKeys() external view { + bytes[] memory keys = ph.changedMappingKeys(address(MAPPING_TARGET), bytes32(uint256(0))); + require(keys.length == 2, "expected 2 changed keys"); + } + + function triggers() external view override { + registerCallTrigger(this.checkChangedKeys.selector); + } +} + +contract TestMappingValueDiff is Assertion { + constructor() payable {} + + function checkValueDiff() external view { + address user = address(0xBEEF); + bytes memory key = abi.encode(user); + + (bytes32 pre, bytes32 post, bool changed) = + ph.mappingValueDiff(address(MAPPING_TARGET), bytes32(uint256(0)), key, 0); + + require(changed, "value should have changed"); + require(pre == bytes32(uint256(0)), "pre should be zero"); + require(post == bytes32(uint256(100)), "post should be 100"); + } + + function triggers() external view override { + registerCallTrigger(this.checkValueDiff.selector); + } +} + +contract MappingTriggeringTx { + constructor() payable { + MAPPING_TARGET.setBalance(address(0xBEEF), 100); + MAPPING_TARGET.setBalance(address(0xCAFE), 200); + } +}