This sheet describes the properties checked during formal verification of ERC-20 contracts. The first section describes the formal verification approach. The second section describes the properties formulas using logical formulas.
Formal verification done using symbolic model checking. Each ERC-20 smart contract written in Solidity is compiled into a mathematical model that reflects all its possible behaviors with respect to the property. The model takes into account the semantics of the Solidity code found in the contract. All verification results that we report are based on that model.
The model also formalizes a simplified execution environment of the Ethereum blockchain and a verification harness that performs the initialization of the contract and all possible interactions with the contract. Initially, the contract state is initialized non-deterministically (i.e. by arbitrary values) and over-approximates the reachable state space of the contract throughout any actual deployment on chain. All valid results thus carry over to the contract's behavior in arbitrary states after it has been deployed.
The following assumptions and simplifications apply to our model:
All properties are expressed in linear temporal logic (LTL). For that matter, we treat each invocation of and each return from a public or an external function as a discrete time step. Our analysis reasons about the contract's state upon entering and upon leaving public or external functions.
Apart from the Boolean connectives and the modal operators "always" (written []
) and "eventually" (written <>
), we use the following predicates as atomic propositions. They are evaluated on the contract's state whenever a discrete time step occurs:
started(f, [cond])
Indicates an invocation of contract function f
within a state satisfying formula cond
.willSucceed(f, [cond])
Indicates an invocation of contract function f
within a state satisfying formula cond
and considers only those executions that do not revert.finished(f, [cond])
Indicates that execution returns from contract function f
in a state satisfying formula cond
. Here, formula cond
may refer to the contract's state variables and to the value they had upon entering the function (using the old
function).reverted(f, [cond])
Indicates that execution of contract function f
was interrupted by an exception in a contract state satisfying formula cond
.The verification performed in this audit operates on a harness that non-deterministically invokes a function of the contract's public or external interface. All formulas are analyzed w.r.t. the trace that corresponds to this function invocation.
The specifications are designed such that they capture the desired and admissible behaviors of the ERC-20 functions transfer
, transferFrom
, approve
, allowance
, balanceOf
, and totalSupply
.
In the following, we list those property specifications.
transfer
Function transfer
Prevents Transfers to the Zero Address.
Any call of the form transfer(recipient, amount)
must fail if the recipient address is the zero address.
Specification:
[](started(contract.transfer(to, value), to == address(0)) ==> <>(reverted(contract.transfer) || finished(contract.transfer(to, value), return == false)))
Function transfer
Succeeds on Admissible Non-self Transfers.
All invocations of the form transfer(recipient, amount)
must succeed and return true
if
recipient
address is not the zero address,amount
does not exceed the balance of address msg.sender
,amount
to the recipient
address does not lead to an overflow of the recipient's balance, and[](started(contract.transfer(to, value), to != address(0) && to != msg.sender && value >= 0 && value <= _balances[msg.sender] && _balances[to] + value < 0x10000000000000000000000000000000000000000000000000000000000000000 && _balances[to] >= 0 && _balances[msg.sender] < 0x10000000000000000000000000000000000000000000000000000000000000000) ==> <>(finished(contract.transfer(to, value), return == true)))
Function transfer
Succeeds on Admissible Self Transfers.
All self-transfers, i.e. invocations of the form transfer(recipient, amount)
where the recipient
address equals the address in msg.sender
must succeed and return true
if
amount
does not exceed the balance of msg.sender
and[](started(contract.transfer(to, value), to != address(0) && to == msg.sender && value >= 0 && value <= _balances[msg.sender] && _balances[msg.sender] >= 0 && _balances[msg.sender] < 0x10000000000000000000000000000000000000000000000000000000000000000) ==> <>(finished(contract.transfer(to, value), return == true)))
Function transfer
Transfers the Correct Amount in Non-self Transfers.
All non-reverting invocations of transfer(recipient, amount)
that return true
must subtract the value in amount
from the balance of msg.sender
and add the same value to the balance of the recipient
address.
Specification:
[](willSucceed(contract.transfer(to, value), to != msg.sender && _balances[to] >= 0 && value >= 0 && _balances[to] + value < 0x10000000000000000000000000000000000000000000000000000000000000000 && _balances[msg.sender] >= 0 && _balances[msg.sender] < 0x10000000000000000000000000000000000000000000000000000000000000000) ==> <>(finished(contract.transfer(to, value), return == true ==> _balances[msg.sender] == old(_balances[msg.sender]) - value && _balances[to] == old(_balances[to]) + value)))
Function transfer
Transfers the Correct Amount in Self Transfers.
All non-reverting invocations of transfer(recipient, amount)
that return true
and where the recipient
address equals msg.sender
(i.e. self-transfers) must not change the balance of address msg.sender
.
Specification:
[](willSucceed(contract.transfer(to, value), to == msg.sender && _balances[to] >= 0 && _balances[to] < 0x10000000000000000000000000000000000000000000000000000000000000000) ==> <>(finished(contract.transfer(to, value), return == true ==> _balances[to] == old(_balances[to]))))
Function transfer
Has No Unexpected State Changes.
All non-reverting invocations of transfer(recipient, amount)
that return true
must only modify the balance entries of the msg.sender
and the recipient
addresses.
Specification:
[](willSucceed(contract.transfer(to, value), p1 != msg.sender && p1 != to) ==> <>(finished(contract.transfer(to, value), return == true ==> (_totalSupply == old(_totalSupply) && _allowances == old(_allowances) && _balances[p1] == old(_balances[p1]) && other_state_variables == old(other_state_variables)))))
Function transfer
Fails if Requested Amount Exceeds Available Balance.
Any transfer of an amount of tokens that exceeds the balance of msg.sender
must fail.
Specification:
[](started(contract.transfer(to, value), value > _balances[msg.sender] && _balances[msg.sender] >= 0 && value < 0x10000000000000000000000000000000000000000000000000000000000000000) ==> <>(reverted(contract.transfer) || finished(contract.transfer(to, value), return == false)))
Function transfer
Prevents Overflows in the Recipient's Balance.
Any invocation of transfer(recipient, amount)
must fail if it causes the balance of the recipient
address to overflow.
Specification:
[](started(contract.transfer(to, value), to != msg.sender && _balances[to] + value >= 0x10000000000000000000000000000000000000000000000000000000000000000 && _balances[to] >= 0 && _balances[to] < 0x10000000000000000000000000000000000000000000000000000000000000000 && _balances[msg.sender] < 0x10000000000000000000000000000000000000000000000000000000000000000 && value > 0 && value <= _balances[msg.sender]) ==> <>(reverted(contract.transfer) || finished(contract.transfer(to, value), return == false) || finished(contract.transfer(to, value), _balances[to] > old(_balances[to]) + value - 0x10000000000000000000000000000000000000000000000000000000000000000)))
If Function transfer
Returns false
, the Contract State Has Not Been Changed.
If the transfer
function in contract contract
fails by returning false
, it must undo all state changes it incurred before returning to the caller.
Specification:
[](willSucceed(contract.transfer(to, value)) ==> <>(finished(contract.transfer(to, value), return == false ==> (_balances == old(_balances) && _totalSupply == old(_totalSupply) && _allowances == old(_allowances) && other_state_variables == old(other_state_variables)))))
Function transfer
Never Returns false
.
The transfer function must never return false
to signal a failure.
Specification:
[](!(finished(contract.transfer, return == false)))
transferFrom
Function transferFrom
Fails for Transfers From the Zero Address.
All calls of the form transferFrom(from, dest, amount)
where the from
address is zero, must fail.
Specification:
[](started(contract.transferFrom(from, to, value), from == address(0)) ==> <>(reverted(contract.transferFrom) || finished(contract.transferFrom, return == false)))
Function transferFrom
Fails for Transfers To the Zero Address.
All calls of the form transferFrom(from, dest, amount)
where the dest
address is zero, must fail.
Specification:
[](started(contract.transferFrom(from, to, value), to == address(0)) ==> <>(reverted(contract.transferFrom) || finished(contract.transferFrom, return == false)))
Function transferFrom
Succeeds on Admissible Non-self Transfers.
All invocations of transferFrom(from, dest, amount)
must succeed and return true
if
amount
does not exceed the balance of address from
,amount
does not exceed the allowance of msg.sender
for address from
,amount
to the address in dest
does not lead to an overflow of the recipient's balance, and[](started(contract.transferFrom(from, to, value), from != address(0) && to != address(0) && from != to && value <= _balances[from] && value <= _allowances[from][msg.sender] && _balances[to] + value < 0x10000000000000000000000000000000000000000000000000000000000000000 && value >= 0 && _balances[to] >= 0 && _balances[from] >= 0 && _balances[from] < 0x10000000000000000000000000000000000000000000000000000000000000000 && _allowances[from][msg.sender] >= 0 && _allowances[from][msg.sender] < 0x10000000000000000000000000000000000000000000000000000000000000000) ==> <>(finished(contract.transferFrom(from, to, value), return == true)))
Function transferFrom
Succeeds on Admissible Self Transfers.
All invocations of transferFrom(from, dest, amount)
where the dest
address equals the from
address (i.e. self-transfers) must succeed and return true
if:
amount
does not exceed the balance of address from
,amount
does not exceed the allowance of msg.sender
for address from
, and[](started(contract.transferFrom(from, to, value), from != address(0) && from == to && value <= _balances[from] && value <= _allowances[from][msg.sender] && value >= 0 && _balances[from] < 0x10000000000000000000000000000000000000000000000000000000000000000 && _allowances[from][msg.sender] < 0x10000000000000000000000000000000000000000000000000000000000000000) ==> <>(finished(contract.transferFrom(from, to, value), return == true)))
Function transferFrom
Transfers the Correct Amount in Non-self Transfers.
All invocations of transferFrom(from, dest, amount)
that succeed and that return true
subtract the value in amount
from the balance of address from
and add the same value to the balance of address dest
.
Specification:
[](willSucceed(contract.transferFrom(from, to, value), from != to && value >= 0 && _balances[from] >= 0 && _balances[from] < 0x10000000000000000000000000000000000000000000000000000000000000000 && _balances[to] >= 0 && _balances[to] + value < 0x10000000000000000000000000000000000000000000000000000000000000000) ==> <>(finished(contract.transferFrom(from, to, value), return == true ==> _balances[from] == old(_balances[from]) - value && _balances[to] == old(_balances[to] + value))))
Function transferFrom
Performs Self Transfers Correctly.
All non-reverting invocations of transferFrom(from, dest, amount)
that return true
and where the address in from
equals the address in dest
(i.e. self-transfers) do not change the balance entry of the from
address (which equals dest
).
Specification:
[](willSucceed(contract.transferFrom(from, to, value), from == to && value >= 0 && value < 0x10000000000000000000000000000000000000000000000000000000000000000 && _balances[from] >= 0 && _balances[from] < 0x10000000000000000000000000000000000000000000000000000000000000000) ==> <>(finished(contract.transferFrom(from, to, value), return == true ==> _balances[from] == old(_balances[from]))))
Function transferFrom
Updated the Allowance Correctly.
All non-reverting invocations of transferFrom(from, dest, amount)
that return true
must decrease the allowance for address msg.sender
over address from
by the value in amount
.
Specification:
[](willSucceed(contract.transferFrom(from, to, value), value >= 0 && value < 0x10000000000000000000000000000000000000000000000000000000000000000 && _balances[from] >= 0 && _balances[from] < 0x10000000000000000000000000000000000000000000000000000000000000000 && _balances[to] >= 0 && _balances[to] < 0x10000000000000000000000000000000000000000000000000000000000000000 && _allowances[from][msg.sender] >= 0 && _allowances[from][msg.sender] < 0x10000000000000000000000000000000000000000000000000000000000000000) ==> <>(finished(contract.transferFrom(from, to, value), return == true ==> ((_allowances[from][msg.sender] == old(_allowances[from][msg.sender]) - value) || (_allowances[from][msg.sender] == old(_allowances[from][msg.sender]) && (from == msg.sender || old(_allowances[from][msg.sender]) == 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF))))))
Function transferFrom
Has No Unexpected State Changes.
All non-reverting invocations of transferFrom(from, dest, amount)
that return true
may only modify the following state variables:
dest
,from
,msg.sender
for the address in from
.
Specification:[](willSucceed(contract.transferFrom(from, to, amount), p1 != from && p1 != to && (p2 != from || p3 != msg.sender)) ==> <>(finished(contract.transferFrom(from, to, amount), return == true ==> (_totalSupply == old(_totalSupply) && _balances[p1] == old(_balances[p1]) && _allowances[p2][p3] == old(_allowances[p2][p3]) && other_state_variables == old(other_state_variables)))))
Function transferFrom
Fails if the Requested Amount Exceeds the Available Balance.
Any call of the form transferFrom(from, dest, amount)
with a value for amount
that exceeds the balance of address from
must fail.
Specification:
[](started(contract.transferFrom(from, to, value), value > _balances[from] && _balances[from] >= 0 && _balances[from] < 0x10000000000000000000000000000000000000000000000000000000000000000) ==> <>(reverted(contract.transferFrom) || finished(contract.transferFrom, return == false)))
Function transferFrom
Fails if the Requested Amount Exceeds the Available Allowance.
Any call of the form transferFrom(from, dest, amount)
with a value for amount
that exceeds the allowance of address msg.sender
must fail.
Specification:
[](started(contract.transferFrom(from, to, value), value > _allowances[from][msg.sender] && _allowances[from][msg.sender] >= 0 && value < 0x10000000000000000000000000000000000000000000000000000000000000000) ==> <>(reverted(contract.transferFrom) || finished(contract.transferFrom(from, to, value), return == false) || finished(contract.transferFrom(from, to, value), return == true && (msg.sender == from || _allowances[from][msg.sender] == 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF))))
Function transferFrom
Prevents Overflows in the Recipient's Balance.
Any call of transferFrom(from, dest, amount)
with a value in amount
whose transfer would cause an overflow of the balance of address dest
must fail.
Specification:
[](started(contract.transferFrom(from, to, value), from != to && _balances[to] + value >= 0x10000000000000000000000000000000000000000000000000000000000000000 && value < 0x10000000000000000000000000000000000000000000000000000000000000000 && _balances[to] >= 0 && _balances[to] < 0x10000000000000000000000000000000000000000000000000000000000000000) ==> <>(reverted(contract.transferFrom) || finished(contract.transferFrom(from, to, value), return == false) || finished(contract.transferFrom(from, to, value), _balances[to] > old(_balances[to]) + value - 0x10000000000000000000000000000000000000000000000000000000000000000)))
If Function transferFrom
Returns false
, the Contract's State Has Not Been Changed.
If transferFrom
returns false
to signal a failure, it must undo all incurred state changes before returning to the caller.
Specification:
[](willSucceed(contract.transferFrom(from, to, value)) ==> <>(finished(contract.transferFrom(from, to, value), return == false ==> (_balances == old(_balances) && _totalSupply == old(_totalSupply) && _allowances == old(_allowances) && other_state_variables == old(other_state_variables)))))
Function transferFrom
Never Returns false
.
The transferFrom
function must never return false
.
Specification:
[](!(finished(contract.transferFrom, return == false)))
totalSupply
Function totalSupply
Always Succeeds.
The function totalSupply
must always succeeds, assuming that its execution does not run out of gas.
Specification:
[](started(contract.totalSupply) ==> <>(finished(contract.totalSupply)))
Function totalSupply
Returns the Value of the Corresponding State Variable.
The totalSupply
function must return the value that is held in the corresponding state variable of contract contract.
Specification:
[](willSucceed(contract.totalSupply) ==> <>(finished(contract.totalSupply, return == _totalSupply)))
Function totalSupply
Does Not Change the Contract's State.
The totalSupply
function in contract contract must not change any state variables.
Specification:
[](willSucceed(contract.totalSupply) ==> <>(finished(contract.totalSupply, _totalSupply == old(_totalSupply) && _balances == old(_balances) && _allowances == old(_allowances) && other_state_variables == old(other_state_variables))))
balanceOf
Function balanceOf
Always Succeeds.
Function balanceOf
must always succeed if it does not run out of gas.
Specification:
[](started(contract.balanceOf) ==> <>(finished(contract.balanceOf)))
Function balanceOf
Returns the Correct Value.
Invocations of balanceOf(owner)
must return the value that is held in the contract's balance mapping for address owner
.
Specification:
[](willSucceed(contract.balanceOf) ==> <>(finished(contract.balanceOf(owner), return == _balances[owner])))
Function balanceOf
Does Not Change the Contract's State.
Function balanceOf
must not change any of the contract's state variables.
Specification:
[](willSucceed(contract.balanceOf) ==> <>(finished(contract.balanceOf(owner), _totalSupply == old(_totalSupply) && _balances == old(_balances) && _allowances == old(_allowances) && other_state_variables == old(other_state_variables))))
allowance
Function allowance
Always Succeeds.
Function allowance
must always succeed, assuming that its execution does not run out of gas.
Specification:
[](started(contract.allowance) ==> <>(finished(contract.allowance)))
Function allowance
Returns Correct Value.
Invocations of allowance(owner, spender)
must return the allowance that address spender
has over tokens held by address owner
.
Specification:
[](willSucceed(contract.allowance(owner, spender)) ==> <>(finished(contract.allowance(owner, spender), return == _allowances[owner][spender])))
Function allowance
Does Not Change the Contract's State.
Function allowance
must not change any of the contract's state variables.
Specification:
[](willSucceed(contract.allowance(owner, spender)) ==> <>(finished(contract.allowance(owner, spender), _totalSupply == old(_totalSupply) && _balances == old(_balances) && _allowances == old(_allowances) && other_state_variables == old(other_state_variables))))
approve
Function approve
Prevents Giving Approvals For the Zero Address.
All calls of the form approve(spender, amount)
must fail if the address in spender
is the zero address.
Specification:
[](started(contract.approve(spender, value), spender == address(0)) ==> <>(reverted(contract.approve) || finished(contract.approve(spender, value), return == false)))
Function approve
Succeeds for Admissible Inputs.
All calls of the form approve(spender, amount)
must succeed, if
spender
is not the zero address and[](started(contract.approve(spender, value), spender != address(0)) ==> <>(finished(contract.approve(spender, value), return == true)))
Function approve
Updates the Approval Mapping Correctly.
All non-reverting calls of the form approve(spender, amount)
that return true
must correctly update the allowance mapping according to the address msg.sender
and the values of spender
and amount
.
Specification:
[](willSucceed(contract.approve(spender, value), spender != address(0) && value >= 0 && value < 0x10000000000000000000000000000000000000000000000000000000000000000) ==> <>(finished(contract.approve(spender, value), return == true ==> _allowances[msg.sender][spender] == value)))
Function approve
Has No Unexpected State Changes.
All calls of the form approve(spender, amount)
must only update the allowance mapping according to the address msg.sender
and the values of spender
and amount
and incur no other state changes.
Specification:
[](willSucceed(contract.approve(spender, value), spender != address(0) && (p1 != msg.sender || p2 != spender)) ==> <>(finished(contract.approve(spender, value), return == true ==> _totalSupply == old(_totalSupply) && _balances == old(_balances) && _allowances[p1][p2] == old(_allowances[p1][p2]) && other_state_variables == old(other_state_variables))))
If Function approve
Returns false
, the Contract's State Has Not Been Changed.
If function approve
returns false
to signal a failure, it must undo all state changes that it incurred before returning to the caller.
Specification:
[](willSucceed(contract.approve(spender, value)) ==> <>(finished(contract.approve(spender, value), return == false ==> (_balances == old(_balances) && _totalSupply == old(_totalSupply) && _allowances == old(_allowances) && other_state_variables == old(other_state_variables)))))
Function approve
Never Returns false
.
The function approve
must never returns false
.
Specification:
[](!(finished(contract.approve, return == false)))