Formal Verification: Securing ERC-4626 Vaults with Certora Prover

Feb 11, 2025

16 Minutes

Overview


Formal verification provides a rigorous method to mathematically prove that a smart contract adheres to its intended specifications, reducing the risk of vulnerabilities.


This article explores the process of formally verifying an ERC-4626 vault—a standardized tokenized yield-bearing vault—using Certora Prover. Certora Prover is a powerful rule-based verification tool that allows developers to define custom correctness properties and enforce them on Solidity smart contracts, ensuring their security before deployment.


We plan to publish additional articles covering more aspects of formal verification in our DeFi example. By the end of this article, you will understand how to set up and run a minimal formal verification using Certora Prover to enhance the security and reliability of smart contracts.

Setup


To formally verify the ERC-4626 vault, we need to prepare the necessary Solidity smart contracts and Certora files. This example is structured as a Foundry project, though the setup can be adapted to other frameworks. The key components are:

  • Smart Contracts


VaultERC.sol – The ERC-4626 vault implementation that is the subject of formal verification. This contract will be analyzed to ensure it adheres to the expected invariants.


MockAssetA.sol – A mock ERC-20 token that represents the underlying asset of the vault.

  • Certora Files


VaultERC.spec – Defines the formal verification rules for VaultERC.sol. This file contains the key properties we aim to prove.


VaultERC.conf – Specifies how Certora should execute the verification, including contract inputs, constraints, and other various configuration settings.


To run the verification, Certora Prover must be installed. Follow the official installation guide here: 👉 Certora Installation Guide


With the setup complete, we can focus later on defining verification rules in VaultERC.spec. It is worth noting that we use the Solmate library: forge install transmissions11/solmate

Smart Contract Implementation

1. MockAssetA: A Tailored ERC20 Implementation


For our testing task, we require an ERC20 token to act as the underlying asset for our ERC4626 vault. While we could use the standard Solmate ERC20 implementation without changes, it presents a challenge due to the use of unchecked arithmetic in its transfer and transferFrom functions:

unchecked {

    balanceOf[to] += amount;

}


Under normal conditions, unchecked arithmetic is not an issue since the overflow is prevented by the fact that the sum of balances is not meant to exceed the ERC20 totalSupply which is guarded by safe maths operations. However, when applying Certora formal verification, the prover introduces havocing, a process that randomly manipulates storage variables under specific conditions. This can cause token balances to assume values that lead to overflows, complicating the verification process.


While we could constrain Certora's havocing a variable in our specification files by adding an invariant and requiring it to be a precondition, a more straightforward solution right now is to modify the ERC20 implementation itself to strictly adhere to safe arithmetic. Since this token is merely a supporting asset in our tests, this modification does not impact our core ERC4626 contract behavior.

Implementation: MockAssetA


To address this, we introduce smart contract MockAssetA, an ERC20 token that overrides the transfer and transferFrom functions to remove unchecked arithmetic:

contract MockAssetA is ERC20 {

    constructor(string memory name_, string memory symbol_, uint8 decimals_) 

      ERC20(name_, symbol_, decimals_) {}

    function transfer(address to, uint256 amount) public override returns (bool) {

        balanceOf[msg.sender] -= amount;

        balanceOf[to] += amount;

        emit Transfer(msg.sender, to, amount);

        return true;

    }

    function transferFrom(address from, address to, uint256 amount) public override returns (bool) {

        uint256 allowed = my allowance[from][msg.sender];

        if (allowed != type(uint256).max) {

            allowance[from][msg.sender] = allowed - amount;

        }

        balanceOf[from] -= amount;

        balanceOf[to] += amount;

        emit Transfer(from, to, amount);

        return true;

    }

}

2. VaultERC: The ERC4626 Implementation Under Test


Our primary contract under test is the VaultERC, which follows the ERC4626 standard for tokenized vaults. This contract extends ERC4626 and simply overrides totalAssets() which is a hard requirement by the base contract to reflect the current balance of the underlying asset held by the vault.

Implementation: VaultERC

pragma solidity >=0.8.0;

import {ERC20} from "solmate/tokens/ERC20.sol";

import {ERC4626} from "solmate/tokens/ERC4626.sol";

