Add Certora's Governance verification rules (#2997)
Co-authored-by: Shelly Grossman <shelly@certora.com> Co-authored-by: Aleksander Kryukov <58052996+RedLikeRosesss@users.noreply.github.com> Co-authored-by: Michael M <91594326+MichaelMorami@users.noreply.github.com> Co-authored-by: Aleksander Kryukov <firealexkryukov@gmail.com>
This commit is contained in:
committed by
GitHub
parent
a0a8bbb57f
commit
915ca181ba
5
.gitignore
vendored
5
.gitignore
vendored
@ -57,3 +57,8 @@ allFiredEvents
|
|||||||
# hardhat
|
# hardhat
|
||||||
cache
|
cache
|
||||||
artifacts
|
artifacts
|
||||||
|
|
||||||
|
# Certora
|
||||||
|
.certora*
|
||||||
|
.last_confs
|
||||||
|
certora_*
|
||||||
|
|||||||
24
certora/Makefile
Normal file
24
certora/Makefile
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
default: help
|
||||||
|
|
||||||
|
PATCH = applyHarness.patch
|
||||||
|
CONTRACTS_DIR = ../contracts
|
||||||
|
MUNGED_DIR = munged
|
||||||
|
|
||||||
|
help:
|
||||||
|
@echo "usage:"
|
||||||
|
@echo " make clean: remove all generated files (those ignored by git)"
|
||||||
|
@echo " make $(MUNGED_DIR): create $(MUNGED_DIR) directory by applying the patch file to $(CONTRACTS_DIR)"
|
||||||
|
@echo " make record: record a new patch file capturing the differences between $(CONTRACTS_DIR) and $(MUNGED_DIR)"
|
||||||
|
|
||||||
|
munged: $(wildcard $(CONTRACTS_DIR)/*.sol) $(PATCH)
|
||||||
|
rm -rf $@
|
||||||
|
cp -r $(CONTRACTS_DIR) $@
|
||||||
|
patch -p0 -d $@ < $(PATCH)
|
||||||
|
|
||||||
|
record:
|
||||||
|
diff -ruN $(CONTRACTS_DIR) $(MUNGED_DIR) | sed 's+../contracts/++g' | sed 's+munged/++g' > $(PATCH)
|
||||||
|
|
||||||
|
clean:
|
||||||
|
git clean -fdX
|
||||||
|
touch $(PATCH)
|
||||||
|
|
||||||
56
certora/README.md
Normal file
56
certora/README.md
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# Running the certora verification tool
|
||||||
|
|
||||||
|
These instructions detail the process for running CVT on the OpenZeppelin (Wizard/Governor) contracts.
|
||||||
|
|
||||||
|
Documentation for CVT and the specification language are available
|
||||||
|
[here](https://certora.atlassian.net/wiki/spaces/CPD/overview)
|
||||||
|
|
||||||
|
## Running the verification
|
||||||
|
|
||||||
|
The scripts in the `certora/scripts` directory are used to submit verification
|
||||||
|
jobs to the Certora verification service. After the job is complete, the results will be available on
|
||||||
|
[the Certora portal](https://vaas-stg.certora.com/).
|
||||||
|
|
||||||
|
These scripts should be run from the root directory; for example by running
|
||||||
|
|
||||||
|
```
|
||||||
|
sh certora/scripts/verifyAll.sh <meaningful comment>
|
||||||
|
```
|
||||||
|
|
||||||
|
The most important of these is `verifyAll.sh`, which checks
|
||||||
|
all of the harnessed contracts (`certora/harness/Wizard*.sol`) against all of
|
||||||
|
the specifications (`certora/spec/*.spec`).
|
||||||
|
|
||||||
|
The other scripts run a subset of the specifications or the contracts. You can
|
||||||
|
verify different contracts or specifications by changing the `--verify` option,
|
||||||
|
and you can run a single rule or method with the `--rule` or `--method` option.
|
||||||
|
|
||||||
|
For example, to verify the `WizardFirstPriority` contract against the
|
||||||
|
`GovernorCountingSimple` specification, you could change the `--verify` line of
|
||||||
|
the `WizardControlFirstPriortity.sh` script to:
|
||||||
|
|
||||||
|
```
|
||||||
|
--verify WizardFirstPriority:certora/specs/GovernorCountingSimple.spec \
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adapting to changes in the contracts
|
||||||
|
|
||||||
|
Some of our rules require the code to be simplified in various ways. Our
|
||||||
|
primary tool for performing these simplifications is to run verification on a
|
||||||
|
contract that extends the original contracts and overrides some of the methods.
|
||||||
|
These "harness" contracts can be found in the `certora/harness` directory.
|
||||||
|
|
||||||
|
This pattern does require some modifications to the original code: some methods
|
||||||
|
need to be made virtual or public, for example. These changes are handled by
|
||||||
|
applying a patch to the code before verification.
|
||||||
|
|
||||||
|
When one of the `verify` scripts is executed, it first applies the patch file
|
||||||
|
`certora/applyHarness.patch` to the `contracts` directory, placing the output
|
||||||
|
in the `certora/munged` directory. We then verify the contracts in the
|
||||||
|
`certora/munged` directory.
|
||||||
|
|
||||||
|
If the original contracts change, it is possible to create a conflict with the
|
||||||
|
patch. In this case, the verify scripts will report an error message and output
|
||||||
|
rejected changes in the `munged` directory. After merging the changes, run
|
||||||
|
`make record` in the `certora` directory; this will regenerate the patch file,
|
||||||
|
which can then be checked into git.
|
||||||
101
certora/applyHarness.patch
Normal file
101
certora/applyHarness.patch
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
diff -ruN .gitignore .gitignore
|
||||||
|
--- .gitignore 1969-12-31 19:00:00.000000000 -0500
|
||||||
|
+++ .gitignore 2021-12-09 14:46:33.923637220 -0500
|
||||||
|
@@ -0,0 +1,2 @@
|
||||||
|
+*
|
||||||
|
+!.gitignore
|
||||||
|
diff -ruN governance/compatibility/GovernorCompatibilityBravo.sol governance/compatibility/GovernorCompatibilityBravo.sol
|
||||||
|
--- governance/compatibility/GovernorCompatibilityBravo.sol 2021-12-03 15:24:56.523654357 -0500
|
||||||
|
+++ governance/compatibility/GovernorCompatibilityBravo.sol 2021-12-09 14:46:33.923637220 -0500
|
||||||
|
@@ -245,7 +245,7 @@
|
||||||
|
/**
|
||||||
|
* @dev See {Governor-_quorumReached}. In this module, only forVotes count toward the quorum.
|
||||||
|
*/
|
||||||
|
- function _quorumReached(uint256 proposalId) internal view virtual override returns (bool) {
|
||||||
|
+ function _quorumReached(uint256 proposalId) public view virtual override returns (bool) { // HARNESS: changed to public from internal
|
||||||
|
ProposalDetails storage details = _proposalDetails[proposalId];
|
||||||
|
return quorum(proposalSnapshot(proposalId)) <= details.forVotes;
|
||||||
|
}
|
||||||
|
@@ -253,7 +253,7 @@
|
||||||
|
/**
|
||||||
|
* @dev See {Governor-_voteSucceeded}. In this module, the forVotes must be scritly over the againstVotes.
|
||||||
|
*/
|
||||||
|
- function _voteSucceeded(uint256 proposalId) internal view virtual override returns (bool) {
|
||||||
|
+ function _voteSucceeded(uint256 proposalId) public view virtual override returns (bool) { // HARNESS: changed to public from internal
|
||||||
|
ProposalDetails storage details = _proposalDetails[proposalId];
|
||||||
|
return details.forVotes > details.againstVotes;
|
||||||
|
}
|
||||||
|
diff -ruN governance/extensions/GovernorCountingSimple.sol governance/extensions/GovernorCountingSimple.sol
|
||||||
|
--- governance/extensions/GovernorCountingSimple.sol 2021-12-03 15:24:56.523654357 -0500
|
||||||
|
+++ governance/extensions/GovernorCountingSimple.sol 2021-12-09 14:46:33.923637220 -0500
|
||||||
|
@@ -64,7 +64,7 @@
|
||||||
|
/**
|
||||||
|
* @dev See {Governor-_quorumReached}.
|
||||||
|
*/
|
||||||
|
- function _quorumReached(uint256 proposalId) internal view virtual override returns (bool) {
|
||||||
|
+ function _quorumReached(uint256 proposalId) public view virtual override returns (bool) {
|
||||||
|
ProposalVote storage proposalvote = _proposalVotes[proposalId];
|
||||||
|
|
||||||
|
return quorum(proposalSnapshot(proposalId)) <= proposalvote.forVotes + proposalvote.abstainVotes;
|
||||||
|
@@ -73,7 +73,7 @@
|
||||||
|
/**
|
||||||
|
* @dev See {Governor-_voteSucceeded}. In this module, the forVotes must be strictly over the againstVotes.
|
||||||
|
*/
|
||||||
|
- function _voteSucceeded(uint256 proposalId) internal view virtual override returns (bool) {
|
||||||
|
+ function _voteSucceeded(uint256 proposalId) public view virtual override returns (bool) {
|
||||||
|
ProposalVote storage proposalvote = _proposalVotes[proposalId];
|
||||||
|
|
||||||
|
return proposalvote.forVotes > proposalvote.againstVotes;
|
||||||
|
diff -ruN governance/extensions/GovernorTimelockControl.sol governance/extensions/GovernorTimelockControl.sol
|
||||||
|
--- governance/extensions/GovernorTimelockControl.sol 2021-12-03 15:24:56.523654357 -0500
|
||||||
|
+++ governance/extensions/GovernorTimelockControl.sol 2021-12-09 14:46:33.923637220 -0500
|
||||||
|
@@ -111,7 +111,7 @@
|
||||||
|
bytes[] memory calldatas,
|
||||||
|
bytes32 descriptionHash
|
||||||
|
) internal virtual override {
|
||||||
|
- _timelock.executeBatch{value: msg.value}(targets, values, calldatas, 0, descriptionHash);
|
||||||
|
+ _timelock.executeBatch{value: msg.value}(targets, values, calldatas, 0, descriptionHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
diff -ruN governance/Governor.sol governance/Governor.sol
|
||||||
|
--- governance/Governor.sol 2021-12-03 15:24:56.523654357 -0500
|
||||||
|
+++ governance/Governor.sol 2021-12-09 14:46:56.411503587 -0500
|
||||||
|
@@ -38,8 +38,8 @@
|
||||||
|
|
||||||
|
string private _name;
|
||||||
|
|
||||||
|
- mapping(uint256 => ProposalCore) private _proposals;
|
||||||
|
-
|
||||||
|
+ mapping(uint256 => ProposalCore) public _proposals;
|
||||||
|
+
|
||||||
|
/**
|
||||||
|
* @dev Restrict access to governor executing address. Some module might override the _executor function to make
|
||||||
|
* sure this modifier is consistant with the execution model.
|
||||||
|
@@ -167,12 +167,12 @@
|
||||||
|
/**
|
||||||
|
* @dev Amount of votes already cast passes the threshold limit.
|
||||||
|
*/
|
||||||
|
- function _quorumReached(uint256 proposalId) internal view virtual returns (bool);
|
||||||
|
+ function _quorumReached(uint256 proposalId) public view virtual returns (bool); // HARNESS: changed to public from internal
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Is the proposal successful or not.
|
||||||
|
*/
|
||||||
|
- function _voteSucceeded(uint256 proposalId) internal view virtual returns (bool);
|
||||||
|
+ function _voteSucceeded(uint256 proposalId) public view virtual returns (bool); // HARNESS: changed to public from internal
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Register a vote with a given support and voting weight.
|
||||||
|
diff -ruN token/ERC20/extensions/ERC20Votes.sol token/ERC20/extensions/ERC20Votes.sol
|
||||||
|
--- token/ERC20/extensions/ERC20Votes.sol 2021-12-03 15:24:56.527654330 -0500
|
||||||
|
+++ token/ERC20/extensions/ERC20Votes.sol 2021-12-09 14:46:33.927637196 -0500
|
||||||
|
@@ -84,7 +84,7 @@
|
||||||
|
*
|
||||||
|
* - `blockNumber` must have been already mined
|
||||||
|
*/
|
||||||
|
- function getPastVotes(address account, uint256 blockNumber) public view returns (uint256) {
|
||||||
|
+ function getPastVotes(address account, uint256 blockNumber) public view virtual returns (uint256) {
|
||||||
|
require(blockNumber < block.number, "ERC20Votes: block not yet mined");
|
||||||
|
return _checkpointsLookup(_checkpoints[account], blockNumber);
|
||||||
|
}
|
||||||
28
certora/harnesses/ERC20VotesHarness.sol
Normal file
28
certora/harnesses/ERC20VotesHarness.sol
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import "../munged/token/ERC20/extensions/ERC20Votes.sol";
|
||||||
|
|
||||||
|
contract ERC20VotesHarness is ERC20Votes {
|
||||||
|
constructor(string memory name, string memory symbol) ERC20Permit(name) ERC20(name, symbol) {}
|
||||||
|
|
||||||
|
mapping(address => mapping(uint256 => uint256)) public _getPastVotes;
|
||||||
|
|
||||||
|
function _afterTokenTransfer(
|
||||||
|
address from,
|
||||||
|
address to,
|
||||||
|
uint256 amount
|
||||||
|
) internal virtual override {
|
||||||
|
super._afterTokenTransfer(from, to, amount);
|
||||||
|
_getPastVotes[from][block.number] -= amount;
|
||||||
|
_getPastVotes[to][block.number] += amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Change delegation for `delegator` to `delegatee`.
|
||||||
|
*
|
||||||
|
* Emits events {DelegateChanged} and {DelegateVotesChanged}.
|
||||||
|
*/
|
||||||
|
function _delegate(address delegator, address delegatee) internal virtual override{
|
||||||
|
super._delegate(delegator, delegatee);
|
||||||
|
_getPastVotes[delegator][block.number] -= balanceOf(delegator);
|
||||||
|
_getPastVotes[delegatee][block.number] += balanceOf(delegator);
|
||||||
|
}
|
||||||
|
}
|
||||||
150
certora/harnesses/WizardControlFirstPriority.sol
Normal file
150
certora/harnesses/WizardControlFirstPriority.sol
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
pragma solidity ^0.8.2;
|
||||||
|
|
||||||
|
import "../munged/governance/Governor.sol";
|
||||||
|
import "../munged/governance/extensions/GovernorCountingSimple.sol";
|
||||||
|
import "../munged/governance/extensions/GovernorVotes.sol";
|
||||||
|
import "../munged/governance/extensions/GovernorVotesQuorumFraction.sol";
|
||||||
|
import "../munged/governance/extensions/GovernorTimelockControl.sol";
|
||||||
|
import "../munged/governance/extensions/GovernorProposalThreshold.sol";
|
||||||
|
|
||||||
|
/*
|
||||||
|
Wizard options:
|
||||||
|
ProposalThreshhold = 10
|
||||||
|
ERC20Votes
|
||||||
|
TimelockController
|
||||||
|
*/
|
||||||
|
|
||||||
|
contract WizardControlFirstPriority is Governor, GovernorProposalThreshold, GovernorCountingSimple, GovernorVotes, GovernorVotesQuorumFraction, GovernorTimelockControl {
|
||||||
|
constructor(ERC20Votes _token, TimelockController _timelock, string memory name, uint256 quorumFraction)
|
||||||
|
Governor(name)
|
||||||
|
GovernorVotes(_token)
|
||||||
|
GovernorVotesQuorumFraction(quorumFraction)
|
||||||
|
GovernorTimelockControl(_timelock)
|
||||||
|
{}
|
||||||
|
|
||||||
|
//HARNESS
|
||||||
|
|
||||||
|
function isExecuted(uint256 proposalId) public view returns (bool) {
|
||||||
|
return _proposals[proposalId].executed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCanceled(uint256 proposalId) public view returns (bool) {
|
||||||
|
return _proposals[proposalId].canceled;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint256 _votingDelay;
|
||||||
|
|
||||||
|
uint256 _votingPeriod;
|
||||||
|
|
||||||
|
uint256 _proposalThreshold;
|
||||||
|
|
||||||
|
mapping(uint256 => uint256) public ghost_sum_vote_power_by_id;
|
||||||
|
|
||||||
|
function _castVote(
|
||||||
|
uint256 proposalId,
|
||||||
|
address account,
|
||||||
|
uint8 support,
|
||||||
|
string memory reason
|
||||||
|
) internal override virtual returns (uint256) {
|
||||||
|
|
||||||
|
uint256 deltaWeight = super._castVote(proposalId, account, support, reason); //HARNESS
|
||||||
|
ghost_sum_vote_power_by_id[proposalId] += deltaWeight;
|
||||||
|
|
||||||
|
return deltaWeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function snapshot(uint256 proposalId) public view returns (uint64) {
|
||||||
|
return _proposals[proposalId].voteStart._deadline;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getExecutor() public view returns (address){
|
||||||
|
return _executor();
|
||||||
|
}
|
||||||
|
|
||||||
|
// original code, harnessed
|
||||||
|
|
||||||
|
function votingDelay() public view override returns (uint256) { // HARNESS: pure -> view
|
||||||
|
return _votingDelay; // HARNESS: parametric
|
||||||
|
}
|
||||||
|
|
||||||
|
function votingPeriod() public view override returns (uint256) { // HARNESS: pure -> view
|
||||||
|
return _votingPeriod; // HARNESS: parametric
|
||||||
|
}
|
||||||
|
|
||||||
|
function proposalThreshold() public view override returns (uint256) { // HARNESS: pure -> view
|
||||||
|
return _proposalThreshold; // HARNESS: parametric
|
||||||
|
}
|
||||||
|
|
||||||
|
// original code, not harnessed
|
||||||
|
// The following functions are overrides required by Solidity.
|
||||||
|
|
||||||
|
function quorum(uint256 blockNumber)
|
||||||
|
public
|
||||||
|
view
|
||||||
|
override(IGovernor, GovernorVotesQuorumFraction)
|
||||||
|
returns (uint256)
|
||||||
|
{
|
||||||
|
return super.quorum(blockNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVotes(address account, uint256 blockNumber)
|
||||||
|
public
|
||||||
|
view
|
||||||
|
override(IGovernor, GovernorVotes)
|
||||||
|
returns (uint256)
|
||||||
|
{
|
||||||
|
return super.getVotes(account, blockNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
function state(uint256 proposalId)
|
||||||
|
public
|
||||||
|
view
|
||||||
|
override(Governor, GovernorTimelockControl)
|
||||||
|
returns (ProposalState)
|
||||||
|
{
|
||||||
|
return super.state(proposalId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function propose(address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description)
|
||||||
|
public
|
||||||
|
override(Governor, GovernorProposalThreshold, IGovernor)
|
||||||
|
returns (uint256)
|
||||||
|
{
|
||||||
|
return super.propose(targets, values, calldatas, description);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _execute(uint256 proposalId, address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash)
|
||||||
|
internal
|
||||||
|
override(Governor, GovernorTimelockControl)
|
||||||
|
{
|
||||||
|
super._execute(proposalId, targets, values, calldatas, descriptionHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _cancel(address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash)
|
||||||
|
internal
|
||||||
|
override(Governor, GovernorTimelockControl)
|
||||||
|
returns (uint256)
|
||||||
|
{
|
||||||
|
return super._cancel(targets, values, calldatas, descriptionHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _executor()
|
||||||
|
internal
|
||||||
|
view
|
||||||
|
override(Governor, GovernorTimelockControl)
|
||||||
|
returns (address)
|
||||||
|
{
|
||||||
|
return super._executor();
|
||||||
|
}
|
||||||
|
|
||||||
|
function supportsInterface(bytes4 interfaceId)
|
||||||
|
public
|
||||||
|
view
|
||||||
|
override(Governor, GovernorTimelockControl)
|
||||||
|
returns (bool)
|
||||||
|
{
|
||||||
|
return super.supportsInterface(interfaceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
141
certora/harnesses/WizardFirstTry.sol
Normal file
141
certora/harnesses/WizardFirstTry.sol
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
pragma solidity ^0.8.2;
|
||||||
|
|
||||||
|
import "../munged/governance/Governor.sol";
|
||||||
|
import "../munged/governance/extensions/GovernorCountingSimple.sol";
|
||||||
|
import "../munged/governance/extensions/GovernorVotes.sol";
|
||||||
|
import "../munged/governance/extensions/GovernorVotesQuorumFraction.sol";
|
||||||
|
import "../munged/governance/extensions/GovernorTimelockCompound.sol";
|
||||||
|
|
||||||
|
/*
|
||||||
|
Wizard options:
|
||||||
|
ERC20Votes
|
||||||
|
TimelockCompound
|
||||||
|
*/
|
||||||
|
|
||||||
|
contract WizardFirstTry is Governor, GovernorCountingSimple, GovernorVotes, GovernorVotesQuorumFraction, GovernorTimelockCompound {
|
||||||
|
constructor(ERC20Votes _token, ICompoundTimelock _timelock, string memory name, uint256 quorumFraction)
|
||||||
|
Governor(name)
|
||||||
|
GovernorVotes(_token)
|
||||||
|
GovernorVotesQuorumFraction(quorumFraction)
|
||||||
|
GovernorTimelockCompound(_timelock)
|
||||||
|
{}
|
||||||
|
|
||||||
|
//HARNESS
|
||||||
|
|
||||||
|
function isExecuted(uint256 proposalId) public view returns (bool) {
|
||||||
|
return _proposals[proposalId].executed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCanceled(uint256 proposalId) public view returns (bool) {
|
||||||
|
return _proposals[proposalId].canceled;
|
||||||
|
}
|
||||||
|
|
||||||
|
function snapshot(uint256 proposalId) public view returns (uint64) {
|
||||||
|
return _proposals[proposalId].voteStart._deadline;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getExecutor() public view returns (address){
|
||||||
|
return _executor();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint256 _votingDelay;
|
||||||
|
|
||||||
|
uint256 _votingPeriod;
|
||||||
|
|
||||||
|
mapping(uint256 => uint256) public ghost_sum_vote_power_by_id;
|
||||||
|
|
||||||
|
function _castVote(
|
||||||
|
uint256 proposalId,
|
||||||
|
address account,
|
||||||
|
uint8 support,
|
||||||
|
string memory reason
|
||||||
|
) internal override virtual returns (uint256) {
|
||||||
|
|
||||||
|
uint256 deltaWeight = super._castVote(proposalId, account, support, reason); //HARNESS
|
||||||
|
ghost_sum_vote_power_by_id[proposalId] += deltaWeight;
|
||||||
|
|
||||||
|
return deltaWeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// original code, harnessed
|
||||||
|
|
||||||
|
function votingDelay() public view override virtual returns (uint256) { // HARNESS: pure -> view
|
||||||
|
return _votingDelay; // HARNESS: parametric
|
||||||
|
}
|
||||||
|
|
||||||
|
function votingPeriod() public view override virtual returns (uint256) { // HARNESS: pure -> view
|
||||||
|
return _votingPeriod; // HARNESS: parametric
|
||||||
|
}
|
||||||
|
|
||||||
|
// original code, not harnessed
|
||||||
|
// The following functions are overrides required by Solidity.
|
||||||
|
|
||||||
|
function quorum(uint256 blockNumber)
|
||||||
|
public
|
||||||
|
view
|
||||||
|
override(IGovernor, GovernorVotesQuorumFraction)
|
||||||
|
returns (uint256)
|
||||||
|
{
|
||||||
|
return super.quorum(blockNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVotes(address account, uint256 blockNumber)
|
||||||
|
public
|
||||||
|
view
|
||||||
|
override(IGovernor, GovernorVotes)
|
||||||
|
returns (uint256)
|
||||||
|
{
|
||||||
|
return super.getVotes(account, blockNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
function state(uint256 proposalId)
|
||||||
|
public
|
||||||
|
view
|
||||||
|
override(Governor, GovernorTimelockCompound)
|
||||||
|
returns (ProposalState)
|
||||||
|
{
|
||||||
|
return super.state(proposalId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function propose(address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description)
|
||||||
|
public
|
||||||
|
override(Governor, IGovernor)
|
||||||
|
returns (uint256)
|
||||||
|
{
|
||||||
|
return super.propose(targets, values, calldatas, description);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _execute(uint256 proposalId, address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash)
|
||||||
|
internal
|
||||||
|
override(Governor, GovernorTimelockCompound)
|
||||||
|
{
|
||||||
|
super._execute(proposalId, targets, values, calldatas, descriptionHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _cancel(address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash)
|
||||||
|
internal
|
||||||
|
override(Governor, GovernorTimelockCompound)
|
||||||
|
returns (uint256)
|
||||||
|
{
|
||||||
|
return super._cancel(targets, values, calldatas, descriptionHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _executor()
|
||||||
|
internal
|
||||||
|
view
|
||||||
|
override(Governor, GovernorTimelockCompound)
|
||||||
|
returns (address)
|
||||||
|
{
|
||||||
|
return super._executor();
|
||||||
|
}
|
||||||
|
|
||||||
|
function supportsInterface(bytes4 interfaceId)
|
||||||
|
public
|
||||||
|
view
|
||||||
|
override(Governor, GovernorTimelockCompound)
|
||||||
|
returns (bool)
|
||||||
|
{
|
||||||
|
return super.supportsInterface(interfaceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
2
certora/munged/.gitignore
vendored
Normal file
2
certora/munged/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
10
certora/scripts/Governor.sh
Executable file
10
certora/scripts/Governor.sh
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
make -C certora munged
|
||||||
|
|
||||||
|
certoraRun certora/harnesses/ERC20VotesHarness.sol certora/harnesses/GovernorHarness.sol \
|
||||||
|
--verify GovernorHarness:certora/specs/GovernorBase.spec \
|
||||||
|
--solc solc8.0 \
|
||||||
|
--staging shelly/forSasha \
|
||||||
|
--optimistic_loop \
|
||||||
|
--settings -copyLoopUnroll=4 \
|
||||||
|
--rule voteStartBeforeVoteEnd \
|
||||||
|
--msg "$1"
|
||||||
10
certora/scripts/GovernorCountingSimple-counting.sh
Normal file
10
certora/scripts/GovernorCountingSimple-counting.sh
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
make -C certora munged
|
||||||
|
|
||||||
|
certoraRun certora/harnesses/ERC20VotesHarness.sol certora/harnesses/GovernorBasicHarness.sol \
|
||||||
|
--verify GovernorBasicHarness:certora/specs/GovernorCountingSimple.spec \
|
||||||
|
--solc solc8.2 \
|
||||||
|
--staging shelly/forSasha \
|
||||||
|
--optimistic_loop \
|
||||||
|
--settings -copyLoopUnroll=4 \
|
||||||
|
--rule hasVotedCorrelation \
|
||||||
|
--msg "$1"
|
||||||
12
certora/scripts/WizardControlFirstPriority.sh
Normal file
12
certora/scripts/WizardControlFirstPriority.sh
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
make -C certora munged
|
||||||
|
|
||||||
|
certoraRun certora/harnesses/ERC20VotesHarness.sol certora/harnesses/WizardControlFirstPriority.sol \
|
||||||
|
--link WizardControlFirstPriority:token=ERC20VotesHarness \
|
||||||
|
--verify WizardControlFirstPriority:certora/specs/GovernorBase.spec \
|
||||||
|
--solc solc8.2 \
|
||||||
|
--disableLocalTypeChecking \
|
||||||
|
--staging shelly/forSasha \
|
||||||
|
--optimistic_loop \
|
||||||
|
--settings -copyLoopUnroll=4 \
|
||||||
|
--rule canVoteDuringVotingPeriod \
|
||||||
|
--msg "$1"
|
||||||
10
certora/scripts/WizardFirstTry.sh
Normal file
10
certora/scripts/WizardFirstTry.sh
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
make -C certora munged
|
||||||
|
|
||||||
|
certoraRun certora/harnesses/ERC20VotesHarness.sol certora/harnesses/WizardFirstTry.sol \
|
||||||
|
--verify WizardFirstTry:certora/specs/GovernorBase.spec \
|
||||||
|
--solc solc8.2 \
|
||||||
|
--staging shelly/forSasha \
|
||||||
|
--optimistic_loop \
|
||||||
|
--disableLocalTypeChecking \
|
||||||
|
--settings -copyLoopUnroll=4 \
|
||||||
|
--msg "$1"
|
||||||
14
certora/scripts/sanity.sh
Normal file
14
certora/scripts/sanity.sh
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
make -C certora munged
|
||||||
|
|
||||||
|
for f in certora/harnesses/Wizard*.sol
|
||||||
|
do
|
||||||
|
echo "Processing $f"
|
||||||
|
file=$(basename $f)
|
||||||
|
echo ${file%.*}
|
||||||
|
certoraRun certora/harnesses/$file \
|
||||||
|
--verify ${file%.*}:certora/specs/sanity.spec "$@" \
|
||||||
|
--solc solc8.2 --staging shelly/forSasha \
|
||||||
|
--optimistic_loop \
|
||||||
|
--msg "checking sanity on ${file%.*}"
|
||||||
|
--settings -copyLoopUnroll=4
|
||||||
|
done
|
||||||
39
certora/scripts/verifyAll.sh
Normal file
39
certora/scripts/verifyAll.sh
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
make -C certora munged
|
||||||
|
|
||||||
|
for contract in certora/harnesses/Wizard*.sol;
|
||||||
|
do
|
||||||
|
for spec in certora/specs/*.spec;
|
||||||
|
do
|
||||||
|
contractFile=$(basename $contract)
|
||||||
|
specFile=$(basename $spec)
|
||||||
|
if [[ "${specFile%.*}" != "RulesInProgress" ]];
|
||||||
|
then
|
||||||
|
echo "Processing ${contractFile%.*} with $specFile"
|
||||||
|
if [[ "${contractFile%.*}" = *"WizardControl"* ]];
|
||||||
|
then
|
||||||
|
certoraRun certora/harnesses/ERC20VotesHarness.sol certora/harnesses/$contractFile \
|
||||||
|
--link ${contractFile%.*}:token=ERC20VotesHarness \
|
||||||
|
--verify ${contractFile%.*}:certora/specs/$specFile "$@" \
|
||||||
|
--solc solc8.2 \
|
||||||
|
--staging shelly/forSasha \
|
||||||
|
--disableLocalTypeChecking \
|
||||||
|
--optimistic_loop \
|
||||||
|
--settings -copyLoopUnroll=4 \
|
||||||
|
--send_only \
|
||||||
|
--msg "checking $specFile on ${contractFile%.*}"
|
||||||
|
else
|
||||||
|
certoraRun certora/harnesses/ERC20VotesHarness.sol certora/harnesses/$contractFile \
|
||||||
|
--verify ${contractFile%.*}:certora/specs/$specFile "$@" \
|
||||||
|
--solc solc8.2 \
|
||||||
|
--staging shelly/forSasha \
|
||||||
|
--disableLocalTypeChecking \
|
||||||
|
--optimistic_loop \
|
||||||
|
--settings -copyLoopUnroll=4 \
|
||||||
|
--send_only \
|
||||||
|
--msg "checking $specFile on ${contractFile%.*}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
334
certora/specs/GovernorBase.spec
Normal file
334
certora/specs/GovernorBase.spec
Normal file
@ -0,0 +1,334 @@
|
|||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
///////////////////// Governor.sol base definitions //////////////////////////
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
using ERC20VotesHarness as erc20votes
|
||||||
|
|
||||||
|
methods {
|
||||||
|
proposalSnapshot(uint256) returns uint256 envfree // matches proposalVoteStart
|
||||||
|
proposalDeadline(uint256) returns uint256 envfree // matches proposalVoteEnd
|
||||||
|
hashProposal(address[],uint256[],bytes[],bytes32) returns uint256 envfree
|
||||||
|
isExecuted(uint256) returns bool envfree
|
||||||
|
isCanceled(uint256) returns bool envfree
|
||||||
|
execute(address[], uint256[], bytes[], bytes32) returns uint256
|
||||||
|
hasVoted(uint256, address) returns bool
|
||||||
|
castVote(uint256, uint8) returns uint256
|
||||||
|
updateQuorumNumerator(uint256)
|
||||||
|
queue(address[], uint256[], bytes[], bytes32) returns uint256
|
||||||
|
|
||||||
|
// internal functions made public in harness:
|
||||||
|
_quorumReached(uint256) returns bool
|
||||||
|
_voteSucceeded(uint256) returns bool envfree
|
||||||
|
|
||||||
|
// function summarization
|
||||||
|
proposalThreshold() returns uint256 envfree
|
||||||
|
|
||||||
|
getVotes(address, uint256) returns uint256 => DISPATCHER(true)
|
||||||
|
|
||||||
|
getPastTotalSupply(uint256 t) returns uint256 => PER_CALLEE_CONSTANT
|
||||||
|
getPastVotes(address a, uint256 t) returns uint256 => PER_CALLEE_CONSTANT
|
||||||
|
|
||||||
|
//scheduleBatch(address[],uint256[],bytes[],bytes32,bytes32,uint256) => DISPATCHER(true)
|
||||||
|
//executeBatch(address[], uint256[], bytes[], bytes32, bytes32) => DISPATCHER(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
//////////////////////////////// Definitions /////////////////////////////////
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
|
// proposal was created - relation proved in noStartBeforeCreation
|
||||||
|
definition proposalCreated(uint256 pId) returns bool = proposalSnapshot(pId) > 0;
|
||||||
|
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
///////////////////////////// Helper Functions ///////////////////////////////
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
function helperFunctionsWithRevert(uint256 proposalId, method f, env e) {
|
||||||
|
address[] targets; uint256[] values; bytes[] calldatas; string reason; bytes32 descriptionHash;
|
||||||
|
uint8 support; uint8 v; bytes32 r; bytes32 s;
|
||||||
|
if (f.selector == propose(address[], uint256[], bytes[], string).selector) {
|
||||||
|
uint256 result = propose@withrevert(e, targets, values, calldatas, reason);
|
||||||
|
require(result == proposalId);
|
||||||
|
} else if (f.selector == execute(address[], uint256[], bytes[], bytes32).selector) {
|
||||||
|
uint256 result = execute@withrevert(e, targets, values, calldatas, descriptionHash);
|
||||||
|
require(result == proposalId);
|
||||||
|
} else if (f.selector == castVote(uint256, uint8).selector) {
|
||||||
|
castVote@withrevert(e, proposalId, support);
|
||||||
|
} else if (f.selector == castVoteWithReason(uint256, uint8, string).selector) {
|
||||||
|
castVoteWithReason@withrevert(e, proposalId, support, reason);
|
||||||
|
} else if (f.selector == castVoteBySig(uint256, uint8,uint8, bytes32, bytes32).selector) {
|
||||||
|
castVoteBySig@withrevert(e, proposalId, support, v, r, s);
|
||||||
|
} else if (f.selector == queue(address[], uint256[], bytes[], bytes32).selector) {
|
||||||
|
queue@withrevert(e, targets, values, calldatas, descriptionHash);
|
||||||
|
} else {
|
||||||
|
calldataarg args;
|
||||||
|
f@withrevert(e, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
///////////////////////////////////////////////////// State Diagram //////////////////////////////////////////////////////////
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// //
|
||||||
|
// castVote(s)() //
|
||||||
|
// ------------- propose() ---------------------- time pass --------------- time passes ----------- //
|
||||||
|
// | No Proposal | --------> | Before Start (Delay) | --------> | Voting Period | ----------------------> | execute() | //
|
||||||
|
// ------------- ---------------------- --------------- -> Executed/Canceled ----------- //
|
||||||
|
// ------------------------------------------------------------|---------------|-------------------------|--------------> //
|
||||||
|
// t start end timelock //
|
||||||
|
// //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
///////////////////////////////// Global Valid States /////////////////////////////////
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Start and end date are either initialized (non zero) or uninitialized (zero) simultaneously
|
||||||
|
* This invariant assumes that the block number cannot be 0 at any stage of the contract cycle
|
||||||
|
* This is very safe assumption as usually the 0 block is genesis block which is uploaded with data
|
||||||
|
* by the developers and will not be valid to raise proposals (at the current way that block chain is functioning)
|
||||||
|
*/
|
||||||
|
// To use env with general preserved block disable type checking [--disableLocalTypeChecking]
|
||||||
|
invariant startAndEndDatesNonZero(uint256 pId)
|
||||||
|
proposalSnapshot(pId) != 0 <=> proposalDeadline(pId) != 0
|
||||||
|
{ preserved with (env e){
|
||||||
|
require e.block.number > 0;
|
||||||
|
}}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If a proposal is canceled it must have a start and an end date
|
||||||
|
*/
|
||||||
|
// To use env with general preserved block disable type checking [--disableLocalTypeChecking]
|
||||||
|
invariant canceledImplyStartAndEndDateNonZero(uint pId)
|
||||||
|
isCanceled(pId) => proposalSnapshot(pId) != 0
|
||||||
|
{preserved with (env e){
|
||||||
|
require e.block.number > 0;
|
||||||
|
}}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If a proposal is executed it must have a start and an end date
|
||||||
|
*/
|
||||||
|
// To use env with general preserved block disable type checking [--disableLocalTypeChecking]
|
||||||
|
invariant executedImplyStartAndEndDateNonZero(uint pId)
|
||||||
|
isExecuted(pId) => proposalSnapshot(pId) != 0
|
||||||
|
{ preserved with (env e){
|
||||||
|
requireInvariant startAndEndDatesNonZero(pId);
|
||||||
|
require e.block.number > 0;
|
||||||
|
}}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A proposal starting block number must be less or equal than the proposal end date
|
||||||
|
*/
|
||||||
|
invariant voteStartBeforeVoteEnd(uint256 pId)
|
||||||
|
// from < to <= because snapshot and deadline can be the same block number if delays are set to 0
|
||||||
|
// This is possible before the integration of GovernorSettings.sol to the system.
|
||||||
|
// After integration of GovernorSettings.sol the invariant expression should be changed from <= to <
|
||||||
|
(proposalSnapshot(pId) > 0 => proposalSnapshot(pId) <= proposalDeadline(pId))
|
||||||
|
// (proposalSnapshot(pId) > 0 => proposalSnapshot(pId) <= proposalDeadline(pId))
|
||||||
|
{ preserved {
|
||||||
|
requireInvariant startAndEndDatesNonZero(pId);
|
||||||
|
}}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A proposal cannot be both executed and canceled simultaneously.
|
||||||
|
*/
|
||||||
|
invariant noBothExecutedAndCanceled(uint256 pId)
|
||||||
|
!isExecuted(pId) || !isCanceled(pId)
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A proposal could be executed only if quorum was reached and vote succeeded
|
||||||
|
*/
|
||||||
|
rule executionOnlyIfQuoromReachedAndVoteSucceeded(uint256 pId, env e, method f){
|
||||||
|
bool isExecutedBefore = isExecuted(pId);
|
||||||
|
bool quorumReachedBefore = _quorumReached(e, pId);
|
||||||
|
bool voteSucceededBefore = _voteSucceeded(pId);
|
||||||
|
|
||||||
|
calldataarg args;
|
||||||
|
f(e, args);
|
||||||
|
|
||||||
|
bool isExecutedAfter = isExecuted(pId);
|
||||||
|
assert (!isExecutedBefore && isExecutedAfter) => (quorumReachedBefore && voteSucceededBefore), "quorum was changed";
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
////////////////////////////////// In-State Rules /////////////////////////////////////
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
//==========================================
|
||||||
|
//------------- Voting Period --------------
|
||||||
|
//==========================================
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A user cannot vote twice
|
||||||
|
*/
|
||||||
|
// Checked for castVote only. all 3 castVote functions call _castVote, so the completness of the verification is counted on
|
||||||
|
// the fact that the 3 functions themselves makes no chages, but rather call an internal function to execute.
|
||||||
|
// That means that we do not check those 3 functions directly, however for castVote & castVoteWithReason it is quite trivial
|
||||||
|
// to understand why this is ok. For castVoteBySig we basically assume that the signature referendum is correct without checking it.
|
||||||
|
// We could check each function seperately and pass the rule, but that would have uglyfied the code with no concrete
|
||||||
|
// benefit, as it is evident that nothing is happening in the first 2 functions (calling a view function), and we do not desire to check the signature verification.
|
||||||
|
rule doubleVoting(uint256 pId, uint8 sup, method f) {
|
||||||
|
env e;
|
||||||
|
address user = e.msg.sender;
|
||||||
|
bool votedCheck = hasVoted(e, pId, user);
|
||||||
|
|
||||||
|
castVote@withrevert(e, pId, sup);
|
||||||
|
|
||||||
|
assert votedCheck => lastReverted, "double voting accured";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//////////////////////////// State Transitions Rules //////////////////////////////////
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
//===========================================
|
||||||
|
//-------- Propose() --> End of Time --------
|
||||||
|
//===========================================
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Once a proposal is created, voteStart and voteEnd are immutable
|
||||||
|
*/
|
||||||
|
rule immutableFieldsAfterProposalCreation(uint256 pId, method f) {
|
||||||
|
uint256 _voteStart = proposalSnapshot(pId);
|
||||||
|
uint256 _voteEnd = proposalDeadline(pId);
|
||||||
|
|
||||||
|
require proposalCreated(pId); // startDate > 0
|
||||||
|
|
||||||
|
env e; calldataarg arg;
|
||||||
|
f(e, arg);
|
||||||
|
|
||||||
|
uint256 voteStart_ = proposalSnapshot(pId);
|
||||||
|
uint256 voteEnd_ = proposalDeadline(pId);
|
||||||
|
assert _voteStart == voteStart_, "Start date was changed";
|
||||||
|
assert _voteEnd == voteEnd_, "End date was changed";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Voting cannot start at a block number prior to proposal’s creation block number
|
||||||
|
*/
|
||||||
|
rule noStartBeforeCreation(uint256 pId) {
|
||||||
|
uint256 previousStart = proposalSnapshot(pId);
|
||||||
|
// This line makes sure that we see only cases where start date is changed from 0, i.e. creation of proposal
|
||||||
|
// We proved in immutableFieldsAfterProposalCreation that once dates set for proposal, it cannot be changed
|
||||||
|
require !proposalCreated(pId); // previousStart == 0;
|
||||||
|
|
||||||
|
env e; calldataarg args;
|
||||||
|
propose(e, args);
|
||||||
|
|
||||||
|
uint256 newStart = proposalSnapshot(pId);
|
||||||
|
// if created, start is after current block number (creation block)
|
||||||
|
assert(newStart != previousStart => newStart >= e.block.number);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//============================================
|
||||||
|
//--- End of Voting Period --> End of Time ---
|
||||||
|
//============================================
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A proposal can neither be executed nor canceled before it ends
|
||||||
|
*/
|
||||||
|
// By induction it cannot be executed nor canceled before it starts, due to voteStartBeforeVoteEnd
|
||||||
|
rule noExecuteOrCancelBeforeDeadline(uint256 pId, method f){
|
||||||
|
require !isExecuted(pId) && !isCanceled(pId);
|
||||||
|
|
||||||
|
env e; calldataarg args;
|
||||||
|
f(e, args);
|
||||||
|
|
||||||
|
assert e.block.number < proposalDeadline(pId) => (!isExecuted(pId) && !isCanceled(pId)), "executed/cancelled before deadline";
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
////////////////////// Integrity Of Functions (Unit Tests) /////////////////////
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
////////////////////////////// High Level Rules ////////////////////////////////
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
///////////////////////////// Not Categorized Yet //////////////////////////////
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* All proposal specific (non-view) functions should revert if proposal is executed
|
||||||
|
*/
|
||||||
|
// In this rule we show that if a function is executed, i.e. execute() was called on the proposal ID,
|
||||||
|
// non of the proposal specific functions can make changes again. In executedOnlyAfterExecuteFunc
|
||||||
|
// we connected the executed attribute to the execute() function, showing that only execute() can
|
||||||
|
// change it, and that it will always change it.
|
||||||
|
rule allFunctionsRevertIfExecuted(method f) filtered { f ->
|
||||||
|
!f.isView && !f.isFallback
|
||||||
|
&& f.selector != updateTimelock(address).selector
|
||||||
|
&& f.selector != updateQuorumNumerator(uint256).selector
|
||||||
|
&& f.selector != queue(address[],uint256[],bytes[],bytes32).selector
|
||||||
|
&& f.selector != relay(address,uint256,bytes).selector
|
||||||
|
&& f.selector != 0xb9a61961 // __acceptAdmin()
|
||||||
|
} {
|
||||||
|
env e; calldataarg args;
|
||||||
|
uint256 pId;
|
||||||
|
require(isExecuted(pId));
|
||||||
|
requireInvariant noBothExecutedAndCanceled(pId);
|
||||||
|
requireInvariant executedImplyStartAndEndDateNonZero(pId);
|
||||||
|
|
||||||
|
helperFunctionsWithRevert(pId, f, e);
|
||||||
|
|
||||||
|
assert(lastReverted, "Function was not reverted");
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* All proposal specific (non-view) functions should revert if proposal is canceled
|
||||||
|
*/
|
||||||
|
rule allFunctionsRevertIfCanceled(method f) filtered {
|
||||||
|
f -> !f.isView && !f.isFallback
|
||||||
|
&& f.selector != updateTimelock(address).selector
|
||||||
|
&& f.selector != updateQuorumNumerator(uint256).selector
|
||||||
|
&& f.selector != queue(address[],uint256[],bytes[],bytes32).selector
|
||||||
|
&& f.selector != relay(address,uint256,bytes).selector
|
||||||
|
&& f.selector != 0xb9a61961 // __acceptAdmin()
|
||||||
|
} {
|
||||||
|
env e; calldataarg args;
|
||||||
|
uint256 pId;
|
||||||
|
require(isCanceled(pId));
|
||||||
|
requireInvariant noBothExecutedAndCanceled(pId);
|
||||||
|
requireInvariant canceledImplyStartAndEndDateNonZero(pId);
|
||||||
|
|
||||||
|
helperFunctionsWithRevert(pId, f, e);
|
||||||
|
|
||||||
|
assert(lastReverted, "Function was not reverted");
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Proposal can be switched to executed only via execute() function
|
||||||
|
*/
|
||||||
|
rule executedOnlyAfterExecuteFunc(address[] targets, uint256[] values, bytes[] calldatas, bytes32 descriptionHash, method f) {
|
||||||
|
env e; calldataarg args;
|
||||||
|
uint256 pId;
|
||||||
|
bool executedBefore = isExecuted(pId);
|
||||||
|
require(!executedBefore);
|
||||||
|
|
||||||
|
helperFunctionsWithRevert(pId, f, e);
|
||||||
|
|
||||||
|
bool executedAfter = isExecuted(pId);
|
||||||
|
assert(executedAfter != executedBefore => f.selector == execute(address[], uint256[], bytes[], bytes32).selector, "isExecuted only changes in the execute method");
|
||||||
|
}
|
||||||
|
|
||||||
221
certora/specs/GovernorCountingSimple.spec
Normal file
221
certora/specs/GovernorCountingSimple.spec
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
import "GovernorBase.spec"
|
||||||
|
|
||||||
|
using ERC20VotesHarness as erc20votes
|
||||||
|
|
||||||
|
methods {
|
||||||
|
ghost_sum_vote_power_by_id(uint256) returns uint256 envfree
|
||||||
|
|
||||||
|
quorum(uint256) returns uint256
|
||||||
|
proposalVotes(uint256) returns (uint256, uint256, uint256) envfree
|
||||||
|
|
||||||
|
quorumNumerator() returns uint256
|
||||||
|
_executor() returns address
|
||||||
|
|
||||||
|
erc20votes._getPastVotes(address, uint256) returns uint256
|
||||||
|
|
||||||
|
getExecutor() returns address
|
||||||
|
|
||||||
|
timelock() returns address
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
///////////////////////////////// GHOSTS /////////////////////////////////////
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
|
//////////// ghosts to keep track of votes counting ////////////
|
||||||
|
|
||||||
|
/*
|
||||||
|
* the sum of voting power of those who voted
|
||||||
|
*/
|
||||||
|
ghost sum_all_votes_power() returns uint256 {
|
||||||
|
init_state axiom sum_all_votes_power() == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
hook Sstore ghost_sum_vote_power_by_id [KEY uint256 pId] uint256 current_power(uint256 old_power) STORAGE {
|
||||||
|
havoc sum_all_votes_power assuming sum_all_votes_power@new() == sum_all_votes_power@old() - old_power + current_power;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* sum of all votes casted per proposal
|
||||||
|
*/
|
||||||
|
ghost tracked_weight(uint256) returns uint256 {
|
||||||
|
init_state axiom forall uint256 p. tracked_weight(p) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* sum of all votes casted
|
||||||
|
*/
|
||||||
|
ghost sum_tracked_weight() returns uint256 {
|
||||||
|
init_state axiom sum_tracked_weight() == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* getter for _proposalVotes.againstVotes
|
||||||
|
*/
|
||||||
|
ghost votesAgainst() returns uint256 {
|
||||||
|
init_state axiom votesAgainst() == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* getter for _proposalVotes.forVotes
|
||||||
|
*/
|
||||||
|
ghost votesFor() returns uint256 {
|
||||||
|
init_state axiom votesFor() == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* getter for _proposalVotes.abstainVotes
|
||||||
|
*/
|
||||||
|
ghost votesAbstain() returns uint256 {
|
||||||
|
init_state axiom votesAbstain() == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
hook Sstore _proposalVotes [KEY uint256 pId].againstVotes uint256 votes(uint256 old_votes) STORAGE {
|
||||||
|
havoc tracked_weight assuming forall uint256 p.(p == pId => tracked_weight@new(p) == tracked_weight@old(p) - old_votes + votes) &&
|
||||||
|
(p != pId => tracked_weight@new(p) == tracked_weight@old(p));
|
||||||
|
havoc sum_tracked_weight assuming sum_tracked_weight@new() == sum_tracked_weight@old() - old_votes + votes;
|
||||||
|
havoc votesAgainst assuming votesAgainst@new() == votesAgainst@old() - old_votes + votes;
|
||||||
|
}
|
||||||
|
|
||||||
|
hook Sstore _proposalVotes [KEY uint256 pId].forVotes uint256 votes(uint256 old_votes) STORAGE {
|
||||||
|
havoc tracked_weight assuming forall uint256 p.(p == pId => tracked_weight@new(p) == tracked_weight@old(p) - old_votes + votes) &&
|
||||||
|
(p != pId => tracked_weight@new(p) == tracked_weight@old(p));
|
||||||
|
havoc sum_tracked_weight assuming sum_tracked_weight@new() == sum_tracked_weight@old() - old_votes + votes;
|
||||||
|
havoc votesFor assuming votesFor@new() == votesFor@old() - old_votes + votes;
|
||||||
|
}
|
||||||
|
|
||||||
|
hook Sstore _proposalVotes [KEY uint256 pId].abstainVotes uint256 votes(uint256 old_votes) STORAGE {
|
||||||
|
havoc tracked_weight assuming forall uint256 p.(p == pId => tracked_weight@new(p) == tracked_weight@old(p) - old_votes + votes) &&
|
||||||
|
(p != pId => tracked_weight@new(p) == tracked_weight@old(p));
|
||||||
|
havoc sum_tracked_weight assuming sum_tracked_weight@new() == sum_tracked_weight@old() - old_votes + votes;
|
||||||
|
havoc votesAbstain assuming votesAbstain@new() == votesAbstain@old() - old_votes + votes;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
////////////////////////////// INVARIANTS ////////////////////////////////////
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* sum of all votes casted is equal to the sum of voting power of those who voted, per each proposal
|
||||||
|
*/
|
||||||
|
invariant SumOfVotesCastEqualSumOfPowerOfVotedPerProposal(uint256 pId)
|
||||||
|
tracked_weight(pId) == ghost_sum_vote_power_by_id(pId)
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* sum of all votes casted is equal to the sum of voting power of those who voted
|
||||||
|
*/
|
||||||
|
invariant SumOfVotesCastEqualSumOfPowerOfVoted()
|
||||||
|
sum_tracked_weight() == sum_all_votes_power()
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* sum of all votes casted is greater or equal to the sum of voting power of those who voted at a specific proposal
|
||||||
|
*/
|
||||||
|
invariant OneIsNotMoreThanAll(uint256 pId)
|
||||||
|
sum_all_votes_power() >= tracked_weight(pId)
|
||||||
|
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
///////////////////////////////// RULES //////////////////////////////////////
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Only sender's voting status can be changed by execution of any cast vote function
|
||||||
|
*/
|
||||||
|
// Checked for castVote only. all 3 castVote functions call _castVote, so the completness of the verification is counted on
|
||||||
|
// the fact that the 3 functions themselves makes no chages, but rather call an internal function to execute.
|
||||||
|
// That means that we do not check those 3 functions directly, however for castVote & castVoteWithReason it is quite trivial
|
||||||
|
// to understand why this is ok. For castVoteBySig we basically assume that the signature referendum is correct without checking it.
|
||||||
|
// We could check each function seperately and pass the rule, but that would have uglyfied the code with no concrete
|
||||||
|
// benefit, as it is evident that nothing is happening in the first 2 functions (calling a view function), and we do not desire to check the signature verification.
|
||||||
|
rule noVoteForSomeoneElse(uint256 pId, uint8 sup, method f) {
|
||||||
|
env e; calldataarg args;
|
||||||
|
|
||||||
|
address voter = e.msg.sender;
|
||||||
|
address user;
|
||||||
|
|
||||||
|
bool hasVotedBefore_User = hasVoted(e, pId, user);
|
||||||
|
|
||||||
|
castVote@withrevert(e, pId, sup);
|
||||||
|
require(!lastReverted);
|
||||||
|
|
||||||
|
bool hasVotedAfter_User = hasVoted(e, pId, user);
|
||||||
|
|
||||||
|
assert user != voter => hasVotedBefore_User == hasVotedAfter_User;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Total voting tally is monotonically non-decreasing in every operation
|
||||||
|
*/
|
||||||
|
rule votingWeightMonotonicity(method f){
|
||||||
|
uint256 votingWeightBefore = sum_tracked_weight();
|
||||||
|
|
||||||
|
env e;
|
||||||
|
calldataarg args;
|
||||||
|
f(e, args);
|
||||||
|
|
||||||
|
uint256 votingWeightAfter = sum_tracked_weight();
|
||||||
|
|
||||||
|
assert votingWeightBefore <= votingWeightAfter, "Voting weight was decreased somehow";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A change in hasVoted must be correlated with an non-decreasing of the vote supports (nondecrease because user can vote with weight 0)
|
||||||
|
*/
|
||||||
|
rule hasVotedCorrelation(uint256 pId, method f, env e, uint256 bn) {
|
||||||
|
address acc = e.msg.sender;
|
||||||
|
|
||||||
|
uint256 againstBefore = votesAgainst();
|
||||||
|
uint256 forBefore = votesFor();
|
||||||
|
uint256 abstainBefore = votesAbstain();
|
||||||
|
|
||||||
|
bool hasVotedBefore = hasVoted(e, pId, acc);
|
||||||
|
|
||||||
|
helperFunctionsWithRevert(pId, f, e);
|
||||||
|
require(!lastReverted);
|
||||||
|
|
||||||
|
uint256 againstAfter = votesAgainst();
|
||||||
|
uint256 forAfter = votesFor();
|
||||||
|
uint256 abstainAfter = votesAbstain();
|
||||||
|
|
||||||
|
bool hasVotedAfter = hasVoted(e, pId, acc);
|
||||||
|
|
||||||
|
assert (!hasVotedBefore && hasVotedAfter) => againstBefore <= againstAfter || forBefore <= forAfter || abstainBefore <= abstainAfter, "no correlation";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Only privileged users can execute privileged operations, e.g. change _quorumNumerator or _timelock
|
||||||
|
*/
|
||||||
|
rule privilegedOnlyNumerator(method f, uint256 newQuorumNumerator){
|
||||||
|
env e;
|
||||||
|
calldataarg arg;
|
||||||
|
uint256 quorumNumBefore = quorumNumerator(e);
|
||||||
|
|
||||||
|
f(e, arg);
|
||||||
|
|
||||||
|
uint256 quorumNumAfter = quorumNumerator(e);
|
||||||
|
address executorCheck = getExecutor(e);
|
||||||
|
|
||||||
|
assert quorumNumBefore != quorumNumAfter => e.msg.sender == executorCheck, "non priveleged user changed quorum numerator";
|
||||||
|
}
|
||||||
|
|
||||||
|
rule privilegedOnlyTimelock(method f, uint256 newQuorumNumerator){
|
||||||
|
env e;
|
||||||
|
calldataarg arg;
|
||||||
|
uint256 timelockBefore = timelock(e);
|
||||||
|
|
||||||
|
f(e, arg);
|
||||||
|
|
||||||
|
uint256 timelockAfter = timelock(e);
|
||||||
|
|
||||||
|
assert timelockBefore != timelockAfter => e.msg.sender == timelockBefore, "non priveleged user changed timelock";
|
||||||
|
}
|
||||||
139
certora/specs/RulesInProgress.spec
Normal file
139
certora/specs/RulesInProgress.spec
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
////////////// THIS SPEC IS A RESERVE FOR NOT IN PROGRESS //////////////
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
import "GovernorBase.spec"
|
||||||
|
|
||||||
|
using ERC20VotesHarness as erc20votes
|
||||||
|
|
||||||
|
methods {
|
||||||
|
ghost_sum_vote_power_by_id(uint256) returns uint256 envfree
|
||||||
|
|
||||||
|
quorum(uint256) returns uint256
|
||||||
|
proposalVotes(uint256) returns (uint256, uint256, uint256) envfree
|
||||||
|
|
||||||
|
quorumNumerator() returns uint256
|
||||||
|
_executor() returns address
|
||||||
|
|
||||||
|
erc20votes._getPastVotes(address, uint256) returns uint256
|
||||||
|
|
||||||
|
getExecutor() returns address
|
||||||
|
|
||||||
|
timelock() returns address
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
///////////////////////////////// GHOSTS /////////////////////////////////////
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
|
//////////// ghosts to keep track of votes counting ////////////
|
||||||
|
|
||||||
|
/*
|
||||||
|
* the sum of voting power of those who voted
|
||||||
|
*/
|
||||||
|
ghost sum_all_votes_power() returns uint256 {
|
||||||
|
init_state axiom sum_all_votes_power() == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
hook Sstore ghost_sum_vote_power_by_id [KEY uint256 pId] uint256 current_power(uint256 old_power) STORAGE {
|
||||||
|
havoc sum_all_votes_power assuming sum_all_votes_power@new() == sum_all_votes_power@old() - old_power + current_power;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* sum of all votes casted per proposal
|
||||||
|
*/
|
||||||
|
ghost tracked_weight(uint256) returns uint256 {
|
||||||
|
init_state axiom forall uint256 p. tracked_weight(p) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* sum of all votes casted
|
||||||
|
*/
|
||||||
|
ghost sum_tracked_weight() returns uint256 {
|
||||||
|
init_state axiom sum_tracked_weight() == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* getter for _proposalVotes.againstVotes
|
||||||
|
*/
|
||||||
|
ghost votesAgainst() returns uint256 {
|
||||||
|
init_state axiom votesAgainst() == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* getter for _proposalVotes.forVotes
|
||||||
|
*/
|
||||||
|
ghost votesFor() returns uint256 {
|
||||||
|
init_state axiom votesFor() == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* getter for _proposalVotes.abstainVotes
|
||||||
|
*/
|
||||||
|
ghost votesAbstain() returns uint256 {
|
||||||
|
init_state axiom votesAbstain() == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
hook Sstore _proposalVotes [KEY uint256 pId].againstVotes uint256 votes(uint256 old_votes) STORAGE {
|
||||||
|
havoc tracked_weight assuming forall uint256 p.(p == pId => tracked_weight@new(p) == tracked_weight@old(p) - old_votes + votes) &&
|
||||||
|
(p != pId => tracked_weight@new(p) == tracked_weight@old(p));
|
||||||
|
havoc sum_tracked_weight assuming sum_tracked_weight@new() == sum_tracked_weight@old() - old_votes + votes;
|
||||||
|
havoc votesAgainst assuming votesAgainst@new() == votesAgainst@old() - old_votes + votes;
|
||||||
|
}
|
||||||
|
|
||||||
|
hook Sstore _proposalVotes [KEY uint256 pId].forVotes uint256 votes(uint256 old_votes) STORAGE {
|
||||||
|
havoc tracked_weight assuming forall uint256 p.(p == pId => tracked_weight@new(p) == tracked_weight@old(p) - old_votes + votes) &&
|
||||||
|
(p != pId => tracked_weight@new(p) == tracked_weight@old(p));
|
||||||
|
havoc sum_tracked_weight assuming sum_tracked_weight@new() == sum_tracked_weight@old() - old_votes + votes;
|
||||||
|
havoc votesFor assuming votesFor@new() == votesFor@old() - old_votes + votes;
|
||||||
|
}
|
||||||
|
|
||||||
|
hook Sstore _proposalVotes [KEY uint256 pId].abstainVotes uint256 votes(uint256 old_votes) STORAGE {
|
||||||
|
havoc tracked_weight assuming forall uint256 p.(p == pId => tracked_weight@new(p) == tracked_weight@old(p) - old_votes + votes) &&
|
||||||
|
(p != pId => tracked_weight@new(p) == tracked_weight@old(p));
|
||||||
|
havoc sum_tracked_weight assuming sum_tracked_weight@new() == sum_tracked_weight@old() - old_votes + votes;
|
||||||
|
havoc votesAbstain assuming votesAbstain@new() == votesAbstain@old() - old_votes + votes;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
////////////////////////////// INVARIANTS ////////////////////////////////////
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
///////////////////////////////// RULES //////////////////////////////////////
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
|
//NOT FINISHED
|
||||||
|
/*
|
||||||
|
* the sum of voting power of those who voted is less or equal to the maximum possible votes, per each proposal
|
||||||
|
*/
|
||||||
|
rule possibleTotalVotes(uint256 pId, uint8 sup, env e, method f) {
|
||||||
|
|
||||||
|
// add requireinvariant for all i, j. i = i - 1 && i < j => checkpointlookup[i] < checkpointlookup[j];
|
||||||
|
require tracked_weight(pId) <= erc20votes.getPastTotalSupply(e, proposalSnapshot(pId));
|
||||||
|
|
||||||
|
uint256 againstB;
|
||||||
|
uint256 forB;
|
||||||
|
uint256 absatinB;
|
||||||
|
againstB, forB, absatinB = proposalVotes(pId);
|
||||||
|
|
||||||
|
calldataarg args;
|
||||||
|
//f(e, args);
|
||||||
|
|
||||||
|
castVote(e, pId, sup);
|
||||||
|
|
||||||
|
uint256 against;
|
||||||
|
uint256 for;
|
||||||
|
uint256 absatin;
|
||||||
|
against, for, absatin = proposalVotes(pId);
|
||||||
|
|
||||||
|
uint256 ps = proposalSnapshot(pId);
|
||||||
|
|
||||||
|
assert tracked_weight(pId) <= erc20votes.getPastTotalSupply(e, proposalSnapshot(pId)), "bla bla bla";
|
||||||
|
}
|
||||||
14
certora/specs/sanity.spec
Normal file
14
certora/specs/sanity.spec
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
/*
|
||||||
|
This rule looks for a non-reverting execution path to each method, including those overridden in the harness.
|
||||||
|
A method has such an execution path if it violates this rule.
|
||||||
|
How it works:
|
||||||
|
- If there is a non-reverting execution path, we reach the false assertion, and the sanity fails.
|
||||||
|
- If all execution paths are reverting, we never call the assertion, and the method will pass this rule vacuously.
|
||||||
|
*/
|
||||||
|
|
||||||
|
rule sanity(method f) {
|
||||||
|
env e;
|
||||||
|
calldataarg arg;
|
||||||
|
f(e, arg);
|
||||||
|
assert false;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user