Testing assertions is very similar to writing regular Forge tests. If you’re familiar with Forge testing, you’ll feel right at home. You get access to all the same testing utilities:

  • Standard assertions (assertEq, assertTrue, etc.)
  • Cheatcodes (vm.prank, vm.deal, vm.warp, etc.)
  • Console logging
  • Test setup with setUp() function

Credible Layer Testing Interface

The Credible Layer extends Forge’s testing capabilities with additional utilities specifically for testing assertions. Here’s the key interface:

contract CLTestEnv {
    // Represents a transaction to test against assertions
    struct AssertionTransaction {
        address from;
        address to;
        uint256 value;
        bytes data;
    }

    // Add an assertion to a contract
    function addAssertion(
        string memory label,          // Identifier for the assertion
        address assertionAdopter,     // Contract to monitor
        bytes memory assertionCode,   // Assertion contract bytecode
        bytes memory constructorArgs  // Constructor arguments
    ) external;

    // Test if a transaction would pass assertions
    function validate(
        string memory label,    // Assertion identifier
        address to,             // Transaction target
        uint256 value,          // ETH value
        bytes calldata data     // Transaction calldata
    ) external;
}

Using the Testing Interface

Here’s how to use these utilities in your tests:

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

contract TestOwnableAssertion is Test, CredibleTest {
    address public newOwner = address(0xdeadbeef);
    Ownable public assertionAdopter;

    function setUp() public {
        assertionAdopter = new Ownable();
        vm.deal(assertionAdopter.owner(), 1 ether);       
    }

    function test_assertionOwnershipChanged() public {
        // Add assertion to a contract
        cl.addAssertion(
            "OwnershipAssertion",                  // Label
            address(assertionAdopter),             // Target contract
            type(OwnableAssertion).creationCode,   // Assertion bytecode
            abi.encode(constructorArgs)            // Constructor args
        );

        // Test a transaction that should invalidate the assertion
        vm.expectRevert(); // Expect assertions validation to revert
        
        cl.validate(
            "OwnershipAssertion",                   // Label
            address(assertionAdopter),              // Target contract
            0,                                      // No ETH value
            abi.encodeCall(                         // Transaction data
                assertionAdopter.transferOwnership, 
                (newOwner)
            )
        );
    }
}

Key concepts:

  • Use cl.addAssertion() to register assertions with contracts you want to monitor
  • Use cl.validate() to test how assertions respond to transactions
  • No return value from cl.validate(), but you can use vm.expectRevert() to check if it reverts
  • Multiple assertions can be linked to the same contract
  • Use the label to specify the assertion contract name for easier debugging

This means you can leverage your existing Forge testing knowledge while using these additional features to verify your assertions work correctly.

Testing Patterns

This section shows useful patterns that can be used when testing assertions.

Batch Operation Testing Pattern

This pattern is used to test assertions that need to verify behavior across multiple operations in a single transaction. It involves creating a helper contract that performs multiple operations in its fallback function, allowing us to test the assertion’s ability to track and validate the cumulative effect of these operations.

Key Components:

  1. A helper contract with a fallback function that performs multiple operations
  2. Testing by calling the helper contract’s fallback with empty calldata
  3. Verification of the final state after all operations

Example Implementation:

// Helper contract that performs multiple operations
contract BatchOperations {
    TargetContract public target;

    constructor(address target_) {
        target = TargetContract(target_);
    }

    fallback() external {
        // Perform multiple operations in a single transaction
        target.deposit(100);
        target.deposit(200);
        target.deposit(300);
    }
}

// Test function
function testMultipleOperations() public {
    // Regular test setup and cl.addAssertion() omitted for brevity
    
    // Create helper contract
    BatchOperations batch = new BatchOperations(address(target));

    // Execute all operations in one transaction
    cl.validate(
        "AssertionLabel",
        address(batch),
        0,
        new bytes(0) // Empty calldata triggers fallback
    );
}

Benefits:

  • Tests multiple operations in a single transaction
  • Verifies that assertions can track cumulative effects
  • More realistic testing of real-world scenarios

This pattern is used in assertions like the Sum of all positions to test how multiple operations affect intra-transaction state changes. It’s particularly valuable for assertions that need to verify invariants across complex sequences of operations within a single transaction.

Protocol Mocking

When testing assertions, it can be difficult to trigger the conditions that would cause an assertion to fail in a real protocol. This is because protocols are designed to be secure and prevent invalid states from occurring. Protocol mocking provides a solution by creating simplified versions of protocols that can be intentionally put into invalid states. For example, you might create a mock protocol that allows direct manipulation of balances or total supply to test if your assertion correctly catches these invalid states.

Key Components:

  1. Create a mock protocol with simplified logic
  2. Test by intentionally putting the protocol into an invalid state
  3. Verify that the assertion correctly catches the invalid state

You can see examples of mock protocols in the Assertion Examples repository.