Important: AI-generated assertions require careful review and testing. Always test your assertions thoroughly before deployment. AI accelerates development but doesn’t replace manual verification.

Overview

AI tools can significantly enhance your assertion development workflow by generating templates that follow Phylax best practices. This guide shows you how to leverage AI assistants and editors like Claude Code, Gemini Code, Cursor, Zed etc. to create a great starting point for your assertions.
This guide focuses on Cursor, but you can adapt the approach for any AI coding assistant.
Our extensive collection of assertion examples and protocol suites provides excellent context for AI to generate high-quality assertions. We always encourage protocols to document their invariants clearly as this improves both security and assertion generation efficiency.

Prerequisites

  • pcl installed (see Installation Guide)
  • credible-std added to your repo
    • forge install credible-std=https://github.com/phylaxsystems/credible-std/
  • Assertions rule file added to your repository (see below)
  • Phylax documentation available as AI context
    • Cursor Settings > Indexing & Docs > add https://docs.phylax.systems
  • Well-defined protocol invariants and security rules (see Defining Effective Invariants)
Add this to .cursor/rules/phylax-assertions.mdc in your repository:
Phylax Assertion Rules
---
description: Any task involving assertion writing or testing of assertion writing. This will be all files in the "assertions/" folder
alwaysApply: false
---
# Phylax Credible Layer Assertion Rules

## Project Structure

### Directory Organization

```bash
project/
├── assertions/
   ├── src/           # Assertion source files (.a.sol)
   └── test/          # Assertion test files (.t.sol)
├── src/               # Protocol smart contracts
├── test/              # Protocol tests
├── script/            # Deployment scripts
├── foundry.toml       # Foundry configuration with assertions profile
└── remappings.txt     # Import remappings for dependencies
```

### File Naming Conventions

- Assertion files: `{ContractOrFeatureName}Assertion.a.sol`
- Test files: `{ContractOrFeatureName}Assertion.t.sol`
- Use descriptive names that indicate the component or module being protected

### Assertion Function Naming

- Assertion functions must start with `assertion{PropertyOrInvariant}`
- Examples: `assertionOwnershipChange`, `assertionBalanceInvariant`, `assertionHealthFactor`
- Use descriptive names that clearly indicate what property or invariant is being verified

### Foundry Configuration

```toml
[profile.assertions]
# Use assertions profile for pcl commands
# FOUNDRY_PROFILE=assertions pcl test

# Optional: Configure specific settings for assertions
# [profile.assertions]
# src = "assertions/src"
# test = "assertions/test"
# out = "out"
# libs = ["lib"]
# remappings = [
#     "credible-std/=lib/credible-std/src/",
#     "forge-std/=lib/forge-std/src/"
# ]
```

#### Profile Usage

- **Environment Variable**: Set `FOUNDRY_PROFILE=assertions` before running pcl commands
- **Command Line**: Use `FOUNDRY_PROFILE=assertions pcl test` to run tests with assertions profile
- **Purpose**: Ensures pcl commands use the correct configuration for assertion development
- **Separation**: Keeps assertion-specific settings separate from main project configuration

### Dependencies Setup

#### Installing credible-std

```bash
# Using git submodules (recommended)
git submodule add https://github.com/phylaxsystems/credible-std.git lib/credible-std

# Or using forge install
forge install phylaxsystems/credible-std
```

#### Remappings Configuration

Add to `remappings.txt`:

```bash
credible-std/=lib/credible-std/src/
forge-std/=lib/forge-std/src/
```

#### Required Dependencies

- `credible-std`: Core Credible Layer functionality
- `forge-std`: Forge standard library for testing
- `openzeppelin-contracts`: (optional, but recommended during development) For utility functions (Strings, etc.)

## Core Assertion Patterns

### Basic Assertion Structure

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import {Assertion} from "credible-std/Assertion.sol";
import {PhEvm} from "credible-std/PhEvm.sol";

