This commit is contained in:
Hadrien Croubois
2023-04-28 14:51:14 +02:00
parent 19b6505a62
commit 5e7a95e638
2 changed files with 288 additions and 46 deletions

View File

@ -24,6 +24,14 @@ contract ERC20VotesHarness is ERC20Votes {
return checkpoints(account, pos).votes;
}
function ckptFromBlockTotalSupply(uint32 pos) public view returns (uint32) {
return checkpointsTotalSupply(pos).fromBlock;
}
function ckptVotesTotalSupply(uint32 pos) public view returns (uint224) {
return checkpointsTotalSupply(pos).votes;
}
function maxSupply() public view returns (uint224) {
return _maxSupply();
}

View File

@ -5,10 +5,13 @@ import "methods/IERC6372.spec"
import "ERC20.spec"
methods {
numCheckpoints(address) returns (uint32) envfree
ckptFromBlock(address, uint32) returns (uint32) envfree
ckptVotes(address, uint32) returns (uint224) envfree
maxSupply() returns (uint224) envfree
numCheckpoints(address) returns (uint32) envfree
ckptFromBlock(address, uint32) returns (uint32) envfree
ckptVotes(address, uint32) returns (uint224) envfree
numCheckpointsTotalSupply() returns (uint32) envfree
ckptFromBlockTotalSupply(uint32) returns (uint32) envfree
ckptVotesTotalSupply(uint32) returns (uint224) envfree
maxSupply() returns (uint224) envfree
}
use invariant totalSupplyIsSumOfBalances
@ -22,35 +25,30 @@ definition max_uint224() returns uint224 = 0xffffffffffffffffffffffffffff;
/*
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
Ghost & hooks
Ghost & hooks: total delegated
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*/
ghost totalVotes() returns uint224 {
init_state axiom totalVotes() == 0;
ghost mapping(address => uint256) balanceOf {
init_state axiom forall address a. balanceOf[a] == 0;
}
ghost mapping(address => uint224) userVotes {
init_state axiom forall address a. userVotes[a] == 0;
ghost mapping(address => address) delegates {
init_state axiom forall address a. delegates[a] == 0;
}
ghost mapping(address => uint32) userClock {
init_state axiom forall address a. userClock[a] == 0;
ghost mapping(address => uint256) getVotes {
init_state axiom forall address a. getVotes[a] == 0;
}
ghost mapping(address => uint32) userCkpts {
init_state axiom forall address a. userCkpts[a] == 0;
hook Sstore _balances[KEY address account] uint256 newAmount (uint256 oldAmount) STORAGE {
balanceOf[account] = newAmount;
getVotes[delegates[account]] = getVotes[delegates[account]] + newAmount - oldAmount;
}
hook Sstore currentContract._checkpoints[KEY address account][INDEX uint32 index].votes uint224 newVotes (uint224 oldVotes) STORAGE {
havoc totalVotes assuming totalVotes@new() == totalVotes@old() + newVotes - userVotes[account];
userVotes[account] = newVotes;
userCkpts[account] = index;
}
hook Sstore currentContract._checkpoints[KEY address account][INDEX uint32 index].fromBlock uint32 newTimepoint (uint32 oldTimepoint) STORAGE {
userClock[account] = newTimepoint;
userCkpts[account] = index;
hook Sstore _delegates[KEY address account] address newDelegate (address oldDelegate) STORAGE {
delegates[account] = newDelegate;
getVotes[oldDelegate] = getVotes[oldDelegate] - balanceOf[account];
getVotes[newDelegate] = getVotes[newDelegate] + balanceOf[account];
}
/*
@ -66,20 +64,28 @@ invariant clockMode(env e)
Invariant: hook correctly track latest checkpoint
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*/
invariant hooksAreCorrect(address a)
numCheckpoints(a) > 0 => (
userVotes[a] == getVotes(a) &&
userVotes[a] == ckptVotes(a, numCheckpoints(a) - 1) &&
userClock[a] == ckptFromBlock(a, numCheckpoints(a) - 1) &&
userCkpts[a] == numCheckpoints(a) - 1
)
invariant delegationConsistency(address a)
balanceOf(a) == balanceOf[a] &&
delegates(a) == delegates[a] &&
getVotes(a) == (a == 0 ? 0 : getVotes[a])
/*
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
Invariant: totalSupply is checkpointed
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*/
invariant totalSupplyTracked()
totalSupply() > 0 => numCheckpointsTotalSupply() > 0
invariant totalSupplyLatest()
numCheckpointsTotalSupply() > 0 => ckptVotesTotalSupply(numCheckpointsTotalSupply() - 1) == totalSupply()
/*
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
Invariant: checkpoint is not in the future
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*/
invariant userClockInThePast(env e, address a)
invariant checkpointInThePast(env e, address a)
numCheckpoints(a) > 0 => ckptFromBlock(a, numCheckpoints(a) - 1) <= clock(e)
{
preserved with (env e2) {
@ -87,6 +93,14 @@ invariant userClockInThePast(env e, address a)
}
}
invariant totalCheckpointInThePast(env e)
numCheckpointsTotalSupply() > 0 => ckptFromBlockTotalSupply(numCheckpointsTotalSupply() - 1) <= clock(e)
{
preserved with (env e2) {
require clock(e2) <= clock(e);
}
}
/*
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
Invariant: checkpoint clock is strictly increassing (implies no duplicate)
@ -96,24 +110,17 @@ invariant checkpointClockIncreassing(address a)
numCheckpoints(a) > 1 => ckptFromBlock(a, numCheckpoints(a) - 2) < ckptFromBlock(a, numCheckpoints(a) - 1)
{
preserved with (env e) {
requireInvariant userClockInThePast(e, a);
requireInvariant checkpointInThePast(e, a);
}
}
/// For some reason "sumOfBalances" is not tracking correctly ...
/// ... and we get counter example where totalSupply is more than the sum of involved balances
// invariant totalVotesLessTotalSupply()
// totalVotes() <= totalSupply()
// {
// preserved {
// requireInvariant totalSupplyIsSumOfBalances;
// }
// }
invariant totalCheckpointClockIncreassing()
numCheckpointsTotalSupply() > 1 => ckptFromBlockTotalSupply(numCheckpointsTotalSupply() - 2) < ckptFromBlockTotalSupply(numCheckpointsTotalSupply() - 1)
{
preserved with (env e) {
requireInvariant totalCheckpointInThePast(e);
}
}
/// For some reason "sumOfBalances" is not tracking correctly ...
/// ... and we get counter example where totalSupply is more than the sum of involved balances
@ -124,3 +131,230 @@ invariant checkpointClockIncreassing(address a)
// requireInvariant totalSupplyIsSumOfBalances;
// }
// }
/*
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
Invariant: Don't track votes delegated to address 0
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*/
invariant addressZeroNoCheckpoints()
numCheckpoints(0) == 0
/*
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
Invariant: Don't track votes delegated to address 0
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*/
rule checkpointsImmutable(env e, method f)
filtered { f -> !f.isView }
{
address account;
uint32 index;
require index < numCheckpoints(account);
uint224 valueBefore = ckptVotes(account, index);
uint32 clockBefore = ckptFromBlock(account, index);
calldataarg args; f(e, args);
uint224 valueAfter = ckptVotes@withrevert(account, index);
assert !lastReverted;
uint32 clockAfter = ckptFromBlock@withrevert(account, index);
assert !lastReverted;
assert clockAfter == clockBefore;
assert valueAfter != valueBefore => clockBefore == clock(e);
}
rule totalCheckpointsImmutable(env e, method f)
filtered { f -> !f.isView }
{
uint32 index;
require index < numCheckpointsTotalSupply();
uint224 valueBefore = ckptVotesTotalSupply(index);
uint32 clockBefore = ckptFromBlockTotalSupply(index);
calldataarg args; f(e, args);
uint224 valueAfter = ckptVotesTotalSupply@withrevert(index);
assert !lastReverted;
uint32 clockAfter = ckptFromBlockTotalSupply@withrevert(index);
assert !lastReverted;
assert clockAfter == clockBefore;
assert valueAfter != valueBefore => clockBefore == clock(e);
}
/*
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
Rules: what function can lead to state changes
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*/
rule changes(env e, method f)
filtered { f -> !f.isView }
{
address account;
calldataarg args;
uint32 ckptsBefore = numCheckpoints(account);
uint256 votesBefore = getVotes(account);
address delegatesBefore = delegates(account);
f(e, args);
uint32 ckptsAfter = numCheckpoints(account);
uint256 votesAfter = getVotes(account);
address delegatesAfter = delegates(account);
assert ckptsAfter != ckptsBefore => (
ckptsAfter == ckptsBefore + 1 &&
ckptFromBlock(account, ckptsAfter - 1) == clock(e) &&
(
f.selector == mint(address,uint256).selector ||
f.selector == burn(address,uint256).selector ||
f.selector == transfer(address,uint256).selector ||
f.selector == transferFrom(address,address,uint256).selector ||
f.selector == delegate(address).selector ||
f.selector == delegateBySig(address,uint256,uint256,uint8,bytes32,bytes32).selector
)
);
assert votesAfter != votesBefore => (
f.selector == mint(address,uint256).selector ||
f.selector == burn(address,uint256).selector ||
f.selector == transfer(address,uint256).selector ||
f.selector == transferFrom(address,address,uint256).selector ||
f.selector == delegate(address).selector ||
f.selector == delegateBySig(address,uint256,uint256,uint8,bytes32,bytes32).selector
);
assert delegatesAfter != delegatesBefore => (
f.selector == delegate(address).selector ||
f.selector == delegateBySig(address,uint256,uint256,uint8,bytes32,bytes32).selector
);
}
/*
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
Rules: mint updates votes
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*/
/* WIP
rule mint(env e) {
requireInvariant totalSupplyIsSumOfBalances();
requireInvariant totalSupplyTracked();
requireInvariant totalSupplyLatest();
require nonpayable(e);
address to;
address toDelegate = delegates(to);
address other;
uint256 amount;
// cache state
uint256 totalSupplyBefore = totalSupply();
uint256 votesBefore = getVotes(toDelegate);
uint32 ckptsBefore = numCheckpoints(toDelegate);
mathint clockBefore = ckptsBefore == 0 ? -1 : numCheckpoints(toDelegate);
uint256 otherVotesBefore = getVotes(other);
uint256 otherCkptsBefore = numCheckpoints(other);
// run transaction
mint@withrevert(e, to, amount);
bool success = !lastReverted;
uint256 votesAfter = getVotes(toDelegate);
uint32 ckptsAfter = numCheckpoints(toDelegate);
mathint clockAfter = ckptsAfter == 0 ? -1 : numCheckpoints(toDelegate);
uint256 otherVotesAfter = getVotes(other);
uint256 otherCkptsAfter = numCheckpoints(other);
// liveness
assert success <=> (to != 0 || totalSupplyBefore + amount <= max_uint256);
// effects
assert (
success &&
toDelegate != 0
) => (
votesAfter == votesBefore + amount &&
ckptsAfter == ckptsBefore + to_mathint(clockBefore == clockAfter ? 0 : 1) &&
clockAfter == clock(e)
);
// no side effects
assert otherVotesAfter != otherVotesBefore => other == toDelegate;
assert otherCkptsAfter != otherCkptsBefore => other == toDelegate;
}
*/
/*
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
Rules: burn updates votes
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
*/
/* WIP
rule burn(env e) {
requireInvariant totalSupplyIsSumOfBalances();
requireInvariant totalSupplyTracked();
requireInvariant totalSupplyLatest();
require nonpayable(e);
address from;
address fromDelegate = delegates(from);
address other;
uint256 amount;
// cache state
uint256 fromBalanceBefore = balanceOf(from);
uint256 votesBefore = getVotes(fromDelegate);
uint32 ckptsBefore = numCheckpoints(fromDelegate);
mathint clockBefore = ckptsBefore == 0 ? -1 : numCheckpoints(fromDelegate);
uint256 otherVotesBefore = getVotes(other);
uint256 otherCkptsBefore = numCheckpoints(other);
// run transaction
burn@withrevert(e, from, amount);
bool success = !lastReverted;
uint256 votesAfter = getVotes(fromDelegate);
uint32 ckptsAfter = numCheckpoints(fromDelegate);
mathint clockAfter = ckptsAfter == 0 ? -1 : numCheckpoints(fromDelegate);
uint256 otherVotesAfter = getVotes(other);
uint256 otherCkptsAfter = numCheckpoints(other);
// liveness
assert success <=> (from != 0 || amount <= fromBalanceBefore);
// effects
assert (
success &&
fromDelegate != 0
) => (
votesAfter == votesBefore + amount &&
ckptsAfter == ckptsBefore + to_mathint(clockBefore == clockAfter ? 0 : 1) &&
clockAfter == clock(e)
);
// no side effects
assert otherVotesAfter != otherVotesBefore => other == fromDelegate;
assert otherCkptsAfter != otherCkptsBefore => other == fromDelegate;
}
*/