From 1f5982b5e3166a6b38c9ebd987c51b8d6fddf81f Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 10 Mar 2023 14:23:48 +0100 Subject: [PATCH 01/61] starting to work on governor specifications --- certora/diff/governance_Governor.sol.patch | 19 ++ certora/harnesses/AccessControlHarness.sol | 1 - certora/harnesses/ERC20FlashMintHarness.sol | 1 - certora/harnesses/ERC20PermitHarness.sol | 1 - certora/harnesses/ERC20VotesHarness.sol | 29 ++ certora/harnesses/ERC20WrapperHarness.sol | 1 - .../harnesses/ERC3156FlashBorrowerHarness.sol | 3 +- certora/harnesses/GovernorHarness.sol | 160 +++++++++++ certora/harnesses/Ownable2StepHarness.sol | 1 - certora/harnesses/OwnableHarness.sol | 1 - certora/run.js | 19 +- certora/{specs.json => specs.js} | 22 +- certora/specs/Governor.helpers.spec | 124 +++++++++ certora/specs/GovernorBase.spec | 250 ++++++++++++++++++ certora/specs/GovernorFunctions.spec | 97 +++++++ certora/specs/GovernorInvariants.spec | 72 +++++ certora/specs/GovernorStates.spec | 212 +++++++++++++++ certora/specs/methods/IGovernor.spec | 45 ++++ 18 files changed, 1038 insertions(+), 20 deletions(-) create mode 100644 certora/diff/governance_Governor.sol.patch create mode 100644 certora/harnesses/ERC20VotesHarness.sol create mode 100644 certora/harnesses/GovernorHarness.sol rename certora/{specs.json => specs.js} (65%) create mode 100644 certora/specs/Governor.helpers.spec create mode 100644 certora/specs/GovernorBase.spec create mode 100644 certora/specs/GovernorFunctions.spec create mode 100644 certora/specs/GovernorInvariants.spec create mode 100644 certora/specs/GovernorStates.spec create mode 100644 certora/specs/methods/IGovernor.spec diff --git a/certora/diff/governance_Governor.sol.patch b/certora/diff/governance_Governor.sol.patch new file mode 100644 index 00000000000..68710da6575 --- /dev/null +++ b/certora/diff/governance_Governor.sol.patch @@ -0,0 +1,19 @@ +--- governance/Governor.sol 2023-03-07 10:48:47.730155491 +0100 ++++ governance/Governor.sol 2023-03-10 10:13:31.926616811 +0100 +@@ -216,6 +216,16 @@ + return _proposals[proposalId].proposer; + } + ++ // FV ++ function _isExecuted(uint256 proposalId) internal view returns (bool) { ++ return _proposals[proposalId].executed; ++ } ++ ++ // FV ++ function _isCanceled(uint256 proposalId) internal view returns (bool) { ++ return _proposals[proposalId].canceled; ++ } ++ + /** + * @dev Amount of votes already cast passes the threshold limit. + */ diff --git a/certora/harnesses/AccessControlHarness.sol b/certora/harnesses/AccessControlHarness.sol index 0cb1e55d41c..b869bd7dcb6 100644 --- a/certora/harnesses/AccessControlHarness.sol +++ b/certora/harnesses/AccessControlHarness.sol @@ -1,5 +1,4 @@ // SPDX-License-Identifier: MIT - pragma solidity ^0.8.0; import "../patched/access/AccessControl.sol"; diff --git a/certora/harnesses/ERC20FlashMintHarness.sol b/certora/harnesses/ERC20FlashMintHarness.sol index 119eb4768fb..39af9371f93 100644 --- a/certora/harnesses/ERC20FlashMintHarness.sol +++ b/certora/harnesses/ERC20FlashMintHarness.sol @@ -1,5 +1,4 @@ // SPDX-License-Identifier: MIT - pragma solidity ^0.8.0; import "../patched/token/ERC20/ERC20.sol"; diff --git a/certora/harnesses/ERC20PermitHarness.sol b/certora/harnesses/ERC20PermitHarness.sol index dd0aacae2fa..effae5f9b25 100644 --- a/certora/harnesses/ERC20PermitHarness.sol +++ b/certora/harnesses/ERC20PermitHarness.sol @@ -1,5 +1,4 @@ // SPDX-License-Identifier: MIT - pragma solidity ^0.8.0; import "../patched/token/ERC20/extensions/ERC20Permit.sol"; diff --git a/certora/harnesses/ERC20VotesHarness.sol b/certora/harnesses/ERC20VotesHarness.sol new file mode 100644 index 00000000000..edc8ce67b2f --- /dev/null +++ b/certora/harnesses/ERC20VotesHarness.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "../patched/token/ERC20/extensions/ERC20Votes.sol"; + +contract ERC20VotesHarness is ERC20Votes { + constructor(string memory name, string memory symbol) ERC20(name, symbol) ERC20Permit(name) {} + + function mint(address account, uint256 amount) external { + _mint(account, amount); + } + + function burn(address account, uint256 amount) external { + _burn(account, amount); + } + + // inspection + function ckptFromBlock(address account, uint32 pos) public view returns (uint32) { + return checkpoints(account, pos).fromBlock; + } + + function ckptVotes(address account, uint32 pos) public view returns (uint224) { + return checkpoints(account, pos).votes; + } + + function maxSupply() public view returns (uint224) { + return _maxSupply(); + } +} diff --git a/certora/harnesses/ERC20WrapperHarness.sol b/certora/harnesses/ERC20WrapperHarness.sol index 50a96cc170a..2ff93e749a1 100644 --- a/certora/harnesses/ERC20WrapperHarness.sol +++ b/certora/harnesses/ERC20WrapperHarness.sol @@ -1,5 +1,4 @@ // SPDX-License-Identifier: MIT - pragma solidity ^0.8.0; import "../patched/token/ERC20/extensions/ERC20Wrapper.sol"; diff --git a/certora/harnesses/ERC3156FlashBorrowerHarness.sol b/certora/harnesses/ERC3156FlashBorrowerHarness.sol index 0ad29a16e24..c9cb829aaa3 100644 --- a/certora/harnesses/ERC3156FlashBorrowerHarness.sol +++ b/certora/harnesses/ERC3156FlashBorrowerHarness.sol @@ -1,9 +1,8 @@ // SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; import "../patched/interfaces/IERC3156FlashBorrower.sol"; -pragma solidity ^0.8.0; - contract ERC3156FlashBorrowerHarness is IERC3156FlashBorrower { bytes32 somethingToReturn; diff --git a/certora/harnesses/GovernorHarness.sol b/certora/harnesses/GovernorHarness.sol new file mode 100644 index 00000000000..ca841ea8803 --- /dev/null +++ b/certora/harnesses/GovernorHarness.sol @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +import "../patched/governance/Governor.sol"; +import "../patched/governance/extensions/GovernorCountingSimple.sol"; +import "../patched/governance/extensions/GovernorTimelockControl.sol"; +import "../patched/governance/extensions/GovernorVotes.sol"; +import "../patched/governance/extensions/GovernorVotesQuorumFraction.sol"; +import "../patched/token/ERC20/extensions/ERC20Votes.sol"; + +contract GovernorHarness is + Governor, + GovernorCountingSimple, + GovernorTimelockControl, + GovernorVotes, + GovernorVotesQuorumFraction +{ + constructor(IVotes _token, TimelockController _timelock, uint256 _quorumNumeratorValue) + Governor("Harness") + GovernorTimelockControl(_timelock) + GovernorVotes(_token) + GovernorVotesQuorumFraction(_quorumNumeratorValue) + {} + + // Harness from Votes + function token_getPastTotalSupply(uint256 blockNumber) public view returns(uint256) { + return token.getPastTotalSupply(blockNumber); + } + + function token_getPastVotes(address account, uint256 blockNumber) public view returns(uint256) { + return token.getPastVotes(account, blockNumber); + } + + function token_clock() public view returns (uint48) { + return token.clock(); + } + + function token_CLOCK_MODE() public view returns (string memory) { + return token.CLOCK_MODE(); + } + + // Harness from Governor + function getExecutor() public view returns (address) { + return _executor(); + } + + function proposalProposer(uint256 proposalId) public view returns (address) { + return _proposalProposer(proposalId); + } + + function quorumReached(uint256 proposalId) public view returns (bool) { + return _quorumReached(proposalId); + } + + function voteSucceeded(uint256 proposalId) public view returns (bool) { + return _voteSucceeded(proposalId); + } + + function isExecuted(uint256 proposalId) public view returns (bool) { + return _isExecuted(proposalId); + } + + function isCanceled(uint256 proposalId) public view returns (bool) { + return _isCanceled(proposalId); + } + + // Harness from GovernorCountingSimple + function getAgainstVotes(uint256 proposalId) public view returns (uint256) { + (uint256 againstVotes,,) = proposalVotes(proposalId); + return againstVotes; + } + + function getForVotes(uint256 proposalId) public view returns (uint256) { + (,uint256 forVotes,) = proposalVotes(proposalId); + return forVotes; + } + + function getAbstainVotes(uint256 proposalId) public view returns (uint256) { + (,,uint256 abstainVotes) = proposalVotes(proposalId); + return abstainVotes; + } + + /// The following functions are overrides required by Solidity added by Certora. + // mapping(uint256 => uint256) public ghost_sum_vote_power_by_id; + // + // function _castVote( + // uint256 proposalId, + // address account, + // uint8 support, + // string memory reason, + // bytes memory params + // ) internal virtual override returns (uint256) { + // uint256 deltaWeight = super._castVote(proposalId, account, support, reason, params); + // ghost_sum_vote_power_by_id[proposalId] += deltaWeight; + // return deltaWeight; + // } + + // The following functions are overrides required by Solidity added by OZ Wizard. + function votingDelay() public pure override returns (uint256) { + return 1; // 1 block + } + + function votingPeriod() public pure override returns (uint256) { + return 45818; // 1 week + } + + function quorum(uint256 blockNumber) + public + view + override(IGovernor, GovernorVotesQuorumFraction) + returns (uint256) + { + return super.quorum(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, 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); + } +} diff --git a/certora/harnesses/Ownable2StepHarness.sol b/certora/harnesses/Ownable2StepHarness.sol index 4d30e504189..dd53cc6fe4c 100644 --- a/certora/harnesses/Ownable2StepHarness.sol +++ b/certora/harnesses/Ownable2StepHarness.sol @@ -1,5 +1,4 @@ // SPDX-License-Identifier: MIT - pragma solidity ^0.8.0; import "../patched/access/Ownable2Step.sol"; diff --git a/certora/harnesses/OwnableHarness.sol b/certora/harnesses/OwnableHarness.sol index 93cbb4770c2..c9e06d1dc78 100644 --- a/certora/harnesses/OwnableHarness.sol +++ b/certora/harnesses/OwnableHarness.sol @@ -1,5 +1,4 @@ // SPDX-License-Identifier: MIT - pragma solidity ^0.8.0; import "../patched/access/Ownable.sol"; diff --git a/certora/run.js b/certora/run.js index f3234c1a344..7623f95c697 100644 --- a/certora/run.js +++ b/certora/run.js @@ -8,26 +8,27 @@ const MAX_PARALLEL = 4; -let specs = require(__dirname + '/specs.json'); - const proc = require('child_process'); const { PassThrough } = require('stream'); const events = require('events'); const limit = require('p-limit')(MAX_PARALLEL); +const strToRegex = str => new RegExp(`^${str.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/[*]/g, '.$&')}$`); + let [, , request = '', ...extraOptions] = process.argv; if (request.startsWith('-')) { extraOptions.unshift(request); request = ''; } +const [reqSpec, reqContract] = request.split(':').reverse(); -if (request) { - const [reqSpec, reqContract] = request.split(':').reverse(); - specs = Object.values(specs).filter(s => reqSpec === s.spec && (!reqContract || reqContract === s.contract)); - if (specs.length === 0) { - console.error(`Error: Requested spec '${request}' not found in specs.json`); - process.exit(1); - } +const specs = require(__dirname + '/specs.js') + .filter(entry => !reqSpec || strToRegex(reqSpec).test(entry.spec)) + .filter(entry => !reqContract || strToRegex(reqContract).test(entry.contract)); + +if (specs.length === 0) { + console.error(`Error: Requested spec '${request}' not found in specs.json`); + process.exit(1); } for (const { spec, contract, files, options = [] } of Object.values(specs)) { diff --git a/certora/specs.json b/certora/specs.js similarity index 65% rename from certora/specs.json rename to certora/specs.js index 228e85fe4f3..1f1879246ca 100644 --- a/certora/specs.json +++ b/certora/specs.js @@ -1,4 +1,7 @@ -[ +/// This helper will be handy when we want to do cross product. Ex: all governor specs on all variations of the clock mode. +const product = (...arrays) => arrays.reduce((a, b) => a.flatMap(ai => b.map(bi => [ai, bi].flat()))); + +module.exports = [ { "spec": "AccessControl", "contract": "AccessControlHarness", @@ -45,5 +48,18 @@ "spec": "Initializable", "contract": "InitializableHarness", "files": ["certora/harnesses/InitializableHarness.sol"] - } -] + }, + ...[ "GovernorBase", "GovernorInvariants", "GovernorStates", "GovernorFunctions" ].map(spec => ({ + spec, + "contract": "GovernorHarness", + "files": [ + "certora/harnesses/GovernorHarness.sol", + "certora/harnesses/ERC20VotesHarness.sol" + ], + "options": [ + "--link GovernorHarness:token=ERC20VotesHarness", + "--optimistic_loop", + "--optimistic_hashing" + ] + })) +]; \ No newline at end of file diff --git a/certora/specs/Governor.helpers.spec b/certora/specs/Governor.helpers.spec new file mode 100644 index 00000000000..5f849876d70 --- /dev/null +++ b/certora/specs/Governor.helpers.spec @@ -0,0 +1,124 @@ +import "methods/IGovernor.spec" + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ States │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +definition UNSET() returns uint8 = 255; +definition PENDING() returns uint8 = 0; +definition ACTIVE() returns uint8 = 1; +definition CANCELED() returns uint8 = 2; +definition DEFEATED() returns uint8 = 3; +definition SUCCEEDED() returns uint8 = 4; +definition QUEUED() returns uint8 = 5; +definition EXPIRED() returns uint8 = 6; +definition EXECUTED() returns uint8 = 7; + +function safeState(env e, uint256 pId) returns uint8 { + uint8 result = state@withrevert(e, pId); + return lastReverted ? UNSET() : result; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Filters │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +definition skip(method f) returns bool = + f.isView || + f.isFallback || + f.selector == relay(address,uint256,bytes).selector || + f.selector == 0xb9a61961 || // __acceptAdmin() + f.selector == onERC721Received(address,address,uint256,bytes).selector || + f.selector == onERC1155Received(address,address,uint256,uint256,bytes).selector || + f.selector == onERC1155BatchReceived(address,address,uint256[],uint256[],bytes).selector; + +definition voting(method f) returns bool = + f.selector == castVote(uint256,uint8).selector || + f.selector == castVoteWithReason(uint256,uint8,string).selector || + f.selector == castVoteWithReasonAndParams(uint256,uint8,string,bytes).selector; + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Helper functions │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +function helperVoteWithRevert(env e, method f, uint256 pId, address voter, uint8 support) returns uint256 { + string reason; bytes params; uint8 v; bytes32 s; bytes32 r; + + if (f.selector == castVote(uint256,uint8).selector) + { + require e.msg.sender == voter; + return castVote@withrevert(e, pId, support); + } + else if (f.selector == castVoteWithReason(uint256,uint8,string).selector) + { + require e.msg.sender == voter; + return castVoteWithReason@withrevert(e, pId, support, reason); + } + else if (f.selector == castVoteWithReasonAndParams(uint256,uint8,string,bytes).selector) + { + require e.msg.sender == voter; + return castVoteWithReasonAndParams@withrevert(e, pId, support, reason, params); + } + else + { + calldataarg args; + f@withrevert(e, args); + return 0; + } +} + +function helperFunctionsWithRevert(env e, method f, uint256 pId) { + if (f.selector == propose(address[], uint256[], bytes[], string).selector) + { + address[] targets; uint256[] values; bytes[] calldatas; string description; + require pId == propose@withrevert(e, targets, values, calldatas, description); + } + else if (f.selector == queue(address[], uint256[], bytes[], bytes32).selector) + { + address[] targets; uint256[] values; bytes[] calldatas; bytes32 description; + require pId == queue@withrevert(e, targets, values, calldatas, description); + } + else if (f.selector == execute(address[], uint256[], bytes[], bytes32).selector) + { + address[] targets; uint256[] values; bytes[] calldatas; bytes32 description; + require pId == execute@withrevert(e, targets, values, calldatas, description); + } + else if (f.selector == cancel(address[], uint256[], bytes[], bytes32).selector) + { + address[] targets; uint256[] values; bytes[] calldatas; bytes32 description; + require pId == cancel@withrevert(e, targets, values, calldatas, description); + } + else if (f.selector == castVote(uint256, uint8).selector) + { + uint8 support; + castVote@withrevert(e, pId, support); + } + else if (f.selector == castVoteWithReason(uint256, uint8, string).selector) + { + uint8 support; string reason; + castVoteWithReason@withrevert(e, pId, support, reason); + } + else if (f.selector == castVoteWithReasonAndParams(uint256,uint8,string,bytes).selector) + { + uint8 support; string reason; bytes params; + castVoteWithReasonAndParams@withrevert(e, pId, support, reason, params); + } + else if (f.selector == castVoteBySig(uint256, uint8,uint8, bytes32, bytes32).selector) + { + uint8 support; uint8 v; bytes32 r; bytes32 s; + castVoteBySig@withrevert(e, pId, support, v, r, s); + } + else if (f.selector == castVoteWithReasonAndParamsBySig(uint256,uint8,string,bytes,uint8,bytes32,bytes32).selector) + { + uint8 support; string reason; bytes params; uint8 v; bytes32 r; bytes32 s; + castVoteWithReasonAndParamsBySig@withrevert(e, pId, support, reason, params, v, r, s); + } + else + { + calldataarg args; + f@withrevert(e, args); + } +} \ No newline at end of file diff --git a/certora/specs/GovernorBase.spec b/certora/specs/GovernorBase.spec new file mode 100644 index 00000000000..977993980ad --- /dev/null +++ b/certora/specs/GovernorBase.spec @@ -0,0 +1,250 @@ +import "methods/IGovernor.spec" +import "Governor.helpers.spec" + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Definitions │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ + +definition proposalCreated(uint256 pId) returns bool = + proposalSnapshot(pId) > 0 && proposalDeadline(pId) > 0 && proposalProposer(pId) != 0; + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: Votes start and end 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) │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant proposalStateConsistency(uint256 pId) + (proposalProposer(pId) != 0 <=> proposalSnapshot(pId) != 0) && (proposalProposer(pId) != 0 <=> proposalDeadline(pId) != 0) + { + preserved with (env e) { + require clock(e) > 0; + } + } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: cancel => created │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant canceledImplyCreated(uint pId) + isCanceled(pId) => proposalCreated(pId) + { + preserved with (env e) { + requireInvariant proposalStateConsistency(pId); + require clock(e) > 0; + } + } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: executed => created │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant executedImplyCreated(uint pId) + isExecuted(pId) => proposalCreated(pId) + { + preserved with (env e) { + requireInvariant proposalStateConsistency(pId); + require clock(e) > 0; + } + } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: Votes start before it ends │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant voteStartBeforeVoteEnd(uint256 pId) + proposalSnapshot(pId) <= proposalDeadline(pId) + { + preserved { + requireInvariant proposalStateConsistency(pId); + } + } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: A proposal cannot be both executed and canceled simultaneously │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant noBothExecutedAndCanceled(uint256 pId) + !isExecuted(pId) || !isCanceled(pId) + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: No double proposition │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule noDoublePropose(uint256 pId, env e) { + require proposalCreated(pId); + + address[] targets; uint256[] values; bytes[] calldatas; string reason; + uint256 result = propose(e, targets, values, calldatas, reason); + + assert result != pId, "double proposal"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: Once a proposal is created, voteStart, voteEnd and proposer are immutable │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule immutableFieldsAfterProposalCreation(uint256 pId, env e, method f, calldataarg arg) { + require proposalCreated(pId); + + uint256 voteStart = proposalSnapshot(pId); + uint256 voteEnd = proposalDeadline(pId); + address proposer = proposalProposer(pId); + + f(e, arg); + + assert voteStart == proposalSnapshot(pId), "Start date was changed"; + assert voteEnd == proposalDeadline(pId), "End date was changed"; + assert proposer == proposalProposer(pId), "Proposer was changed"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: A user cannot vote twice │ +│ │ +│ Checked for castVote only. all 3 castVote functions call _castVote, so the completeness of the verification is │ +│ counted on the fact that the 3 functions themselves makes no changes, 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 separately 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 noDoubleVoting(uint256 pId, env e, uint8 sup) { + bool votedCheck = hasVoted(pId, e.msg.sender); + + castVote@withrevert(e, pId, sup); + + assert votedCheck => lastReverted, "double voting occurred"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: A proposal could be executed only if quorum was reached and vote succeeded │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule executionOnlyIfQuoromReachedAndVoteSucceeded(uint256 pId, env e, method f, calldataarg args) { + require !isExecuted(pId); + + bool quorumReachedBefore = quorumReached(pId); + bool voteSucceededBefore = voteSucceeded(pId); + + f(e, args); + + assert isExecuted(pId) => (quorumReachedBefore && voteSucceededBefore), "quorum was changed"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: Voting cannot start at a block number prior to proposal’s creation block number │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule noStartBeforeCreation(uint256 pId, env e, method f, calldataarg args){ + require !proposalCreated(pId); + f(e, args); + assert proposalCreated(pId) => proposalSnapshot(pId) >= clock(e), "starts before proposal"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: A proposal cannot be executed before it ends │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule noExecuteBeforeDeadline(uint256 pId, env e, method f, calldataarg args) { + require !isExecuted(pId); + f(e, args); + assert isExecuted(pId) => proposalDeadline(pId) <= clock(e), "executed before deadline"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: 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(uint256 pId, env e, method f, calldataarg args) filtered { + f -> !f.isView && !f.isFallback + && f.selector != updateTimelock(address).selector + && f.selector != updateQuorumNumerator(uint256).selector + && f.selector != relay(address,uint256,bytes).selector + && f.selector != 0xb9a61961 // __acceptAdmin() + && f.selector != onERC721Received(address,address,uint256,bytes).selector + && f.selector != onERC1155Received(address,address,uint256,uint256,bytes).selector + && f.selector != onERC1155BatchReceived(address,address,uint256[],uint256[],bytes).selector +} { + require isExecuted(pId); + requireInvariant noBothExecutedAndCanceled(pId); + requireInvariant executedImplyCreated(pId); + + helperFunctionsWithRevert(pId, f, e); + + assert lastReverted, "Function was not reverted"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: All proposal specific (non-view) functions should revert if proposal is canceled │ +│ │ +│ 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 allFunctionsRevertIfCanceled(uint256 pId, env e, method f, calldataarg args) filtered { + f -> !f.isView && !f.isFallback + && f.selector != updateTimelock(address).selector + && f.selector != updateQuorumNumerator(uint256).selector + && f.selector != relay(address,uint256,bytes).selector + && f.selector != 0xb9a61961 // __acceptAdmin() + && f.selector != onERC721Received(address,address,uint256,bytes).selector + && f.selector != onERC1155Received(address,address,uint256,uint256,bytes).selector + && f.selector != onERC1155BatchReceived(address,address,uint256[],uint256[],bytes).selector +} { + require isCanceled(pId); + requireInvariant noBothExecutedAndCanceled(pId); + requireInvariant canceledImplyCreated(pId); + + helperFunctionsWithRevert(pId, f, e); + + assert lastReverted, "Function was not reverted"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: Proposal can be switched state only by specific functions │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule stateOnlyAfterFunc(uint256 pId, env e, method f) { + bool createdBefore = proposalCreated(pId); + bool executedBefore = isExecuted(pId); + bool canceledBefore = isCanceled(pId); + + helperFunctionsWithRevert(pId, f, e); + + assert (proposalCreated(pId) != createdBefore) + => (createdBefore == false && f.selector == propose(address[], uint256[], bytes[], string).selector), + "proposalCreated only changes in the propose method"; + + assert (isExecuted(pId) != executedBefore) + => (executedBefore == false && f.selector == execute(address[], uint256[], bytes[], bytes32).selector), + "isExecuted only changes in the execute method"; + + assert (isCanceled(pId) != canceledBefore) + => (canceledBefore == false && f.selector == cancel(address[], uint256[], bytes[], bytes32).selector), + "isCanceled only changes in the cancel method"; +} diff --git a/certora/specs/GovernorFunctions.spec b/certora/specs/GovernorFunctions.spec new file mode 100644 index 00000000000..40ad27213d0 --- /dev/null +++ b/certora/specs/GovernorFunctions.spec @@ -0,0 +1,97 @@ +import "helpers.spec" +import "methods/IGovernor.spec" +import "Governor.helpers.spec" + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: propose effect and liveness. Includes "no double proposition" │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule propose(uint256 pId, env e) { + require nonpayable(e); + + uint256 otherId; + + uint8 stateBefore = state(e, pId); + uint8 otherStateBefore = state(e, otherId); + uint256 otherVoteStart = proposalSnapshot(otherId); + uint256 otherVoteEnd = proposalDeadline(otherId); + address otherProposer = proposalProposer(otherId); + + address[] targets; uint256[] values; bytes[] calldatas; string reason; + require pId == propose@withrevert(e, targets, values, calldatas, reason); + bool success = !lastReverted; + + // liveness & double proposal + assert success <=> stateBefore == UNSET(); + + // effect + assert success => ( + state(e, pId) == PENDING() && + proposalProposer(pId) == e.msg.sender && + proposalSnapshot(pId) == clock(e) + votingDelay() && + proposalDeadline(pId) == clock(e) + votingDelay() + votingPeriod() + ); + + // no side-effect + assert state(e, otherId) != otherStateBefore => otherId == pId; + assert proposalSnapshot(otherId) == otherVoteStart; + assert proposalDeadline(otherId) == otherVoteEnd; + assert proposalProposer(otherId) == otherProposer; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: votes effect and liveness. Includes "A user cannot vote twice" │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule castVote(uint256 pId, env e, method f) + filtered { f -> voting(f) } +{ + require nonpayable(e); + + uint8 support; + address voter; + address otherVoter; + uint256 otherId; + + uint8 stateBefore = state(e, pId); + bool hasVotedBefore = hasVoted(pId, voter); + bool otherVotedBefore = hasVoted(otherId, otherVoter); + uint256 againstVotesBefore = getAgainstVotes(pId); + uint256 forVotesBefore = getForVotes(pId); + uint256 abstainVotesBefore = getAbstainVotes(pId); + uint256 otherAgainstVotesBefore = getAgainstVotes(otherId); + uint256 otherForVotesBefore = getForVotes(otherId); + uint256 otherAbstainVotesBefore = getAbstainVotes(otherId); + + // voting weight overflow check + uint256 voterWeight = token_getPastVotes(voter, proposalSnapshot(pId)); + require againstVotesBefore + voterWeight <= max_uint256; + require forVotesBefore + voterWeight <= max_uint256; + require abstainVotesBefore + voterWeight <= max_uint256; + + uint256 weight = helperVoteWithRevert(e, f, pId, voter, support); + bool success = !lastReverted; + + assert success <=> ( + stateBefore == ACTIVE() && + !hasVotedBefore && + (support == 0 || support == 1 || support == 2) + ); + + assert success => ( + state(e, pId) == ACTIVE() && + voterWeight == weight && + getAgainstVotes(pId) == againstVotesBefore + (support == 0 ? weight : 0) && + getForVotes(pId) == forVotesBefore + (support == 1 ? weight : 0) && + getAbstainVotes(pId) == abstainVotesBefore + (support == 2 ? weight : 0) && + hasVoted(pId, voter) + ); + + // no side-effect + assert hasVoted(otherId, otherVoter) != otherVotedBefore => (otherId == pId && otherVoter == voter); + assert getAgainstVotes(otherId) != otherAgainstVotesBefore => (otherId == pId); + assert getForVotes(otherId) != otherForVotesBefore => (otherId == pId); + assert getAbstainVotes(otherId) != otherAbstainVotesBefore => (otherId == pId); +} diff --git a/certora/specs/GovernorInvariants.spec b/certora/specs/GovernorInvariants.spec new file mode 100644 index 00000000000..a8a200ca375 --- /dev/null +++ b/certora/specs/GovernorInvariants.spec @@ -0,0 +1,72 @@ +import "helpers.spec" +import "methods/IGovernor.spec" +import "Governor.helpers.spec" + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: clock is consistent between the goernor and the token │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule clockMode(env e) { + assert clock(e) == e.block.number || clock(e) == e.block.timestamp; + assert clock(e) == token_clock(e); +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: Proposal is UNSET iff the proposer, the snapshot and the deadline are unset. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant createdConsistency(env e, uint256 pId) + safeState(e, pId) == UNSET() <=> proposalProposer(pId) == 0 && + safeState(e, pId) == UNSET() <=> proposalSnapshot(pId) == 0 && + safeState(e, pId) == UNSET() <=> proposalDeadline(pId) == 0 && + safeState(e, pId) == UNSET() => !isExecuted(pId) && + safeState(e, pId) == UNSET() => !isCanceled(pId) + { + preserved { + require clock(e) > 0; + } + } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: Votes start before it ends │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant voteStartBeforeVoteEnd(uint256 pId) + proposalSnapshot(pId) <= proposalDeadline(pId) + { + preserved with (env e) { + requireInvariant createdConsistency(e, pId); + } + } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: A proposal cannot be both executed and canceled simultaneously │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant noBothExecutedAndCanceled(uint256 pId) + !isExecuted(pId) || !isCanceled(pId) + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: Once a proposal is created, voteStart, voteEnd and proposer are immutable │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule immutableFieldsAfterProposalCreation(uint256 pId, env e, method f, calldataarg arg) + filtered { f -> !skip(f) } +{ + require state(e, pId) != UNSET(); + + uint256 voteStart = proposalSnapshot(pId); + uint256 voteEnd = proposalDeadline(pId); + address proposer = proposalProposer(pId); + + f(e, arg); + + assert voteStart == proposalSnapshot(pId), "Start date was changed"; + assert voteEnd == proposalDeadline(pId), "End date was changed"; + assert proposer == proposalProposer(pId), "Proposer was changed"; +} diff --git a/certora/specs/GovernorStates.spec b/certora/specs/GovernorStates.spec new file mode 100644 index 00000000000..d2ddf994788 --- /dev/null +++ b/certora/specs/GovernorStates.spec @@ -0,0 +1,212 @@ +import "helpers.spec" +import "methods/IGovernor.spec" +import "Governor.helpers.spec" + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: state returns one of the value in the enumeration │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule stateConsistency(env e, uint256 pId) { + uint8 result = state(e, pId); + assert ( + result == PENDING() || + result == ACTIVE() || + result == CANCELED() || + result == DEFEATED() || + result == SUCCEEDED() || + result == QUEUED() || + result == EXECUTED() + ); +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: State transition │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule stateTransitionFn(uint256 pId, env e, method f, calldataarg args) + filtered { f -> !skip(f) } +{ + require clock(e) > 0; // Sanity + + uint8 stateBefore = state(e, pId); + f(e, args); + uint8 stateAfter = state(e, pId); + + assert (stateBefore != stateAfter) => ( + stateBefore == UNSET() => ( + stateAfter == PENDING() && f.selector == propose(address[],uint256[],bytes[],string).selector + ) && + stateBefore == PENDING() => ( + (stateAfter == CANCELED() && f.selector == cancel(address[],uint256[],bytes[],bytes32).selector) + ) && + stateBefore == SUCCEEDED() => ( + (stateAfter == QUEUED() && f.selector == queue(address[],uint256[],bytes[],bytes32).selector) || + (stateAfter == EXECUTED() && f.selector == execute(address[],uint256[],bytes[],bytes32).selector) + ) && + stateBefore == QUEUED() => ( + (stateAfter == EXECUTED() && f.selector == execute(address[],uint256[],bytes[],bytes32).selector) + ) && + stateBefore == ACTIVE() => false && + stateBefore == CANCELED() => false && + stateBefore == DEFEATED() => false && + stateBefore == EXECUTED() => false + ); +} + +rule stateTransitionWait(uint256 pId, env e1, env e2) { + require clock(e1) > 0; // Sanity + require clock(e2) > clock(e1); + + uint8 stateBefore = state(e1, pId); + uint8 stateAfter = state(e2, pId); + + assert (stateBefore != stateAfter) => ( + stateBefore == PENDING() => ( + stateAfter == ACTIVE() + ) && + stateBefore == ACTIVE() => ( + stateAfter == SUCCEEDED() || + stateAfter == DEFEATED() + ) && + stateBefore == UNSET() => false && + stateBefore == SUCCEEDED() => false && + stateBefore == QUEUED() => false && + stateBefore == CANCELED() => false && + stateBefore == DEFEATED() => false && + stateBefore == EXECUTED() => false + ); +} + + + + + + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: Once a proposal is created, voteStart, voteEnd and proposer are immutable │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule immutableFieldsAfterProposalCreation(uint256 pId, env e, method f, calldataarg arg) + filtered { f -> !skip(f) } +{ + require state(e, pId) != UNSET(); + + uint256 voteStart = proposalSnapshot(pId); + uint256 voteEnd = proposalDeadline(pId); + address proposer = proposalProposer(pId); + + f(e, arg); + + assert voteStart == proposalSnapshot(pId), "Start date was changed"; + assert voteEnd == proposalDeadline(pId), "End date was changed"; + assert proposer == proposalProposer(pId), "Proposer was changed"; +} + + + + + + + + + + + + + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: propose effect and liveness. Includes "no double proposition" │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule propose(uint256 pId, env e) { + require nonpayable(e); + + uint256 otherId; + + uint8 stateBefore = state(e, pId); + uint8 otherStateBefore = state(e, otherId); + uint256 otherVoteStart = proposalSnapshot(otherId); + uint256 otherVoteEnd = proposalDeadline(otherId); + address otherProposer = proposalProposer(otherId); + + address[] targets; uint256[] values; bytes[] calldatas; string reason; + require pId == propose@withrevert(e, targets, values, calldatas, reason); + bool success = !lastReverted; + + // liveness & double proposal + assert success <=> stateBefore == UNSET(); + + // effect + assert success => ( + state(e, pId) == PENDING() && + proposalProposer(pId) == e.msg.sender && + proposalSnapshot(pId) == clock(e) + votingDelay() && + proposalDeadline(pId) == clock(e) + votingDelay() + votingPeriod() + ); + + // no side-effect + assert state(e, otherId) != otherStateBefore => otherId == pId; + assert proposalSnapshot(otherId) == otherVoteStart; + assert proposalDeadline(otherId) == otherVoteEnd; + assert proposalProposer(otherId) == otherProposer; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: votes effect and liveness. Includes "A user cannot vote twice" │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule castVote(uint256 pId, env e, method f) + filtered { f -> voting(f) } +{ + require nonpayable(e); + + uint8 support; + address voter; + address otherVoter; + uint256 otherId; + + uint8 stateBefore = state(e, pId); + bool hasVotedBefore = hasVoted(pId, voter); + bool otherVotedBefore = hasVoted(otherId, otherVoter); + uint256 againstVotesBefore = getAgainstVotes(pId); + uint256 forVotesBefore = getForVotes(pId); + uint256 abstainVotesBefore = getAbstainVotes(pId); + uint256 otherAgainstVotesBefore = getAgainstVotes(otherId); + uint256 otherForVotesBefore = getForVotes(otherId); + uint256 otherAbstainVotesBefore = getAbstainVotes(otherId); + + // voting weight overflow check + uint256 voterWeight = token_getPastVotes(voter, proposalSnapshot(pId)); + require againstVotesBefore + voterWeight <= max_uint256; + require forVotesBefore + voterWeight <= max_uint256; + require abstainVotesBefore + voterWeight <= max_uint256; + + uint256 weight = helperVoteWithRevert(e, f, pId, voter, support); + bool success = !lastReverted; + + assert success <=> ( + stateBefore == ACTIVE() && + !hasVotedBefore && + (support == 0 || support == 1 || support == 2) + ); + + assert success => ( + state(e, pId) == ACTIVE() && + voterWeight == weight && + getAgainstVotes(pId) == againstVotesBefore + (support == 0 ? weight : 0) && + getForVotes(pId) == forVotesBefore + (support == 1 ? weight : 0) && + getAbstainVotes(pId) == abstainVotesBefore + (support == 2 ? weight : 0) && + hasVoted(pId, voter) + ); + + // no side-effect + assert hasVoted(otherId, otherVoter) != otherVotedBefore => (otherId == pId && otherVoter == voter); + assert getAgainstVotes(otherId) != otherAgainstVotesBefore => (otherId == pId); + assert getForVotes(otherId) != otherForVotesBefore => (otherId == pId); + assert getAbstainVotes(otherId) != otherAbstainVotesBefore => (otherId == pId); +} diff --git a/certora/specs/methods/IGovernor.spec b/certora/specs/methods/IGovernor.spec new file mode 100644 index 00000000000..53f43b71384 --- /dev/null +++ b/certora/specs/methods/IGovernor.spec @@ -0,0 +1,45 @@ +// includes some non standard (from extension) and harness functions +methods { + name() returns string envfree + version() returns string envfree + clock() returns uint48 + CLOCK_MODE() returns string + COUNTING_MODE() returns string envfree + hashProposal(address[],uint256[],bytes[],bytes32) returns uint256 envfree + state(uint256) returns uint8 + proposalThreshold() returns uint256 envfree + proposalSnapshot(uint256) returns uint256 envfree + proposalDeadline(uint256) returns uint256 envfree + votingDelay() returns uint256 envfree + votingPeriod() returns uint256 envfree + quorum(uint256) returns uint256 envfree + getVotes(address,uint256) returns uint256 envfree + getVotesWithParams(address,uint256,bytes) returns uint256 envfree + hasVoted(uint256,address) returns bool envfree + + propose(address[],uint256[],bytes[],string) returns uint256 + execute(address[],uint256[],bytes[],bytes32) returns uint256 + queue(address[], uint256[], bytes[], bytes32) returns uint256 + cancel(address[],uint256[],bytes[],bytes32) returns uint256 + castVote(uint256,uint8) returns uint256 + castVoteWithReason(uint256,uint8,string) returns uint256 + castVoteWithReasonAndParams(uint256,uint8,string,bytes) returns uint256 + castVoteBySig(uint256,uint8,uint8,bytes32,bytes32) returns uint256 + castVoteWithReasonAndParamsBySig(uint256,uint8,string,bytes,uint8,bytes32,bytes32) returns uint256 + updateQuorumNumerator(uint256) + + // harness + token_getPastTotalSupply(uint256) returns uint256 envfree + token_getPastVotes(address,uint256) returns uint256 envfree + token_clock() returns uint48 + token_CLOCK_MODE() returns string + getExecutor() returns address envfree + proposalProposer(uint256) returns address envfree + quorumReached(uint256) returns bool envfree + voteSucceeded(uint256) returns bool envfree + isExecuted(uint256) returns bool envfree + isCanceled(uint256) returns bool envfree + getAgainstVotes(uint256) returns uint256 envfree + getForVotes(uint256) returns uint256 envfree + getAbstainVotes(uint256) returns uint256 envfree +} \ No newline at end of file From 5421355e578b188f4e88f2e6af6a6a7ef1320551 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 10 Mar 2023 14:30:45 +0100 Subject: [PATCH 02/61] test both modes --- ...s.sol => ERC20VotesBlocknumberHarness.sol} | 2 +- .../harnesses/ERC20VotesTimestampHarness.sol | 39 +++++++++++++++++++ certora/specs.js | 10 +++-- 3 files changed, 46 insertions(+), 5 deletions(-) rename certora/harnesses/{ERC20VotesHarness.sol => ERC20VotesBlocknumberHarness.sol} (93%) create mode 100644 certora/harnesses/ERC20VotesTimestampHarness.sol diff --git a/certora/harnesses/ERC20VotesHarness.sol b/certora/harnesses/ERC20VotesBlocknumberHarness.sol similarity index 93% rename from certora/harnesses/ERC20VotesHarness.sol rename to certora/harnesses/ERC20VotesBlocknumberHarness.sol index edc8ce67b2f..f00e3ec057d 100644 --- a/certora/harnesses/ERC20VotesHarness.sol +++ b/certora/harnesses/ERC20VotesBlocknumberHarness.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.0; import "../patched/token/ERC20/extensions/ERC20Votes.sol"; -contract ERC20VotesHarness is ERC20Votes { +contract ERC20VotesBlocknumberHarness is ERC20Votes { constructor(string memory name, string memory symbol) ERC20(name, symbol) ERC20Permit(name) {} function mint(address account, uint256 amount) external { diff --git a/certora/harnesses/ERC20VotesTimestampHarness.sol b/certora/harnesses/ERC20VotesTimestampHarness.sol new file mode 100644 index 00000000000..e8b96cf3fea --- /dev/null +++ b/certora/harnesses/ERC20VotesTimestampHarness.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "../patched/token/ERC20/extensions/ERC20Votes.sol"; + +contract ERC20VotesTimestampHarness is ERC20Votes { + constructor(string memory name, string memory symbol) ERC20(name, symbol) ERC20Permit(name) {} + + function mint(address account, uint256 amount) external { + _mint(account, amount); + } + + function burn(address account, uint256 amount) external { + _burn(account, amount); + } + + // inspection + function ckptFromBlock(address account, uint32 pos) public view returns (uint32) { + return checkpoints(account, pos).fromBlock; + } + + function ckptVotes(address account, uint32 pos) public view returns (uint224) { + return checkpoints(account, pos).votes; + } + + function maxSupply() public view returns (uint224) { + return _maxSupply(); + } + + // clock + function clock() public view override returns (uint48) { + return uint48(block.timestamp); + } + + // solhint-disable-next-line func-name-mixedcase + function CLOCK_MODE() public view virtual override returns (string memory) { + return "mode=timestamp"; + } +} diff --git a/certora/specs.js b/certora/specs.js index 1f1879246ca..2d9ee3e8b02 100644 --- a/certora/specs.js +++ b/certora/specs.js @@ -1,4 +1,3 @@ -/// This helper will be handy when we want to do cross product. Ex: all governor specs on all variations of the clock mode. const product = (...arrays) => arrays.reduce((a, b) => a.flatMap(ai => b.map(bi => [ai, bi].flat()))); module.exports = [ @@ -49,15 +48,18 @@ module.exports = [ "contract": "InitializableHarness", "files": ["certora/harnesses/InitializableHarness.sol"] }, - ...[ "GovernorBase", "GovernorInvariants", "GovernorStates", "GovernorFunctions" ].map(spec => ({ + ...product( + [ "GovernorBase", "GovernorInvariants", "GovernorStates", "GovernorFunctions" ], + [ "ERC20VotesBlocknumberHarness", "ERC20VotesTimestampHarness" ], + ).map(([ spec, token ]) => ({ spec, "contract": "GovernorHarness", "files": [ "certora/harnesses/GovernorHarness.sol", - "certora/harnesses/ERC20VotesHarness.sol" + `certora/harnesses/${token}.sol` ], "options": [ - "--link GovernorHarness:token=ERC20VotesHarness", + `--link GovernorHarness:token=${token}`, "--optimistic_loop", "--optimistic_hashing" ] From f35c82443527025ef465a419053e804094656f21 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 10 Mar 2023 14:52:39 +0100 Subject: [PATCH 03/61] fix specs --- certora/specs/Governor.helpers.spec | 38 ++++--- certora/specs/GovernorBase.spec | 40 +++---- certora/specs/GovernorInvariants.spec | 17 ++- certora/specs/GovernorStates.spec | 150 ++------------------------ 4 files changed, 53 insertions(+), 192 deletions(-) diff --git a/certora/specs/Governor.helpers.spec b/certora/specs/Governor.helpers.spec index 5f849876d70..bd6b4b3fca3 100644 --- a/certora/specs/Governor.helpers.spec +++ b/certora/specs/Governor.helpers.spec @@ -45,7 +45,7 @@ definition voting(method f) returns bool = └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ function helperVoteWithRevert(env e, method f, uint256 pId, address voter, uint8 support) returns uint256 { - string reason; bytes params; uint8 v; bytes32 s; bytes32 r; + string reason; bytes params; if (f.selector == castVote(uint256,uint8).selector) { @@ -64,39 +64,38 @@ function helperVoteWithRevert(env e, method f, uint256 pId, address voter, uint8 } else { - calldataarg args; - f@withrevert(e, args); + require false; return 0; } } function helperFunctionsWithRevert(env e, method f, uint256 pId) { - if (f.selector == propose(address[], uint256[], bytes[], string).selector) + if (f.selector == propose(address[],uint256[],bytes[],string).selector) { - address[] targets; uint256[] values; bytes[] calldatas; string description; - require pId == propose@withrevert(e, targets, values, calldatas, description); + address[] targets; uint256[] values; bytes[] calldatas; string descr; + require pId == propose@withrevert(e, targets, values, calldatas, descr); } - else if (f.selector == queue(address[], uint256[], bytes[], bytes32).selector) + else if (f.selector == queue(address[],uint256[],bytes[],bytes32).selector) { - address[] targets; uint256[] values; bytes[] calldatas; bytes32 description; - require pId == queue@withrevert(e, targets, values, calldatas, description); + address[] targets; uint256[] values; bytes[] calldatas; bytes32 descrHash; + require pId == queue@withrevert(e, targets, values, calldatas, descrHash); } - else if (f.selector == execute(address[], uint256[], bytes[], bytes32).selector) + else if (f.selector == execute(address[],uint256[],bytes[],bytes32).selector) { - address[] targets; uint256[] values; bytes[] calldatas; bytes32 description; - require pId == execute@withrevert(e, targets, values, calldatas, description); + address[] targets; uint256[] values; bytes[] calldatas; bytes32 descrHash; + require pId == execute@withrevert(e, targets, values, calldatas, descrHash); } - else if (f.selector == cancel(address[], uint256[], bytes[], bytes32).selector) + else if (f.selector == cancel(address[],uint256[],bytes[],bytes32).selector) { - address[] targets; uint256[] values; bytes[] calldatas; bytes32 description; - require pId == cancel@withrevert(e, targets, values, calldatas, description); + address[] targets; uint256[] values; bytes[] calldatas; bytes32 descrHash; + require pId == cancel@withrevert(e, targets, values, calldatas, descrHash); } - else if (f.selector == castVote(uint256, uint8).selector) + else if (f.selector == castVote(uint256,uint8).selector) { uint8 support; castVote@withrevert(e, pId, support); } - else if (f.selector == castVoteWithReason(uint256, uint8, string).selector) + else if (f.selector == castVoteWithReason(uint256,uint8,string).selector) { uint8 support; string reason; castVoteWithReason@withrevert(e, pId, support, reason); @@ -106,7 +105,7 @@ function helperFunctionsWithRevert(env e, method f, uint256 pId) { uint8 support; string reason; bytes params; castVoteWithReasonAndParams@withrevert(e, pId, support, reason, params); } - else if (f.selector == castVoteBySig(uint256, uint8,uint8, bytes32, bytes32).selector) + else if (f.selector == castVoteBySig(uint256,uint8,uint8,bytes32,bytes32).selector) { uint8 support; uint8 v; bytes32 r; bytes32 s; castVoteBySig@withrevert(e, pId, support, v, r, s); @@ -118,7 +117,6 @@ function helperFunctionsWithRevert(env e, method f, uint256 pId) { } else { - calldataarg args; - f@withrevert(e, args); + require false; } } \ No newline at end of file diff --git a/certora/specs/GovernorBase.spec b/certora/specs/GovernorBase.spec index 977993980ad..6f4fcab9393 100644 --- a/certora/specs/GovernorBase.spec +++ b/certora/specs/GovernorBase.spec @@ -99,14 +99,14 @@ rule immutableFieldsAfterProposalCreation(uint256 pId, env e, method f, calldata require proposalCreated(pId); uint256 voteStart = proposalSnapshot(pId); - uint256 voteEnd = proposalDeadline(pId); - address proposer = proposalProposer(pId); + uint256 voteEnd = proposalDeadline(pId); + address proposer = proposalProposer(pId); f(e, arg); assert voteStart == proposalSnapshot(pId), "Start date was changed"; - assert voteEnd == proposalDeadline(pId), "End date was changed"; - assert proposer == proposalProposer(pId), "Proposer was changed"; + assert voteEnd == proposalDeadline(pId), "End date was changed"; + assert proposer == proposalProposer(pId), "Proposer was changed"; } /* @@ -177,21 +177,14 @@ rule noExecuteBeforeDeadline(uint256 pId, env e, method f, calldataarg args) { │ attribute to the execute() function, showing that only execute() can change it, and that it will always change it. │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ -rule allFunctionsRevertIfExecuted(uint256 pId, env e, method f, calldataarg args) filtered { - f -> !f.isView && !f.isFallback - && f.selector != updateTimelock(address).selector - && f.selector != updateQuorumNumerator(uint256).selector - && f.selector != relay(address,uint256,bytes).selector - && f.selector != 0xb9a61961 // __acceptAdmin() - && f.selector != onERC721Received(address,address,uint256,bytes).selector - && f.selector != onERC1155Received(address,address,uint256,uint256,bytes).selector - && f.selector != onERC1155BatchReceived(address,address,uint256[],uint256[],bytes).selector -} { +rule allFunctionsRevertIfExecuted(uint256 pId, env e, method f, calldataarg args) + filtered { f -> !skip(f) } +{ require isExecuted(pId); requireInvariant noBothExecutedAndCanceled(pId); requireInvariant executedImplyCreated(pId); - helperFunctionsWithRevert(pId, f, e); + helperFunctionsWithRevert(e, f, pId); assert lastReverted, "Function was not reverted"; } @@ -205,21 +198,14 @@ rule allFunctionsRevertIfExecuted(uint256 pId, env e, method f, calldataarg args │ attribute to the execute() function, showing that only execute() can change it, and that it will always change it. │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ -rule allFunctionsRevertIfCanceled(uint256 pId, env e, method f, calldataarg args) filtered { - f -> !f.isView && !f.isFallback - && f.selector != updateTimelock(address).selector - && f.selector != updateQuorumNumerator(uint256).selector - && f.selector != relay(address,uint256,bytes).selector - && f.selector != 0xb9a61961 // __acceptAdmin() - && f.selector != onERC721Received(address,address,uint256,bytes).selector - && f.selector != onERC1155Received(address,address,uint256,uint256,bytes).selector - && f.selector != onERC1155BatchReceived(address,address,uint256[],uint256[],bytes).selector -} { +rule allFunctionsRevertIfCanceled(uint256 pId, env e, method f, calldataarg args) + filtered { f -> !skip(f) } +{ require isCanceled(pId); requireInvariant noBothExecutedAndCanceled(pId); requireInvariant canceledImplyCreated(pId); - helperFunctionsWithRevert(pId, f, e); + helperFunctionsWithRevert(e, f, pId); assert lastReverted, "Function was not reverted"; } @@ -234,7 +220,7 @@ rule stateOnlyAfterFunc(uint256 pId, env e, method f) { bool executedBefore = isExecuted(pId); bool canceledBefore = isCanceled(pId); - helperFunctionsWithRevert(pId, f, e); + helperFunctionsWithRevert(e, f, pId); assert (proposalCreated(pId) != createdBefore) => (createdBefore == false && f.selector == propose(address[], uint256[], bytes[], string).selector), diff --git a/certora/specs/GovernorInvariants.spec b/certora/specs/GovernorInvariants.spec index a8a200ca375..223f504b933 100644 --- a/certora/specs/GovernorInvariants.spec +++ b/certora/specs/GovernorInvariants.spec @@ -21,14 +21,23 @@ invariant createdConsistency(env e, uint256 pId) safeState(e, pId) == UNSET() <=> proposalProposer(pId) == 0 && safeState(e, pId) == UNSET() <=> proposalSnapshot(pId) == 0 && safeState(e, pId) == UNSET() <=> proposalDeadline(pId) == 0 && - safeState(e, pId) == UNSET() => !isExecuted(pId) && - safeState(e, pId) == UNSET() => !isCanceled(pId) + safeState(e, pId) == UNSET() => !isExecuted(pId) && + safeState(e, pId) == UNSET() => !isCanceled(pId) { preserved { require clock(e) > 0; } } +invariant createdConsistencyWeak(uint256 pId) + proposalProposer(pId) == 0 <=> proposalSnapshot(pId) == 0 && + proposalProposer(pId) == 0 <=> proposalDeadline(pId) == 0 + { + preserved with (env e) { + require clock(e) > 0; + } + } + /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ Invariant: Votes start before it ends │ @@ -37,8 +46,8 @@ invariant createdConsistency(env e, uint256 pId) invariant voteStartBeforeVoteEnd(uint256 pId) proposalSnapshot(pId) <= proposalDeadline(pId) { - preserved with (env e) { - requireInvariant createdConsistency(e, pId); + preserved { + requireInvariant createdConsistencyWeak(pId); } } diff --git a/certora/specs/GovernorStates.spec b/certora/specs/GovernorStates.spec index d2ddf994788..b7737ce24b6 100644 --- a/certora/specs/GovernorStates.spec +++ b/certora/specs/GovernorStates.spec @@ -22,7 +22,7 @@ rule stateConsistency(env e, uint256 pId) { /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -│ Rule: State transition │ +│ Rule: State transitions caused by function calls │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ rule stateTransitionFn(uint256 pId, env e, method f, calldataarg args) @@ -55,6 +55,11 @@ rule stateTransitionFn(uint256 pId, env e, method f, calldataarg args) ); } +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: State transitions caused by time passing │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ rule stateTransitionWait(uint256 pId, env e1, env e2) { require clock(e1) > 0; // Sanity require clock(e2) > clock(e1); @@ -63,14 +68,9 @@ rule stateTransitionWait(uint256 pId, env e1, env e2) { uint8 stateAfter = state(e2, pId); assert (stateBefore != stateAfter) => ( - stateBefore == PENDING() => ( - stateAfter == ACTIVE() - ) && - stateBefore == ACTIVE() => ( - stateAfter == SUCCEEDED() || - stateAfter == DEFEATED() - ) && - stateBefore == UNSET() => false && + stateBefore == PENDING() => stateAfter == ACTIVE() && + stateBefore == ACTIVE() => (stateAfter == SUCCEEDED() || stateAfter == DEFEATED()) && + stateBefore == UNSET() => false && stateBefore == SUCCEEDED() => false && stateBefore == QUEUED() => false && stateBefore == CANCELED() => false && @@ -78,135 +78,3 @@ rule stateTransitionWait(uint256 pId, env e1, env e2) { stateBefore == EXECUTED() => false ); } - - - - - - -/* -┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -│ Rule: Once a proposal is created, voteStart, voteEnd and proposer are immutable │ -└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -*/ -rule immutableFieldsAfterProposalCreation(uint256 pId, env e, method f, calldataarg arg) - filtered { f -> !skip(f) } -{ - require state(e, pId) != UNSET(); - - uint256 voteStart = proposalSnapshot(pId); - uint256 voteEnd = proposalDeadline(pId); - address proposer = proposalProposer(pId); - - f(e, arg); - - assert voteStart == proposalSnapshot(pId), "Start date was changed"; - assert voteEnd == proposalDeadline(pId), "End date was changed"; - assert proposer == proposalProposer(pId), "Proposer was changed"; -} - - - - - - - - - - - - - -/* -┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -│ Rule: propose effect and liveness. Includes "no double proposition" │ -└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -*/ -rule propose(uint256 pId, env e) { - require nonpayable(e); - - uint256 otherId; - - uint8 stateBefore = state(e, pId); - uint8 otherStateBefore = state(e, otherId); - uint256 otherVoteStart = proposalSnapshot(otherId); - uint256 otherVoteEnd = proposalDeadline(otherId); - address otherProposer = proposalProposer(otherId); - - address[] targets; uint256[] values; bytes[] calldatas; string reason; - require pId == propose@withrevert(e, targets, values, calldatas, reason); - bool success = !lastReverted; - - // liveness & double proposal - assert success <=> stateBefore == UNSET(); - - // effect - assert success => ( - state(e, pId) == PENDING() && - proposalProposer(pId) == e.msg.sender && - proposalSnapshot(pId) == clock(e) + votingDelay() && - proposalDeadline(pId) == clock(e) + votingDelay() + votingPeriod() - ); - - // no side-effect - assert state(e, otherId) != otherStateBefore => otherId == pId; - assert proposalSnapshot(otherId) == otherVoteStart; - assert proposalDeadline(otherId) == otherVoteEnd; - assert proposalProposer(otherId) == otherProposer; -} - -/* -┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -│ Rule: votes effect and liveness. Includes "A user cannot vote twice" │ -└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -*/ -rule castVote(uint256 pId, env e, method f) - filtered { f -> voting(f) } -{ - require nonpayable(e); - - uint8 support; - address voter; - address otherVoter; - uint256 otherId; - - uint8 stateBefore = state(e, pId); - bool hasVotedBefore = hasVoted(pId, voter); - bool otherVotedBefore = hasVoted(otherId, otherVoter); - uint256 againstVotesBefore = getAgainstVotes(pId); - uint256 forVotesBefore = getForVotes(pId); - uint256 abstainVotesBefore = getAbstainVotes(pId); - uint256 otherAgainstVotesBefore = getAgainstVotes(otherId); - uint256 otherForVotesBefore = getForVotes(otherId); - uint256 otherAbstainVotesBefore = getAbstainVotes(otherId); - - // voting weight overflow check - uint256 voterWeight = token_getPastVotes(voter, proposalSnapshot(pId)); - require againstVotesBefore + voterWeight <= max_uint256; - require forVotesBefore + voterWeight <= max_uint256; - require abstainVotesBefore + voterWeight <= max_uint256; - - uint256 weight = helperVoteWithRevert(e, f, pId, voter, support); - bool success = !lastReverted; - - assert success <=> ( - stateBefore == ACTIVE() && - !hasVotedBefore && - (support == 0 || support == 1 || support == 2) - ); - - assert success => ( - state(e, pId) == ACTIVE() && - voterWeight == weight && - getAgainstVotes(pId) == againstVotesBefore + (support == 0 ? weight : 0) && - getForVotes(pId) == forVotesBefore + (support == 1 ? weight : 0) && - getAbstainVotes(pId) == abstainVotesBefore + (support == 2 ? weight : 0) && - hasVoted(pId, voter) - ); - - // no side-effect - assert hasVoted(otherId, otherVoter) != otherVotedBefore => (otherId == pId && otherVoter == voter); - assert getAgainstVotes(otherId) != otherAgainstVotesBefore => (otherId == pId); - assert getForVotes(otherId) != otherForVotesBefore => (otherId == pId); - assert getAbstainVotes(otherId) != otherAbstainVotesBefore => (otherId == pId); -} From 9f39697a44708d865c1a885e30e5a25b0e3c1eb6 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 10 Mar 2023 15:08:42 +0100 Subject: [PATCH 04/61] lint --- certora/specs.js | 80 +++++++++++++++++++----------------------------- 1 file changed, 32 insertions(+), 48 deletions(-) diff --git a/certora/specs.js b/certora/specs.js index 2d9ee3e8b02..7b1e0a03462 100644 --- a/certora/specs.js +++ b/certora/specs.js @@ -2,66 +2,50 @@ const product = (...arrays) => arrays.reduce((a, b) => a.flatMap(ai => b.map(bi module.exports = [ { - "spec": "AccessControl", - "contract": "AccessControlHarness", - "files": ["certora/harnesses/AccessControlHarness.sol"] + spec: 'AccessControl', + contract: 'AccessControlHarness', + files: ['certora/harnesses/AccessControlHarness.sol'], }, { - "spec": "Ownable", - "contract": "OwnableHarness", - "files": ["certora/harnesses/OwnableHarness.sol"] + spec: 'Ownable', + contract: 'OwnableHarness', + files: ['certora/harnesses/OwnableHarness.sol'], }, { - "spec": "Ownable2Step", - "contract": "Ownable2StepHarness", - "files": ["certora/harnesses/Ownable2StepHarness.sol"] + spec: 'Ownable2Step', + contract: 'Ownable2StepHarness', + files: ['certora/harnesses/Ownable2StepHarness.sol'], }, { - "spec": "ERC20", - "contract": "ERC20PermitHarness", - "files": ["certora/harnesses/ERC20PermitHarness.sol"], - "options": ["--optimistic_loop"] + spec: 'ERC20', + contract: 'ERC20PermitHarness', + files: ['certora/harnesses/ERC20PermitHarness.sol'], + options: ['--optimistic_loop'], }, { - "spec": "ERC20FlashMint", - "contract": "ERC20FlashMintHarness", - "files": [ - "certora/harnesses/ERC20FlashMintHarness.sol", - "certora/harnesses/ERC3156FlashBorrowerHarness.sol" - ], - "options": ["--optimistic_loop"] + spec: 'ERC20FlashMint', + contract: 'ERC20FlashMintHarness', + files: ['certora/harnesses/ERC20FlashMintHarness.sol', 'certora/harnesses/ERC3156FlashBorrowerHarness.sol'], + options: ['--optimistic_loop'], }, { - "spec": "ERC20Wrapper", - "contract": "ERC20WrapperHarness", - "files": [ - "certora/harnesses/ERC20PermitHarness.sol", - "certora/harnesses/ERC20WrapperHarness.sol" - ], - "options": [ - "--link ERC20WrapperHarness:_underlying=ERC20PermitHarness", - "--optimistic_loop" - ] + spec: 'ERC20Wrapper', + contract: 'ERC20WrapperHarness', + files: ['certora/harnesses/ERC20PermitHarness.sol', 'certora/harnesses/ERC20WrapperHarness.sol'], + options: ['--link ERC20WrapperHarness:_underlying=ERC20PermitHarness', '--optimistic_loop'], }, { - "spec": "Initializable", - "contract": "InitializableHarness", - "files": ["certora/harnesses/InitializableHarness.sol"] + spec: 'Initializable', + contract: 'InitializableHarness', + files: ['certora/harnesses/InitializableHarness.sol'], }, ...product( - [ "GovernorBase", "GovernorInvariants", "GovernorStates", "GovernorFunctions" ], - [ "ERC20VotesBlocknumberHarness", "ERC20VotesTimestampHarness" ], - ).map(([ spec, token ]) => ({ + ['GovernorBase', 'GovernorInvariants', 'GovernorStates', 'GovernorFunctions'], + ['ERC20VotesBlocknumberHarness', 'ERC20VotesTimestampHarness'], + ).map(([spec, token]) => ({ spec, - "contract": "GovernorHarness", - "files": [ - "certora/harnesses/GovernorHarness.sol", - `certora/harnesses/${token}.sol` - ], - "options": [ - `--link GovernorHarness:token=${token}`, - "--optimistic_loop", - "--optimistic_hashing" - ] - })) -]; \ No newline at end of file + contract: 'GovernorHarness', + files: ['certora/harnesses/GovernorHarness.sol', `certora/harnesses/${token}.sol`], + options: [`--link GovernorHarness:token=${token}`, '--optimistic_loop', '--optimistic_hashing'], + })), +]; From ec82e2f6fd8dc8a5152f7f5df40b17cfa90365c1 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 10 Mar 2023 15:34:48 +0100 Subject: [PATCH 05/61] use micromatch --- certora/run.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/certora/run.js b/certora/run.js index 7623f95c697..175cedd88b0 100644 --- a/certora/run.js +++ b/certora/run.js @@ -11,10 +11,9 @@ const MAX_PARALLEL = 4; const proc = require('child_process'); const { PassThrough } = require('stream'); const events = require('events'); +const micromatch = require('micromatch'); const limit = require('p-limit')(MAX_PARALLEL); -const strToRegex = str => new RegExp(`^${str.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/[*]/g, '.$&')}$`); - let [, , request = '', ...extraOptions] = process.argv; if (request.startsWith('-')) { extraOptions.unshift(request); @@ -23,8 +22,8 @@ if (request.startsWith('-')) { const [reqSpec, reqContract] = request.split(':').reverse(); const specs = require(__dirname + '/specs.js') - .filter(entry => !reqSpec || strToRegex(reqSpec).test(entry.spec)) - .filter(entry => !reqContract || strToRegex(reqContract).test(entry.contract)); + .filter(entry => !reqSpec || micromatch(entry.spec, reqSpec)) + .filter(entry => !reqContract || micromatch(entry.contract, reqContract)); if (specs.length === 0) { console.error(`Error: Requested spec '${request}' not found in specs.json`); From f44e26fa7b0988072f591174e0de5e31c94dadaa Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 10 Mar 2023 16:55:37 +0100 Subject: [PATCH 06/61] disable wip specs --- certora/specs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certora/specs.js b/certora/specs.js index 7b1e0a03462..3df13520054 100644 --- a/certora/specs.js +++ b/certora/specs.js @@ -40,7 +40,7 @@ module.exports = [ files: ['certora/harnesses/InitializableHarness.sol'], }, ...product( - ['GovernorBase', 'GovernorInvariants', 'GovernorStates', 'GovernorFunctions'], + ['GovernorBase', 'GovernorInvariants', 'GovernorStates', /*'GovernorFunctions'*/], ['ERC20VotesBlocknumberHarness', 'ERC20VotesTimestampHarness'], ).map(([spec, token]) => ({ spec, From 318cfd501badddb1fd68a553334015f85600a52a Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 10 Mar 2023 20:30:25 +0100 Subject: [PATCH 07/61] update --- certora/diff/governance_Governor.sol.patch | 9 +- certora/diff/token_ERC721_ERC721.sol.patch | 14 +++ certora/harnesses/GovernorHarness.sol | 4 + certora/run.js | 5 +- certora/specs.js | 16 ++- certora/specs/Governor.helpers.spec | 3 + ...vernorBase.spec => GovernorBaseRules.spec} | 104 ++++-------------- certora/specs/GovernorFunctions.spec | 76 +++++++++++++ certora/specs/GovernorInvariants.spec | 81 +++++++++----- certora/specs/GovernorStates.spec | 45 ++++++++ certora/specs/helpers.spec | 2 + certora/specs/methods/IGovernor.spec | 1 + requirements.txt | 2 +- 13 files changed, 243 insertions(+), 119 deletions(-) create mode 100644 certora/diff/token_ERC721_ERC721.sol.patch rename certora/specs/{GovernorBase.spec => GovernorBaseRules.spec} (62%) diff --git a/certora/diff/governance_Governor.sol.patch b/certora/diff/governance_Governor.sol.patch index 68710da6575..e3ad3e9b3b1 100644 --- a/certora/diff/governance_Governor.sol.patch +++ b/certora/diff/governance_Governor.sol.patch @@ -1,6 +1,6 @@ --- governance/Governor.sol 2023-03-07 10:48:47.730155491 +0100 -+++ governance/Governor.sol 2023-03-10 10:13:31.926616811 +0100 -@@ -216,6 +216,16 @@ ++++ governance/Governor.sol 2023-03-13 14:07:30.704202049 +0100 +@@ -216,6 +216,21 @@ return _proposals[proposalId].proposer; } @@ -13,6 +13,11 @@ + function _isCanceled(uint256 proposalId) internal view returns (bool) { + return _proposals[proposalId].canceled; + } ++ ++ // FV ++ function _governanceCallLength() public view returns (uint256) { ++ return _governanceCall.length(); ++ } + /** * @dev Amount of votes already cast passes the threshold limit. diff --git a/certora/diff/token_ERC721_ERC721.sol.patch b/certora/diff/token_ERC721_ERC721.sol.patch new file mode 100644 index 00000000000..312fc4ff672 --- /dev/null +++ b/certora/diff/token_ERC721_ERC721.sol.patch @@ -0,0 +1,14 @@ +--- token/ERC721/ERC721.sol 2023-03-07 10:48:47.736822221 +0100 ++++ token/ERC721/ERC721.sol 2023-03-09 19:50:20.555856358 +0100 +@@ -199,6 +199,11 @@ + return _owners[tokenId]; + } + ++ // FV ++ function _getApproved(uint256 tokenId) internal view returns (address) { ++ return _tokenApprovals[tokenId]; ++ } ++ + /** + * @dev Returns whether `tokenId` exists. + * diff --git a/certora/harnesses/GovernorHarness.sol b/certora/harnesses/GovernorHarness.sol index ca841ea8803..52a09416df1 100644 --- a/certora/harnesses/GovernorHarness.sol +++ b/certora/harnesses/GovernorHarness.sol @@ -64,6 +64,10 @@ contract GovernorHarness is return _isCanceled(proposalId); } + function governanceCallLength() public view returns (uint256) { + return _governanceCallLength(); + } + // Harness from GovernorCountingSimple function getAgainstVotes(uint256 proposalId) public view returns (uint256) { (uint256 againstVotes,,) = proposalVotes(proposalId); diff --git a/certora/run.js b/certora/run.js index 175cedd88b0..50e8a1dcfaf 100644 --- a/certora/run.js +++ b/certora/run.js @@ -22,14 +22,15 @@ if (request.startsWith('-')) { const [reqSpec, reqContract] = request.split(':').reverse(); const specs = require(__dirname + '/specs.js') - .filter(entry => !reqSpec || micromatch(entry.spec, reqSpec)) - .filter(entry => !reqContract || micromatch(entry.contract, reqContract)); + .filter(entry => !reqSpec || micromatch.isMatch(entry.spec, reqSpec)) + .filter(entry => !reqContract || micromatch.isMatch(entry.contract, reqContract)); if (specs.length === 0) { console.error(`Error: Requested spec '${request}' not found in specs.json`); process.exit(1); } +console.table(specs.map(spec => `${spec.contract}:${spec.spec} ${spec.options.join(' ')}`)) for (const { spec, contract, files, options = [] } of Object.values(specs)) { limit(runCertora, spec, contract, files, [...options.flatMap(opt => opt.split(' ')), ...extraOptions]); } diff --git a/certora/specs.js b/certora/specs.js index 3df13520054..630865542db 100644 --- a/certora/specs.js +++ b/certora/specs.js @@ -1,6 +1,7 @@ const product = (...arrays) => arrays.reduce((a, b) => a.flatMap(ai => b.map(bi => [ai, bi].flat()))); module.exports = [ + // AccessControl { spec: 'AccessControl', contract: 'AccessControlHarness', @@ -16,6 +17,7 @@ module.exports = [ contract: 'Ownable2StepHarness', files: ['certora/harnesses/Ownable2StepHarness.sol'], }, + // Tokens { spec: 'ERC20', contract: 'ERC20PermitHarness', @@ -34,13 +36,15 @@ module.exports = [ files: ['certora/harnesses/ERC20PermitHarness.sol', 'certora/harnesses/ERC20WrapperHarness.sol'], options: ['--link ERC20WrapperHarness:_underlying=ERC20PermitHarness', '--optimistic_loop'], }, + // Proxy { spec: 'Initializable', contract: 'InitializableHarness', files: ['certora/harnesses/InitializableHarness.sol'], }, + // Governor ...product( - ['GovernorBase', 'GovernorInvariants', 'GovernorStates', /*'GovernorFunctions'*/], + ['GovernorInvariants', 'GovernorBaseRules', 'GovernorStates'], ['ERC20VotesBlocknumberHarness', 'ERC20VotesTimestampHarness'], ).map(([spec, token]) => ({ spec, @@ -48,4 +52,14 @@ module.exports = [ files: ['certora/harnesses/GovernorHarness.sol', `certora/harnesses/${token}.sol`], options: [`--link GovernorHarness:token=${token}`, '--optimistic_loop', '--optimistic_hashing'], })), + // WIP part + // ...product( + // ['GovernorFunctions'], + // ['ERC20VotesBlocknumberHarness'], + // ).map(([spec, token]) => ({ + // spec, + // contract: 'GovernorHarness', + // files: ['certora/harnesses/GovernorHarness.sol', `certora/harnesses/${token}.sol`], + // options: [`--link GovernorHarness:token=${token}`, '--optimistic_loop', '--optimistic_hashing'], + // })), ]; diff --git a/certora/specs/Governor.helpers.spec b/certora/specs/Governor.helpers.spec index bd6b4b3fca3..f43e5606fc7 100644 --- a/certora/specs/Governor.helpers.spec +++ b/certora/specs/Governor.helpers.spec @@ -20,6 +20,9 @@ function safeState(env e, uint256 pId) returns uint8 { return lastReverted ? UNSET() : result; } +definition proposalCreated(uint256 pId) returns bool = + proposalSnapshot(pId) > 0 && proposalDeadline(pId) > 0 && proposalProposer(pId) != 0; + /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ Filters │ diff --git a/certora/specs/GovernorBase.spec b/certora/specs/GovernorBaseRules.spec similarity index 62% rename from certora/specs/GovernorBase.spec rename to certora/specs/GovernorBaseRules.spec index 6f4fcab9393..3063adae41e 100644 --- a/certora/specs/GovernorBase.spec +++ b/certora/specs/GovernorBaseRules.spec @@ -1,80 +1,11 @@ import "methods/IGovernor.spec" import "Governor.helpers.spec" +import "GovernorInvariants.spec" -/* -┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -│ Definitions │ -└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -*/ - -definition proposalCreated(uint256 pId) returns bool = - proposalSnapshot(pId) > 0 && proposalDeadline(pId) > 0 && proposalProposer(pId) != 0; - -/* -┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -│ Invariant: Votes start and end 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) │ -└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -*/ -invariant proposalStateConsistency(uint256 pId) - (proposalProposer(pId) != 0 <=> proposalSnapshot(pId) != 0) && (proposalProposer(pId) != 0 <=> proposalDeadline(pId) != 0) - { - preserved with (env e) { - require clock(e) > 0; - } - } - -/* -┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -│ Invariant: cancel => created │ -└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -*/ -invariant canceledImplyCreated(uint pId) - isCanceled(pId) => proposalCreated(pId) - { - preserved with (env e) { - requireInvariant proposalStateConsistency(pId); - require clock(e) > 0; - } - } - -/* -┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -│ Invariant: executed => created │ -└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -*/ -invariant executedImplyCreated(uint pId) - isExecuted(pId) => proposalCreated(pId) - { - preserved with (env e) { - requireInvariant proposalStateConsistency(pId); - require clock(e) > 0; - } - } - -/* -┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -│ Invariant: Votes start before it ends │ -└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -*/ -invariant voteStartBeforeVoteEnd(uint256 pId) - proposalSnapshot(pId) <= proposalDeadline(pId) - { - preserved { - requireInvariant proposalStateConsistency(pId); - } - } - -/* -┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -│ Invariant: A proposal cannot be both executed and canceled simultaneously │ -└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -*/ -invariant noBothExecutedAndCanceled(uint256 pId) - !isExecuted(pId) || !isCanceled(pId) +use invariant proposalStateConsistency +use invariant canceledImplyCreated +use invariant executedImplyCreated +use invariant noBothExecutedAndCanceled /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ @@ -143,7 +74,7 @@ rule executionOnlyIfQuoromReachedAndVoteSucceeded(uint256 pId, env e, method f, f(e, args); - assert isExecuted(pId) => (quorumReachedBefore && voteSucceededBefore), "quorum was changed"; + assert isExecuted(pId) => (quorumReachedBefore && voteSucceededBefore), "quorum not met or vote not succesfull"; } /* @@ -216,21 +147,24 @@ rule allFunctionsRevertIfCanceled(uint256 pId, env e, method f, calldataarg args └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ rule stateOnlyAfterFunc(uint256 pId, env e, method f) { - bool createdBefore = proposalCreated(pId); + bool createdBefore = proposalCreated(pId); bool executedBefore = isExecuted(pId); bool canceledBefore = isCanceled(pId); helperFunctionsWithRevert(e, f, pId); - assert (proposalCreated(pId) != createdBefore) - => (createdBefore == false && f.selector == propose(address[], uint256[], bytes[], string).selector), - "proposalCreated only changes in the propose method"; + assert (proposalCreated(pId) != createdBefore) => ( + createdBefore == false && + f.selector == propose(address[], uint256[], bytes[], string).selector + ), "proposalCreated only changes in the propose method"; - assert (isExecuted(pId) != executedBefore) - => (executedBefore == false && f.selector == execute(address[], uint256[], bytes[], bytes32).selector), - "isExecuted only changes in the execute method"; + assert (isExecuted(pId) != executedBefore) => ( + executedBefore == false && + f.selector == execute(address[], uint256[], bytes[], bytes32).selector + ), "isExecuted only changes in the execute method"; - assert (isCanceled(pId) != canceledBefore) - => (canceledBefore == false && f.selector == cancel(address[], uint256[], bytes[], bytes32).selector), - "isCanceled only changes in the cancel method"; + assert (isCanceled(pId) != canceledBefore) => ( + canceledBefore == false && + f.selector == cancel(address[], uint256[], bytes[], bytes32).selector + ), "isCanceled only changes in the cancel method"; } diff --git a/certora/specs/GovernorFunctions.spec b/certora/specs/GovernorFunctions.spec index 40ad27213d0..ec50c044a43 100644 --- a/certora/specs/GovernorFunctions.spec +++ b/certora/specs/GovernorFunctions.spec @@ -95,3 +95,79 @@ rule castVote(uint256 pId, env e, method f) assert getForVotes(otherId) != otherForVotesBefore => (otherId == pId); assert getAbstainVotes(otherId) != otherAbstainVotesBefore => (otherId == pId); } + + +rule queue(uint256 pId, env e) { + require nonpayable(e); + + uint256 otherId; + + uint8 stateBefore = state(e, pId); + uint8 otherStateBefore = state(e, otherId); + + address[] targets; uint256[] values; bytes[] calldatas; string reason; + require pId == queue@withrevert(e, targets, values, calldatas, reason); + bool success = !lastReverted; + + // liveness + assert success <=> stateBefore == SUCCEEDED(); + + // effect + assert success => ( + state(e, pId) == QUEUED() + ); + + // no side-effect + assert state(e, otherId) != otherStateBefore => otherId == pId; +} + +rule execute(uint256 pId, env e) { + require nonpayable(e); + + uint256 otherId; + + uint8 stateBefore = state(e, pId); + uint8 otherStateBefore = state(e, otherId); + + address[] targets; uint256[] values; bytes[] calldatas; string reason; + require pId == execute@withrevert(e, targets, values, calldatas, reason); + bool success = !lastReverted; + + // liveness: can't check full equivalence because of execution call reverts + assert success => (stateBefore == SUCCEEDED() || stateBefore == QUEUED()); + + // effect + assert success => ( + state(e, pId) == EXECUTED() + ); + + // no side-effect + assert state(e, otherId) != otherStateBefore => otherId == pId; +} + +rule cancel(uint256 pId, env e) { + require nonpayable(e); + + uint256 otherId; + + uint8 stateBefore = state(e, pId); + uint8 otherStateBefore = state(e, otherId); + + address[] targets; uint256[] values; bytes[] calldatas; string reason; + require pId == cancel@withrevert(e, targets, values, calldatas, reason); + bool success = !lastReverted; + + // liveness + assert success <=> ( + stateBefore == PENDING() && + e.msg.sender == proposalProposer(pId) + ); + + // effect + assert success => ( + state(e, pId) == CANCELED() + ); + + // no side-effect + assert state(e, otherId) != otherStateBefore => otherId == pId; +} \ No newline at end of file diff --git a/certora/specs/GovernorInvariants.spec b/certora/specs/GovernorInvariants.spec index 223f504b933..dfe95e81f24 100644 --- a/certora/specs/GovernorInvariants.spec +++ b/certora/specs/GovernorInvariants.spec @@ -8,32 +8,70 @@ import "Governor.helpers.spec" └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ rule clockMode(env e) { + require e.block.number < max_uint48() && e.block.timestamp < max_uint48(); + assert clock(e) == e.block.number || clock(e) == e.block.timestamp; assert clock(e) == token_clock(e); + + /// This causes a failure in the prover + // assert CLOCK_MODE(e) == token_CLOCK_MODE(e); } /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -│ Invariant: Proposal is UNSET iff the proposer, the snapshot and the deadline are unset. │ +│ Invariant: Votes start and end 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) │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ -invariant createdConsistency(env e, uint256 pId) - safeState(e, pId) == UNSET() <=> proposalProposer(pId) == 0 && - safeState(e, pId) == UNSET() <=> proposalSnapshot(pId) == 0 && - safeState(e, pId) == UNSET() <=> proposalDeadline(pId) == 0 && - safeState(e, pId) == UNSET() => !isExecuted(pId) && - safeState(e, pId) == UNSET() => !isCanceled(pId) +invariant proposalStateConsistency(uint256 pId) + (proposalProposer(pId) == 0 <=> proposalSnapshot(pId) == 0) && + (proposalProposer(pId) == 0 <=> proposalDeadline(pId) == 0) { - preserved { + preserved with (env e) { + require clock(e) > 0; + } + } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: cancel => created │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant canceledImplyCreated(uint pId) + isCanceled(pId) => proposalCreated(pId) + { + preserved with (env e) { + requireInvariant proposalStateConsistency(pId); require clock(e) > 0; } } -invariant createdConsistencyWeak(uint256 pId) - proposalProposer(pId) == 0 <=> proposalSnapshot(pId) == 0 && - proposalProposer(pId) == 0 <=> proposalDeadline(pId) == 0 +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: executed => created │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant executedImplyCreated(uint pId) + isExecuted(pId) => proposalCreated(pId) { preserved with (env e) { + requireInvariant proposalStateConsistency(pId); + require clock(e) > 0; + } + } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: The state UNSET() correctly catched uninitialized proposal. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant proposalStateConsistencyUnset(env e, uint256 pId) + proposalCreated(pId) <=> safeState(e, pId) == UNSET() + { + preserved { require clock(e) > 0; } } @@ -47,7 +85,7 @@ invariant voteStartBeforeVoteEnd(uint256 pId) proposalSnapshot(pId) <= proposalDeadline(pId) { preserved { - requireInvariant createdConsistencyWeak(pId); + requireInvariant proposalStateConsistency(pId); } } @@ -61,21 +99,8 @@ invariant noBothExecutedAndCanceled(uint256 pId) /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -│ Rule: Once a proposal is created, voteStart, voteEnd and proposer are immutable │ +│ Invariant: the "governance call" dequeue is empty │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ -rule immutableFieldsAfterProposalCreation(uint256 pId, env e, method f, calldataarg arg) - filtered { f -> !skip(f) } -{ - require state(e, pId) != UNSET(); - - uint256 voteStart = proposalSnapshot(pId); - uint256 voteEnd = proposalDeadline(pId); - address proposer = proposalProposer(pId); - - f(e, arg); - - assert voteStart == proposalSnapshot(pId), "Start date was changed"; - assert voteEnd == proposalDeadline(pId), "End date was changed"; - assert proposer == proposalProposer(pId), "Proposer was changed"; -} +invariant governanceCallLength() + governanceCallLength() == 0 \ No newline at end of file diff --git a/certora/specs/GovernorStates.spec b/certora/specs/GovernorStates.spec index b7737ce24b6..0e548b6e63b 100644 --- a/certora/specs/GovernorStates.spec +++ b/certora/specs/GovernorStates.spec @@ -1,6 +1,9 @@ import "helpers.spec" import "methods/IGovernor.spec" import "Governor.helpers.spec" +import "GovernorInvariants.spec" + +use invariant proposalStateConsistency /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ @@ -78,3 +81,45 @@ rule stateTransitionWait(uint256 pId, env e1, env e2) { stateBefore == EXECUTED() => false ); } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: State corresponds to the vote timming and results │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule stateFollowsVoteTimmingAndResult(uint256 pId, env e) { + require clock(e) > 0; // Sanity + requireInvariant proposalStateConsistency(pId); + + uint8 currentState = state(e, pId); + uint48 currentClock = clock(e); + + // Pending = before vote starts + assert currentState == PENDING() => ( + proposalSnapshot(pId) >= currentClock + ); + + // Active = after vote starts & before vote ends + assert currentState == ACTIVE() => ( + proposalSnapshot(pId) < currentClock && + proposalDeadline(pId) >= currentClock + ); + + // Succeeded = after vote end, with vote successfull and quorum reached + assert currentState == SUCCEEDED() => ( + proposalDeadline(pId) < currentClock && + ( + quorumReached(pId) && + voteSucceeded(pId) + ) + ); + + // Succeeded = after vote end, with vote not successfull or quorum not reached + assert currentState == DEFEATED() => ( + proposalDeadline(pId) < currentClock && + ( + !quorumReached(pId) || + !voteSucceeded(pId) + ) + ); +} diff --git a/certora/specs/helpers.spec b/certora/specs/helpers.spec index 24842d62f13..20a11336ea3 100644 --- a/certora/specs/helpers.spec +++ b/certora/specs/helpers.spec @@ -1 +1,3 @@ definition nonpayable(env e) returns bool = e.msg.value == 0; + +definition max_uint48() returns uint48 = 0xffffffffffff; \ No newline at end of file diff --git a/certora/specs/methods/IGovernor.spec b/certora/specs/methods/IGovernor.spec index 53f43b71384..ea0308c18dd 100644 --- a/certora/specs/methods/IGovernor.spec +++ b/certora/specs/methods/IGovernor.spec @@ -39,6 +39,7 @@ methods { voteSucceeded(uint256) returns bool envfree isExecuted(uint256) returns bool envfree isCanceled(uint256) returns bool envfree + governanceCallLength() returns uint256 envfree getAgainstVotes(uint256) returns uint256 envfree getForVotes(uint256) returns uint256 envfree getAbstainVotes(uint256) returns uint256 envfree diff --git a/requirements.txt b/requirements.txt index 797b3598d37..da3e95766cb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -certora-cli==3.6.3 +certora-cli==3.6.4 From f71bc6899fd29960a371eec75cdf6ea24431ebfd Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 13 Mar 2023 14:31:38 +0100 Subject: [PATCH 08/61] clean --- certora/run.js | 1 - 1 file changed, 1 deletion(-) diff --git a/certora/run.js b/certora/run.js index 50e8a1dcfaf..f6ee0cff438 100644 --- a/certora/run.js +++ b/certora/run.js @@ -30,7 +30,6 @@ if (specs.length === 0) { process.exit(1); } -console.table(specs.map(spec => `${spec.contract}:${spec.spec} ${spec.options.join(' ')}`)) for (const { spec, contract, files, options = [] } of Object.values(specs)) { limit(runCertora, spec, contract, files, [...options.flatMap(opt => opt.split(' ')), ...extraOptions]); } From 4b11b4d3a6ffd20cc472e926ca26cfc9f02a315f Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 13 Mar 2023 14:54:39 +0100 Subject: [PATCH 09/61] codespell --- certora/specs/GovernorBaseRules.spec | 2 +- certora/specs/GovernorInvariants.spec | 2 +- certora/specs/GovernorStates.spec | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/certora/specs/GovernorBaseRules.spec b/certora/specs/GovernorBaseRules.spec index 3063adae41e..4c83991ec7d 100644 --- a/certora/specs/GovernorBaseRules.spec +++ b/certora/specs/GovernorBaseRules.spec @@ -74,7 +74,7 @@ rule executionOnlyIfQuoromReachedAndVoteSucceeded(uint256 pId, env e, method f, f(e, args); - assert isExecuted(pId) => (quorumReachedBefore && voteSucceededBefore), "quorum not met or vote not succesfull"; + assert isExecuted(pId) => (quorumReachedBefore && voteSucceededBefore), "quorum not met or vote not successful"; } /* diff --git a/certora/specs/GovernorInvariants.spec b/certora/specs/GovernorInvariants.spec index dfe95e81f24..0a6ad61eb5a 100644 --- a/certora/specs/GovernorInvariants.spec +++ b/certora/specs/GovernorInvariants.spec @@ -65,7 +65,7 @@ invariant executedImplyCreated(uint pId) /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -│ Invariant: The state UNSET() correctly catched uninitialized proposal. │ +│ Invariant: The state UNSET() correctly catch uninitialized proposal. │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ invariant proposalStateConsistencyUnset(env e, uint256 pId) diff --git a/certora/specs/GovernorStates.spec b/certora/specs/GovernorStates.spec index 0e548b6e63b..af5cef7921f 100644 --- a/certora/specs/GovernorStates.spec +++ b/certora/specs/GovernorStates.spec @@ -84,7 +84,7 @@ rule stateTransitionWait(uint256 pId, env e1, env e2) { /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -│ Rule: State corresponds to the vote timming and results │ +│ Rule: State corresponds to the vote timing and results │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ rule stateFollowsVoteTimmingAndResult(uint256 pId, env e) { @@ -105,7 +105,7 @@ rule stateFollowsVoteTimmingAndResult(uint256 pId, env e) { proposalDeadline(pId) >= currentClock ); - // Succeeded = after vote end, with vote successfull and quorum reached + // Succeeded = after vote end, with vote successful and quorum reached assert currentState == SUCCEEDED() => ( proposalDeadline(pId) < currentClock && ( @@ -114,7 +114,7 @@ rule stateFollowsVoteTimmingAndResult(uint256 pId, env e) { ) ); - // Succeeded = after vote end, with vote not successfull or quorum not reached + // Succeeded = after vote end, with vote not successful or quorum not reached assert currentState == DEFEATED() => ( proposalDeadline(pId) < currentClock && ( From c33e7bd3409c7839ba95b98f68ce4a6547cbb1d5 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 13 Mar 2023 17:26:38 +0100 Subject: [PATCH 10/61] update governor specs --- certora/diff/governance_Governor.sol.patch | 2 +- ...tensions_GovernorTimelockControl.sol.patch | 14 ++++++ certora/harnesses/GovernorHarness.sol | 4 ++ certora/specs.js | 38 ++++++++++----- certora/specs/Governor.helpers.spec | 18 +++++-- certora/specs/GovernorChanges.spec | 27 +++++++++++ certora/specs/GovernorFunctions.spec | 48 ++++++++++++++----- certora/specs/GovernorInvariants.spec | 21 ++++---- certora/specs/GovernorStates.spec | 17 +++---- certora/specs/methods/IGovernor.spec | 1 + 10 files changed, 144 insertions(+), 46 deletions(-) create mode 100644 certora/diff/governance_extensions_GovernorTimelockControl.sol.patch create mode 100644 certora/specs/GovernorChanges.spec diff --git a/certora/diff/governance_Governor.sol.patch b/certora/diff/governance_Governor.sol.patch index e3ad3e9b3b1..aa667d8d217 100644 --- a/certora/diff/governance_Governor.sol.patch +++ b/certora/diff/governance_Governor.sol.patch @@ -1,5 +1,5 @@ --- governance/Governor.sol 2023-03-07 10:48:47.730155491 +0100 -+++ governance/Governor.sol 2023-03-13 14:07:30.704202049 +0100 ++++ governance/Governor.sol 2023-03-13 15:30:49.107167674 +0100 @@ -216,6 +216,21 @@ return _proposals[proposalId].proposer; } diff --git a/certora/diff/governance_extensions_GovernorTimelockControl.sol.patch b/certora/diff/governance_extensions_GovernorTimelockControl.sol.patch new file mode 100644 index 00000000000..5d5cf28f694 --- /dev/null +++ b/certora/diff/governance_extensions_GovernorTimelockControl.sol.patch @@ -0,0 +1,14 @@ +--- governance/extensions/GovernorTimelockControl.sol 2023-03-07 10:48:47.733488857 +0100 ++++ governance/extensions/GovernorTimelockControl.sol 2023-03-13 16:18:10.255122179 +0100 +@@ -84,6 +84,11 @@ + return eta == 1 ? 0 : eta; // _DONE_TIMESTAMP (1) should be replaced with a 0 value + } + ++ // FV ++ function _proposalQueueId(uint256 proposalId) internal view returns (bytes32) { ++ return _timelockIds[proposalId]; ++ } ++ + /** + * @dev Function to queue a proposal to the timelock. + */ diff --git a/certora/harnesses/GovernorHarness.sol b/certora/harnesses/GovernorHarness.sol index 52a09416df1..a8a2652de3c 100644 --- a/certora/harnesses/GovernorHarness.sol +++ b/certora/harnesses/GovernorHarness.sol @@ -64,6 +64,10 @@ contract GovernorHarness is return _isCanceled(proposalId); } + function isQueued(uint256 proposalId) public view returns (bool) { + return _proposalQueueId(proposalId) != bytes32(0); + } + function governanceCallLength() public view returns (uint256) { return _governanceCallLength(); } diff --git a/certora/specs.js b/certora/specs.js index 630865542db..525afd51fd3 100644 --- a/certora/specs.js +++ b/certora/specs.js @@ -44,22 +44,36 @@ module.exports = [ }, // Governor ...product( - ['GovernorInvariants', 'GovernorBaseRules', 'GovernorStates'], + ['GovernorInvariants', 'GovernorBaseRules', 'GovernorChanges', 'GovernorStates'], ['ERC20VotesBlocknumberHarness', 'ERC20VotesTimestampHarness'], ).map(([spec, token]) => ({ spec, contract: 'GovernorHarness', - files: ['certora/harnesses/GovernorHarness.sol', `certora/harnesses/${token}.sol`], - options: [`--link GovernorHarness:token=${token}`, '--optimistic_loop', '--optimistic_hashing'], + files: [ + 'certora/harnesses/GovernorHarness.sol', + `certora/harnesses/${token}.sol`, + ], + options: [ + `--link GovernorHarness:token=${token}`, + '--optimistic_loop', + '--optimistic_hashing', + ], })), // WIP part - // ...product( - // ['GovernorFunctions'], - // ['ERC20VotesBlocknumberHarness'], - // ).map(([spec, token]) => ({ - // spec, - // contract: 'GovernorHarness', - // files: ['certora/harnesses/GovernorHarness.sol', `certora/harnesses/${token}.sol`], - // options: [`--link GovernorHarness:token=${token}`, '--optimistic_loop', '--optimistic_hashing'], - // })), + ...product( + ['GovernorFunctions'], + ['ERC20VotesBlocknumberHarness'], + ).map(([spec, token]) => ({ + spec, + contract: 'GovernorHarness', + files: [ + 'certora/harnesses/GovernorHarness.sol', + `certora/harnesses/${token}.sol`, + ], + options: [ + `--link GovernorHarness:token=${token}`, + '--optimistic_loop', + '--optimistic_hashing', + ], + })), ]; diff --git a/certora/specs/Governor.helpers.spec b/certora/specs/Governor.helpers.spec index f43e5606fc7..5e44b7fa11a 100644 --- a/certora/specs/Governor.helpers.spec +++ b/certora/specs/Governor.helpers.spec @@ -1,5 +1,18 @@ +import "helpers.spec" import "methods/IGovernor.spec" +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Sanity │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +function clockSanity(env e) returns bool { + return + e.block.number < max_uint48() && + e.block.timestamp < max_uint48() && + clock(e) > 0; +} + /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ States │ @@ -16,8 +29,7 @@ definition EXPIRED() returns uint8 = 6; definition EXECUTED() returns uint8 = 7; function safeState(env e, uint256 pId) returns uint8 { - uint8 result = state@withrevert(e, pId); - return lastReverted ? UNSET() : result; + return proposalCreated(pId) ? state(e, pId): UNSET(); } definition proposalCreated(uint256 pId) returns bool = @@ -122,4 +134,4 @@ function helperFunctionsWithRevert(env e, method f, uint256 pId) { { require false; } -} \ No newline at end of file +} diff --git a/certora/specs/GovernorChanges.spec b/certora/specs/GovernorChanges.spec new file mode 100644 index 00000000000..2404bbc618d --- /dev/null +++ b/certora/specs/GovernorChanges.spec @@ -0,0 +1,27 @@ +import "helpers.spec" +import "methods/IGovernor.spec" +import "Governor.helpers.spec" + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: internal variables can only change though specific functions calls │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule changes(uint256 pId, env e) { + require nonpayable(e); + require clockSanity(e); + + address user; + + bool isExecutedBefore = isExecuted(pId); + bool isCanceledBefore = isCanceled(pId); + bool isQueuedBefore = isQueued(pId); + bool hasVotedBefore = hasVoted(pId, user); + + method f; calldataarg args; f(e, args); + + assert isExecuted(pId) != isExecutedBefore => (!isExecutedBefore && f.selector == execute(address[],uint256[],bytes[],bytes32).selector); + assert isCanceled(pId) != isCanceledBefore => (!isCanceledBefore && f.selector == cancel(address[],uint256[],bytes[],bytes32).selector); + assert isQueued(pId) != isQueuedBefore => (!isQueuedBefore && f.selector == queue(address[],uint256[],bytes[],bytes32).selector); + assert hasVoted(pId, user) != hasVotedBefore => (!hasVotedBefore && voting(f)); +} diff --git a/certora/specs/GovernorFunctions.spec b/certora/specs/GovernorFunctions.spec index ec50c044a43..b834663b9c2 100644 --- a/certora/specs/GovernorFunctions.spec +++ b/certora/specs/GovernorFunctions.spec @@ -9,6 +9,7 @@ import "Governor.helpers.spec" */ rule propose(uint256 pId, env e) { require nonpayable(e); + require clockSanity(e); uint256 otherId; @@ -18,8 +19,8 @@ rule propose(uint256 pId, env e) { uint256 otherVoteEnd = proposalDeadline(otherId); address otherProposer = proposalProposer(otherId); - address[] targets; uint256[] values; bytes[] calldatas; string reason; - require pId == propose@withrevert(e, targets, values, calldatas, reason); + address[] targets; uint256[] values; bytes[] calldatas; string descr; + require pId == propose@withrevert(e, targets, values, calldatas, descr); bool success = !lastReverted; // liveness & double proposal @@ -49,6 +50,7 @@ rule castVote(uint256 pId, env e, method f) filtered { f -> voting(f) } { require nonpayable(e); + require clockSanity(e); uint8 support; address voter; @@ -96,17 +98,24 @@ rule castVote(uint256 pId, env e, method f) assert getAbstainVotes(otherId) != otherAbstainVotesBefore => (otherId == pId); } - +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: queue effect and liveness. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ rule queue(uint256 pId, env e) { require nonpayable(e); + require clockSanity(e); uint256 otherId; - uint8 stateBefore = state(e, pId); - uint8 otherStateBefore = state(e, otherId); + uint8 stateBefore = state(e, pId); + uint8 otherStateBefore = state(e, otherId); + bool queuedBefore = isQueued(pId) + bool otherQueuedBefore = isQueued(otherId) - address[] targets; uint256[] values; bytes[] calldatas; string reason; - require pId == queue@withrevert(e, targets, values, calldatas, reason); + address[] targets; uint256[] values; bytes[] calldatas; bytes32 descrHash; + require pId == queue@withrevert(e, targets, values, calldatas, descrHash); bool success = !lastReverted; // liveness @@ -115,22 +124,31 @@ rule queue(uint256 pId, env e) { // effect assert success => ( state(e, pId) == QUEUED() + !queuedBefore && + isQueued(pId) ); // no side-effect assert state(e, otherId) != otherStateBefore => otherId == pId; + assert isQueued(otherId) != queuedBefore => otherId == pId; } +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: execute effect and liveness. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ rule execute(uint256 pId, env e) { require nonpayable(e); + require clockSanity(e); uint256 otherId; uint8 stateBefore = state(e, pId); uint8 otherStateBefore = state(e, otherId); - address[] targets; uint256[] values; bytes[] calldatas; string reason; - require pId == execute@withrevert(e, targets, values, calldatas, reason); + address[] targets; uint256[] values; bytes[] calldatas; bytes32 descrHash; + require pId == execute@withrevert(e, targets, values, calldatas, descrHash); bool success = !lastReverted; // liveness: can't check full equivalence because of execution call reverts @@ -145,16 +163,22 @@ rule execute(uint256 pId, env e) { assert state(e, otherId) != otherStateBefore => otherId == pId; } +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: cancel (public) effect and liveness. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ rule cancel(uint256 pId, env e) { require nonpayable(e); + require clockSanity(e); uint256 otherId; uint8 stateBefore = state(e, pId); uint8 otherStateBefore = state(e, otherId); - address[] targets; uint256[] values; bytes[] calldatas; string reason; - require pId == cancel@withrevert(e, targets, values, calldatas, reason); + address[] targets; uint256[] values; bytes[] calldatas; bytes32 descrHash; + require pId == cancel@withrevert(e, targets, values, calldatas, descrHash); bool success = !lastReverted; // liveness @@ -170,4 +194,4 @@ rule cancel(uint256 pId, env e) { // no side-effect assert state(e, otherId) != otherStateBefore => otherId == pId; -} \ No newline at end of file +} diff --git a/certora/specs/GovernorInvariants.spec b/certora/specs/GovernorInvariants.spec index 0a6ad61eb5a..1247789fa7f 100644 --- a/certora/specs/GovernorInvariants.spec +++ b/certora/specs/GovernorInvariants.spec @@ -8,7 +8,7 @@ import "Governor.helpers.spec" └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ rule clockMode(env e) { - require e.block.number < max_uint48() && e.block.timestamp < max_uint48(); + require clockSanity(e); assert clock(e) == e.block.number || clock(e) == e.block.timestamp; assert clock(e) == token_clock(e); @@ -31,7 +31,7 @@ invariant proposalStateConsistency(uint256 pId) (proposalProposer(pId) == 0 <=> proposalDeadline(pId) == 0) { preserved with (env e) { - require clock(e) > 0; + require clockSanity(e); } } @@ -44,8 +44,8 @@ invariant canceledImplyCreated(uint pId) isCanceled(pId) => proposalCreated(pId) { preserved with (env e) { + require clockSanity(e); requireInvariant proposalStateConsistency(pId); - require clock(e) > 0; } } @@ -58,21 +58,22 @@ invariant executedImplyCreated(uint pId) isExecuted(pId) => proposalCreated(pId) { preserved with (env e) { + require clockSanity(e); requireInvariant proposalStateConsistency(pId); - require clock(e) > 0; } } /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -│ Invariant: The state UNSET() correctly catch uninitialized proposal. │ +│ Invariant: queued => created │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ -invariant proposalStateConsistencyUnset(env e, uint256 pId) - proposalCreated(pId) <=> safeState(e, pId) == UNSET() +invariant queuedImplyCreated(uint pId) + isQueued(pId) => proposalCreated(pId) { - preserved { - require clock(e) > 0; + preserved with (env e) { + require clockSanity(e); + requireInvariant proposalStateConsistency(pId); } } @@ -103,4 +104,4 @@ invariant noBothExecutedAndCanceled(uint256 pId) └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ invariant governanceCallLength() - governanceCallLength() == 0 \ No newline at end of file + governanceCallLength() == 0 diff --git a/certora/specs/GovernorStates.spec b/certora/specs/GovernorStates.spec index af5cef7921f..958d7ae47a8 100644 --- a/certora/specs/GovernorStates.spec +++ b/certora/specs/GovernorStates.spec @@ -31,7 +31,7 @@ rule stateConsistency(env e, uint256 pId) { rule stateTransitionFn(uint256 pId, env e, method f, calldataarg args) filtered { f -> !skip(f) } { - require clock(e) > 0; // Sanity + require clockSanity(e); uint8 stateBefore = state(e, pId); f(e, args); @@ -64,7 +64,8 @@ rule stateTransitionFn(uint256 pId, env e, method f, calldataarg args) └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ rule stateTransitionWait(uint256 pId, env e1, env e2) { - require clock(e1) > 0; // Sanity + require clockSanity(e1); + require clockSanity(e2); require clock(e2) > clock(e1); uint8 stateBefore = state(e1, pId); @@ -87,25 +88,25 @@ rule stateTransitionWait(uint256 pId, env e1, env e2) { │ Rule: State corresponds to the vote timing and results │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ -rule stateFollowsVoteTimmingAndResult(uint256 pId, env e) { - require clock(e) > 0; // Sanity +rule stateIsConsistentWithVotes(uint256 pId, env e) { + require clockSanity(e); requireInvariant proposalStateConsistency(pId); uint8 currentState = state(e, pId); uint48 currentClock = clock(e); - // Pending = before vote starts + // Pending: before vote starts assert currentState == PENDING() => ( proposalSnapshot(pId) >= currentClock ); - // Active = after vote starts & before vote ends + // Active: after vote starts & before vote ends assert currentState == ACTIVE() => ( proposalSnapshot(pId) < currentClock && proposalDeadline(pId) >= currentClock ); - // Succeeded = after vote end, with vote successful and quorum reached + // Succeeded: after vote end, with vote successful and quorum reached assert currentState == SUCCEEDED() => ( proposalDeadline(pId) < currentClock && ( @@ -114,7 +115,7 @@ rule stateFollowsVoteTimmingAndResult(uint256 pId, env e) { ) ); - // Succeeded = after vote end, with vote not successful or quorum not reached + // Defeated: after vote end, with vote not successful or quorum not reached assert currentState == DEFEATED() => ( proposalDeadline(pId) < currentClock && ( diff --git a/certora/specs/methods/IGovernor.spec b/certora/specs/methods/IGovernor.spec index ea0308c18dd..ba75f808908 100644 --- a/certora/specs/methods/IGovernor.spec +++ b/certora/specs/methods/IGovernor.spec @@ -39,6 +39,7 @@ methods { voteSucceeded(uint256) returns bool envfree isExecuted(uint256) returns bool envfree isCanceled(uint256) returns bool envfree + isQueued(uint256) returns bool envfree governanceCallLength() returns uint256 envfree getAgainstVotes(uint256) returns uint256 envfree getForVotes(uint256) returns uint256 envfree From b320e1ec4c3baa42e8f8760091c4633a510dfca6 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 13 Mar 2023 17:30:13 +0100 Subject: [PATCH 11/61] lint --- certora/specs.js | 27 +++++---------------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/certora/specs.js b/certora/specs.js index 525afd51fd3..8f2a128559a 100644 --- a/certora/specs.js +++ b/certora/specs.js @@ -49,31 +49,14 @@ module.exports = [ ).map(([spec, token]) => ({ spec, contract: 'GovernorHarness', - files: [ - 'certora/harnesses/GovernorHarness.sol', - `certora/harnesses/${token}.sol`, - ], - options: [ - `--link GovernorHarness:token=${token}`, - '--optimistic_loop', - '--optimistic_hashing', - ], + files: ['certora/harnesses/GovernorHarness.sol', `certora/harnesses/${token}.sol`], + options: [`--link GovernorHarness:token=${token}`, '--optimistic_loop', '--optimistic_hashing'], })), // WIP part - ...product( - ['GovernorFunctions'], - ['ERC20VotesBlocknumberHarness'], - ).map(([spec, token]) => ({ + ...product(['GovernorFunctions'], ['ERC20VotesBlocknumberHarness']).map(([spec, token]) => ({ spec, contract: 'GovernorHarness', - files: [ - 'certora/harnesses/GovernorHarness.sol', - `certora/harnesses/${token}.sol`, - ], - options: [ - `--link GovernorHarness:token=${token}`, - '--optimistic_loop', - '--optimistic_hashing', - ], + files: ['certora/harnesses/GovernorHarness.sol', `certora/harnesses/${token}.sol`], + options: [`--link GovernorHarness:token=${token}`, '--optimistic_loop', '--optimistic_hashing'], })), ]; From 704e265c414e1b4e1b848d3a0fba76ff9e2b32ec Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 14 Mar 2023 10:02:10 +0100 Subject: [PATCH 12/61] fix governor changes spec --- certora/specs/Governor.helpers.spec | 13 +++++++++++-- certora/specs/GovernorChanges.spec | 9 +++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/certora/specs/Governor.helpers.spec b/certora/specs/Governor.helpers.spec index 5e44b7fa11a..737310fbadb 100644 --- a/certora/specs/Governor.helpers.spec +++ b/certora/specs/Governor.helpers.spec @@ -54,6 +54,13 @@ definition voting(method f) returns bool = f.selector == castVoteWithReason(uint256,uint8,string).selector || f.selector == castVoteWithReasonAndParams(uint256,uint8,string,bytes).selector; +definition votingBySig(method f) returns bool = + f.selector == castVoteBySig(uint256,uint8,uint8,bytes32,bytes32).selector || + f.selector == castVoteWithReasonAndParamsBySig(uint256,uint8,string,bytes,uint8,bytes32,bytes32).selector; + +definition votingAll(method f) returns bool = + voting(f) || votingBySig(f); + /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ Helper functions │ @@ -79,7 +86,8 @@ function helperVoteWithRevert(env e, method f, uint256 pId, address voter, uint8 } else { - require false; + calldataarg args; + f(e, args); return 0; } } @@ -132,6 +140,7 @@ function helperFunctionsWithRevert(env e, method f, uint256 pId) { } else { - require false; + calldataarg args; + f(e, args); } } diff --git a/certora/specs/GovernorChanges.spec b/certora/specs/GovernorChanges.spec index 2404bbc618d..bd3ff681f50 100644 --- a/certora/specs/GovernorChanges.spec +++ b/certora/specs/GovernorChanges.spec @@ -22,6 +22,11 @@ rule changes(uint256 pId, env e) { assert isExecuted(pId) != isExecutedBefore => (!isExecutedBefore && f.selector == execute(address[],uint256[],bytes[],bytes32).selector); assert isCanceled(pId) != isCanceledBefore => (!isCanceledBefore && f.selector == cancel(address[],uint256[],bytes[],bytes32).selector); - assert isQueued(pId) != isQueuedBefore => (!isQueuedBefore && f.selector == queue(address[],uint256[],bytes[],bytes32).selector); - assert hasVoted(pId, user) != hasVotedBefore => (!hasVotedBefore && voting(f)); + assert hasVoted(pId, user) != hasVotedBefore => (!hasVotedBefore && votingAll(f)); + + // queue is cleared on cancel + assert isQueued(pId) != isQueuedBefore => ( + (!isQueuedBefore && f.selector == queue(address[],uint256[],bytes[],bytes32).selector) || + (isQueuedBefore && f.selector == cancel(address[],uint256[],bytes[],bytes32).selector) + ); } From e1120b91372556fc6b56b5dd7f506e929ce0e261 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 14 Mar 2023 10:23:49 +0100 Subject: [PATCH 13/61] try optimise GovernorStates --- certora/specs/GovernorFunctions.spec | 15 +++++++++------ certora/specs/GovernorStates.spec | 26 +++++++++++++++----------- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/certora/specs/GovernorFunctions.spec b/certora/specs/GovernorFunctions.spec index b834663b9c2..35fc5736518 100644 --- a/certora/specs/GovernorFunctions.spec +++ b/certora/specs/GovernorFunctions.spec @@ -111,8 +111,8 @@ rule queue(uint256 pId, env e) { uint8 stateBefore = state(e, pId); uint8 otherStateBefore = state(e, otherId); - bool queuedBefore = isQueued(pId) - bool otherQueuedBefore = isQueued(otherId) + bool queuedBefore = isQueued(pId); + bool otherQueuedBefore = isQueued(otherId); address[] targets; uint256[] values; bytes[] calldatas; bytes32 descrHash; require pId == queue@withrevert(e, targets, values, calldatas, descrHash); @@ -174,8 +174,9 @@ rule cancel(uint256 pId, env e) { uint256 otherId; - uint8 stateBefore = state(e, pId); - uint8 otherStateBefore = state(e, otherId); + uint8 stateBefore = state(e, pId); + uint8 otherStateBefore = state(e, otherId); + bool otherQueuedBefore = isQueued(otherId); address[] targets; uint256[] values; bytes[] calldatas; bytes32 descrHash; require pId == cancel@withrevert(e, targets, values, calldatas, descrHash); @@ -189,9 +190,11 @@ rule cancel(uint256 pId, env e) { // effect assert success => ( - state(e, pId) == CANCELED() + state(e, pId) == CANCELED() && + !isQueued(pId) // cancel resets timelockId ); // no side-effect - assert state(e, otherId) != otherStateBefore => otherId == pId; + assert state(e, otherId) != otherStateBefore => otherId == pId; + assert isQueued(otherId) != otherQueuedBefore => otherId == pId; } diff --git a/certora/specs/GovernorStates.spec b/certora/specs/GovernorStates.spec index 958d7ae47a8..417b7b0c6b0 100644 --- a/certora/specs/GovernorStates.spec +++ b/certora/specs/GovernorStates.spec @@ -92,35 +92,39 @@ rule stateIsConsistentWithVotes(uint256 pId, env e) { require clockSanity(e); requireInvariant proposalStateConsistency(pId); - uint8 currentState = state(e, pId); - uint48 currentClock = clock(e); + uint48 currentClock = clock(e); + uint8 currentState = state(e, pId); + uint256 snapshot = proposalSnapshot(pId); + uint256 deadline = proposalDeadline(pId); + bool quorumSuccess = quorumReached(pId); + bool voteSuccess = voteSucceeded(pId); // Pending: before vote starts assert currentState == PENDING() => ( - proposalSnapshot(pId) >= currentClock + snapshot >= currentClock ); // Active: after vote starts & before vote ends assert currentState == ACTIVE() => ( - proposalSnapshot(pId) < currentClock && - proposalDeadline(pId) >= currentClock + snapshot < currentClock && + deadline >= currentClock ); // Succeeded: after vote end, with vote successful and quorum reached assert currentState == SUCCEEDED() => ( - proposalDeadline(pId) < currentClock && + deadline < currentClock && ( - quorumReached(pId) && - voteSucceeded(pId) + quorumSuccess && + voteSuccess ) ); // Defeated: after vote end, with vote not successful or quorum not reached assert currentState == DEFEATED() => ( - proposalDeadline(pId) < currentClock && + deadline < currentClock && ( - !quorumReached(pId) || - !voteSucceeded(pId) + !quorumSuccess || + !voteSuccess ) ); } From 728e2c8899acf9c7b3e861dbcb9b3252898a9c2e Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 14 Mar 2023 11:49:16 +0100 Subject: [PATCH 14/61] fix --- certora/specs/GovernorFunctions.spec | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/certora/specs/GovernorFunctions.spec b/certora/specs/GovernorFunctions.spec index 35fc5736518..04d40565871 100644 --- a/certora/specs/GovernorFunctions.spec +++ b/certora/specs/GovernorFunctions.spec @@ -123,14 +123,14 @@ rule queue(uint256 pId, env e) { // effect assert success => ( - state(e, pId) == QUEUED() + state(e, pId) == QUEUED() && !queuedBefore && isQueued(pId) ); // no side-effect - assert state(e, otherId) != otherStateBefore => otherId == pId; - assert isQueued(otherId) != queuedBefore => otherId == pId; + assert state(e, otherId) != otherStateBefore => otherId == pId; + assert isQueued(otherId) != otherQueuedBefore => otherId == pId; } /* From 397f4cdfe237e4951f412cd2e90c24787d770d10 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 14 Mar 2023 15:51:22 +0100 Subject: [PATCH 15/61] filter functions that should revert --- certora/specs/GovernorBaseRules.spec | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/certora/specs/GovernorBaseRules.spec b/certora/specs/GovernorBaseRules.spec index 4c83991ec7d..992921c4aff 100644 --- a/certora/specs/GovernorBaseRules.spec +++ b/certora/specs/GovernorBaseRules.spec @@ -108,9 +108,11 @@ rule noExecuteBeforeDeadline(uint256 pId, env e, method f, calldataarg args) { │ attribute to the execute() function, showing that only execute() can change it, and that it will always change it. │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ -rule allFunctionsRevertIfExecuted(uint256 pId, env e, method f, calldataarg args) - filtered { f -> !skip(f) } -{ +rule allFunctionsRevertIfExecuted(uint256 pId, env e, method f, calldataarg args) filtered { f -> + !skip(f) && + f.selector != updateQuorumNumerator(uint256).selector && + f.selector != updateTimelock(address).selector +} { require isExecuted(pId); requireInvariant noBothExecutedAndCanceled(pId); requireInvariant executedImplyCreated(pId); @@ -129,9 +131,11 @@ rule allFunctionsRevertIfExecuted(uint256 pId, env e, method f, calldataarg args │ attribute to the execute() function, showing that only execute() can change it, and that it will always change it. │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ -rule allFunctionsRevertIfCanceled(uint256 pId, env e, method f, calldataarg args) - filtered { f -> !skip(f) } -{ +rule allFunctionsRevertIfCanceled(uint256 pId, env e, method f, calldataarg args) filtered { f -> + !skip(f) && + f.selector != updateQuorumNumerator(uint256).selector && + f.selector != updateTimelock(address).selector +} { require isCanceled(pId); requireInvariant noBothExecutedAndCanceled(pId); requireInvariant canceledImplyCreated(pId); From d7884251aaf7fed9514cdaaa229108e81c35b56e Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 14 Mar 2023 21:33:13 +0100 Subject: [PATCH 16/61] update --- certora/specs.js | 2 +- certora/specs/Governor.helpers.spec | 1 + certora/specs/GovernorBaseRules.spec | 28 ---------------------------- certora/specs/GovernorChanges.spec | 2 +- 4 files changed, 3 insertions(+), 30 deletions(-) diff --git a/certora/specs.js b/certora/specs.js index 19d62e150ba..3ab802a1452 100644 --- a/certora/specs.js +++ b/certora/specs.js @@ -47,7 +47,7 @@ module.exports = [ spec: 'TimelockController', contract: 'TimelockControllerHarness', files: ['certora/harnesses/TimelockControllerHarness.sol'], - options: ['--optimistic_hashing', '--optimistic_loop'] + options: ['--optimistic_hashing', '--optimistic_loop'], }, // Governor ...product( diff --git a/certora/specs/Governor.helpers.spec b/certora/specs/Governor.helpers.spec index 737310fbadb..8bc0d831080 100644 --- a/certora/specs/Governor.helpers.spec +++ b/certora/specs/Governor.helpers.spec @@ -68,6 +68,7 @@ definition votingAll(method f) returns bool = */ function helperVoteWithRevert(env e, method f, uint256 pId, address voter, uint8 support) returns uint256 { string reason; bytes params; + require reason.length >= 0; if (f.selector == castVote(uint256,uint8).selector) { diff --git a/certora/specs/GovernorBaseRules.spec b/certora/specs/GovernorBaseRules.spec index 992921c4aff..405a1085c8c 100644 --- a/certora/specs/GovernorBaseRules.spec +++ b/certora/specs/GovernorBaseRules.spec @@ -144,31 +144,3 @@ rule allFunctionsRevertIfCanceled(uint256 pId, env e, method f, calldataarg args assert lastReverted, "Function was not reverted"; } - -/* -┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -│ Rule: Proposal can be switched state only by specific functions │ -└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -*/ -rule stateOnlyAfterFunc(uint256 pId, env e, method f) { - bool createdBefore = proposalCreated(pId); - bool executedBefore = isExecuted(pId); - bool canceledBefore = isCanceled(pId); - - helperFunctionsWithRevert(e, f, pId); - - assert (proposalCreated(pId) != createdBefore) => ( - createdBefore == false && - f.selector == propose(address[], uint256[], bytes[], string).selector - ), "proposalCreated only changes in the propose method"; - - assert (isExecuted(pId) != executedBefore) => ( - executedBefore == false && - f.selector == execute(address[], uint256[], bytes[], bytes32).selector - ), "isExecuted only changes in the execute method"; - - assert (isCanceled(pId) != canceledBefore) => ( - canceledBefore == false && - f.selector == cancel(address[], uint256[], bytes[], bytes32).selector - ), "isCanceled only changes in the cancel method"; -} diff --git a/certora/specs/GovernorChanges.spec b/certora/specs/GovernorChanges.spec index bd3ff681f50..c521f7995c1 100644 --- a/certora/specs/GovernorChanges.spec +++ b/certora/specs/GovernorChanges.spec @@ -4,7 +4,7 @@ import "Governor.helpers.spec" /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -│ Rule: internal variables can only change though specific functions calls │ +│ Rule: Proposal can be switched state only by specific functions │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ rule changes(uint256 pId, env e) { From 0d4df8972e3a280d25dff7af85521f59bc71ef02 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 14 Mar 2023 22:12:14 +0100 Subject: [PATCH 17/61] add filter to improve prover perf --- certora/diff/governance_Governor.sol.patch | 4 ++-- ...tensions_GovernorTimelockControl.sol.patch | 4 ++-- certora/diff/token_ERC721_ERC721.sol.patch | 2 +- certora/specs/GovernorBaseRules.spec | 18 +++++++++++++----- certora/specs/GovernorChanges.spec | 19 +++++++++++++------ 5 files changed, 31 insertions(+), 16 deletions(-) diff --git a/certora/diff/governance_Governor.sol.patch b/certora/diff/governance_Governor.sol.patch index aa667d8d217..1e9d6321544 100644 --- a/certora/diff/governance_Governor.sol.patch +++ b/certora/diff/governance_Governor.sol.patch @@ -1,5 +1,5 @@ --- governance/Governor.sol 2023-03-07 10:48:47.730155491 +0100 -+++ governance/Governor.sol 2023-03-13 15:30:49.107167674 +0100 ++++ governance/Governor.sol 2023-03-14 22:09:12.664754077 +0100 @@ -216,6 +216,21 @@ return _proposals[proposalId].proposer; } @@ -15,7 +15,7 @@ + } + + // FV -+ function _governanceCallLength() public view returns (uint256) { ++ function _governanceCallLength() internal view returns (uint256) { + return _governanceCall.length(); + } + diff --git a/certora/diff/governance_extensions_GovernorTimelockControl.sol.patch b/certora/diff/governance_extensions_GovernorTimelockControl.sol.patch index 5d5cf28f694..57e213f3461 100644 --- a/certora/diff/governance_extensions_GovernorTimelockControl.sol.patch +++ b/certora/diff/governance_extensions_GovernorTimelockControl.sol.patch @@ -1,5 +1,5 @@ ---- governance/extensions/GovernorTimelockControl.sol 2023-03-07 10:48:47.733488857 +0100 -+++ governance/extensions/GovernorTimelockControl.sol 2023-03-13 16:18:10.255122179 +0100 +--- governance/extensions/GovernorTimelockControl.sol 2023-03-14 15:48:49.307543354 +0100 ++++ governance/extensions/GovernorTimelockControl.sol 2023-03-14 22:09:12.661420438 +0100 @@ -84,6 +84,11 @@ return eta == 1 ? 0 : eta; // _DONE_TIMESTAMP (1) should be replaced with a 0 value } diff --git a/certora/diff/token_ERC721_ERC721.sol.patch b/certora/diff/token_ERC721_ERC721.sol.patch index 312fc4ff672..1623beae99b 100644 --- a/certora/diff/token_ERC721_ERC721.sol.patch +++ b/certora/diff/token_ERC721_ERC721.sol.patch @@ -1,5 +1,5 @@ --- token/ERC721/ERC721.sol 2023-03-07 10:48:47.736822221 +0100 -+++ token/ERC721/ERC721.sol 2023-03-09 19:50:20.555856358 +0100 ++++ token/ERC721/ERC721.sol 2023-03-14 22:09:12.654753162 +0100 @@ -199,6 +199,11 @@ return _owners[tokenId]; } diff --git a/certora/specs/GovernorBaseRules.spec b/certora/specs/GovernorBaseRules.spec index 405a1085c8c..ebf5e9cf470 100644 --- a/certora/specs/GovernorBaseRules.spec +++ b/certora/specs/GovernorBaseRules.spec @@ -26,14 +26,16 @@ rule noDoublePropose(uint256 pId, env e) { │ Rule: Once a proposal is created, voteStart, voteEnd and proposer are immutable │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ -rule immutableFieldsAfterProposalCreation(uint256 pId, env e, method f, calldataarg arg) { +rule immutableFieldsAfterProposalCreation(uint256 pId, env e, method f, calldataarg args) + filtered { f -> !skip(f) } +{ require proposalCreated(pId); uint256 voteStart = proposalSnapshot(pId); uint256 voteEnd = proposalDeadline(pId); address proposer = proposalProposer(pId); - f(e, arg); + f(e, args); assert voteStart == proposalSnapshot(pId), "Start date was changed"; assert voteEnd == proposalDeadline(pId), "End date was changed"; @@ -66,7 +68,9 @@ rule noDoubleVoting(uint256 pId, env e, uint8 sup) { │ Rule: A proposal could be executed only if quorum was reached and vote succeeded │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ -rule executionOnlyIfQuoromReachedAndVoteSucceeded(uint256 pId, env e, method f, calldataarg args) { +rule executionOnlyIfQuoromReachedAndVoteSucceeded(uint256 pId, env e, method f, calldataarg args) + filtered { f -> !skip(f) } +{ require !isExecuted(pId); bool quorumReachedBefore = quorumReached(pId); @@ -82,7 +86,9 @@ rule executionOnlyIfQuoromReachedAndVoteSucceeded(uint256 pId, env e, method f, │ Rule: Voting cannot start at a block number prior to proposal’s creation block number │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ -rule noStartBeforeCreation(uint256 pId, env e, method f, calldataarg args){ +rule noStartBeforeCreation(uint256 pId, env e, method f, calldataarg args) + filtered { f -> !skip(f) } +{ require !proposalCreated(pId); f(e, args); assert proposalCreated(pId) => proposalSnapshot(pId) >= clock(e), "starts before proposal"; @@ -93,7 +99,9 @@ rule noStartBeforeCreation(uint256 pId, env e, method f, calldataarg args){ │ Rule: A proposal cannot be executed before it ends │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ -rule noExecuteBeforeDeadline(uint256 pId, env e, method f, calldataarg args) { +rule noExecuteBeforeDeadline(uint256 pId, env e, method f, calldataarg args) + filtered { f -> !skip(f) } +{ require !isExecuted(pId); f(e, args); assert isExecuted(pId) => proposalDeadline(pId) <= clock(e), "executed before deadline"; diff --git a/certora/specs/GovernorChanges.spec b/certora/specs/GovernorChanges.spec index c521f7995c1..a1d9922e305 100644 --- a/certora/specs/GovernorChanges.spec +++ b/certora/specs/GovernorChanges.spec @@ -1,28 +1,35 @@ import "helpers.spec" import "methods/IGovernor.spec" import "Governor.helpers.spec" +import "GovernorInvariants.spec" + +use invariant proposalStateConsistency /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ Rule: Proposal can be switched state only by specific functions │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ -rule changes(uint256 pId, env e) { - require nonpayable(e); +rule changes(uint256 pId, env e, method f, calldataarg args) + filtered { f -> !skip(f) } +{ require clockSanity(e); + requireInvariant proposalStateConsistency(pId); address user; + bool existBefore = proposalCreated(pId); bool isExecutedBefore = isExecuted(pId); bool isCanceledBefore = isCanceled(pId); bool isQueuedBefore = isQueued(pId); bool hasVotedBefore = hasVoted(pId, user); - method f; calldataarg args; f(e, args); + f(e, args); - assert isExecuted(pId) != isExecutedBefore => (!isExecutedBefore && f.selector == execute(address[],uint256[],bytes[],bytes32).selector); - assert isCanceled(pId) != isCanceledBefore => (!isCanceledBefore && f.selector == cancel(address[],uint256[],bytes[],bytes32).selector); - assert hasVoted(pId, user) != hasVotedBefore => (!hasVotedBefore && votingAll(f)); + assert proposalCreated(pId) != existBefore => (!existBefore && f.selector == propose(address[],uint256[],bytes[],string).selector); + assert isExecuted(pId) != isExecutedBefore => (!isExecutedBefore && f.selector == execute(address[],uint256[],bytes[],bytes32).selector); + assert isCanceled(pId) != isCanceledBefore => (!isCanceledBefore && f.selector == cancel(address[],uint256[],bytes[],bytes32).selector); + assert hasVoted(pId, user) != hasVotedBefore => (!hasVotedBefore && votingAll(f)); // queue is cleared on cancel assert isQueued(pId) != isQueuedBefore => ( From 198c4b7728420be261002741f4f06fd19a7a8ac3 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 15 Mar 2023 11:54:31 +0100 Subject: [PATCH 18/61] update --- certora/harnesses/GovernorHarness.sol | 15 --------- certora/specs/Governor.helpers.spec | 48 +++++++++++++++++---------- certora/specs/GovernorBaseRules.spec | 18 ++++++++++ certora/specs/GovernorChanges.spec | 38 +++++++++++++++------ certora/specs/GovernorFunctions.spec | 6 +++- certora/specs/methods/IGovernor.spec | 6 ++++ 6 files changed, 87 insertions(+), 44 deletions(-) diff --git a/certora/harnesses/GovernorHarness.sol b/certora/harnesses/GovernorHarness.sol index a8a2652de3c..e758d38102c 100644 --- a/certora/harnesses/GovernorHarness.sol +++ b/certora/harnesses/GovernorHarness.sol @@ -88,21 +88,6 @@ contract GovernorHarness is return abstainVotes; } - /// The following functions are overrides required by Solidity added by Certora. - // mapping(uint256 => uint256) public ghost_sum_vote_power_by_id; - // - // function _castVote( - // uint256 proposalId, - // address account, - // uint8 support, - // string memory reason, - // bytes memory params - // ) internal virtual override returns (uint256) { - // uint256 deltaWeight = super._castVote(proposalId, account, support, reason, params); - // ghost_sum_vote_power_by_id[proposalId] += deltaWeight; - // return deltaWeight; - // } - // The following functions are overrides required by Solidity added by OZ Wizard. function votingDelay() public pure override returns (uint256) { return 1; // 1 block diff --git a/certora/specs/Governor.helpers.spec b/certora/specs/Governor.helpers.spec index 8bc0d831080..59fe8373a4c 100644 --- a/certora/specs/Governor.helpers.spec +++ b/certora/specs/Governor.helpers.spec @@ -7,10 +7,23 @@ import "methods/IGovernor.spec" └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ function clockSanity(env e) returns bool { - return - e.block.number < max_uint48() && - e.block.timestamp < max_uint48() && - clock(e) > 0; + return e.block.number < max_uint48() + && e.block.timestamp < max_uint48() + && clock(e) > 0; +} + +function validProposal(address[] targets, uint256[] values, bytes[] calldatas) returns bool { + return targets.length > 0 + && targets.length == values.length + && targets.length == calldatas.length; +} + +function validString(string s) returns bool { + return s.length < 0xffff; +} + +function validBytes(bytes b) returns bool { + return b.length < 0xffff; } /* @@ -18,15 +31,15 @@ function clockSanity(env e) returns bool { │ States │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ -definition UNSET() returns uint8 = 255; -definition PENDING() returns uint8 = 0; -definition ACTIVE() returns uint8 = 1; -definition CANCELED() returns uint8 = 2; -definition DEFEATED() returns uint8 = 3; -definition SUCCEEDED() returns uint8 = 4; -definition QUEUED() returns uint8 = 5; -definition EXPIRED() returns uint8 = 6; -definition EXECUTED() returns uint8 = 7; +definition UNSET() returns uint8 = 255; +definition PENDING() returns uint8 = 0; +definition ACTIVE() returns uint8 = 1; +definition CANCELED() returns uint8 = 2; +definition DEFEATED() returns uint8 = 3; +definition SUCCEEDED() returns uint8 = 4; +definition QUEUED() returns uint8 = 5; +definition EXPIRED() returns uint8 = 6; +definition EXECUTED() returns uint8 = 7; function safeState(env e, uint256 pId) returns uint8 { return proposalCreated(pId) ? state(e, pId): UNSET(); @@ -67,9 +80,6 @@ definition votingAll(method f) returns bool = └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ function helperVoteWithRevert(env e, method f, uint256 pId, address voter, uint8 support) returns uint256 { - string reason; bytes params; - require reason.length >= 0; - if (f.selector == castVote(uint256,uint8).selector) { require e.msg.sender == voter; @@ -77,12 +87,14 @@ function helperVoteWithRevert(env e, method f, uint256 pId, address voter, uint8 } else if (f.selector == castVoteWithReason(uint256,uint8,string).selector) { - require e.msg.sender == voter; + string reason; + require e.msg.sender == voter && validString(reason); return castVoteWithReason@withrevert(e, pId, support, reason); } else if (f.selector == castVoteWithReasonAndParams(uint256,uint8,string,bytes).selector) { - require e.msg.sender == voter; + string reason; bytes params; + require e.msg.sender == voter && validString(reason) && validBytes(params); return castVoteWithReasonAndParams@withrevert(e, pId, support, reason, params); } else diff --git a/certora/specs/GovernorBaseRules.spec b/certora/specs/GovernorBaseRules.spec index ebf5e9cf470..a49206b56c3 100644 --- a/certora/specs/GovernorBaseRules.spec +++ b/certora/specs/GovernorBaseRules.spec @@ -152,3 +152,21 @@ rule allFunctionsRevertIfCanceled(uint256 pId, env e, method f, calldataarg args assert lastReverted, "Function was not reverted"; } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: Update operation are restricted to executor │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule privilegedUpdate(env e, method f, calldataarg args) + filtered { f -> !skip(f) } +{ + address executorBefore = getExecutor(); + uint256 quorumNumeratorBefore = quorumNumerator(); + address timelockBefore = timelock(); + + f(e, args); + + assert quorumNumerator() != quorumNumeratorBefore => e.msg.sender == executorBefore; + assert timelock() != timelockBefore => e.msg.sender == executorBefore; +} diff --git a/certora/specs/GovernorChanges.spec b/certora/specs/GovernorChanges.spec index a1d9922e305..0fff10a5ad9 100644 --- a/certora/specs/GovernorChanges.spec +++ b/certora/specs/GovernorChanges.spec @@ -18,22 +18,40 @@ rule changes(uint256 pId, env e, method f, calldataarg args) address user; - bool existBefore = proposalCreated(pId); - bool isExecutedBefore = isExecuted(pId); - bool isCanceledBefore = isCanceled(pId); - bool isQueuedBefore = isQueued(pId); - bool hasVotedBefore = hasVoted(pId, user); + bool existBefore = proposalCreated(pId); + bool isExecutedBefore = isExecuted(pId); + bool isCanceledBefore = isCanceled(pId); + bool isQueuedBefore = isQueued(pId); + bool hasVotedBefore = hasVoted(pId, user); + uint256 votesAgainstBefore = getAgainstVotes(pId); + uint256 votesForBefore = getForVotes(pId); + uint256 votesAbstainBefore = getAbstainVotes(pId); f(e, args); - assert proposalCreated(pId) != existBefore => (!existBefore && f.selector == propose(address[],uint256[],bytes[],string).selector); - assert isExecuted(pId) != isExecutedBefore => (!isExecutedBefore && f.selector == execute(address[],uint256[],bytes[],bytes32).selector); - assert isCanceled(pId) != isCanceledBefore => (!isCanceledBefore && f.selector == cancel(address[],uint256[],bytes[],bytes32).selector); - assert hasVoted(pId, user) != hasVotedBefore => (!hasVotedBefore && votingAll(f)); + bool existAfter = proposalCreated(pId); + bool isExecutedAfter = isExecuted(pId); + bool isCanceledAfter = isCanceled(pId); + bool isQueuedAfter = isQueued(pId); + bool hasVotedAfter = hasVoted(pId, user); + uint256 votesAgainstAfter = getAgainstVotes(pId); + uint256 votesForAfter = getForVotes(pId); + uint256 votesAbstainAfter = getAbstainVotes(pId); + + // propose, execute, cancel + assert existAfter != existBefore => (!existBefore && f.selector == propose(address[],uint256[],bytes[],string).selector); + assert isExecutedAfter != isExecutedBefore => (!isExecutedBefore && f.selector == execute(address[],uint256[],bytes[],bytes32).selector); + assert isCanceledAfter != isCanceledBefore => (!isCanceledBefore && f.selector == cancel(address[],uint256[],bytes[],bytes32).selector); // queue is cleared on cancel - assert isQueued(pId) != isQueuedBefore => ( + assert isQueuedAfter != isQueuedBefore => ( (!isQueuedBefore && f.selector == queue(address[],uint256[],bytes[],bytes32).selector) || (isQueuedBefore && f.selector == cancel(address[],uint256[],bytes[],bytes32).selector) ); + + // votes + assert hasVotedAfter != hasVotedBefore => (!hasVotedBefore && votingAll(f)); + assert votesAgainstAfter != votesAgainstBefore => (votesAgainstAfter > votesAgainstBefore && votingAll(f)); + assert votesForAfter != votesForBefore => (votesForAfter > votesForBefore && votingAll(f)); + assert votesAbstainAfter != votesAbstainBefore => (votesAbstainAfter > votesAbstainBefore && votingAll(f)); } diff --git a/certora/specs/GovernorFunctions.spec b/certora/specs/GovernorFunctions.spec index 04d40565871..1c404e907a2 100644 --- a/certora/specs/GovernorFunctions.spec +++ b/certora/specs/GovernorFunctions.spec @@ -20,11 +20,15 @@ rule propose(uint256 pId, env e) { address otherProposer = proposalProposer(otherId); address[] targets; uint256[] values; bytes[] calldatas; string descr; + require validString(descr); require pId == propose@withrevert(e, targets, values, calldatas, descr); bool success = !lastReverted; // liveness & double proposal - assert success <=> stateBefore == UNSET(); + assert success <=> ( + stateBefore == UNSET() && + validProposal(targets, values, calldatas) + ); // effect assert success => ( diff --git a/certora/specs/methods/IGovernor.spec b/certora/specs/methods/IGovernor.spec index ba75f808908..b3e1017d846 100644 --- a/certora/specs/methods/IGovernor.spec +++ b/certora/specs/methods/IGovernor.spec @@ -2,6 +2,8 @@ methods { name() returns string envfree version() returns string envfree + token() returns address envfree + timelock() returns address envfree clock() returns uint48 CLOCK_MODE() returns string COUNTING_MODE() returns string envfree @@ -16,6 +18,9 @@ methods { getVotes(address,uint256) returns uint256 envfree getVotesWithParams(address,uint256,bytes) returns uint256 envfree hasVoted(uint256,address) returns bool envfree + quorumNumerator() returns uint256 envfree + quorumNumerator(uint256) returns uint256 envfree + quorumDenominator() returns uint256 envfree propose(address[],uint256[],bytes[],string) returns uint256 execute(address[],uint256[],bytes[],bytes32) returns uint256 @@ -27,6 +32,7 @@ methods { castVoteBySig(uint256,uint8,uint8,bytes32,bytes32) returns uint256 castVoteWithReasonAndParamsBySig(uint256,uint8,string,bytes,uint8,bytes32,bytes32) returns uint256 updateQuorumNumerator(uint256) + updateTimelock(address) // harness token_getPastTotalSupply(uint256) returns uint256 envfree From 0874adbd1f9b2195df53bae42f9b828ec964984b Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 15 Mar 2023 14:16:37 +0100 Subject: [PATCH 19/61] spacing --- certora/specs/helpers.spec | 2 +- certora/specs/methods/IGovernor.spec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/certora/specs/helpers.spec b/certora/specs/helpers.spec index 20a11336ea3..53d115c82dd 100644 --- a/certora/specs/helpers.spec +++ b/certora/specs/helpers.spec @@ -1,3 +1,3 @@ definition nonpayable(env e) returns bool = e.msg.value == 0; -definition max_uint48() returns uint48 = 0xffffffffffff; \ No newline at end of file +definition max_uint48() returns uint48 = 0xffffffffffff; diff --git a/certora/specs/methods/IGovernor.spec b/certora/specs/methods/IGovernor.spec index b3e1017d846..f44c2f06516 100644 --- a/certora/specs/methods/IGovernor.spec +++ b/certora/specs/methods/IGovernor.spec @@ -50,4 +50,4 @@ methods { getAgainstVotes(uint256) returns uint256 envfree getForVotes(uint256) returns uint256 envfree getAbstainVotes(uint256) returns uint256 envfree -} \ No newline at end of file +} From 4ea73a8c053ed75519187fd5560217e78e5fface Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 15 Mar 2023 16:55:14 +0100 Subject: [PATCH 20/61] add PreventLateQuorum specs --- ...nsions_GovernorPreventLateQuorum.sol.patch | 14 ++ .../harnesses/GovernorPreventLateHarness.sol | 176 ++++++++++++++++++ certora/specs.js | 7 + certora/specs/Governor.helpers.spec | 1 - certora/specs/GovernorBaseRules.spec | 45 ++++- certora/specs/GovernorInvariants.spec | 15 ++ certora/specs/GovernorPreventLateQuorum.spec | 68 +++++++ certora/specs/GovernorStates.spec | 20 ++ 8 files changed, 342 insertions(+), 4 deletions(-) create mode 100644 certora/diff/governance_extensions_GovernorPreventLateQuorum.sol.patch create mode 100644 certora/harnesses/GovernorPreventLateHarness.sol create mode 100644 certora/specs/GovernorPreventLateQuorum.spec diff --git a/certora/diff/governance_extensions_GovernorPreventLateQuorum.sol.patch b/certora/diff/governance_extensions_GovernorPreventLateQuorum.sol.patch new file mode 100644 index 00000000000..776a1539402 --- /dev/null +++ b/certora/diff/governance_extensions_GovernorPreventLateQuorum.sol.patch @@ -0,0 +1,14 @@ +--- governance/extensions/GovernorPreventLateQuorum.sol 2023-03-07 10:48:47.733488857 +0100 ++++ governance/extensions/GovernorPreventLateQuorum.sol 2023-03-15 14:14:59.121060484 +0100 +@@ -84,6 +84,11 @@ + return _voteExtension; + } + ++ // FV ++ function _getExtendedDeadline(uint256 proposalId) internal view returns (uint64) { ++ return _extendedDeadlines[proposalId]; ++ } ++ + /** + * @dev Changes the {lateQuorumVoteExtension}. This operation can only be performed by the governance executor, + * generally through a governance proposal. diff --git a/certora/harnesses/GovernorPreventLateHarness.sol b/certora/harnesses/GovernorPreventLateHarness.sol new file mode 100644 index 00000000000..048ceae3c59 --- /dev/null +++ b/certora/harnesses/GovernorPreventLateHarness.sol @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +import "../patched/governance/Governor.sol"; +import "../patched/governance/extensions/GovernorCountingSimple.sol"; +import "../patched/governance/extensions/GovernorPreventLateQuorum.sol"; +import "../patched/governance/extensions/GovernorTimelockControl.sol"; +import "../patched/governance/extensions/GovernorVotes.sol"; +import "../patched/governance/extensions/GovernorVotesQuorumFraction.sol"; +import "../patched/token/ERC20/extensions/ERC20Votes.sol"; + +contract GovernorPreventLateHarness is + Governor, + GovernorCountingSimple, + GovernorPreventLateQuorum, + GovernorTimelockControl, + GovernorVotes, + GovernorVotesQuorumFraction +{ + constructor(IVotes _token, TimelockController _timelock, uint256 _quorumNumeratorValue, uint64 _initialVoteExtension) + Governor("Harness") + GovernorPreventLateQuorum(_initialVoteExtension) + GovernorTimelockControl(_timelock) + GovernorVotes(_token) + GovernorVotesQuorumFraction(_quorumNumeratorValue) + {} + + // Harness from Votes + function token_getPastTotalSupply(uint256 blockNumber) public view returns(uint256) { + return token.getPastTotalSupply(blockNumber); + } + + function token_getPastVotes(address account, uint256 blockNumber) public view returns(uint256) { + return token.getPastVotes(account, blockNumber); + } + + function token_clock() public view returns (uint48) { + return token.clock(); + } + + function token_CLOCK_MODE() public view returns (string memory) { + return token.CLOCK_MODE(); + } + + // Harness from Governor + function getExecutor() public view returns (address) { + return _executor(); + } + + function proposalProposer(uint256 proposalId) public view returns (address) { + return _proposalProposer(proposalId); + } + + function quorumReached(uint256 proposalId) public view returns (bool) { + return _quorumReached(proposalId); + } + + function voteSucceeded(uint256 proposalId) public view returns (bool) { + return _voteSucceeded(proposalId); + } + + function isExecuted(uint256 proposalId) public view returns (bool) { + return _isExecuted(proposalId); + } + + function isCanceled(uint256 proposalId) public view returns (bool) { + return _isCanceled(proposalId); + } + + function isQueued(uint256 proposalId) public view returns (bool) { + return _proposalQueueId(proposalId) != bytes32(0); + } + + function governanceCallLength() public view returns (uint256) { + return _governanceCallLength(); + } + + // Harness from GovernorPreventLateQuorum + function getExtendedDeadline(uint256 proposalId) public view returns (uint64) { + return _getExtendedDeadline(proposalId); + } + + // Harness from GovernorCountingSimple + function getAgainstVotes(uint256 proposalId) public view returns (uint256) { + (uint256 againstVotes,,) = proposalVotes(proposalId); + return againstVotes; + } + + function getForVotes(uint256 proposalId) public view returns (uint256) { + (,uint256 forVotes,) = proposalVotes(proposalId); + return forVotes; + } + + function getAbstainVotes(uint256 proposalId) public view returns (uint256) { + (,,uint256 abstainVotes) = proposalVotes(proposalId); + return abstainVotes; + } + + // The following functions are overrides required by Solidity added by OZ Wizard. + function votingDelay() public pure override returns (uint256) { + return 1; // 1 block + } + + function votingPeriod() public pure override returns (uint256) { + return 45818; // 1 week + } + + function quorum(uint256 blockNumber) + public + view + override(IGovernor, GovernorVotesQuorumFraction) + returns (uint256) + { + return super.quorum(blockNumber); + } + + function state(uint256 proposalId) public view override(Governor, GovernorTimelockControl) returns (ProposalState) { + return super.state(proposalId); + } + + function proposalDeadline(uint256 proposalId) public view override(IGovernor, Governor, GovernorPreventLateQuorum) returns (uint256) { + return super.proposalDeadline(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, 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 _castVote( + uint256 proposalId, + address account, + uint8 support, + string memory reason, + bytes memory params + ) internal override(Governor, GovernorPreventLateQuorum) returns (uint256) { + return super._castVote(proposalId, account, support, reason, params); + } + + function _executor() internal view override(Governor, GovernorTimelockControl) returns (address) { + return super._executor(); + } + + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(Governor, GovernorTimelockControl) + returns (bool) + { + return super.supportsInterface(interfaceId); + } +} diff --git a/certora/specs.js b/certora/specs.js index 3ab802a1452..e013e88915b 100644 --- a/certora/specs.js +++ b/certora/specs.js @@ -66,4 +66,11 @@ module.exports = [ files: ['certora/harnesses/GovernorHarness.sol', `certora/harnesses/${token}.sol`], options: [`--link GovernorHarness:token=${token}`, '--optimistic_loop', '--optimistic_hashing'], })), + // WIP prevent late quorum + ...product(['GovernorPreventLateQuorum'], ['ERC20VotesBlocknumberHarness']).map(([spec, token]) => ({ + spec, + contract: 'GovernorPreventLateHarness', + files: ['certora/harnesses/GovernorPreventLateHarness.sol', `certora/harnesses/${token}.sol`], + options: [`--link GovernorPreventLateHarness:token=${token}`, '--optimistic_loop', '--optimistic_hashing'], + })), ]; diff --git a/certora/specs/Governor.helpers.spec b/certora/specs/Governor.helpers.spec index 59fe8373a4c..ec7b610a7e1 100644 --- a/certora/specs/Governor.helpers.spec +++ b/certora/specs/Governor.helpers.spec @@ -57,7 +57,6 @@ definition skip(method f) returns bool = f.isView || f.isFallback || f.selector == relay(address,uint256,bytes).selector || - f.selector == 0xb9a61961 || // __acceptAdmin() f.selector == onERC721Received(address,address,uint256,bytes).selector || f.selector == onERC1155Received(address,address,uint256,uint256,bytes).selector || f.selector == onERC1155BatchReceived(address,address,uint256[],uint256[],bytes).selector; diff --git a/certora/specs/GovernorBaseRules.spec b/certora/specs/GovernorBaseRules.spec index a49206b56c3..436fc273d7c 100644 --- a/certora/specs/GovernorBaseRules.spec +++ b/certora/specs/GovernorBaseRules.spec @@ -55,14 +55,35 @@ rule immutableFieldsAfterProposalCreation(uint256 pId, env e, method f, calldata │ (calling a view function), and we do not desire to check the signature verification. │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ -rule noDoubleVoting(uint256 pId, env e, uint8 sup) { - bool votedCheck = hasVoted(pId, e.msg.sender); +rule noDoubleVoting(uint256 pId, env e, method f) + filtered { f -> voting(f) } +{ + address voter; + uint8 support; + + bool votedCheck = hasVoted(pId, voter); - castVote@withrevert(e, pId, sup); + helperVoteWithRevert(e, f, pId, voter, support); assert votedCheck => lastReverted, "double voting occurred"; } +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: Voting against a proposal does not count towards quorum. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule againstVotesDontCountTowardsQuorum(uint256 pId, env e, method f) + filtered { f -> voting(f) } +{ + address voter; + uint8 support = 0; // Against + + helperVoteWithRevert(e, f, pId, voter, support); + + assert quorumReached(pId) == quorumBefore, "quorum must not be reached with an against vote"; +} + /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ Rule: A proposal could be executed only if quorum was reached and vote succeeded │ @@ -90,7 +111,9 @@ rule noStartBeforeCreation(uint256 pId, env e, method f, calldataarg args) filtered { f -> !skip(f) } { require !proposalCreated(pId); + f(e, args); + assert proposalCreated(pId) => proposalSnapshot(pId) >= clock(e), "starts before proposal"; } @@ -103,10 +126,26 @@ rule noExecuteBeforeDeadline(uint256 pId, env e, method f, calldataarg args) filtered { f -> !skip(f) } { require !isExecuted(pId); + f(e, args); + assert isExecuted(pId) => proposalDeadline(pId) <= clock(e), "executed before deadline"; } +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: The quorum numerator is always less than or equal to the quorum denominator │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant quorumRatioLessThanOne(env e, uint256 blockNumber) + quorumNumerator(e, blockNumber) <= quorumDenominator() + filtered { f -> !skip(f) } + { + preserved { + require clockSanity(e); + } + } + /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ Rule: All proposal specific (non-view) functions should revert if proposal is executed │ diff --git a/certora/specs/GovernorInvariants.spec b/certora/specs/GovernorInvariants.spec index 1247789fa7f..d66bd591b62 100644 --- a/certora/specs/GovernorInvariants.spec +++ b/certora/specs/GovernorInvariants.spec @@ -35,6 +35,21 @@ invariant proposalStateConsistency(uint256 pId) } } +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: votes recorded => proposal snapshot is in the past │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant votesImplySnapshotPassed(env e, uint256 pId) + getAgainstVotes(pId) == 0 => proposalSnapshot(pId) < clock(e) && + getForVotes(pId) == 0 => proposalSnapshot(pId) < clock(e) && + getAbstainVotes(pId) == 0 => proposalSnapshot(pId) < clock(e) + { + preserved { + require clockSanity(e); + } + } + /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ Invariant: cancel => created │ diff --git a/certora/specs/GovernorPreventLateQuorum.spec b/certora/specs/GovernorPreventLateQuorum.spec new file mode 100644 index 00000000000..d8df72f948b --- /dev/null +++ b/certora/specs/GovernorPreventLateQuorum.spec @@ -0,0 +1,68 @@ +import "helpers.spec" +import "methods/IGovernor.spec" +import "Governor.helpers.spec" +import "GovernorInvariants.spec" + +methods { + lateQuorumVoteExtension() returns uint64 envfree + getExtendedDeadline(uint256) returns uint64 envfree +} + +use invariant proposalStateConsistency +use invariant votesImplySnapshotPassed + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: │ +│ * Deadline can never be reduced │ +│ * If deadline increases then we are in `deadlineExtended` state and `castVote` was called. │ +│ * A proposal's deadline can't change in `deadlineExtended` state. │ +│ * A proposal's deadline can't be unextended. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule deadlineChangeToPreventLateQuorum(uint256 pId, env e, method f, calldataarg args) + filtered { f -> !skip(f) } +{ + requireInvariant proposalStateConsistency(pId); + requireInvariant votesImplySnapshotPassed(pId); + + // This should be a direct consequence of the invariant: `getExtendedDeadline(pId) > 0 => quorumReached(pId)` + // But this is not (easily) provable because the prover think `_totalSupplyCheckpoints` can arbitrarily change, + // which causes the quorum() to change. Not sure how to fix that. + require !quorumReached(pId) => getExtendedDeadline(pId) == 0; + + uint256 deadlineBefore = proposalDeadline(pId); + bool deadlineExtendedBefore = getExtendedDeadline(pId) > 0; + bool quorumReachedBefore = quorumReached(pId); + + f(e, args); + + uint256 deadlineAfter = proposalDeadline(pId); + bool deadlineExtendedAfter = getExtendedDeadline(pId) > 0; + bool quorumReachedAfter = quorumReached(pId); + + // deadline can never be reduced + assert deadlineBefore <= proposalDeadline(pId); + + // deadline can only be extended in proposal or on cast vote + assert deadlineAfter != deadlineBefore => ( + ( + !deadlineExtendedBefore && + !deadlineExtendedAfter && + f.selector == propose(address[], uint256[], bytes[], string).selector + ) || ( + !deadlineExtendedBefore && + deadlineExtendedAfter && + !quorumReachedBefore && + quorumReachedAfter && + deadlineAfter == clock(e) + lateQuorumVoteExtension() && + votingAll(f) + ) + ); + + // a deadline can only be extended once + assert deadlineExtendedBefore => deadlineBefore == deadlineAfter; + + // a deadline cannot be un-extended + assert deadlineExtendedBefore => deadlineExtendedAfter; +} diff --git a/certora/specs/GovernorStates.spec b/certora/specs/GovernorStates.spec index 417b7b0c6b0..9488163cd6a 100644 --- a/certora/specs/GovernorStates.spec +++ b/certora/specs/GovernorStates.spec @@ -128,3 +128,23 @@ rule stateIsConsistentWithVotes(uint256 pId, env e) { ) ); } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: `updateQuorumNumerator` cannot cause quorumReached to change. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule onlyVoteCanChangeQuorumReached(uint256 pId, env e, method f, calldataarg args) + filtered { f -> !skip(f) } +{ + require clockSanity(e); + + bool quorumReachedBefore = quorumReached(e, pId); + + f(e, args); + + assert quorumReached(e, pId) != quorumReachedBefore => ( + !quorumReachedBefore && + votingAll(f) + ); +} From 50a13d52b9d2c92600ab1706e2706b40f5ee134e Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 15 Mar 2023 17:13:14 +0100 Subject: [PATCH 21/61] uo --- certora/specs/GovernorPreventLateQuorum.spec | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/certora/specs/GovernorPreventLateQuorum.spec b/certora/specs/GovernorPreventLateQuorum.spec index d8df72f948b..a4eae4ea70e 100644 --- a/certora/specs/GovernorPreventLateQuorum.spec +++ b/certora/specs/GovernorPreventLateQuorum.spec @@ -26,11 +26,6 @@ rule deadlineChangeToPreventLateQuorum(uint256 pId, env e, method f, calldataarg requireInvariant proposalStateConsistency(pId); requireInvariant votesImplySnapshotPassed(pId); - // This should be a direct consequence of the invariant: `getExtendedDeadline(pId) > 0 => quorumReached(pId)` - // But this is not (easily) provable because the prover think `_totalSupplyCheckpoints` can arbitrarily change, - // which causes the quorum() to change. Not sure how to fix that. - require !quorumReached(pId) => getExtendedDeadline(pId) == 0; - uint256 deadlineBefore = proposalDeadline(pId); bool deadlineExtendedBefore = getExtendedDeadline(pId) > 0; bool quorumReachedBefore = quorumReached(pId); @@ -53,7 +48,7 @@ rule deadlineChangeToPreventLateQuorum(uint256 pId, env e, method f, calldataarg ) || ( !deadlineExtendedBefore && deadlineExtendedAfter && - !quorumReachedBefore && + // !quorumReachedBefore && // Not sure how to prove that quorumReachedAfter && deadlineAfter == clock(e) + lateQuorumVoteExtension() && votingAll(f) From dfafd796928b773fa0515c61c447306be2bf25d6 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 15 Mar 2023 17:16:47 +0100 Subject: [PATCH 22/61] uo --- certora/specs/GovernorPreventLateQuorum.spec | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/certora/specs/GovernorPreventLateQuorum.spec b/certora/specs/GovernorPreventLateQuorum.spec index a4eae4ea70e..5ed48a55502 100644 --- a/certora/specs/GovernorPreventLateQuorum.spec +++ b/certora/specs/GovernorPreventLateQuorum.spec @@ -26,6 +26,10 @@ rule deadlineChangeToPreventLateQuorum(uint256 pId, env e, method f, calldataarg requireInvariant proposalStateConsistency(pId); requireInvariant votesImplySnapshotPassed(pId); + // This is not (easily) provable because the prover think `_totalSupplyCheckpoints` can arbitrarily change, + // which causes the quorum() to change. Not sure how to fix that. + require !quorumReached(pId) <=> getExtendedDeadline(pId) == 0; + uint256 deadlineBefore = proposalDeadline(pId); bool deadlineExtendedBefore = getExtendedDeadline(pId) > 0; bool quorumReachedBefore = quorumReached(pId); @@ -48,7 +52,7 @@ rule deadlineChangeToPreventLateQuorum(uint256 pId, env e, method f, calldataarg ) || ( !deadlineExtendedBefore && deadlineExtendedAfter && - // !quorumReachedBefore && // Not sure how to prove that + !quorumReachedBefore && // Not sure how to prove that quorumReachedAfter && deadlineAfter == clock(e) + lateQuorumVoteExtension() && votingAll(f) From 96553597fa2944c7bfb89469532bcc8fe02c8194 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 15 Mar 2023 21:18:50 +0100 Subject: [PATCH 23/61] disable GovernorFunctions --- certora/specs.js | 45 ++++++++++---------- certora/specs/GovernorPreventLateQuorum.spec | 4 +- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/certora/specs.js b/certora/specs.js index e013e88915b..3591f6bba6e 100644 --- a/certora/specs.js +++ b/certora/specs.js @@ -1,6 +1,6 @@ const product = (...arrays) => arrays.reduce((a, b) => a.flatMap(ai => b.map(bi => [ai, bi].flat()))); -module.exports = [ +module.exports = [].concat( // AccessControl { spec: 'AccessControl', @@ -42,35 +42,34 @@ module.exports = [ contract: 'InitializableHarness', files: ['certora/harnesses/InitializableHarness.sol'], }, - // TimelockController + // Governance { spec: 'TimelockController', contract: 'TimelockControllerHarness', files: ['certora/harnesses/TimelockControllerHarness.sol'], options: ['--optimistic_hashing', '--optimistic_loop'], }, - // Governor - ...product( - ['GovernorInvariants', 'GovernorBaseRules', 'GovernorChanges', 'GovernorStates'], + // Govenor: carthesian product of (spec + harness contract) and (token) + product( + [].concat( + ['GovernorInvariants', 'GovernorBaseRules', 'GovernorChanges', 'GovernorStates'].map(spec => ({ + contract: 'GovernorHarness', + spec, + })), + ['GovernorPreventLateHarness'].map(spec => ({ contract: 'GovernorPreventLateHarness', spec })), + ), ['ERC20VotesBlocknumberHarness', 'ERC20VotesTimestampHarness'], - ).map(([spec, token]) => ({ + ).map(([{ contract, spec }, token]) => ({ spec, - contract: 'GovernorHarness', - files: ['certora/harnesses/GovernorHarness.sol', `certora/harnesses/${token}.sol`], + contract, + files: [`certora/harnesses/${contract}.sol`, `certora/harnesses/${token}.sol`], options: [`--link GovernorHarness:token=${token}`, '--optimistic_loop', '--optimistic_hashing'], })), - // WIP part - ...product(['GovernorFunctions'], ['ERC20VotesBlocknumberHarness']).map(([spec, token]) => ({ - spec, - contract: 'GovernorHarness', - files: ['certora/harnesses/GovernorHarness.sol', `certora/harnesses/${token}.sol`], - options: [`--link GovernorHarness:token=${token}`, '--optimistic_loop', '--optimistic_hashing'], - })), - // WIP prevent late quorum - ...product(['GovernorPreventLateQuorum'], ['ERC20VotesBlocknumberHarness']).map(([spec, token]) => ({ - spec, - contract: 'GovernorPreventLateHarness', - files: ['certora/harnesses/GovernorPreventLateHarness.sol', `certora/harnesses/${token}.sol`], - options: [`--link GovernorPreventLateHarness:token=${token}`, '--optimistic_loop', '--optimistic_hashing'], - })), -]; + /// WIP part + // product(['GovernorFunctions'], ['ERC20VotesBlocknumberHarness']).map(([spec, token]) => ({ + // spec, + // contract: 'GovernorHarness', + // files: ['certora/harnesses/GovernorHarness.sol', `certora/harnesses/${token}.sol`], + // options: [`--link GovernorHarness:token=${token}`, '--optimistic_loop', '--optimistic_hashing'], + // })), +); diff --git a/certora/specs/GovernorPreventLateQuorum.spec b/certora/specs/GovernorPreventLateQuorum.spec index 5ed48a55502..25cc0388816 100644 --- a/certora/specs/GovernorPreventLateQuorum.spec +++ b/certora/specs/GovernorPreventLateQuorum.spec @@ -26,8 +26,8 @@ rule deadlineChangeToPreventLateQuorum(uint256 pId, env e, method f, calldataarg requireInvariant proposalStateConsistency(pId); requireInvariant votesImplySnapshotPassed(pId); - // This is not (easily) provable because the prover think `_totalSupplyCheckpoints` can arbitrarily change, - // which causes the quorum() to change. Not sure how to fix that. + // This is not (easily) provable as an invariant because the prover think `_totalSupplyCheckpoints` + // can arbitrarily change, which causes the quorum() to change. Not sure how to fix that. require !quorumReached(pId) <=> getExtendedDeadline(pId) == 0; uint256 deadlineBefore = proposalDeadline(pId); From 89ceb34f0d18f24c173547ee9a9d226a0a0c9790 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 15 Mar 2023 21:29:40 +0100 Subject: [PATCH 24/61] don't run GovernorFunctions in CI --- certora/specs.js | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/certora/specs.js b/certora/specs.js index 3591f6bba6e..95640578a11 100644 --- a/certora/specs.js +++ b/certora/specs.js @@ -49,27 +49,28 @@ module.exports = [].concat( files: ['certora/harnesses/TimelockControllerHarness.sol'], options: ['--optimistic_hashing', '--optimistic_loop'], }, - // Govenor: carthesian product of (spec + harness contract) and (token) + // Govenor product( - [].concat( - ['GovernorInvariants', 'GovernorBaseRules', 'GovernorChanges', 'GovernorStates'].map(spec => ({ - contract: 'GovernorHarness', - spec, - })), - ['GovernorPreventLateHarness'].map(spec => ({ contract: 'GovernorPreventLateHarness', spec })), - ), + [ + ...product(['GovernorHarness'], ['GovernorInvariants', 'GovernorBaseRules', 'GovernorChanges', 'GovernorStates']), + ...product(['GovernorPreventLateHarness'], ['GovernorPreventLateHarness']), + ], ['ERC20VotesBlocknumberHarness', 'ERC20VotesTimestampHarness'], - ).map(([{ contract, spec }, token]) => ({ + ).map(([contract, spec, token]) => ({ spec, contract, files: [`certora/harnesses/${contract}.sol`, `certora/harnesses/${token}.sol`], options: [`--link GovernorHarness:token=${token}`, '--optimistic_loop', '--optimistic_hashing'], })), /// WIP part - // product(['GovernorFunctions'], ['ERC20VotesBlocknumberHarness']).map(([spec, token]) => ({ - // spec, - // contract: 'GovernorHarness', - // files: ['certora/harnesses/GovernorHarness.sol', `certora/harnesses/${token}.sol`], - // options: [`--link GovernorHarness:token=${token}`, '--optimistic_loop', '--optimistic_hashing'], - // })), + process.env.CI + ? [] + : product(['GovernorHarness'], ['GovernorFunctions'], ['ERC20VotesBlocknumberHarness']).map( + ([contract, spec, token]) => ({ + spec, + contract, + files: [`certora/harnesses/${contract}.sol`, `certora/harnesses/${token}.sol`], + options: [`--link GovernorHarness:token=${token}`, '--optimistic_loop', '--optimistic_hashing'], + }), + ), ); From 82bbdb2c648b5c9787f948e5da8d54b02f89f543 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 15 Mar 2023 21:30:51 +0100 Subject: [PATCH 25/61] codespell --- certora/specs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certora/specs.js b/certora/specs.js index 95640578a11..81f83aa60f0 100644 --- a/certora/specs.js +++ b/certora/specs.js @@ -49,7 +49,7 @@ module.exports = [].concat( files: ['certora/harnesses/TimelockControllerHarness.sol'], options: ['--optimistic_hashing', '--optimistic_loop'], }, - // Govenor + // Governor product( [ ...product(['GovernorHarness'], ['GovernorInvariants', 'GovernorBaseRules', 'GovernorChanges', 'GovernorStates']), From d0b259546fc188ea2ac0795c559e0a0086ee501b Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 15 Mar 2023 21:35:42 +0100 Subject: [PATCH 26/61] fix options --- certora/specs.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/certora/specs.js b/certora/specs.js index 81f83aa60f0..94b443e6993 100644 --- a/certora/specs.js +++ b/certora/specs.js @@ -60,7 +60,7 @@ module.exports = [].concat( spec, contract, files: [`certora/harnesses/${contract}.sol`, `certora/harnesses/${token}.sol`], - options: [`--link GovernorHarness:token=${token}`, '--optimistic_loop', '--optimistic_hashing'], + options: [`--link ${contract}:token=${token}`, '--optimistic_loop', '--optimistic_hashing'], })), /// WIP part process.env.CI @@ -70,7 +70,7 @@ module.exports = [].concat( spec, contract, files: [`certora/harnesses/${contract}.sol`, `certora/harnesses/${token}.sol`], - options: [`--link GovernorHarness:token=${token}`, '--optimistic_loop', '--optimistic_hashing'], + options: [`--link ${contract}:token=${token}`, '--optimistic_loop', '--optimistic_hashing'], }), ), ); From 74f613f5cc4c61b00062b0209b8c0f182dc16414 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 15 Mar 2023 22:17:11 +0100 Subject: [PATCH 27/61] fix specs --- certora/specs.js | 2 +- certora/specs/GovernorBaseRules.spec | 16 ++++++---------- certora/specs/GovernorPreventLateQuorum.spec | 2 +- certora/specs/GovernorStates.spec | 4 ++-- 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/certora/specs.js b/certora/specs.js index 94b443e6993..aac07123c57 100644 --- a/certora/specs.js +++ b/certora/specs.js @@ -53,7 +53,7 @@ module.exports = [].concat( product( [ ...product(['GovernorHarness'], ['GovernorInvariants', 'GovernorBaseRules', 'GovernorChanges', 'GovernorStates']), - ...product(['GovernorPreventLateHarness'], ['GovernorPreventLateHarness']), + ...product(['GovernorPreventLateHarness'], ['GovernorPreventLateQuorum']), ], ['ERC20VotesBlocknumberHarness', 'ERC20VotesTimestampHarness'], ).map(([contract, spec, token]) => ({ diff --git a/certora/specs/GovernorBaseRules.spec b/certora/specs/GovernorBaseRules.spec index 436fc273d7c..40e0ca2a49d 100644 --- a/certora/specs/GovernorBaseRules.spec +++ b/certora/specs/GovernorBaseRules.spec @@ -77,11 +77,12 @@ rule againstVotesDontCountTowardsQuorum(uint256 pId, env e, method f) filtered { f -> voting(f) } { address voter; - uint8 support = 0; // Against - helperVoteWithRevert(e, f, pId, voter, support); + bool quorumReachedBefore = quorumReached(pId); + + helperVoteWithRevert(e, f, pId, voter, 0); // support 0 = against - assert quorumReached(pId) == quorumBefore, "quorum must not be reached with an against vote"; + assert quorumReached(pId) == quorumReachedBefore, "quorum must not be reached with an against vote"; } /* @@ -137,14 +138,9 @@ rule noExecuteBeforeDeadline(uint256 pId, env e, method f, calldataarg args) │ Invariant: The quorum numerator is always less than or equal to the quorum denominator │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ -invariant quorumRatioLessThanOne(env e, uint256 blockNumber) - quorumNumerator(e, blockNumber) <= quorumDenominator() +invariant quorumRatioLessThanOne(uint256 blockNumber) + quorumNumerator(blockNumber) <= quorumDenominator() filtered { f -> !skip(f) } - { - preserved { - require clockSanity(e); - } - } /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ diff --git a/certora/specs/GovernorPreventLateQuorum.spec b/certora/specs/GovernorPreventLateQuorum.spec index 25cc0388816..0af3be92595 100644 --- a/certora/specs/GovernorPreventLateQuorum.spec +++ b/certora/specs/GovernorPreventLateQuorum.spec @@ -24,7 +24,7 @@ rule deadlineChangeToPreventLateQuorum(uint256 pId, env e, method f, calldataarg filtered { f -> !skip(f) } { requireInvariant proposalStateConsistency(pId); - requireInvariant votesImplySnapshotPassed(pId); + requireInvariant votesImplySnapshotPassed(e, pId); // This is not (easily) provable as an invariant because the prover think `_totalSupplyCheckpoints` // can arbitrarily change, which causes the quorum() to change. Not sure how to fix that. diff --git a/certora/specs/GovernorStates.spec b/certora/specs/GovernorStates.spec index 9488163cd6a..eb44ae28bca 100644 --- a/certora/specs/GovernorStates.spec +++ b/certora/specs/GovernorStates.spec @@ -139,11 +139,11 @@ rule onlyVoteCanChangeQuorumReached(uint256 pId, env e, method f, calldataarg ar { require clockSanity(e); - bool quorumReachedBefore = quorumReached(e, pId); + bool quorumReachedBefore = quorumReached(pId); f(e, args); - assert quorumReached(e, pId) != quorumReachedBefore => ( + assert quorumReached(pId) != quorumReachedBefore => ( !quorumReachedBefore && votingAll(f) ); From dd6a9ee240779aa125f73a199924d25bb7720c4c Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 15 Mar 2023 23:23:22 +0100 Subject: [PATCH 28/61] fix attempt --- ...overnance_extensions_GovernorPreventLateQuorum.sol.patch | 2 +- certora/specs/GovernorBaseRules.spec | 3 +++ certora/specs/GovernorInvariants.spec | 6 +++--- certora/specs/GovernorStates.spec | 2 ++ certora/specs/methods/IGovernor.spec | 1 + 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/certora/diff/governance_extensions_GovernorPreventLateQuorum.sol.patch b/certora/diff/governance_extensions_GovernorPreventLateQuorum.sol.patch index 776a1539402..05f72f8a98a 100644 --- a/certora/diff/governance_extensions_GovernorPreventLateQuorum.sol.patch +++ b/certora/diff/governance_extensions_GovernorPreventLateQuorum.sol.patch @@ -1,4 +1,4 @@ ---- governance/extensions/GovernorPreventLateQuorum.sol 2023-03-07 10:48:47.733488857 +0100 +--- governance/extensions/GovernorPreventLateQuorum.sol 2023-03-15 17:13:06.879632860 +0100 +++ governance/extensions/GovernorPreventLateQuorum.sol 2023-03-15 14:14:59.121060484 +0100 @@ -84,6 +84,11 @@ return _voteExtension; diff --git a/certora/specs/GovernorBaseRules.spec b/certora/specs/GovernorBaseRules.spec index 40e0ca2a49d..2bcaa0839dc 100644 --- a/certora/specs/GovernorBaseRules.spec +++ b/certora/specs/GovernorBaseRules.spec @@ -141,6 +141,9 @@ rule noExecuteBeforeDeadline(uint256 pId, env e, method f, calldataarg args) invariant quorumRatioLessThanOne(uint256 blockNumber) quorumNumerator(blockNumber) <= quorumDenominator() filtered { f -> !skip(f) } + { + require quorumNumeratorLength() < max_uint256; + } /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ diff --git a/certora/specs/GovernorInvariants.spec b/certora/specs/GovernorInvariants.spec index d66bd591b62..4d9de20efd0 100644 --- a/certora/specs/GovernorInvariants.spec +++ b/certora/specs/GovernorInvariants.spec @@ -41,9 +41,9 @@ invariant proposalStateConsistency(uint256 pId) └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ invariant votesImplySnapshotPassed(env e, uint256 pId) - getAgainstVotes(pId) == 0 => proposalSnapshot(pId) < clock(e) && - getForVotes(pId) == 0 => proposalSnapshot(pId) < clock(e) && - getAbstainVotes(pId) == 0 => proposalSnapshot(pId) < clock(e) + getAgainstVotes(pId) > 0 => proposalSnapshot(pId) <= clock(e) && + getForVotes(pId) > 0 => proposalSnapshot(pId) <= clock(e) && + getAbstainVotes(pId) > 0 => proposalSnapshot(pId) <= clock(e) { preserved { require clockSanity(e); diff --git a/certora/specs/GovernorStates.spec b/certora/specs/GovernorStates.spec index eb44ae28bca..d601fc996a1 100644 --- a/certora/specs/GovernorStates.spec +++ b/certora/specs/GovernorStates.spec @@ -4,6 +4,7 @@ import "Governor.helpers.spec" import "GovernorInvariants.spec" use invariant proposalStateConsistency +use invariant votesImplySnapshotPassed /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ @@ -138,6 +139,7 @@ rule onlyVoteCanChangeQuorumReached(uint256 pId, env e, method f, calldataarg ar filtered { f -> !skip(f) } { require clockSanity(e); + requireInvariant votesImplySnapshotPassed(e, pId); bool quorumReachedBefore = quorumReached(pId); diff --git a/certora/specs/methods/IGovernor.spec b/certora/specs/methods/IGovernor.spec index f44c2f06516..6cc25599f69 100644 --- a/certora/specs/methods/IGovernor.spec +++ b/certora/specs/methods/IGovernor.spec @@ -50,4 +50,5 @@ methods { getAgainstVotes(uint256) returns uint256 envfree getForVotes(uint256) returns uint256 envfree getAbstainVotes(uint256) returns uint256 envfree + quorumNumeratorLength() returns uint256 envfree } From 7512b8e171cb7780832d722cddb6a72e75aac555 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 15 Mar 2023 23:23:37 +0100 Subject: [PATCH 29/61] missing diff --- ...xtensions_GovernorVotesQuorumFraction.sol.patch | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 certora/diff/governance_extensions_GovernorVotesQuorumFraction.sol.patch diff --git a/certora/diff/governance_extensions_GovernorVotesQuorumFraction.sol.patch b/certora/diff/governance_extensions_GovernorVotesQuorumFraction.sol.patch new file mode 100644 index 00000000000..39a1a0c9087 --- /dev/null +++ b/certora/diff/governance_extensions_GovernorVotesQuorumFraction.sol.patch @@ -0,0 +1,14 @@ +--- governance/extensions/GovernorVotesQuorumFraction.sol 2023-03-07 10:48:47.733488857 +0100 ++++ governance/extensions/GovernorVotesQuorumFraction.sol 2023-03-15 22:51:51.267890807 +0100 +@@ -62,6 +62,11 @@ + return _quorumNumeratorHistory.upperLookupRecent(timepoint.toUint32()); + } + ++ // FV ++ function quorumNumeratorLength() public view returns (uint256) { ++ return _quorumNumeratorHistory._checkpoints.length; ++ } ++ + /** + * @dev Returns the quorum denominator. Defaults to 100, but may be overridden. + */ From 06baea7fa89f87e3320396553a1e6d4643d571d3 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 15 Mar 2023 23:28:50 +0100 Subject: [PATCH 30/61] up --- certora/specs/GovernorStates.spec | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/certora/specs/GovernorStates.spec b/certora/specs/GovernorStates.spec index d601fc996a1..af6ffcf2663 100644 --- a/certora/specs/GovernorStates.spec +++ b/certora/specs/GovernorStates.spec @@ -143,8 +143,14 @@ rule onlyVoteCanChangeQuorumReached(uint256 pId, env e, method f, calldataarg ar bool quorumReachedBefore = quorumReached(pId); + uint256 snapshot = proposalSnapshot(pId); + uint256 totalSupply = token_getPastTotalSupply(snapshot); + f(e, args); + // Needed because the prover doesn't understand the checkpoint properties of the voting token. + require clock(e) > snapshot => token_getPastTotalSupply(snapshot) == cache; + assert quorumReached(pId) != quorumReachedBefore => ( !quorumReachedBefore && votingAll(f) From a355bf0de27b01ed94e2ff7019d31b741dd1c67d Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 16 Mar 2023 09:28:48 +0100 Subject: [PATCH 31/61] fix --- certora/specs/GovernorBaseRules.spec | 4 +++- certora/specs/GovernorPreventLateQuorum.spec | 5 +++-- certora/specs/GovernorStates.spec | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/certora/specs/GovernorBaseRules.spec b/certora/specs/GovernorBaseRules.spec index 2bcaa0839dc..baa4d18c48c 100644 --- a/certora/specs/GovernorBaseRules.spec +++ b/certora/specs/GovernorBaseRules.spec @@ -142,7 +142,9 @@ invariant quorumRatioLessThanOne(uint256 blockNumber) quorumNumerator(blockNumber) <= quorumDenominator() filtered { f -> !skip(f) } { - require quorumNumeratorLength() < max_uint256; + preserved { + require quorumNumeratorLength() < max_uint256; + } } /* diff --git a/certora/specs/GovernorPreventLateQuorum.spec b/certora/specs/GovernorPreventLateQuorum.spec index 0af3be92595..cb906810bba 100644 --- a/certora/specs/GovernorPreventLateQuorum.spec +++ b/certora/specs/GovernorPreventLateQuorum.spec @@ -23,12 +23,13 @@ use invariant votesImplySnapshotPassed rule deadlineChangeToPreventLateQuorum(uint256 pId, env e, method f, calldataarg args) filtered { f -> !skip(f) } { + require clockSanity(e); requireInvariant proposalStateConsistency(pId); requireInvariant votesImplySnapshotPassed(e, pId); // This is not (easily) provable as an invariant because the prover think `_totalSupplyCheckpoints` // can arbitrarily change, which causes the quorum() to change. Not sure how to fix that. - require !quorumReached(pId) <=> getExtendedDeadline(pId) == 0; + require quorumReached(pId) <=> getExtendedDeadline(pId) > 0; uint256 deadlineBefore = proposalDeadline(pId); bool deadlineExtendedBefore = getExtendedDeadline(pId) > 0; @@ -52,7 +53,7 @@ rule deadlineChangeToPreventLateQuorum(uint256 pId, env e, method f, calldataarg ) || ( !deadlineExtendedBefore && deadlineExtendedAfter && - !quorumReachedBefore && // Not sure how to prove that + !quorumReachedBefore && quorumReachedAfter && deadlineAfter == clock(e) + lateQuorumVoteExtension() && votingAll(f) diff --git a/certora/specs/GovernorStates.spec b/certora/specs/GovernorStates.spec index af6ffcf2663..3d1c9de4d1b 100644 --- a/certora/specs/GovernorStates.spec +++ b/certora/specs/GovernorStates.spec @@ -149,7 +149,7 @@ rule onlyVoteCanChangeQuorumReached(uint256 pId, env e, method f, calldataarg ar f(e, args); // Needed because the prover doesn't understand the checkpoint properties of the voting token. - require clock(e) > snapshot => token_getPastTotalSupply(snapshot) == cache; + require clock(e) > snapshot => token_getPastTotalSupply(snapshot) == totalSupply; assert quorumReached(pId) != quorumReachedBefore => ( !quorumReachedBefore && From dbb4a29dc983b15207f2a9941234a40330adddb8 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 16 Mar 2023 09:46:52 +0100 Subject: [PATCH 32/61] split function rules --- certora/specs/GovernorFunctions.spec | 255 ++++++++++++++++++--------- 1 file changed, 175 insertions(+), 80 deletions(-) diff --git a/certora/specs/GovernorFunctions.spec b/certora/specs/GovernorFunctions.spec index 1c404e907a2..317f3b30dda 100644 --- a/certora/specs/GovernorFunctions.spec +++ b/certora/specs/GovernorFunctions.spec @@ -7,36 +7,53 @@ import "Governor.helpers.spec" │ Rule: propose effect and liveness. Includes "no double proposition" │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ -rule propose(uint256 pId, env e) { +rule propose_liveness(uint256 pId, env e) { require nonpayable(e); require clockSanity(e); - uint256 otherId; - - uint8 stateBefore = state(e, pId); - uint8 otherStateBefore = state(e, otherId); - uint256 otherVoteStart = proposalSnapshot(otherId); - uint256 otherVoteEnd = proposalDeadline(otherId); - address otherProposer = proposalProposer(otherId); + uint8 stateBefore = state(e, pId); address[] targets; uint256[] values; bytes[] calldatas; string descr; require validString(descr); + require targets.length < 0xffff; + require values.length < 0xffff; + require calldatas.length < 0xffff; require pId == propose@withrevert(e, targets, values, calldatas, descr); - bool success = !lastReverted; // liveness & double proposal - assert success <=> ( + assert !lastReverted <=> ( stateBefore == UNSET() && validProposal(targets, values, calldatas) ); +} + +rule propose_effect(uint256 pId, env e) { + require nonpayable(e); + require clockSanity(e); + + address[] targets; uint256[] values; bytes[] calldatas; string descr; + require pId == propose(e, targets, values, calldatas, descr); // effect - assert success => ( - state(e, pId) == PENDING() && - proposalProposer(pId) == e.msg.sender && - proposalSnapshot(pId) == clock(e) + votingDelay() && - proposalDeadline(pId) == clock(e) + votingDelay() + votingPeriod() - ); + assert state(e, pId) == PENDING(); + assert proposalProposer(pId) == e.msg.sender; + assert proposalSnapshot(pId) == clock(e) + votingDelay(); + assert proposalDeadline(pId) == clock(e) + votingDelay() + votingPeriod(); +} + +rule propose_sideeffect(uint256 pId, env e) { + require nonpayable(e); + require clockSanity(e); + + uint256 otherId; + + uint8 otherStateBefore = state(e, otherId); + uint256 otherVoteStart = proposalSnapshot(otherId); + uint256 otherVoteEnd = proposalDeadline(otherId); + address otherProposer = proposalProposer(otherId); + + address[] targets; uint256[] values; bytes[] calldatas; string descr; + require pId == propose(e, targets, values, calldatas, descr); // no side-effect assert state(e, otherId) != otherStateBefore => otherId == pId; @@ -50,7 +67,7 @@ rule propose(uint256 pId, env e) { │ Rule: votes effect and liveness. Includes "A user cannot vote twice" │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ -rule castVote(uint256 pId, env e, method f) +rule castVote_liveness(uint256 pId, env e, method f) filtered { f -> voting(f) } { require nonpayable(e); @@ -58,42 +75,68 @@ rule castVote(uint256 pId, env e, method f) uint8 support; address voter; - address otherVoter; - uint256 otherId; - uint8 stateBefore = state(e, pId); - bool hasVotedBefore = hasVoted(pId, voter); - bool otherVotedBefore = hasVoted(otherId, otherVoter); - uint256 againstVotesBefore = getAgainstVotes(pId); - uint256 forVotesBefore = getForVotes(pId); - uint256 abstainVotesBefore = getAbstainVotes(pId); - uint256 otherAgainstVotesBefore = getAgainstVotes(otherId); - uint256 otherForVotesBefore = getForVotes(otherId); - uint256 otherAbstainVotesBefore = getAbstainVotes(otherId); + uint8 stateBefore = state(e, pId); + bool hasVotedBefore = hasVoted(pId, voter); + uint256 voterWeight = token_getPastVotes(voter, proposalSnapshot(pId)); // voting weight overflow check - uint256 voterWeight = token_getPastVotes(voter, proposalSnapshot(pId)); - require againstVotesBefore + voterWeight <= max_uint256; - require forVotesBefore + voterWeight <= max_uint256; - require abstainVotesBefore + voterWeight <= max_uint256; + require getAgainstVotes(pId) + voterWeight <= max_uint256; + require getForVotes(pId) + voterWeight <= max_uint256; + require getAbstainVotes(pId) + voterWeight <= max_uint256; - uint256 weight = helperVoteWithRevert(e, f, pId, voter, support); - bool success = !lastReverted; + helperVoteWithRevert(e, f, pId, voter, support); - assert success <=> ( + assert !lastReverted <=> ( stateBefore == ACTIVE() && !hasVotedBefore && (support == 0 || support == 1 || support == 2) ); +} - assert success => ( - state(e, pId) == ACTIVE() && - voterWeight == weight && - getAgainstVotes(pId) == againstVotesBefore + (support == 0 ? weight : 0) && - getForVotes(pId) == forVotesBefore + (support == 1 ? weight : 0) && - getAbstainVotes(pId) == abstainVotesBefore + (support == 2 ? weight : 0) && - hasVoted(pId, voter) - ); +rule castVote_effect(uint256 pId, env e, method f) + filtered { f -> voting(f) } +{ + require nonpayable(e); + require clockSanity(e); + + uint8 support; + address voter; + + uint256 againstVotesBefore = getAgainstVotes(pId); + uint256 forVotesBefore = getForVotes(pId); + uint256 abstainVotesBefore = getAbstainVotes(pId); + uint256 voterWeight = token_getPastVotes(voter, proposalSnapshot(pId)); + + uint256 weight = helperVoteWithRevert(e, f, pId, voter, support); + require !lastReverted; + + assert state(e, pId) == ACTIVE(); + assert voterWeight == weight; + assert getAgainstVotes(pId) == againstVotesBefore + (support == 0 ? weight : 0); + assert getForVotes(pId) == forVotesBefore + (support == 1 ? weight : 0); + assert getAbstainVotes(pId) == abstainVotesBefore + (support == 2 ? weight : 0); + assert hasVoted(pId, voter); +} + +rule castVote_sideeffect(uint256 pId, env e, method f) + filtered { f -> voting(f) } +{ + require nonpayable(e); + require clockSanity(e); + + uint8 support; + address voter; + address otherVoter; + uint256 otherId; + + bool otherVotedBefore = hasVoted(otherId, otherVoter); + uint256 otherAgainstVotesBefore = getAgainstVotes(otherId); + uint256 otherForVotesBefore = getForVotes(otherId); + uint256 otherAbstainVotesBefore = getAbstainVotes(otherId); + + helperVoteWithRevert(e, f, pId, voter, support); + require !lastReverted; // no side-effect assert hasVoted(otherId, otherVoter) != otherVotedBefore => (otherId == pId && otherVoter == voter); @@ -107,30 +150,48 @@ rule castVote(uint256 pId, env e, method f) │ Rule: queue effect and liveness. │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ -rule queue(uint256 pId, env e) { +rule queue_liveness(uint256 pId, env e) { require nonpayable(e); require clockSanity(e); - uint256 otherId; - - uint8 stateBefore = state(e, pId); - uint8 otherStateBefore = state(e, otherId); - bool queuedBefore = isQueued(pId); - bool otherQueuedBefore = isQueued(otherId); + uint8 stateBefore = state(e, pId); address[] targets; uint256[] values; bytes[] calldatas; bytes32 descrHash; + require targets.length < 0xffff; + require values.length < 0xffff; + require calldatas.length < 0xffff; require pId == queue@withrevert(e, targets, values, calldatas, descrHash); - bool success = !lastReverted; // liveness - assert success <=> stateBefore == SUCCEEDED(); + assert !lastReverted <=> stateBefore == SUCCEEDED(); +} - // effect - assert success => ( - state(e, pId) == QUEUED() && - !queuedBefore && - isQueued(pId) - ); +rule queue_effect(uint256 pId, env e) { + require nonpayable(e); + require clockSanity(e); + + uint8 stateBefore = state(e, pId); + bool queuedBefore = isQueued(pId); + + address[] targets; uint256[] values; bytes[] calldatas; bytes32 descrHash; + require pId == queue(e, targets, values, calldatas, descrHash); + + assert state(e, pId) == QUEUED(); + assert isQueued(pId); + assert !queuedBefore; +} + +rule queue_sideeffect(uint256 pId, env e) { + require nonpayable(e); + require clockSanity(e); + + uint256 otherId; + + uint8 otherStateBefore = state(e, otherId); + bool otherQueuedBefore = isQueued(otherId); + + address[] targets; uint256[] values; bytes[] calldatas; bytes32 descrHash; + require pId == queue(e, targets, values, calldatas, descrHash); // no side-effect assert state(e, otherId) != otherStateBefore => otherId == pId; @@ -142,26 +203,43 @@ rule queue(uint256 pId, env e) { │ Rule: execute effect and liveness. │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ -rule execute(uint256 pId, env e) { +rule execute_liveness(uint256 pId, env e) { require nonpayable(e); require clockSanity(e); - uint256 otherId; - - uint8 stateBefore = state(e, pId); - uint8 otherStateBefore = state(e, otherId); + uint8 stateBefore = state(e, pId); address[] targets; uint256[] values; bytes[] calldatas; bytes32 descrHash; + require targets.length < 0xffff; + require values.length < 0xffff; + require calldatas.length < 0xffff; require pId == execute@withrevert(e, targets, values, calldatas, descrHash); - bool success = !lastReverted; // liveness: can't check full equivalence because of execution call reverts - assert success => (stateBefore == SUCCEEDED() || stateBefore == QUEUED()); + assert !lastReverted => (stateBefore == SUCCEEDED() || stateBefore == QUEUED()); +} + +rule execute_effect(uint256 pId, env e) { + require nonpayable(e); + require clockSanity(e); + + address[] targets; uint256[] values; bytes[] calldatas; bytes32 descrHash; + require pId == execute(e, targets, values, calldatas, descrHash); // effect - assert success => ( - state(e, pId) == EXECUTED() - ); + assert state(e, pId) == EXECUTED(); +} + +rule execute_sideeffect(uint256 pId, env e) { + require nonpayable(e); + require clockSanity(e); + + uint256 otherId; + + uint8 otherStateBefore = state(e, otherId); + + address[] targets; uint256[] values; bytes[] calldatas; bytes32 descrHash; + require pId == execute(e, targets, values, calldatas, descrHash); // no side-effect assert state(e, otherId) != otherStateBefore => otherId == pId; @@ -172,31 +250,48 @@ rule execute(uint256 pId, env e) { │ Rule: cancel (public) effect and liveness. │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ -rule cancel(uint256 pId, env e) { +rule cancel_liveness(uint256 pId, env e) { require nonpayable(e); require clockSanity(e); - uint256 otherId; - - uint8 stateBefore = state(e, pId); - uint8 otherStateBefore = state(e, otherId); - bool otherQueuedBefore = isQueued(otherId); + uint8 stateBefore = state(e, pId); address[] targets; uint256[] values; bytes[] calldatas; bytes32 descrHash; + require targets.length < 0xffff; + require values.length < 0xffff; + require calldatas.length < 0xffff; require pId == cancel@withrevert(e, targets, values, calldatas, descrHash); - bool success = !lastReverted; // liveness - assert success <=> ( + assert !lastReverted <=> ( stateBefore == PENDING() && e.msg.sender == proposalProposer(pId) ); +} + +rule cancel_effect(uint256 pId, env e) { + require nonpayable(e); + require clockSanity(e); + + address[] targets; uint256[] values; bytes[] calldatas; bytes32 descrHash; + require pId == cancel(e, targets, values, calldatas, descrHash); // effect - assert success => ( - state(e, pId) == CANCELED() && - !isQueued(pId) // cancel resets timelockId - ); + assert state(e, pId) == CANCELED(); + assert !isQueued(pId); // cancel resets timelockId +} + +rule cancel_sideeffect(uint256 pId, env e) { + require nonpayable(e); + require clockSanity(e); + + uint256 otherId; + + uint8 otherStateBefore = state(e, otherId); + bool otherQueuedBefore = isQueued(otherId); + + address[] targets; uint256[] values; bytes[] calldatas; bytes32 descrHash; + require pId == cancel(e, targets, values, calldatas, descrHash); // no side-effect assert state(e, otherId) != otherStateBefore => otherId == pId; From 607268bd97a7a3abb8f795c100f7564c3e0763ee Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 16 Mar 2023 11:02:15 +0100 Subject: [PATCH 33/61] timeout --- certora/specs/GovernorBaseRules.spec | 4 ++-- certora/specs/GovernorPreventLateQuorum.spec | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/certora/specs/GovernorBaseRules.spec b/certora/specs/GovernorBaseRules.spec index baa4d18c48c..7e856dbe16b 100644 --- a/certora/specs/GovernorBaseRules.spec +++ b/certora/specs/GovernorBaseRules.spec @@ -138,8 +138,8 @@ rule noExecuteBeforeDeadline(uint256 pId, env e, method f, calldataarg args) │ Invariant: The quorum numerator is always less than or equal to the quorum denominator │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ -invariant quorumRatioLessThanOne(uint256 blockNumber) - quorumNumerator(blockNumber) <= quorumDenominator() +invariant quorumRatioLessThanOne() + quorumNumerator() <= quorumDenominator() filtered { f -> !skip(f) } { preserved { diff --git a/certora/specs/GovernorPreventLateQuorum.spec b/certora/specs/GovernorPreventLateQuorum.spec index cb906810bba..ec85e07377c 100644 --- a/certora/specs/GovernorPreventLateQuorum.spec +++ b/certora/specs/GovernorPreventLateQuorum.spec @@ -29,17 +29,17 @@ rule deadlineChangeToPreventLateQuorum(uint256 pId, env e, method f, calldataarg // This is not (easily) provable as an invariant because the prover think `_totalSupplyCheckpoints` // can arbitrarily change, which causes the quorum() to change. Not sure how to fix that. - require quorumReached(pId) <=> getExtendedDeadline(pId) > 0; + // require quorumReached(pId) <=> getExtendedDeadline(pId) > 0; // Timeout uint256 deadlineBefore = proposalDeadline(pId); bool deadlineExtendedBefore = getExtendedDeadline(pId) > 0; - bool quorumReachedBefore = quorumReached(pId); + // bool quorumReachedBefore = quorumReached(pId); // Timeout f(e, args); uint256 deadlineAfter = proposalDeadline(pId); bool deadlineExtendedAfter = getExtendedDeadline(pId) > 0; - bool quorumReachedAfter = quorumReached(pId); + // bool quorumReachedAfter = quorumReached(pId); // Timeout // deadline can never be reduced assert deadlineBefore <= proposalDeadline(pId); @@ -53,8 +53,8 @@ rule deadlineChangeToPreventLateQuorum(uint256 pId, env e, method f, calldataarg ) || ( !deadlineExtendedBefore && deadlineExtendedAfter && - !quorumReachedBefore && - quorumReachedAfter && + // !quorumReachedBefore && + // quorumReachedAfter && deadlineAfter == clock(e) + lateQuorumVoteExtension() && votingAll(f) ) From 3f79e2610c96e988746cd0914442790f262c89e7 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 16 Mar 2023 16:08:55 +0100 Subject: [PATCH 34/61] update --- ...tensions_GovernorTimelockControl.sol.patch | 11 +++- ...ions_GovernorVotesQuorumFraction.sol.patch | 2 +- certora/harnesses/GovernorHarness.sol | 4 ++ .../harnesses/GovernorPreventLateHarness.sol | 4 ++ certora/specs.js | 43 ++++++++++--- certora/specs/Governor.helpers.spec | 13 ++-- certora/specs/GovernorBaseRules.spec | 2 +- certora/specs/GovernorChanges.spec | 1 - certora/specs/GovernorFunctions.spec | 63 +++++++++++-------- certora/specs/GovernorInvariants.spec | 1 - certora/specs/GovernorPreventLateQuorum.spec | 18 +++--- certora/specs/GovernorStates.spec | 1 - certora/specs/TimelockController.spec | 25 +------- certora/specs/methods/IGovernor.spec | 33 +++++----- .../specs/methods/ITimelockController.spec | 22 +++++++ 15 files changed, 148 insertions(+), 95 deletions(-) create mode 100644 certora/specs/methods/ITimelockController.spec diff --git a/certora/diff/governance_extensions_GovernorTimelockControl.sol.patch b/certora/diff/governance_extensions_GovernorTimelockControl.sol.patch index 57e213f3461..f1688e2abc1 100644 --- a/certora/diff/governance_extensions_GovernorTimelockControl.sol.patch +++ b/certora/diff/governance_extensions_GovernorTimelockControl.sol.patch @@ -1,5 +1,14 @@ --- governance/extensions/GovernorTimelockControl.sol 2023-03-14 15:48:49.307543354 +0100 -+++ governance/extensions/GovernorTimelockControl.sol 2023-03-14 22:09:12.661420438 +0100 ++++ governance/extensions/GovernorTimelockControl.sol 2023-03-16 16:01:13.857331689 +0100 +@@ -24,7 +24,7 @@ + * _Available since v4.3._ + */ + abstract contract GovernorTimelockControl is IGovernorTimelock, Governor { +- TimelockController private _timelock; ++ TimelockController public _timelock; // FV: public for link + mapping(uint256 => bytes32) private _timelockIds; + + /** @@ -84,6 +84,11 @@ return eta == 1 ? 0 : eta; // _DONE_TIMESTAMP (1) should be replaced with a 0 value } diff --git a/certora/diff/governance_extensions_GovernorVotesQuorumFraction.sol.patch b/certora/diff/governance_extensions_GovernorVotesQuorumFraction.sol.patch index 39a1a0c9087..98d3b25d264 100644 --- a/certora/diff/governance_extensions_GovernorVotesQuorumFraction.sol.patch +++ b/certora/diff/governance_extensions_GovernorVotesQuorumFraction.sol.patch @@ -1,5 +1,5 @@ --- governance/extensions/GovernorVotesQuorumFraction.sol 2023-03-07 10:48:47.733488857 +0100 -+++ governance/extensions/GovernorVotesQuorumFraction.sol 2023-03-15 22:51:51.267890807 +0100 ++++ governance/extensions/GovernorVotesQuorumFraction.sol 2023-03-15 22:52:06.424537201 +0100 @@ -62,6 +62,11 @@ return _quorumNumeratorHistory.upperLookupRecent(timepoint.toUint32()); } diff --git a/certora/harnesses/GovernorHarness.sol b/certora/harnesses/GovernorHarness.sol index e758d38102c..d3494d66b43 100644 --- a/certora/harnesses/GovernorHarness.sol +++ b/certora/harnesses/GovernorHarness.sol @@ -40,6 +40,10 @@ contract GovernorHarness is } // Harness from Governor + function hashProposal(address[] memory targets,uint256[] memory values,bytes[] memory calldatas,string memory description) public returns (uint256) { + return hashProposal(targets, values, calldatas, keccak256(bytes(description))); + } + function getExecutor() public view returns (address) { return _executor(); } diff --git a/certora/harnesses/GovernorPreventLateHarness.sol b/certora/harnesses/GovernorPreventLateHarness.sol index 048ceae3c59..a4b1ca3f7a4 100644 --- a/certora/harnesses/GovernorPreventLateHarness.sol +++ b/certora/harnesses/GovernorPreventLateHarness.sol @@ -43,6 +43,10 @@ contract GovernorPreventLateHarness is } // Harness from Governor + function hashProposal(address[] memory targets,uint256[] memory values,bytes[] memory calldatas,string memory description) public returns (uint256) { + return hashProposal(targets, values, calldatas, keccak256(bytes(description))); + } + function getExecutor() public view returns (address) { return _executor(); } diff --git a/certora/specs.js b/certora/specs.js index aac07123c57..ba466b95784 100644 --- a/certora/specs.js +++ b/certora/specs.js @@ -59,18 +59,41 @@ module.exports = [].concat( ).map(([contract, spec, token]) => ({ spec, contract, - files: [`certora/harnesses/${contract}.sol`, `certora/harnesses/${token}.sol`], - options: [`--link ${contract}:token=${token}`, '--optimistic_loop', '--optimistic_hashing'], + files: [ + `certora/harnesses/${contract}.sol`, + `certora/harnesses/${token}.sol`, + `certora/harnesses/TimelockControllerHarness.sol`, + ], + options: [ + `--link ${contract}:token=${token}`, + `--link ${contract}:_timelock=TimelockControllerHarness`, + '--optimistic_loop', + '--optimistic_hashing', + ], })), /// WIP part process.env.CI ? [] - : product(['GovernorHarness'], ['GovernorFunctions'], ['ERC20VotesBlocknumberHarness']).map( - ([contract, spec, token]) => ({ - spec, - contract, - files: [`certora/harnesses/${contract}.sol`, `certora/harnesses/${token}.sol`], - options: [`--link ${contract}:token=${token}`, '--optimistic_loop', '--optimistic_hashing'], - }), - ), + : product( + ['GovernorHarness'], + ['GovernorFunctions'], + ['ERC20VotesBlocknumberHarness'], + ['propose', 'castVote', 'queue', 'execute', 'cancel'], + ).map(([contract, spec, token, fn]) => ({ + spec, + contract, + files: [ + `certora/harnesses/${contract}.sol`, + `certora/harnesses/${token}.sol`, + `certora/harnesses/TimelockControllerHarness.sol`, + ], + options: [ + `--link ${contract}:token=${token}`, + `--link ${contract}:_timelock=TimelockControllerHarness`, + '--optimistic_loop', + '--optimistic_hashing', + '--rules', + ['liveness', 'effect', 'sideeffect'].map(rule => `${fn}_${rule}`).join(' '), + ], + })), ); diff --git a/certora/specs/Governor.helpers.spec b/certora/specs/Governor.helpers.spec index ec7b610a7e1..968f0b0ee91 100644 --- a/certora/specs/Governor.helpers.spec +++ b/certora/specs/Governor.helpers.spec @@ -1,5 +1,6 @@ import "helpers.spec" import "methods/IGovernor.spec" +import "methods/ITimelockController.spec" /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ @@ -108,22 +109,26 @@ function helperFunctionsWithRevert(env e, method f, uint256 pId) { if (f.selector == propose(address[],uint256[],bytes[],string).selector) { address[] targets; uint256[] values; bytes[] calldatas; string descr; - require pId == propose@withrevert(e, targets, values, calldatas, descr); + require pId == hashProposal(targets, values, calldatas, descr); + propose@withrevert(e, targets, values, calldatas, descr); } else if (f.selector == queue(address[],uint256[],bytes[],bytes32).selector) { address[] targets; uint256[] values; bytes[] calldatas; bytes32 descrHash; - require pId == queue@withrevert(e, targets, values, calldatas, descrHash); + require pId == hashProposal(targets, values, calldatas, descrHash); + queue@withrevert(e, targets, values, calldatas, descrHash); } else if (f.selector == execute(address[],uint256[],bytes[],bytes32).selector) { address[] targets; uint256[] values; bytes[] calldatas; bytes32 descrHash; - require pId == execute@withrevert(e, targets, values, calldatas, descrHash); + require pId == hashProposal(targets, values, calldatas, descrHash); + execute@withrevert(e, targets, values, calldatas, descrHash); } else if (f.selector == cancel(address[],uint256[],bytes[],bytes32).selector) { address[] targets; uint256[] values; bytes[] calldatas; bytes32 descrHash; - require pId == cancel@withrevert(e, targets, values, calldatas, descrHash); + require pId == hashProposal(targets, values, calldatas, descrHash); + cancel@withrevert(e, targets, values, calldatas, descrHash); } else if (f.selector == castVote(uint256,uint8).selector) { diff --git a/certora/specs/GovernorBaseRules.spec b/certora/specs/GovernorBaseRules.spec index 7e856dbe16b..ad51157279b 100644 --- a/certora/specs/GovernorBaseRules.spec +++ b/certora/specs/GovernorBaseRules.spec @@ -1,4 +1,4 @@ -import "methods/IGovernor.spec" +import "helpers.spec" import "Governor.helpers.spec" import "GovernorInvariants.spec" diff --git a/certora/specs/GovernorChanges.spec b/certora/specs/GovernorChanges.spec index 0fff10a5ad9..6ab149fad51 100644 --- a/certora/specs/GovernorChanges.spec +++ b/certora/specs/GovernorChanges.spec @@ -1,5 +1,4 @@ import "helpers.spec" -import "methods/IGovernor.spec" import "Governor.helpers.spec" import "GovernorInvariants.spec" diff --git a/certora/specs/GovernorFunctions.spec b/certora/specs/GovernorFunctions.spec index 317f3b30dda..016c123601d 100644 --- a/certora/specs/GovernorFunctions.spec +++ b/certora/specs/GovernorFunctions.spec @@ -1,5 +1,4 @@ import "helpers.spec" -import "methods/IGovernor.spec" import "Governor.helpers.spec" /* @@ -14,11 +13,10 @@ rule propose_liveness(uint256 pId, env e) { uint8 stateBefore = state(e, pId); address[] targets; uint256[] values; bytes[] calldatas; string descr; - require validString(descr); - require targets.length < 0xffff; - require values.length < 0xffff; - require calldatas.length < 0xffff; - require pId == propose@withrevert(e, targets, values, calldatas, descr); + require pId == hashProposal(targets, values, calldatas, descr); + //require validString(descr); + + propose@withrevert(e, targets, values, calldatas, descr); // liveness & double proposal assert !lastReverted <=> ( @@ -32,7 +30,9 @@ rule propose_effect(uint256 pId, env e) { require clockSanity(e); address[] targets; uint256[] values; bytes[] calldatas; string descr; - require pId == propose(e, targets, values, calldatas, descr); + require pId == hashProposal(targets, values, calldatas, descr); + + propose(e, targets, values, calldatas, descr); // effect assert state(e, pId) == PENDING(); @@ -53,7 +53,9 @@ rule propose_sideeffect(uint256 pId, env e) { address otherProposer = proposalProposer(otherId); address[] targets; uint256[] values; bytes[] calldatas; string descr; - require pId == propose(e, targets, values, calldatas, descr); + require pId == hashProposal(targets, values, calldatas, descr); + + propose(e, targets, values, calldatas, descr); // no side-effect assert state(e, otherId) != otherStateBefore => otherId == pId; @@ -157,10 +159,9 @@ rule queue_liveness(uint256 pId, env e) { uint8 stateBefore = state(e, pId); address[] targets; uint256[] values; bytes[] calldatas; bytes32 descrHash; - require targets.length < 0xffff; - require values.length < 0xffff; - require calldatas.length < 0xffff; - require pId == queue@withrevert(e, targets, values, calldatas, descrHash); + require pId == hashProposal(targets, values, calldatas, descrHash); + + queue@withrevert(e, targets, values, calldatas, descrHash); // liveness assert !lastReverted <=> stateBefore == SUCCEEDED(); @@ -174,7 +175,9 @@ rule queue_effect(uint256 pId, env e) { bool queuedBefore = isQueued(pId); address[] targets; uint256[] values; bytes[] calldatas; bytes32 descrHash; - require pId == queue(e, targets, values, calldatas, descrHash); + require pId == hashProposal(targets, values, calldatas, descrHash); + + queue(e, targets, values, calldatas, descrHash); assert state(e, pId) == QUEUED(); assert isQueued(pId); @@ -191,7 +194,9 @@ rule queue_sideeffect(uint256 pId, env e) { bool otherQueuedBefore = isQueued(otherId); address[] targets; uint256[] values; bytes[] calldatas; bytes32 descrHash; - require pId == queue(e, targets, values, calldatas, descrHash); + require pId == hashProposal(targets, values, calldatas, descrHash); + + queue(e, targets, values, calldatas, descrHash); // no side-effect assert state(e, otherId) != otherStateBefore => otherId == pId; @@ -210,10 +215,9 @@ rule execute_liveness(uint256 pId, env e) { uint8 stateBefore = state(e, pId); address[] targets; uint256[] values; bytes[] calldatas; bytes32 descrHash; - require targets.length < 0xffff; - require values.length < 0xffff; - require calldatas.length < 0xffff; - require pId == execute@withrevert(e, targets, values, calldatas, descrHash); + require pId == hashProposal(targets, values, calldatas, descrHash); + + execute@withrevert(e, targets, values, calldatas, descrHash); // liveness: can't check full equivalence because of execution call reverts assert !lastReverted => (stateBefore == SUCCEEDED() || stateBefore == QUEUED()); @@ -224,7 +228,9 @@ rule execute_effect(uint256 pId, env e) { require clockSanity(e); address[] targets; uint256[] values; bytes[] calldatas; bytes32 descrHash; - require pId == execute(e, targets, values, calldatas, descrHash); + require pId == hashProposal(targets, values, calldatas, descrHash); + + execute(e, targets, values, calldatas, descrHash); // effect assert state(e, pId) == EXECUTED(); @@ -239,7 +245,9 @@ rule execute_sideeffect(uint256 pId, env e) { uint8 otherStateBefore = state(e, otherId); address[] targets; uint256[] values; bytes[] calldatas; bytes32 descrHash; - require pId == execute(e, targets, values, calldatas, descrHash); + require pId == hashProposal(targets, values, calldatas, descrHash); + + execute(e, targets, values, calldatas, descrHash); // no side-effect assert state(e, otherId) != otherStateBefore => otherId == pId; @@ -257,10 +265,9 @@ rule cancel_liveness(uint256 pId, env e) { uint8 stateBefore = state(e, pId); address[] targets; uint256[] values; bytes[] calldatas; bytes32 descrHash; - require targets.length < 0xffff; - require values.length < 0xffff; - require calldatas.length < 0xffff; - require pId == cancel@withrevert(e, targets, values, calldatas, descrHash); + require pId == hashProposal(targets, values, calldatas, descrHash); + + cancel@withrevert(e, targets, values, calldatas, descrHash); // liveness assert !lastReverted <=> ( @@ -274,7 +281,9 @@ rule cancel_effect(uint256 pId, env e) { require clockSanity(e); address[] targets; uint256[] values; bytes[] calldatas; bytes32 descrHash; - require pId == cancel(e, targets, values, calldatas, descrHash); + require pId == hashProposal(targets, values, calldatas, descrHash); + + cancel(e, targets, values, calldatas, descrHash); // effect assert state(e, pId) == CANCELED(); @@ -291,7 +300,9 @@ rule cancel_sideeffect(uint256 pId, env e) { bool otherQueuedBefore = isQueued(otherId); address[] targets; uint256[] values; bytes[] calldatas; bytes32 descrHash; - require pId == cancel(e, targets, values, calldatas, descrHash); + require pId == hashProposal(targets, values, calldatas, descrHash); + + cancel(e, targets, values, calldatas, descrHash); // no side-effect assert state(e, otherId) != otherStateBefore => otherId == pId; diff --git a/certora/specs/GovernorInvariants.spec b/certora/specs/GovernorInvariants.spec index 4d9de20efd0..9be77e24457 100644 --- a/certora/specs/GovernorInvariants.spec +++ b/certora/specs/GovernorInvariants.spec @@ -1,5 +1,4 @@ import "helpers.spec" -import "methods/IGovernor.spec" import "Governor.helpers.spec" /* diff --git a/certora/specs/GovernorPreventLateQuorum.spec b/certora/specs/GovernorPreventLateQuorum.spec index ec85e07377c..808c45ae97e 100644 --- a/certora/specs/GovernorPreventLateQuorum.spec +++ b/certora/specs/GovernorPreventLateQuorum.spec @@ -1,5 +1,4 @@ import "helpers.spec" -import "methods/IGovernor.spec" import "Governor.helpers.spec" import "GovernorInvariants.spec" @@ -11,6 +10,15 @@ methods { use invariant proposalStateConsistency use invariant votesImplySnapshotPassed +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ This is not (easily) provable as an invariant because the prover think `_totalSupplyCheckpoints` can arbitrarily │ +│ change, which causes the quorum() to change. Not sure how to fix that. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +// invariant deadlineExtendedEquivQuorumReached(uint256 pId) +// getExtendedDeadline(pId) > 0 <=> (quorumReached(pId) && !isCanceled(pId)) + /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ Rule: │ @@ -27,19 +35,13 @@ rule deadlineChangeToPreventLateQuorum(uint256 pId, env e, method f, calldataarg requireInvariant proposalStateConsistency(pId); requireInvariant votesImplySnapshotPassed(e, pId); - // This is not (easily) provable as an invariant because the prover think `_totalSupplyCheckpoints` - // can arbitrarily change, which causes the quorum() to change. Not sure how to fix that. - // require quorumReached(pId) <=> getExtendedDeadline(pId) > 0; // Timeout - uint256 deadlineBefore = proposalDeadline(pId); bool deadlineExtendedBefore = getExtendedDeadline(pId) > 0; - // bool quorumReachedBefore = quorumReached(pId); // Timeout f(e, args); uint256 deadlineAfter = proposalDeadline(pId); bool deadlineExtendedAfter = getExtendedDeadline(pId) > 0; - // bool quorumReachedAfter = quorumReached(pId); // Timeout // deadline can never be reduced assert deadlineBefore <= proposalDeadline(pId); @@ -53,8 +55,6 @@ rule deadlineChangeToPreventLateQuorum(uint256 pId, env e, method f, calldataarg ) || ( !deadlineExtendedBefore && deadlineExtendedAfter && - // !quorumReachedBefore && - // quorumReachedAfter && deadlineAfter == clock(e) + lateQuorumVoteExtension() && votingAll(f) ) diff --git a/certora/specs/GovernorStates.spec b/certora/specs/GovernorStates.spec index 3d1c9de4d1b..eece8bf09e5 100644 --- a/certora/specs/GovernorStates.spec +++ b/certora/specs/GovernorStates.spec @@ -1,5 +1,4 @@ import "helpers.spec" -import "methods/IGovernor.spec" import "Governor.helpers.spec" import "GovernorInvariants.spec" diff --git a/certora/specs/TimelockController.spec b/certora/specs/TimelockController.spec index e140c11de4d..46a1cb1c050 100644 --- a/certora/specs/TimelockController.spec +++ b/certora/specs/TimelockController.spec @@ -1,29 +1,6 @@ import "helpers.spec" import "methods/IAccessControl.spec" - -methods { - TIMELOCK_ADMIN_ROLE() returns (bytes32) envfree - PROPOSER_ROLE() returns (bytes32) envfree - EXECUTOR_ROLE() returns (bytes32) envfree - CANCELLER_ROLE() returns (bytes32) envfree - isOperation(bytes32) returns (bool) envfree - isOperationPending(bytes32) returns (bool) envfree - isOperationReady(bytes32) returns (bool) - isOperationDone(bytes32) returns (bool) envfree - getTimestamp(bytes32) returns (uint256) envfree - getMinDelay() returns (uint256) envfree - - hashOperation(address, uint256, bytes, bytes32, bytes32) returns(bytes32) envfree - hashOperationBatch(address[], uint256[], bytes[], bytes32, bytes32) returns(bytes32) envfree - - schedule(address, uint256, bytes, bytes32, bytes32, uint256) - scheduleBatch(address[], uint256[], bytes[], bytes32, bytes32, uint256) - execute(address, uint256, bytes, bytes32, bytes32) - executeBatch(address[], uint256[], bytes[], bytes32, bytes32) - cancel(bytes32) - - updateDelay(uint256) -} +import "methods/ITimelockController.spec" /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ diff --git a/certora/specs/methods/IGovernor.spec b/certora/specs/methods/IGovernor.spec index 6cc25599f69..7737bb23e78 100644 --- a/certora/specs/methods/IGovernor.spec +++ b/certora/specs/methods/IGovernor.spec @@ -35,20 +35,21 @@ methods { updateTimelock(address) // harness - token_getPastTotalSupply(uint256) returns uint256 envfree - token_getPastVotes(address,uint256) returns uint256 envfree - token_clock() returns uint48 - token_CLOCK_MODE() returns string - getExecutor() returns address envfree - proposalProposer(uint256) returns address envfree - quorumReached(uint256) returns bool envfree - voteSucceeded(uint256) returns bool envfree - isExecuted(uint256) returns bool envfree - isCanceled(uint256) returns bool envfree - isQueued(uint256) returns bool envfree - governanceCallLength() returns uint256 envfree - getAgainstVotes(uint256) returns uint256 envfree - getForVotes(uint256) returns uint256 envfree - getAbstainVotes(uint256) returns uint256 envfree - quorumNumeratorLength() returns uint256 envfree + token_getPastTotalSupply(uint256) returns uint256 envfree + token_getPastVotes(address,uint256) returns uint256 envfree + token_clock() returns uint48 + token_CLOCK_MODE() returns string + hashProposal(address[],uint256[],bytes[],string) returns uint256 envfree + getExecutor() returns address envfree + proposalProposer(uint256) returns address envfree + quorumReached(uint256) returns bool envfree + voteSucceeded(uint256) returns bool envfree + isExecuted(uint256) returns bool envfree + isCanceled(uint256) returns bool envfree + isQueued(uint256) returns bool envfree + governanceCallLength() returns uint256 envfree + getAgainstVotes(uint256) returns uint256 envfree + getForVotes(uint256) returns uint256 envfree + getAbstainVotes(uint256) returns uint256 envfree + quorumNumeratorLength() returns uint256 envfree } diff --git a/certora/specs/methods/ITimelockController.spec b/certora/specs/methods/ITimelockController.spec new file mode 100644 index 00000000000..a91fe211acf --- /dev/null +++ b/certora/specs/methods/ITimelockController.spec @@ -0,0 +1,22 @@ +methods { + TIMELOCK_ADMIN_ROLE() returns (bytes32) envfree => DISPATCHER(true) + PROPOSER_ROLE() returns (bytes32) envfree => DISPATCHER(true) + EXECUTOR_ROLE() returns (bytes32) envfree => DISPATCHER(true) + CANCELLER_ROLE() returns (bytes32) envfree => DISPATCHER(true) + isOperation(bytes32) returns (bool) envfree => DISPATCHER(true) + isOperationPending(bytes32) returns (bool) envfree => DISPATCHER(true) + isOperationReady(bytes32) returns (bool) => DISPATCHER(true) + isOperationDone(bytes32) returns (bool) envfree => DISPATCHER(true) + getTimestamp(bytes32) returns (uint256) envfree => DISPATCHER(true) + getMinDelay() returns (uint256) envfree => DISPATCHER(true) + + hashOperation(address, uint256, bytes, bytes32, bytes32) returns(bytes32) envfree => DISPATCHER(true) + hashOperationBatch(address[], uint256[], bytes[], bytes32, bytes32) returns(bytes32) envfree => DISPATCHER(true) + + schedule(address, uint256, bytes, bytes32, bytes32, uint256) => DISPATCHER(true) + scheduleBatch(address[], uint256[], bytes[], bytes32, bytes32, uint256) => DISPATCHER(true) + execute(address, uint256, bytes, bytes32, bytes32) => DISPATCHER(true) + executeBatch(address[], uint256[], bytes[], bytes32, bytes32) => DISPATCHER(true) + cancel(bytes32) => DISPATCHER(true) + updateDelay(uint256) => DISPATCHER(true) +} \ No newline at end of file From 5ef4d207a61313b8d1e64b98a17070b947ee2320 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 16 Mar 2023 17:49:21 +0100 Subject: [PATCH 35/61] more fixes ? --- certora/harnesses/GovernorHarness.sol | 2 +- .../harnesses/GovernorPreventLateHarness.sol | 2 +- certora/specs/GovernorFunctions.spec | 49 +++---------------- certora/specs/GovernorInvariants.spec | 19 +++++-- 4 files changed, 25 insertions(+), 47 deletions(-) diff --git a/certora/harnesses/GovernorHarness.sol b/certora/harnesses/GovernorHarness.sol index d3494d66b43..a6b1a273899 100644 --- a/certora/harnesses/GovernorHarness.sol +++ b/certora/harnesses/GovernorHarness.sol @@ -40,7 +40,7 @@ contract GovernorHarness is } // Harness from Governor - function hashProposal(address[] memory targets,uint256[] memory values,bytes[] memory calldatas,string memory description) public returns (uint256) { + function hashProposal(address[] memory targets,uint256[] memory values,bytes[] memory calldatas,string memory description) public pure returns (uint256) { return hashProposal(targets, values, calldatas, keccak256(bytes(description))); } diff --git a/certora/harnesses/GovernorPreventLateHarness.sol b/certora/harnesses/GovernorPreventLateHarness.sol index a4b1ca3f7a4..dc306d63e76 100644 --- a/certora/harnesses/GovernorPreventLateHarness.sol +++ b/certora/harnesses/GovernorPreventLateHarness.sol @@ -43,7 +43,7 @@ contract GovernorPreventLateHarness is } // Harness from Governor - function hashProposal(address[] memory targets,uint256[] memory values,bytes[] memory calldatas,string memory description) public returns (uint256) { + function hashProposal(address[] memory targets,uint256[] memory values,bytes[] memory calldatas,string memory description) public pure returns (uint256) { return hashProposal(targets, values, calldatas, keccak256(bytes(description))); } diff --git a/certora/specs/GovernorFunctions.spec b/certora/specs/GovernorFunctions.spec index 016c123601d..b5e6d561c2d 100644 --- a/certora/specs/GovernorFunctions.spec +++ b/certora/specs/GovernorFunctions.spec @@ -1,6 +1,8 @@ import "helpers.spec" import "Governor.helpers.spec" +use invariant queuedImplySuccess + /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ Rule: propose effect and liveness. Includes "no double proposition" │ @@ -26,9 +28,6 @@ rule propose_liveness(uint256 pId, env e) { } rule propose_effect(uint256 pId, env e) { - require nonpayable(e); - require clockSanity(e); - address[] targets; uint256[] values; bytes[] calldatas; string descr; require pId == hashProposal(targets, values, calldatas, descr); @@ -41,12 +40,7 @@ rule propose_effect(uint256 pId, env e) { assert proposalDeadline(pId) == clock(e) + votingDelay() + votingPeriod(); } -rule propose_sideeffect(uint256 pId, env e) { - require nonpayable(e); - require clockSanity(e); - - uint256 otherId; - +rule propose_sideeffect(uint256 pId, env e, uint256 otherId) { uint8 otherStateBefore = state(e, otherId); uint256 otherVoteStart = proposalSnapshot(otherId); uint256 otherVoteEnd = proposalDeadline(otherId); @@ -99,9 +93,6 @@ rule castVote_liveness(uint256 pId, env e, method f) rule castVote_effect(uint256 pId, env e, method f) filtered { f -> voting(f) } { - require nonpayable(e); - require clockSanity(e); - uint8 support; address voter; @@ -124,9 +115,6 @@ rule castVote_effect(uint256 pId, env e, method f) rule castVote_sideeffect(uint256 pId, env e, method f) filtered { f -> voting(f) } { - require nonpayable(e); - require clockSanity(e); - uint8 support; address voter; address otherVoter; @@ -168,9 +156,6 @@ rule queue_liveness(uint256 pId, env e) { } rule queue_effect(uint256 pId, env e) { - require nonpayable(e); - require clockSanity(e); - uint8 stateBefore = state(e, pId); bool queuedBefore = isQueued(pId); @@ -184,12 +169,7 @@ rule queue_effect(uint256 pId, env e) { assert !queuedBefore; } -rule queue_sideeffect(uint256 pId, env e) { - require nonpayable(e); - require clockSanity(e); - - uint256 otherId; - +rule queue_sideeffect(uint256 pId, env e, uint256 otherId) { uint8 otherStateBefore = state(e, otherId); bool otherQueuedBefore = isQueued(otherId); @@ -224,9 +204,6 @@ rule execute_liveness(uint256 pId, env e) { } rule execute_effect(uint256 pId, env e) { - require nonpayable(e); - require clockSanity(e); - address[] targets; uint256[] values; bytes[] calldatas; bytes32 descrHash; require pId == hashProposal(targets, values, calldatas, descrHash); @@ -236,12 +213,7 @@ rule execute_effect(uint256 pId, env e) { assert state(e, pId) == EXECUTED(); } -rule execute_sideeffect(uint256 pId, env e) { - require nonpayable(e); - require clockSanity(e); - - uint256 otherId; - +rule execute_sideeffect(uint256 pId, env e, uint256 otherId) { uint8 otherStateBefore = state(e, otherId); address[] targets; uint256[] values; bytes[] calldatas; bytes32 descrHash; @@ -261,6 +233,7 @@ rule execute_sideeffect(uint256 pId, env e) { rule cancel_liveness(uint256 pId, env e) { require nonpayable(e); require clockSanity(e); + requireInvariant queuedImplySuccess(pId); uint8 stateBefore = state(e, pId); @@ -277,9 +250,6 @@ rule cancel_liveness(uint256 pId, env e) { } rule cancel_effect(uint256 pId, env e) { - require nonpayable(e); - require clockSanity(e); - address[] targets; uint256[] values; bytes[] calldatas; bytes32 descrHash; require pId == hashProposal(targets, values, calldatas, descrHash); @@ -290,12 +260,7 @@ rule cancel_effect(uint256 pId, env e) { assert !isQueued(pId); // cancel resets timelockId } -rule cancel_sideeffect(uint256 pId, env e) { - require nonpayable(e); - require clockSanity(e); - - uint256 otherId; - +rule cancel_sideeffect(uint256 pId, env e, uint256 otherId) { uint8 otherStateBefore = state(e, otherId); bool otherQueuedBefore = isQueued(otherId); diff --git a/certora/specs/GovernorInvariants.spec b/certora/specs/GovernorInvariants.spec index 9be77e24457..7bbb5bf8bf1 100644 --- a/certora/specs/GovernorInvariants.spec +++ b/certora/specs/GovernorInvariants.spec @@ -40,9 +40,9 @@ invariant proposalStateConsistency(uint256 pId) └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ invariant votesImplySnapshotPassed(env e, uint256 pId) - getAgainstVotes(pId) > 0 => proposalSnapshot(pId) <= clock(e) && - getForVotes(pId) > 0 => proposalSnapshot(pId) <= clock(e) && - getAbstainVotes(pId) > 0 => proposalSnapshot(pId) <= clock(e) + (getAgainstVotes(pId) > 0 => proposalSnapshot(pId) <= clock(e)) && + (getForVotes(pId) > 0 => proposalSnapshot(pId) <= clock(e)) && + (getAbstainVotes(pId) > 0 => proposalSnapshot(pId) <= clock(e)) { preserved { require clockSanity(e); @@ -91,6 +91,19 @@ invariant queuedImplyCreated(uint pId) } } +invariant queuedImplyVoteOverAndSuccessful(env e, uint pId) + isQueued(pId) => ( + quorumReached(pId) && + voteSucceeded(pId) && + proposalDeadline(pId) < clock(e) + ) + { + preserved { + require clockSanity(e); + requireInvariant proposalStateConsistency(pId); + } + } + /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ Invariant: Votes start before it ends │ From ddaf4bccf2c195e8d4cb6b9f6943f2df08ffc54d Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 16 Mar 2023 20:56:33 +0100 Subject: [PATCH 36/61] up --- certora/specs/GovernorFunctions.spec | 4 ++-- certora/specs/GovernorInvariants.spec | 8 ++------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/certora/specs/GovernorFunctions.spec b/certora/specs/GovernorFunctions.spec index b5e6d561c2d..3f361f2b0ae 100644 --- a/certora/specs/GovernorFunctions.spec +++ b/certora/specs/GovernorFunctions.spec @@ -1,7 +1,7 @@ import "helpers.spec" import "Governor.helpers.spec" -use invariant queuedImplySuccess +use invariant queuedImplyVoteOver /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ @@ -233,7 +233,7 @@ rule execute_sideeffect(uint256 pId, env e, uint256 otherId) { rule cancel_liveness(uint256 pId, env e) { require nonpayable(e); require clockSanity(e); - requireInvariant queuedImplySuccess(pId); + requireInvariant queuedImplyVoteOver(pId); uint8 stateBefore = state(e, pId); diff --git a/certora/specs/GovernorInvariants.spec b/certora/specs/GovernorInvariants.spec index 7bbb5bf8bf1..c1676e44941 100644 --- a/certora/specs/GovernorInvariants.spec +++ b/certora/specs/GovernorInvariants.spec @@ -91,12 +91,8 @@ invariant queuedImplyCreated(uint pId) } } -invariant queuedImplyVoteOverAndSuccessful(env e, uint pId) - isQueued(pId) => ( - quorumReached(pId) && - voteSucceeded(pId) && - proposalDeadline(pId) < clock(e) - ) +invariant queuedImplyVoteOver(env e, uint pId) + isQueued(pId) => proposalDeadline(pId) < clock(e) { preserved { require clockSanity(e); From 5770dfbe36c6366fc9eca4a7c80a6eb21a9f2882 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 16 Mar 2023 21:25:16 +0100 Subject: [PATCH 37/61] wip --- certora/specs/GovernorFunctions.spec | 4 +++- certora/specs/GovernorInvariants.spec | 12 +++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/certora/specs/GovernorFunctions.spec b/certora/specs/GovernorFunctions.spec index 3f361f2b0ae..3f3c80736ec 100644 --- a/certora/specs/GovernorFunctions.spec +++ b/certora/specs/GovernorFunctions.spec @@ -1,6 +1,8 @@ import "helpers.spec" import "Governor.helpers.spec" +import "GovernorInvariants.spec" +use invariant proposalStateConsistency use invariant queuedImplyVoteOver /* @@ -233,7 +235,7 @@ rule execute_sideeffect(uint256 pId, env e, uint256 otherId) { rule cancel_liveness(uint256 pId, env e) { require nonpayable(e); require clockSanity(e); - requireInvariant queuedImplyVoteOver(pId); + requireInvariant queuedImplyVoteOver(e, pId); uint8 stateBefore = state(e, pId); diff --git a/certora/specs/GovernorInvariants.spec b/certora/specs/GovernorInvariants.spec index c1676e44941..364bf9a1d5f 100644 --- a/certora/specs/GovernorInvariants.spec +++ b/certora/specs/GovernorInvariants.spec @@ -40,12 +40,14 @@ invariant proposalStateConsistency(uint256 pId) └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ invariant votesImplySnapshotPassed(env e, uint256 pId) - (getAgainstVotes(pId) > 0 => proposalSnapshot(pId) <= clock(e)) && - (getForVotes(pId) > 0 => proposalSnapshot(pId) <= clock(e)) && - (getAbstainVotes(pId) > 0 => proposalSnapshot(pId) <= clock(e)) + ( + getAgainstVotes(pId) > 0 || + getForVotes(pId) > 0 || + getAbstainVotes(pId) > 0 + ) => proposalSnapshot(pId) < clock(e) { - preserved { - require clockSanity(e); + preserved with (env e2) { + require clock(e) == clock(e2); } } From a64bb8801cf529b656044395e2fe71c4ab9447ce Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 16 Mar 2023 21:52:20 +0100 Subject: [PATCH 38/61] update --- certora/specs.js | 53 +++++++++++++-------------- certora/specs/GovernorFunctions.spec | 4 +- certora/specs/GovernorInvariants.spec | 31 +++++++++++++--- 3 files changed, 52 insertions(+), 36 deletions(-) diff --git a/certora/specs.js b/certora/specs.js index 7caf78c06ec..ac0dce088e5 100644 --- a/certora/specs.js +++ b/certora/specs.js @@ -38,9 +38,9 @@ module.exports = [].concat( }, // Security { - "spec": "Pausable", - "contract": "PausableHarness", - "files": ["certora/harnesses/PausableHarness.sol"] + spec: 'Pausable', + contract: 'PausableHarness', + files: ['certora/harnesses/PausableHarness.sol'], }, // Proxy { @@ -77,29 +77,26 @@ module.exports = [].concat( '--optimistic_hashing', ], })), - /// WIP part - process.env.CI - ? [] - : product( - ['GovernorHarness'], - ['GovernorFunctions'], - ['ERC20VotesBlocknumberHarness'], - ['propose', 'castVote', 'queue', 'execute', 'cancel'], - ).map(([contract, spec, token, fn]) => ({ - spec, - contract, - files: [ - `certora/harnesses/${contract}.sol`, - `certora/harnesses/${token}.sol`, - `certora/harnesses/TimelockControllerHarness.sol`, - ], - options: [ - `--link ${contract}:token=${token}`, - `--link ${contract}:_timelock=TimelockControllerHarness`, - '--optimistic_loop', - '--optimistic_hashing', - '--rules', - ['liveness', 'effect', 'sideeffect'].map(rule => `${fn}_${rule}`).join(' '), - ], - })), + product( + ['GovernorHarness'], + ['GovernorFunctions'], + ['ERC20VotesBlocknumberHarness'], // 'ERC20VotesTimestampHarness' + ['propose', 'castVote', 'queue', 'execute', 'cancel'], + ).map(([contract, spec, token, fn]) => ({ + spec, + contract, + files: [ + `certora/harnesses/${contract}.sol`, + `certora/harnesses/${token}.sol`, + `certora/harnesses/TimelockControllerHarness.sol`, + ], + options: [ + `--link ${contract}:token=${token}`, + `--link ${contract}:_timelock=TimelockControllerHarness`, + '--optimistic_loop', + '--optimistic_hashing', + '--rules', + ...['liveness', 'effect', 'sideeffect'].map(kind => `${fn}_${kind}`), + ], + })), ); diff --git a/certora/specs/GovernorFunctions.spec b/certora/specs/GovernorFunctions.spec index 3f3c80736ec..1f03f2dac02 100644 --- a/certora/specs/GovernorFunctions.spec +++ b/certora/specs/GovernorFunctions.spec @@ -3,7 +3,7 @@ import "Governor.helpers.spec" import "GovernorInvariants.spec" use invariant proposalStateConsistency -use invariant queuedImplyVoteOver +use invariant queuedImplyDeadlineOver /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ @@ -235,7 +235,7 @@ rule execute_sideeffect(uint256 pId, env e, uint256 otherId) { rule cancel_liveness(uint256 pId, env e) { require nonpayable(e); require clockSanity(e); - requireInvariant queuedImplyVoteOver(e, pId); + requireInvariant queuedImplyDeadlineOver(e, pId); uint8 stateBefore = state(e, pId); diff --git a/certora/specs/GovernorInvariants.spec b/certora/specs/GovernorInvariants.spec index 364bf9a1d5f..b4257f2097c 100644 --- a/certora/specs/GovernorInvariants.spec +++ b/certora/specs/GovernorInvariants.spec @@ -36,18 +36,19 @@ invariant proposalStateConsistency(uint256 pId) /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -│ Invariant: votes recorded => proposal snapshot is in the past │ +│ Invariant: votes recorded => created │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ -invariant votesImplySnapshotPassed(env e, uint256 pId) +invariant votesImplyCreated(uint256 pId) ( getAgainstVotes(pId) > 0 || getForVotes(pId) > 0 || getAbstainVotes(pId) > 0 - ) => proposalSnapshot(pId) < clock(e) + ) => proposalCreated(pId) { - preserved with (env e2) { - require clock(e) == clock(e2); + preserved with (env e) { + require clockSanity(e); + requireInvariant proposalStateConsistency(pId); } } @@ -93,7 +94,25 @@ invariant queuedImplyCreated(uint pId) } } -invariant queuedImplyVoteOver(env e, uint pId) + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: timmings │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant votesImplySnapshotPassed(env e, uint256 pId) + ( + getAgainstVotes(pId) > 0 || + getForVotes(pId) > 0 || + getAbstainVotes(pId) > 0 + ) => proposalSnapshot(pId) < clock(e) + { + preserved with (env e2) { + require clock(e) == clock(e2); + } + } + +invariant queuedImplyDeadlineOver(env e, uint pId) isQueued(pId) => proposalDeadline(pId) < clock(e) { preserved { From 67a00ccaea62daff509bc18add4354fbd8cc40df Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 17 Mar 2023 11:14:02 +0100 Subject: [PATCH 39/61] disable specs we can't fix :/ --- certora/specs.js | 6 ++- certora/specs/GovernorBaseRules.spec | 23 +++++++--- certora/specs/GovernorStates.spec | 65 ++++++++++++++++++---------- 3 files changed, 62 insertions(+), 32 deletions(-) diff --git a/certora/specs.js b/certora/specs.js index ac0dce088e5..0b7c36cb819 100644 --- a/certora/specs.js +++ b/certora/specs.js @@ -73,8 +73,9 @@ module.exports = [].concat( options: [ `--link ${contract}:token=${token}`, `--link ${contract}:_timelock=TimelockControllerHarness`, - '--optimistic_loop', '--optimistic_hashing', + '--optimistic_loop', + '--loop_iter 3', ], })), product( @@ -93,8 +94,9 @@ module.exports = [].concat( options: [ `--link ${contract}:token=${token}`, `--link ${contract}:_timelock=TimelockControllerHarness`, - '--optimistic_loop', '--optimistic_hashing', + '--optimistic_loop', + '--loop_iter 3', '--rules', ...['liveness', 'effect', 'sideeffect'].map(kind => `${fn}_${kind}`), ], diff --git a/certora/specs/GovernorBaseRules.spec b/certora/specs/GovernorBaseRules.spec index ad51157279b..cac8da2a363 100644 --- a/certora/specs/GovernorBaseRules.spec +++ b/certora/specs/GovernorBaseRules.spec @@ -73,18 +73,27 @@ rule noDoubleVoting(uint256 pId, env e, method f) │ Rule: Voting against a proposal does not count towards quorum. │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ -rule againstVotesDontCountTowardsQuorum(uint256 pId, env e, method f) - filtered { f -> voting(f) } +rule againstVotesDontCountTowardsQuorum(uint256 pId, env e) { - address voter; - bool quorumReachedBefore = quorumReached(pId); - - helperVoteWithRevert(e, f, pId, voter, 0); // support 0 = against - + castVote(e, pId, 0); assert quorumReached(pId) == quorumReachedBefore, "quorum must not be reached with an against vote"; } +/// This version is more exaustive, but to slow because "quorumReached" is a FV nightmare +// rule againstVotesDontCountTowardsQuorum(uint256 pId, env e, method f) +// filtered { f -> voting(f) } +// { +// address voter; +// +// bool quorumReachedBefore = quorumReached(pId); +// +// helperVoteWithRevert(e, f, pId, voter, 0); // support 0 = against +// +// assert quorumReached(pId) == quorumReachedBefore, "quorum must not be reached with an against vote"; +// } + + /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ Rule: A proposal could be executed only if quorum was reached and vote succeeded │ diff --git a/certora/specs/GovernorStates.spec b/certora/specs/GovernorStates.spec index eece8bf09e5..a1dc7eadac2 100644 --- a/certora/specs/GovernorStates.spec +++ b/certora/specs/GovernorStates.spec @@ -3,7 +3,7 @@ import "Governor.helpers.spec" import "GovernorInvariants.spec" use invariant proposalStateConsistency -use invariant votesImplySnapshotPassed +// use invariant votesImplySnapshotPassed /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ @@ -131,27 +131,46 @@ rule stateIsConsistentWithVotes(uint256 pId, env e) { /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -│ Rule: `updateQuorumNumerator` cannot cause quorumReached to change. │ +│ [NEED WORK] Rule: `updateQuorumNumerator` cannot cause quorumReached to change. │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ -rule onlyVoteCanChangeQuorumReached(uint256 pId, env e, method f, calldataarg args) - filtered { f -> !skip(f) } -{ - require clockSanity(e); - requireInvariant votesImplySnapshotPassed(e, pId); - - bool quorumReachedBefore = quorumReached(pId); - - uint256 snapshot = proposalSnapshot(pId); - uint256 totalSupply = token_getPastTotalSupply(snapshot); - - f(e, args); - - // Needed because the prover doesn't understand the checkpoint properties of the voting token. - require clock(e) > snapshot => token_getPastTotalSupply(snapshot) == totalSupply; - - assert quorumReached(pId) != quorumReachedBefore => ( - !quorumReachedBefore && - votingAll(f) - ); -} +//// This would be nice, but its way to slow to run because "quorumReached" is a FV nightmare +//// Also, for it to work we need to prove that the checkpoints have (strictly) increase values ... what a nightmare +// rule onlyVoteCanChangeQuorumReached(uint256 pId, env e, method f, calldataarg args) +// filtered { f -> !skip(f) } +// { +// require clockSanity(e); +// require clock(e) > proposalSnapshot(pId); // vote has started +// require quorumNumeratorLength() < max_uint256; // sanity +// +// bool quorumReachedBefore = quorumReached(pId); +// +// uint256 snapshot = proposalSnapshot(pId); +// uint256 totalSupply = token_getPastTotalSupply(snapshot); +// +// f(e, args); +// +// // Needed because the prover doesn't understand the checkpoint properties of the voting token. +// require clock(e) > snapshot => token_getPastTotalSupply(snapshot) == totalSupply; +// +// assert quorumReached(pId) != quorumReachedBefore => ( +// !quorumReachedBefore && +// votingAll(f) +// ); +// } + +//// To prove that, we need to prove that the checkpoints have (strictly) increase values ... what a nightmare +//// otherwise it gives us counter example where the checkpoint history has keys: +//// [ 12,12,13,13,12] and the lookup obviously fail to get the correct value +// rule quorumUpdateDoesntAffectPastProposals(uint256 pId, env e) { +// require clockSanity(e); +// require clock(e) > proposalSnapshot(pId); // vote has started +// require quorumNumeratorLength() < max_uint256; // sanity +// +// bool quorumReachedBefore = quorumReached(pId); +// +// uint256 newQuorumNumerator; +// updateQuorumNumerator(e, newQuorumNumerator); +// +// assert quorumReached(pId) == quorumReachedBefore; +// } From ecec8a735341559348768aa184e94500206d65ef Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Sat, 18 Mar 2023 09:35:41 +0100 Subject: [PATCH 40/61] codespell --- certora/specs/GovernorBaseRules.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certora/specs/GovernorBaseRules.spec b/certora/specs/GovernorBaseRules.spec index cac8da2a363..ef5be882340 100644 --- a/certora/specs/GovernorBaseRules.spec +++ b/certora/specs/GovernorBaseRules.spec @@ -80,7 +80,7 @@ rule againstVotesDontCountTowardsQuorum(uint256 pId, env e) assert quorumReached(pId) == quorumReachedBefore, "quorum must not be reached with an against vote"; } -/// This version is more exaustive, but to slow because "quorumReached" is a FV nightmare +/// This version is more exhaustive, but to slow because "quorumReached" is a FV nightmare // rule againstVotesDontCountTowardsQuorum(uint256 pId, env e, method f) // filtered { f -> voting(f) } // { From 7c62ed2e8a14997567c9aa8e00b08a8f464f0639 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Sat, 18 Mar 2023 09:38:01 +0100 Subject: [PATCH 41/61] disable problematic rules --- certora/specs.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/certora/specs.js b/certora/specs.js index 0b7c36cb819..e3db84fd1cc 100644 --- a/certora/specs.js +++ b/certora/specs.js @@ -75,14 +75,13 @@ module.exports = [].concat( `--link ${contract}:_timelock=TimelockControllerHarness`, '--optimistic_hashing', '--optimistic_loop', - '--loop_iter 3', ], })), product( ['GovernorHarness'], ['GovernorFunctions'], ['ERC20VotesBlocknumberHarness'], // 'ERC20VotesTimestampHarness' - ['propose', 'castVote', 'queue', 'execute', 'cancel'], + ['castVote', 'execute'], // 'propose', 'queue', 'cancel' // timeout ).map(([contract, spec, token, fn]) => ({ spec, contract, @@ -96,7 +95,6 @@ module.exports = [].concat( `--link ${contract}:_timelock=TimelockControllerHarness`, '--optimistic_hashing', '--optimistic_loop', - '--loop_iter 3', '--rules', ...['liveness', 'effect', 'sideeffect'].map(kind => `${fn}_${kind}`), ], From 8d029476af1001b9c1efa6d4874199dc781ad670 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Sat, 18 Mar 2023 21:53:17 +0100 Subject: [PATCH 42/61] test function with both clocks --- certora/specs.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/certora/specs.js b/certora/specs.js index e3db84fd1cc..eebec974f27 100644 --- a/certora/specs.js +++ b/certora/specs.js @@ -80,8 +80,8 @@ module.exports = [].concat( product( ['GovernorHarness'], ['GovernorFunctions'], - ['ERC20VotesBlocknumberHarness'], // 'ERC20VotesTimestampHarness' - ['castVote', 'execute'], // 'propose', 'queue', 'cancel' // timeout + ['ERC20VotesBlocknumberHarness', 'ERC20VotesTimestampHarness'], + ['castVote', 'execute'], // 'propose', 'queue', 'cancel' // these rules timeout/fail ).map(([contract, spec, token, fn]) => ({ spec, contract, From 5e71c01bcc14b41ef1df3291978c947a04a64ce2 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 25 Apr 2023 11:08:38 +0200 Subject: [PATCH 43/61] =?UTF-8?q?rename=20valid=20=E2=86=92=20sanity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- certora/specs/Governor.helpers.spec | 8 ++++---- certora/specs/GovernorFunctions.spec | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/certora/specs/Governor.helpers.spec b/certora/specs/Governor.helpers.spec index 968f0b0ee91..67cadd61929 100644 --- a/certora/specs/Governor.helpers.spec +++ b/certora/specs/Governor.helpers.spec @@ -19,11 +19,11 @@ function validProposal(address[] targets, uint256[] values, bytes[] calldatas) r && targets.length == calldatas.length; } -function validString(string s) returns bool { +function sanityString(string s) returns bool { return s.length < 0xffff; } -function validBytes(bytes b) returns bool { +function sanityBytes(bytes b) returns bool { return b.length < 0xffff; } @@ -88,13 +88,13 @@ function helperVoteWithRevert(env e, method f, uint256 pId, address voter, uint8 else if (f.selector == castVoteWithReason(uint256,uint8,string).selector) { string reason; - require e.msg.sender == voter && validString(reason); + require e.msg.sender == voter && sanityString(reason); return castVoteWithReason@withrevert(e, pId, support, reason); } else if (f.selector == castVoteWithReasonAndParams(uint256,uint8,string,bytes).selector) { string reason; bytes params; - require e.msg.sender == voter && validString(reason) && validBytes(params); + require e.msg.sender == voter && sanityString(reason) && sanityBytes(params); return castVoteWithReasonAndParams@withrevert(e, pId, support, reason, params); } else diff --git a/certora/specs/GovernorFunctions.spec b/certora/specs/GovernorFunctions.spec index 1f03f2dac02..beb9b0c6bda 100644 --- a/certora/specs/GovernorFunctions.spec +++ b/certora/specs/GovernorFunctions.spec @@ -18,7 +18,7 @@ rule propose_liveness(uint256 pId, env e) { address[] targets; uint256[] values; bytes[] calldatas; string descr; require pId == hashProposal(targets, values, calldatas, descr); - //require validString(descr); + //require sanityString(descr); propose@withrevert(e, targets, values, calldatas, descr); From e072521fcb660302049a345f168e210dff7072d6 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 25 Apr 2023 12:04:22 +0200 Subject: [PATCH 44/61] fix harness --- certora/harnesses/GovernorHarness.sol | 4 ---- 1 file changed, 4 deletions(-) diff --git a/certora/harnesses/GovernorHarness.sol b/certora/harnesses/GovernorHarness.sol index a6b1a273899..4d26775db2f 100644 --- a/certora/harnesses/GovernorHarness.sol +++ b/certora/harnesses/GovernorHarness.sol @@ -48,10 +48,6 @@ contract GovernorHarness is return _executor(); } - function proposalProposer(uint256 proposalId) public view returns (address) { - return _proposalProposer(proposalId); - } - function quorumReached(uint256 proposalId) public view returns (bool) { return _quorumReached(proposalId); } From 5af91670304ffe39da38d07714a28483b109c480 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 25 Apr 2023 16:15:43 +0200 Subject: [PATCH 45/61] address PR comments --- certora/specs/Governor.helpers.spec | 25 +++++++- certora/specs/GovernorBaseRules.spec | 65 +++++++------------- certora/specs/GovernorChanges.spec | 2 +- certora/specs/GovernorFunctions.spec | 1 - certora/specs/GovernorInvariants.spec | 6 +- certora/specs/GovernorPreventLateQuorum.spec | 4 +- certora/specs/GovernorStates.spec | 57 ++++++++--------- requirements.txt | 2 +- 8 files changed, 82 insertions(+), 80 deletions(-) diff --git a/certora/specs/Governor.helpers.spec b/certora/specs/Governor.helpers.spec index 67cadd61929..64ec0a684af 100644 --- a/certora/specs/Governor.helpers.spec +++ b/certora/specs/Governor.helpers.spec @@ -42,6 +42,8 @@ definition QUEUED() returns uint8 = 5; definition EXPIRED() returns uint8 = 6; definition EXECUTED() returns uint8 = 7; +// This helper is an alternative to state(e, pId) that will return UNSET() instead of reverting when then proposal +// does not exist (not created yet) function safeState(env e, uint256 pId) returns uint8 { return proposalCreated(pId) ? state(e, pId): UNSET(); } @@ -54,7 +56,7 @@ definition proposalCreated(uint256 pId) returns bool = │ Filters │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ -definition skip(method f) returns bool = +definition assumedSafe(method f) returns bool = f.isView || f.isFallback || f.selector == relay(address,uint256,bytes).selector || @@ -62,6 +64,19 @@ definition skip(method f) returns bool = f.selector == onERC1155Received(address,address,uint256,uint256,bytes).selector || f.selector == onERC1155BatchReceived(address,address,uint256[],uint256[],bytes).selector; +// These function are covered by helperFunctionsWithRevert +definition operateOnProposal(method f) returns bool = + f.selector == propose(address[],uint256[],bytes[],string).selector || + f.selector == queue(address[],uint256[],bytes[],bytes32).selector || + f.selector == execute(address[],uint256[],bytes[],bytes32).selector || + f.selector == cancel(address[],uint256[],bytes[],bytes32).selector || + f.selector == castVote(uint256,uint8).selector || + f.selector == castVoteWithReason(uint256,uint8,string).selector || + f.selector == castVoteWithReasonAndParams(uint256,uint8,string,bytes).selector || + f.selector == castVoteBySig(uint256,uint8,uint8,bytes32,bytes32).selector || + f.selector == castVoteWithReasonAndParamsBySig(uint256,uint8,string,bytes,uint8,bytes32,bytes32).selector + +// These function are covered by helperVoteWithRevert definition voting(method f) returns bool = f.selector == castVote(uint256,uint8).selector || f.selector == castVoteWithReason(uint256,uint8,string).selector || @@ -105,6 +120,14 @@ function helperVoteWithRevert(env e, method f, uint256 pId, address voter, uint8 } } +// Governor function that operates on a given proposalId may or may not include the proposalId in the arguments. This +// helper restricts the call to method `f` in a way that it's operating on a specific proposal. +// +// This can be used to say "consider any function call that operates on proposal `pId`" or "consider a propose call +// that corresponds to a given pId". +// +// This is for example used when proving that not 2 proposals can be proposed with the same id: Once the proposal is +// proposed a first time, we want to prove that "any propose call that corresponds to the same id should revert". function helperFunctionsWithRevert(env e, method f, uint256 pId) { if (f.selector == propose(address[],uint256[],bytes[],string).selector) { diff --git a/certora/specs/GovernorBaseRules.spec b/certora/specs/GovernorBaseRules.spec index ef5be882340..d1dc2958129 100644 --- a/certora/specs/GovernorBaseRules.spec +++ b/certora/specs/GovernorBaseRules.spec @@ -27,7 +27,7 @@ rule noDoublePropose(uint256 pId, env e) { └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ rule immutableFieldsAfterProposalCreation(uint256 pId, env e, method f, calldataarg args) - filtered { f -> !skip(f) } + filtered { f -> !assumedSafe(f) } { require proposalCreated(pId); @@ -46,13 +46,9 @@ rule immutableFieldsAfterProposalCreation(uint256 pId, env e, method f, calldata ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ Rule: A user cannot vote twice │ │ │ -│ Checked for castVote only. all 3 castVote functions call _castVote, so the completeness of the verification is │ -│ counted on the fact that the 3 functions themselves makes no changes, 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 separately 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. │ +│ This rule is checked for castVote, castVoteWithReason and castVoteWithReasonAndParams. For the signature variants │ +│ (castVoteBySig and castVoteWithReasonAndParamsBySig) we basically assume that the signature referendum is correct │ +│ without checking it. │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ rule noDoubleVoting(uint256 pId, env e, method f) @@ -76,31 +72,20 @@ rule noDoubleVoting(uint256 pId, env e, method f) rule againstVotesDontCountTowardsQuorum(uint256 pId, env e) { bool quorumReachedBefore = quorumReached(pId); + + // Ideally we would use `helperVoteWithRevert` here, but it causes timeout. Consider changing it if/when the prover improves. castVote(e, pId, 0); + assert quorumReached(pId) == quorumReachedBefore, "quorum must not be reached with an against vote"; } -/// This version is more exhaustive, but to slow because "quorumReached" is a FV nightmare -// rule againstVotesDontCountTowardsQuorum(uint256 pId, env e, method f) -// filtered { f -> voting(f) } -// { -// address voter; -// -// bool quorumReachedBefore = quorumReached(pId); -// -// helperVoteWithRevert(e, f, pId, voter, 0); // support 0 = against -// -// assert quorumReached(pId) == quorumReachedBefore, "quorum must not be reached with an against vote"; -// } - - /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ Rule: A proposal could be executed only if quorum was reached and vote succeeded │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ rule executionOnlyIfQuoromReachedAndVoteSucceeded(uint256 pId, env e, method f, calldataarg args) - filtered { f -> !skip(f) } + filtered { f -> !assumedSafe(f) } { require !isExecuted(pId); @@ -118,7 +103,7 @@ rule executionOnlyIfQuoromReachedAndVoteSucceeded(uint256 pId, env e, method f, └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ rule noStartBeforeCreation(uint256 pId, env e, method f, calldataarg args) - filtered { f -> !skip(f) } + filtered { f -> !assumedSafe(f) } { require !proposalCreated(pId); @@ -133,7 +118,7 @@ rule noStartBeforeCreation(uint256 pId, env e, method f, calldataarg args) └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ rule noExecuteBeforeDeadline(uint256 pId, env e, method f, calldataarg args) - filtered { f -> !skip(f) } + filtered { f -> !assumedSafe(f) } { require !isExecuted(pId); @@ -149,7 +134,7 @@ rule noExecuteBeforeDeadline(uint256 pId, env e, method f, calldataarg args) */ invariant quorumRatioLessThanOne() quorumNumerator() <= quorumDenominator() - filtered { f -> !skip(f) } + filtered { f -> !assumedSafe(f) } { preserved { require quorumNumeratorLength() < max_uint256; @@ -160,16 +145,14 @@ invariant quorumRatioLessThanOne() ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ Rule: 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. │ +│ In this rule we show that if a function is executed, i.e. execute() was called on the proposal ID, none of the │ +│ proposal specific functions can make changes again. Note that we prove that only the `execute()` function can set | +| isExecuted() to true in in `GorvernorChanges.spec`. | └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ -rule allFunctionsRevertIfExecuted(uint256 pId, env e, method f, calldataarg args) filtered { f -> - !skip(f) && - f.selector != updateQuorumNumerator(uint256).selector && - f.selector != updateTimelock(address).selector -} { +rule allFunctionsRevertIfExecuted(uint256 pId, env e, method f, calldataarg args) + filtered { f -> operateOnProposal(f) } +{ require isExecuted(pId); requireInvariant noBothExecutedAndCanceled(pId); requireInvariant executedImplyCreated(pId); @@ -184,15 +167,13 @@ rule allFunctionsRevertIfExecuted(uint256 pId, env e, method f, calldataarg args │ Rule: All proposal specific (non-view) functions should revert if proposal is canceled │ │ │ │ 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. │ +│ proposal specific functions can make changes again. Note that we prove that only the `execute()` function can set | +| isExecuted() to true in in `GorvernorChanges.spec`. | └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ -rule allFunctionsRevertIfCanceled(uint256 pId, env e, method f, calldataarg args) filtered { f -> - !skip(f) && - f.selector != updateQuorumNumerator(uint256).selector && - f.selector != updateTimelock(address).selector -} { +rule allFunctionsRevertIfCanceled(uint256 pId, env e, method f, calldataarg args) + filtered { f -> operateOnProposal(f) } +{ require isCanceled(pId); requireInvariant noBothExecutedAndCanceled(pId); requireInvariant canceledImplyCreated(pId); @@ -208,7 +189,7 @@ rule allFunctionsRevertIfCanceled(uint256 pId, env e, method f, calldataarg args └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ rule privilegedUpdate(env e, method f, calldataarg args) - filtered { f -> !skip(f) } + filtered { f -> !assumedSafe(f) } { address executorBefore = getExecutor(); uint256 quorumNumeratorBefore = quorumNumerator(); diff --git a/certora/specs/GovernorChanges.spec b/certora/specs/GovernorChanges.spec index 6ab149fad51..bce702156b1 100644 --- a/certora/specs/GovernorChanges.spec +++ b/certora/specs/GovernorChanges.spec @@ -10,7 +10,7 @@ use invariant proposalStateConsistency └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ rule changes(uint256 pId, env e, method f, calldataarg args) - filtered { f -> !skip(f) } + filtered { f -> !assumedSafe(f) } { require clockSanity(e); requireInvariant proposalStateConsistency(pId); diff --git a/certora/specs/GovernorFunctions.spec b/certora/specs/GovernorFunctions.spec index beb9b0c6bda..fd6ea41912b 100644 --- a/certora/specs/GovernorFunctions.spec +++ b/certora/specs/GovernorFunctions.spec @@ -18,7 +18,6 @@ rule propose_liveness(uint256 pId, env e) { address[] targets; uint256[] values; bytes[] calldatas; string descr; require pId == hashProposal(targets, values, calldatas, descr); - //require sanityString(descr); propose@withrevert(e, targets, values, calldatas, descr); diff --git a/certora/specs/GovernorInvariants.spec b/certora/specs/GovernorInvariants.spec index b4257f2097c..b4325ba3657 100644 --- a/certora/specs/GovernorInvariants.spec +++ b/certora/specs/GovernorInvariants.spec @@ -97,7 +97,7 @@ invariant queuedImplyCreated(uint pId) /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -│ Invariant: timmings │ +│ Invariant: timings │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ invariant votesImplySnapshotPassed(env e, uint256 pId) @@ -108,7 +108,9 @@ invariant votesImplySnapshotPassed(env e, uint256 pId) ) => proposalSnapshot(pId) < clock(e) { preserved with (env e2) { - require clock(e) == clock(e2); + // In this invariant, `env e` is representing the present. And `clock(e)` the current timestamp. + // It should hold for any transitions in the pasts + require clock(e2) <= clock(e); } } diff --git a/certora/specs/GovernorPreventLateQuorum.spec b/certora/specs/GovernorPreventLateQuorum.spec index 808c45ae97e..97f4c8f6f27 100644 --- a/certora/specs/GovernorPreventLateQuorum.spec +++ b/certora/specs/GovernorPreventLateQuorum.spec @@ -29,7 +29,7 @@ use invariant votesImplySnapshotPassed └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ rule deadlineChangeToPreventLateQuorum(uint256 pId, env e, method f, calldataarg args) - filtered { f -> !skip(f) } + filtered { f -> !assumedSafe(f) } { require clockSanity(e); requireInvariant proposalStateConsistency(pId); @@ -44,7 +44,7 @@ rule deadlineChangeToPreventLateQuorum(uint256 pId, env e, method f, calldataarg bool deadlineExtendedAfter = getExtendedDeadline(pId) > 0; // deadline can never be reduced - assert deadlineBefore <= proposalDeadline(pId); + assert deadlineBefore <= deadlineAfter; // deadline can only be extended in proposal or on cast vote assert deadlineAfter != deadlineBefore => ( diff --git a/certora/specs/GovernorStates.spec b/certora/specs/GovernorStates.spec index a1dc7eadac2..891b42e5c53 100644 --- a/certora/specs/GovernorStates.spec +++ b/certora/specs/GovernorStates.spec @@ -3,7 +3,7 @@ import "Governor.helpers.spec" import "GovernorInvariants.spec" use invariant proposalStateConsistency -// use invariant votesImplySnapshotPassed +use invariant votesImplySnapshotPassed /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ @@ -29,32 +29,21 @@ rule stateConsistency(env e, uint256 pId) { └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ rule stateTransitionFn(uint256 pId, env e, method f, calldataarg args) - filtered { f -> !skip(f) } + filtered { f -> !assumedSafe(f) } { require clockSanity(e); + require quorumNumeratorLength() < max_uint256; // sanity uint8 stateBefore = state(e, pId); f(e, args); uint8 stateAfter = state(e, pId); assert (stateBefore != stateAfter) => ( - stateBefore == UNSET() => ( - stateAfter == PENDING() && f.selector == propose(address[],uint256[],bytes[],string).selector - ) && - stateBefore == PENDING() => ( - (stateAfter == CANCELED() && f.selector == cancel(address[],uint256[],bytes[],bytes32).selector) - ) && - stateBefore == SUCCEEDED() => ( - (stateAfter == QUEUED() && f.selector == queue(address[],uint256[],bytes[],bytes32).selector) || - (stateAfter == EXECUTED() && f.selector == execute(address[],uint256[],bytes[],bytes32).selector) - ) && - stateBefore == QUEUED() => ( - (stateAfter == EXECUTED() && f.selector == execute(address[],uint256[],bytes[],bytes32).selector) - ) && - stateBefore == ACTIVE() => false && - stateBefore == CANCELED() => false && - stateBefore == DEFEATED() => false && - stateBefore == EXECUTED() => false + (stateBefore == UNSET() && stateAfter == PENDING() && f.selector == propose(address[],uint256[],bytes[],string).selector ) || + (stateBefore == PENDING() && stateAfter == CANCELED() && f.selector == cancel(address[],uint256[],bytes[],bytes32).selector ) || + (stateBefore == SUCCEEDED() && stateAfter == QUEUED() && f.selector == queue(address[],uint256[],bytes[],bytes32).selector ) || + (stateBefore == SUCCEEDED() && stateAfter == EXECUTED() && f.selector == execute(address[],uint256[],bytes[],bytes32).selector) || + (stateBefore == QUEUED() && stateAfter == EXECUTED() && f.selector == execute(address[],uint256[],bytes[],bytes32).selector) ); } @@ -68,18 +57,26 @@ rule stateTransitionWait(uint256 pId, env e1, env e2) { require clockSanity(e2); require clock(e2) > clock(e1); + // Force the state to be consistent with e1 (before). We want the storage related to `pId` to match what is + // possible before the time passes. We don't want the state transition include elements that cannot have happened + // before e1. This ensure that the e1 → e2 state transition is purelly a consequence of time passing. + requireInvariant votesImplySnapshotPassed(e1, pId); + uint8 stateBefore = state(e1, pId); uint8 stateAfter = state(e2, pId); assert (stateBefore != stateAfter) => ( - stateBefore == PENDING() => stateAfter == ACTIVE() && - stateBefore == ACTIVE() => (stateAfter == SUCCEEDED() || stateAfter == DEFEATED()) && - stateBefore == UNSET() => false && - stateBefore == SUCCEEDED() => false && - stateBefore == QUEUED() => false && - stateBefore == CANCELED() => false && - stateBefore == DEFEATED() => false && - stateBefore == EXECUTED() => false + (stateBefore == PENDING() && stateAfter == ACTIVE() ) || + (stateBefore == PENDING() && stateAfter == DEFEATED() ) || + (stateBefore == ACTIVE() && stateAfter == SUCCEEDED()) || + (stateBefore == ACTIVE() && stateAfter == DEFEATED() ) || + // Strange consequence of the timelock binding: + // When transitioning from ACTIVE to SUCCEEDED (because of the clock moving forward) the proposal state in + // the timelock is suddenly considered. Prior state set in the timelock can cause the proposal to already be + // queued, executed or canceled. + (stateBefore == ACTIVE() && stateAfter == CANCELED()) || + (stateBefore == ACTIVE() && stateAfter == EXECUTED()) || + (stateBefore == ACTIVE() && stateAfter == QUEUED()) ); } @@ -135,9 +132,9 @@ rule stateIsConsistentWithVotes(uint256 pId, env e) { └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ //// This would be nice, but its way to slow to run because "quorumReached" is a FV nightmare -//// Also, for it to work we need to prove that the checkpoints have (strictly) increase values ... what a nightmare +//// Also, for it to work we need to prove that the checkpoints have (strictly) increasing keys. // rule onlyVoteCanChangeQuorumReached(uint256 pId, env e, method f, calldataarg args) -// filtered { f -> !skip(f) } +// filtered { f -> !assumedSafe(f) } // { // require clockSanity(e); // require clock(e) > proposalSnapshot(pId); // vote has started @@ -159,7 +156,7 @@ rule stateIsConsistentWithVotes(uint256 pId, env e) { // ); // } -//// To prove that, we need to prove that the checkpoints have (strictly) increase values ... what a nightmare +//// To prove that, we need to prove that the checkpoints have (strictly) increasing keys. //// otherwise it gives us counter example where the checkpoint history has keys: //// [ 12,12,13,13,12] and the lookup obviously fail to get the correct value // rule quorumUpdateDoesntAffectPastProposals(uint256 pId, env e) { diff --git a/requirements.txt b/requirements.txt index da3e95766cb..5aad982b2f5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -certora-cli==3.6.4 +certora-cli==3.6.8 From 8339187625c569fc690632ffb15a71c63fa7c9f9 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 25 Apr 2023 22:28:47 +0200 Subject: [PATCH 46/61] fix --- certora/specs.js | 14 +++++++------- certora/specs/Governor.helpers.spec | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/certora/specs.js b/certora/specs.js index 8c6232a9b2c..b5ed38fff7e 100644 --- a/certora/specs.js +++ b/certora/specs.js @@ -37,10 +37,10 @@ module.exports = [].concat( options: ['--link ERC20WrapperHarness:_underlying=ERC20PermitHarness', '--optimistic_loop'], }, { - spec: "ERC721", - contract: "ERC721Harness", - files: ["certora/harnesses/ERC721Harness.sol", "certora/harnesses/ERC721ReceiverHarness.sol"], - options: ["--optimistic_loop"], + spec: 'ERC721', + contract: 'ERC721Harness', + files: ['certora/harnesses/ERC721Harness.sol', 'certora/harnesses/ERC721ReceiverHarness.sol'], + options: ['--optimistic_loop'], }, // Security { @@ -56,9 +56,9 @@ module.exports = [].concat( }, // Structures { - spec: "DoubleEndedQueue", - contract: "DoubleEndedQueueHarness", - files: ["certora/harnesses/DoubleEndedQueueHarness.sol"], + spec: 'DoubleEndedQueue', + contract: 'DoubleEndedQueueHarness', + files: ['certora/harnesses/DoubleEndedQueueHarness.sol'], }, // Governance { diff --git a/certora/specs/Governor.helpers.spec b/certora/specs/Governor.helpers.spec index 64ec0a684af..b923f7cfc99 100644 --- a/certora/specs/Governor.helpers.spec +++ b/certora/specs/Governor.helpers.spec @@ -74,7 +74,7 @@ definition operateOnProposal(method f) returns bool = f.selector == castVoteWithReason(uint256,uint8,string).selector || f.selector == castVoteWithReasonAndParams(uint256,uint8,string,bytes).selector || f.selector == castVoteBySig(uint256,uint8,uint8,bytes32,bytes32).selector || - f.selector == castVoteWithReasonAndParamsBySig(uint256,uint8,string,bytes,uint8,bytes32,bytes32).selector + f.selector == castVoteWithReasonAndParamsBySig(uint256,uint8,string,bytes,uint8,bytes32,bytes32).selector; // These function are covered by helperVoteWithRevert definition voting(method f) returns bool = From e9284661838b244a935b6b275c73818c4be77116 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 3 May 2023 09:18:48 +0200 Subject: [PATCH 47/61] Do not run the FV workflow automatically on master --- .github/workflows/formal-verification.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/formal-verification.yml b/.github/workflows/formal-verification.yml index f94152a529b..ae5eba0065a 100644 --- a/.github/workflows/formal-verification.yml +++ b/.github/workflows/formal-verification.yml @@ -1,10 +1,6 @@ name: formal verification on: - push: - branches: - - master - - release-v* pull_request: types: - opened From 0daafdb01eee77f0f45cb1d9efad91296722b80a Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 3 May 2023 12:31:07 +0200 Subject: [PATCH 48/61] fix harness --- certora/harnesses/GovernorPreventLateHarness.sol | 4 ---- 1 file changed, 4 deletions(-) diff --git a/certora/harnesses/GovernorPreventLateHarness.sol b/certora/harnesses/GovernorPreventLateHarness.sol index dc306d63e76..1adb8eb9080 100644 --- a/certora/harnesses/GovernorPreventLateHarness.sol +++ b/certora/harnesses/GovernorPreventLateHarness.sol @@ -51,10 +51,6 @@ contract GovernorPreventLateHarness is return _executor(); } - function proposalProposer(uint256 proposalId) public view returns (address) { - return _proposalProposer(proposalId); - } - function quorumReached(uint256 proposalId) public view returns (bool) { return _quorumReached(proposalId); } From 75d6f5a42cc773ea07f24420709dbf754b2d5123 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 3 May 2023 17:52:59 +0200 Subject: [PATCH 49/61] move Governor.helpers.spec to helpers folder --- certora/specs/GovernorBaseRules.spec | 4 ++-- certora/specs/GovernorChanges.spec | 4 ++-- certora/specs/GovernorFunctions.spec | 4 ++-- certora/specs/GovernorInvariants.spec | 4 ++-- certora/specs/GovernorPreventLateQuorum.spec | 4 ++-- certora/specs/GovernorStates.spec | 4 ++-- certora/specs/{ => helpers}/Governor.helpers.spec | 0 7 files changed, 12 insertions(+), 12 deletions(-) rename certora/specs/{ => helpers}/Governor.helpers.spec (100%) diff --git a/certora/specs/GovernorBaseRules.spec b/certora/specs/GovernorBaseRules.spec index d1dc2958129..2f206de2257 100644 --- a/certora/specs/GovernorBaseRules.spec +++ b/certora/specs/GovernorBaseRules.spec @@ -1,5 +1,5 @@ -import "helpers.spec" -import "Governor.helpers.spec" +import "helpers/helpers.spec" +import "helpers/Governor.helpers.spec" import "GovernorInvariants.spec" use invariant proposalStateConsistency diff --git a/certora/specs/GovernorChanges.spec b/certora/specs/GovernorChanges.spec index bce702156b1..2940500357a 100644 --- a/certora/specs/GovernorChanges.spec +++ b/certora/specs/GovernorChanges.spec @@ -1,5 +1,5 @@ -import "helpers.spec" -import "Governor.helpers.spec" +import "helpers/helpers.spec" +import "helpers/Governor.helpers.spec" import "GovernorInvariants.spec" use invariant proposalStateConsistency diff --git a/certora/specs/GovernorFunctions.spec b/certora/specs/GovernorFunctions.spec index fd6ea41912b..11560bdd7fb 100644 --- a/certora/specs/GovernorFunctions.spec +++ b/certora/specs/GovernorFunctions.spec @@ -1,5 +1,5 @@ -import "helpers.spec" -import "Governor.helpers.spec" +import "helpers/helpers.spec" +import "helpers/Governor.helpers.spec" import "GovernorInvariants.spec" use invariant proposalStateConsistency diff --git a/certora/specs/GovernorInvariants.spec b/certora/specs/GovernorInvariants.spec index b4325ba3657..255c5c3449c 100644 --- a/certora/specs/GovernorInvariants.spec +++ b/certora/specs/GovernorInvariants.spec @@ -1,5 +1,5 @@ -import "helpers.spec" -import "Governor.helpers.spec" +import "helpers/helpers.spec" +import "helpers/Governor.helpers.spec" /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ diff --git a/certora/specs/GovernorPreventLateQuorum.spec b/certora/specs/GovernorPreventLateQuorum.spec index 97f4c8f6f27..c12f6ae8a95 100644 --- a/certora/specs/GovernorPreventLateQuorum.spec +++ b/certora/specs/GovernorPreventLateQuorum.spec @@ -1,5 +1,5 @@ -import "helpers.spec" -import "Governor.helpers.spec" +import "helpers/helpers.spec" +import "helpers/Governor.helpers.spec" import "GovernorInvariants.spec" methods { diff --git a/certora/specs/GovernorStates.spec b/certora/specs/GovernorStates.spec index 891b42e5c53..2b1746bd31b 100644 --- a/certora/specs/GovernorStates.spec +++ b/certora/specs/GovernorStates.spec @@ -1,5 +1,5 @@ -import "helpers.spec" -import "Governor.helpers.spec" +import "helpers/helpers.spec" +import "helpers/Governor.helpers.spec" import "GovernorInvariants.spec" use invariant proposalStateConsistency diff --git a/certora/specs/Governor.helpers.spec b/certora/specs/helpers/Governor.helpers.spec similarity index 100% rename from certora/specs/Governor.helpers.spec rename to certora/specs/helpers/Governor.helpers.spec From 3d9ef789cc81899367061d82a10b24447d23aba4 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 3 May 2023 18:40:46 +0200 Subject: [PATCH 50/61] fix import path --- certora/specs/helpers/Governor.helpers.spec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/certora/specs/helpers/Governor.helpers.spec b/certora/specs/helpers/Governor.helpers.spec index b923f7cfc99..de452c68974 100644 --- a/certora/specs/helpers/Governor.helpers.spec +++ b/certora/specs/helpers/Governor.helpers.spec @@ -1,6 +1,6 @@ import "helpers.spec" -import "methods/IGovernor.spec" -import "methods/ITimelockController.spec" +import "../methods/IGovernor.spec" +import "../methods/ITimelockController.spec" /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ From 2a6ccebfb7a7c080ef8bcf547115b65731665ba2 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 4 May 2023 10:20:40 +0200 Subject: [PATCH 51/61] Fix early reporting of FV prover's output --- certora/run.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/certora/run.js b/certora/run.js index 2dc7d01817a..24a676d945e 100755 --- a/certora/run.js +++ b/certora/run.js @@ -75,13 +75,16 @@ async function runCertora(spec, contract, files, options = []) { // as soon as we have a jobStatus link, print it stream.on('data', function logStatusUrl(data) { - const urls = data.toString('utf8').match(/https?:\S*/g); - for (const url of urls ?? []) { - if (url.includes('/jobStatus/')) { - console.error(`[${spec}] ${url.replace('/jobStatus/', '/output/')}`); - stream.off('data', logStatusUrl); - break; - } + const { '-DjobId': jobId, '-DuserId': userId } = Object.fromEntries( + data + .toString('utf8') + .match(/-D\S+=\S+/g) + ?.map(s => s.split('=')) || [], + ); + + if (jobId && userId) { + console.error(`[${spec}] https://prover.certora.com/output/${userId}/${jobId}/`); + stream.off('data', logStatusUrl); } }); @@ -98,7 +101,7 @@ async function runCertora(spec, contract, files, options = []) { stream.end(); // write results in markdown format - writeEntry(spec, contract, code || signal, (await output).match(/https:\S*/)?.[0]); + writeEntry(spec, contract, code || signal, (await output).match(/https:\/\/prover.certora.com\/output\/\S*/)?.[0]); // write all details console.error(`+ certoraRun ${args.join(' ')}\n` + (await output)); From 7c37ea0ff677b916acfefe7b3c4fd7531ddc4ae2 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 4 May 2023 10:23:59 +0200 Subject: [PATCH 52/61] fix rewrite --- certora/run.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certora/run.js b/certora/run.js index 24a676d945e..3fb2c6c5689 100755 --- a/certora/run.js +++ b/certora/run.js @@ -139,8 +139,8 @@ function writeEntry(spec, contract, success, url) { spec, contract, success ? ':x:' : ':heavy_check_mark:', + url ? `[link](${url?.replace('/output/', '/jobStatus/')})` : 'error', url ? `[link](${url})` : 'error', - url ? `[link](${url?.replace('/jobStatus/', '/output/')})` : 'error', ), ); } From e83fdf0828814747fac5e645942554be51e95ca8 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 4 May 2023 11:32:39 +0200 Subject: [PATCH 53/61] trying to fix timeout --- certora/specs/GovernorStates.spec | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/certora/specs/GovernorStates.spec b/certora/specs/GovernorStates.spec index 2b1746bd31b..84b43b39458 100644 --- a/certora/specs/GovernorStates.spec +++ b/certora/specs/GovernorStates.spec @@ -29,7 +29,12 @@ rule stateConsistency(env e, uint256 pId) { └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ rule stateTransitionFn(uint256 pId, env e, method f, calldataarg args) - filtered { f -> !assumedSafe(f) } + filtered { f -> !assumedSafe(f) + && f.selector != castVoteBySig(uint256,uint8,uint8,bytes32,bytes32).selector + && f.selector != castVoteWithReasonAndParams(uint256,uint8,string,bytes).selector + && f.selector != castVoteWithReason(uint256,uint8,string).selector + && f.selector != castVoteWithReasonAndParamsBySig(uint256,uint8,string,bytes,uint8,bytes32,bytes32).selector + } { require clockSanity(e); require quorumNumeratorLength() < max_uint256; // sanity From fd5f309d86c82227ff1623b66d8310b1d67d5d34 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 4 May 2023 16:51:18 +0200 Subject: [PATCH 54/61] improve stateTransitionWait --- certora/diff/governance_Governor.sol.patch | 6 +++--- ...extensions_GovernorPreventLateQuorum.sol.patch | 6 +++--- ...e_extensions_GovernorTimelockControl.sol.patch | 14 ++++++++++++-- ...tensions_GovernorVotesQuorumFraction.sol.patch | 8 ++++---- certora/diff/token_ERC721_ERC721.sol.patch | 4 ++-- certora/run.js | 2 +- certora/specs/GovernorStates.spec | 15 +++++++-------- certora/specs/methods/IGovernor.spec | 1 + 8 files changed, 33 insertions(+), 23 deletions(-) diff --git a/certora/diff/governance_Governor.sol.patch b/certora/diff/governance_Governor.sol.patch index 1e9d6321544..7bdca832ee9 100644 --- a/certora/diff/governance_Governor.sol.patch +++ b/certora/diff/governance_Governor.sol.patch @@ -1,6 +1,6 @@ ---- governance/Governor.sol 2023-03-07 10:48:47.730155491 +0100 -+++ governance/Governor.sol 2023-03-14 22:09:12.664754077 +0100 -@@ -216,6 +216,21 @@ +--- governance/Governor.sol 2023-05-03 09:17:45.566699712 +0200 ++++ governance/Governor.sol 2023-05-04 15:18:42.667741565 +0200 +@@ -224,6 +224,21 @@ return _proposals[proposalId].proposer; } diff --git a/certora/diff/governance_extensions_GovernorPreventLateQuorum.sol.patch b/certora/diff/governance_extensions_GovernorPreventLateQuorum.sol.patch index 05f72f8a98a..1d387b37684 100644 --- a/certora/diff/governance_extensions_GovernorPreventLateQuorum.sol.patch +++ b/certora/diff/governance_extensions_GovernorPreventLateQuorum.sol.patch @@ -1,6 +1,6 @@ ---- governance/extensions/GovernorPreventLateQuorum.sol 2023-03-15 17:13:06.879632860 +0100 -+++ governance/extensions/GovernorPreventLateQuorum.sol 2023-03-15 14:14:59.121060484 +0100 -@@ -84,6 +84,11 @@ +--- governance/extensions/GovernorPreventLateQuorum.sol 2023-05-03 09:17:45.566699712 +0200 ++++ governance/extensions/GovernorPreventLateQuorum.sol 2023-05-04 15:18:42.657742113 +0200 +@@ -82,6 +82,11 @@ return _voteExtension; } diff --git a/certora/diff/governance_extensions_GovernorTimelockControl.sol.patch b/certora/diff/governance_extensions_GovernorTimelockControl.sol.patch index f1688e2abc1..7213188a642 100644 --- a/certora/diff/governance_extensions_GovernorTimelockControl.sol.patch +++ b/certora/diff/governance_extensions_GovernorTimelockControl.sol.patch @@ -1,5 +1,5 @@ ---- governance/extensions/GovernorTimelockControl.sol 2023-03-14 15:48:49.307543354 +0100 -+++ governance/extensions/GovernorTimelockControl.sol 2023-03-16 16:01:13.857331689 +0100 +--- governance/extensions/GovernorTimelockControl.sol 2023-05-04 11:44:55.587737817 +0200 ++++ governance/extensions/GovernorTimelockControl.sol 2023-05-04 15:18:42.661075263 +0200 @@ -24,7 +24,7 @@ * _Available since v4.3._ */ @@ -21,3 +21,13 @@ /** * @dev Function to queue a proposal to the timelock. */ +@@ -163,4 +168,9 @@ + emit TimelockChange(address(_timelock), address(newTimelock)); + _timelock = newTimelock; + } ++ ++ // FV ++ function timelockId(uint256 proposalId) public view returns (bytes32) { ++ return _timelockIds[proposalId]; ++ } + } diff --git a/certora/diff/governance_extensions_GovernorVotesQuorumFraction.sol.patch b/certora/diff/governance_extensions_GovernorVotesQuorumFraction.sol.patch index 98d3b25d264..bdf9d1b114e 100644 --- a/certora/diff/governance_extensions_GovernorVotesQuorumFraction.sol.patch +++ b/certora/diff/governance_extensions_GovernorVotesQuorumFraction.sol.patch @@ -1,7 +1,7 @@ ---- governance/extensions/GovernorVotesQuorumFraction.sol 2023-03-07 10:48:47.733488857 +0100 -+++ governance/extensions/GovernorVotesQuorumFraction.sol 2023-03-15 22:52:06.424537201 +0100 -@@ -62,6 +62,11 @@ - return _quorumNumeratorHistory.upperLookupRecent(timepoint.toUint32()); +--- governance/extensions/GovernorVotesQuorumFraction.sol 2023-05-03 09:17:45.570033048 +0200 ++++ governance/extensions/GovernorVotesQuorumFraction.sol 2023-05-04 15:18:42.664408414 +0200 +@@ -61,6 +61,11 @@ + return _quorumNumeratorHistory.upperLookupRecent(SafeCast.toUint32(timepoint)); } + // FV diff --git a/certora/diff/token_ERC721_ERC721.sol.patch b/certora/diff/token_ERC721_ERC721.sol.patch index c3eae357a4c..0c53650aad6 100644 --- a/certora/diff/token_ERC721_ERC721.sol.patch +++ b/certora/diff/token_ERC721_ERC721.sol.patch @@ -1,5 +1,5 @@ ---- token/ERC721/ERC721.sol 2023-03-07 10:48:47.736822221 +0100 -+++ token/ERC721/ERC721.sol 2023-03-09 19:49:39.669338673 +0100 +--- token/ERC721/ERC721.sol 2023-04-27 22:16:54.864065073 +0200 ++++ token/ERC721/ERC721.sol 2023-05-04 15:18:42.671074716 +0200 @@ -199,6 +199,11 @@ return _owners[tokenId]; } diff --git a/certora/run.js b/certora/run.js index cd4722541aa..331630500d8 100755 --- a/certora/run.js +++ b/certora/run.js @@ -59,7 +59,7 @@ if (process.exitCode) { } for (const { spec, contract, files, options = [] } of specs) { - limit(runCertora, spec, contract, files, [...options.flatMap(opt => opt.split(' ')), ...argv.options]); + limit(runCertora, spec, contract, files, [...options, ...argv.options].flatMap(opt => opt.split(' '))); } // Run certora, aggregate the output and print it at the end diff --git a/certora/specs/GovernorStates.spec b/certora/specs/GovernorStates.spec index 84b43b39458..6baba699bd8 100644 --- a/certora/specs/GovernorStates.spec +++ b/certora/specs/GovernorStates.spec @@ -57,6 +57,11 @@ rule stateTransitionFn(uint256 pId, env e, method f, calldataarg args) │ Rule: State transitions caused by time passing │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ +// The timelockId can be set in states QUEUED, EXECUTED and CANCELED. However, checking the full scope of this results +// in a timeout. This is a weaker version that is still usefull +invariant noTimelockBeforeEndOfVote(env e, uint256 pId) + state(e, pId) == ACTIVE() => timelockId(pId) == 0 + rule stateTransitionWait(uint256 pId, env e1, env e2) { require clockSanity(e1); require clockSanity(e2); @@ -66,6 +71,7 @@ rule stateTransitionWait(uint256 pId, env e1, env e2) { // possible before the time passes. We don't want the state transition include elements that cannot have happened // before e1. This ensure that the e1 → e2 state transition is purelly a consequence of time passing. requireInvariant votesImplySnapshotPassed(e1, pId); + requireInvariant noTimelockBeforeEndOfVote(e1, pId); uint8 stateBefore = state(e1, pId); uint8 stateAfter = state(e2, pId); @@ -74,14 +80,7 @@ rule stateTransitionWait(uint256 pId, env e1, env e2) { (stateBefore == PENDING() && stateAfter == ACTIVE() ) || (stateBefore == PENDING() && stateAfter == DEFEATED() ) || (stateBefore == ACTIVE() && stateAfter == SUCCEEDED()) || - (stateBefore == ACTIVE() && stateAfter == DEFEATED() ) || - // Strange consequence of the timelock binding: - // When transitioning from ACTIVE to SUCCEEDED (because of the clock moving forward) the proposal state in - // the timelock is suddenly considered. Prior state set in the timelock can cause the proposal to already be - // queued, executed or canceled. - (stateBefore == ACTIVE() && stateAfter == CANCELED()) || - (stateBefore == ACTIVE() && stateAfter == EXECUTED()) || - (stateBefore == ACTIVE() && stateAfter == QUEUED()) + (stateBefore == ACTIVE() && stateAfter == DEFEATED() ) ); } diff --git a/certora/specs/methods/IGovernor.spec b/certora/specs/methods/IGovernor.spec index 7737bb23e78..25abb6c22ef 100644 --- a/certora/specs/methods/IGovernor.spec +++ b/certora/specs/methods/IGovernor.spec @@ -52,4 +52,5 @@ methods { getForVotes(uint256) returns uint256 envfree getAbstainVotes(uint256) returns uint256 envfree quorumNumeratorLength() returns uint256 envfree + timelockId(uint256) returns bytes32 envfree } From df88ea34d0c2b9c952933a55b4b0311fbd7c29fc Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 4 May 2023 17:22:59 +0200 Subject: [PATCH 55/61] fix lint --- certora/run.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/certora/run.js b/certora/run.js index 331630500d8..a3564a10d96 100755 --- a/certora/run.js +++ b/certora/run.js @@ -59,12 +59,17 @@ if (process.exitCode) { } for (const { spec, contract, files, options = [] } of specs) { - limit(runCertora, spec, contract, files, [...options, ...argv.options].flatMap(opt => opt.split(' '))); + limit(runCertora, spec, contract, files, [...options, ...argv.options]); } // Run certora, aggregate the output and print it at the end async function runCertora(spec, contract, files, options = []) { - const args = [...files, '--verify', `${contract}:certora/specs/${spec}.spec`, ...options]; + const args = [ + ...files, + '--verify', + `${contract}:certora/specs/${spec}.spec`, + ...options.flatMap(opt => opt.split(' ')), + ]; const child = proc.spawn('certoraRun', args); const stream = new PassThrough(); From a97d3f5ce9bbb58e988b0d7b7929386d3b68da65 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 4 May 2023 17:26:16 +0200 Subject: [PATCH 56/61] codespell --- certora/specs/GovernorStates.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certora/specs/GovernorStates.spec b/certora/specs/GovernorStates.spec index 6baba699bd8..2aa7bbcb4d2 100644 --- a/certora/specs/GovernorStates.spec +++ b/certora/specs/GovernorStates.spec @@ -58,7 +58,7 @@ rule stateTransitionFn(uint256 pId, env e, method f, calldataarg args) └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ // The timelockId can be set in states QUEUED, EXECUTED and CANCELED. However, checking the full scope of this results -// in a timeout. This is a weaker version that is still usefull +// in a timeout. This is a weaker version that is still useful invariant noTimelockBeforeEndOfVote(env e, uint256 pId) state(e, pId) == ACTIVE() => timelockId(pId) == 0 From 9a33b0d2a26aff2fb9d219e95caa16dc82246106 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 5 May 2023 15:07:56 +0200 Subject: [PATCH 57/61] split stateTransitionFn as multiple rules with requires --- certora/specs/GovernorStates.spec | 79 ++++++++++++++++++++++++------- 1 file changed, 63 insertions(+), 16 deletions(-) diff --git a/certora/specs/GovernorStates.spec b/certora/specs/GovernorStates.spec index 2aa7bbcb4d2..23233ef8149 100644 --- a/certora/specs/GovernorStates.spec +++ b/certora/specs/GovernorStates.spec @@ -28,30 +28,77 @@ rule stateConsistency(env e, uint256 pId) { │ Rule: State transitions caused by function calls │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ -rule stateTransitionFn(uint256 pId, env e, method f, calldataarg args) - filtered { f -> !assumedSafe(f) - && f.selector != castVoteBySig(uint256,uint8,uint8,bytes32,bytes32).selector - && f.selector != castVoteWithReasonAndParams(uint256,uint8,string,bytes).selector - && f.selector != castVoteWithReason(uint256,uint8,string).selector - && f.selector != castVoteWithReasonAndParamsBySig(uint256,uint8,string,bytes,uint8,bytes32,bytes32).selector - } -{ +/// Previous version that results in the prover timing out. +// rule stateTransitionFn(uint256 pId, env e, method f, calldataarg args) +// filtered { f -> !assumedSafe(f) } +// { +// require clockSanity(e); +// require quorumNumeratorLength() < max_uint256; // sanity +// +// uint8 stateBefore = state(e, pId); +// f(e, args); +// uint8 stateAfter = state(e, pId); +// +// assert (stateBefore != stateAfter) => ( +// (stateBefore == UNSET() && stateAfter == PENDING() && f.selector == propose(address[],uint256[],bytes[],string).selector ) || +// (stateBefore == PENDING() && stateAfter == CANCELED() && f.selector == cancel(address[],uint256[],bytes[],bytes32).selector ) || +// (stateBefore == SUCCEEDED() && stateAfter == QUEUED() && f.selector == queue(address[],uint256[],bytes[],bytes32).selector ) || +// (stateBefore == SUCCEEDED() && stateAfter == EXECUTED() && f.selector == execute(address[],uint256[],bytes[],bytes32).selector) || +// (stateBefore == QUEUED() && stateAfter == EXECUTED() && f.selector == execute(address[],uint256[],bytes[],bytes32).selector) +// ); +// } + +function stateTransitionFnHelper(method f, uint8 s) returns uint8 { + uint256 pId; env e; calldataarg args; + require clockSanity(e); require quorumNumeratorLength() < max_uint256; // sanity - uint8 stateBefore = state(e, pId); + require state(e, pId) == s; // constrain state before f(e, args); - uint8 stateAfter = state(e, pId); + require state(e, pId) != s; // constrain state after - assert (stateBefore != stateAfter) => ( - (stateBefore == UNSET() && stateAfter == PENDING() && f.selector == propose(address[],uint256[],bytes[],string).selector ) || - (stateBefore == PENDING() && stateAfter == CANCELED() && f.selector == cancel(address[],uint256[],bytes[],bytes32).selector ) || - (stateBefore == SUCCEEDED() && stateAfter == QUEUED() && f.selector == queue(address[],uint256[],bytes[],bytes32).selector ) || - (stateBefore == SUCCEEDED() && stateAfter == EXECUTED() && f.selector == execute(address[],uint256[],bytes[],bytes32).selector) || - (stateBefore == QUEUED() && stateAfter == EXECUTED() && f.selector == execute(address[],uint256[],bytes[],bytes32).selector) + return state(e, pId); +} + +rule stateTransitionFn_PENDING(method f) filtered { f -> !assumedSafe(f) } { + uint8 stateAfter = stateTransitionFnHelper(f, PENDING()); + assert stateAfter == CANCELED() && f.selector == cancel(address[],uint256[],bytes[],bytes32).selector; +} + +rule stateTransitionFn_ACTIVE(method f) filtered { f -> !assumedSafe(f) } { + uint8 stateAfter = stateTransitionFnHelper(f, ACTIVE()); + assert false; +} + +rule stateTransitionFn_CANCELED(method f) filtered { f -> !assumedSafe(f) } { + uint8 stateAfter = stateTransitionFnHelper(f, CANCELED()); + assert false; +} + +rule stateTransitionFn_DEFEATED(method f) filtered { f -> !assumedSafe(f) } { + uint8 stateAfter = stateTransitionFnHelper(f, DEFEATED()); + assert false; +} + +rule stateTransitionFn_SUCCEEDED(method f) filtered { f -> !assumedSafe(f) } { + uint8 stateAfter = stateTransitionFnHelper(f, SUCCEEDED()); + assert ( + (stateAfter == QUEUED() && f.selector == queue(address[],uint256[],bytes[],bytes32).selector) || + (stateAfter == EXECUTED() && f.selector == execute(address[],uint256[],bytes[],bytes32).selector) ); } +rule stateTransitionFn_QUEUED(method f) filtered { f -> !assumedSafe(f) } { + uint8 stateAfter = stateTransitionFnHelper(f, QUEUED()); + assert state(e, pId) == EXECUTED() && f.selector == execute(address[],uint256[],bytes[],bytes32).selector; +} + +rule stateTransitionFn_EXECUTED(method f) filtered { f -> !assumedSafe(f) } { + uint8 stateAfter = stateTransitionFnHelper(f, EXECUTED()); + assert false; +} + /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ Rule: State transitions caused by time passing │ From 34316245987e0ba43084bc683c58741822c0bf50 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 5 May 2023 15:14:07 +0200 Subject: [PATCH 58/61] up --- certora/specs/GovernorStates.spec | 33 ++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/certora/specs/GovernorStates.spec b/certora/specs/GovernorStates.spec index 23233ef8149..369459f64a6 100644 --- a/certora/specs/GovernorStates.spec +++ b/certora/specs/GovernorStates.spec @@ -10,9 +10,13 @@ use invariant votesImplySnapshotPassed │ Rule: state returns one of the value in the enumeration │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ -rule stateConsistency(env e, uint256 pId) { - uint8 result = state(e, pId); +invariant stateConsistency(env e, uint256 pId) + state(e, pId) == safeState(e, pId) + +rule stateDomain(env e, uint256 pId) { + uint8 result = safeState(e, pId); assert ( + result == UNSET() || result == PENDING() || result == ACTIVE() || result == CANCELED() || @@ -35,9 +39,9 @@ rule stateConsistency(env e, uint256 pId) { // require clockSanity(e); // require quorumNumeratorLength() < max_uint256; // sanity // -// uint8 stateBefore = state(e, pId); +// uint8 stateBefore = safeState(e, pId); // f(e, args); -// uint8 stateAfter = state(e, pId); +// uint8 stateAfter = safeState(e, pId); // // assert (stateBefore != stateAfter) => ( // (stateBefore == UNSET() && stateAfter == PENDING() && f.selector == propose(address[],uint256[],bytes[],string).selector ) || @@ -54,11 +58,16 @@ function stateTransitionFnHelper(method f, uint8 s) returns uint8 { require clockSanity(e); require quorumNumeratorLength() < max_uint256; // sanity - require state(e, pId) == s; // constrain state before + require safeState(e, pId) == s; // constrain state before f(e, args); - require state(e, pId) != s; // constrain state after + require safeState(e, pId) != s; // constrain state after + + return safeState(e, pId); +} - return state(e, pId); +rule stateTransitionFn_UNSET(method f) filtered { f -> !assumedSafe(f) } { + uint8 stateAfter = stateTransitionFnHelper(f, UNSET()); + assert stateAfter == PENDING() && f.selector == propose(address[],uint256[],bytes[],string).selector; } rule stateTransitionFn_PENDING(method f) filtered { f -> !assumedSafe(f) } { @@ -91,7 +100,7 @@ rule stateTransitionFn_SUCCEEDED(method f) filtered { f -> !assumedSafe(f) } { rule stateTransitionFn_QUEUED(method f) filtered { f -> !assumedSafe(f) } { uint8 stateAfter = stateTransitionFnHelper(f, QUEUED()); - assert state(e, pId) == EXECUTED() && f.selector == execute(address[],uint256[],bytes[],bytes32).selector; + assert stateAfter == EXECUTED() && f.selector == execute(address[],uint256[],bytes[],bytes32).selector; } rule stateTransitionFn_EXECUTED(method f) filtered { f -> !assumedSafe(f) } { @@ -107,7 +116,7 @@ rule stateTransitionFn_EXECUTED(method f) filtered { f -> !assumedSafe(f) } { // The timelockId can be set in states QUEUED, EXECUTED and CANCELED. However, checking the full scope of this results // in a timeout. This is a weaker version that is still useful invariant noTimelockBeforeEndOfVote(env e, uint256 pId) - state(e, pId) == ACTIVE() => timelockId(pId) == 0 + safeState(e, pId) == ACTIVE() => timelockId(pId) == 0 rule stateTransitionWait(uint256 pId, env e1, env e2) { require clockSanity(e1); @@ -120,8 +129,8 @@ rule stateTransitionWait(uint256 pId, env e1, env e2) { requireInvariant votesImplySnapshotPassed(e1, pId); requireInvariant noTimelockBeforeEndOfVote(e1, pId); - uint8 stateBefore = state(e1, pId); - uint8 stateAfter = state(e2, pId); + uint8 stateBefore = safeState(e1, pId); + uint8 stateAfter = safeState(e2, pId); assert (stateBefore != stateAfter) => ( (stateBefore == PENDING() && stateAfter == ACTIVE() ) || @@ -141,7 +150,7 @@ rule stateIsConsistentWithVotes(uint256 pId, env e) { requireInvariant proposalStateConsistency(pId); uint48 currentClock = clock(e); - uint8 currentState = state(e, pId); + uint8 currentState = safeState(e, pId); uint256 snapshot = proposalSnapshot(pId); uint256 deadline = proposalDeadline(pId); bool quorumSuccess = quorumReached(pId); From c664f5f2c13e8d0772aecb116e26eb3a7312eb9b Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 5 May 2023 21:20:12 +0200 Subject: [PATCH 59/61] try to simplify rules --- certora/specs/GovernorStates.spec | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/certora/specs/GovernorStates.spec b/certora/specs/GovernorStates.spec index 369459f64a6..eb0f103f9f6 100644 --- a/certora/specs/GovernorStates.spec +++ b/certora/specs/GovernorStates.spec @@ -5,6 +5,13 @@ import "GovernorInvariants.spec" use invariant proposalStateConsistency use invariant votesImplySnapshotPassed +definition assumedSafeOrDuplicateOrDuplicate(method f) returns bool = + assumedSafeOrDuplicate(f) + || f.selector == castVoteWithReason(uint256,uint8,string).selector + || f.selector == castVoteWithReasonAndParams(uint256,uint8,string,bytes).selector + || f.selector == castVoteBySig(uint256,uint8,uint8,bytes32,bytes32).selector + || f.selector == castVoteWithReasonAndParamsBySig(uint256,uint8,string,bytes,uint8,bytes32,bytes32).selector; + /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ Rule: state returns one of the value in the enumeration │ @@ -12,6 +19,7 @@ use invariant votesImplySnapshotPassed */ invariant stateConsistency(env e, uint256 pId) state(e, pId) == safeState(e, pId) + filtered { f -> !assumedSafeOrDuplicateOrDuplicate(f); } rule stateDomain(env e, uint256 pId) { uint8 result = safeState(e, pId); @@ -34,7 +42,7 @@ rule stateDomain(env e, uint256 pId) { */ /// Previous version that results in the prover timing out. // rule stateTransitionFn(uint256 pId, env e, method f, calldataarg args) -// filtered { f -> !assumedSafe(f) } +// filtered { f -> !assumedSafeOrDuplicate(f) } // { // require clockSanity(e); // require quorumNumeratorLength() < max_uint256; // sanity @@ -65,32 +73,32 @@ function stateTransitionFnHelper(method f, uint8 s) returns uint8 { return safeState(e, pId); } -rule stateTransitionFn_UNSET(method f) filtered { f -> !assumedSafe(f) } { +rule stateTransitionFn_UNSET(method f) filtered { f -> !assumedSafeOrDuplicate(f) } { uint8 stateAfter = stateTransitionFnHelper(f, UNSET()); assert stateAfter == PENDING() && f.selector == propose(address[],uint256[],bytes[],string).selector; } -rule stateTransitionFn_PENDING(method f) filtered { f -> !assumedSafe(f) } { +rule stateTransitionFn_PENDING(method f) filtered { f -> !assumedSafeOrDuplicate(f) } { uint8 stateAfter = stateTransitionFnHelper(f, PENDING()); assert stateAfter == CANCELED() && f.selector == cancel(address[],uint256[],bytes[],bytes32).selector; } -rule stateTransitionFn_ACTIVE(method f) filtered { f -> !assumedSafe(f) } { +rule stateTransitionFn_ACTIVE(method f) filtered { f -> !assumedSafeOrDuplicate(f) } { uint8 stateAfter = stateTransitionFnHelper(f, ACTIVE()); assert false; } -rule stateTransitionFn_CANCELED(method f) filtered { f -> !assumedSafe(f) } { +rule stateTransitionFn_CANCELED(method f) filtered { f -> !assumedSafeOrDuplicate(f) } { uint8 stateAfter = stateTransitionFnHelper(f, CANCELED()); assert false; } -rule stateTransitionFn_DEFEATED(method f) filtered { f -> !assumedSafe(f) } { +rule stateTransitionFn_DEFEATED(method f) filtered { f -> !assumedSafeOrDuplicate(f) } { uint8 stateAfter = stateTransitionFnHelper(f, DEFEATED()); assert false; } -rule stateTransitionFn_SUCCEEDED(method f) filtered { f -> !assumedSafe(f) } { +rule stateTransitionFn_SUCCEEDED(method f) filtered { f -> !assumedSafeOrDuplicate(f) } { uint8 stateAfter = stateTransitionFnHelper(f, SUCCEEDED()); assert ( (stateAfter == QUEUED() && f.selector == queue(address[],uint256[],bytes[],bytes32).selector) || @@ -98,12 +106,12 @@ rule stateTransitionFn_SUCCEEDED(method f) filtered { f -> !assumedSafe(f) } { ); } -rule stateTransitionFn_QUEUED(method f) filtered { f -> !assumedSafe(f) } { +rule stateTransitionFn_QUEUED(method f) filtered { f -> !assumedSafeOrDuplicate(f) } { uint8 stateAfter = stateTransitionFnHelper(f, QUEUED()); assert stateAfter == EXECUTED() && f.selector == execute(address[],uint256[],bytes[],bytes32).selector; } -rule stateTransitionFn_EXECUTED(method f) filtered { f -> !assumedSafe(f) } { +rule stateTransitionFn_EXECUTED(method f) filtered { f -> !assumedSafeOrDuplicate(f) } { uint8 stateAfter = stateTransitionFnHelper(f, EXECUTED()); assert false; } @@ -194,7 +202,7 @@ rule stateIsConsistentWithVotes(uint256 pId, env e) { //// This would be nice, but its way to slow to run because "quorumReached" is a FV nightmare //// Also, for it to work we need to prove that the checkpoints have (strictly) increasing keys. // rule onlyVoteCanChangeQuorumReached(uint256 pId, env e, method f, calldataarg args) -// filtered { f -> !assumedSafe(f) } +// filtered { f -> !assumedSafeOrDuplicate(f) } // { // require clockSanity(e); // require clock(e) > proposalSnapshot(pId); // vote has started From 2e1d0b375695c39fc0e2a61093099471719ebdc5 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 9 May 2023 10:31:17 +0200 Subject: [PATCH 60/61] fix spec file --- certora/specs/GovernorStates.spec | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/certora/specs/GovernorStates.spec b/certora/specs/GovernorStates.spec index eb0f103f9f6..bb6a6f524a8 100644 --- a/certora/specs/GovernorStates.spec +++ b/certora/specs/GovernorStates.spec @@ -5,8 +5,8 @@ import "GovernorInvariants.spec" use invariant proposalStateConsistency use invariant votesImplySnapshotPassed -definition assumedSafeOrDuplicateOrDuplicate(method f) returns bool = - assumedSafeOrDuplicate(f) +definition assumedSafeOrDuplicate(method f) returns bool = + assumedSafe(f) || f.selector == castVoteWithReason(uint256,uint8,string).selector || f.selector == castVoteWithReasonAndParams(uint256,uint8,string,bytes).selector || f.selector == castVoteBySig(uint256,uint8,uint8,bytes32,bytes32).selector @@ -19,7 +19,7 @@ definition assumedSafeOrDuplicateOrDuplicate(method f) returns bool = */ invariant stateConsistency(env e, uint256 pId) state(e, pId) == safeState(e, pId) - filtered { f -> !assumedSafeOrDuplicateOrDuplicate(f); } + filtered { f -> !assumedSafeOrDuplicate(f) } rule stateDomain(env e, uint256 pId) { uint8 result = safeState(e, pId); From 6d539e6c31dcf584107ff9d5e8c1072b3fc770be Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 9 May 2023 17:17:57 +0200 Subject: [PATCH 61/61] comment out rules that timeout --- certora/specs/GovernorStates.spec | 125 +++++++++++++++--------------- 1 file changed, 61 insertions(+), 64 deletions(-) diff --git a/certora/specs/GovernorStates.spec b/certora/specs/GovernorStates.spec index bb6a6f524a8..dc449f5fbd3 100644 --- a/certora/specs/GovernorStates.spec +++ b/certora/specs/GovernorStates.spec @@ -17,10 +17,6 @@ definition assumedSafeOrDuplicate(method f) returns bool = │ Rule: state returns one of the value in the enumeration │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ -invariant stateConsistency(env e, uint256 pId) - state(e, pId) == safeState(e, pId) - filtered { f -> !assumedSafeOrDuplicate(f) } - rule stateDomain(env e, uint256 pId) { uint8 result = safeState(e, pId); assert ( @@ -37,7 +33,7 @@ rule stateDomain(env e, uint256 pId) { /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -│ Rule: State transitions caused by function calls │ +│ [DISABLED] Rule: State transitions caused by function calls │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ /// Previous version that results in the prover timing out. @@ -60,61 +56,62 @@ rule stateDomain(env e, uint256 pId) { // ); // } -function stateTransitionFnHelper(method f, uint8 s) returns uint8 { - uint256 pId; env e; calldataarg args; - - require clockSanity(e); - require quorumNumeratorLength() < max_uint256; // sanity - - require safeState(e, pId) == s; // constrain state before - f(e, args); - require safeState(e, pId) != s; // constrain state after - - return safeState(e, pId); -} - -rule stateTransitionFn_UNSET(method f) filtered { f -> !assumedSafeOrDuplicate(f) } { - uint8 stateAfter = stateTransitionFnHelper(f, UNSET()); - assert stateAfter == PENDING() && f.selector == propose(address[],uint256[],bytes[],string).selector; -} - -rule stateTransitionFn_PENDING(method f) filtered { f -> !assumedSafeOrDuplicate(f) } { - uint8 stateAfter = stateTransitionFnHelper(f, PENDING()); - assert stateAfter == CANCELED() && f.selector == cancel(address[],uint256[],bytes[],bytes32).selector; -} - -rule stateTransitionFn_ACTIVE(method f) filtered { f -> !assumedSafeOrDuplicate(f) } { - uint8 stateAfter = stateTransitionFnHelper(f, ACTIVE()); - assert false; -} - -rule stateTransitionFn_CANCELED(method f) filtered { f -> !assumedSafeOrDuplicate(f) } { - uint8 stateAfter = stateTransitionFnHelper(f, CANCELED()); - assert false; -} - -rule stateTransitionFn_DEFEATED(method f) filtered { f -> !assumedSafeOrDuplicate(f) } { - uint8 stateAfter = stateTransitionFnHelper(f, DEFEATED()); - assert false; -} - -rule stateTransitionFn_SUCCEEDED(method f) filtered { f -> !assumedSafeOrDuplicate(f) } { - uint8 stateAfter = stateTransitionFnHelper(f, SUCCEEDED()); - assert ( - (stateAfter == QUEUED() && f.selector == queue(address[],uint256[],bytes[],bytes32).selector) || - (stateAfter == EXECUTED() && f.selector == execute(address[],uint256[],bytes[],bytes32).selector) - ); -} - -rule stateTransitionFn_QUEUED(method f) filtered { f -> !assumedSafeOrDuplicate(f) } { - uint8 stateAfter = stateTransitionFnHelper(f, QUEUED()); - assert stateAfter == EXECUTED() && f.selector == execute(address[],uint256[],bytes[],bytes32).selector; -} - -rule stateTransitionFn_EXECUTED(method f) filtered { f -> !assumedSafeOrDuplicate(f) } { - uint8 stateAfter = stateTransitionFnHelper(f, EXECUTED()); - assert false; -} +/// This version also causes a lot of timeouts so we comment it out of now +// function stateTransitionFnHelper(method f, uint8 s) returns uint8 { +// uint256 pId; env e; calldataarg args; +// +// require clockSanity(e); +// require quorumNumeratorLength() < max_uint256; // sanity +// +// require safeState(e, pId) == s; // constrain state before +// f(e, args); +// require safeState(e, pId) != s; // constrain state after +// +// return safeState(e, pId); +// } +// +// rule stateTransitionFn_UNSET(method f) filtered { f -> !assumedSafeOrDuplicate(f) } { +// uint8 stateAfter = stateTransitionFnHelper(f, UNSET()); +// assert stateAfter == PENDING() && f.selector == propose(address[],uint256[],bytes[],string).selector; +// } +// +// rule stateTransitionFn_PENDING(method f) filtered { f -> !assumedSafeOrDuplicate(f) } { +// uint8 stateAfter = stateTransitionFnHelper(f, PENDING()); +// assert stateAfter == CANCELED() && f.selector == cancel(address[],uint256[],bytes[],bytes32).selector; +// } +// +// rule stateTransitionFn_ACTIVE(method f) filtered { f -> !assumedSafeOrDuplicate(f) } { +// uint8 stateAfter = stateTransitionFnHelper(f, ACTIVE()); +// assert false; +// } +// +// rule stateTransitionFn_CANCELED(method f) filtered { f -> !assumedSafeOrDuplicate(f) } { +// uint8 stateAfter = stateTransitionFnHelper(f, CANCELED()); +// assert false; +// } +// +// rule stateTransitionFn_DEFEATED(method f) filtered { f -> !assumedSafeOrDuplicate(f) } { +// uint8 stateAfter = stateTransitionFnHelper(f, DEFEATED()); +// assert false; +// } +// +// rule stateTransitionFn_SUCCEEDED(method f) filtered { f -> !assumedSafeOrDuplicate(f) } { +// uint8 stateAfter = stateTransitionFnHelper(f, SUCCEEDED()); +// assert ( +// (stateAfter == QUEUED() && f.selector == queue(address[],uint256[],bytes[],bytes32).selector) || +// (stateAfter == EXECUTED() && f.selector == execute(address[],uint256[],bytes[],bytes32).selector) +// ); +// } +// +// rule stateTransitionFn_QUEUED(method f) filtered { f -> !assumedSafeOrDuplicate(f) } { +// uint8 stateAfter = stateTransitionFnHelper(f, QUEUED()); +// assert stateAfter == EXECUTED() && f.selector == execute(address[],uint256[],bytes[],bytes32).selector; +// } +// +// rule stateTransitionFn_EXECUTED(method f) filtered { f -> !assumedSafeOrDuplicate(f) } { +// uint8 stateAfter = stateTransitionFnHelper(f, EXECUTED()); +// assert false; +// } /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ @@ -122,9 +119,9 @@ rule stateTransitionFn_EXECUTED(method f) filtered { f -> !assumedSafeOrDuplicat └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ // The timelockId can be set in states QUEUED, EXECUTED and CANCELED. However, checking the full scope of this results -// in a timeout. This is a weaker version that is still useful +// in a timeout. This is a weaker version that is still useful. Ideally we would check `safeState`. invariant noTimelockBeforeEndOfVote(env e, uint256 pId) - safeState(e, pId) == ACTIVE() => timelockId(pId) == 0 + state(e, pId) == ACTIVE() => timelockId(pId) == 0 rule stateTransitionWait(uint256 pId, env e1, env e2) { require clockSanity(e1); @@ -137,8 +134,8 @@ rule stateTransitionWait(uint256 pId, env e1, env e2) { requireInvariant votesImplySnapshotPassed(e1, pId); requireInvariant noTimelockBeforeEndOfVote(e1, pId); - uint8 stateBefore = safeState(e1, pId); - uint8 stateAfter = safeState(e2, pId); + uint8 stateBefore = state(e1, pId); // Ideally we would use "safeState(e1, pId)" + uint8 stateAfter = state(e2, pId); // Ideally we would use "safeState(e2, pId)" assert (stateBefore != stateAfter) => ( (stateBefore == PENDING() && stateAfter == ACTIVE() ) ||