Skip to content

Commit ff7366b

Browse files
committed
add article about ffi with example
1 parent 21031b5 commit ff7366b

File tree

1 file changed

+111
-0
lines changed

1 file changed

+111
-0
lines changed
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# Interacting with off-chain data using the `ffi` cheatcode
2+
3+
4+
## Introduction
5+
6+
Since the implementation of the HEVM cheat codes in Echidna, it is possible 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.
7+
8+
9+
## A word of caution
10+
11+
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.
12+
13+
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.
14+
15+
The usage of this cheatcode should be extremely limited, well documented, and only reserved for cases where there is not a secure alternative.
16+
17+
18+
## Pre-requisites
19+
20+
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.
21+
22+
To enable the cheatcode, set the 'allowFFI` flag to `true` in your Echidna configuration file:
23+
24+
```yaml
25+
allowFFI: true
26+
```
27+
28+
29+
## Uses
30+
31+
Some of the use cases for `ffi` are:
32+
33+
* 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.
34+
* 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.
35+
* 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.
36+
37+
38+
## Example: Call an off-chain program and read its output
39+
40+
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.
41+
42+
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) helpers for converting.
43+
44+
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.
45+
46+
(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)
47+
48+
```python
49+
import sys
50+
import secrets
51+
from web3 import Web3
52+
from eth_abi import encode
53+
54+
# Usage: python3 script.py number
55+
number = int(sys.argv[1])
56+
57+
# Generate a 10-byte random number
58+
random = int(secrets.token_hex(10), 16)
59+
60+
# Generate the keccak hash of the input value
61+
hashed = Web3.solidity_keccak(['uint256'], [number])
62+
63+
# ABI-encode the output
64+
abi_encoded = encode(['uint256', 'bytes32'], [random, hashed]).hex()
65+
66+
# Make sure that it doesn't print a newline character
67+
print("0x" + abi_encoded, end="")
68+
```
69+
70+
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.
71+
72+
Now let's create the Solidity contract that will be run by Echidna to interact with the previous script.
73+
74+
```solidity
75+
pragma solidity ^0.8.0;
76+
77+
// HEVM helper
78+
import "@crytic/properties/contracts/util/Hevm.sol";
79+
80+
// Helpers to convert uint256 to string
81+
import "@crytic/properties/contracts/util/PropertiesHelper.sol";
82+
83+
contract TestFFI {
84+
function test_ffi(uint256 number) public {
85+
86+
// Prepare the array of executable and parameters
87+
string[] memory inp = new string[](3);
88+
inp[0] = "python3";
89+
inp[1] = "script.py";
90+
inp[2] = PropertiesLibString.toString(number);
91+
92+
// Call the program outside the EVM environment
93+
bytes memory res = hevm.ffi(inp);
94+
95+
// Decode the return values
96+
(uint256 random, bytes32 hashed) = abi.decode(res, (uint256, bytes32));
97+
98+
// Make sure the return value is the expected
99+
bytes32 hashed_solidity = keccak256(abi.encodePacked(number));
100+
assert(hashed_solidity == hashed);
101+
}
102+
}
103+
```
104+
105+
The minimal configuration file for this test is the following:
106+
107+
```yaml
108+
testMode: "assertion"
109+
allowFFI: true
110+
```
111+

0 commit comments

Comments
 (0)