contract ProtocolAssertion is Assertion {
    // Required: Register triggers for assertion functions
    function triggers() external view override {
        registerCallTrigger(this.assertionFunction.selector, Protocol.functionToMonitor.selector);
    }

    // Assertion function - must be public/external
    function assertionFunction() external {
        // Get protocol instance
        Protocol protocol = Protocol(ph.getAssertionAdopter());
        
        // State comparison logic
        ph.forkPreTx();
        // ... pre-state logic
        
        ph.forkPostTx();
        // ... post-state logic
        
        // Require statements for invariants
        require(condition, "Descriptive error message");
    }
}
```

### Essential Cheatcodes

#### State Management

- `ph.forkPreTx()` - Snapshot before transaction
- `ph.forkPostTx()` - Snapshot after transaction
- `ph.forkPreCall(callId)` - Snapshot before specific call
- `ph.forkPostCall(callId)` - Snapshot after specific call

#### Call Input Tracking

- `ph.getCallInputs(target, selector)` - Get all CALL opcode inputs
- `ph.getStaticCallInputs(target, selector)` - Get STATICCALL inputs
- `ph.getDelegateCallInputs(target, selector)` - Get DELEGATECALL inputs
- `ph.getCallCodeInputs(target, selector)` - Get CALLCODE inputs
- `ph.getAllCallInputs(target, selector)` - Get all call types (avoid double-counting)

#### State Change Tracking

- `ph.getStateChangesUint(target, slot)` - Track uint state changes
- `ph.getStateChangesAddress(target, slot)` - Track address state changes
- `ph.getStateChangesBytes32(target, slot)` - Track bytes32 state changes

#### Protocol Access

- `ph.getAssertionAdopter()` - Get the contract being protected

### Constructor Usage

- **Prefer `ph.getAssertionAdopter()` over constructors** for getting the protected contract address
- Constructors run against empty state and cannot read from other contracts
- Use constructors only for additional values your assertion needs (e.g., token addresses)
- If using constructor arguments, append them to creation code: `abi.encodePacked(type(Assertion).creationCode, abi.encode(arguments))`
- Constructor storage persists in the assertion contract

### Trigger Registration Patterns

#### Call Triggers

```solidity
function triggers() external view override {
    // Single function trigger
    registerCallTrigger(this.assertionFunction.selector, Protocol.functionToMonitor.selector);
    
    // Multiple functions trigger same assertion
    registerCallTrigger(this.assertionFunction.selector, Protocol.function1.selector);
    registerCallTrigger(this.assertionFunction.selector, Protocol.function2.selector);
}
```

#### Storage Change Triggers

```solidity
function triggers() external view override {
    // Trigger on specific storage slot changes
    registerStorageChangeTrigger(this.assertionFunction.selector, bytes32(uint256(0)));
}
```

#### Balance Change Triggers

```solidity
function triggers() external view override {
    // Trigger on balance changes
    registerBalanceChangeTrigger(this.assertionFunction.selector, address(0x...));
}
```

### Common Assertion Patterns

#### Pre/Post State Comparison

```solidity
function assertionStateInvariant() external {
    Protocol protocol = Protocol(ph.getAssertionAdopter());
    
    ph.forkPreTx();
    uint256 preValue = protocol.someValue();
    
    ph.forkPostTx();
    uint256 postValue = protocol.someValue();
    
    require(postValue >= preValue, "Value cannot decrease");
}
```

#### Call Input Analysis

```solidity
function assertionCallValidation() external {
    Protocol protocol = Protocol(ph.getAssertionAdopter());
    
    PhEvm.CallInputs[] memory calls = ph.getCallInputs(
        address(protocol), 
        protocol.functionToMonitor.selector
    );
    
    for (uint256 i = 0; i < calls.length; i++) {
        // Decode function parameters
        (uint256 param1, address param2) = abi.decode(calls[i].input, (uint256, address));
        
        // Validate parameters
        require(param1 > 0, "Parameter must be positive");
        require(param2 != address(0), "Address cannot be zero");
    }
}
```

#### Multi-Layered Validation

```solidity
function assertionComplexInvariant() external {
    Protocol protocol = Protocol(ph.getAssertionAdopter());
    
    // Layer 1: Basic state check (cheapest)
    ph.forkPreTx();
    uint256 preBalance = protocol.balance();
    
    ph.forkPostTx();
    uint256 postBalance = protocol.balance();
    
    if (postBalance < preBalance) {
        // Layer 2: Detailed analysis only if basic check fails
        PhEvm.CallInputs[] memory calls = ph.getCallInputs(
            address(protocol), 
            protocol.withdraw.selector
        );
        
        uint256 totalWithdrawn = 0;
        for (uint256 i = 0; i < calls.length; i++) {
            uint256 amount = abi.decode(calls[i].input, (uint256));
            totalWithdrawn += amount;
        }
        
        require(postBalance == preBalance - totalWithdrawn, "Balance mismatch");
    }
}
```

#### Intra-Transaction State Monitoring

```solidity
function assertionIntraTxOracleDeviation() external {
    IOracle oracle = IOracle(ph.getAssertionAdopter());
    
    // Get initial state before transaction
    ph.forkPreTx();
    uint256 initialPrice = oracle.price();
    
    // Calculate allowed deviation thresholds
    uint256 maxAllowedPrice = (initialPrice * 110) / 100; // +10%
    uint256 minAllowedPrice = (initialPrice * 90) / 100;  // -10%
    
    // Check final state after transaction
    ph.forkPostTx();
    uint256 finalPrice = oracle.price();
    require(
        finalPrice >= minAllowedPrice && finalPrice <= maxAllowedPrice,
        "Oracle final price deviation exceeds threshold"
    );
    
    // Get all price update calls in this transaction
    PhEvm.CallInputs[] memory priceUpdates = ph.getCallInputs(
        address(oracle), 
        oracle.updatePrice.selector
    );
    
    // Check each intermediate state after each call
    for (uint256 i = 0; i < priceUpdates.length; i++) {
        // Snapshot state after this specific call
        ph.forkPostCall(priceUpdates[i].id);
        
        // Check price at this point in the transaction
        uint256 intermediatePrice = oracle.price();
        require(
            intermediatePrice >= minAllowedPrice && intermediatePrice <= maxAllowedPrice,
            "Oracle intra-tx price deviation exceeds threshold"
        );
    }
}
```

## Testing Patterns

### Test Structure

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import {CredibleTest} from "credible-std/CredibleTest.sol";
import {Test} from "forge-std/Test.sol";
import {ProtocolAssertion} from "../src/ProtocolAssertion.a.sol";
import {Protocol} from "../../src/Protocol.sol";

contract TestProtocolAssertion is CredibleTest, Test {
    Protocol public assertionAdopter;
    ProtocolAssertion public assertion;

    function setUp() public {
        assertionAdopter = new Protocol();
        assertion = new ProtocolAssertion();
        
        // Setup test environment
        vm.deal(address(0xBEEF), 100 ether);
    }

    function testAssertionPasses() public {
        // Register assertion for next transaction
        cl.assertion({
            adopter: address(assertionAdopter),
            createData: type(ProtocolAssertion).creationCode,
            fnSelector: ProtocolAssertion.assertionFunction.selector
        });

        // Execute valid transaction
        vm.prank(address(0xBEEF));
        assertionAdopter.safeFunction();
    }

    function testAssertionFails() public {
        cl.assertion({
            adopter: address(assertionAdopter),
            createData: type(ProtocolAssertion).creationCode,
            fnSelector: ProtocolAssertion.assertionFunction.selector
        });

        // Execute invalid transaction
        vm.prank(address(0xBEEF));
        vm.expectRevert("Descriptive error message");
        assertionAdopter.unsafeFunction();
    }
}
```

