mirror of openzeppelin-contracts
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
openzeppelin-contracts/certora/specs/ERC20Votes.spec

383 lines
12 KiB

3 years ago
methods {
// functions
checkpoints(address, uint32) envfree
numCheckpoints(address) returns (uint32) envfree
delegates(address) returns (address) envfree
getVotes(address) returns (uint256) envfree
getPastVotes(address, uint256) returns (uint256)
getPastTotalSupply(uint256) returns (uint256)
delegate(address)
_delegate(address, address)
3 years ago
// delegateBySig(address, uint256, uint256, uint8, bytes32, bytes32)
3 years ago
totalSupply() returns (uint256) envfree
_maxSupply() returns (uint224) envfree
// harnesss functions
ckptFromBlock(address, uint32) returns (uint32) envfree
ckptVotes(address, uint32) returns (uint224) envfree
mint(address, uint256)
burn(address, uint256)
unsafeNumCheckpoints(address) returns (uint256) envfree
3 years ago
// solidity generated getters
_delegates(address) returns (address) envfree
// external functions
}
// gets the most recent votes for a user
ghost userVotes(address) returns uint224;
// sums the total votes for all users
ghost totalVotes() returns mathint {
init_state axiom totalVotes() == 0;
3 years ago
axiom totalVotes() >= 0;
3 years ago
}
ghost lastIndex(address) returns uint32;
3 years ago
// helper
3 years ago
hook Sstore _checkpoints[KEY address account][INDEX uint32 index].votes uint224 newVotes (uint224 oldVotes) STORAGE {
havoc userVotes assuming
userVotes@new(account) == newVotes;
havoc totalVotes assuming
totalVotes@new() == totalVotes@old() + to_mathint(newVotes) - to_mathint(userVotes(account));
havoc lastIndex assuming
lastIndex@new(account) == index;
3 years ago
}
ghost lastFromBlock(address) returns uint32;
ghost doubleFromBlock(address) returns bool {
init_state axiom forall address a. doubleFromBlock(a) == false;
}
3 years ago
hook Sstore _checkpoints[KEY address account][INDEX uint32 index].fromBlock uint32 newBlock (uint32 oldBlock) STORAGE {
havoc lastFromBlock assuming
lastFromBlock@new(account) == newBlock;
havoc doubleFromBlock assuming
doubleFromBlock@new(account) == (newBlock == lastFromBlock(account));
3 years ago
}
rule sanity(method f) {
env e;
calldataarg arg;
f(e, arg);
assert false;
3 years ago
}
// something stupid just to see
invariant sanity_invariant()
totalSupply() >= 0
// sum of user balances is >= total amount of delegated votes
// blocked by tool error
3 years ago
invariant votes_solvency()
to_mathint(totalSupply()) >= totalVotes()
{ preserved with(env e) {
require forall address account. numCheckpoints(account) < 1000000;
requireInvariant totalVotes_sums_accounts();
} }
invariant totalVotes_sums_accounts()
forall address a. forall address b. (a != b && a != 0x0 && b != 0x0) => totalVotes() >= getVotes(delegates(a)) + getVotes(delegates(b))
3 years ago
// for some checkpoint, the fromBlock is less than the current block number
invariant blockNum_constrains_fromBlock(address account, uint32 index, env e)
3 years ago
ckptFromBlock(account, index) < e.block.number
{
preserved {
require index < numCheckpoints(account);
}
}
// TODO add a note about this in the report
// // numCheckpoints are less than maxInt
// // passes because numCheckpoints does a safeCast
// invariant maxInt_constrains_numBlocks(address account)
// numCheckpoints(account) < 4294967295 // 2^32
// // fails because there are no checks to stop it
// invariant maxInt_constrains_ckptsLength(address account)
// unsafeNumCheckpoints(account) < 4294967295 // 2^32
3 years ago
// can't have more checkpoints for a given account than the last from block
// passes
3 years ago
invariant fromBlock_constrains_numBlocks(address account)
numCheckpoints(account) <= ckptFromBlock(account, numCheckpoints(account) - 1)
{ preserved with(env e) {
require e.block.number >= ckptFromBlock(account, numCheckpoints(account) - 1); // this should be true from the invariant above!!
}}
// for any given checkpoint, the fromBlock must be greater than the checkpoint
// this proves the above invariant in combination with the below invariant
// if checkpoint has a greater fromBlock than the last, and the FromBlock is always greater than the pos.
// Then the number of positions must be less than the currentFromBlock
// ^note that the tool is assuming it's possible for the starting fromBlock to be 0 or anything, and does not know the current starting block
// passes + rule sanity
invariant fromBlock_greaterThanEq_pos(address account, uint32 pos)
ckptFromBlock(account, pos) >= pos
3 years ago
// a larger index must have a larger fromBlock
// passes + rule sanity
invariant fromBlock_increasing(address account, uint32 pos, uint32 pos2)
pos > pos2 => ckptFromBlock(account, pos) > ckptFromBlock(account, pos2)
3 years ago
// converted from an invariant to a rule to slightly change the logic
// if the fromBlock is the same as before, then the number of checkpoints stays the same
// however if the fromBlock is new than the number of checkpoints increases
3 years ago
// passes, fails rule sanity because tautology check seems to be bugged
rule unique_checkpoints_rule(method f) {
env e; calldataarg args;
address account;
uint32 num_ckpts_ = numCheckpoints(account);
uint32 fromBlock_ = num_ckpts_ == 0 ? 0 : ckptFromBlock(account, num_ckpts_ - 1);
f(e, args);
uint32 _num_ckpts = numCheckpoints(account);
uint32 _fromBlock = _num_ckpts == 0 ? 0 : ckptFromBlock(account, _num_ckpts - 1);
3 years ago
assert fromBlock_ == _fromBlock => num_ckpts_ == _num_ckpts || _num_ckpts == 1, "same fromBlock, new checkpoint";
}
3 years ago
// assumes neither account has delegated
// currently fails due to this scenario. A has maxint number of checkpoints
// an additional checkpoint is added which overflows and sets A's votes to 0
3 years ago
// passes + rule sanity (- a bad tautology check)
3 years ago
rule transfer_safe() {
env e;
uint256 amount;
address a; address b;
3 years ago
require delegates(a) != delegates(b); // confirmed if they both delegate to the same person then transfer keeps the votes the same
3 years ago
3 years ago
require numCheckpoints(delegates(a)) < 1000000;
require numCheckpoints(delegates(b)) < 1000000;
uint256 votesA_pre = getVotes(delegates(a));
uint256 votesB_pre = getVotes(delegates(b));
3 years ago
mathint totalVotes_pre = totalVotes();
transferFrom(e, a, b, amount);
3 years ago
mathint totalVotes_post = totalVotes();
uint256 votesA_post = getVotes(delegates(a));
uint256 votesB_post = getVotes(delegates(b));
3 years ago
// if an account that has not delegated transfers balance to an account that has, it will increase the total supply of votes
3 years ago
assert totalVotes_pre == totalVotes_post, "transfer changed total supply";
assert delegates(a) != 0 => votesA_pre - votesA_post == amount, "A lost the wrong amount of votes";
assert delegates(b) != 0 => votesB_post - votesB_pre == amount, "B lost the wrong amount of votes";
}
// for any given function f, if the delegate is changed the function must be delegate or delegateBySig
// passes
rule delegates_safe(method f) filtered {f -> (f.selector != delegate(address).selector &&
f.selector != _delegate(address, address).selector &&
f.selector != delegateBySig(address, uint256, uint256, uint8, bytes32, bytes32).selector) }
{
env e; calldataarg args;
address account;
address pre = delegates(account);
f(e, args);
address post = delegates(account);
assert pre == post, "invalid delegate change";
3 years ago
}
// delegates increases the delegatee's votes by the proper amount
// passes + rule sanity
3 years ago
rule delegatee_receives_votes() {
env e;
address delegator; address delegatee;
3 years ago
require delegates(delegator) != delegatee;
require delegatee != 0x0;
3 years ago
3 years ago
3 years ago
uint256 delegator_bal = balanceOf(e, delegator);
uint256 votes_= getVotes(delegatee);
_delegate(e, delegator, delegatee);
require lastIndex(delegatee) < 1000000;
3 years ago
uint256 _votes = getVotes(delegatee);
assert _votes == votes_ + delegator_bal, "delegatee did not receive votes";
}
3 years ago
// passes + rule sanity
rule previous_delegatee_votes_removed() {
3 years ago
env e;
address delegator; address delegatee; address third;
require third != delegatee;
3 years ago
require delegates(delegator) == third;
3 years ago
require numCheckpoints(third) < 1000000;
3 years ago
uint256 delegator_bal = balanceOf(e, delegator);
uint256 votes_ = getVotes(third);
_delegate(e, delegator, delegatee);
uint256 _votes = getVotes(third);
3 years ago
assert third != 0x0 => _votes == votes_ - delegator_bal, "votes not removed from the previous delegatee";
3 years ago
}
3 years ago
// passes with rule sanity
3 years ago
rule delegate_contained() {
env e;
address delegator; address delegatee; address other;
require other != delegatee;
require other != delegates(delegator);
uint256 votes_ = getVotes(other);
_delegate(e, delegator, delegatee);
uint256 _votes = getVotes(other);
assert votes_ == _votes, "votes not contained";
}
rule delegate_no_frontrunning(method f) {
env e; calldataarg args;
address delegator; address delegatee; address third; address other;
3 years ago
3 years ago
require numCheckpoints(delegatee) < 1000000;
require numCheckpoints(third) < 1000000;
3 years ago
f(e, args);
uint256 delegator_bal = balanceOf(e, delegator);
3 years ago
uint256 delegatee_votes_ = getVotes(delegatee);
uint256 third_votes_ = getVotes(third);
uint256 other_votes_ = getVotes(other);
require delegates(delegator) == third;
require third != delegatee;
require other != third;
require other != delegatee;
require delegatee != 0x0;
3 years ago
_delegate(e, delegator, delegatee);
3 years ago
uint256 _delegatee_votes = getVotes(delegatee);
uint256 _third_votes = getVotes(third);
uint256 _other_votes = getVotes(other);
3 years ago
3 years ago
// previous delegatee loses all of their votes
3 years ago
// delegatee gains that many votes
// third loses any votes delegated to them
3 years ago
assert _delegatee_votes == delegatee_votes_ + delegator_bal, "delegatee did not receive votes";
assert third != 0 => _third_votes == third_votes_ - delegator_bal, "votes not removed from third";
assert other_votes_ == _other_votes, "delegate not contained";
3 years ago
}
3 years ago
// passes
rule mint_increases_totalSupply() {
env e;
uint256 amount; address account;
uint256 fromBlock = e.block.number;
uint256 totalSupply_ = totalSupply();
mint(e, account, amount);
uint256 _totalSupply = totalSupply();
require _totalSupply < _maxSupply();
assert _totalSupply == totalSupply_ + amount, "totalSupply not increased properly";
assert getPastTotalSupply(e, fromBlock) == totalSupply_ , "previous total supply not saved properly";
}
// passes
rule burn_decreases_totalSupply() {
env e;
uint256 amount; address account;
uint256 fromBlock = e.block.number;
uint256 totalSupply_ = totalSupply();
burn(e, account, amount);
uint256 _totalSupply = totalSupply();
assert _totalSupply == totalSupply_ - amount, "totalSupply not decreased properly";
assert getPastTotalSupply(e, fromBlock) == totalSupply_ , "previous total supply not saved properly";
}
// passes
rule mint_doesnt_increase_totalVotes() {
env e;
uint256 amount; address account;
mathint totalVotes_ = totalVotes();
mint(e, account, amount);
assert totalVotes() == totalVotes_, "totalVotes increased";
}
// passes
rule burn_doesnt_decrease_totalVotes() {
env e;
uint256 amount; address account;
mathint totalVotes_ = totalVotes();
burn(e, account, amount);
assert totalVotes() == totalVotes_, "totalVotes decreased";
}
// // fails
// rule mint_increases_totalVotes() {
// env e;
// uint256 amount; address account;
// mathint totalVotes_ = totalVotes();
// mint(e, account, amount);
// assert totalVotes() == totalVotes_ + to_mathint(amount), "totalVotes not increased";
// }
// // fails
// rule burn_decreases_totalVotes() {
// env e;
// uint256 amount; address account;
// mathint totalVotes_ = totalVotes();
// burn(e, account, amount);
// assert totalVotes() == totalVotes_ - to_mathint(amount), "totalVotes not decreased";
// }