|
| 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