### Testing Specifics

- **One `cl.assertion()` can only trigger one assertion function per test**
- Create separate test functions for each assertion function that can be triggered
- Test each assertion function in isolation to ensure proper coverage
- Use different test scenarios for different assertion functions

### Testing Commands

- Use `pcl test` instead of `forge test` for assertion testing
- Use `FOUNDRY_PROFILE=assertions pcl test` to ensure correct profile
- Use `pcl build` to compile assertions
- **pcl test supports all forge test commands** for debugging:
  - `pcl test --match-test testFunctionName` - Run specific test function
  - `pcl test --match-contract TestContractName` - Run specific test contract
  - `pcl test path/to/test/file.t.sol` - Run specific test file
  - `pcl test -vvv` - Verbose output for debugging
  - Use these commands to isolate and debug specific test cases

### Testing Best Practices

- Test both positive (should pass) and negative (should fail) cases
- Use descriptive test function names
- Test edge cases and boundary conditions
- Use `vm.expectRevert()` with exact error messages
- Test with realistic data and scenarios
- **Utilize forge-std cheatcodes** for testing scenarios:
  - `vm.deal()` for minting ETH to addresses
  - `vm.prank()` for impersonating addresses
  - `vm.roll()` for manipulating block numbers
  - `vm.warp()` for manipulating timestamps
  - `vm.mockCall()` for mocking external calls
  - Use these tools to create comprehensive test scenarios