import {Owned} from "solmate/auth/Owned.sol";

contract VaultERC is Owned, ERC4626 {

    constructor(address _asset) 

        Owned(msg.sender)

        ERC4626(ERC20(_asset), "Vault Wrapper", "4626") 

    {}

    function totalAssets() public view override returns (uint256) {

        return asset.balanceOf(address(this));

    }

}

Formal Verification Specification: ERC4626.spec


A spec file in Certora Prover is a formal verification script that defines the rules and constraints a smart contract must satisfy. It serves as the blueprint for how the prover checks correctness by specifying invariants, function behaviors, and assumptions about the smart contract’s state.

Unlike traditional testing, which relies on specific inputs and outputs, a spec file enables mathematical reasoning over all possible states, ensuring that the smart contract adheres to its intended logic under all conditions, including edge cases.

We first start the ERC4626.spec specification file by the following statement:

ERC4626.spec


Using MockAssetA as AssetA;


This line establishes an alias for the MockAssetA contract, allowing us to refer to it as AssetA throughout the spec file.


At this point we fill the methods block in the file. The methods block in Certora's specification file defines which functions from our smart contracts will be used during the verification process.

methods{

    /// VaultERC methods

    function asset() external returns address envfree;

    function totalSupply() external returns uint256 envfree;

    function deposit(uint256,address) external;

    function mint(uint256,address) external;

    function withdraw(uint256,address,address) external;

    function redeem(uint256,address,address) external;

    function totalAssets() external returns uint256 envfree;

    ...

}

These function declarations match the actual functions in the VaultERC (ERC4626) contract. Certora will verify their behavior based on the rules we define in the spec. Marking a method with envfree has two effects.

First, calls to the method from CVL do not require an explicit environment value as the first argument. Second, the Prover ensures that the method's implementation in the verified contract does not rely on any that is considered an environment variable (e.g. msg.sender).

During verification, the Prover does not solely execute functions from VaultERC. We must also define additional functions in the methods block to inform the Prover about other relevant methods it should consider.

function _.balanceOf(address) external  => DISPATCHER(true);

function _.transfer(address,uint256) external  => DISPATCHER(true);

function _.transferFrom(address,address,uint256) external => DISPATCHER(true);

function AssetA.balanceOf(address) external returns uint256 envfree;

function AssetA.allowance(address, address) external returns uint256 envfree;

function AssetA.transferFrom(address,address,uint256) external returns bool;

The first three entries do not specify a specific contract but instead use _ (a wildcard), meaning they apply to any contract that implements these methods. With DISPATCHER(true), these functions are treated as dispatcher summaries, allowing the Prover to dynamically resolve the actual implementation during the run. When executing these methods, the Prover will identify and apply the correct implementation from any contract included in the verification run, such as ERC20.sol, MockAssetA.sol, or others.

The last three entries explicitly reference AssetA, which is an alias for the MockAssetA contract. In this case, the Prover directly resolves these method implementations from MockAssetA.sol.

Ghost variables and hooks


At this stage, we define ghost variables, which serve as an 'extension' of the state of the contracts under verification.

ghost mathint sumOfBalances {

    init_state axiom sumOfBalances == 0;

}

init_state axiom sumOfBalances == 0; → Ensures that sumOfBalances is initialized to zero in the initial state. Without an init_state axiom, the value of sumOfBalances would be havoced (randomized).


The role of this ghost variable is to track the total sum of balances across all users throughout the run.


To make this ghost variable meaningful, we define hooks, which allow us to attach Certora Verification Language (CVL) code to specific low-level operations, such as reading from (Sload) or writing to (Sstore) storage variables.


Each hook consists of:

  • A pattern that determines which operations will trigger the hook.

  • A code block that executes when the specified operation occurs. Here's an example:

hook Sstore balanceOf[KEY address a] uint256 newValue (uint256 oldValue)  {

    sumOfBalances = sumOfBalances + newValue - oldValue;

}

This is triggered whenever the contract updates the storage of the balanceOf mapping for an arbitrary address a. The hook adjusts the total sum by adding the new balance and subtracting the old balance, ensuring that sumOfBalances correctly tracks all balance changes.

Another example:

