Formal verification proves a smart contract's code adheres to a set of rigorously defined security properties. Unlike testing, which checks specific inputs, verification mathematically guarantees correctness for all possible execution paths. The process begins not with writing code, but with defining these properties. They are precise, machine-readable statements that act as the formal specification against which the contract's logic is verified. Common frameworks for this include invariants (conditions that must always hold), post-conditions (outcomes of a function), and state transition rules.
How to Define Security Properties for Smart Contract Verification
How to Define Security Properties for Smart Contract Verification
Learn the core methodology for specifying what your smart contract must and must not do, forming the basis for formal verification.
Security properties fall into two primary categories: safety and liveness. Safety properties are about "nothing bad ever happens." For a token contract, a safety property might be: "The total supply of tokens must always equal the sum of all balances." This prevents inflation bugs. Liveness properties ensure "something good eventually happens," such as: "A user's withdrawal request will eventually be processable." Most smart contract verification focuses on safety properties, as they guard against critical vulnerabilities like reentrancy, overflow, and access control failures.
To define a property, you must translate a security requirement into a logical formula. For example, consider an access-controlled mint function. An informal requirement is: "Only the owner can mint tokens." The formal property could be written in a specification language like CVL (Certora Verification Language) as: rule onlyOwnerMints { require msg.sender == owner; }. This states that if the mint function is called, the sender must be the owner. Tools like the Certora Prover or Scribble (for Solidity) then use this rule to check every possible scenario in the contract's bytecode.
Effective property definition requires deep understanding of the contract's intended behavior and potential failure modes. Start by identifying core invariants: values that should never change or relationships that must persist. For a lending protocol, a key invariant is: totalCollateral >= totalDebt. Then, define functional correctness properties for each key operation, specifying pre-conditions and post-conditions. Finally, consider temporal properties across multiple transactions, like "a user's vote cannot be changed after it is cast."
Here is a concrete example for a simple vault contract. We want to ensure that deposits always increase a user's recorded balance. In Scribble annotation for Solidity, this property might be written directly in the code: /// #if_succeeds old(balances[msg.sender]) + amount == balances[msg.sender];. This annotation specifies that after the deposit function executes, the sender's new balance must equal their old balance plus the deposited amount. The formal verifier will attempt to find any input or state that violates this condition.
Defining precise properties is the most critical and challenging step in formal verification. Ambiguous or incomplete properties lead to verified but insecure contracts. Collaborate with domain experts, audit historical exploits for similar contracts, and iteratively refine your specifications. The result is a executable security checklist that provides the highest level of assurance for your protocol's core logic, moving beyond bug hunting to guaranteeing correctness.
How to Define Security Properties for Smart Contract Verification
Before you can formally verify a smart contract, you must first define what 'secure' means for your specific application. This guide explains how to translate high-level security goals into precise, machine-checkable properties.
Smart contract verification tools like Chainscore, Certora Prover, or Halmos don't inherently know what is 'correct' or 'secure' for your contract. You must provide a formal specification—a set of security properties—that defines the intended behavior. These properties are logical statements that must hold true for all possible inputs and execution paths. Common property categories include functional correctness (e.g., "tokens are never created from nothing"), access control (e.g., "only the owner can pause the contract"), and financial safety (e.g., "the contract's ETH balance never decreases without a corresponding user withdrawal").
To define effective properties, start with your contract's invariants. An invariant is a condition that must always be true before and after any transaction. For an ERC-20 token, a core invariant is conservation of supply: totalSupply() == sum of all balances. For a lending protocol like Aave or Compound, a critical invariant is solvency: totalAssets >= totalLiabilities. Write these in plain language first, then translate them into formal logic using the contract's state variables and public functions as building blocks.
Next, specify state transition properties. These define how the contract's state can legally change. For a function transfer(address to, uint256 amount), you would assert: "If the call succeeds, the sender's balance decreases by amount, the recipient's balance increases by amount, and all other balances remain unchanged." Tools often express this as a post-condition. You must also consider reentrancy guards: "No state variable v can be read after a call to an external contract and then written again before that call returns."
For complex protocols, leverage rule-based specifications. Instead of enumerating every scenario, write general rules. For a decentralized exchange (DEX) pool, a rule could be: "For any successful swap operation, the product of the reserves (reserve0 * reserve1) must not decrease below its value before the swap, minus a defined fee." This captures the core constant product formula invariant of Uniswap V2-style pools. Reference real-world audits and formal verification reports from projects like MakerDAO or Uniswap to see how they codified their most critical properties.
Finally, integrate these properties into your verification setup. In the Certora Verification Language (CVL), you'd write rule invariantTotalSupply with a require and assert block. In Foundry's Forge, you might write a fuzzing invariant test using invariant { ... }. The key is to make properties falsifiable—they must be specific enough that a counterexample can be found if the contract violates them. Start with 3-5 core properties covering asset safety, access control, and critical invariants, then expand as you test and audit.
How to Define Security Properties for Smart Contract Verification
Defining precise security properties is the critical first step in formal verification, transforming abstract goals into machine-checkable logic.
Security properties are formal, logical statements that specify the intended, correct behavior of a smart contract. Unlike general goals like "be secure," a property is a testable assertion about the contract's state transitions. For example, a goal might be "tokens should not be created from nothing." The corresponding formal property could be: totalSupplyPre == totalSupplyPost - mintedAmount. This property asserts that the total token supply after a transaction must only increase by the explicitly minted amount, preventing inflationary bugs. Defining these properties forces you to articulate the invariants—conditions that must always hold true—and post-conditions—outcomes that must be true after an operation.
The process begins with identifying core invariants. Common categories include: value conservation (e.g., the sum of user balances equals totalSupply), access control (e.g., only the owner can call function X), state machine validity (e.g., a vault can only be open after initialization), and arithmetic safety (e.g., no overflow in balance calculations). For a lending protocol, a key invariant is that the sum of all collateral values must always be greater than or equal to the sum of all borrowed amounts, ensuring solvency. Tools like the Certora Prover or Foundry's invariant testing use these formalized statements as the benchmark for verification.
To write an effective property, you must translate the natural language rule into a precise logical formula. Consider a vesting contract. A rule might be: "A user cannot withdraw more tokens than they have vested." This translates to a property checking the contract's state before and after the withdraw function: withdrawnAmount + amount <= vestedAmount(user). Here, withdrawnAmount is the historical total withdrawn, amount is the current withdrawal attempt, and vestedAmount(user) is the calculable vested total at the current block time. The property is violated if this inequality fails. This formal model captures the essence of the security requirement in a way a fuzzer or theorem prover can automatically reason about and test across all possible execution paths.
Properties must be composable and modular. A complex DeFi protocol like a decentralized exchange (DEX) has layered properties. The core ConstantProductPool contract has an invariant: reserve0 * reserve1 >= k, where k is a constant. The surrounding Router contract, which interacts with the pool, must have the property that any trade it executes preserves the pool's invariant (or improves it). This modular approach allows you to verify components independently before reasoning about their integration. It also mirrors good software design, where security guarantees are enforced at the lowest possible level.
Finally, property definition is an iterative process. Initial properties often miss edge cases, which are revealed during verification when a counterexample is found. A counterexample is a specific transaction sequence that violates your property, exposing a bug or an overly restrictive specification. This feedback loop is where the real security hardening occurs. You refine the property to be more precise, potentially adding pre-conditions (e.g., require(block.timestamp > startTime)) or accounting for more state variables. The end result is a rigorous, executable specification that acts as a permanent, automated security audit for your contract's logic.
Four Fundamental Property Types
Formal verification proves a smart contract behaves as intended by checking it against a formal specification. These are the four core property types you must define.
Reentrancy & Liveness
Proves the contract is safe from reentrancy attacks and that functions will eventually complete (liveness).
- Reentrancy: Formally verify that state updates occur before external calls (Checks-Effects-Interactions pattern).
- Liveness: Ensure functions do not enter infinite loops or deadlock states, especially in complex multi-contract interactions.
- Real-World Impact: This property class directly addresses vulnerabilities responsible for losses like the 2016 DAO hack ($60M) and 2022 Beanstalk exploit ($182M).
Property Examples for Common DeFi Primitives
Example security properties to specify when verifying smart contracts for different DeFi components.
| Primitive | Core Safety Property | Core Liveness Property | Example Invariant |
|---|---|---|---|
ERC-20 Token | Total supply is preserved across all transfers | Any user with sufficient balance can transfer tokens | totalSupply == sum(balances[address]) |
Constant Product AMM (Uniswap V2) | Product of reserves is non-decreasing (k >= k_initial) | Any user can add/remove liquidity while maintaining ratio | reserve0 * reserve1 >= k |
Lending Pool (Compound/Aave) | Total borrows cannot exceed total liquidity | A solvent user can always repay and withdraw collateral | totalBorrows <= totalLiquidity && collateralRatio >= minRatio |
Vesting Contract | Tokens released never exceed total allocated | Tokens unlock precisely at scheduled timestamps | releasedAmount <= totalAllocated && block.timestamp >= cliff |
Multi-signature Wallet | Transaction requires M-of-N signatures | Any valid set of M signatures can execute a proposal | signatureCount >= threshold && signersAreValid |
Staking Contract | Total staked plus rewards equals deposited assets | Users can unstake after the lock-up period expires | totalStaked + rewardPool == totalDeposited && block.timestamp >= unlockTime |
Rebasing Token (e.g., stETH) | User's share of total supply remains constant between rebases | Rebase operation updates all balances proportionally | userBalance / totalSupply == constant && sum(balances) == totalSupply |
Step-by-Step: Writing a Property
Learn how to formally define the security properties that a smart contract must uphold, a critical step for automated verification tools like Chainscore.
A security property is a formal, machine-readable statement that defines a specific condition your smart contract must always satisfy. Unlike unit tests that check specific inputs, properties define invariants—rules that should hold true across all possible states and transactions. Common property types include: - State invariants: Rules about contract storage (e.g., total supply equals sum of balances). - Function postconditions: Guarantees about a function's outcome (e.g., a transfer never creates tokens). - Access control rules: Specifications about who can call certain functions. Writing precise properties forces you to explicitly define your contract's intended behavior, which is the foundation of formal verification.
To write a property, you first need to understand the contract's state variables and function logic. Start by identifying the core invariants. For an ERC-20 token, a fundamental property is conservation of supply: totalSupply == sum(balances[address]). In a Solidity-like specification language, this might be expressed as a rule checked after every transaction. Another example is an access control property for an onlyOwner modifier, ensuring that a critical setAdmin function can only be successfully called by the owner's address from any initial state.
Properties are written in a specification language understood by the verification tool. For Chainscore, you use the Chainscore Specification Language (CSL), which is designed to be intuitive for Solidity developers. A basic property has a description and a rule expressed in logical predicates. For instance, to specify that an auction's highestBid should only increase, you might write:
codeproperty "Bid amount monotonic increase" { description: "The highestBid should never decrease"; rule: "before.highestBid <= after.highestBid"; }
The before and after keywords refer to the contract state before and after a transaction.
Effective property writing involves thinking about edge cases and interactions. Consider reentrancy: a property could state that a contract's ether balance should never change unexpectedly during a single external call. Also, consider temporal properties across multiple transactions, like "a user's staked tokens cannot be withdrawn before the lock period expires." Tools like Chainscore will then use symbolic execution to mathematically prove if the contract's code violates any of these properties under all possible scenarios, providing a higher assurance level than traditional testing.
Start by writing properties for the most critical contracts—those holding significant value or governing access. Focus on financial invariants (no money gets created/destroyed), access control, and state machine correctness (functions are called in the right order). Reference established standards like the Consensys Diligence Security Property Library for inspiration. The process is iterative: you write properties, run the verifier, and refine them based on counter-examples the tool provides, deepening your understanding of the contract's behavior with each cycle.
Common Mistakes and How to Avoid Them
Defining precise security properties is the foundation of formal verification. These are the most frequent pitfalls developers encounter and how to correct them.
Confusing safety and liveness is a fundamental error that leads to incorrect verification goals.
Safety properties are about "nothing bad ever happens." They are invariant assertions that must hold in all reachable states of the system. Examples include:
- "The total token supply never exceeds 1 million."
- "A user's balance never becomes negative."
- "An admin role cannot be revoked from the zero address."
Liveness properties are about "something good eventually happens." They assert that the system will eventually reach a desirable state. Examples include:
- "A withdrawal request will eventually be fulfilled."
- "A governance proposal will eventually become executable or expire."
Most smart contract verification focuses on safety properties because they are easier to specify and prove. Liveness often depends on external, uncontrollable factors (like network conditions).
Tools and Specification Languages
Formal verification requires precise definitions of security properties. These tools and languages help you write specifications that define what "correct" means for your contract.
Defining Access Control Properties
A critical category of security properties. Specifications must define authorization invariants.
- Example: "Only the
ownercan callsetAdmin." - Formal Rule (CVL):
rule onlyOwnerSetAdmin { require msg.sender == owner; } - Test (Foundry):
assertEq(msg.sender, owner)in a function's modifier. - Failure to specify these is a common source of privilege escalation vulnerabilities.
Temporal and Liveness Properties
Learn to define formal security properties that specify how a smart contract should behave over time, a critical step for automated verification.
Smart contract verification requires precise, machine-readable specifications. While safety properties (like "funds are never stolen") are common, many critical behaviors are temporal properties—they describe allowed sequences of events over time. For example, a property might state: "If a user deposits funds, they must eventually be able to withdraw them." This combines a condition (deposit) with a future obligation (withdrawal), which cannot be expressed with a simple invariant checked at a single point in time. Temporal logic, such as Linear Temporal Logic (LTL) or Computation Tree Logic (CTL), provides the formal language to write these specifications.
Two fundamental categories of temporal properties are liveness and fairness. A liveness property asserts that "something good eventually happens." In our example, the user's right to withdraw is a liveness guarantee. A fairness property ensures the system does not indefinitely ignore a valid request. For instance, "if a valid governance proposal is submitted, it will eventually be put to a vote." These properties are essential for systems involving auctions, governance, or cross-chain messaging, where stalling represents a critical failure. Tools like the TLA+ modeling language or Model Checkers like Cadence for Move contracts are built to reason about these complex behaviors.
To define these properties for a Solidity contract, you often use specification languages or annotation systems. The Solidity Specification Language (SSL) or ACT for Certora Prover allow you to write rules. A liveness rule for a withdrawal might look like: rule eventualWithdrawal { require deposited[user] > 0; ensure eventually(withdrawn[user] == true); }. This tells the verifier to prove that from any state where a user has a deposit, a future state exists where the withdrawal is completed. The challenge is defining what "eventually" means in a blockchain context—it typically means within a finite, unbounded number of transactions.
A practical example is verifying a simple timelock contract. A safety property ensures lockedFunds >= 0. A liveness property must ensure that if (block.timestamp > releaseTime[user]) then eventually userCanWithdraw == true. Writing this requires careful consideration of path quantifiers: are we checking that all possible execution paths lead to withdrawal (AG in CTL), or that there exists a path where it happens (EF)? For most user guarantees, the "for all paths" (AG eventual) is required, as you must guarantee the outcome regardless of other interactions.
Finally, integrating these properties into your development workflow involves writing specifications alongside your contract code. Frameworks like Foundry's invariant testing can simulate long-running state sequences to probe for liveness violations. The key takeaway is moving beyond simple "state snapshots" to behavioral specifications. By formally defining that certain actions must become possible in the future, you can mathematically verify that your contract cannot deadlock, censor users, or indefinitely withhold assets—elevating security from preventing bad states to guaranteeing good outcomes.
Frequently Asked Questions
Common questions and troubleshooting for defining security properties when verifying smart contracts with formal methods.
In formal verification, a security property is a precise, logical statement about a smart contract's intended behavior that must always hold true. An invariant is a specific type of property that must remain unchanged before and after any transaction. For example, in an ERC-20 contract, a critical invariant is totalSupply == sum(balances). Properties are written in the specification language of the verification tool (e.g., CVL for Certora, Solidity for Foundry). Defining these properties is the core of the verification process, as the prover mathematically checks them against all possible execution paths of the contract code.
Further Resources
These resources focus on how to define, formalize, and validate security properties before and during smart contract verification. Each one provides concrete methods, languages, or tooling you can apply directly to production Solidity code.
Conclusion and Next Steps
Defining security properties is the critical first step in formal verification, transforming abstract security goals into machine-checkable logic.
Formal verification is only as strong as the properties you define. A well-specified property acts as a precise, unambiguous contract for your SmartContract's behavior. The process moves from high-level security goals—like "funds are safe" or "votes are counted correctly"—to formal logical statements in languages like TLA+, Coq, or the native specification language of your chosen tool (e.g., CVL for Certora or Spec for Foundry). This translation forces you to confront edge cases and implicit assumptions that are often overlooked in manual testing.
Start by categorizing properties into core types. Safety properties assert that "nothing bad happens," such as totalSupply never decreasing or a user's balance never exceeding the total supply. Liveness properties assert that "something good eventually happens," like a withdrawal request eventually being fulfillable. Access control invariants are crucial, specifying that only an owner can call a critical function. For a voting contract, a key property might be: "The sum of all votes cast cannot exceed the total voting power issued." Tools like the OpenZeppelin Contracts Wizard can generate initial access control patterns to verify against.
To implement, integrate a verification tool into your development workflow. For Foundry, you write property tests in Solidity using the forge test command with invariants. For Certora, you write rules in the CVL specification language. Begin with a simple, critical property for a core function, run the verifier, and iteratively expand your specification. This fail-fast approach catches design flaws early. Remember, the verifier proves your code satisfies the specified properties; it cannot guarantee the properties themselves are complete. A buggy or incomplete spec yields a false sense of security.
Your next steps should be practical and incremental. 1) Audit a simple function: Pick a transfer or mint function from your code and write one safety invariant. 2) Explore tooling: Set up a local Foundry project and run forge invariant test on a sample contract. Review the Certora Tutorials for rule writing. 3) Study specifications: Examine verified specs for major protocols like Compound or Aave on their GitHub repositories to see real-world examples. 4) Formalize one module: Choose a core, isolated module (e.g., a token vault) and aim for full specification coverage of its public interface.
Ultimately, defining security properties is an exercise in rigorous thinking. It requires deep understanding of both the intended protocol logic and all possible states and interactions. By embedding this practice into your development lifecycle, you shift security from reactive auditing to proactive assurance. The initial investment in learning and writing specifications pays compounding dividends in reduced vulnerability risk, stronger audit outcomes, and higher confidence in your system's core logic.