Introduction

This guide will walk you through the process of creating, testing, and submitting an assertion using the pcl (Phylax Credible Layer) CLI. By the end of this tutorial, you’ll understand how to:

  1. Set up your project structure
  2. Write a simple assertion
  3. Test your assertion
  4. Authenticate with pcl
  5. Deploy your contract
  6. Create a project
  7. Store your assertion
  8. Submit your assertion to the Credible Layer
  9. Activate your assertion
  10. Verify that the assertion is working

Prerequisites

Before you begin, make sure you have:

0. Lazy Mode

We’ve created an example project with the content of this guide that you can use to follow along and use as a starting point for your own projects. The project can be found here.

The credible-layer-starter repo has several examples that you can deploy and try out once you’re done with this guide. Specfic instructions can be found in the README.

You can clone the example project by running the following command:

git clone --recurse-submodules https://github.com/phylaxsystems/credible-layer-starter.git
cd credible-layer-starter

You can jump directly to the Running Tests section if you have the example project cloned and pcl installed.

1. Project Setup

First, let’s set up a project with the correct directory structure:

mkdir pcl-tutorial-project
cd pcl-tutorial-project

Create the required directories:

mkdir -p assertions/src assertions/test src lib

Next, in order to make sure that forge works correctly, we need to the root folder of the project to be a git repository.

git init

Installing forge-std

Next, we need to install forge-std as a project dependency:

forge install foundry-rs/forge-std --no-commit

This will add forge-std as a dependency to your project, which is required for running tests and managing dependencies.

Installing the credible-std Library

Next, you need to install the credible-std library, which provides the base contracts and utilities for writing assertions:

forge install credible-std=https://github.com/phylaxsystems/credible-std/ --no-commit

After installation, create a remappings.txt file at the root of your project with the following content:

credible-std/=lib/credible-std/src/
forge-std/=lib/credible-std/lib/forge-std/src/

These remappings will ensure that your imports work correctly when referencing the credible-std and forge-std libraries. The pcl CLI will automatically detect and use these remappings when compiling your contracts.

Next, let’s create the smart contract that our assertion will monitor. This is a simple Ownable contract that tracks ownership of a contract.

Create a file src/Ownable.sol:

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

contract Ownable {
    address private _owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    constructor(address initialOwner) {
        _owner = initialOwner;
        emit OwnershipTransferred(address(0), _owner);
    }

    modifier onlyOwner() {
        require(_owner == msg.sender, "Ownable: caller is not the owner");
        _;
    }

    // Get the current 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;
    }
}

Currently, we rely on the contract having an owner() function for ownership verification during project creation, so make sure to have a owner() function in your contract.

2. Writing Your First Assertion

Assertions in the Credible Layer are Solidity contracts that inherit from the Assertion base contract. They define checks that can be run against smart contracts to verify specific properties.

Let’s create a simple assertion that checks if an Ownable contract’s ownership has changed after a transaction.

For a detailed breakdown of the assertion code, see the Assertion Guide.

Create a file assertions/src/OwnableAssertion.a.sol:

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

import {Assertion} from "credible-std/Assertion.sol"; // Using remapping for credible-std
import {Ownable} from "../../src/Ownable.sol"; // Ownable contract

contract OwnableAssertion is Assertion {
    // The contract we're monitoring
    Ownable ownable;

    // Constructor takes the address of the contract to monitor
    constructor(address ownable_) {
        ownable = Ownable(ownable_); // Initialize the Ownable contract reference
    }

    // The triggers function tells the Credible Layer which assertion functions to run
    // This is required by the Assertion interface
    function triggers() external view override {
        // Register our assertion function to be called when transactions interact with the Ownable contract
        registerCallTrigger(this.assertionOwnershipChange.selector);
    }

    // This assertion checks if ownership has changed between pre and post transaction states
    function assertionOwnershipChange() external {
        // Create a snapshot of the blockchain state before the transaction
        ph.forkPreState(); // For more details on cheatcodes like this, see [PCL Cheatcodes Reference](credible/cheatcodes)
        
        // Get the owner before the transaction
        address preOwner = ownable.owner();
        
        // Create a snapshot of the blockchain state after the transaction
        ph.forkPostState();
        
        // Get the owner after the transaction
        address postOwner = ownable.owner();
        
        // Assert that the owner hasn't changed
        // If this requirement fails, the assertion will revert
        require(postOwner == preOwner, "Ownership has changed");
    }
}