hook Sload uint256 val balanceOf[KEY address a]  {

    require sumOfBalances >= val;

}

This is triggered whenever the contract reads a value from the balanceOf mapping in storage. It ensures that the total recorded sum of balances is always greater than or equal to the individual balance being read. This prevents inconsistencies, especially in cases where storage values could be havoced (randomized) during verification.

Invariants


The final step in the spec file is defining the invariant, which specifies a property that must always hold true throughout the execution of the contract.

Here is the invariant we define:

invariant totalSupplyIsSumOfBalances()

    totalSupply() == sumOfBalances;

This condition is straightforward—it asserts that the totalSupply() of the vault token must always be equal to sumOfBalances, which represents the sum of all balances in the balanceOf mapping.

Since we previously defined hooks to track updates to sumOfBalances, we know that it correctly reflects the cumulative balance of all token holders.


This invariant is crucial because it ensures that the total supply of tokens is always consistent with the sum of individual balances. If it is ever violated, it indicates an inconsistency in how tokens are being minted, burned, or transferred, requiring further investigation to identify a potential bug.

Running the spec file for verification

Writing the conf file


Once we have defined the ERC4626.spec file containing the formal verification rules, the next step is to run the Certora Prover to check whether our ERC4626 implementation adheres to that expected property. This requires configuring a .conf file to specify the contracts involved, the verification setup and other configuration settings.

  • Setting Up the Configuration File

The following configuration file, ERC4626.conf, defines the inputs and settings for the verification process:

{

    "files": [

        "src/VaultERC.sol",

        "src/mocks/MockAssetA.sol"

    ],

    "verify": "VaultERC:certora/VaultERC.spec",

    "link": ["VaultERC:asset=MockAssetA"],

    "optimistic_loop": true,

    "rule_sanity": "basic",

    "msg": "ERC4626 certora formal verification"

}


With the configuration file in place, we can initiate the test by executing the following command:

certoraRun certora/ERC4626.conf


Once the verification process is completed, Certora will generate a detailed verification report containing:

✅ Successful Checks – Indicating which properties held true across all possible executions.
❌ Failed Checks – Showing violations of the defined rules or invariants.

Viewing the Results


To inspect the results, Certora provides an output link to its verification dashboard, where a structured breakdown of the results is available.


The following snapshot presents the test results of this run via the Certora Prover. Initially, the totalSupply variable in the VaultERC contract and the ghost variable sumOfBalances were both havoced and assigned a value of zero before execution.


The verification test executes a deposit function call with an amount of 0x1128. As expected, both totalSupply and sumOfBalances increase by this exact value, maintaining consistency between the two. This confirms that the total supply of vault tokens correctly reflects the sum of all balances, thereby satisfying the defined invariant.


It's important to note how the Certora Prover operates: it attempts to find counterexamples where our defined rules or invariants fail. If it cannot find a violation, it presents one valid example among an infinite number of possible executions where the invariant holds. The image shows such a successful case, reinforcing the correctness of our ERC4626 implementation for this particular scenario.

Testing the test!


Now, someone proposed a genius implementation (sarcasm intended) that gives away extra minted shares to the owner, modifying the afterDeposit function as follows:

function afterDeposit(uint256, uint256 shares) internal override {

    balanceOf[owner] += shares / 100;

}

This modification was added to VaultERC, where afterDeposit() is an ERC4626 hook that gets triggered after calling deposit and mint.

Running the Prover with This Change


After executing the verification process with this new implementation, the test results in the screenshot reveal a failure in both mint and deposit.


The invariant failed to invoke mint and deposit because the balanceOf mapping was modified to grant additional shares to the contract owner. However, totalSupply was not updated accordingly, causing an inconsistency. This led to a mismatch between totalSupply and sumOfBalances, breaking our invariant.


As a result, the prover correctly detected the issue, demonstrating that this code mutation violates the expected ERC4626 behavior by causing an imbalance between total supply and individual balances. The purpose of introducing this intentional bug was to verify that our spec file can effectively catch such violations. This approach helps ensure that our test is robust and capable of identifying real issues.

Conclusion


