We will formally verify the part that checks that the bytecode is welltyped, so that when a smart contract is executed it cannot encounter critical errors. The type checker itself is also written in Rust, and we will verify it using the proof assistant Coq ๐ and our tool coqofrust that translates Rust programs to Coq.
To formally verify your Rust code and ensure it contains no bugs or vulnerabilities, contact us at ๐งcontact@formal.land.
The cost is โฌ10 per line of Rust code (excluding comments) and โฌ20 per line for concurrent code.
The plan for this project is as follows:
coqofrust
. This part will give more precise results than the tests, as it will cover all possible inputs and states of the program. The complexity of this part is to go through all the details that exist in the Rust code, like the use of references to manipulate the memory, the macros after expansion, and the parts of the Rust standard library that the code depends on.For now, we have written a simulation for the type checker in CoqOfRust/move_sui/simulations/move_bytecode_verifier/type_safety.v. We are now:
In the following blog posts, we will describe how we structured the simulations and how we are testing or verifying them.
This project is kindly founded by the Sui Foundation which we thank for their trust and support.
The proofs are still tedious for now, as there are around 1,000 lines of proofs for 100 lines of Solidity. We plan to automate this work as much as possible in the subsequent iterations of the tool. One good thing about the interactive theorem prover Coq is that we know we can never be stuck, so we can always make progress in our proof techniques and verify complex properties even if it takes time โจ.
Formal verification with an interactive proof assistant is the strongest way to verify programs since:
To audit your smart contracts and make sure they contain no bugs, contact us at ๐งcontact@formal.land.
We refund our work if we missed a high/critical severity bug.
We specify the ERC20 smart contract by writing an equivalent version in Coq that acts as a functional specification. In this specification, we ignore the emit
operations that are logging events in Solidity and the precise payload of revert operations (we only say that "a revert occurs"). We make all our arithmetic operations on Z
the type of unbounded integers with explicit overflow checks.
For example, here is the _transfer
function of the Solidity smart contract:
function _transfer(address from, address to, uint256 value) internal {
require(to != address(0), "ERC20: transfer to the zero address");
// The subtraction and addition here will revert on overflow.
_balances[from] = _balances[from]  value;
_balances[to] = _balances[to] + value;
emit Transfer(from, to, value);
}
We specify it in the file erc20.v by:
Definition _transfer (from to : Address.t) (value : U256.t) (s : Storage.t)
: Result.t Storage.t :=
if to =? 0 then
revert_address_null
else if balanceOf s from <? value then
revert_arithmetic
else
let s :=
s < Storage.balances :=
Dict.declare_or_assign s.(Storage.balances) from (balanceOf s from  value)
> in
if balanceOf s to + value >=? 2 ^ 256 then
revert_arithmetic
else
Result.Success s < Storage.balances :=
Dict.declare_or_assign s.(Storage.balances) to (balanceOf s to + value)
>.
With the Coq notation:
storage < field := new_value >
we modify a storage element as in the equivalent Solidity:
field = new_value;
With the two tests:
if balanceOf s from <? value then
if balanceOf s to + value >=? 2 ^ 256 then
we make explicit the overflow checks that are implicit in the Solidity code.
A Solidity smart contract has two public functions:
We will focus on the second one. It takes the contract's payload in a specific format:
This blog article Deconstructing a Solidity ContractโโPart III: The Function Selector from OpenZeppelin gives more information about it. In Coq, we represent the payload of a contract with a sum type:
Module Payload.
Inductive t : Set :=
 Transfer (to: Address.t) (value: U256.t)
 Approve (spender: Address.t) (value: U256.t)
 TransferFrom (from: Address.t) (to: Address.t) (value: U256.t)
 IncreaseAllowance (spender: Address.t) (addedValue: U256.t)
 DecreaseAllowance (spender: Address.t) (subtractedValue: U256.t)
 TotalSupply
 BalanceOf (owner: Address.t)
 Allowance (owner: Address.t) (spender: Address.t).
End Payload.
We define how to get this payload from the binary representation:
Definition of_calldata (callvalue : U256.t) (calldata: list U256.t) :
option Payload.t :=
if Z.of_nat (List.length calldata) <? 4 then
None
else
let selector := Stdlib.Pure.shr (256  32) (StdlibAux.get_calldata_u256 calldata 0) in
if selector =? get_selector "approve(address,uint256)" then
let to := StdlibAux.get_calldata_u256 calldata (4 + 32 * 0) in
let value := StdlibAux.get_calldata_u256 calldata (4 + 32 * 1) in
if negb (callvalue =? 0) then
None
else if negb (get_have_enough_calldata (32 * 2) calldata) then
None
else if negb (get_is_address_valid to) then
None
else
Some (Approve to value)
else if selector =? get_selector "totalSupply()" then
(* ... other cases ... *)
The callvalue
is the amount of Ether sent with the transaction, which has to be zero for nonpayable functions. The calldata
is the list bytes of the payload of the transaction. We check that the length of the payload is at least 4 bytes, then we extract the selector and the arguments of the function. We check that the arguments are valid, and we return the corresponding payload or None
in case of error.
Note that a lot of the code is very repetitive and can be generated automatically by AI. For example the definition of the Payload.t
type was automatically generated by Claude.ai in one shot, with the code of the smart contract and its specification in context.
Here is the lemma stating that, for any possible user inputs and storage values, the Solidity smart contract and the Coq specification behave exactly the same:
Lemma run_body codes environment state
(s : erc20.Storage.t)
(H_environment : Environment.Valid.t environment)
(H_s : erc20.Storage.Valid.t s) :
let memoryguard := 128 in
let memory_start :=
[0; 0; 0; 0; 0] in
let state_start :=
make_state environment state memory_start (SimulatedStorage.of_erc20_state s) in
let output :=
The functional specification here:
erc20.body
environment.(Environment.caller)
environment.(Environment.callvalue)
s
environment.(Environment.calldata) in
let memory_end_middle :=
[memoryguard; 0] in
let state_end :=
match output with
 erc20.Result.Revert _ _ => None
 erc20.Result.Success (memory_end_beginning, memory_end_end, s) =>
Some (make_state environment state
(memory_end_beginning ++ memory_end_middle ++ memory_end_end)
(SimulatedStorage.of_erc20_state s)
)
end in
{{? codes, environment, Some state_start 
The original code here:
ERC20_403.ERC20_403_deployed.body โ
match output with
 erc20.Result.Revert p s => Result.Revert p s
 erc20.Result.Success (_, memory_end_end, _) =>
Result.Return memoryguard (32 * Z.of_nat (List.length memory_end_end))
end
 state_end ?}}.
The proof is done in the same way as in the previous blog post ๐ช Coq of Solidity โ part 3 about the verification of the _approve
function. The body of the contract calls all the other functions of the contract, and we reuse the equivalence proofs for the other functions here.
The main difficulty we encountered in the proof was missing information in the specification. For example, our predicate of equivalence requires for the memory of the smart contract to have the exact same value as its specification at the end of execution, except in case of revert. This means we needed to add the final state of the memory in the specification also, even if this is an implementation detail. We will refine our equivalence statement in the future to avoid this kind of issue.
For the most part of the proof, the work was about stepping through both codes and making sure, by automatic unification, that the twos are indeed equal.
The development of coqofsolidity
is made possible thanks to the AlephZero project. We thank the AlephZero Foundation for their support ๐.
We have presented how to specify and formally verify a typical smart contract in Solidity, the ERC20 token, using our tool coqofsolidity
(opensource). In the next post, we will see how to verify an invariant on the code and how the proof system Coq reacts if we introduce a bug.
This is very important as a single bug can lead to the loss of millions of dollars in smart contracts, as we have regularly seen in the past, and we can never be sure that a human review of the code did not miss anything.
Our tool coqofsolidity
is one of the only tools using an interactive theorem prover for Solidity, together with Clear from Nethermind. This might be the most powerful approach to making code without bugs, as exemplified in this PLDI paper comparing the reliability of various C compilers. They found numerous bugs in each compiler except in the formally verified one!
In this blog post we show how we functionally specify and verify the _approve
function of an ERC20 smart contract. We will see how we prove that a refined version of the function is equivalent to the original one.
The development of coqofsolidity
is made possible thanks to the AlephZero project. We thank the AlephZero Foundation for their support ๐.
Here is the _approve
function of the Solidity smart contract that we want to specify:
mapping (address => mapping (address => uint256)) private _allowances;
function _approve(address owner, address spender, uint256 value) internal {
require(owner != address(0), "ERC20: approve from the zero address");
require(spender != address(0), "ERC20: approve to the zero address");
_allowances[owner][spender] = value;
emit Approval(owner, spender, value);
}
It modifies an item in the _allowances
map and emits an Approval
event after a few sanity checks. We will now write a functional specification of this function in Coq. The idea is to explain what this function is supposed to do describing its behavior with an idiomatic Coq code. This will be useful to make sure there are no mistakes in the smart contract, although here we have a very simple example. From the functional specification, we will also be able to check higherlevel properties of the smart contract, such as the fact that the total amount of tokens is always conserved.
Here is the Coq version of the _approve
function:
Module Storage.
Record t := {
allowances : Dict.t (Address.t * Address.t) U256.t;
(* other fields *)
}.
End Storage.
Definition _approve (owner spender : Address.t) (value : U256.t) (s : Storage.t) :
Result.t Storage.t :=
if (owner =? 0)  (spender =? 0) then
revert_address_null
else
Result.Success s < Storage.allowances :=
Dict.declare_or_assign s.(Storage.allowances) (owner, spender) value
>.
It takes the same parameters as the Solidity code: owner
, spender
, value
, and the current state s
of the storage. It returns a Result.t Storage.t
type, which is either a Result.Success
with the new storage after the execution of the _approve
function, or a revert_address_null
if the owner
or spender
is the null address. To create the new storage, we use the corresponding Coq notation and function to update the _allowances
map.
We ignore the emit
primitives for now.
Now let us show that, for any possible owner
, spender
, value
, and storage state s
, the _approve
function in Solidity will behave exactly as the Coq specification.
Here is the Coq translation of the _approve
function as generated by coqofsolidity
:
Definition fun_approve (var_owner : U256.t) (var_spender : U256.t) (var_value : U256.t) : M.t unit :=
let~ _1 := [[ and ~( var_owner, (sub ~( (shl ~( 160, 1 )), 1 )) ) ]] in
do~ [[
M.if_unit ( iszero ~( _1 ),
let~ memPtr := [[ mload ~( 64 ) ]] in
do~ [[ mstore ~( memPtr, (shl ~( 229, 4594637 )) ) ]] in
do~ [[ mstore ~( (add ~( memPtr, 4 )), 32 ) ]] in
do~ [[ mstore ~( (add ~( memPtr, 36 )), 36 ) ]] in
do~ [[ mstore ~( (add ~( memPtr, 68 )), 0x45524332303a20617070726f76652066726f6d20746865207a65726f20616464 ) ]] in
do~ [[ mstore ~( (add ~( memPtr, 100 )), 0x7265737300000000000000000000000000000000000000000000000000000000 ) ]] in
do~ [[ revert ~( memPtr, 132 ) ]] in
M.pure tt
)
]] in
let~ _2 := [[ and ~( var_spender, (sub ~( (shl ~( 160, 1 )), 1 )) ) ]] in
do~ [[
M.if_unit ( iszero ~( _2 ),
let~ memPtr_1 := [[ mload ~( 64 ) ]] in
do~ [[ mstore ~( memPtr_1, (shl ~( 229, 4594637 )) ) ]] in
do~ [[ mstore ~( (add ~( memPtr_1, 4 )), 32 ) ]] in
do~ [[ mstore ~( (add ~( memPtr_1, 36 )), 34 ) ]] in
do~ [[ mstore ~( (add ~( memPtr_1, 68 )), 0x45524332303a20617070726f766520746f20746865207a65726f206164647265 ) ]] in
do~ [[ mstore ~( (add ~( memPtr_1, 100 )), 0x7373000000000000000000000000000000000000000000000000000000000000 ) ]] in
do~ [[ revert ~( memPtr_1, 132 ) ]] in
M.pure tt
)
]] in
do~ [[ mstore ~( 0x00, _1 ) ]] in
do~ [[ mstore ~( 0x20, 0x01 ) ]] in
let~ dataSlot := [[ keccak256 ~( 0x00, 0x40 ) ]] in
let~ dataSlot_1 := [[ 0 ]] in
do~ [[ mstore ~( 0, _2 ) ]] in
do~ [[ mstore ~( 0x20, dataSlot ) ]] in
let~ dataSlot_1 := [[ keccak256 ~( 0, 0x40 ) ]] in
do~ [[ sstore ~( dataSlot_1, var_value ) ]] in
let~ _3 := [[ mload ~( 0x40 ) ]] in
do~ [[ mstore ~( _3, var_value ) ]] in
do~ [[ log3 ~( _3, 0x20, 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925, _1, _2 ) ]] in
M.pure tt.
We plug into the Solidity compiler and translate the intermediate representation Yul that solc
uses to generate EVM bytecode. We automatically refine the Yul generated by the Solidity compiler but for now this refinement is limited.
The two M.if_unit
at the beginning correspond to the require
statements in the Solidity code. The revert
statements are used to return an error message to the caller. The mstore
and sstore
functions are used to store values in the memory and the storage of the EVM. The keccak256
function encodes the storage addresses to access the _allowances
map. The log3
function is used to emit an event at the end.
This representation of the _approve
function is very verbose as it corresponds exactly to what the source code does and contains a lot of implementation details. Our goal now is to show that this version is equivalent to the functional specification that we wrote by hand.
We express that the functional specification is equivalent to the original one with this lemma:
Lemma run_fun_approve codes environment state
(owner spender : Address.t) (value : U256.t) (s : erc20.Storage.t)
(mem_0 mem_1 mem_3 mem_4 : U256.t)
(H_owner : Address.Valid.t owner)
(H_spender : Address.Valid.t spender) :
let memoryguard := 0x80 in
let memory_start :=
[mem_0; mem_1; memoryguard; mem_3; mem_4] in
let state_start :=
make_state environment state memory_start (SimulatedStorage.of_erc20_state s) in
let output :=
erc20._approve owner spender value s in
let memory_end :=
[spender; erc20.keccak256_tuple2 owner 1; memoryguard; mem_3; value] in
let state_end :=
match output with
 erc20.Result.Revert _ _ => None
 erc20.Result.Success s =>
