First steps with Certora Formal Verification: Catching a Real Bug with a Universal 5-Line Rule

Search for a command to run...

Hello Alex! Thank you so much for the introductory blog for FV. I am studying your GitHub repo - uniswapV4 and euler FV. They show the importance of identifying Valid States of the project is fundamental for FV.
Would you mind making a blog on the thought process of identifying Valid States? Do you have a SOP for it?
By the way, your code style is very clean and concise. It is very inspiring. I wish you would keep up the good work!
Thank you so much in advance!
@alexzoid Thank you so much for the reply🙌🙌 I am looking forward to your great work🫶🏻🫶🏻
Introduction This article originally was published about one year ago (in Jan 2024). I’ve slightly edited it and fixed sources to support the latest (7.26.0) Prover version. In 2023, I got involved in formal verification by joining Certora community ...

What are Certora contests? How do they differ from standard audit competitions? How do you get started? Are there any specific nuances? You’ll find the answers to all of these questions here. At the time of writing this article, I’ve participated in ...

Curious about Certora Formal Verification but unsure where to begin? This tutorial provides a step-by-step setup and a powerful five-line rule for catching a very common class of storage-related bugs. It’s illustrated with a real issue discovered in the past Sherlock contest. Read on to learn more!
Below is a quick guide on installing Certora and its dependencies on a fresh Ubuntu (Installation docs). Skip this section if you already have Certora installed.
Install Java
sudo apt update
sudo apt install default-jre
java -version
Install pipx
sudo apt install pipx
pipx ensurepath
Install Certora CLI
pipx install certora-cli
Install solc-select
pipx install solc-select
Then install the Solidity compiler version that our project requires:
solc-select install 0.8.24
solc-select use 0.8.24
Set up Certora key
You can get a free Certora key through their discord or on the website. Once you have it, export it to your environment variables:
echo "export CERTORAKEY=<your_certora_api_key>" >> ~/.bashrc
Next, let’s clone my repository (adapted from a Sherlock contest) and run the Certora Prover.
Clone and build
git clone https://github.com/alexzoid-eth/2024-08-flayer-fv
cd 2024-08-flayer-fv/flayer
forge build
Run Certora
The Certora CLI command certoraRun accepts a JSON configuration file path:
certoraRun certora/confs/UniswapImplementation.conf
This compiles your Solidity files and uploads them, along with the specification and .conf file, to Certora’s remote prover. A link to the live job will appear in your terminal, and you can also monitor the process at prover.certora.com.
Certora Dashboard
After running the command, you’ll see a unique URL such as:
https://prover.certora.com/output/52567/ebcd153233744cc983869261222e416b/?anonymousKey=e4d88a2858d6cbf65b68ac391e25ce2a6f3a03b2
Clicking this link or visiting the dashboard shows your job’s verification progress and results. Note: Because the URL contains an anonymousKey, anyone with that link can view your Solidity code and spec. If you prefer to share it privately (e.g., with Certora support), omit the /?anonymousKey=... part.
A Certora configuration file is a convenient way to instruct the prover on how to handle your specifications and Solidity sources. Although you can provide these options via command line arguments (CLI docs), using a .conf file often keeps things cleaner.
{
"files": [
"src/contracts/implementation/UniswapImplementation.sol",
],
"verify": "UniswapImplementation:certora/specs/UniswapImplementation.spec",
}
In our case, the minimal configuration contains two json key fields:
files: An array of Solidity source files to compile and analyze.
verify: The ContractName:PathToSpecFile indicating which contract to verify and which spec file to apply.
You can also include more advanced settings (Config docs).
A Certora specification is stored in a file ending with .spec and is written in the Certora Verification Language (CVL), which resembles Solidity. Each rule in your spec must include at least one assert() or satisfy() statement:
rule dummy() {
assert(true);
}
assert(expr)
Similar to Solidity’s assert, this requires that expr always holds on every valid execution path. (assert docs)
satisfy(expr)
A reachability requirement ensuring expr holds on at least one execution path. (satisfy docs)
A CVL rule can be divided into three logical sections:
Prerequirements (optional)
Constraints on the contract’s initial state or the environment before the rule runs, otherwise arbitrary state applied.
Contract Execution
One or more function calls.
Statements
The final assert/satisfy statements that verify or require certain conditions to hold after execution.
Here’s a practical demonstration of a rule for catching a real bug I found in a past Sherlock contest. The contract’s admin function stored an updated parameter in memory instead of storage, so changes were never persisted.
function setFee(PoolId _poolId, uint24 _fee) public onlyOwner {
// Validate the fee amount
_fee.validate();
// Set our pool fee overwrite value
PoolParams memory poolParams = _poolParams[_poolId]; // <-- "memory" instead of "storage"
poolParams.poolFee = _fee;
// Emit our event
emit PoolFeeSet(poolParams.collection, _fee);
}
To detect this and similar issues automatically, we can craft a universal rule in plain English:
“Every non-view function must change contract storage in at least one execution path.”
Below is our Certora rule that implements this idea:
// Ensures that any non-view function changes storage
rule noOpFunctionDetection(env e, method f, calldataarg args)
filtered { f -> !f.isView && !f.isPure }
{
// 1. Prerequirements: Save contract storage before the call
storage before = lastStorage;
// 2. Contract Execution: Call the function with arbitrary arguments
f(e, args);
// 3. Statements: Storage not equal on at least one execution path
storage after = lastStorage;
satisfy(before[currentContract] != after[currentContract]);
}
Parametric Rule
The env e, method f, calldataarg args parameters with f(e, args) call instruct the Certora Prover to execute each relevant function with all possible inputs and environment variables (like msg.sender, timestamp, etc.). (Parametric docs) Note: It doesn’t matter whether you declare these variables inside the rule body or pass them as function arguments.
Filters A filtered block lets us exclude specific methods like view and pure functions from testing. (Filters docs)
Storage Snapshots
We take a snapshot of the contract’s storage before the function call and another after it. (Storage docs)
satisfy() Statement
By requiring before[currentContract] != after[currentContract], we ensure that at least one execution path of any non-view function must modify contract storage. If a function never modifies storage in any path, the rule fails, flagging a potential no-op bug.
This rule is broadly reusable - just drop it into other auditing projects to reveal any non-view functions that fail to persist changes.

After running the prover, I received this link. The dashboard indicates several external functions in UniswapImplementation.sol are flagged by our rule:
Reverting by design:
afterInitialize()
beforeDonate()
afterDonate()
initializeCollection()
Doing nothing but emitting an event:
afterAddLiquidity()
afterRemoveLiquidity()
These scenarios produce false positives for our no-op rule, as the functions either revert immediately or only emit an event.
The remaining flagged functions include:
unlockCallback(), which modifies another contract’s state,
and setFee(), where we found our real bug.

Once we fix the setFee function (using storage instead of memory) and rerun the verifier (updated link), our noOpFunctionDetection rule no longer flags setFee, confirming that the bug is resolved.
This demonstrates how a small, generic specification can quickly catch common issues.