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
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
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.
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.
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.Transaction Data
getLogs
Retrieves logs from the assertion triggering transaction.
getAllCallInputs
Gets all call inputs for a given target and selector. This includes all types of calls (CALL, STATICCALL, DELEGATECALL, CALLCODE).
getCallInputs
Gets call inputs for a given target and selector. Only includes calls made using ‘CALL’ opcode.
getStaticCallInputs
Gets static call inputs for a given target and selector. Only includes calls made using ‘STATICCALL’ opcode.
getDelegateCallInputs
Gets delegate call inputs for a given target and selector. Only includes calls made using ‘DELEGATECALL’ opcode.
getCallCodeInputs
Gets call code inputs for a given target and selector. Only includes calls made using ‘CALLCODE’ opcode.
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()
.
getAllCallInputs()
, you may still need to filter out proxy 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: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.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.
getStateChangesAddress
: Convert to address valuesgetStateChangesBool
: Convert to boolean valuesgetStateChangesBytes32
: Get raw bytes32 values
Triggers
Triggers determine when your assertion functions will be executed. You can register different types of triggers in your assertion’striggers()
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.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.Balance Triggers
Executes assertion on ETH balance changes.Combining Triggers
Triggers can be combined for comprehensive coverage:Helpers
getAssertionAdopter
Get assertion adopter contract address associated with the assertion triggering transaction.
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.