Some (make_state environment state memory_end (SimulatedStorage.of_erc20_state s))
end in
{{? codes, environment, Some state_start 
ERC20_403.ERC20_403_deployed.fun_approve owner spender value โ
match output with
 erc20.Result.Revert p s => Result.Revert p s
 erc20.Result.Success _ => Result.Ok tt
end
 state_end ?}}.
This lemma of equivalence requires some parameters:
codes
, environment
, and state
values, that describe the state of the blockchain before the execution of the _approve
function,memoryguard
value that gives a memory zone that we are safe to use,mem_i
variables, as we do not know the exact values of the memory slots before the execution of the function,owner
, spender
, and value
that are the parameters of the _approve
function,s
that is the state of storage of the smart contract before the execution of the _approve
function,H_owner
and H_spender
proofs that the owner
and spender
are valid addresses. These two proofs are required to execute the function as expected and always available, thanks to runtime checks made at the entrypoints of the smart contract.The lemma will hold for any possible values of the parameters above, even if there are infinite possibilities. This is the power of formal verification: we can prove that our smart contract is correct for all possible inputs and states.
The core statement uses the predicate:
{{? codes, environment, start_state 
original_code โ
refined_code
 end_state ?}}
It says that some original_code
executed in the start_state
environment will give the same output as the refined_code
and will result in the final state end_state
. The state is an option type: either Some
state or None
if the execution reverted. That way we do not have to deal with describing the state after a contract revert, that will reset the storage anyways.
The statement of equivalence is relatively verbose so there could be mistakes in the way it is stated. This is not really an issue, as the _approve
function is an intermediate function, so the only statement that really matters is the one on the main function of the contract that dispatches to the relevant entrypoint according to the payload of the transaction. There could also be mistakes there, but perhaps we can automatically generate this statement from the Solidity code.
The way we write the proof is interesting. We use Coq as a symbolic debugger, where we execute both the original code and the functional specification until we reach the end of execution for all the branches, always with the same result.
Here is an example of a debugging step (in the proof mode of Coq):
{{?codes, environment,
Some
(make_state environment state [spender; erc20.keccak256_tuple2 owner 1; 128; mem_3; mem_4]
[IsStorable.IMap.(IsStorable.to_storable_value) s.(erc20.Storage.balances);
StorableValue.Map2
(Dict.declare_or_assign s.(erc20.Storage.allowances) (owner, spender) value);
StorableValue.U256 s.(erc20.Storage.total_supply)])

The original code here:
do~ call (Stdlib.mstore 128 value)
in (do~ call
(Stdlib.log3 128 32
63486140976153616755203102783360879283472101686154884697241723088393386309925
owner spender) in LowM.Pure (Result.Ok tt)) โ
The functional specification here:
Result.Ok tt
 Some
(make_state environment state [spender; erc20.keccak256_tuple2 owner 1; 128; mem_3; value]
(SimulatedStorage.of_erc20_state
s<erc20.Storage.allowances:= Dict.declare_or_assign s.(erc20.Storage.allowances)
(owner, spender) value>))?}}
On the original code side we can recognize:
do~ [[ mstore ~( _3, var_value ) ]] in
do~ [[ log3 ~( _3, 0x20, 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925, _1, _2 ) ]] in
M.pure tt
that corresponds to the end of the execution of the _approve
function. On the functional specification, we have:
Result.Ok tt
that ends the execution successfully but does not return anything. This is because we ignore the emit
operation, translated as a log3
Yul primitive. We also ignore the mstore
call as it is only used to fill information for the log3
call.
Here are the various commands to step through the code, encoded as Coq tactics:
p
: final Pure expressionpn
: final Pure expression ignoring the resulting state with a None (for a revert)pe
: final Pure expression with nontrivial Equality of resultspr
: Yul PRimitiveprn
: Yul PRimitive ignoring the resulting state with a Nonel
: step in a Letlu
: step in a Let by Unfoldingc
: step in a function Callcu
: step in a function Call by Unfoldings
: Simplify the goalThese commands verify that the two programs are equivalent as we step through them. As a reference, the proof is in CoqOfSolidity/proofs/ERC20_functional.v:
Proof.
simpl.
unfold ERC20_403.ERC20_403_deployed.fun_approve, erc20._approve.
l. {
now apply run_is_non_null_address.
}
unfold Stdlib.Pure.iszero.
lu.
c; [p].
s.
unfold Stdlib.Pure.iszero.
destruct (owner =? 0); s. {
change (true  _) with true; s.
lu; c. {
apply_run_mload.
}
repeat (
lu 
cu 
(prn; intro) 
s 
p
).
}
l. {
now apply run_is_non_null_address.
}
lu.
c; [p]; s.
unfold Stdlib.Pure.iszero.
change (false  ?e) with e; s.
destruct (spender =? 0); s. {
lu; c. {
apply_run_mload.
}
repeat (
lu 
cu 
(prn; intro) 
s 
p
).
}
lu; c. {
apply_run_mstore.
}
CanonizeState.execute.
lu; c. {
apply_run_mstore.
}
CanonizeState.execute.
lu; c. {
apply_run_keccak256_tuple2.
}
lu.
lu; c. {
apply_run_mstore.
}
CanonizeState.execute.
lu; c. {
apply_run_mstore.
}
CanonizeState.execute.
lu; c. {
apply_run_keccak256_tuple2.
}
lu; c. {
apply_run_sstore_map2_u256.
}
CanonizeState.execute.
lu; c. {
apply_run_mload.
}
s.
lu; c. {
apply_run_mstore.
}
CanonizeState.execute.
lu; c. {
p.
}
p.
Qed.
To audit your smart contracts and make sure they contain no bugs, contact us at ๐งcontact@formal.land.
We refund our work in case we missed any high/critical severity bugs.
We have presented how to functionally specify a function with coqofsolidity
. In the next blog post we will see how to extend this proof and specification to the entire ERC20 smart contract.
We work by translating the Yul version of a smart contract to the formal language Coq ๐, in which we then express the code specifications/security properties and formally verify them ๐. The Yul language is an intermediate language used by the Solidity compiler and others to generate EVM bytecode. Yul is simpler than Solidity and at a higher level than the EVM bytecode, making it a good target for formal verification.
In this blog post we present the recent developments we made to simplify the reasoning ๐ง about Yul programs once translated in Coq.
This development is made possible thanks to AlephZero. We thank the Aleph Zero Foundation for their support to bring more security to the Web3 space ๐.
We present here the general workflow to use coqofsolidity
to make sure your smart contracts contain no bugs ๐.
The workflow is as follows:
coqofyul
tool generates a first Coq version. This version is very lowlevel, with, for example, variable names represented by the string of their names.prepare.py
script makes as many refinements as possible in the Coq code to make it more readable and easier to reason about. For example, we order the functions definitions by the order in which they are used and replace the Yul variables by standard Coq variables.The code that coqofsolidity
generates is very verbose. For example, for this Yul function generated by the Solidity compiler to make an addition with overflow check:
function checked_add_uint256(x) > sum
{
sum := add(x, /** @src 0:419:421 "20" */ 0x14)
/// @src 0:33:3484 "contract ERC20 {..."
if gt(x, sum)
{
mstore(0, shl(224, 0x4e487b71))
mstore(4, 0x11)
revert(0, 0x24)
}
}
we get a Coq translation:
Code.Function.make (
"checked_add_uint256",
["x"],
["sum"],
M.scope (
do! ltac:(M.monadic (
M.assign (
["sum"],
Some (M.call (
"add",
[
M.get_var ( "x" );
[Literal.number 0x14]
]
))
)
)) in
do! ltac:(M.monadic (
M.if_ (
M.call (
"gt",
[
M.get_var ( "x" );
M.get_var ( "sum" )
]
),
M.scope (
do! ltac:(M.monadic (
M.expr_stmt (
M.call (
"mstore",
[
[Literal.number 0];
M.call (
"shl",
[
[Literal.number 224];
[Literal.number 0x4e487b71]
]
)
]
)
)
)) in
do! ltac:(M.monadic (
M.expr_stmt (
M.call (
"mstore",
[
[Literal.number 4];
[Literal.number 0x11]
]
)
)
)) in
do! ltac:(M.monadic (
M.expr_stmt (
M.call (
"revert",
[
[Literal.number 0];
[Literal.number 0x24]
]
)
)
)) in
M.pure BlockUnit.Tt
)
)
)) in
M.pure BlockUnit.Tt
)
)
This is quite long to follow, and even harder to use to write formal proofs. We made a script prepare.py that simplifies the code above to:
Definition checked_add_uint256 (x : U256.t) : M.t U256.t :=
let~ sum := [[ add ~( x, 0x14 ) ]] in
do~ [[
M.if_unit ( gt ~( x, sum ),
do~ [[ mstore ~( 0, (shl ~( 224, 0x4e487b71 )) ) ]] in
do~ [[ mstore ~( 4, 0x11 ) ]] in
do~ [[ revert ~( 0, 0x24 ) ]] in
M.pure tt
)
]] in
M.pure sum.
This is much more readable. We have monadic notations to compose all the primitive Yul functions such as mstore
and revert
, that may cause side effects such as memory mutation or premature return. The code uses standard Coq variables and functions instead of strings, which simplifies the proofs.
To make sure that this transformation is correct, we also generate a Coq proof file that shows that our transformation is correct and that the original and transformed code from prepare.py
are equivalent โ๏ธ.
We can simplify the code even further. For example:
add
, gt
, and shl
are purely functional, so we could explicit this property in the Coq code. For now they are called as monadic functions with the notation f ~( arg1, ..., argn )
even if they never make side effects.mstore
function stores values at fixed addresses in the memory, here 0
and 4
. We could remove these memory operations by introducing named variables to hold the results instead.We hope to be able to automate as many refinements as possible in the future, but for now we have to do some manual work ๐ง.
We manually refine the code by showing that it returns the same result, for every possible input and initial memory state, as a simplified code written by hand. For the checked_add_uint256
function above we use:
Definition simulation_checked_add_uint256 (x y : Z) : Result.t Z :=
if x + y >=? 2 ^ 256 then
Result.Revert 0 0x24
else
Result.Ok (x + y).
Here, all the computations are made with the Z
type of unbounded integers that are simpler to manipulate for the proofs. We use an if
statement to explicitly detect the overflows. The revert statement has the same parameters as in the original code, but we do not fill the memory area 0
to 0x24
anymore. The reason is that we ignore what the revert
returned in our specifications as this is not relevant for now and also simplifies the proofs.
In the code above we do not manipulate the memory anymore. In general, we do the following kinds of refinements:
revert
statement.keccak256
hash encoding of the addresses.keccak256
function.For now these transformations are manual and semiautomated, but we hope to automate them as much as possible in the future. By proving that simulation_checked_add_uint256
behaves as the original checked_add_uint256
function we are sure that we can reason on the simplified code instead of the original one without losing any information ๐.
To audit your smart contracts with the method above contact us at ๐งcontact@formal.land.
Compared to other auditing methods, formal verification has the strong advantage of covering all possible execution cases ๐ช.
We have presented the current status of our work to formally verify smart contracts, especially the refinements steps that make the reasoning possible. In our next posts we will continue seeing how we can verify a full smart contract ๐ฎ.
]]>Formal verification is a technique to test a program on all possible entries, even when there are infinitely many. This contrasts with the traditional test techniques, which can only execute a finite set of scenarios. As such, it appears to be an ideal way to bring more security to smart contract audits.
In this blog post, we present the formal verification tool coqofsolidity
that we are developing for Solidity. Its specificities are that:
Here, we present how we translate Solidity code into Coq using the intermediate language Yul. We explain the semantics we use and what remains to be done.
The code is available in our fork of the Solidity compiler at github.com/formalland/coqofsolidity.
We reuse the code of the standard Solidity compiler solc
in order to make sure that we can stay in sync with the evolutions of the language and be compatible with all the Solidity features. Thus, our most straightforward path to implementing a translation tool from Solidity to Coq was to fork the C++ code of solc
in github.com/formalland/coqofsolidity. We add a new solc
's flag ircoq
that tells the compiler to also generate a Coq output in addition to the expected EVM bytecode.
At first, we looked at the direct translation from the Solidity language to Coq, but this was getting too complex. We changed our strategy to instead target the Yul language, an intermediate language used by the Solidity compiler to have an intermediate step in its translation to the EVM bytecode. The Yul language is simpler than Solidity and still has a higher level than the EVM bytecode, making it a good target for formal verification. In contrast to the EVM bytecode, there are no explicit stackmanipulation or goto
instructions in Yul simplifying formal verification.
To give an idea of the size difference between Solidity and Yul, here are the files to export these languages to JSON in the Solidity compiler:
The Solidity language appears as more complex than Yul as the code to translate it to JSON is five times longer.
We copied the file libyul/AsmJsonConverter.cpp
above to make a version that translates Yul to Coq: libyul/AsmCoqConverter.cpp. We reused the code for compilation flags to add a new option ircoq
, which runs the conversion to Coq instead of the conversion to JSON.
To limit the size of the generated Coq code, we translate the Yul code after the optimization passes. This helps to remove boilerplate code but may make the Yul code less relatable to the Solidity sources. Thankfully, the optimized Yul code is still readable in our tests, and the Solidity compiler can prettyprint a version of the optimized Yul code with comments to quote the corresponding Solidity source code.
As an example, here is how we translate the if keyword of Yul:
std::string AsmCoqConverter::operator()(If const& _node)
{
yulAssert(_node.condition, "Invalid if condition.");
std::string ret = "M.if_ (\n";
m_indent++;
ret += indent() + std::visit(*this, *_node.condition) + ",\n";
ret += indent() + (*this)(_node.body) + "\n";
m_indent;
ret += indent() + ")";
return ret;
}
We convert each Yul _node
to an std::string
that represents the Coq code. We use the m_indent
variable to keep track of the indentation level, and the indent()
function to add the right number of spaces at the beginning of each line. We do not need to add extra parenthesis to disambiguate priorities, as the Yul language is simple enough.
Here is the generated Coq code for the beginning of the erc20.sol example from the Solidity compiler's test suite:
(* Generated by solc *)
Require Import CoqOfSolidity.CoqOfSolidity.
Module ERC20_403.
Definition code : M.t BlockUnit.t :=
do* ltac:(M.monadic (
M.function (
"allocate_unbounded",
[],
["memPtr"],
do* ltac:(M.monadic (
M.assign (
["memPtr"],
Some (M.call (
"mload",
[
[Literal.number 64]
]
))
)
)) in
M.od
)
)) in
do* ltac:(M.monadic (
M.function (
"revert_error_ca66f745a3ce8ff40e2ccaf1ad45db7774001b90d25810abd9040049be7bf4bb",
[],
[],
do* ltac:(M.monadic (
M.expr_stmt (
M.call (
"revert",
[
[Literal.number 0];
[Literal.number 0]
]
)
)
)) in
M.od
)
)) in
(* ... 6,000 remaining lines ... *)
This code is quite verbose, for an original smart contract size of 100 lines of Solidity. As a reference, the corresponding Yul code is 1,000 lines long and starts with:
/// @usesrc 0:"erc20.sol"
object "ERC20_403" {
code {
function allocate_unbounded() > memPtr
{ memPtr := mload(64) }
function revert_error_ca66f745a3ce8ff40e2ccaf1ad45db7774001b90d25810abd9040049be7bf4bb()
{ revert(0, 0) }
// ... 1,000 remaining lines ...
The content is actually the same up to the notations, but we use many more line breaks and keywords in the Coq version.
Now that the code is translated in Coq, we need to define a runtime for the Coq code. This means giving a definition for all the functions and types that are used in the generated code, like M.t BlockUnit.t
, M.monadic
, M.function
, ... This runtime gives the semantics of the Yul language, that is to say, the meaning of all the primitives of the language.
We first define a monadic notation ltac:(M.monadic ...)
to make a monadic transformation on the generated code. We reuse here what we have done for our Rust translation to Coq, which we describe in our blog post ๐ฆ Monadic notation for the Rust translation. The notation:
f ( x_1, ..., x_n )
corresponds to the call of a monadic function. The tactic M.monadic
automatically chains all these calls using the monadic bind operator.
The do* ... in ...
is another monadic notation to chain monadic expressions, directly calling the monadic bind. This notation is more explicit, and we use it in combination with the ltac:(M.monadic ...)
notation as it might be more efficient to typecheck very large files.
To represent the side effects in Yul, we use the following Coq monad, that we define in CoqOfSolidity/CoqOfSolidity.v:
Module U256.
Definition t := Z.
End U256.
Module Environment.
Record t : Set := {
caller : U256.t;
(** Amount of wei sent to the current contract *)
callvalue : U256.t;
calldata : list Z;
(** The address of the contract. *)
address : U256.t;
}.
End Environment.
Module BlockUnit.
(** The return value of a code block. *)
Inductive t : Set :=
(** The default value in case of success *)
 Tt
(** The instruction `break` was called *)
 Break
(** The instruction `continue` was called *)
 Continue
(** The instruction `leave` was called *)
 Leave.
End BlockUnit.
Module Result.
(** A wrapper for the result of an expression or a code block. We can either return a normal value
with [Ok], or a special instruction [Return] that will stop the execution of the contract. *)
Inductive t (A : Set) : Set :=
 Ok (output : A)
 Return (p s : U256.t)
 Revert (p s : U256.t).
Arguments Ok {_}.
Arguments Return {_}.
Arguments Revert {_}.
End Result.
Module Primitive.
(** We group together primitives that share being impure functions operating over the state. *)
Inductive t : Set > Set :=
 OpenScope : t unit
 CloseScope : t unit
 GetVar (name : string) : t U256.t
 DeclareVars (names : list string) (values : list U256.t) : t unit
 AssignVars (names : list string) (values : list U256.t) : t unit
 MLoad (address length : U256.t) : t (list Z)
 MStore (address : U256.t) (bytes : list Z) : t unit
 SLoad (address : U256.t) : t U256.t
 SStore (address value : U256.t) : t unit
 RLoad : t (list Z)
 TLoad (address : U256.t) : t U256.t
 TStore (address value : U256.t) : t unit
 Log (topics : list U256.t) (payload : list Z) : t unit
 GetEnvironment : t Environment.t
 GetNonce : t U256.t
 GetCodedata (address : U256.t) : t (list Z)
 CreateAccount (address code : U256.t) (codedata : list Z) : t unit
 UpdateCodeForDeploy (address code : U256.t) : t unit
 LoadImmutable (name : U256.t) : t U256.t
 SetImmutable (name value : U256.t) : t unit
(** The call stack is there to debug the semantics of Yul. *)
 CallStackPush (name : string) (arguments : list (string * U256.t)) : t unit
 CallStackPop : t unit.
End Primitive.
Module LowM.
Inductive t (A : Set) : Set :=
 Pure (output : A)
 Primitive {B : Set}
(primitive : Primitive.t B)
(k : B > t A)
 DeclareFunction
(name : string)
(body : list U256.t > t (Result.t (list U256.t)))
(k : t A)
 CallFunction
(name : string)
(arguments : list U256.t)
(k : Result.t (list U256.t) > t A)
 Loop {B : Set}
(body : t B)
(** The final value to return if we decide to break of the loop. *)
(break_with : B > option B)
(k : B > t A)
 CallContract
(address : U256.t)
(value : U256.t)
(input : list Z)
(k : U256.t > t A)
(** Explicit cut in the monadic expressions, to provide better composition for the proofs. *)
 Let {B : Set} (e1 : t B) (k : B > t A)
 Impossible (message : string).
End LowM.
Module M.
Definition t (A : Set) := LowM.t (Result.t A).
The only type for values in Yul is the 256bit unsigned integer U256.t
that we represent with the Z
type of Coq. The BlockUnit.t
type represents the possible outcomes of a block of code:
Ok
for the normal ending;Break
or Continue
to propagate a premature return from a call to the break
or continue
primitives;Leave
to propagate the call to the leave
primitive to terminate a function.We define the monad in two steps. First, we define the LowM.t
monad parameterized by the type of output A
. The monad has the following constructors:
Pure
to return a value without side effects;Primitive
to execute one of the primitive, that are functions operating over the state (defined later);DeclareFunction
to declare a function with a name and a body, which is a function taking a list of arguments and returning a list of results, as this is the case in Yul;CallFunction
to call a function by its name with a list of arguments;Loop
to execute a block of code in a loop, with a function to decide if we should break the loop, helpful to implement the for
construct;CallContract
a dedicated primitive to implement the call
instruction of the EVM to call another contract located at a certain address;Let
to compose two monadic expressions in a more explicit way than using the continuations;Impossible
to signal an unexpected branch in the code.This monad is purely descriptive. We give the list of primitives but we do not explain here how each operator behaves. Most of the primitives take a continuation k
, which is a function from the output of the primitive to the rest of the code. This is a way to chain the primitives together. For convenience we define a monadic bind let_
that chains these continuations to chain two monadic expressions.
Then we define a monad M.t
as:
Module M.
Definition t (A : Set) := LowM.t (Result.t A).
to represent calculations that return a Result.t
to take into account that a contract might return or revert at any point in its execution.
Finally, we define the Yul keywords from these primitives. For example, for the if
keyword:
Definition if_ (condition : list U256.t) (success : t BlockUnit.t) : t BlockUnit.t :=
match condition with
 [0] => pure BlockUnit.Tt
 [_] => success
 _ => LowM.Impossible "if: expected a single value as condition"
end.
To define how to run the primitives of the Yul's monad, we use evaluation rules in CoqOfSolidity/simulations/CoqOfSolidity.v:
Module Run.
Reserved Notation "{{ environment , state  e โ output  state' }}"
(at level 70, no associativity).
Inductive t {A : Set} (environment : Environment.t) (state : State.t) (output : A) :
LowM.t A > State.t > Prop :=
 Pure : {{ environment, state  LowM.Pure output โ output  state }}
 Primitive {B : Set} (primitive : Primitive.t B) (k : B > LowM.t A) value state_inter state' :
inl (value, state_inter) = eval_primitive environment primitive state >
{{ environment, state_inter  k value โ output  state' }} >
{{ environment, state  LowM.Primitive primitive k โ output  state' }}
 DeclareFunction name body k stack_inter state' :
inl stack_inter = Stack.declare_function state.(State.stack) name body >
let state_inter := state < State.stack := stack_inter > in
{{ environment, state_inter  k โ output  state' }} >
{{ environment, state  LowM.DeclareFunction name body k โ output  state' }}
 CallFunction name arguments k results state_inter state' :
let function := Stack.get_function state.(State.stack) name in
{{ environment, state  function arguments โ results  state_inter }} >
{{ environment, state_inter  k results โ output  state' }} >
{{ environment, state  LowM.CallFunction name arguments k โ output  state' }}
 Let {B : Set} (e1 : LowM.t B) k state_inter output_inter state' :
{{ environment, state  e1 โ output_inter  state_inter }} >
{{ environment, state_inter  k output_inter โ output  state' }} >
{{ environment, state  LowM.Let e1 k โ output  state' }}
where "{{ environment , state  e โ output  state' }}" :=
(t environment state output e state').
End Run.
We use the notation:
{{ environment , state  e โ output  state' }}
to say that a certain monadic expression e
evaluates to the value output
, with the environment environment
, the initial state state
, and the final state state'
. We define the evaluation rules for each primitive of the monad.
We also define an evaluation function that will be useful in further tests to extract the Coq code back to OCaml and run tests to compare its behavior with the original Yul code. We define the evaluation function as follows:
(** A function to evaluate an expression given enough [fuel]. *)
Fixpoint eval {A : Set}
(fuel : nat)
(environment : Environment.t)
(e : LowM.t A) :
State.t > (A + string) * State.t :=
match fuel with
 O => fun state => (inr "out of fuel", state)
 S fuel =>
match e with
 LowM.Pure output => fun state => (inl output, state)
 LowM.Primitive primitive k =>
fun state =>
let value_state := eval_primitive environment primitive state in
match value_state with
 inl (value, state) => eval fuel environment (k value) state
 inr error => (inr error, state)
end
 LowM.DeclareFunction name body k =>
(* ... other cases ... *)
It uses a fuel
parameter to make sure that the evaluation terminates. For a monadic expression e
and an initial state and environment, it returns either the value of the expression or an error message, as well as a final state. The error might be due to an unexpected branch in the code, like a break
outside a loop, or to a lack of fuel. We plan to prove that it is equivalent to the evaluation rules defined above.
To test that our translation works, we ran it on all the Solidity files in the test suite of the Solidity compiler. There are, at the time of writing, 4856 .sol
example files in the semanticTests and syntaxTests folders. On each of them we run the Solidity compiler with the ircoq
flag to generate the Coq code. This works for most of the test files, although some of the test files have a special format that combine several Solidity files into one file that we do not handle yet. Then typecheck the generated code with Coq, what succeeds for all the Solidity files we translate.
A more complex check is to ensure that our semantics is correct, that is to say that when we run our eval
function in Coq on a smart contract, we get the same output as running this smart contract on an actual EVM once compiled with the Solidity compiler. We have a mechanism to extract the expected execution traces in the semantic tests to equivalent checks in Coq. We succeed in more than 90% of the test cases now. There are still a few builtin functions that we need to implement, like precompiled contracts.
There are already a few formal verification tools for Solidity, as smart contracts are an important kind of program to check. A few of them, like the Certora Prover, are closed source. Most work at the EVM bytecode level, as the semantics of the EVM is simpler than the semantics of Solidity. A disadvantage of working at the EVM level is that this is a lowlevel language, so the code is hard to understand (explicit stack manipulations, ...). This is the reason why we believe this approach is mostly used with automated verification tools.
It is hard to have a rather complete support for the Solidity language, despite of many attempts including one of ours. We can cite the Verisol project from Microsoft to verify Solidity programs.
The Yul language offers a good compromise between the highlevel Solidity language and the lowlevel EVM bytecode. It was actually designed with formal verification in mind, according to its documentation. These notes from Franck Cassez give a good overview of the formal verification efforts for Yul. One of the conclusions is that a lot of the existing work is either incomplete/unmaintained or not designed for the formal verification of smart contracts, but rather to verify the Yul language itself. As a result, they propose a formal verification framework for Yul in Dafny with yuldafny.
If you have smart contract projects that you want to formally verify, going further than a manual audit to find bugs, contact us at contact@formal.land! Formal verification has the strong advantage of covering all possible execution cases.
We have presented our ongoing development of a formal verification tool for Solidity using the Coq proof assistant. We have briefly shown how we translate Solidity code to Coq using the Yul intermediate language and how we define the semantics of Yul in Coq. We have tested our tool on the examples of the Solidity compiler's test suite to check that our formalization is correct.
Our next steps will be to:
We will present this idea in detail and illustrate how it works for a very simple example.
We typically use formal verification for critical applications, where either:
With formal verification, in theory, we can guarantee that the software will never fail, as we can check all possible cases for a given property. A property can be that no nonadmin users can read sensitive data, or that a program never fails with uncaught exceptions. For that to be truly the case, we need to verify the whole software stack for all the relevant properties.
In this research paper Finding and Understanding Bugs in C Compilers, no bugs were found in the middleend of the formally verified CompCert C compiler, while the other C compilers (GCC, LLVM, ...) all contained subtle bugs. This illustrates that formal verification can be an effective way to make complex software with zero bugs!
To be able to reason on a program we go back to the definition of programming languages. The programming languages (C, JavaScript, Python, ...) are generally defined with a precise set of rules. For example, in Python, the if
statement is defined in the reference manual by:
if_stmt ::= "if" assignment_expression ":" suite
("elif" assignment_expression ":" suite)*
["else" ":" suite]
It selects exactly one of the suites by evaluating the expressions one by one until one is found to be true (see section Boolean operations for the definition of true and false); then that suite is executed (and no other part of the if statement is executed or evaluated). If all expressions are false, the suite of the else clause, if present, is executed.
โ The Python's reference manual
This means that the Python code:
if condition:
a
else:
b
will execute a
when the condition
is true, and b
otherwise. There are similar rules for all other program constructs (loops, function definitions, classes, ...).
To make these rules more manageable, we generally split them into two parts:
if
statement.In formal verification, we will focus on the semantics of programs, assuming that the syntax is already verified by the compiler or interpreter, generating "syntax errors" in case of illformed programs.
We consider this short Python example of a function returning the maximum number in a list:
def my_max(l):
m = l[0]
for x in l:
if x > m:
m = x
return m
We assume that the list l
is not empty and only contains integers. If we run it on a few examples:
my_max([1, 2, 3]) # => 3
my_max([3, 2, 1]) # => 3
my_max([1, 3, 2]) # => 3
it always returns 3
, the biggest number in the list! But can we make sure this is always the case?
We can certainly not run my_max
on all possible lists of integers, as there are infinitely many of them. We need to reason from the definition of the Python language, which is what we call formal verification reasoning.
Here is a general specification that we give of the my_max
function above:
forall (index : int) (l : list[int]),
0 โค index < len(l) โ
l[index] โค my_max(l)
It says that for all integer index
and list of integers l
, if the index is valid (between 0
and the length of the list), then the element at this index is less than or equal to the maximum of the list that we compute.
To verify this property for all possible list l
, we reason by induction. A nonempty list is either:
At the start of the code, we will always have:
def my_max(l):
m = l[0]
with m
being equal to the first item of the list. Then:
for
loop, with x
equal to l[0]
. The condition:
if x > m:
if l[0] > l[0]:
m = l[0]
, which is the only element of the list, and it verifies our property as:
l[0] โค l[0]
for
loop and iterate over all the elements until the last one. Our induction hypothesis tells us that the property we verify is true for the first part of the list, excluding the last element. This means that:
l[index] โค m
index
between 0
and len(l)  2
. When we reach the last element, we have:
if x > m:
m = x
x
being l[len(l)  1]
. There are two possibilities. Either (i) x
is less than or equal to m
, and we do not update m
, or (ii) x
is greater than m
, and we update m
to x
. In both cases, the property is verified for the last element of the list, as:
m
stays the same, so it is still larger or equal to all the elements of the list except the last one, as well as larger or equal to the last one according to this last if
statement.m
is updated to x
, which is the last element of the list and a greater value than the original m
. Then it means that m
is still larger or equal to all the elements of the list except the last one, being larger that the original m
, and larger or equal to the last one as it is in fact equals to the last one.We have now closed our induction proof and verified that our property is true for all possible lists of integers! The reasoning above is rather verbose but should actually correspond to the intuition of most programmers when reading this code.
In practice, with formal verification, the reasoning above is done in a proof assistance such as Coq to help making sure that we did not forget any case, and automatically solve simple cases for us. Having a proof written in a proof language like Coq also allows us to rerun it to check that it is still valid after a change in the code, and allows thirdparty persons to check it without reading all the details.
An additional property that we did not verify is:
forall (l : list[int]),
exists (index : int),
0 โค index < len(l) and
l[index] = my_max(l)
It says that the maximum of the list is actually in the list. We can verify it by induction in the same way as we did for the first property. You can detail this verification as an exercise.
If you want to go into more details for the formal verification of Python programs, you can look at our coqofpython project, where we define the semantics of Python in Coq and verify properties of Python programs (ongoing project!). We also provide formal verification services for Rust and other languages like OCaml. Contact us at contact@formal.land to discuss if you have critical applications to check!
We have presented here the idea of formal verification, a technique to verify the absence of bugs in a program by reasoning from first principles. We have illustrated this idea for a simple Python example, showing how we can verify that a function computing the maximum of a list is correct for all possible lists of integers.
We will continue with more blog posts explaining what we can do with formal verification and why it matters. Feel free to share this post and to tell us what subjects you want to see covered!
]]>We will show in this article how we can merge the steps 2. and 3. to save time in the verification process. We do so by relying on the proof mode of Coq and unification.
Our midterm goal is to formally specify the Ethereum Virtual Machine (EVM) and prove that this specification is correct according to reference implementation of the EVM in Python. This would ensure that it is always uptodate and exhaustive. The code of this project is opensource and available on GitHub: formalland/coqofpython.
We put the Python code that we import in Coq in a monad M
to represent all the features that are hard to express in Coq, mainly the side effects. This monad is a combination of two levels:
LowM
for the side effects except the control flow.M
that adds an error monad on top of LowM
to handle the control flow (exceptions, break
instruction, ...).Here is the definition of the LowM
monad in CoqOfPython.v:
Module Primitive.
Inductive t : Set > Set :=
 StateAlloc (object : Object.t Value.t) : t (Pointer.t Value.t)
 StateRead (mutable : Pointer.Mutable.t Value.t) : t (Object.t Value.t)
 StateWrite (mutable : Pointer.Mutable.t Value.t) (update : Object.t Value.t) : t unit
 GetInGlobals (globals : Globals.t) (name : string) : t Value.t.
End Primitive.
Module LowM.
Inductive t (A : Set) : Set :=
 Pure (a : A)
 CallPrimitive {B : Set} (primitive : Primitive.t B) (k : B > t A)
 CallClosure {B : Set} (closure : Data.t Value.t) (args kwargs : Value.t) (k : B > t A)
 Impossible.
Arguments Pure {_}.
Arguments CallPrimitive {_ _}.
Arguments CallClosure {_ _}.
Arguments Impossible {_}.
Fixpoint bind {A B : Set} (e1 : t A) (e2 : A > t B) : t B :=
match e1 with
 Pure a => e2 a
 CallPrimitive primitive k => CallPrimitive primitive (fun v => bind (k v) e2)
 CallClosure closure args kwargs k => CallClosure closure args kwargs (fun a => bind (k a) e2)
 Impossible => Impossible
end.
End LowM.
This is a monad defined by continuation (the variable k
):
Pure
and some result a
, that can be any purely functional expression.Primitive.t
that are side effects:
StateAlloc
to allocate a new object in the memory,StateRead
to read an object from the memory,StateWrite
to write an object in the memory,GetInGlobals
to read a global variable, doing name resolution. This is a side effects as function definitions in Python do not need to be ordered.CallClosure
. This is required for termination, as we cannot define an eval function on the type of Python values since some do not terminate like the ฮฉ expression. See our previous post Translation of Python code to Coq for our definition of Python values. The combinator CallClosure
is also very convenient to modularize our proofs: we reason on each closure independently.Impossible
.The final monad M
is defined as:
Definition M : Set :=
LowM.t (Value.t + Exception.t).
It has no parameters as Python is untyped, so all expressions have the same result type:
Value.t
,Exception.t
, with some special cases to represent a return
, a break
, or a continue
instruction.We define the monadic bind of M
like for the error monad:
Definition bind (e1 : M) (e2 : Value.t > M) : M :=
LowM.bind e1 (fun v => match v with
 inl v => e2 v
 inr e => LowM.Pure (inr e)
end).
We define our semantics of a computation e
of type M
in simulations/proofs/CoqOfPython.v with the predicate:
{{ stack, heap  e โ to_value  P_stack, P_heap }}
that we call a run or a trace, saying that:
stack
, heap
,e
terminates with a value,to_value
,P_stack
and P_heap
.Note that we do not explicit the resulting value and memory state of a computation in this predicate. We only say that it exists and verifies a few properties, that are here for compositionality. We have a purely functional function evaluate
that can derive the result of a run of a computation:
evaluate :
forall `{Heap.Trait} {A B : Set}
{stack : Stack.t} {heap : Heap} {e : LowM.t B}
{to_value : A > B} {P_stack : Stack.t > Prop} {P_heap : Heap > Prop}
(run : {{ stack, heap  e โ to_value  P_stack, P_heap }}),
A * { stack : Stack.t  P_stack stack } * { heap : Heap  P_heap heap }
The function evaluate
is defined in Coq by a Fixpoint
. Its result is what we call a simulation, which is a purely functional definition equivalent to the orignal computation e
from Python. It is equivalent by construction.
A trace is an inductive in Set
that we can build with the following constructors:
Inductive t `{Heap.Trait} {A B : Set}
(stack : Stack.t) (heap : Heap)
(to_value : A > B) (P_stack : Stack.t > Prop) (P_heap : Heap > Prop) :
LowM.t B > Set :=
(* [Pure] primitive *)
 Pure
(result : A)
(result' : B) :
result' = to_value result >
P_stack stack >
P_heap heap >
{{ stack, heap 
LowM.Pure result' โ
to_value
 P_stack, P_heap }}
(* [StateRead] primitive *)
 CallPrimitiveStateRead
(mutable : Pointer.Mutable.t Value.t)
(object : Object.t Value.t)
(k : Object.t Value.t > LowM.t B) :
IsRead.t stack heap mutable object >
{{ stack, heap 
k object โ
to_value
 P_stack, P_heap }} >
{{ stack, heap 
LowM.CallPrimitive (Primitive.StateRead mutable) k โ
to_value
 P_stack, P_heap }}
(* [CallClosure] primitive *)
 CallClosure {C : Set}
(f : Value.t > Value.t > M)
(args kwargs : Value.t)
(to_value_inter : C > Value.t + Exception.t)
(P_stack_inter : Stack.t > Prop) (P_heap_inter : Heap > Prop)
(k : Value.t + Exception.t > LowM.t B) :
let closure := Data.Closure f in
{{ stack, heap 
f args kwargs โ
to_value_inter
 P_stack_inter, P_heap_inter }} >
(* We quantify over every possible values as we cannot compute the result of the closure here.
We only know that it exists and respects some constraints in this inductive definition. *)
(forall value_inter stack_inter heap_inter,
P_stack_inter stack_inter >
P_heap_inter heap_inter >
{{ stack_inter, heap_inter 
k (to_value_inter value_inter) โ
to_value
 P_stack, P_heap }}
) >
{{ stack, heap 
LowM.CallClosure closure args kwargs k โ
to_value
 P_stack, P_heap }}
(* ...cases for the other primitives of the monad... *)
In the Pure
case we return the final result of the computation. We check the state fulfills the predicate P_stack
and P_heap
, and that the result is the image by the function to_value
of some result
.
To read a value in memory, we rely on another predicate IsRead
that checks if the mutable
pointer is valid in the stack
or heap
and that the object
is the value at this pointer. We then call the continuation k
with this object. We have similar rules for allocating a new object in memory and writing at a pointer.
Note that we parameterize all our semantics by `{Heap.Trait}
that provides a specific Heap
type with read and write primitives. We can choose the implementation of the memory model that we want to use in our simulations in order to simplify the reasoning.
To call a closure, we first evaluate the closure with the arguments and keyword arguments. We then call the continuation k
with the result of the closure. We quantify over all possible results of the closure, as we cannot compute it here. This would require to be able to define Fixpoint
together with Inductive
, which is not possible in Coq. So we only know that the result of the closure exists, and can use the constraints on its result (the function to_value
and the predicates P_stack_inter
and P_heap_inter
) to build a run of the continuation.
The other constructors are not presented here but are similar to the above. We will also add a monadic primitive for loops with the following idea: we show that a loop terminates by building a trace, as traces are Inductive
so must be finite. We have no rules for the Impossible
case so that building the trace of a computation also shows that the Impossible
calls are in unreachable paths.
We have applied these technique to a small code example with allocation, memory read, and closure call primitives. We were able to show that the resulting simulation obtained by running evaluate
on the trace is equal to a simulation written by hand. The proof was just the tactic reflexivity
. We believe that we can automate most of the tactics used to build a run, except for the allocations were the user needs to make a choice (immediate, stack, or heap allocation, which address, ...).
To continue our experiments we now need to complete our semantics of Python, especially to take into account method and operator calls.
We have presented an alternative way to build simulations of imperative Python code in purely functional Coq code. The idea is to enable faster reasoning over Python code by removing the need to build explicit simulations. We plan to port this technique to other tools like coqofrust as well.
To see what we can do for you talk with us at contact@formal.land ๐. For our previous projects, see our formal verification of the Tezos' L1!
]]>In this article, we will see how we specify the EVM in Coq by writing an interpreter that closely mimics the behavior of the Python code. We call that implementation a simulation as it aims to reproduce the behavior of the Python code, the reference.
In contrast to the automatic translation from Python, the simulation is a manual translation written in idiomatic Coq. We expect it to be ten times smaller in lines compared to the automatic translation, and of about the same size as the Python code. This is because the automatic translation needs to encode all the Python specific features in Coq, like variable mutations and the class system.
In the following article, we will show how we can prove that the simulation is correct, meaning that it behaves exactly as the automatic translation.
The code of this project is opensource and available on GitHub: formalland/coqofpython. This work follows a call from Vitalik Buterin for more formal verification of the Ethereum's code.
add
function ๐งฎโWe focus on a simulation for the add
function in vm/instructions/arithmetic.py that implements the addition primitive of the EVM. The Python code is:
def add(evm: Evm) > None:
"""
Adds the top two elements of the stack together, and pushes the result back
on the stack.
Parameters

evm :
The current EVM frame.
"""
# STACK
x = pop(evm.stack)
y = pop(evm.stack)
# GAS
charge_gas(evm, GAS_VERY_LOW)
# OPERATION
result = x.wrapping_add(y)
push(evm.stack, result)
# PROGRAM COUNTER
evm.pc += 1
Most of the functions of the interpreter are written in this style. They take the global state of the interpreter, called Evm
as input, and mutate it with the effect of the current instruction.
The Evm
structure is defined as:
@dataclass
class Evm:
"""The internal state of the virtual machine."""
pc: Uint
stack: List[U256]
memory: bytearray
code: Bytes
gas_left: Uint
env: Environment
valid_jump_destinations: Set[Uint]
logs: Tuple[Log, ...]
refund_counter: int
running: bool
message: Message
output: Bytes
accounts_to_delete: Set[Address]
touched_accounts: Set[Address]
return_data: Bytes
error: Optional[Exception]
accessed_addresses: Set[Address]
accessed_storage_keys: Set[Tuple[Address, Bytes32]]
It contains the current instruction pointer pc
, the stack of the EVM, the memory, the code, the gas left, ...
As the EVM is a stackbased machine, the addition function does the following:
x
and y
,result = x + y
,pc
.Note that all these operations might fail and raise an exception, for example,if the stack is empty when we pop x
and y
at the beginning.
The main sideeffects that we want to integrate into the Coq simulations are:
Evm
,For that, we use a state and error monad MS?
:
Module StateError.
Definition t (State Error A : Set) : Set :=
State > (A + Error) * State.
Definition return_ {State Error A : Set}
(value : A) :
t State Error A :=
fun state => (inl value, state).
Definition bind {State Error A B : Set}
(value : t State Error A)
(f : A > t State Error B) :
t State Error B :=
fun state =>
let (value, state) := value state in
match value with
 inl value => f value state
 inr error => (inr error, state)
end.
End StateError.
Notation "MS?" := StateError.t.
We parametrize it by an equivalent definition in Coq of the type Evm
and the type of exceptions that we might raise.
In Python the exceptions are a class that is extended as needed to add new kinds of exceptions. We use a closed sum type in Coq to represent the all possible exceptions that might happen in the EVM interpreter.
For the Evm
state, some functions might actually only modify a part of it. For example, the pop
function only modifies the stack
field. We use a mechanism of lens to specialize the state monad to only modify a part of the state. For example, the pop
function has the type:
pop : MS? (list U256.t) Exception.t U256.t
where list U256.t
is the type of the stack, while the add
function has type:
add : MS? Evm.t Exception.t unit
We define a lens for the stack in the Evm
type with:
Module Lens.
Record t (Big_A A : Set) : Set := {
read : Big_A > A;
write : Big_A > A > Big_A
}.
End Lens.
Module Evm.
Module Lens.
Definition stack : Lens.t Evm.t (list U256.t) := {
Lens.read := (* ... *);
Lens.write := (* ... *);
}.
We can then lift the pop
function to be used in a context where the Evm
state is modified with:
letS? x := StateError.lift_lens Evm.Lens.stack pop in
We keep in Coq all the type names from the Python source code. When a new class is created we create a new Coq type. When the class inherits from another one, we add a field in the Coq type to represent the parent class. Thus we work by composition rather than inheritance.
Here is an example of the primitive types defined in base_types.py:
class FixedUint(int):
MAX_VALUE: ClassVar["FixedUint"]
# ...
def __add__(self: T, right: int) > T:
# ...
class U256(FixedUint):
MAX_VALUE = 2**256  1
# ...
We simulate it by:
Module FixedUint.
Record t : Set := {
MAX_VALUE : Z;
value : Z;
}.
Definition __add__ (self right_ : t) : M? Exception.t t :=
(* ... *).
End FixedUint.
Module U256.
Inductive t : Set :=
 Make (value : FixedUint.t).
Definition of_Z (value : Z) : t :=
Make {
FixedUint.MAX_VALUE := 2^256  1;
FixedUint.value := value;
}.
(* ... *)
End U256.
For the imports, that are generally written with an explicit list of names:
from ethereum.base_types import U255_CEIL_VALUE, U256, U256_CEIL_VALUE, Uint
we follow the same pattern in Coq:
Require ethereum.simulations.base_types.
Definition U255_CEIL_VALUE := base_types.U255_CEIL_VALUE.
Module U256 := base_types.U256.
Definition U256_CEIL_VALUE := base_types.U256_CEIL_VALUE.
Module Uint := base_types.Uint.
This is a bit more verbose than the usual way in Coq to import a module, but it makes the translation more straightforward.
Finally, our Coq simulation of the add
function is the following:
Definition add : MS? Evm.t Exception.t unit :=
(* STACK *)
letS? x := StateError.lift_lens Evm.Lens.stack pop in
letS? y := StateError.lift_lens Evm.Lens.stack pop in
(* GAS *)
letS? _ := charge_gas GAS_VERY_LOW in
(* OPERATION *)
let result := U256.wrapping_add x y in
letS? _ := StateError.lift_lens Evm.Lens.stack (push result) in
(* PROGRAM COUNTER *)
letS? _ := StateError.lift_lens Evm.Lens.pc (fun pc =>
(inl tt, Uint.__add__ pc (Uint.Make 1))) in
returnS? tt.
We believe that it has a size and readability close to the original Python code. You can look at this definition in vm/instructions/simulations/arithmetic.v. As a reference, the automatic translation is 65 lines long and in vm/instructions/arithmetic.v.
We have seen how to write a simulation for one example of a Python function. We now need to do it for the rest of the code of the interpreter. We will also see in a following article how to prove that the simulation behaves as the automatic translation of the Python code in Coq.
For our formal verification services, reach us at contact@formal.land ๐! To know more about what we have done, see our previous project on the verification of the L1 of Tezos.
]]>We want to import specifications written in Python to a formal system like Coq. In particular, we are interested in the reference specification of Ethereum, which describes how EVM smart contracts run. Then, we will be able to use this specification to either formally verify the various implementations of the EVM or smart contracts.
All this effort follows a Tweet from Vitalik Buterin hoping for more formal verification of the Ethereum's code:
One application of AI that I am excited about is AIassisted formal verification of code and bug finding.
Right now ethereum's biggest technical risk probably is bugs in code, and anything that could significantly change the game on that would be amazing.
โ Vitalik Buterin
We will now describe the technical development of coqofpython
. For the curious, all the code is on GitHub: formalland/coqofpython.
A first step we need to do to translate Python code is to read it in a programmatic way. For simplicity and better integration, we chose to write coqofpython
in Python.
We use the ast module to parse the code and get an abstract syntax tree (AST) of the code. This is a tree representation of the code that we can manipulate in Python. We could have used other representations, such as the Python bytecode, but it seemed too lowlevel to be understandable by a human.
Given the path to a Python file, we get its AST with the following code:
import ast
def read_python_file(path: str) > ast.Module:
with open(path, "r") as file:
return ast.parse(file.read())
This code is very short, and we benefit from the general elegance of Python. There is no typing or advanced data types in Python, keeping the AST rather small. Here is an extract of it:
expr = BoolOp(boolop op, expr* values)
 NamedExpr(expr target, expr value)
 BinOp(expr left, operator op, expr right)
 UnaryOp(unaryop op, expr operand)
 Lambda(arguments args, expr body)
 IfExp(expr test, expr body, expr orelse)
 Dict(expr* keys, expr* values)
 Set(expr* elts)
 ListComp(expr elt, comprehension* generators)
 SetComp(expr elt, comprehension* generators)
 ... more cases ...
An expression is described as being of one of several kinds. For example, the application of a binary operator such as:
1 + 2
corresponds to the case BinOp
with 1
as the left
expression, +
as the op
operator, and 2
as the right
expression.
We translate each element of the Python's AST into a string of Coq code. We keep track of the current indentation level in order to present a nice output. Here is the code to translate the binary operator expressions:
def generate_expr(indent, is_with_paren, node: ast.expr):
if isinstance(node, ast.BoolOp):
...
elif isinstance(node, ast.BinOp):
return paren(
is_with_paren,
generate_operator(node.op) + " (\n" +
generate_indent(indent + 1) +
generate_expr(indent + 1, False, node.left) + ",\n" +
generate_indent(indent + 1) +
generate_expr(indent + 1, False, node.right) + "\n" +
generate_indent(indent) + ")"
)
elif ...
We have the current number of indentation levels in the indent
variable. We use the flag is_with_paren
to know whether we should add parenthesis around the current expression if it is the subexpression of another one.
We apply the node.op
operator on the two parameters node.left
and node.right
. For example, the translation of the Python code 1 + 2
will be:
BinOp.add (
Constant.int 1,
Constant.int 2
)
We use a special notation f ( x1, ..., xn )
to represent a function application in a monadic context. In the next section, we explain why we need this notation.
One of the difficulties in translating some code to a language such as Coq is that Coq is purely functional. This means that a function can never modify a variable or raise an exception. The nonpurely functional actions are called sideeffects.
To solve this issue, we represent the sideeffects of the Python code in a monad in Coq. A monad is a special data structure representing the sideeffects of a computation. We can chain monadic actions together to represent a sequence of sideeffects.
We thus have two Coq types:
Value.t
for the Python values (there is only one type for all values, as Python is a dynamically typed language),M
for the monadic expressions.Note that we do not need to parametrize the monad by the type of the values, as we only have one type of value.
According to the reference manual of Python on the data model:
All data in a Python program is represented by objects or by relations between objects.
Every object has an identity, a type and a value. An objectโs identity never changes once it has been created; you may think of it as the objectโs address in memory.
Like its identity, an objectโs type is also unchangeable.
The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable.
By following this description, we propose this formalization for the values:
Module Data.
Inductive t (Value : Set) : Set :=
 Ellipsis
 Bool (b : bool)
 Integer (z : Z)
 Tuple (items : list Value)
(* ... various other primitive types like lists, ... *)
 Closure {Value M : Set} (f : Value > Value > M)
 Klass {Value M : Set}
(bases : list (string * string))
(class_methods : list (string * (Value > Value > M)))
(methods : list (string * (Value > Value > M))).
End Data.
Module Object.
Record t {Value : Set} : Set := {
internal : option (Data.t Value);
fields : list (string * Value);
}.
End Object.
Module Pointer.
Inductive t (Value : Set) : Set :=
 Imm (data : Object.t Value)
 Mutable {Address A : Set}
(address : Address)
(to_object : A > Object.t Value).
End Pointer.
Module Value.
Inductive t : Set :=
 Make (globals : string) (klass : string) (value : Pointer.t t).
End Value.
We describe a Value.t
by:
klass
and a module name globals
from which the class is defined,A Pointer.t
is either an immutable object Imm
or a mutable object Mutable
with an address and a function to get the object from what is stored in the memory. This function to_object
is required as we plan to allow the user to provide its own custom memory model.
An Object.t
has a list of named fields that we can populate in the __init__
method of a class. It also has a special internal
field that we can use to store special kinds of data, like primitive values.
In Data.t
, we list the various primitive values that we use to define the primitive types of the Python language. We have:
*args
and **kwargs
and returns a monadic value,For now, we axiomatize the monad M
:
Parameter M : Set.
We will see later how to define it, probably by taking some inspiration from our monad from our similar project coqofrust.
To make the monadic code less heavy, we use a notation inspired by the async/await
notation of many languages. We believe it to be less heavy than the monadic notation of languages like Haskell. We note:
f ( x1, ..., xn )
to call a function f
of type:
Value.t > ... > Value.t > M
with the arguments x1
, ..., xn
of type Value.t
and binds its result to the current continuation in the context of the tactic ltac:(M.monadic ...)
. See our blog post Monadic notation for the Rust translation for more information.
In summary:
f ( x1, ..., xn )
is like await
,ltac:(M.monadic ...)
is like async
.Now we talk about how we handle the variable names and link them to their definitions. In the reference manual of Python, the part Execution model gives some information.
For now, we distinguish between two scopes, the global one (toplevel definitions) and the local one for variables defined in a function. We might introduce a stack of local scopes to handle nested functions.
We name the global scope with a string, that is the path of the current file. Having absolute names helps us translating each file independently. The only file that a translated file requires is CoqOfPython.CoqOfPython
, to have the definition of the values and the monad.
To translate import
statements, we use assertions:
Axiom ethereum_crypto_imports_elliptic_curve :
IsImported globals "ethereum.crypto" "elliptic_curve".
Axiom ethereum_crypto_imports_finite_field :
IsImported globals "ethereum.crypto" "finite_field".
This represents:
from . import elliptic_curve, finite_field
It means that in the current global scope globals
we can use the name "elliptic_curve"
from the other global scope "ethereum.crypto"
.
We set the local scope at the entry of a function with the call:
M.set_locals ( args, kwargs, [ "x1"; ...; "xn" ] )
for a function whose parameter names are x1
, ..., xn
. For uniformity, we always group the function's parameters as *args
and **kwargs
. We do not yet handle the default values.
When a user creates or updates a local variable x
with a value value
, we run:
M.assign_local "x" value : M
To read a variable, we have a primitive:
M.get_name : string > string > M
It takes as a parameter the name of the current global scope and the name of the variable the are reading. The local scope should be accessible from the monad. For now all these primitives are axiomatized.
The code base that we analyze, the Python specification of Ethereum, contains 28,455 lines of Python, excluding comments. When we translate it to Coq we obtain 299,484 lines. This is a roughly ten times increase.
The generated code completely compiles. For now, we avoid some complex Python expressions, like list comprehension, by generating a dummy expression instead. Having all the code that compiles will allow us to iterate and add support for more Python features with a simple check: making sure that all the code still compiles.
As an example, we translate the following function:
def bnf2_to_bnf12(x: BNF2) > BNF12:
"""
Lift a field element in `BNF2` to `BNF12`.
"""
return BNF12.from_int(x[0]) + BNF12.from_int(x[1]) * (
BNF12.i_plus_9  BNF12.from_int(9)
)
to the Coq code:
Definition bnf2_to_bnf12 : Value.t > Value.t > M :=
fun (args kwargs : Value.t) => ltac:(M.monadic (
let _ := M.set_locals ( args, kwargs, [ "x" ] ) in
let _ := Constant.str "
Lift a field element in `BNF2` to `BNF12`.
" in
let _ := M.return_ (
BinOp.add (
M.call (
M.get_field ( M.get_name ( globals, "BNF12" ), "from_int" ),
make_list [
M.get_subscript (
M.get_name ( globals, "x" ),
Constant.int 0
)
],
make_dict []
),
BinOp.mult (
M.call (
M.get_field ( M.get_name ( globals, "BNF12" ), "from_int" ),
make_list [
M.get_subscript (
M.get_name ( globals, "x" ),
Constant.int 1
)
],
make_dict []
),
BinOp.sub (
M.get_field ( M.get_name ( globals, "BNF12" ), "i_plus_9" ),
M.call (
M.get_field ( M.get_name ( globals, "BNF12" ), "from_int" ),
make_list [
Constant.int 9
],
make_dict []
)
)
)
)
) in
M.pure Constant.None_)).
We continue working on the translation from Python to Coq, especially to now add a semantics to the translation. Our next goal is to have a version, written in idiomatic Coq, of the file src/ethereum/paris/vm/instructions/arithmetic.py, and proven equal to the original code. This will open the door to making a Coq specification of the EVM that is always synchronized to the Python's version.
For our services, reach us at contact@formal.land ๐! We want to ensure the blockchain's L1 and L2 are bugfree, thanks to a mathematical analysis of the code. See our previous project on the L1 of Tezos.
]]>To solve this issue, we worked on the translation of the core and alloc crates of Rust using coqofrust
. These are very large code bases, with a lot of unsafe or advanced Rust code. We present what we did to have a "best effort" translation of these crates. The resulting translation is in the following folders:
This work is funded by the Aleph Zero cryptocurrency to verify their Rust smart contracts. You can follow us on X to get our updates. We propose tools and services to make your codebase bugfree with formal verification.
Contact us at contact@formal.land to chat โ๏ธ!
An initial run of coqofrust
on the alloc
and core
crates of Rust generated us two files of a few hundred thousands lines of Coq corresponding to the whole translation of these crates. This is a first good news, as it means the tool runs of these large code bases. However the generated Coq code does not compile, even if the errors are very rare (one every few thousands lines).
To get an idea, here is the size of the input Rust code as given by the cloc
command:
alloc
: 26,299 lines of Rust codecore
: 54,192 lines of Rust codeGiven that this code uses macros that we expand in our translation, the actual size that we have to translate is even bigger.
The main change we made was to split the output generated by coqofrust
with one file for each input Rust file. This is possible because our translation is insensitive to the order of definitions and contextfree. So, even if there are typically cyclic dependencies between the files in Rust, something that is forbidden in Coq, we can still split them.
We get the following sizes as output:
alloc
: 54 Coq files, 171,783 lines of Coq codecore
: 190 Coq files, 592,065 lines of Coq codeThe advantages of having the code split are:
We had some bugs related to the collisions between module names. These can occur when we choose a name for the module for an impl
block. We fixed these by adding more information in the module names to make them more unique, like the where
clauses that were missing. For example, for the implementation of the Default
trait for the Mapping
type:
#[derive(Default)]
struct Mapping<K, V> {
// ...
}
we were generating the following Coq code:
Module Impl_core_default_Default_for_dns_Mapping_K_V.
(* ...trait implementation ... *)
End Impl_core_default_Default_for_dns_Mapping_K_V.
We now generate:
Module Impl_core_default_Default_where_core_default_Default_K_where_core_default_Default_V_for_dns_Mapping_K_V.
(* ... *)
with a module name that includes the where
clauses of the impl
block, stating that both K
and V
should implement the Default
trait.
Here is the list of files that do not compile in Coq, as of today:
alloc/boxed.v
core/any.v
core/array/mod.v
core/cmp/bytewise.v
core/error.v
core/escape.v
core/iter/adapters/flatten.v
core/net/ip_addr.v
This represents 4% of the files. Note that in the files that compile there are some unhandled Rust constructs that are axiomatized, so this does not give the whole picture of what we do not support.
Here is the source code of the unwrap_or_default
method for the Option
type:
pub fn unwrap_or_default(self) > T
where
T: Default,
{
match self {
Some(x) => x,
None => T::default(),
}
}
We translate it to:
Definition unwrap_or_default (T : Ty.t) (ฯ : list Ty.t) (ฮฑ : list Value.t) : M :=
let Self : Ty.t := Self T in
match ฯ, ฮฑ with
 [], [ self ] =>
ltac:(M.monadic
(let self := M.alloc ( self ) in
M.read (
M.match_operator (
self,
[
fun ฮณ =>
ltac:(M.monadic
(let ฮณ0_0 :=
M.get_struct_tuple_field_or_break_match (
ฮณ,
"core::option::Option::Some",
0
) in
let x := M.copy ( ฮณ0_0 ) in
x));
fun ฮณ =>
ltac:(M.monadic
(M.alloc (
M.call_closure (
M.get_trait_method ( "core::default::Default", T, [], "default", [] ),
[]
)
)))
]
)
)))
 _, _ => M.impossible
end.
We prove that it is equivalent to the simpler functional code:
Definition unwrap_or_default {T : Set}
{_ : core.simulations.default.Default.Trait T}
(self : Self T) :
T :=
match self with
 None => core.simulations.default.Default.default (Self := T)
 Some x => x
end.
This simpler definition is what we use when verifying code. The proof of equivalence is in CoqOfRust/core/proofs/option.v. In case the original source code changes, we are sure to capture these changes thanks to our proof. Because the translation of the core
library was done automatically, we trust the generated definitions more than definitions that would be done by hand. However, there can still be mistakes or incompleteness in coqofrust
, so we still need to check at proof time that the code makes sense.
We can now work on the verification of Rust programs with more trust in our formalization of the standard library. Our next target is to simplify our proof process, which is still tedious. In particular, showing that simulations are equivalent to the original Rust code requires doing the name resolution, introduction of highlevel types, and removal of the sideeffects. We would like to split these steps.
If you are interested in formally verifying your Rust projects, do not hesitate to get in touch with us at contact@formal.land ๐! Formal verification provides the highest level of safety for critical applications, with a mathematical guarantee of the absence of bugs for a given specification.
]]>One of the challenges of our translation from Rust to Coq is that the generated code is very verbose. The size increase is about ten folds in our examples. A reasons is that we use a monad to represent side effects in Coq, so we need to name each intermediate result and apply the bind
operator. Here, we will present a monadic notation that prevents naming intermediate results to make the code more readable.
This work is funded by the Aleph Zero cryptocurrency to verify their Rust smart contracts. You can follow us on X to get our updates. We propose tools and services to make your codebase bugfree with formal verification.
Contact us at contact@formal.land to chat โ๏ธ!
Here is the Rust source code that we consider:
fn add(a: i32, b: i32) > i32 {
a + b
}
Before, we were generating the following Coq code, with let*
as the notation for the bind:
Definition add (ฯ : list Ty.t) (ฮฑ : list Value.t) : M :=
match ฯ, ฮฑ with
 [], [ a; b ] =>
let* a := M.alloc a in
let* b := M.alloc b in
let* ฮฑ0 := M.read a in
let* ฮฑ1 := M.read b in
BinOp.Panic.add ฮฑ0 ฮฑ1
 _, _ => M.impossible
end.
Now, with the new monadic notation, we generate:
Definition add (ฯ : list Ty.t) (ฮฑ : list Value.t) : M :=
match ฯ, ฮฑ with
 [], [ a; b ] =>
ltac:(M.monadic
(let a := M.alloc ( a ) in
let b := M.alloc ( b ) in
BinOp.Panic.add ( M.read ( a ), M.read ( b ) )))
 _, _ => M.impossible
end.
The main change is that we do not need to introduce intermediate let*
expressions with generated names. The code structure is more similar to the original Rust code, with additional calls to memory primitives such as M.alloc
and M.read
.
The notation f ( x1, ..., xn )
represents the call to the function f
with the arguments x1
, ..., xn
returning a monadic result. We bind the result with the current continuation that goes up to the wrapping ltac:(M.monadic ...)
tactic. We automatically transform the let
into a let*
with the M.monadic
tactic when needed.
We use this notation in all the function bodies that we generate, that are all in a monad to represent side effects. We call the ltac:(M.monadic ...)
tactic at the start of the functions, as well as at the start of closure bodies that are defined inside functions. This also applies to the translation of if
, match
, and loop
expressions, as we represent their bodies as functions.
Here is an example of code with a match
expression:
fn add(a: i32, b: i32) > i32 {
match a  b {
0 => a + b,
_ => a  b,
}
}
We translate it to:
Definition add (ฯ : list Ty.t) (ฮฑ : list Value.t) : M :=
match ฯ, ฮฑ with
 [], [ a; b ] =>
ltac:(M.monadic
(let a := M.alloc ( a ) in
let b := M.alloc ( b ) in
M.read (
M.match_operator (
M.alloc ( BinOp.Panic.sub ( M.read ( a ), M.read ( b ) ) ),
[
fun ฮณ =>
ltac:(M.monadic
(let _ :=
M.is_constant_or_break_match (
M.read ( ฮณ ),
Value.Integer Integer.I32 0
) in
M.alloc (
BinOp.Panic.add ( M.read ( a ), M.read ( b ) )
)));
fun ฮณ =>
ltac:(M.monadic (
M.alloc (
BinOp.Panic.sub ( M.read ( a ), M.read ( b ) )
)
))
]
)
)))
 _, _ => M.impossible
end.
We see that we call the tactic M.monadic
for each branch of the match
expression.
The M.monadic
tactic is defined in M.v. The main part is:
Ltac monadic e :=
lazymatch e with
(* ... *)
 context ctxt [M.run ?x] =>
lazymatch context ctxt [M.run x] with
 M.run x => monadic x
 _ =>
refine (M.bind _ _);
[ monadic x
 let v := fresh "v" in
intro v;
let y := context ctxt [v] in
monadic y
]
end
(* ... *)
end.
In our translation of Rust, all of the values have the common type Value.t
. The monadic bind is of type M > (Value.t > M) > M
where M
is the type of the monad. The M.run
function is an axiom that we use as a marker to know where we need to apply M.bind
. The type of M.run
is:
Axiom run : M > Value.t.
The notation for monadic function calls is defined using the M.run
axiom with:
Notation "e ( e1 , .. , en )" := (M.run ((.. (e e1) ..) en)).
When we encounter a M.run
(line 4) we apply the M.bind
(line 8) to the monadic expression x
(line 9) and its continuation ctx
that we obtain thanks to the context
keyword (line 4) of the matching of expressions in Ltac.
There is another case in the M.monadic
tactic to handle the let
expressions, that is not shown here.
Thanks to this new monadic notation, the generated Coq code is more readable and closer to the original Rust code. This should simplify our work in writing proofs on the generated code, as well as debugging the translation.
If you are interested in formally verifying your Rust projects, do not hesitate to get in touch with us at contact@formal.land ๐! Formal verification provides the highest level of safety for critical applications, with a mathematical guarantee of the absence of bugs for a given specification.
]]>coqofrust
to:
This work is funded by the Aleph Zero cryptocurrency to verify their Rust smart contracts. You can follow us on X to get our updates. We propose tools and services to make your codebase bugfree with formal verification.
Contact us at contact@formal.land to chat โ๏ธ!
dns
example ๐โWe continue with our previous example dns.rs, which is composed of around 200 lines of Rust code.
The next error that we encounter when typechecking the Coq translation of dns.rs
is:
File "./examples/default/examples/ink_contracts/dns.v", line 233, characters 2227:
Error: The reference deref was not found in the current environment.
In Rust, we can either take the address of a value with &
, or dereference a reference with *
. In our translation, we do not distinguish between the four following pointer types:
&
&mut
*const
*mut
We let the user handle these in different ways if it can simplify their proofs, especially regarding the distinction between mutable and nonmutable pointers. It simplifies the definition of our borrowing and dereferencing operators, as we need only two to cover all cases. We even go further: we remove these two operators in the translation, as they are the identity in our case!
To better understand why they are the identity, we need to see that there are two kinds of Rust values in our representation:
The value itself is useful to compute over the values. For example, we use it to define the primitive addition over integers. The value with its address corresponds to the final Rust expression. Indeed, we can take the address of any subexpression in Rust with the &
operator, so each subexpression should come with its address. When we take the address of an expression, we:
Thus, the &
operator behaves as the identity function followed by an allocation. Similarly, the *
is a memory read followed by the identity function. Since we already use the alloc and read operations to go from a value to a value with its address and the other way around, we do not need to define the *
and &
operators in our translation and remove them.
We now need to distinguish between the function calls, that use the primitive:
M.get_function : string > M
to find the right function to call when defining the semantics of the program (even if the function is defined later), and the calls to primitive operators (+
, *
, !
, ...) that we define in our base library for Rust in Coq. The full list of primitive operators is given by:
We adapted the handling of primitive operators from the code we had before and added a few other fixes so that now the dns.rs
example typechecks in Coq ๐! We will now focus on fixing the other examples.
But let us first clean the code a bit. All the expressions in the internal AST of coqofrust
are in a wrapper with the current type of the expression:
pub(crate) struct Expr {
pub(crate) kind: Rc<ExprKind>,
pub(crate) ty: Option<Rc<CoqType>>,
}
pub(crate) enum ExprKind {
Pure(Rc<Expr>),
LocalVar(String),
Var(Path),
Constructor(Path),
// ... all the cases
Having access to the type of each subexpression was useful before annotating the let
expressions. This is not required anymore, as all the values have the type Value.t
. Thus, we remove the wrapper Expr
and rename ExprKind
into Expr
. The resulting code is easier to read, as wrapping everything with a type was verbose sometimes.
We also cleaned some translated types that were not used anymore in the code, removed unused Derive
traits, and removed the monadic translation on the types.
To handle the remaining examples of our test suite (extracted from the snippets of the Rust by Example book), we mainly needed to reimplement the pattern matching on the new untyped values. Here is an example of Rust code with matching:
fn matching(tuple: (i32, i32)) > i32 {
match tuple {
(0, 0) => 0,
(_, _) => 1,
}
}
with its translation in Coq:
Definition matching (๐ : list Ty.t) (ฮฑ : list Value.t) : M :=
match ๐, ฮฑ with
 [], [ tuple ] =>
let* tuple := M.alloc tuple in
let* ฮฑ0 :=
match_operator
tuple
[
fun ฮณ =>
let* ฮณ0_0 := M.get_tuple_field ฮณ 0 in
let* ฮณ0_1 := M.get_tuple_field ฮณ 1 in
let* _ :=
let* ฮฑ0 := M.read ฮณ0_0 in
M.is_constant_or_break_match ฮฑ0 (Value.Integer Integer.I32 0) in
let* _ :=
let* ฮฑ0 := M.read ฮณ0_1 in
M.is_constant_or_break_match ฮฑ0 (Value.Integer Integer.I32 0) in
M.alloc (Value.Integer Integer.I32 0);
fun ฮณ =>
let* ฮณ0_0 := M.get_tuple_field ฮณ 0 in
let* ฮณ0_1 := M.get_tuple_field ฮณ 1 in
M.alloc (Value.Integer Integer.I32 1)
] in
M.read ฮฑ0
 _, _ => M.impossible
end.
Here is a breakdown of how it works:
match_operator
primitive that takes a value to match on, tuple
, and a list of functions that try to match the value with a pattern and execute some code in case of success. We execute the matching functions successively until one succeeds and we stop. There should be at least one succeeding function as patternmatch in Rust is exhaustive.ฮณ
that is the address of the tuple tuple
given as parameter to the function. Having the address might be required for some operations, like doing subsequent matching by reference or using the &
operator in the match
's body.ฮณ
are generated to avoid name clashes. They correspond to the depth of the subpattern being considered, followed by the index of the current item in this subpattern.0
. We use the M.is_constant_or_break_match
primitive that checks if the value is a constant and if it is equal to the expected value. If it is not the case, it exits the current matching function, and the match_operator
primitive will evaluate the next one, going to line 19.M.alloc
followed by M.read
to return the result. This could be simplified, as immediately reading an allocated value is like running the identity function.By implementing the new version of the patternmatching, as well as a few other smaller fixes, we were able to make all the examples typecheck again! We now need to fix the proofs we had on the erc20.v example, as the generated code changed a lot.
Unfortunately, all these changes in the generated code are breaking our proofs. We still want to write our specifications and proofs by first showing a simulation of the Rust code with a simpler and functional definition. Before, with our simulations, we were:
Now we also have to:
We have not finished updating the proofs but still merged our work in main
with the pull request #472 as this was taking too long. The proof that we want to update is in the file proofs/erc20.v and is about the smart contract erc20.rs.
Our basic strategy for the proof, in order to handle the untyped Rust values of the new translation, is to define various ฯ
operators coming from a userdefined Coq type to a Rust value of type Value.t
. These translate the data types that we define to represent the Rust types of the original program. Note that we previously had trouble translating the Rust types in the general case, especially for mutually recursive types or types involving a lot of trait manipulations.
More formally, we introduce the Coq typeclass:
Class ToValue (A : Set) : Set := {
ฮฆ : Ty.t;
ฯ : A > Value.t;
}.
Arguments ฮฆ _ {_}.
This describes how to go from a userdefined type in Coq to the equivalent representation in Value.t
. In addition to the ฯ
operator, we also define the ฮฆ
operator that gives the Rust type of the Coq type. This type is required to give for polymorphic definitions.
We always go from userdefined types to Value.t
. We write our simulation statements like this:
{{env, state 
code.example.get_at_index [] [ฯ vector; ฯ index] โ
inl (ฯ (simulations.example.get_at_index vector index))
 state'}}
where:
{{env, state  rust_program โ simulation_result  state'}}
is our predicate to state an evaluation of a Rust program to a simulation result. We apply the ฯ
operator to the arguments of the Rust program and to the result of the simulation. In some proofs, we set this operator as Opaque
in order to keep track of it and avoid unwanted reductions.
The trait definitions, as well as trait constraints, are absent from the generated Coq code. For now, we add them back as follows, for the example of the Default
trait:
We define a Default
typeclass in Coq:
Module Default.
Class Trait (Self : Set) : Set := {
default : Self;
}.
End Default.
We define what it means to implement the Default
trait and have a corresponding simulation:
Module Default.
Record TraitHasRun (Self : Set)
`{ToValue Self}
`{core.simulations.default.Default.Trait Self} :
Prop := {
default :
exists default,
IsTraitMethod
"core::default::Default" (ฮฆ Self) []
"default" default /\
Run.pure
(default [] [])
(inl (ฯ core.simulations.default.Default.default));
}.
End Default.
where Run.pure
is our simulation predicate for the case where the state
does not change.
Finally, we use the TraitHasRun
predicate as an additional hypothesis for simulation proofs on functions that depend on the Default
trait in Rust:
(** Simulation proof for `unwrap_or_default` on the type `Option`. *)
Lemma run_unwrap_or_default {T : Set}
{_ : ToValue T}
{_ : core.simulations.default.Default.Trait T}
(self : option T) :
core.proofs.default.Default.TraitHasRun T >
Run.pure
(core.option.Impl_Option_T.unwrap_or_default (ฮฆ T) [] [ฯ self])
(inl (ฯ (core.simulations.option.Impl_Option_T.unwrap_or_default self))).
Proof.
(* ... *)
Qed.
We still have a lot to do, especially in finding the right approach to verify the newly generated Rust code. But we have finalized our new translation mode without types and ordering, which helps to successfully translate many more Rust examples. We also do not need to translate the dependencies of a project anymore before compiling it.
Our next target is to translate the whole of Rust's standard library (with the help of some axioms for the expressions which we do not handle yet), in order to have a faithful definition of the Rust primitives, such as functions of the option and vec modules.
If you are interested in formally verifying your Rust projects, do not hesitate to get in touch with us at contact@formal.land ๐! Formal verification provides the highest level of safety for critical applications, with a mathematical guarantee of the absence of bugs for a given specification.
]]>coqofrust
to make the generated code use these new definitions.
With this new translation strategy, to support more Rust code, we want:
This work is funded by the Aleph Zero cryptocurrency to verify their Rust smart contracts. You can follow us on X to get our updates. We propose tools and services to make your codebase bugfree with formal verification.
Contact us at contact@formal.land to chat!
We implemented the new monad and the type Value.t
holding any kind of Rust values as described in the previous blog post. For now, we have removed the definitions related to the standard library of Rust (everything except the base definitions such as the integer types). This should not be an issue to typecheck the generated Coq code, as the new code should be independent of the ordering of definitions: in particular, it should typecheck even if the needed definitions are not yet there.
We added some definitions for the primitive unary and binary operators. These include some operations on the integers such arithmetic operations (with or without overflow, depending on the compilation mode), as well as comparisons (equality, lesser or equal than, ...).
Now that the main library file CoqOfRust/CoqOfRust.v compiles in Coq, we can start to test the translation on our examples.
We generate new snapshots for our translations with:
cargo build && time python run_tests.py
This builds the project coqofrust
(with a lot of warning about unused code for now) and regenerates our snapshots: for each Rust file in the examples directory, we generate a Coq file with the same name but the extension .v
. We generate two versions:
We first try to typecheck and fix the code generated in axiom mode.
We have a first error for type aliases that we do not translate properly. We need access to the fully qualified name of the alias. We do that by combining calls to the functions:
As a result, for the file examples/ink_contracts/basic_contract_caller.rs, we translate the type alias:
type Hash = [u8; 32];
into the Coq code:
Axiom Hash :
(Ty.path "basic_contract_caller::Hash") =
(Ty.apply (Ty.path "array") [Ty.path "u8"]).
Then, during the proofs, we will be able to substitute the type Hash
by its definition when it appears. Note that we now translate types by values of the type Ty.t
, so there should be no difficulties in rewriting types.
We should add the length of the array in the type. This is not done yet.
In axiom mode, we remove most of the trait definitions. Instead, with our new translation model, the traits are mostly unique names (the absolute path of the trait definition). The main use of traits is to distinguish them from other traits, to know which trait implementation to use when calling a trait's method. We still translate the provided methods (that are default methods in the trait definition) to axioms and add a predicate stating that they are associated with the current trait. For example, we translate the following Rust trait:
// crate `my_crate`
trait Animal {
fn new(name: &'static str) > Self;
fn name(&self) > &'static str;
fn noise(&self) > &'static str;
fn talk(&self) {
println!("{} says {}", self.name(), self.noise());
}
}
to the Coq code:
(* Trait *)
Module Animal.
Parameter talk : (list Ty.t) > (list Value.t) > M.
Axiom ProvidedMethod_talk : M.IsProvidedMethod "my_crate::Animal" talk.
End Animal.
We realize with this example that the translation in axiom mode generates very few errors, as we remove all the type definitions and all the function axioms have the same signature:
(* A list of types that can be empty for nonpolymorphic functions,
a list of parameters, and a return value in the monad `M`. *)
list Ty.t > list Value.t > M
so the typechecking of these axioms never fails. We thus jump to the full definition mode as this is where our new approach might fail.
We now try to typecheck the generated Coq code in full definition mode. We start with the dns.rs smart contract example.
This example is interesting, as it contains polymorphic implementations, such as for the mock type Mapping
:
#[derive(Default)]
struct Mapping<K, V> {
_key: core::marker::PhantomData<K>,
_value: core::marker::PhantomData<V>,
}
that implements the Default trait on the type Mapping<K, V>
for two type parameters K
and V
. We translate it to:
(* Struct Mapping *)
Module Impl_core_default_Default_for_dns_Mapping_K_V.
(*
Default
*)
Definition default (๐ : list Ty.t) (ฮฑ : list Value.t) : M :=
match ๐, ฮฑ with
 [ Self; K; V ], [] =>
let* ฮฑ0 :=
M.get_method
"core::default::Default"
"default"
[ (* Self *) Ty.apply (Ty.path "core::marker::PhantomData") [ K ] ] in
let* ฮฑ1 := M.call ฮฑ0 [] in
let* ฮฑ2 :=
M.get_method
"core::default::Default"
"default"
[ (* Self *) Ty.apply (Ty.path "core::marker::PhantomData") [ V ] ] in
let* ฮฑ3 := M.call ฮฑ2 [] in
M.pure
(Value.StructRecord "dns::Mapping" [ ("_key", ฮฑ1); ("_value", ฮฑ3) ])
 _, _ => M.impossible
end.
Axiom Implements :
forall (K V : Ty.t),
M.IsTraitInstance
"core::default::Default"
(* Self *) (Ty.apply (Ty.path "dns::Mapping") [ K; V ])
[]
[ ("default", InstanceField.Method default) ]
[ K; V ].
End Impl_core_default_Default_for_dns_Mapping_K_V.
Here are the interesting bits of this code:
On line 1, we translate the Mapping
type into a single comment, as the types disappear in our translation and become just markers. The marker for Mapping
is its absolute name Ty.path "dns::Mapping"
.
On line 7, the function default
takes a list of types ๐
as a parameter in case it is polymorphic. Here, this method is not polymorphic, but we still add the ๐
parameter for uniformity. We also take three additional type parameters:
Self
K
V
that represent the Self
type on which the trait is implemented, and the two type parameters of the Mapping
type. These will be provided when calling the default
method.
On line 11, we use the primitive M.get_method
(axiomatized for now) to get the method default
of the trait core::default::Default
for the type core::marker::PhantomData<K>
. Here, we see that having access to the type K
in the body of the default
function is useful, as it helps us to disambiguate between the various implementations of the Default
trait instances that we call. Here, we provide the Self
type of the trait in a list of a single element. If the Default
trait or the default
method were polymorphic, we would also append these type parameters in this list.
On line 15, we call the default
method instance that we found with an empty list of arguments.
On line 23, we build a value of type Mapping
with the two fields _key
and _value
initialized with the results of the two calls to the default
method. We use the Value.StructRecord
constructor to build the value, and its result is of type Value.t
like all other Rust values.
On line 24, we eliminate a case with a wrong number of type and value arguments. This should never happen as the arity of all the function calls is checked by the Rust typechecker.
On line 27, we state that we have a new instance of the Default
trait for the Mapping
type, with the default
method implemented by the default
function. This is true for any values of the types K
and V
.
On line 34, we specify that [K, V]
are the type parameters of this implementation that should be given as extra parameters when calling the default
method of this instance, together with the Self
type.
Next, we have a polymorphic implementation of mock associated functions for the Mapping
type:
impl<K, V> Mapping<K, V> {
fn contains(&self, _key: &K) > bool {
unimplemented!()
}
// ...
We translate it to:
Module Impl_dns_Mapping_K_V.
Definition Self (K V : Ty.t) : Ty.t :=
Ty.apply (Ty.path "dns::Mapping") [ K; V ].
(*
fn contains(&self, _key: &K) > bool {
unimplemented!()
}
*)
Definition contains (๐ : list Ty.t) (ฮฑ : list Value.t) : M :=
match ๐, ฮฑ with
 [ Self; K; V ], [ self; _key ] =>
let* self := M.alloc self in
let* _key := M.alloc _key in
let* ฮฑ0 := M.var "core::panicking::panic" in
let* ฮฑ1 := M.read (mk_str "not implemented") in
let* ฮฑ2 := M.call ฮฑ0 [ ฮฑ1 ] in
never_to_any ฮฑ2
 _, _ => M.impossible
end.
Axiom AssociatedFunction_contains :
forall (K V : Ty.t),
M.IsAssociatedFunction (Self K V) "contains" contains [ K; V ].
(* ... *)
We follow a similar approach as for the translation of trait implementations, especially regarding the handling of polymorphic type variables. Here are some differences:
Self
type as a function of the type parameters K
and V
. This is useful for avoiding repeating the same type expression later.M.IsAssociatedFunction
to state that we have a new associated function contains
for the Mapping
type, with the contains
method implemented by the contains
function. This is true for any values of the types K
and V
. Like for the trait implementations, we explicit the list [K, V]
that will be given as an extra parameter to the function contains
.In the next blog post, we will see how we continue to translate the examples in full definition mode. There is still a lot to do to get to the same level of Rust support as before, but we are hopeful that our new approach will be more robust and easier to maintain.
If you are interested in formally verifying your Rust projects, do not hesitate to get in touch with us at contact@formal.land! Formal verification provides the highest level of safety for critical applications. See the White House report on secure software development for more on the importance of formal verification.
]]>coqofrust
still has some limitations:
We will present how we plan to improve our tool to address these limitations.
As emphasized in the recent report from the White House, memory safety and formal verification are keys to ensure secure and correct software. Rust provides memory safety and we provide formal verification on top of it with coqofrust
.
We will take the Rust serde serialization library to have an example of code to translate in Coq. This is a popular Rust library that is used in almost all projects, either as a direct or transitive dependency. Serialization has a simple specification (being a bijection between the data and its serialized form) and is a good candidate for formal verification. We might verify this library afterwards if there is a need.
This work is funded by the Aleph Zero cryptocurrency in order to verify their Rust smart contracts. You can follow us on X to get our updates. We propose tools and services to make your codebase totally bugfree. Contact us at contact@formal.land to chat! We offer a free audit to assess the feasibility of formal verification on your case.
Our company goal is to make formal verification accessible to all projects, reducing its cost to 20% of the development cost. There should be no reason to have bugs in enduser products!
We start by running the command:
cargo coqofrust
in the serde
directory. We get a lot of warnings, but the translation does not panic as it tries to always produce something for debugging purposes. We have two kinds of warnings.
The warning is the following:
warning: Constants in patterns are not yet supported.
> serde/src/de/mod.rs:2277:13

2277  0 => panic!(), // special case elsewhere
 ^
The reason why we did not handle constants in patterns is that they are represented in a special format in the Rust compiler that was not obvious to handle. The definition of rustc_middle::mir::consts::Const representing the constants in patterns is:
pub enum Const<'tcx> {
Ty(Const<'tcx>),
Unevaluated(UnevaluatedConst<'tcx>, Ty<'tcx>),
Val(ConstValue<'tcx>, Ty<'tcx>),
}
There are three cases, and each contains several more cases. To fix this issue, we added the code to handle the signed and unsigned integers, which are enough for our serde
example. We will need to add other cases later, especially for the strings. This allowed us to discover and fix a bug in our handling of patterns for tuples with elision ..
, like in the example:
fn main() {
let triple = (0, 2, 3);
match triple {
(0, y, z) => println!("First is `0`, `y` is {:?}, and `z` is {:?}", y, z),
(1, ..) => println!("First is `1` and the rest doesn't matter"),
(.., 2) => println!("last is `2` and the rest doesn't matter"),
(3, .., 4) => println!("First is `3`, last is `4`, and the rest doesn't matter"),
_ => println!("It doesn't matter what they are"),
}
}
These changes are in the pullrequest coqofrust#470.
parent_kind
โWe get a second form of warning:
unimplemented parent_kind: Struct
expression: Expr {
kind: ZstLiteral {
user_ty: None,
},
ty: FnDef(
DefId(2:31137 ~ core[10bc]::cmp::Reverse::{constructor#0}),
[
T/#1,
],
),
temp_lifetime: Some(
Node(14),
),
span: serde/src/de/impls.rs:778:22: 778:29 (#0),
}
This is for some cases of expressions rustc_middle::thir::ExprKind::ZstLiteral in the Rust's THIR representation that we do not handle. If we look at the span
field, we see that it appears in the source in the file serde/src/de/impls.rs
at line 778:
forwarded_impl! {
(T), Reverse<T>, Reverse // Here is the error
}
This is not very informative as this code is generated by a macro. Another similar kind of expression appears later:
impl<'de, T> Deserialize<'de> for Wrapping<T>
where
T: Deserialize<'de>,
{
fn deserialize<D>(deserializer: D) > Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Deserialize::deserialize(deserializer).map(
// Here is the error:
Wrapping
)
}
}
The Wrapping
term is the constructor of a structure, used as a function. We add the support of this case in the pullrequest coqofrust#471.
When we typecheck the generated Coq code, we quickly get an error:
(* Generated by coqofrust *)
Require Import CoqOfRust.CoqOfRust.
Module lib.
Module core.
End core.
End lib.
Module macros.
End macros.
Module integer128.
End integer128.
Module de.
Module value.
Module Error.
Section Error.
Record t : Set := {
(* Here is the error: *)
err : ltac:(serde.de.value.ErrorImpl);
}.
(* 180.000 more lines! *)
The reason is that serde.de.value.ErrorImpl
is not yet defined here. In Coq, we must order the definitions in the order of dependencies to ensure that there are no nonterminating definitions with infinite recursive calls and to preserve the consistency of the system.
This issue does not seem easy to us, as in a Rust crate, everything can depend on each other:
impl
blocksOur current solutions are:
coqofrust
), but it is not very practical. Indeed, reordering elements in a big project generates a lot of conflicts in the version control system, especially if we cannot upstream the changes to the original project.In order to handle large projects, such as serde
, we need to find a more definitive solution to handle the order of dependencies.
Our idea is to use a more verbose, but simpler translation, to generate Coq code that is not sensitive to the ordering of Rust. In addition, we should have a more robust mechanism for the traits, as there are still some edge cases that we do not handle well.
Our main ingredients are:
Value
type. With this approach, we can represent mutually recursive Rust types, that are generally hard to translate in a sound manner to Coq. We should also avoid a lot of errors on the Coq side related to type inference.These ingredients have some drawbacks:
We rework our definitions of values, pointers and monad to represent the effects, taking into account the fact that we remove the types from the translation. Here are the main definitions that we are planning to use. We have not tested them yet as we need to update the translation to Coq to use them. We will do that just after.
Module Pointer.
Module Index.
Inductive t : Set :=
 Tuple (index : Z)
 Array (index : Z)
 StructRecord (constructor field : string)
 StructTuple (constructor : string) (index : Z).
End Index.
Module Path.
Definition t : Set := list Index.t.
End Path.
Inductive t (Value : Set) : Set :=
 Immediate (value : Value)
 Mutable {Address : Set} (address : Address) (path : Path.t).
Arguments Immediate {_}.
Arguments Mutable {_ _}.
End Pointer.
A pointer is either:
The type of Address
is not enforced yet, but we will do it when defining the semantics.
Module Value.
Inductive t : Set :=
 Bool : bool > t
 Integer : Integer.t > Z > t
(** For now we do not know how to represent floats so we use a string *)
 Float : string > t
 UnicodeChar : Z > t
 String : string > t
 Tuple : list t > t
 Array : list t > t
 StructRecord : string > list (string * t) > t
 StructTuple : string > list t > t
 Pointer : Pointer.t t > t
(** The two existential types of the closure must be [Value.t] and [M]. We
cannot enforce this constraint there yet, but we will do when defining the
semantics. *)
 Closure : {'(t, M) : Set * Set @ t > M} > t.
End Value.
Here, this type aims to represent any Rust value. We might add a few cases later to represent the dyn
values, for example. Most of the cases of this type are as expected:
StructRecord
is for constructors of struct
or enum
with named fields.StructTuple
is for constructors of struct
or enum
with unnamed fields.Pointer
is for pointers to data, that could be either &
, &mut
, *const
, or *mut
.Closure
is for closures (anonymous functions). To prevent errors with the positivity checker of Coq, we use an existential type for the type Value.t
(as well as M
, which will be defined later). Note that we are using impredicative Set
in Coq, and {A : Set @ P A}
is our notation for existential Set
in Set
. Without impredicative sets, we could have issues with the universe levels. The fact that these existential types are always Value.t
and M
will be enforced when defining the semantics.Module Primitive.
Inductive t : Set :=
 StateAlloc (value : Value.t)
 StateRead {Address : Set} (address : Address)
 StateWrite {Address : Set} (address : Address) (value : Value.t)
 EnvRead.
End Primitive.
Here are the IO calls to the system that the monad can make. This list might be extended later. For now, we mainly have primitives to access the memory.
Module LowM.
Inductive t (A : Set) : Set :=
 Pure : A > t A
 CallPrimitive : Primitive.t > (Value.t > t A) > t A
 Loop : t A > (A > bool) > (A > t A) > t A
 Impossible : t A
(** This constructor is not strictly necessary, but is used as a marker for
functions calls in the generated code, to help the tactics to recognize
points where we can compose about functions. *)
 Call : t A > (A > t A) > t A.
Arguments Pure {_}.
Arguments CallPrimitive {_}.
Arguments Loop {_}.
Arguments Impossible {_}.
Arguments Call {_}.
Fixpoint let_ {A : Set} (e1 : t A) (f : A > t A) : t A :=
match e1 with
 Pure v => f v
 CallPrimitive primitive k =>
CallPrimitive primitive (fun v => let_ (k v) f)
 Loop body is_break k =>
Loop body is_break (fun v => let_ (k v) f)
 Impossible => Impossible
 Call e k =>
Call e (fun v => let_ (k v) f)
end.
End LowM.
This is the first layer of our monad, very similar to what we had before. We remove the cast operation, as now everything has the same type. We use a style by continuation, but we also define a let_
function to have a "bind" operator. Note that we always have the same type as parameter, so this is not really a monad as the "bind" operator should have the type:
forall {A B : Set}, M A > (A > M B) > M B
Always having the same type is enough for us as we use a single type of all Rust values.
We have the same type as before for the exceptions, representing the panics and all the special control flow operations such as continue
, return
, and break
:
Module Exception.
Inductive t : Set :=
(** exceptions for Rust's `return` *)
 Return : Value.t > t
(** exceptions for Rust's `continue` *)
 Continue : t
(** exceptions for Rust's `break` *)
 Break : t
(** escape from a match branch once we know that it is not valid *)
 BreakMatch : t
 Panic : string > t.
End Exception.
Our final monad definition is a thin wrapper around LowM
, to add an error monad to propagate the exceptions:
Definition M : Set :=
LowM.t (Value.t + Exception.t).
Definition let_ (e1 : M) (e2 : Value.t > M) : M :=
LowM.let_ e1 (fun v1 =>
match v1 with
 inl v1 => e2 v1
 inr error => LowM.Pure (inr error)
end).
Once again, this is not really a monad as the type of the values that we compute is always the same, and we do not need more. Having a definition in two steps (LowM
and M
) is useful to separate the part that can be defined by computation (the M
part) from the part whose semantics can only be given by inductive predicates (the LowM
part).
Next, we will see how we can use this new definition of Rust values, whether it works to translate our examples, and most importantly, how to modify coqofrust
to generate terms without types.
If you are interested in formally verifying Rust projects, do not hesitate to get in touch with us at contact@formal.land or go to our GitHub repository for coqofrust
.
The goal is to formally verify Go programs to make them totally bugfree. It is actually possible to make a program totally bugfree, as formal verification can cover all execution cases and kinds of properties thanks to the use of mathematical methods. This corresponds to the highest level of the Evaluation Assurance Levels used for critical applications, such as the space industry.
All the code of our work is available on GitHub at github.com/formalland/coqofgo.
We believe that there are not yet a lot of formal verification tools for Go. We can cite Goose, which is working by translation from Go to the proof system Coq. We will follow a similar approach, translating the Go language to our favorite proof system Coq. In contrast to Goose, we plan to support the whole Go language, even at the expense of the simplicity of the translation.
For that, we target the translation of the SSA form of Go of Go instead of the Go AST. The SSA form is a more lowlevel representation of Go, so we hope to capture the semantics of the whole Go language more easily. This should be at the expense of the simplicity of the generated translation, but we hope that having full language support outweighs this.
Go is an interesting target as:
Among interesting properties that we can verify are:
panic
in the code,