### Testing with Mock Protocols

- **It may not be possible to simulate failing assertion for well-implemented protocols** - this is expected behavior
- **Create mock protocols** that simulate vulnerable behavior to test assertion failure cases
- Use mock contracts that intentionally violate invariants to verify assertions catch violations
- Test assertion logic by temporarily removing safety checks in mock protocols
- This ensures assertions are working correctly even when the real protocol is secure

## Common Pitfalls and Anti-Patterns

### Gas Limit Issues

- Avoid expensive operations in loops
- Use early returns for basic checks
- Monitor gas usage with `pcl test`

### Call Input Double-Counting

```solidity
// ❌ WRONG: May include duplicate calls from proxy contracts
PhEvm.CallInputs[] memory calls = ph.getAllCallInputs(target, selector);

// ✅ CORRECT: Use specific call type
PhEvm.CallInputs[] memory calls = ph.getCallInputs(target, selector);
```

### Incorrect Function Selectors

```solidity
// ❌ WRONG: Using wrong selector or bytes signature
registerCallTrigger(this.assertionFunction.selector, bytes4(0x12345678));

// ✅ CORRECT: Use actual name of function selector
registerCallTrigger(this.assertionFunction.selector, Protocol.targetFunction.selector);
```

### Missing Error Messages

```solidity
// ❌ WRONG: No error message
require(condition);

// ✅ CORRECT: Descriptive error message
require(condition, "Specific invariant violation description");
```

### State Persistence Issues

- Use `cl.assertion()` for testing
- State changes from non-reverting transactions are persisted

## Best Practices

### General Solidity Practices

- **Regular Solidity best practices apply** to assertion contracts
- Follow standard coding conventions and patterns
- Use proper error handling and validation
- Implement clean, readable, and maintainable code
- Follow security best practices for smart contract development

### Single Responsibility

- Each assertion should verify one specific property
- Split complex assertions into multiple focused functions
- Use descriptive function names starting with "assertion" (see Assertion Function Naming section)

### Gas Optimization

- Assertions have 100k gas limit per assertion function - aim to stay under this limit
- Check basic invariants first (fail fast)
- Use early returns for simple checks
- Avoid expensive operations in loops
- Monitor gas usage during development

### Error Handling

- Use descriptive error messages
- Include context in error messages
- Test error conditions thoroughly

### Documentation

- Comment complex logic
- Follow best practice Solidity style for comments
- Explain the invariant being protected
- Document any assumptions or limitations
- Keep documentation in the code files, unless specified otherwise

### Testing Coverage

- Test both valid and invalid scenarios
- Test edge cases and boundary conditions
- Test with realistic data
- Use fuzz testing for parameter validation
- Create separate tests for each assertion function (see Testing Specifics)

## Debugging Tips

### Console Logging

```solidity
import {console} from "forge-std/console.sol";
import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol";

function assertionFunction() external {
    // Use string concatenation since console.log only accepts strings
    console.log(string.concat("Pre-state value: ", Strings.toString(someValue)));
    ph.forkPreTx();
    
    ph.forkPostTx();
    console.log(string.concat("Post-state value: ", Strings.toString(someValue)));
}
```

### Gas Monitoring

