Skip to main content
This guide walks you through writing your first assertion from scratch. By the end, you’ll have a working assertion that prevents unauthorized ownership transfers.
Use this tutorial to learn the basic shape of an assertion. If you already understand the workflow and need specific syntax, jump to Triggers and the Cheatcodes API Reference.
Prerequisites: Familiarity with Solidity, the Assertions Overview, and pcl installed. What you’ll build: An assertion that blocks any transaction attempting to change a contract’s owner.

The Example Contract

We’ll protect a simple ownership contract:
Our assertion will verify that the owner remains unchanged after each transaction. This protects against attacks like the 2024 Radiant Capital hack where attackers gained multisig access and changed the protocol owner, resulting in $50M losses.

Step 1: Set Up Your Project

Clone the starter repository:
The starter repository includes the complete ownership assertion example from this guide. You can follow along or explore the finished code directly.
This includes the necessary structure:
For manual setup, see the Quickstart Guide.

Step 2: Write the Assertion

Create assertions/src/OwnableAssertion.a.sol:

How It Works

This assertion compares the owner before and after the transaction. If the owner changed, the require fails, the assertion reverts, and the transaction is dropped from the block. In general: if an assertion reverts, the transaction is blocked. This prevents attacks entirely rather than just detecting them.

Key Components

Imports and inheritance:
The Assertion base class provides cheatcodes via the ph namespace. Triggers:
Triggers define when assertions run. Here, assertionOwnershipChange runs whenever transferOwnership is called. Key points about triggers:
  • Each assertion function must be registered via its selector
  • You can define multiple assertion functions in one contract
  • Each trigger maps to exactly one assertion function
  • Use triggers to run assertions only when needed (saves gas)
See Triggers for more trigger types and optimization. Assertion logic:
  • ph.getAssertionAdopter(): Returns the protected contract’s address
  • PhEvm.ForkId: Identifies the pre-transaction and post-transaction snapshots
  • ph.loadStateAt(): Reads the protected contract’s owner slot at the requested snapshot
  • The require blocks the transaction if ownership changed

Best Practices

  1. Single responsibility: Each assertion should verify one property
  2. Use triggers efficiently: Only run assertions when relevant functions are called
  3. Return early: Check simple conditions before complex logic
  4. Use explicit ForkIds: For checks around a matched call, use ph.context() and construct PhEvm.ForkId({forkType: 2, callIndex: ctx.callStart}) or PhEvm.ForkId({forkType: 3, callIndex: ctx.callEnd}).

Step 3: Test the Assertion

Create assertions/test/OwnableAssertion.t.sol:

Key Testing Concepts

  • cl.assertion(): Registers the assertion to run on the next transaction
  • vm.expectRevert(): Verifies the assertion blocks the transaction
  • Test both cases: invalid (ownership changes) and valid (no change)
For more details on testing assertions, see Testing Assertions. Run the tests:

Recap

You’ve built a complete assertion:
  1. Set up: Clone the starter repo with correct structure
  2. Write: Create assertion with triggers and validation logic
  3. Test: Verify it blocks attacks and allows normal operations

Video Walkthrough

For a visual walkthrough of assertion development:

Next Steps

Apply Assertions

Deploy your assertions with pcl apply

From Invariant to Assertion

Learn to write complex real-world assertions

Cheatcodes Reference

All available assertion cheatcodes

Assertions Book

More assertion examples