Skip to content

Commit 339acd7

Browse files
authored
Merge pull request #273 from crytic/dev-ffi-cheatcode
Add FFI cheatcode example
2 parents 6e1ef9c + 67ad653 commit 339acd7

File tree

3 files changed

+106
-0
lines changed

3 files changed

+106
-0
lines changed

SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
- [How to seed Echidna with unit tests](./program-analysis/echidna/advanced/end-to-end-testing.md)
8383
- [How to fuzz contracts with external libraries](./program-analysis/echidna/advanced/working-with-libraries.md)
8484
- [Understanding and using `multi-abi`](./program-analysis/echidna/advanced/using-multi-abi.md)
85+
- [Interacting with off-chain data via FFI cheatcode](./program-analysis/echidna/advanced/interacting-with-offchain-data-via-ffi.md)
8586
- [Fuzzing tips](./program-analysis/echidna/fuzzing_tips.md)
8687
- [Frequently Asked Questions](./program-analysis/echidna/frequently_asked_questions.md)
8788
- [Exercises](./program-analysis/echidna/exercises/README.md)

program-analysis/echidna/advanced/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@
99
- [How to Use Hevm Cheats to Test Permit](./hevm-cheats-to-test-permit.md): Find out how to test code that relies on ecrecover signatures by using hevm cheat codes.
1010
- [How to Seed Echidna with Unit Tests](./end-to-end-testing.md): Discover how to use existing unit tests to seed Echidna.
1111
- [Understanding and Using `multi-abi`](./using-multi-abi.md): Learn what `multi-abi` testing is and how to utilize it effectively.
12+
- [Interacting with off-chain data via FFI cheatcode](./interacting-with-offchain-data-via-ffi.md): Using the `ffi` cheatcode as a way of communicating with the operating system.
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# Interacting with off-chain data using the `ffi` cheatcode
2+
3+
## Introduction
4+
5+
It is possible for Echidna to interact with off-chain data by means of the `ffi` cheatcode. This function allows the caller to execute an arbitrary command on the system running Echidna and read its output, enabling the possibility of getting external data into a fuzzing campaign.
6+
7+
## A word of caution
8+
9+
In general, the usage of cheatcodes is not encouraged, since manipulating the EVM execution environment can lead to unpredictable results and false positives or negatives in fuzzing tests.
10+
11+
This piece of advice becomes more critical when using `ffi`. This cheatcode basically allows arbitrary code execution on the host system, so it's not just the EVM execution environment that can be manipulated. Running malicious or untrusted tests with `ffi` can have disastrous consequences.
12+
13+
The usage of this cheatcode should be extremely limited, well documented, and only reserved for cases where there is not a secure alternative.
14+
15+
## Pre-requisites
16+
17+
If reading the previous section didn't scare you enough and you still want to use `ffi`, you will need to explicitly tell Echidna to allow the cheatcode in the tests. This safety measure makes sure you don't accidentally execute `ffi` code.
18+
19+
To enable the cheatcode, set the `allowFFI` flag to `true` in your Echidna configuration file:
20+
21+
```yaml
22+
allowFFI: true
23+
```
24+
25+
## Uses
26+
27+
Some of the use cases for `ffi` are:
28+
29+
- Making prices or other information available on-chain during a fuzzing campaign. For example, you can use `ffi` to feed an oracle with "live" data.
30+
- Get randomness in a test. As you know, there is no randomness source on-chain, so using this cheatcode you can get a random value from the device running the fuzz tests.
31+
- Integrate with algorithms not ported to Solidity language, or perform comparisons between two implementations. Some examples for this item include signing and hashing, or custom calculations algorithms.
32+
33+
## Example: Call an off-chain program and read its output
34+
35+
This example will show how to create a simple call to an external executable, passing some values as parameters, and read its output. Keep in mind that the return values of the called program should be an abi-encoded data chunk that can be later decoded via `abi.decode()`. No newlines are allowed in the return values.
36+
37+
Before digging into the example, there's something else to keep in mind: When interacting with external processes, you will need to convert from Solidity data types to string, to pass values as arguments to the off-chain executable. You can use the [crytic/properties](https://github.com/crytic/properties) `toString` [helpers](https://github.com/crytic/properties/blob/main/contracts/util/PropertiesHelper.sol#L447) for converting.
38+
39+
For the example we will be creating a python example script that returns a random `uint256` value and a `bytes32` hash calculated from an integer input value. This doesn't represent a "useful" use case, but will be enough to show how the `ffi` cheatcode is used. Finally, we won't perform sanity checks for data types or values, we will just assume the input data will be correct.
40+
41+
This script was tested with Python 3.11, Web3 6.0.0 and eth-abi 4.0.0. Some functions had different names in prior versions of the libraries.
42+
43+
```python
44+
import sys
45+
import secrets
46+
from web3 import Web3
47+
from eth_abi import encode
48+
49+
# Usage: python3 script.py number
50+
number = int(sys.argv[1])
51+
52+
# Generate a 10-byte random number
53+
random = int(secrets.token_hex(10), 16)
54+
55+
# Generate the keccak hash of the input value
56+
hashed = Web3.solidity_keccak(['uint256'], [number])
57+
58+
# ABI-encode the output
59+
abi_encoded = encode(['uint256', 'bytes32'], [random, hashed]).hex()
60+
61+
# Make sure that it doesn't print a newline character
62+
print("0x" + abi_encoded, end="")
63+
```
64+
65+
You can test this program with various inputs and see what the output is. If it works correctly, the program should output a 512-bit hex string that is the ABI-encoded representation of a 256-bit integer followed by a bytes32.
66+
67+
Now let's create the Solidity contract that will be run by Echidna to interact with the previous script.
68+
69+
```solidity
70+
pragma solidity ^0.8.0;
71+
72+
// HEVM helper
73+
import "@crytic/properties/contracts/util/Hevm.sol";
74+
75+
// Helpers to convert uint256 to string
76+
import "@crytic/properties/contracts/util/PropertiesHelper.sol";
77+
78+
contract TestFFI {
79+
function test_ffi(uint256 number) public {
80+
// Prepare the array of executable and parameters
81+
string[] memory inp = new string[](3);
82+
inp[0] = "python3";
83+
inp[1] = "script.py";
84+
inp[2] = PropertiesLibString.toString(number);
85+
86+
// Call the program outside the EVM environment
87+
bytes memory res = hevm.ffi(inp);
88+
89+
// Decode the return values
90+
(uint256 random, bytes32 hashed) = abi.decode(res, (uint256, bytes32));
91+
92+
// Make sure the return value is the expected
93+
bytes32 hashed_solidity = keccak256(abi.encodePacked(number));
94+
assert(hashed_solidity == hashed);
95+
}
96+
}
97+
```
98+
99+
The minimal configuration file for this test is the following:
100+
101+
```yaml
102+
testMode: "assertion"
103+
allowFFI: true
104+
```

0 commit comments

Comments
 (0)