4.4. Ghosts and hooks¶
Ghost variables¶
Ghost variables are variables defined in CVL that behave like the verified contract’s storage data. For example, they revert when the storage reverts, and they can be set to specific values after the constructor. They can be directly accessed from the spec, both to read and to write.
Examples¶
ghost mathint numVoted;
ghost mapping(address => uint256) balanceGhost;
Setting initial state¶
To set the initial value of a ghost variable (i.e. its value after the constructor), we use the following syntax:
ghost mathint someGhost {
init_state axiom someGhost == 0;
}
The expression init_state axiom <boolean condition>;
ensures that immediately
after the constructor, the boolean condition would be true. Note, however, that at any
other time its value is not constrained.
Important
The <boolean condition>
expression may not contain calls to solidity functions.
Here is another simple example:
ghost bool boolGhost {
init_state axiom !boolGhost;
}
The example below sets the value of func(x)
to be x
whenever x
is
even:
ghost mapping(uint256 => uint256) func {
init_state axiom forall uint256 x. (x % 2 == 0) => (func(x) == x);
}
Storage hooks¶
Storage hooks allow us to inject CVL code whenever a storage variable is accessed, whether for read or write. The syntax is detailed in the Hooks documentation. One use for storage hooks is to update ghost variables.
Example¶
Consider the simple voting contract from Voting.sol shown below.
contract Voting {
mapping(address => bool) internal _hasVoted;
uint256 public votesInFavor;
uint256 public votesAgainst;
uint256 public totalVotes;
function vote(bool isInFavor) public {
require(!_hasVoted[msg.sender]);
_hasVoted[msg.sender] = true;
totalVotes += 1;
if (isInFavor) {
votesInFavor += 1;
} else {
votesAgainst += 1;
}
}
function hasVoted(address voter) public view returns (bool) {
return _hasVoted[voter];
}
}
We want to prove that the number of times _hasVoted
is updated equals
totalVotes
. To do this we define a ghost variable numVoted
that is
updated every time the _hasVoted
mapping is modified.
This allows us to count the number of times values in _hasVoted
changed from
false to true.
ghost mathint numVoted {
// No votes at start
init_state axiom numVoted == 0;
}
hook Sstore _hasVoted[KEY address voter]
bool newVal (bool oldVal) {
numVoted = numVoted + 1;
}
/// @title Total voted intergrity
invariant sumResultsEqualsTotalVotes()
to_mathint(totalVotes()) == numVoted;
To be precise, the invariant sumResultsEqualsTotalVotes
means that
totalVotes
equals the number of times a value has changed from false to true
in _hasVoted
.