Foundry Specifications
UNDER CONSTRUCTION
Example Usage
From the root of the KEVM repository, after having:
- Successfully built (or installed) KEVM, and
- Have
kevm
onPATH
, and - Have stepped into the virtual environment (see the README).
KEVM supports Foundry specifications via two CLI utilities, foundry-kompile
and foundry-prove
.
To get help, you can do kevm foundry-kompile --help
and kevm foundry-prove --help
.
Each command takes as input the Foundry out/
directory.
For example, in the root of this repository, you can run:
Build Foundry Project:
$ cd tests/foundry
$ forge build
[⠊] Compiling...
[⠑] Compiling 44 files with 0.8.13
[⠊] Solc 0.8.13 finished in 3.58s
Compiler run successful (with warnings)
$ cd ../..
Kompile to KEVM specification (inside virtual environment):
(venv) $ kevm foundry-kompile tests/foundry/out
WARNING 2022-09-11 15:36:00,448 kevm_pyk.solc_to_k - Using generic sort K for type: uint256[]
WARNING 2022-09-11 15:36:00,448 kevm_pyk.solc_to_k - Using generic sort K for type: uint256[]
WARNING 2022-09-11 15:36:00,448 kevm_pyk.solc_to_k - Using generic sort K for type: uint256[]
WARNING 2022-09-11 15:36:00,448 kevm_pyk.solc_to_k - Using generic sort K for type: uint256[]
WARNING 2022-09-11 15:36:00,448 kevm_pyk.solc_to_k - Using generic sort K for type: uint256[]
WARNING 2022-09-11 15:36:00,448 kevm_pyk.solc_to_k - Unknown range predicate for type: uint256[]
WARNING 2022-09-11 15:36:00,448 kevm_pyk.solc_to_k - Unsupported ABI type for method LoopsTest.method_LoopsTest_testMax, will not generate calldata sugar: uint256[]
WARNING 2022-09-11 15:36:00,448 kevm_pyk.solc_to_k - Using generic sort K for type: uint256[]
WARNING 2022-09-11 15:36:00,448 kevm_pyk.solc_to_k - Unknown range predicate for type: uint256[]
WARNING 2022-09-11 15:36:00,448 kevm_pyk.solc_to_k - Unsupported ABI type for method LoopsTest.method_LoopsTest_testMaxBroken, will not generate calldata sugar: uint256[]
WARNING 2022-09-11 15:36:00,448 kevm_pyk.solc_to_k - Using generic sort K for type: uint256[]
WARNING 2022-09-11 15:36:00,448 kevm_pyk.solc_to_k - Unknown range predicate for type: uint256[]
WARNING 2022-09-11 15:36:00,448 kevm_pyk.solc_to_k - Unsupported ABI type for method LoopsTest.method_LoopsTest_testSort, will not generate calldata sugar: uint256[]
WARNING 2022-09-11 15:36:00,448 kevm_pyk.solc_to_k - Using generic sort K for type: uint256[]
WARNING 2022-09-11 15:36:00,448 kevm_pyk.solc_to_k - Unknown range predicate for type: uint256[]
WARNING 2022-09-11 15:36:00,448 kevm_pyk.solc_to_k - Unsupported ABI type for method LoopsTest.method_LoopsTest_testSortBroken, will not generate calldata sugar: uint256[]
WARNING 2022-09-11 15:36:00,449 kevm_pyk.solc_to_k - Using generic sort K for type: uint256[]
WARNING 2022-09-11 15:36:00,449 kevm_pyk.solc_to_k - Using generic sort K for type: uint256[]
WARNING 2022-09-11 15:36:00,449 kevm_pyk.solc_to_k - Using generic sort K for type: uint256[]
WARNING 2022-09-11 15:36:00,449 kevm_pyk.solc_to_k - Using generic sort K for type: uint256[]
And discharge some specific test as a proof obligation (inside virtual environment):
(venv) $ kevm foundry-prove tests/foundry/out --test AssertTest.test_assert_true
WARNING 2022-09-11 15:37:31,956 __main__ - Ignoring command-line option: --definition: /home/dev/src/evm-semantics/.build/usr/lib/kevm/haskell
#Top
Foundry Module for KEVM
Foundry's testing framework provides a series of cheatcodes so that developers can specify what situation they want to test. This file describes the KEVM specification of the Foundry testing framework, which includes the definition of said cheatcodes and what does it mean for a test to pass.
requires "evm.md"
requires "hashed-locations.md"
requires "abi.md"
requires "edsl.md"
module FOUNDRY
imports FOUNDRY-SUCCESS
imports FOUNDRY-CHEAT-CODES
imports FOUNDRY-ACCOUNTS
imports EDSL
configuration
<foundry>
<kevm/>
<cheatcodes/>
</foundry>
endmodule
Foundry Success Predicate
Foundry has several baked-in convenience accounts for helping to define the "cheat-codes". Here we define their addresses, and important storage-locations.
module FOUNDRY-ACCOUNTS
imports SOLIDITY-FIELDS
syntax Int ::= #address ( Contract ) [macro]
syntax Contract ::= FoundryContract
syntax Field ::= FoundryField
syntax FoundryContract ::= "Foundry" [klabel(contract_Foundry)]
| "FoundryTest" [klabel(contract_FoundryTest)]
| "FoundryCheat" [klabel(contract_FoundryCheat)]
// -------------------------------------------------------------------------
rule #address(Foundry) => 137122462167341575662000267002353578582749290296 // 0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38
rule #address(FoundryTest) => 1032069922050249630382865877677304880282300743300 // 0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84
rule #address(FoundryCheat) => 645326474426547203313410069153905908525362434349 // 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D
syntax FoundryField ::= "Failed" [klabel(slot_failed)]
// ------------------------------------------------------
rule #loc(FoundryCheat . Failed) => 46308022326495007027972728677917914892729792999299745830475596687180801507328 // 0x6661696c65640000000000000000000000000000000000000000000000000000
endmodule
The Foundry success predicate performs the same checks as the is_success
function from Foundry in evm/src/executor/mod.rs
.
These checks are:
- The call to the test contract has not reverted, and
DSTest.assert*
have not failed.
The last condition is implemented in the fail()
function from DSTest
.
If a DSTest assertion should fail, the fail()
function stores bytes32(uint256(0x01))
at the storage slot bytes32("failed")
.
Hence, checking if a DSTest.assert*
has failed amounts to reading as a boolean the data from that particular storage slot.
TODO: It should also be checked if the code present in the FoundryCheat
account is non-empty, and return false if it is.
module FOUNDRY-SUCCESS
imports EVM
syntax Bool ::= "foundry_success" "(" StatusCode "," Int ")" [function, klabel(foundry_success), symbol]
// --------------------------------------------------------------------------------------------------------
rule foundry_success(EVMC_SUCCESS, 0) => true
rule foundry_success(_, _) => false [owise]
endmodule
Foundry Cheat Codes
The configuration of the Foundry Cheat Codes is defined as follwing:
- The
<prank>
subconfiguration stores values which are used during the execution of any kind ofprank
cheatcode:<prevCaller>
keeps the current address of the contract that initiated the prank.<prevOrigin>
keeps the current address of thetx.origin
value.<newCaller>
and<newOrigin>
are addresses to be assigned after the prank call tomsg.sender
andtx.origin
.<depth>
records the current call depth at which the prank was invoked.<singleCall>
tells whether the prank stops by itself after the next call or when astopPrank
cheat code is invoked.
module FOUNDRY-CHEAT-CODES
imports EVM
imports EVM-ABI
imports FOUNDRY-ACCOUNTS
configuration
<cheatcodes>
<prank>
<prevCaller> .Account </prevCaller>
<prevOrigin> .Account </prevOrigin>
<newCaller> .Account </newCaller>
<newOrigin> .Account </newOrigin>
<depth> 0 </depth>
<singleCall> false </singleCall>
</prank>
</cheatcodes>
First we have some helpers in K which can:
- Inject a given boolean condition into's this execution's path condition, and
- Check that a given boolean condition holds (recording failure if not).
syntax KItem ::= #assume ( Bool ) [klabel(foundry_assume), symbol]
| #assert ( Bool ) [klabel(foundry_assert), symbol]
// ------------------------------------------------------------------
rule <k> #assume(B) => . ... </k> ensures B
rule <k> #assert(false) => . ... </k>
<account>
<acctID> #address(FoundryCheat) </acctID>
<storage> STORAGE => STORAGE [ #loc(FoundryCheat . Failed) <- 1 ] </storage>
...
</account>
Structure of execution
The foundry.call
rule is used to inject specific behaviour for each cheat code.
The rule has a higher priority than any other CALL
rule and will match every call made to the Foundry cheatcode address.
The function selector, represented as #asWord(#range(LM, ARGSTART, 4))
and the call data #range(LM, ARGSTART +Int 4, ARGWIDTH -Int 4)
are passed to the #call_foundry
production, which will further rewrite using rules defined for implemented cheat codes.
Finally, the rule for #return_foundry
is used to end the execution of the CALL
OpCode.
rule [foundry.call]:
<k> CALL _ CHEAT_ADDR _VALUE ARGSTART ARGWIDTH RETSTART RETWIDTH
=> #call_foundry #asWord(#range(LM, ARGSTART, 4)) #range(LM, ARGSTART +Int 4, ARGWIDTH -Int 4)
~> #return_foundry RETSTART RETWIDTH
...
</k>
<localMem> LM </localMem>
<output> _ => .ByteArray </output>
requires CHEAT_ADDR ==Int #address(FoundryCheat)
[priority(40)]
We define two productions named #return_foundry
and #call_foundry
, which will be used by each cheat code.
The rule foundry.return
will rewrite the #return_foundry
production into other productions that will place the output of the execution into the local memory, refund the gas value of the call and push the value 1
on the call stack.
syntax KItem ::= "#error_foundry" [klabel(foundry_error)]
| "#return_foundry" Int Int [klabel(foundry_return)]
| "#call_foundry" Int ByteArray [klabel(foundry_call)]
// ---------------------------------------------------------------------
rule [foundry.return]:
<k> #return_foundry RETSTART RETWIDTH
=> #setLocalMem RETSTART RETWIDTH OUT
~> #refund GCALL
~> 1 ~> #push
... </k>
<output> OUT </output>
<callGas> GCALL </callGas>
Below, we define rules for the #call_foundry
production, handling the cheat codes.
assume
- Insert a condition
function assume(bool) external;
foundry.call.assume
will match when the assume
cheat code function is called.
This rule then takes a bool
condition from the function call data, represented using the ARGS
variable, and injects it into the execution path using the #assume
production.
==K #bufStrict(32, 1)
is used to mark that the condition should hold.
rule [foundry.call.assume]:
<k> #call_foundry SELECTOR ARGS => #assume(ARGS ==K #bufStrict(32, 1)) ... </k>
requires SELECTOR ==Int selector ( "assume(bool)" )
deal
- Set a given balance to a given account.
function deal(address who, uint256 newBalance) external;
foundry.call.deal
will match when the deal
cheat code function is called.
The rule then takes from the function call data the target account, using #asWord(#range(ARGS, 0, 32)
, and the new balance, using #asWord(#range(ARGS, 32, 32))
, and forwards them to the #setBalance
production which updates the account accordingly.
rule [foundry.call.deal]:
<k> #call_foundry SELECTOR ARGS => #loadAccount #asWord(#range(ARGS, 0, 32)) ~> #setBalance #asWord(#range(ARGS, 0, 32)) #asWord(#range(ARGS, 32, 32)) ... </k>
requires SELECTOR ==Int selector ( "deal(address,uint256)" )
etch
- Sets the code of an account.
function etch(address who, bytes calldata code) external;
foundry.call.etch
will match when the etch
cheat code function is called.
This rule then takes from the function call data the target account, using #asWord(#range(ARGS, 0, 32)
, and the new bytecode, using #range(ARGS, CODE_START, CODE_LENGTH)
, where #asWord(#range(ARGS, 64, 32))
is used to determine the length of the second argument.
The values are forwarded to the #setCode
production which updates the account accordingly.
rule [foundry.call.etch]:
<k> #call_foundry SELECTOR ARGS
=> #loadAccount #asWord(#range(ARGS, 0, 32))
~> #let CODE_START = 96 #in
#let CODE_LENGTH = #asWord(#range(ARGS, 64, 32)) #in
#setCode #asWord(#range(ARGS, 0, 32)) #range(ARGS, CODE_START, CODE_LENGTH)
...
</k>
requires SELECTOR ==Int selector ( "etch(address,bytes)" )
warp
- Sets the block timestamp.
function warp(uint256) external;
foundry.call.warp
will match when the warp
cheat code function is called.
This rule then takes the uint256
value from the function call data which is represented as ARGS
and updates the <timestamp>
cell.
rule [foundry.call.warp]:
<k> #call_foundry SELECTOR ARGS => . ... </k>
<timestamp> _ => #asWord(ARGS) </timestamp>
requires SELECTOR ==Int selector( "warp(uint256)" )
roll
- Sets the block number.
function roll(uint256) external;
foundry.call.roll
will match when the roll
cheat code function is called.
This rule then takes the uint256
value from the function call data which is represented as ARGS
and updates the <number>
cell.
rule [foundry.call.roll]:
<k> #call_foundry SELECTOR ARGS => . ... </k>
<number> _ => #asWord(ARGS) </number>
requires SELECTOR ==Int selector ( "roll(uint256)" )
fee
- Sets the block base fee.
function fee(uint256) external;
foundry.call.fee
will match when the fee
cheat code function is called.
This rule then takes the uint256
value from the function call data which is represented as ARGS
and updates the <baseFee>
cell.
rule [foundry.call.fee]:
<k> #call_foundry SELECTOR ARGS => . ... </k>
<baseFee> _ => #asWord(ARGS) </baseFee>
requires SELECTOR ==Int selector ( "fee(uint256)" )
chainId
- Sets the chain ID.
function chainId(uint256) external;
foundry.call.chainId
will match when the chainId
cheat code function is called.
This rule then takes the uint256
value from the function call data which is represented as ARGS
and updates the <chainID>
cell.
rule [foundry.call.chainId]:
<k> #call_foundry SELECTOR ARGS => . ... </k>
<chainID> _ => #asWord(ARGS) </chainID>
requires SELECTOR ==Int selector ( "chainId(uint256)" )
coinbase
- Sets the block coinbase.
function coinbase(address) external;
foundry.call.coinbase
will match when the coinbase
cheat code function is called.
This rule then takes the uint256
value from the function call data which is represented as ARGS
and updates the <coinbase>
cell.
rule [foundry.call.coinbase]:
<k> #call_foundry SELECTOR ARGS => . ... </k>
<coinbase> _ => #asWord(ARGS) </coinbase>
requires SELECTOR ==Int selector ( "coinbase(address)" )
label
- Sets a label for a given address.
function label(address addr, string calldata label) external;
foundry.call.label
will match when the label
cheat code function is called.
If an address is labelled, the label will show up in test traces instead of the address.
However, there is no change on the state and therefore this rule just skips the cheatcode invocation.
rule [foundry.call.label]:
<k> #call_foundry SELECTOR _ARGS => . ... </k>
requires SELECTOR ==Int selector ( "label(address,string)" )
getNonce
- Gets the nonce of the given account.
function getNonce(address account) external returns (uint64);
foundry.call.getNonce
will match when the getNonce
cheat code function is called.
This rule takes the address
value from the function call data, which is represented as ARGS
, and forwards it to the #returnNonce
production, which will update the <output>
cell with the nonce
of the account.
rule [foundry.call.getNonce]:
<k> #call_foundry SELECTOR ARGS => #loadAccount #asWord(ARGS) ~> #returnNonce #asWord(ARGS) ... </k>
requires SELECTOR ==Int selector ( "getNonce(address)" )
setNonce
- Sets the nonce of the given account.
function setNonce(address account, uint64 nonce) external;
foundry.call.setNonce
will match when the setNonce
function is called.
This rule takes the address
value and uint64
value corresponding to new nonce using from the function call data, which is represented as ARGS
forwards it to the #setNonce
production, which will update the nonce of the account.
rule [foundry.call.setNonce]:
<k> #call_foundry SELECTOR ARGS => #loadAccount #asWord(#range(ARGS, 0, 32)) ~> #setNonce #asWord(#range(ARGS, 0, 32)) #asWord(#range(ARGS, 32, 32)) ... </k>
requires SELECTOR ==Int selector ( "setNonce(address,uint64)" )
addr
- Computes the address for a given private key.
function addr(uint256 privateKey) external returns (address);
foundry.call.addr
will match when the addr
cheat code function is called.
This rule takes uint256
private key from the function call data, which is represented as ARGS
, and computes the address.
The <output>
cell will be updated with the value of the address generated from the private key using #addrFromPrivateKey(#unparseByteStack(ARGS))
.
#bufStrict
is used to pad the value to a length of 32.
rule [foundry.call.addr]:
<k> #call_foundry SELECTOR ARGS => . ... </k>
<output> _ => #bufStrict(32, #addrFromPrivateKey(#unparseByteStack(ARGS))) </output>
requires SELECTOR ==Int selector ( "addr(uint256)" )
load
- Loads a storage slot from an address.
function load(address account, bytes32 slot) external returns (bytes32);
foundry.call.load
will match when the load
cheat code function is called.
This rule loads the storage slot identified by #asWord(#range(ARGS, 32, 32))
(referring to slot
argument) from account #asWord(#range(ARGS, 0, 32))
(referring to account
).
The value of the identified storage slot is placed in the <output>
cell using the #returnStorage
production.
rule [foundry.call.load]:
<k> #call_foundry SELECTOR ARGS => #loadAccount #asWord(#range(ARGS, 0, 32)) ~> #returnStorage #asWord(#range(ARGS, 0, 32)) #asWord(#range(ARGS, 32, 32)) ... </k>
requires SELECTOR ==Int selector ( "load(address,bytes32)" )
store
- Stores a value to an address' storage slot.
function store(address account, bytes32 slot, bytes32 value) external;
foundry.call.store
will match when the store
cheat code function is called.
This rule then takes from the function call data the account using #asWord(#range(ARGS, 0, 32))
and the new slot value using #asWord(#range(ARGS, 32, 32))
and updates the slot denoted by #asWord(#range(ARGS, 64, 32))
.
rule [foundry.call.store]:
<k> #call_foundry SELECTOR ARGS => #loadAccount #asWord(#range(ARGS, 0, 32)) ~> #setStorage #asWord(#range(ARGS, 0, 32)) #asWord(#range(ARGS, 32, 32)) #asWord(#range(ARGS, 64, 32)) ... </k>
requires SELECTOR ==Int selector ( "store(address,bytes32,bytes32)" )
Otherwise, throw an error for any other call to the Foundry contract.
rule [foundry.call.owise]:
<k> #call_foundry _ _ => #error_foundry ... </k> [owise]
Utils
#loadAccount ACCT
creates a new, empty account forACCT
if it does not already exist. Otherwise, it has no effect
syntax KItem ::= "#loadAccount" Int [klabel(foundry_loadAccount)]
// -----------------------------------------------------------------
rule <k> #loadAccount ACCT => #accessAccounts ACCT ... </k>
<activeAccounts> ACCTS:Set </activeAccounts>
requires ACCT in ACCTS
rule <k> #loadAccount ACCT => #newAccount ACCT ~> #accessAccounts ACCT ... </k>
<activeAccounts> ACCTS:Set </activeAccounts>
requires notBool ACCT in ACCTS
#setBalance ACCTID NEWBAL
sets the balance of a given account.
syntax KItem ::= "#setBalance" Int Int [klabel(foundry_setBalance)]
// -------------------------------------------------------------------
rule <k> #setBalance ACCTID NEWBAL => . ... </k>
<account>
<acctID> ACCTID </acctID>
<balance> _ => NEWBAL </balance>
...
</account>
#setCode ACCTID CODE
sets the code of a given account.
syntax KItem ::= "#setCode" Int ByteArray [klabel(foundry_setCode)]
// -------------------------------------------------------------------
rule <k> #setCode ACCTID CODE => . ... </k>
<account>
<acctID> ACCTID </acctID>
<code> _ => #if #asWord(CODE) ==Int 0 #then .ByteArray:AccountCode #else {CODE}:>AccountCode #fi </code>
...
</account>
#returnNonce ACCTID
takes the nonce of a given account and places it on the<output>
cell.
syntax KItem ::= "#returnNonce" Int [klabel(foundry_returnNonce)]
// -----------------------------------------------------------------
rule <k> #returnNonce ACCTID => . ... </k>
<output> _ => #bufStrict(32, NONCE) </output>
<account>
<acctID> ACCTID </acctID>
<nonce> NONCE </nonce>
...
</account>
#setNonce ACCTID NONCE
takes a given account and a given nonce and update the account accordingly.
syntax KItem ::= "#setNonce" Int Int [klabel(foundry_setNonce)]
// ----------------------------------------------------------------
rule <k> #setNonce ACCTID NONCE => . ... </k>
<account>
<acctID> ACCTID </acctID>
<nonce> _ => NONCE </nonce>
...
</account>
#returnStorage ACCTID LOC
takes a storage value from a given account and places it on the<output>
cell.
syntax KItem ::= "#returnStorage" Int Int [klabel(foundry_returnStorage)]
// -------------------------------------------------------------------------
rule <k> #returnStorage ACCTID LOC => . ... </k>
<output> _ => #bufStrict(32, #lookup(STORAGE, LOC)) </output>
<account>
<acctID> ACCTID </acctID>
<storage> STORAGE </storage>
...
</account>
#setStorage ACCTID LOC VALUE
sets a given value to a given location of an account.
syntax KItem ::= "#setStorage" Int Int Int [klabel(foundry_setStorage)]
// -----------------------------------------------------------------------
rule <k> #setStorage ACCTID LOC VALUE => . ... </k>
<account>
<acctID> ACCTID </acctID>
<storage> STORAGE => STORAGE [ LOC <- VALUE ] </storage>
...
</account>
- selectors for cheat code functions.
rule ( selector ( "assume(bool)" ) => 1281615202 )
rule ( selector ( "deal(address,uint256)" ) => 3364511341 )
rule ( selector ( "etch(address,bytes)" ) => 3033974658 )
rule ( selector ( "warp(uint256)" ) => 3856056066 )
rule ( selector ( "roll(uint256)" ) => 528174896 )
rule ( selector ( "fee(uint256)" ) => 968063664 )
rule ( selector ( "chainId(uint256)" ) => 1078582738 )
rule ( selector ( "coinbase(address)" ) => 4282924116 )
rule ( selector ( "label(address,string)" ) => 3327641368 )
rule ( selector ( "getNonce(address)" ) => 755185067 )
rule ( selector ( "addr(uint256)" ) => 4288775753 )
rule ( selector ( "load(address,bytes32)" ) => 1719639408 )
rule ( selector ( "store(address,bytes32,bytes32)" ) => 1892290747 )
rule ( selector ( "setNonce(address,uint64)" ) => 4175530839 )
endmodule