- Use `pcl test -vvv` for detailed gas usage
- Monitor gas consumption in loops
- Optimize expensive operations

### Common Debugging Patterns

- Add intermediate state checks
- Use console.log for debugging
- Test with minimal examples
- Verify function selectors match expected signatures

## Integration Guidelines

### Protocol Integration

- Use `ph.getAssertionAdopter()` instead of constructor parameters
- Ensure proper interface definitions
- Handle protocol-specific edge cases

### Deployment Workflow

1. Write and test assertions locally with `pcl test`
2. Build assertions with `pcl build`
3. Deploy protocol contracts
4. Submit assertions to Credible Layer
5. Activate assertions for protection

Defining Effective Invariants for Assertions Generation

  • Get a basic understanding of invariants in this guide by our DevRel Jacob
  • Focus on security-critical invariants that protect against real attacks
  • Avoid invariants already enforced by require statements with CEI or FREI-PI patterns as these are already covered in your code
  • Target invariants that cannot be directly checked with require statements, such as:
    • State changes occurring in the callstack
    • Price manipulation attacks via flash loans
    • Cross-function state inconsistencies
  • Define invariants in simple, clear language
  • Specify the contracts and functions involved
  • Review AI-generated invariants carefully for accuracy
Reference Aave’s invariants for excellent examples of clear invariant documentation.

Prompt Template

Use this template in your protocol repository:
Create Phylax assertions to protect the protocol in this repository.

The invariants that should be protected are defined in this document: [reference your invariants document]

If invariant tests are available check them for additional context.

The user-facing contracts that can trigger assertions are: [list contracts]

The most critical logic is in the following contracts: [list contracts]

The rules for writing assertions are available in: [reference your rules file]

The Phylax documentation to follow is: [reference documentation]

Share your implementation plan before making code changes.

Ask any clarifying questions to ensure alignment on the approach before you start making changes to the code.

Example: Origin Protocol

We’ll demonstrate using Origin Protocol’s contracts as an example. Their token logic documentation provides clear invariant definitions.

Sample Prompt

Create Phylax assertions for the Origin Protocol in this repository. 

The invariants that the assertions should implement are defined in this document: @README-token-logic

Follow the rules for writing assertions in @phylax-assertions

You can consult the Phylax documentation for additional context: @phylax-docs

Share your implementation plan before making code changes.

Ask any clarifying questions to ensure alignment on the approach.
The AI will implement assertions in a new assertions folder at the repository root.

Reviewing Generated Assertions

AI-generated assertions are typically high-quality but require careful review:
  • Verify the assertions make logical sense for your protocol
  • Check that they use the most efficient data retrieval methods and use as little gas as possible
  • Ask the AI for a detailed overview of what each assertion covers
  • Ensure they follow Phylax best practices

Refining Assertions

If assertions need correction or optimization, provide specific instructions to the AI:
Add protocol-specific instructions to your rules file or create a protocol-specific rules file for AI context.

Generating Test Cases

Comprehensive testing ensures assertions correctly identify valid and invalid states. AI can generate test suites for your assertions.

Test Generation Prompt

Based on the assertions in `@assertions/src`, generate test cases for each assertion function. Test both positive and negative scenarios following the patterns in @phylax-assertions.

Share your implementation plan before making code changes.

Ask any clarifying questions to ensure alignment on the approach.

Testing Workflow

The AI will implement tests in assertions/test. Even with passing tests, review them carefully:
  • Verify test scenarios are realistic and comprehensive
  • Check that tests aren’t just assert(true) statements
  • Use test failures to identify and fix assertion bugs
  • Ask the AI to adjust assertions based on discovered issues

Best Practices

1

Start with clear invariants

Document your protocol’s security invariants in simple language before using AI.
2

Use the Rules File

Use the rules file to guide the AI’s assertion generation.
3

Use structured prompts

Follow the provided template to ensure comprehensive coverage.
4

Review thoroughly

Never deploy AI-generated assertions without manual review and testing.
5

Iterate and refine

Use AI feedback to improve both assertions and your invariant documentation.

Next Steps

Join the Phylax Telegram for community support and to share your improvements to the rules file.