How to use Echidna to test smart contracts
Installation
Echidna can be installed through docker or using the pre-compiled binary.
Echidna through docker
docker pull trailofbits/eth-security-toolboxdocker run -it -v "$PWD":/home/training trailofbits/eth-security-toolbox
The last command runs eth-security-toolbox in a docker that has access to your current directory. You can change the files from your host, and run the tools on the files from the docker
Inside docker, run :
solc-select 0.5.11cd /home/training
Binary
https://github.com/crytic/echidna/releases/tag/v1.4.0.0
Introduction to property-based fuzzing
Echidna is a property-based fuzzer, we described in our previous blogposts (1, 2, 3).
Fuzzing
Fuzzing is a well-known technique in the security community. It consists to generate more or less randomly inputs to find bugs in the program. Fuzzers for traditional software (such as AFL or LibFuzzer) are known to be efficient tools to find bugs.
Beyond the purely random generation of inputs, there are many techniques and strategies to generate good inputs, including:
- Obtain feedback from each execution and guide generation using it. For example, if a newly generated input leads to the discovery of a new path, it can make sense to generate new inputs closes to it.
- Generating the input respecting a structural constraint. For example, if your input contains a header with a checksum, it will make sense to let the fuzzer generates input validating the checksum.
- Using known inputs to generate new inputs: if you have access to a large dataset of valid input, your fuzzer can generate new inputs from them, rather than starting from scratch its generation. These are usually called seeds.
Property-based fuzzing
Echidna belongs to a specific family of fuzzer: property-based fuzzing heavily inspired by QuickCheck. In contrast to classic fuzzer that will try to find crashes, Echidna will try to break user-defined invariants.
In smart contracts, invariants are Solidity functions, that can represent any incorrect or invalid state that the contract can reach, including:
- Incorrect access control: the attacker became the owner of the contract.
- Incorrect state machine: the tokens can be transferred while the contract is paused.
- Incorrect arithmetic: the user can underflow its balance and get unlimited free tokens.
Testing a property with Echidna
We will see how to test a smart contract with Echidna. The target is the following smart contract token.sol:
1contract Token{2 mapping(address => uint) public balances;3 function airdrop() public{4 balances[msg.sender] = 1000;5 }6 function consume() public{7 require(balances[msg.sender]>0);8 balances[msg.sender] -= 1;9 }10 function backdoor() public{11 balances[msg.sender] += 1;12 }13}14सर्व दाखवाकॉपी
We will make the assumption that this token must have the following properties:
- Anyone can have at maximum 1000 tokens
- The token cannot be transferred (it is not an ERC20 token)
Write a property
Echidna properties are Solidity functions. A property must:
- Have no argument
- Return
trueif it is successful - Have its name starting with
echidna
Echidna will:
- Automatically generate arbitrary transactions to test the property.
- Report any transactions leading a property to return
falseor throw an error. - Discard side-effect when calling a property (i.e. if the property changes a state variable, it is discarded after the test)
The following property checks that the caller has no more than 1000 tokens:
1function echidna_balance_under_1000() public view returns(bool){2 return balances[msg.sender] <= 1000;3}4कॉपी
Use inheritance to separate your contract from your properties:
1contract TestToken is Token{2 function echidna_balance_under_1000() public view returns(bool){3 return balances[msg.sender] <= 1000;4 }5 }6कॉपी
token.sol implements the property and inherits from the token.
Initiate a contract
Echidna needs a constructor without argument. If your contract needs a specific initialization, you need to do it in the constructor.
There are some specific addresses in Echidna:
0x00a329c0648769A73afAc7F9381E08FB43dBEA72which calls the constructor.0x10000,0x20000, and0x00a329C0648769a73afAC7F9381e08fb43DBEA70which randomly calls the other functions.
We do not need any particular initialization in our current example, as a result our constructor is empty.
Run Echidna
Echidna is launched with:
echidna-test contract.sol
If contract.sol contains multiple contracts, you can specify the target:
echidna-test contract.sol --contract MyContract
Summary: Testing a property
The following summarizes the run of echidna on our example:
1contract TestToken is Token{2 constructor() public {}3 function echidna_balance_under_1000() public view returns(bool){4 return balances[msg.sender] <= 1000;5 }6 }7कॉपी
echidna-test testtoken.sol --contract TestToken...echidna_balance_under_1000: failed!💥Call sequence, shrinking (1205/5000):airdrop()backdoor()...सर्व दाखवा
Echidna found that the property is violated if backdoor is called.
Filtering functions to call during a fuzzing campaign
We will see how to filter the functions to be fuzzed. The target is the following smart contract:
1contract C {2 bool state1 = false;3 bool state2 = false;4 bool state3 = false;5 bool state4 = false;67 function f(uint x) public {8 require(x == 12);9 state1 = true;10 }1112 function g(uint x) public {13 require(state1);14 require(x == 8);15 state2 = true;16 }1718 function h(uint x) public {19 require(state2);20 require(x == 42);21 state3 = true;22 }2324 function i() public {25 require(state3);26 state4 = true;27 }2829 function reset1() public {30 state1 = false;31 state2 = false;32 state3 = false;33 return;34 }3536 function reset2() public {37 state1 = false;38 state2 = false;39 state3 = false;40 return;41 }4243 function echidna_state4() public returns (bool) {44 return (!state4);45 }46}47सर्व दाखवाकॉपी
This small example forces Echidna to find a certain sequence of transactions to change a state variable. This is hard for a fuzzer (it is recommended to use a symbolic execution tool like Manticore). We can run Echidna to verify this:
echidna-test multi.sol...echidna_state4: passed! 🎉Seed: -3684648582249875403
Filtering functions
Echidna has trouble finding the correct sequence to test this contract because the two reset functions (reset1 and reset2) will set all the state variables to false.
However, we can use a special Echidna feature to either blacklist the reset function or to whitelist only the f, g,
h and i functions.
To blacklist functions, we can use this configuration file:
1filterBlacklist: true2filterFunctions: ["reset1", "reset2"]3
Another approach to filter functions is to list the whitelisted functions. To do that, we can use this configuration file:
1filterBlacklist: false2filterFunctions: ["f", "g", "h", "i"]3
filterBlacklististrueby default.- Filtering will be performed by name only (without parameters). If you have
f()andf(uint256), the filter"f"will match both functions.
Run Echidna
To run Echidna with a configuration file blacklist.yaml:
echidna-test multi.sol --config blacklist.yaml...echidna_state4: failed!💥Call sequence:f(12)g(8)h(42)i()
Echidna will find the sequence of transactions to falsify the property almost immediately.
Summary: Filtering functions
Echidna can either blacklist or whitelist functions to call during a fuzzing campaign using:
1filterBlacklist: true2filterFunctions: ["f1", "f2", "f3"]3
echidna-test contract.sol --config config.yaml...
Echidna starts a fuzzing campaign either blacklisting f1, f2 and f3 or only calling these, according
to the value of the filterBlacklist boolean.
How to test Solidity's assert with Echidna
In this short tutorial, we are going to show how to use Echidna to test assertion checking in contracts. Let's suppose we have a contract like this one:
1contract Incrementor {2 uint private counter = 2**200;34 function inc(uint val) public returns (uint){5 uint tmp = counter;6 counter += val;7 // tmp <= counter8 return (counter - tmp);9 }10}11सर्व दाखवाकॉपी
Write an assertion
We want to make sure that tmp is less or equal than counter after returning its difference. We could write an
Echidna property, but we will need to store the tmp value somewhere. Instead, we could use an assertion like this one:
1contract Incrementor {2 uint private counter = 2**200;34 function inc(uint val) public returns (uint){5 uint tmp = counter;6 counter += val;7 assert (tmp <= counter);8 return (counter - tmp);9 }10}11सर्व दाखवाकॉपी
Run Echidna
To enable the assertion failure testing, create an Echidna configuration file config.yaml:
1checkAsserts: true2
When we run this contract in Echidna, we obtain the expected results:
echidna-test assert.sol --config config.yamlAnalyzing contract: assert.sol:Incrementorassertion in inc: failed!💥Call sequence, shrinking (2596/5000):inc(21711016731996786641919559689128982722488122124807605757398297001483711807488)inc(7237005577332262213973186563042994240829374041602535252466099000494570602496)inc(86844066927987146567678238756515930889952488499230423029593188005934847229952)Seed: 1806480648350826486