Overview

Cheatcodes are PhEVM (Phylax EVM) specific instructions implemented in native code instead of EVM bytecode. They can communicate with Internal APIs and support all existing Ethereum cheatcodes up to Dancun, plus new capabilities for:

  1. Manipulation of the PhEVM state
  2. Forking of state from one or multiple EVM networks
  3. Utilities for writing and managing assertions

Storage Operations

load

Loads a storage slot from an address.

function load(
    address target, 
    bytes32 slot
) external view returns (bytes32 data)

State Management

forkPreState

Updates the active fork to the state before the triggering transaction. Useful for:

  • Checking chain state before a transaction
  • Comparing values between pre and post states
function forkPreState() external

forkPostState

Updates the active fork to the state after the triggering transaction. Useful for:

  • Verifying chain state after a transaction
  • Comparing values between pre and post states
function forkPostState() external

Transaction Data

getLogs

Retrieves all logs emitted by the latest 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)

getCallInputs

Gets the call inputs of all the calls made in the current transaction and returns them in an array of CallInputs structs. This is a useful precompile to use for intra-transaction assertions such as checking if the price reported by an oracle deviates from an allowed range, which could be a sign of an oracle manipulation.

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
}

function getCallInputs(
    address target, 
    bytes4 selector
) external view returns (CallInputs[] memory calls);
Note on Internal Calls

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.

function getStateChanges(
    address contractAddress,
    bytes32 slot
) external view returns (bytes32[] memory stateChanges);

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.

Call Triggers

Call triggers execute your assertion when specific contract calls occur.

// Trigger on any call to the contract
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.

// Trigger on ANY storage slot change
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
    );
}