Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.phylax.systems/llms.txt

Use this file to discover all available pages before exploring further.

When to Use This Pattern

Prevents unauthorized ownership transfers, which could allow attackers to take complete control of the protocol. Critical for DeFi protocols with owner-controlled administrative functions, lending protocols, yield aggregators, cross-chain bridges, and governance systems. For example, in the Radiant Capital hack, the attacker gained control over 3 signers of the multisig, which allowed them to change ownership of the lending pools and ultimately drain the protocol. This assertion would have prevented such an attack.

What This Pattern Checks

Monitors changes to the owner address storage slot in contracts using:
  • _preTx() / _postTx() with Reshiram snapshot reads: Compare owner address before and after transaction
  • ph.loadStateAt(): Load the owner and admin slots at each snapshot
  • registerTxEndTrigger(): Trigger when owner address changes
The assertion performs both direct pre/post comparison and verification of all intermediate state changes to detect unauthorized ownership modifications. For more information about cheatcodes, see the Cheatcodes Documentation.

Assertion Pattern

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

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

contract OwnerChangeAssertion is Assertion {
    bytes32 internal constant OWNER_SLOT = bytes32(uint256(0));
    bytes32 internal constant ADMIN_SLOT = bytes32(uint256(1));

    constructor() {
        registerAssertionSpec(AssertionSpec.Reshiram);
    }

    function triggers() external view override {
        registerTxEndTrigger(this.assertionOwnerChange.selector);
        registerTxEndTrigger(this.assertionAdminChange.selector);
    }

    /// @notice Checks that the owner slot is unchanged across the transaction.
    function assertionOwnerChange() external view {
        address adopter = ph.getAssertionAdopter();
        PhEvm.ForkId memory preFork = _preTx();
        PhEvm.ForkId memory postFork = _postTx();

        address preOwner = _loadAddressAt(adopter, OWNER_SLOT, preFork);
        address postOwner = _loadAddressAt(adopter, OWNER_SLOT, postFork);

        require(preOwner == postOwner, "Owner changed");
    }

    /// @notice Checks that the admin slot is unchanged across the transaction.
    function assertionAdminChange() external view {
        address adopter = ph.getAssertionAdopter();
        PhEvm.ForkId memory preFork = _preTx();
        PhEvm.ForkId memory postFork = _postTx();

        address preAdmin = _loadAddressAt(adopter, ADMIN_SLOT, preFork);
        address postAdmin = _loadAddressAt(adopter, ADMIN_SLOT, postFork);

        require(preAdmin == postAdmin, "Admin changed");
    }

    function _loadAddressAt(address target, bytes32 slot, PhEvm.ForkId memory fork) internal view returns (address) {
        return address(uint160(uint256(ph.loadStateAt(target, slot, fork))));
    }
}
Full examples and mock protocol code are available in credible-std.