Introduction

Cheatcodes are specialized testing and assertion utilities in the Phylax Credible Layer that extend beyond traditional EVM capabilities. Implemented as native code (Rust) rather than EVM bytecode, they provide developers with powerful tools for writing and testing assertions by offering deep inspection into transaction execution, storage changes, call traces, and blockchain state. Some of the main capabilities cheatcodes provide are:
  • Forking the state of the blockchain before and after a transaction
  • Callstack introspection in ways not possible with standard EVM tools
  • State change tracking for specific storage slots
  • Triggering assertions on specific events
You may notice the terms “precompiles” and “cheatcodes” used interchangeably in some contexts. We use the term “cheatcodes” as it’s more familiar to Solidity developers who are already using testing frameworks like Foundry or Hardhat. Both terms refer to native functions that extend the EVM’s capabilities, but “cheatcodes” better reflects their role in the developer experience.

State Management

forkPreTx

Updates the active fork to the state prior to the assertion triggering transaction. Useful for:
  • Checking chain state before a transaction
  • Comparing values between pre and post states
function forkPreTx() external

forkPostTx

Updates the active fork to the state after the assertion triggering transaction. Useful for:
  • Verifying chain state after a transaction
  • Comparing values between pre and post states
function forkPostTx() external

forkPreCall

Updates the active fork to the state at the start of call execution for the specified id. getCallInputs(..) can be used to get ids to fork to.
function forkPreCall(uint256 id) external

forkPostCall

Updates the active fork to the state after the call execution for the specified id. getCallInputs(..) can be used to get ids to fork to.
function forkPostCall(uint256 id) external

Storage Operations

load

Loads a storage slot from an address.
Using forge inspect <ContractName> storage-layout is an easy way to get the storage layout of a contract. This can be used to determine the storage slot of a specific variable to use in the cheatcode.
function load(
    address target, 
    bytes32 slot
) external view returns (bytes32 data)
You can see an example of how to use this cheatcode in the Storage Lookups use case mapping.

Transaction Data

getLogs

Retrieves logs from the assertion triggering transaction.
struct Log {
    bytes32[] topics;   // Log topics, including signature
    bytes data;         // Raw log data
    address emitter;    // Log emitter address
}

function getLogs() external returns (Log[] memory logs)
You can see an example of how to use this cheatcode in the Read Logs use case mapping.

getAllCallInputs

Gets all call inputs for a given target and selector. This includes all types of calls (CALL, STATICCALL, DELEGATECALL, CALLCODE).
struct CallInputs {
    bytes input;                // Call data
    uint64 gas_limit;           // Gas limit
    address bytecode_address;   // Code execution address
    address target_address;     // Storage modification target
    address caller;             // Transaction caller
    uint256 value;              // Call value
    uint256 id;                 // Call id (used for forkPreCall and forkPostCall)
}

function getAllCallInputs(
    address target, 
    bytes4 selector
) external view returns (CallInputs[] memory calls);

getCallInputs

Gets call inputs for a given target and selector. Only includes calls made using ‘CALL’ opcode.
function getCallInputs(
    address target, 
    bytes4 selector
) external view returns (CallInputs[] memory calls);
You can see an example of how to use this cheatcode in the Function Call Inputs use case mapping.

getStaticCallInputs

Gets static call inputs for a given target and selector. Only includes calls made using ‘STATICCALL’ opcode.
function getStaticCallInputs(
    address target, 
    bytes4 selector
) external view returns (CallInputs[] memory calls);

getDelegateCallInputs

Gets delegate call inputs for a given target and selector. Only includes calls made using ‘DELEGATECALL’ opcode.
function getDelegateCallInputs(
    address target, 
    bytes4 selector
) external view returns (CallInputs[] memory calls);

getCallCodeInputs

Gets call code inputs for a given target and selector. Only includes calls made using ‘CALLCODE’ opcode.
function getCallCodeInputs(
    address target, 
    bytes4 selector
) external view returns (CallInputs[] memory calls);
The specific call input cheatcodes (getCallInputs, getStaticCallInputs, getDelegateCallInputs, getCallCodeInputs) eliminate double-counting issues by allowing you to target only the specific call type you need. Use these instead of getAllCallInputs() when you want to avoid duplicate entries from proxy contracts. For example, if you’re monitoring a proxy contract and only want the actual delegate calls (not the proxy calls), use getDelegateCallInputs() instead of getAllCallInputs().
// GOOD: Only get delegate calls, no duplicates
CallInputs[] memory delegateCalls = ph.getDelegateCallInputs(target, selector);

// AVOID: May include both proxy and delegate calls for the same operation
CallInputs[] memory allCalls = ph.getAllCallInputs(target, selector);
If you must use getAllCallInputs(), you may still need to filter out proxy calls:
// Filter proxy calls when using getAllCallInputs
for (uint256 i = 0; i < callInputs.length; i++) {
  if (callInputs[i].bytecode_address == callInputs[i].target_address) {
    continue; // Skip proxy calls, only process delegate calls
  }
  // Process the actual call
}
Internal calls are not actual EVM calls but rather a Solidity language feature. They are not traced by the CallTracer and will not result in CallInputs or trigger call-based assertions. However, using the this keyword (e.g., this.functionName()) creates an external call that will be traced and can trigger assertions.Example:
function foo() external {
    // Internal call - not traced, no CallInputs generated
    bar();
   
    // External call via 'this' - traced, generates CallInputs
    this.bar();
}