Key Components of an Assertion:

  1. Inheritance: All assertions must inherit from the Assertion base contract.
  2. Constructor: Initialize references to the contract you want to monitor. You can hardcode the address of the contract you want to monitor in the assertion contract, but you lose the flexibility of being able to use the same assertion for different smart contracts.
  3. Triggers Function: Register which assertion functions should be triggered.
  4. Assertion Functions: Implement the actual checks using pre and post transaction states.

3. Testing Your Assertion

To test your assertion, create a test file in the assertions/test directory:

Create a file assertions/test/OwnableAssertion.t.sol:

// SPDX-License-Identifier: MIT
pragma 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 {
    // Contract state variables
    Ownable public assertionAdopter;
    address public initialOwner = address(0xdead);
    address public newOwner = address(0xdeadbeef);

    // Set up the test environment
    function setUp() public {
        assertionAdopter = new Ownable(initialOwner);
        vm.deal(initialOwner, 1 ether);
    }

    // Test case: Ownership changes should trigger the assertion
    function test_assertionOwnershipChanged() public {
        address aaAddress = address(assertionAdopter);
        string memory label = "OwnableAssertion";

        // Associate the assertion with the protocol
        // cl will manage the correct assertion execution when the protocol is called
        cl.addAssertion(label, aaAddress, type(OwnableAssertion).creationCode, abi.encode(assertionAdopter));

        // Simulate a transaction that changes ownership
        vm.prank(initialOwner);
        vm.expectRevert("Assertions Reverted");
        cl.validate(
            label, aaAddress, 0, abi.encodePacked(assertionAdopter.transferOwnership.selector, abi.encode(newOwner))
        );
    }

    // Test case: No ownership change should pass the assertion
    function test_assertionOwnershipNotChanged() public {
        string memory label = "OwnableAssertion";
        address aaAddress = address(assertionAdopter);

        cl.addAssertion(label, aaAddress, type(OwnableAssertion).creationCode, abi.encode(assertionAdopter));

        // Simulate a transaction that doesn't change ownership (transferring to same owner)
        vm.prank(initialOwner);
        cl.validate(
            label, aaAddress, 0, abi.encodePacked(assertionAdopter.transferOwnership.selector, abi.encode(initialOwner))
        );
    }
}

4. Running Tests

Use the pcl CLI to run your tests:

pcl test

This command will compile your assertion and run the tests. You should see output looking like this indicating that the tests have passed:

Ran 2 tests for assertions/test/OwnableAssertion.t.sol:TestOwnableAssertion
[PASS] test_assertionOwnershipChanged() (gas: 806650)
[PASS] test_assertionOwnershipNotChanged() (gas: 804708)
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 648.17ms (1.11s CPU time)

Troubleshooting Test Issues

If your tests fail, check for these common issues:

  • Compilation errors: Ensure your Solidity syntax is correct
  • Incorrect imports: Verify all import paths are correct
  • State mismatch: Make sure your test properly sets up the initial state
  • Assertion logic: Double-check the logic in your assertion function

5. Deploy Your Contract

You can deploy the Ownable contract using the following command:

forge create src/Ownable.sol:Ownable \
       --rpc-url <RPC_URL> \
       --private-key <PRIVATE_KEY_YOU_USED_TO_SIGN_IN_TO_DAPP> \
       --broadcast \
       --constructor-args <INITIAL_OWNER_ADDRESS>

Explanation of the arguments:

  • <RPC_URL>: The RPC URL of the network you’re deploying to
  • <PRIVATE_KEY_YOU_USED_TO_SIGN_IN_TO_DAPP>: The private key of the account you used to sign in to the dApp
  • <INITIAL_OWNER_ADDRESS>: The address of the initial owner of the contract. Use the same address as you will be using in your browser wallet to authenticate with the Credible Layer.

Make sure to note down the address of the deployed contract as you’ll need it to create a project in the next step. It will be the Deployed to: address in the output of the command.

6. Authenticating with Credible Layer

Here’s a full video that you can consult to follow along with the process entire process from authentication to activating the assertion:

Before submitting your assertion, you need to authenticate:

pcl auth login

Make sure to use the same address as you set as the initial owner of the contract in the previous step.

This will provide you with with a URL and an authentication code that you can use to authenticate with the Credible Layer.

If authentication fails, ensure:

  • Your wallet has the correct network selected
  • The pcl CLI uses the correct url
  • You have an internet connection
  • The pcl CLI is properly installed

7. Create a Project

Once you have deployed your contract, you’ll need to create a project in the dApp if you don’t have one already. Navigate to the browser window opened by the pcl auth login command and create a new project. When asked to link your contract, use the address of the contract you deployed in the previous step.

For a more detailed overview of how to use the dApp and manage projects, see the dApp Guide.

8. Storing Your Assertion