Formal verification using Certora Prover has proven to be a powerful tool for ensuring the correctness of our ERC4626 implementation. Through our test, we confirmed that valid implementations passed that verification, while intentional code mutations introducing inconsistencies were successfully detected. This demonstrates the effectiveness of our specification file in catching bugs and maintaining the integrity of the ERC4626 vault.

You can find an example in this GitHub repository.

By leveraging formal verification, we gain a higher level of confidence in the security and correctness of our contract, reducing potential vulnerabilities before deployment. This method complements traditional testing approaches by ensuring that the contract behaves correctly under all possible conditions, not just predefined test cases.

Copyright Disclaimer and Notice

All Rights Reserved.

All material appearing on the Zokyo's website (the “Content”) is protected by copyright under U.S. Copyright laws and is the property of Zokyo or the party credited as the provider of the Content. You may not copy, reproduce, distribute, publish, display, perform, modify, create derivative works, transmit, or in any way exploit any such Content, nor may you distribute any part of this Content over any network, including a local area network, sell or offer it for sale, or use such Content to construct any kind of database. You may not alter or remove any copyright or other notice from copies of the content on Zokyo's website. Copying or storing any Content is expressly prohibited without prior written permission of the Zokyo or the copyright holder identified in the individual content’s copyright notice. For permission to use the Content on the Zokyo's website, please contact hello@zokyo.io

Zokyo attempts to ensure that Content is accurate and obtained from reliable sources, but does not represent it to be error-free. Zokyo may add, amend or repeal any policy, procedure or regulation, and failure to timely post such changes to its website shall not be construed as a waiver of enforcement. Zokyo does not warrant that any functions on its website will be uninterrupted, that defects will be corrected, or that the website will be free from viruses or other harmful components. Any links to third party information on the Zokyo's website are provided as a courtesy and do not constitute an endorsement of those materials or the third party providing them.

Copyright Disclaimer and Notice

All Rights Reserved.

All material appearing on the Zokyo's website (the “Content”) is protected by copyright under U.S. Copyright laws and is the property of Zokyo or the party credited as the provider of the Content. You may not copy, reproduce, distribute, publish, display, perform, modify, create derivative works, transmit, or in any way exploit any such Content, nor may you distribute any part of this Content over any network, including a local area network, sell or offer it for sale, or use such Content to construct any kind of database. You may not alter or remove any copyright or other notice from copies of the content on Zokyo's website. Copying or storing any Content is expressly prohibited without prior written permission of the Zokyo or the copyright holder identified in the individual content’s copyright notice. For permission to use the Content on the Zokyo's website, please contact hello@zokyo.io

Zokyo attempts to ensure that Content is accurate and obtained from reliable sources, but does not represent it to be error-free. Zokyo may add, amend or repeal any policy, procedure or regulation, and failure to timely post such changes to its website shall not be construed as a waiver of enforcement. Zokyo does not warrant that any functions on its website will be uninterrupted, that defects will be corrected, or that the website will be free from viruses or other harmful components. Any links to third party information on the Zokyo's website are provided as a courtesy and do not constitute an endorsement of those materials or the third party providing them.

Copyright Disclaimer and Notice

All Rights Reserved.

All material appearing on the Zokyo's website (the “Content”) is protected by copyright under U.S. Copyright laws and is the property of Zokyo or the party credited as the provider of the Content. You may not copy, reproduce, distribute, publish, display, perform, modify, create derivative works, transmit, or in any way exploit any such Content, nor may you distribute any part of this Content over any network, including a local area network, sell or offer it for sale, or use such Content to construct any kind of database. You may not alter or remove any copyright or other notice from copies of the content on Zokyo's website. Copying or storing any Content is expressly prohibited without prior written permission of the Zokyo or the copyright holder identified in the individual content’s copyright notice. For permission to use the Content on the Zokyo's website, please contact hello@zokyo.io

Zokyo attempts to ensure that Content is accurate and obtained from reliable sources, but does not represent it to be error-free. Zokyo may add, amend or repeal any policy, procedure or regulation, and failure to timely post such changes to its website shall not be construed as a waiver of enforcement. Zokyo does not warrant that any functions on its website will be uninterrupted, that defects will be corrected, or that the website will be free from viruses or other harmful components. Any links to third party information on the Zokyo's website are provided as a courtesy and do not constitute an endorsement of those materials or the third party providing them.