function bar() public {
    // Function implementation
}

State Change Tracking

getStateChanges

Returns state changes for a contract’s storage slot within the current call stack.
Using forge inspect <ContractName> storage-layout is an easy way to get the storage layout of a contract. This can be used to determine the storage slot of a specific variable to use in the cheatcode.
function getStateChanges(
    address contractAddress,
    bytes32 slot
) external view returns (bytes32[] memory stateChanges);
You can see an example of how to use this cheatcode in the State Changes use case mapping.
The array returned includes the initial value of the slot as the first element, so the length of the array is either 0 or >= 2.

Helper Functions

The following helpers convert state changes to specific types. Each has three variants:
  • Basic: Get changes for a single slot
  • Mapping: Get changes using a mapping key
  • Mapping with Offset: Get changes using a key and offset for complex storage

getStateChangesUint

Gets state changes for a storage slot as uint256 values.
// Basic
function getStateChangesUint(
    address target,
    bytes32 slot
) internal view returns (uint256[] memory)

// With mapping
function getStateChangesUint(
    address target,
    bytes32 slot,
    uint256 key
) internal view returns (uint256[] memory)

// With mapping and offset
function getStateChangesUint(
    address target,
    bytes32 slot,
    uint256 key,
    uint256 offset
) internal view returns (uint256[] memory)
Similar helper functions exist for:
  • getStateChangesAddress: Convert to address values
  • getStateChangesBool: Convert to boolean values
  • getStateChangesBytes32: Get raw bytes32 values
Each of these functions follows the same pattern with basic, mapping, and mapping-with-offset variants.

Triggers

Triggers determine when your assertion functions will be executed. You can register different types of triggers in your assertion’s triggers() function.
Use triggers to ensure that assertions are only called when they are needed in order to save gas and resources. For example, instead of triggering an assertion on every call to a contract, you can trigger it only on specific function calls that are able to change the state of the specific value you are asserting on.

Call Triggers

Call triggers execute your assertion when specific contract calls occur.
// Trigger on any call to the contract
// Note: It is not recommended to use this trigger as it will trigger the assertion on every call to the contract
// This can be expensive and inefficient
function registerCallTrigger(
    bytes4 assertionFnSelector
) internal view

// Trigger on specific function calls
function registerCallTrigger(
    bytes4 assertionFnSelector,
    bytes4 triggerSelector
) internal view
Example:
function triggers() external view override {
    // Run assertion on ALL calls to the contract
    registerCallTrigger(this.checkAllCalls.selector);
    
    // Run assertion only on transfer calls
    registerCallTrigger(
        this.checkTransfer.selector,
        IERC20.transfer.selector
    );
}

Storage Triggers

Storage triggers execute your assertion when contract storage changes.
Using forge inspect <ContractName> storage-layout is an easy way to get the storage layout of a contract. This can be used to determine the storage slot of a specific variable to use in the cheatcode.
// Trigger on ANY storage slot change
// Note: It is not recommended to use this trigger as it will trigger the assertion on every storage slot change
// This can be expensive and inefficient
function registerStorageChangeTrigger(
    bytes4 assertionFnSelector
) internal view

// Trigger on changes to a specific storage slot
function registerStorageChangeTrigger(
    bytes4 assertionFnSelector,
    bytes32 slot
) internal view
Example:
function triggers() external view override {
    // Run assertion on ANY storage change
    registerStorageChangeTrigger(this.checkAllStorage.selector);
    
    // Run assertion when totalSupply slot changes
    registerStorageChangeTrigger(
        this.checkSupply.selector,
        TOTAL_SUPPLY_SLOT
    );
}

Balance Triggers

Executes assertion on ETH balance changes.
function registerBalanceChangeTrigger(
    bytes4 assertionFnSelector
) internal view

Combining Triggers

Triggers can be combined for comprehensive coverage:
function triggers() external view override {
    // Multiple triggers for one assertion
    registerStorageChangeTrigger(this.mainInvariant.selector);
    registerCallTrigger(this.mainInvariant.selector);
    
    // Different assertions for different events
    registerBalanceChangeTrigger(this.checkBalances.selector);
    registerCallTrigger(
        this.checkCalls.selector,
        IERC20.transfer.selector
    );
}

Helpers

getAssertionAdopter

Get assertion adopter contract address associated with the assertion triggering transaction.
function getAssertionAdopter() external view returns (address);
This cheatcode removes the need to define an assertion adopter contract in the assertion contract constructor. The cheatcode is only available in the triggers() function and the assertion functions. It cannot be used in the constructor since assertion contracts have no state during deployment.

console.log

Prints debug information to the console during testing with pcl test. Useful for debugging assertion logic and understanding transaction flow.
function console.log(string memory message) external;
Example:
import { console } from "credible-std/console.sol";

function assertionExample() external {
    ph.forkPreTx();
    uint256 preBalance = token.balanceOf(user);
    
    ph.forkPostTx();
    uint256 postBalance = token.balanceOf(user);
    
    console.log("Checking user balance assertion");
    
    require(postBalance >= preBalance, "Balance should not decrease");
}