Tutorial: build a complete assertion that blocks unauthorized ownership changes
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.
// SPDX-License-Identifier: MITpragma solidity ^0.8.13;contract Ownable { address private _owner; event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); constructor() { _owner = address(0xdead); emit OwnershipTransferred(address(0), _owner); } modifier onlyOwner() { require(_owner == msg.sender, "Ownable: caller is not the owner"); _; } function owner() public view returns (address) { return _owner; } function transferOwnership(address newOwner) public onlyOwner { require(newOwner != address(0), "Ownable: new owner is the zero address"); emit OwnershipTransferred(_owner, newOwner); _owner = newOwner; }}
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.
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.
// SPDX-License-Identifier: MITpragma solidity ^0.8.13;import {OwnableAssertion} from "../src/OwnableAssertion.a.sol";import {Ownable} from "../../src/Ownable.sol";import {CredibleTest} from "credible-std/CredibleTest.sol";import {Test} from "forge-std/Test.sol";contract TestOwnableAssertion is CredibleTest, Test { Ownable public assertionAdopter; address public initialOwner = address(0xf00); address public newOwner = address(0xdeadbeef); function setUp() public { assertionAdopter = new Ownable(initialOwner); vm.deal(initialOwner, 1 ether); } function test_assertionOwnershipChanged() public { assertEq(assertionAdopter.owner(), initialOwner); cl.assertion({ adopter: address(assertionAdopter), createData: type(OwnableAssertion).creationCode, fnSelector: OwnableAssertion.assertionOwnershipChange.selector }); vm.prank(initialOwner); vm.expectRevert("Ownership has changed"); assertionAdopter.transferOwnership(newOwner); assertEq(assertionAdopter.owner(), initialOwner); } function test_assertionOwnershipNotChanged() public { cl.assertion({ adopter: address(assertionAdopter), createData: type(OwnableAssertion).creationCode, fnSelector: OwnableAssertion.assertionOwnershipChange.selector }); vm.prank(initialOwner); assertionAdopter.transferOwnership(initialOwner); }}