We have a protobuf description of an ABI value, so something like:
```
message VboolPB {
required bool = 1;
}
message VUIntPB {
required uint64 = 1;
}
...
message AbiValuePB {
oneof types_oneof {
VboolPB = 1;
VUIntPB = 2;
...
}
}
```
So we can for example represent a value of type ``(uint256,uint256[])`` with values ``(42, [1, 2, 3, 4])``.
During fuzzing we will end up having an Entry point that's called with a message of type ``AbiTypePB``.
This ends up in a C++ entry point like:
```
DEFINE_PROTO_FUZZER(AbiValue const& _input)
{
std::vector<char> binaryProtobufMessage = _input.serialize();
// or however this is actually done
// it may even be possible to catch the binary message before it
// is de-serialized somewhere "before" "DEFINE_PROTO_FUZZER"
std::string result = executeHaskellProcess("/path/to/haskell/encoder/binary", toHex(binaryProtobufMessage));
// so we just pass the binary message as argument to a haskell binary.
// We receive a hex string of the binary encoding of _input.
/*
* The following is supposed to generate a contract from
* message _input, that in the example case looks like this:
* contract C {
* function test(uint256 a,uint256[] b) public returns (int) {
* if (a != 42) return 1;
* if (b.length != 4) return 2;
* if (b[0] != 1) return 3;
* if (b[1] != 2) return 4;
* if (b[2] != 3) return 5;
* if (b[3] != 4) return 6;
* return 0;
* }
* }
*/
std::string contract_source = ProtobufConverter{}.generateContractForAbiValue(_input);
// now we compile the contract contract_source and pass it fromHex(result) as input.
// The contract will return 0 on success or an error code otherwise.
}
```
This would effectively fuzz the abi decoder for validly encoded data.
===========================
Next to fuzz the encoder we can do the following (assuming the same input):
```
DEFINE_PROTO_FUZZER(AbiValue const& _input)
{
std::vector<char> binaryProtobufMessage = _input.serialize();
/*
* The following is supposed to generate a contract
* from message _input, that in the example case looks like this:
*
* contract C {
* function f(uint256 a,uint256[] b) public returns (bytes memory msg) {
* return bytes(msg.data[4:]);
* return 0;
* }
* function test(bytes calldata verifiedEncoding) external returns (int) {
* uint256 a = 42;
* bytes b = new bytes(4);
* b[0] = 1;
* b[1] = 2;
* b[2] = 3;
* b[3] = 4;
* bytes memory solidityEncoding = this.f(a, b);
* if (verifiedEncoding.length != solidityEncoding.length) return 1;
* for (int i = 0; i < verifiedEncoding.length; i++)
* if (verifiedEncoding[i] != solidityEncoding[i]) return 1000 + i;
* return 0;
* }
* }
*/
std::string contract_source = ProtobufConverter{}.generateContractForAbiValue(_input);
// now we compile the contract contract_source and pass it fromHex(result) as input.
// The contract will return 0 on success or an error code otherwise.
}
```
===========================
But even better we can actually try fuzzing on *invalid* data:
```
DEFINE_PROTO_FUZZER(AbiValue const& _input)
{
std::vector<char> binaryProtobufMessage = _input.serialize();
std::string encoding = executeHaskellProcess("/path/to/haskell/encoder/binary", toHex(binaryProtobufMessage));
flipRandomBits(encoding);
// So we flip random bits in result - what we get may still be
// a valid encoding, but also may no longer be valid!
// Alternatively we can just choose an encoding at random instead.
// Maybe we can actually do both.
std::string result = executeHaskellProcess("/path/to/haskell/decoder/binary", toHex(binaryProtobufMessage), toHex(encoding));
// So we call a second haskell binary that will just return 1
// if ``encoding`` is a valid encoding of the same type as
// the AbiValue in ``encoding``.
/*
* The following is supposed to generate a contract from message _input, that in the example case looks like this:
* contract C {
* function test(uint256 a,uint256[] b) public returns (int) {
* return 0;
* }
* }
*/
std::string contract_source = ProtobufConverter{}.generateContractForAbiValue(_input);
// Now we compile the contract contract_source and pass it fromHex(encoding) as input.
// Now if ``result == "1"`` we expect the call to return 0.
// Now if ``result == "0"`` we expect the call to revert.
}
```
Further notes
===================
- Only the call to the solidity compiler would be instrumented (if at all), not the calls to the haskell binaries.
- The main challenge in this, of course, would be to write the haskell binaries /path/to/haskell/encoder/binary and /path/to/haskell/decoder/binary :-).
- Having written the above I realize: actually it doesn't even need to be standalone haskell binaries anymore - a C to haskell calls with byte strings should be enough for this :-).