Introduction
The Credible Layer is a security prevention framework that helps protect smart contracts by continuously verifying critical protocol invariants. This guide assumes that you have already installed the Credible Layer. If you haven’t, please see the Credible Layer Quickstart for instructions. For a visual learners a video introduction to assertions is available: We will use a simple ownership protocol as an example that demonstrates how to protect against unauthorized ownership transfers - a critical security concern in smart contracts.1. Setting Up Your Project
For detailed instructions on setting up your Credible Layer project structure, please refer to the Credible Layer Quickstart Guide. Alternatively, you can quickly get started by cloning the Credible Layer starter repository:2. Writing the Assertion
Let’s create our assertion inOwnableAssertion.a.sol
. Here’s the complete code first, followed by a detailed breakdown:
How Assertions Work
The key principle of assertions is simple: if an assertion reverts, it means the assertion failed and an undesired state was detected. In our case, the assertion will revert if the contract’s owner changes, allowing us to prevent unauthorized ownership transfers. If an assertion reverts, the transaction resulting in the undesired state is reverted and not included in the block. This results in actual hack prevention, not just mitigation.Gas Consumption
Assertion functions are limited to a maximum execution of 100,000 gas. If an assertion function exceeds this gas limit, the transaction will be invalidated and dropped. Please be aware that the gas consumption of certain cheatcodes, such asgetCallInputs
, can be variable, due to unknown lengths of the call inputs.
We are actively working to improve this as soon as possible. Staying mindful of gas consumption is in general a good practice.
See the Assertion Patterns for more details on ways to optimize gas consumption.
Anatomy of the Assertion
Let’s break down the key components of our assertion:1. Imports and Contract Definition
- The contract inherits from
Assertion
, which provides the base functionality and cheatcodes for assertions - We import the contract we want to protect (
Ownable
)
2. State Variables and Constructor
- If you are defining state variables for additional contracts your assertion should know about this can be done in the constructor.
- The constructor is optional and can be initialized during the storing and submitting of the assertion through the
pcl
. - In this example we entirely leave out the constructor, since we use the
ph.getAssertionAdopter()
cheatcode to get the address of the contract we want to protect. - The constructor runs against an empty state. This means you can pass values in the constructor but you can not read values from other contracts.
- The constructor can be used to set additional values that your assertion contract should have access to. For example, the address of a token that you are checking the balance of.
- Storage that is set in the constructor persists in the assertion contract.
- Cheatcodes/precompiles are not accessible in the constructor.
3. Triggering Assertions
- The
triggers
function is required by theAssertion
interface. - Each assertion function must be registered here via its function selector.
- Multiple assertions can be defined in a single contract.
registerCallTrigger
specifies the function selector of the assertion function to run and the function selector of the protected contract function that should trigger the assertion.- A trigger can only trigger one assertion function.
- Use triggers to make sure that assertions are only called when they are needed in order to save gas and resources.
- The Credible Layer supports different trigger types. See the “Triggers” section in the cheatcodes documentation for more information.
4. Assertion Logic
- The main assertion logic uses cheatcodes:
ph.forkPreTx()
: Creates a snapshot of the blockchain state before the transaction.ph.forkPostTx()
: Creates a snapshot of the blockchain state after the transaction.
- The assertion reverts if the condition is not met. You can have several require checks in the same function to make sure no rules are violated.
- The
ph
namespace gives access to the Credible Layer cheatcodes, which provide utility functions for assertions. For more information see the cheatcodes documentation. - All assertion functions must be public or external to be callable by the Credible Layer.
It is possible to call write functions inside assertion functions. This is not recommended though, since this leads to unexpected behavior.
Assertions should only be thought of as a way to check the current state of a protocol and revert if an undesired state is detected.
Best Practices for Writing Assertions
- Single Responsibility: Each assertion should verify one specific property or invariant.
- Sometimes it makes sense to check multiple properties in the same assertion function.
- Aim to have as many specific assertion functions as possible, since the Credible Layer executes assertions in parallel.
- State Management: Use
forkPreTx()
andforkPostTx()
to compare values before and after transactions. - State Management: Use
forkPreCall()
andforkPostCall()
to compare values before and after specific calls. - Gas Optimization: Use triggers to make sure that assertions are only called when they are needed in order to save gas and resources.
- Return Early: When writing more complex assertions checking multiple function calls in the callstack, it is recommended to check basic invariants first before starting to iterate over the callstack.
- Debugging: Use
console.log()
in assertion functions to print debug information to the console during testing withpcl test
.
3. Testing the Assertion
Testing assertions is a critical step in developing effective security prevention. Since assertions act as automated guardians for your protocol, it’s essential to verify they correctly identify both safe and unsafe conditions. A false positive could unnecessarily block legitimate protocol operations, while a false negative could miss critical security violations. To run the tests, use the following command:- Verify that assertions fail when they should (e.g., when detecting unauthorized changes)
- Confirm that assertions pass during normal operation
- Simulate realistic protocol interactions as well as edge cases
Test Imports
OwnableAssertion
: Imports the assertion we created that we want to testOwnable
: Imports the protocol contract that our assertion is protectingCredibleTest
: Provides special testing utilities for assertion testing, including thecl
cheatcode interfaceTest
: Standard testing base contract from Forge, providing utilities likevm.prank
andvm.expectRevert
Test Contract Structure
- Inherits from
CredibleTest
andTest
which provides testing utilities CredibleTest
provides thecl
cheatcode interface for testing assertionsTest
is the standard Forge test base contractassertionAdopter
is the contract we’re protectingnewOwner
is used to test ownership transfersinitialOwner
is the owner of the contract before the tests are run
Setup
- Creates a new instance of the
Ownable
contract for testing - Gives the owner some ETH using Forge’s
vm.deal
- This setup runs before each test function
Testing Assertion Reverted
This test checks that the assertion reverts when ownership changes.cl.assertion()
: Sets up the assertion to be run on the next transactionadopter
: Address of contract to protectcreateData
: Bytecode of the assertion contractfnSelector
: Selector of the assertion function to run
vm.prank()
: Simulates a call from the owner’s addressvm.expectRevert()
: Explicitly expects the assertion to revert with “Ownership has changed”assertionAdopter.transferOwnership(newOwner)
: Simulates a transaction that changes ownershipassertEq(assertionAdopter.owner(), initialOwner)
: Checks that the owner didn’t change
cl
is the cheatcode exposed by the CredibleTest
contract used specifically for testing assertions. It provides tools to validate your assertions under different conditions without having to deploy them to a real blockchain.Testing Assertion Success
- Similar to the failing test, but tests the case where no ownership change occurs.
- Uses transferOwnership to the same owner to simulate a transaction that doesn’t change the state in ways that violate our assertion
transferOwnership
will not revert if the assertion passes so we don’t need to usevm.expectRevert()
Full Test Contract
Key Testing Concepts
- Assertion Registration: Use
cl.assertion()
to register assertions that will be run on the next transaction - State Manipulation: Use Forge’s cheatcodes (
vm.prank
,vm.deal
) to set up test scenarios - Verification: Use
vm.expectRevert()
to verify that the assertion reverts when expected
Running the Tests
Conclusion
Assertions provide a powerful mechanism for protecting your protocol’s state and preventing security violations in real-time. By implementing well-designed assertions, you can:- Enhance Security: Block unauthorized changes to critical protocol parameters
- Eliminate Risk: Prevent potential exploits before any damage occurs
- Build Trust: Demonstrate to users that your protocol is actively protected
- Sleep Better: Know that your protocol has an active defense layer
- Total supply of tokens should never increase unexpectedly
- Funds should never be withdrawn without proper authorization
- Key protocol parameters should only change through governance
- Protocol reserves should always be sufficient to cover liabilities
- Transaction values should never exceed the protocol’s limits
- Voting power recorded during snapshot should match the actual voting power
- Deviation of the price of a token from the oracle should never exceed a certain threshold
- And many more…
What’s Next?
- Explore the assertion examples in the Assertions Book
- Learn about the Cheatcodes
- Follow the PCL Quickstart to learn how to deploy your assertions
- For a comprehensive list of terms and concepts used in PCL, see the Glossary.