Next, store your assertion in the Assertion Data Availability layer (Assertion DA):

pcl store OwnableAssertion <ADDRESS_OF_OWNABLE_CONTRACT>

Here OwnableAssertion is the name of the assertion and 0xADDRESS_OF_OWNABLE_CONTRACT is the address of the contract you want to protect.

The 0xADDRESS_OF_OWNABLE_CONTRACT is a constructor argument for the assertion, so if your assertion contract has a constructor argument, you need to provide it when storing the assertion.

This command submits your assertion’s bytecode and source code to be stored by the Assertion DA, making it available for verification by the network.

9. Submitting Your Assertion

Finally, submit your assertion to the Credible Layer dApp:

pcl submit

This will prompt you to select the project and assertion(s) you want to submit. Follow the interactive prompts to complete the submission.

Alternatively, you can specify the project and assertion directly as per the output of the pcl store command:

pcl submit -a 'OwnableAssertion(0xADDRESS_OF_OWNABLE_CONTRACT)' -p <project_name>

Note, that <project_name> is the name of the project you created in the dApp, capitalized in the same way as you did when creating the project.

10. Activating Your Assertion

Last step is to go to the dApp and activate the assertion. Go back to the url that you opened with the pcl auth login command and navigate to the project that the assertions was added to.

You’ll notice that there’s one assertion ready for submission, go ahead and proceed to review and activate it.

For a more detailed overview of how to use the dApp, see the dApp Guide.

11. Verify That The Assertion Is Working

Now that your assertion is activated, let’s verify that it’s working as expected. We’ll do this by attempting to change the ownership of the contract, which should trigger our assertion and prevent the change.

First, let’s check the current owner of the Ownable contract. Replace ADDRESS_OF_OWNABLE_CONTRACT with the address of your deployed Ownable contract and RPC_URL with your network’s RPC URL:

cast call <ADDRESS_OF_OWNABLE_CONTRACT> "owner()" --rpc-url <RPC_URL>

This command should return the initial owner address that was set when we deployed the contract.

Next, let’s attempt to transfer ownership to a new address. Make sure you replace NEW_OWNER_ADDRESS with an address that is not the initial owner and PRIVATE_KEY_OF_THE_OWNER with the private key of the owner of the contract. This transaction should trigger the assertion and revert:

cast send <ADDRESS_OF_OWNABLE_CONTRACT> "transferOwnership(address)" <NEW_OWNER_ADDRESS> --rpc-url <RPC_URL> --private-key <PRIVATE_KEY_OF_THE_OWNER> --timeout 20

The transaction should timeout after about 20 seconds which means that the assertion reverted the transaction:

Error: transaction was not confirmed within the timeout

If you try to do another transaction with the same private key, you will most likely get this a replacement transaction error:

- server returned an error response: error code -32603: replacement transaction underpriced

This is a known limitation of the system - when an assertion reverts a transaction, it gets dropped by the builder rather than being included in a block. This means that wallets and tools like cast will still increment their local nonce, potentially causing issues with subsequent transactions. While this creates some UX friction, it only occurs when someone attempts to violate an assertion (i.e., attempt to hack a protocol), so we consider this an acceptable tradeoff. In the future, we plan to work with wallet providers to better surface these dropped transactions.

We recommend doing a simple ether transfer with a higher gas price, to replace the dropped transaction:

cast nonce <your-address> --rpc-url <your-rpc>

and then use the nonce to send a new transaction:

cast send <your-address> --value 0 --gas-price <higher-gas-price> --nonce <nonce> --private-key <your-private-key> --rpc-url <your-rpc>

To confirm that the ownership hasn’t changed, let’s check the owner again:

cast call <ADDRESS_OF_OWNABLE_CONTRACT> "owner()" --rpc-url <RPC_URL>

The owner should still be the original address, confirming that our assertion successfully prevented the ownership change.

Conclusion

Congratulations! You’ve successfully created, tested, activated and verified your first assertion using the Credible Layer CLI. You can now go ahead and start implementing assertions in your own projects.

Next Steps

  1. Read the Assertions Book: Check out the Assertions Book for more detailed explanations and a collection of assertions for various use cases
  2. Try more complex assertions: We’ve created some more assertions in the credible-layer-starter repo that are ready to be deployed and used with a couple of commands
  3. Integrate with your own projects: Apply assertions to your existing smart contracts
  4. Join the community: Share your assertions and learn from others in the Phylax Telegram

For more detailed information about the Credible Layer CLI and its commands, see the CLI Reference Guide.

For a comprehensive list of terms and concepts used in the Credible Layer, see the Glossary.