Use Case & Applications

Ensures protocols maintain proper security controls during emergency pause states, allowing existing users to withdraw funds while preventing new deposits. Critical for yield aggregators and vaults (Yearn, Beefy), lending protocols with pause mechanisms, DEXs with emergency circuit breakers, cross-chain bridges with pause functionality, and any protocol with emergency pause capabilities. Improper handling of pause states can lead to fund lockups, unauthorized access, or users being unable to exit during emergencies.

Explanation

Monitors protocol balance and pause state to ensure proper emergency behavior using a multi-layered approach:
  • ph.forkPreState() / ph.forkPostState(): Capture protocol state before and after transaction
  • getStateChangesUint(): Track all state changes during transaction
  • registerCallTrigger(): Monitor all function calls to the contract
  • Verify protocol balance can only decrease when paused (allowing withdrawals)
  • Detect unauthorized modifications during pause periods
The assertion ensures users can withdraw funds during emergencies while preventing new deposits and maintaining protocol state consistency.
Note: In the future, this assertion could be optimized with a new trigger type that only fires when a specific storage slot has a specific value (e.g., when the pause flag is true). This would reduce unnecessary assertion checks and improve efficiency.
For more information about cheatcodes, see the Cheatcodes Documentation.

Code Example

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

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

contract EmergencyStateAssertion is Assertion {
    function triggers() external view override {
        // Register trigger for all function calls to ensure comprehensive coverage
        registerCallTrigger(this.assertionPanickedCanOnlyDecreaseBalance.selector);
    }

    // Check that if the state is panicked that the pool balance can only decrease
    // This ensures users can withdraw but prevents new deposits
    function assertionPanickedCanOnlyDecreaseBalance() external {
        // Get the assertion adopter address
        IEmergencyPausable adopter = IEmergencyPausable(ph.getAssertionAdopter());

        // Get pre-state values
        ph.forkPreTx();
        bool isPanicked = adopter.paused();
        uint256 preBalance = adopter.balance();

        // Get post-state values
        ph.forkPostTx();
        uint256 postBalance = adopter.balance();

        // If protocol is paused, ensure balance can only decrease
        if (isPanicked) {
            require(postBalance <= preBalance, "Balance can only decrease when panicked");

            // Additional check: verify no unauthorized state changes
            uint256[] memory changes = getStateChangesUint(
                address(adopter),
                bytes32(uint256(1)) // Balance storage slot, change according to your contract
            );

            // Ensure all intermediate states maintain the decrease-only property
            for (uint256 i = 0; i < changes.length; i++) {
                require(changes[i] <= preBalance, "Unauthorized state change during pause");
            }
        }
    }
}

interface IEmergencyPausable {
    function paused() external view returns (bool);
    function balance() external view returns (uint256);
}
Note: Full examples with tests available in the Phylax Assertion Examples Repository.