Skip to main content

EVM Transaction

The EVMTransaction attestation type enables data collection about a transaction on an EVM chain. The currently supported chain are: ETH, FLR, and SGB. You can learn more about it in the official specification repo.

We will now demonstrate how the FDC protocol can be used to collect the data of a given Ethereum transaction. The transaction we will be observing has the hash 0x4e636c6590b22d8dcdade7ee3b5ae5572f42edb1878f09b3034b2f7c3362ef3c; this is an arbitrary transaction that we acquired from the Sepolia Ethereum testnet explorer.

The same procedure works for all supported sources, ETH, FLR, and SGB. The source then requires only a slight modification; we will remind you of that when it comes up in the guide.

In this guide, we will be following the steps outlined in the FDC Overview.

Our implementation requires handling the FDC voting round finalization process. To manage this, we will create separate scripts in script/fdcExample/EVMTransaction.s.sol that handle different stages of the validation process:

scrip/fdcExample/EVMTransaction.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

import {Script} from "dependencies/forge-std-1.9.5/src/Script.sol";
...

string constant attestationTypeName = "EVMTransaction";
string constant dirPath = "data/";

contract PrepareAttestationRequest is Script {
...
}

contract SubmitAttestationRequest is Script {
...
}

contract RetrieveDataAndProof is Script {
...
}

contract Deploy is Script {
...
}
...

The names of included contracts mostly mirror the steps described in the FDC guide.

To bridge the separate executions of the scripts, we will save the relevant data of each script to a file in the dirPath folder. Each succeeding script will then read that file to load the data.

Prepare request

The JSON request to the verifier is the same form for all attestation types, but the values of the fields differ between them. It contains the following fields.

Required Fields

  • attestationType is the UTF8 hex string encoding of the attestation type name, zero-padded to 32 bytes.
  • sourceId is the UTF8 hex string encoding of the data source identifier name, zero-padded to 32 bytes.
  • requestBody is different for each attestation type.

In the case of EVMTransaction, requestBody is a JSON containing the fields:

  • transactionHash: hash (address) of the observed transaction
  • requiredConfirmations: the depth of the block containing the transaction at which it is considered confirmed, i.e. when the transaction itself is considered confirmed; as uint16
  • provideInput: a bool determining whether the input field is included in the response
  • listEvents: a bool determining whether the events field is included in the response
  • logIndices: an uint32 array of indices of the events to be included in the response; if listEvents is set to false false and this field is not [], the attestation request will fail

Reference Documentation

Example Values

  • transactionHash: 0x4e636c6590b22d8dcdade7ee3b5ae5572f42edb1878f09b3034b2f7c3362ef3c
  • requiredConfirmations: 1
  • provideInput: true
  • listEvents: true
  • logIndices: []

Encoding Functions

To encode values into UTF8 hex:

  • toUtf8HexString: Converts a string to UTF8 hex.
  • toHexString: Zero-right-pads the string to 32 bytes.

These functions are included in the Base library within the example repository, but they can also be defined locally in your contract or script.

The first function translates a string to a UTF8 encoded hex string. The other then zero-right-pads such a string, so that it is 32 bytes long.

scrip/fdcExample/Base.s.sol
function toHexString(
bytes memory data
) public pure returns (string memory) {
bytes memory alphabet = "0123456789abcdef";

bytes memory str = new bytes(2 + data.length * 2);
str[0] = "0";
str[1] = "x";
for (uint i = 0; i < data.length; i++) {
str[2 + i * 2] = alphabet[uint(uint8(data[i] >> 4))];
str[3 + i * 2] = alphabet[uint(uint8(data[i] & 0x0f))];
}
return string(str);
}
scrip/fdcExample/Base.s.sol
function toUtf8HexString(
string memory _string
) internal pure returns (string memory) {
string memory encodedString = toHexString(
abi.encodePacked(_string)
);
uint256 stringLength = bytes(encodedString).length;
require(stringLength <= 64, "String too long");
uint256 paddingLength = 64 - stringLength + 2;
for (uint256 i = 0; i < paddingLength; i++) {
encodedString = string.concat(encodedString, "0");
}
return encodedString;
}

We also define a helper function for formatting data into a JSON string.

scrip/fdcExample/Base.s.sol
function prepareAttestationRequest(
string memory attestationType,
string memory sourceId,
string memory requestBody
) internal view returns (string[] memory, string memory) {
// We read the API key from the .env file
string memory apiKey = vm.envString("VERIFIER_API_KEY");

// Preparing headers
string[] memory headers = prepareHeaders(apiKey);
// Preparing body
string memory body = prepareBody(
attestationType,
sourceId,
requestBody
);

console.log(
"headers: %s",
string.concat("{", headers[0], ", ", headers[1]),
"}\n"
);
console.log("body: %s\n", body);
return (headers, body);
}

function prepareHeaders(
string memory apiKey
) internal pure returns (string[] memory) {
string[] memory headers = new string[](2);
headers[0] = string.concat('"X-API-KEY": ', apiKey);
headers[1] = '"Content-Type": "application/json"';
return headers;
}

function prepareBody(
string memory attestationType,
string memory sourceId,
string memory body
) internal pure returns (string memory) {
return
string.concat(
'{"attestationType": ',
'"',
attestationType,
'"',
', "sourceId": ',
'"',
sourceId,
'"',
', "requestBody": ',
body,
"}"
);
}

In the example repository, these are once again included within the Base library file.

Thus, the part of the script that prepares the verifier request looks like:

scrip/fdcExample/EVMTransaction.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

import {console} from "dependencies/forge-std-1.9.5/src/console.sol";
import {Script} from "dependencies/forge-std-1.9.5/src/Script.sol";
import {Base} from "./Base.s.sol";
...

string constant attestationTypeName = "EVMTransaction";
string constant dirPath = "data/";

contract PrepareAttestationRequest is Script {
using Surl for *;
// Setting request data
string public transactionHash =
"0x4e636c6590b22d8dcdade7ee3b5ae5572f42edb1878f09b3034b2f7c3362ef3c";
string public requiredConfirmations = "1";
string public provideInput = "true";
string public listEvents = "true";
string public logIndices = "[]";
string public sourceName = "testETH"; // Bitcoin chain ID
string public baseSourceName = "eth"; // Part of verifier URL

function prepareRequestBody(
string memory transactionHash,
string memory requiredConfirmations,
string memory provideInput,
string memory listEvents,
string memory logIndices
) private pure returns (string memory) {
return
string.concat(
'{"transactionHash": ',
'"',
transactionHash,
'"',
', "requiredConfirmations": ',
'"',
requiredConfirmations,
'"',
', "provideInput": ',
provideInput,
', "listEvents": ',
listEvents,
', "logIndices": ',
logIndices,
"}"
);
}

function run() external {
// Preparing request data
string memory attestationType = toUtf8HexString(
attestationTypeName
);
string memory sourceId = toUtf8HexString(sourceName);
string memory requestBody = prepareRequestBody(
transactionHash,
requiredConfirmations,
provideInput,
listEvents,
logIndices
);

(string[] memory headers, string memory body) =
prepareAttestationRequest(attestationType, sourceId, requestBody);

...
}
}

...

If you are accessing a different chain, replace the baseSourceName with an appropriate value, flr or sgb.

The code above differs slightly from the starter example. But, if we remove the ellipses ... signifying missing code, we can still run the script.

Because of the console.log commands it will produce JSON strings that represent valid requests; we can then pass this to the interactive verifier to check what the response will be.

We can run the script by calling the following commands in the console.

source .env
forge script script/fdcExample/EVMTransaction.s.sol:PrepareAttestationRequest --private-key $PRIVATE_KEY --rpc-url $COSTON2_RPC_URL --etherscan-api-key $FLARE_API_KEY --broadcast  --ffi

The prerequisite for this is that the .env file is not missing the PRIVATE KEY and COSTON2_RPC_URL values. The script can also access other chains; that can be achieved by replacing the --rpc-url value with COSTON_RPC_URL, FLARE_RPC_URL, or SONGBIRD_RPC_URL.

Post request to verifier

Before submitting address validation requests to the FDC protocol, we first need to prepare and send them to a verifier server. This section walks through the request submission process using the surl package. We place using Surl for *; at the start of our PostRequest contract, and then call its post method on the verifier URL.

scrip/fdcExample/EVMTransaction.s.sol
(, bytes memory data) = url.post(headers, body);

We construct the URL by appending to the verifier address https://fdc-verifiers-testnet.flare.network/ the path verifier/btc/EVMTransaction/prepareRequest. We can do so dynamically with the following code.

scrip/fdcExample/EVMTransaction.s.sol
string memory baseUrl = "https://fdc-verifiers-testnet.flare.network/";
string memory url = string.concat(
baseUrl,
"verifier/",
baseSourceName,
"/",
attestationTypeName,
"/prepareRequest"
);
console.log("url: %s", url);
string memory requestBody = string.concat(
'{"addressStr": "',
addressStr,
'"}'
);

Lastly, we parse the return data from the verifier server. Using the Foundry parseJson shortcode, and a custom struct AttestationResponse, we decode the returned data and extract from it the ABI encoded request.

scrip/fdcExample/Base.s.sol
function parseAttestationRequest(
bytes memory data
) internal pure returns (AttestationResponse memory) {
string memory dataString = string(data);
bytes memory dataJson = vm.parseJson(dataString);

AttestationResponse memory response = abi.decode(
dataJson,
(AttestationResponse)
);

console.log("response status: %s\n", response.status);
console.log("response abiEncodedRequest: ");
console.logBytes(response.abiEncodedRequest);
console.log("\n");

return response;
}
Understanding the abiEncodedRequest.

If everything went right, the abiEncodedRequest should look something like this (minus the line breaks - we split it after the 0x symbol and then after every 64 characters (32 bytes), for the sake of clarity).

0x
45564d5472616e73616374696f6e000000000000000000000000000000000000
7465737445544800000000000000000000000000000000000000000000000000
9d410778cc0b2b8f1b8eaa79cbd0eed5d3be7514dea070e2041ad00a4c6e88f8
0000000000000000000000000000000000000000000000000000000000000020
4e636c6590b22d8dcdade7ee3b5ae5572f42edb1878f09b3034b2f7c3362ef3c
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000001
00000000000000000000000000000000000000000000000000000000000000a0
0000000000000000000000000000000000000000000000000000000000000000

Let's break it down line by line:

  • First line: toUtf8HexString("EVMTransaction")
  • Second line: toUtf8HexString("testETH")
  • Third line: message integrity code (MIC), a hash of the whole response salted with a string "Flare", ensures the integrity of the attestation
  • Remaining lines: ABI encoded EVMTransaction.RequestBody Solidity struct

What this demonstrates is that, with some effort, the abiEncodedRequest can be constructed manually.

We write the abiEncodedRequest to a file (data/EVMTransaction_abiEncodedRequest.txt) to it in the next step.

scrip/fdcExample/EVMTransaction.s.sol
Base.writeToFile(
dirPath,
string.concat(attestationTypeName, "_abiEncodedRequest"),
StringsBase.toHexString(response.abiEncodedRequest),
true
);

Submit request to FDC

This step transitions from offchain request preparation to on-chain interaction with the FDC protocol. Now, we submit the validated request to the blockchain using deployed smart contracts.

Submit request

The entire submission process requires only five key steps:

scrip/fdcExample/Base.s.sol
function submitAttestationRequest(bytes memory abiEncodedRequest) internal {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
IFdcRequestFeeConfigurations fdcRequestFeeConfigurations = ContractRegistry
.getFdcRequestFeeConfigurations();
uint256 requestFee = fdcRequestFeeConfigurations.getRequestFee(
abiEncodedRequest
);
console.log("request fee: %s\n", requestFee);
vm.stopBroadcast();

vm.startBroadcast(deployerPrivateKey);

// address fdcHubAddress = 0x48aC463d7975828989331F4De43341627b9c5f1D;
IFdcHub fdcHub = ContractRegistry.getFdcHub();
console.log("fcdHub address:");
console.log(address(fdcHub));
console.log("\n");

fdcHub.requestAttestation{value: requestFee}(abiEncodedRequest);
vm.stopBroadcast();
}

Step-by-Step Breakdown

  1. Load Private Key The private key is read from the .env file using Foundry's envUint function:
       uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
  1. Obtain Request Fee We retrieve the required requestFee from the FdcRequestFeeConfigurations contract:
        IFdcRequestFeeConfigurations fdcRequestFeeConfigurations = ContractRegistry
.getFdcRequestFeeConfigurations();
uint256 requestFee = fdcRequestFeeConfigurations.getRequestFee(
response.abiEncodedRequest
);

This is done in a separate broadcast to ensure requestFee is available before submitting the request.

  1. Access FdcHub Contract Using the ContractRegistry library (from flare-periphery), we fetch the FdcHub contract:
   IFdcHub fdcHub = ContractRegistry.getFdcHub();
console.log("fcdHub address:");
console.log(address(fdcHub));
console.log("\n");
  1. Submit the Attestation Request We send the attestation request with the required fee:
 fdcHub.requestAttestation{value: requestFee}(response.abiEncodedRequest);
  1. Calculate the Voting Round Number To determine the voting round in which the attestation request is processed, we query the FlareSystemsManager contract:
       // Calculating roundId
IFlareSystemsManager flareSystemsManager = ContractRegistry
.getFlareSystemsManager();

uint32 roundId = flareSystemsManager.getCurrentVotingEpochId();
console.log("roundId: %s\n", Strings.toString(roundId));

This can be done within the existing broadcast or in a new one (as done in the demo repository for better code organization).

Again, we write the roundId to a file (data/EVMTransaction_roundId.txt).

Wait for response

We wait for the round to finalize. This takes no more than 180 seconds.

You can check if the request was submitted successfully on the AttestationRequests page on the Flare Systems Explorer website. To check if the round has been finalized, go to Finalizations page.

If you want to learn more about how the FDC protocol works, check here.

Prepare proof request

We prepare the proof request in a similar manner as in the step Prepare the request, by string concatenation. We import two new variables from the .env file; the URL of a verifier server and the corresponding API key.

scrip/fdcExample/EVMTransaction.s.sol
string memory daLayerUrl = vm.envString("COSTON2_DA_LAYER_URL");
string memory apiKey = vm.envString("X_API_KEY");

Also, by repeatedly using the Foundry shortcode vm.readLine, we read the data, saved to a file in the previous step, to variables.

scrip/fdcExample/EVMTransaction.s.sol
string memory requestBytes = vm.readLine(
string.concat(
dirPath,
attestationTypeName,
"_abiEncodedRequest",
".txt"
)
);
string memory votingRoundId = vm.readLine(
string.concat(
dirPath,
attestationTypeName,
"_votingRoundId",
".txt"
)
);

The code is as follows.

scrip/fdcExample/EVMTransaction.s.sol
contract RetrieveDataAndProof is Script {
using Surl for *;

function run() external {
string memory daLayerUrl = vm.envString("COSTON2_DA_LAYER_URL");
string memory apiKey = vm.envString("X_API_KEY");

string memory requestBytes = vm.readLine(
string.concat(
dirPath,
attestationTypeName,
"_abiEncodedRequest",
".txt"
)
);
string memory votingRoundId = vm.readLine(
string.concat(
dirPath,
attestationTypeName,
"_votingRoundId",
".txt"
)
);

console.log("votingRoundId: %s\n", votingRoundId);
console.log("requestBytes: %s\n", requestBytes);

string[] memory headers = Base.prepareHeaders(apiKey);
string memory body = string.concat(
'{"votingRoundId":',
votingRoundId,
',"requestBytes":"',
requestBytes,
'"}'
);
console.log("body: %s\n", body);
console.log(
"headers: %s",
string.concat("{", headers[0], ", ", headers[1]),
"}\n"
);


...
}
}

Post proof request to DA Layer

We post the proof request to a chosen DA Layer provider server also with the same code as we did in the previous step.

scrip/fdcExample/EVMTransaction.s.sol
string memory url = string.concat(
daLayerUrl,
// "api/v0/fdc/get-proof-round-id-bytes"
"api/v1/fdc/proof-by-request-round-raw"
);
console.log("url: %s\n", url);

(, bytes memory data) = Base.postAttestationRequest(url, headers, body);

Parsing the returned data requires the definition of an auxiliary struct.

scrip/fdcExample/Base.s.sol
struct ParsableProof {
bytes32 attestationType;
bytes32[] proofs;
bytes responseHex;
}

The field attestationType holds the UTF8 encoded hex string of the attestation type name, padded to 32 bytes. Thus, it should match the value of the attestationType parameter in the Prepare the request step. In our case, that value is 0x45564d5472616e73616374696f6e000000000000000000000000000000000000.

The array proofs holds the Merkle proofs of our attestation request.

Lastly, responseHex is the ABI encoding of the chosen attestation type response struct. In this case, it is the IEVMTransaction.Response struct. We retrieve this data as follows.

scrip/fdcExample/EVMTransaction.s.sol
bytes memory dataJson = parseData(data);
ParsableProof memory proof = abi.decode(dataJson, (ParsableProof));

IEVMTransaction.Response memory proofResponse = abi.decode(
proof.responseHex,
(IEVMTransaction.Response)
);
An example complete proof response and decoded IEVMTransaction.Response.

An example DA Layer response for a request using the data provided in this example is:

{
response_hex: '0x
0000000000000000000000000000000000000000000000000000000000000020
45564d5472616e73616374696f6e000000000000000000000000000000000000
7465737445544800000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000e6c2c
0000000000000000000000000000000000000000000000000000000067724b20
00000000000000000000000000000000000000000000000000000000000000c0
0000000000000000000000000000000000000000000000000000000000000180
4e636c6590b22d8dcdade7ee3b5ae5572f42edb1878f09b3034b2f7c3362ef3c
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000001
00000000000000000000000000000000000000000000000000000000000000a0
0000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000070acc6
0000000000000000000000000000000000000000000000000000000067724b20
00000000000000000000000070ad32b82b4fe2821c798e628d93645218e2a806
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000003fc91a3afd70395cd496c647d5a6cc9d4b2b7fad
00000000000000000000000000000000000000000000000000d8b72d434c8000
0000000000000000000000000000000000000000000000000000000000000120
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000520
00000000000000000000000000000000000000000000000000000000000003c5
3593564c00000000000000000000000000000000000000000000000000000000
0000006000000000000000000000000000000000000000000000000000000000
000000a000000000000000000000000000000000000000000000000000000000
6772521a00000000000000000000000000000000000000000000000000000000
000000040b000604000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
0000000400000000000000000000000000000000000000000000000000000000
0000008000000000000000000000000000000000000000000000000000000000
000000e000000000000000000000000000000000000000000000000000000000
0000020000000000000000000000000000000000000000000000000000000000
0000028000000000000000000000000000000000000000000000000000000000
0000004000000000000000000000000000000000000000000000000000000000
0000000200000000000000000000000000000000000000000000000000d8b72d
434c800000000000000000000000000000000000000000000000000000000000
0000010000000000000000000000000000000000000000000000000000000000
0000000200000000000000000000000000000000000000000000000000d8b72d
434c800000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
000000a000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
0000002bfff9976782d46cc05630d1f6ebab18b2324d6b140001f41c7d4b196c
b0c7b01d743fbc6116a902379c72380000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
000000600000000000000000000000001c7d4b196cb0c7b01d743fbc6116a902
379c7238000000000000000000000000e49acc3b16c097ec88dc9352ce4cd57a
b7e35b9500000000000000000000000000000000000000000000000000000000
0000001900000000000000000000000000000000000000000000000000000000
000000600000000000000000000000001c7d4b196cb0c7b01d743fbc6116a902
379c723800000000000000000000000070ad32b82b4fe2821c798e628d936452
18e2a80600000000000000000000000000000000000000000000000000000000
ad2090e40c000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000006
00000000000000000000000000000000000000000000000000000000000000c0
0000000000000000000000000000000000000000000000000000000000000200
0000000000000000000000000000000000000000000000000000000000000360
00000000000000000000000000000000000000000000000000000000000004c0
00000000000000000000000000000000000000000000000000000000000006a0
0000000000000000000000000000000000000000000000000000000000000800
000000000000000000000000000000000000000000000000000000000000003f
000000000000000000000000fff9976782d46cc05630d1f6ebab18b2324d6b14
00000000000000000000000000000000000000000000000000000000000000a0
0000000000000000000000000000000000000000000000000000000000000100
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000002
e1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c
0000000000000000000000003fc91a3afd70395cd496c647d5a6cc9d4b2b7fad
0000000000000000000000000000000000000000000000000000000000000020
00000000000000000000000000000000000000000000000000d8b72d434c8000
0000000000000000000000000000000000000000000000000000000000000040
0000000000000000000000001c7d4b196cb0c7b01d743fbc6116a902379c7238
00000000000000000000000000000000000000000000000000000000000000a0
0000000000000000000000000000000000000000000000000000000000000120
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000003
ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
0000000000000000000000003289680dd4d6c10bb19b899729cda5eef58aeff1
0000000000000000000000003fc91a3afd70395cd496c647d5a6cc9d4b2b7fad
0000000000000000000000000000000000000000000000000000000000000020
00000000000000000000000000000000000000000000000000000000ae6dcda8
0000000000000000000000000000000000000000000000000000000000000041
000000000000000000000000fff9976782d46cc05630d1f6ebab18b2324d6b14
00000000000000000000000000000000000000000000000000000000000000a0
0000000000000000000000000000000000000000000000000000000000000120
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000003
ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
0000000000000000000000003fc91a3afd70395cd496c647d5a6cc9d4b2b7fad
0000000000000000000000003289680dd4d6c10bb19b899729cda5eef58aeff1
0000000000000000000000000000000000000000000000000000000000000020
00000000000000000000000000000000000000000000000000d8b72d434c8000
0000000000000000000000000000000000000000000000000000000000000042
0000000000000000000000003289680dd4d6c10bb19b899729cda5eef58aeff1
00000000000000000000000000000000000000000000000000000000000000a0
0000000000000000000000000000000000000000000000000000000000000120
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000003
c42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67
0000000000000000000000003fc91a3afd70395cd496c647d5a6cc9d4b2b7fad
0000000000000000000000003fc91a3afd70395cd496c647d5a6cc9d4b2b7fad
00000000000000000000000000000000000000000000000000000000000000a0
ffffffffffffffffffffffffffffffffffffffffffffffffffffffff51923258
00000000000000000000000000000000000000000000000000d8b72d434c8000
00000000000000000000000000000000000011d79ac448fce087b0605d7423c8
000000000000000000000000000000000000000000000000002231596d817570
000000000000000000000000000000000000000000000000000000000002925f
0000000000000000000000000000000000000000000000000000000000000043
0000000000000000000000001c7d4b196cb0c7b01d743fbc6116a902379c7238
00000000000000000000000000000000000000000000000000000000000000a0
0000000000000000000000000000000000000000000000000000000000000120
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000003
ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
0000000000000000000000003fc91a3afd70395cd496c647d5a6cc9d4b2b7fad
000000000000000000000000e49acc3b16c097ec88dc9352ce4cd57ab7e35b95
0000000000000000000000000000000000000000000000000000000000000020
00000000000000000000000000000000000000000000000000000000006fa26f
0000000000000000000000000000000000000000000000000000000000000044
0000000000000000000000001c7d4b196cb0c7b01d743fbc6116a902379c7238
00000000000000000000000000000000000000000000000000000000000000a0
0000000000000000000000000000000000000000000000000000000000000120
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000003
ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
0000000000000000000000003fc91a3afd70395cd496c647d5a6cc9d4b2b7fad
00000000000000000000000070ad32b82b4fe2821c798e628d93645218e2a806
0000000000000000000000000000000000000000000000000000000000000020
00000000000000000000000000000000000000000000000000000000adfe2b39',
attestation_type: '0x45564d5472616e73616374696f6e000000000000000000000000000000000000',
proof: [
'0x9251c0e3047688af1305daf61f2b757527b731e7d1fad3c71d08734772fbebeb',
'0xad5fdf0f8cb6bc42cdab5affb8f03a1fadaf1ef60875af76344b7ca3ab694c9b',
'0xead06bac3be86604034e138784c86f0b14f2481c001e31e17d03c185488033dc'
]
}

The proof field is dependent on the round in which the attestation request was submitted; it contains proofs for all of the requests submitted in that round. In the case of a single attestation request it is an empty list [] (the proof is the merkle root itself).

The decoded IEVMTransaction.Response struct is:

[
attestationType: '0x45564d5472616e73616374696f6e000000000000000000000000000000000000',
sourceId: '0x7465737445544800000000000000000000000000000000000000000000000000',
votingRound: '945196',
lowestUsedTimestamp: '1735543584',
requestBody: [
'0x4e636c6590b22d8dcdade7ee3b5ae5572f42edb1878f09b3034b2f7c3362ef3c',
'1',
true,
true,
[],
transactionHash: '0x4e636c6590b22d8dcdade7ee3b5ae5572f42edb1878f09b3034b2f7c3362ef3c',
requiredConfirmations: '1',
provideInput: true,
listEvents: true,
logIndices: []
],
responseBody: [
'7384262',
'1735543584',
'0x70Ad32B82B4FE2821C798e628d93645218E2A806',
false,
'0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD',
'61000000000000000',
'0x3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000006772521a00000000000000000000000000000000000000000000000000000000000000040b000604000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000d8b72d434c80000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000d8b72d434c8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bfff9976782d46cc05630d1f6ebab18b2324d6b140001f41c7d4b196cb0c7b01d743fbc6116a902379c723800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000001c7d4b196cb0c7b01d743fbc6116a902379c7238000000000000000000000000e49acc3b16c097ec88dc9352ce4cd57ab7e35b95000000000000000000000000000000000000000000000000000000000000001900000000000000000000000000000000000000000000000000000000000000600000000000000000000000001c7d4b196cb0c7b01d743fbc6116a902379c723800000000000000000000000070ad32b82b4fe2821c798e628d93645218e2a80600000000000000000000000000000000000000000000000000000000ad2090e40c',
'1',
[ ... ],
blockNumber: '7384262',
timestamp: '1735543584',
sourceAddress: '0x70Ad32B82B4FE2821C798e628d93645218E2A806',
isDeployment: false,
receivingAddress: '0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD',
value: '61000000000000000',
input: '0x3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000006772521a00000000000000000000000000000000000000000000000000000000000000040b000604000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000d8b72d434c80000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000d8b72d434c8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bfff9976782d46cc05630d1f6ebab18b2324d6b140001f41c7d4b196cb0c7b01d743fbc6116a902379c723800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000001c7d4b196cb0c7b01d743fbc6116a902379c7238000000000000000000000000e49acc3b16c097ec88dc9352ce4cd57ab7e35b95000000000000000000000000000000000000000000000000000000000000001900000000000000000000000000000000000000000000000000000000000000600000000000000000000000001c7d4b196cb0c7b01d743fbc6116a902379c723800000000000000000000000070ad32b82b4fe2821c798e628d93645218e2a80600000000000000000000000000000000000000000000000000000000ad2090e40c',
status: '1',
events: [ ... ]
]
]

Verify proof

FDC optimizes on-chain storage costs by implementing a hybrid data verification system. Instead of storing complete datasets on-chain, it stores only Merkle proofs, while maintaining the actual data through trusted offchain providers. This approach significantly reduces gas costs while preserving data integrity.

When requested, data providers supply the original data along with its corresponding Merkle proof. The protocol verifies data authenticity by comparing the provided Merkle proof against the on-chain Merkle root. A successful match confirms the data's integrity and authenticity within the FDC system.

While data verification is optional if you trust your data provider, FDC ensures transparency by making verification possible at any time. This capability is crucial for maintaining system integrity and allowing users to independently verify data when needed, particularly in production environments.

FDC provides verification functionality through the FdcVerification contract. To verify address validity, we first format our data using the IEVMTransaction.Proof struct, which contains both the Merkle proof and the response data.

scrip/fdcExample/EVMTransaction.s.sol
IEVMTransaction.Proof memory _proof = IEVMTransaction.Proof(
proof.proofs,
proofResponse
);

We then access the FdcVerification contract through the ContractRegistry, and feed it the proof. If we proof is valid, the function verifyEVMTransaction will return true, otherwise false. As before, we wrap the whole thing into a broadcast environment, using the PRIVATE_KEY variable from our .env file.

uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);

bool isValid = ContractRegistry
.getFdcVerification()
.verifyEVMTransaction(proof);
console.log("proof is valid: %s\n", StringsBase.toString(isValid));

vm.stopBroadcast();

In actuality, we will only verify the proof within a deployed contract, which we will define in the next step. What we will do here instead is, we will save the proof to a file so that it can be later loaded into a variable. The code that does this is as follows.

scrip/fdcExample/EVMTransaction.s.sol
Base.writeToFile(
dirPath,
string.concat(attestationTypeName, "_proof"),
StringsBase.toHexString(abi.encode(_proof)),
true
);

Use the data

We will now define a simple contract, that will demonstrate how the data can be used onchain. The contract will receive data and proof of an Ethereum transaction, and store all token transfers contained into an array of TokenTransfer structs. It will do so only if the transaction is valid.

src/fdcExample/EVMTransaction.sol
struct TokenTransfer {
address from;
address to;
uint256 value;
}

First, we define an interface that the contract will inherit from. We do so, so that we may contact the contract later through a script.

src/fdcExample/EVMTransaction.sol
interface ITransferEventListener {
function collectTransferEvents(
IEVMTransaction.Proof calldata _transaction
) external;
}

The interface exposes the only function the script will call, collectTransferEvents. We now define the contract as follows.

src/fdcExample/EVMTransaction.sol
contract TransferEventListener is ITransferEventListener {
TokenTransfer[] public tokenTransfers;
address public USDC_CONTRACT = 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238; // USDC contract address on sepolia

function isEVMTransactionProofValid(
IEVMTransaction.Proof calldata transaction
) public view returns (bool) {
// Use the library to get the verifier contract and verify that this transaction was proved by state connector
IFdcVerification fdc = ContractRegistry.getFdcVerification();
console.log("transaction: %s\n", FdcStrings.toJsonString(transaction));
return true;
//return fdc.verifyEVMTransaction(transaction);
}

function collectTransferEvents(
IEVMTransaction.Proof calldata _transaction
) external {
// 1. FDC Logic
// Check that this EVMTransaction has indeed been confirmed by the FDC
require(
isEVMTransactionProofValid(_transaction),
"Invalid transaction proof"
);

// 2. Business logic
// Go through all events
for (
uint256 i = 0;
i < _transaction.data.responseBody.events.length;
i++
) {
// Get current event
IEVMTransaction.Event memory _event = _transaction
.data
.responseBody
.events[i];

// Disregard events that are not from the USDC contract
if (_event.emitterAddress != USDC_CONTRACT) {
continue;
}

// Disregard non Transfer events
if (
_event.topics.length == 0 || // No topics
// The topic0 doesn't match the Transfer event
_event.topics[0] !=
keccak256(abi.encodePacked("Transfer(address,address,uint256)"))
) {
continue;
}

// We now know that this is a Transfer event from the USDC contract - and therefore know how to decode topics and data
// Topic 1 is the sender
address sender = address(uint160(uint256(_event.topics[1])));
// Topic 2 is the receiver
address receiver = address(uint160(uint256(_event.topics[2])));
// Data is the amount
uint256 value = abi.decode(_event.data, (uint256));

// Add the transfer to the list
tokenTransfers.push(
TokenTransfer({from: sender, to: receiver, value: value})
);
}
}

function getTokenTransfers()
external
view
returns (TokenTransfer[] memory)
{
TokenTransfer[] memory result = new TokenTransfer[](
tokenTransfers.length
);
for (uint256 i = 0; i < tokenTransfers.length; i++) {
result[i] = tokenTransfers[i];
}
return result;
}
}

We deploy the contract through a simple script. The script creates a new TransferEventListener contract, and writes its address to a file (data/EVMTransaction_listenerAddress.txt).

scrip/fdcExample/EVMTransaction.s.sol
contract DeployContract is Script {
function run() external {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
TransferEventListener listener = new TransferEventListener();
address listenerAddress = address(listener);
vm.stopBroadcast();

Base.writeToFile(
dirPath,
string.concat(attestationTypeName, "_listenerAddress"),
StringsBase.toHexString(abi.encodePacked(listenerAddress)),
true
);
}
}

We deploy the contract with the following console command.

forge script script/fdcExample/EVMTransaction.s.sol:DeployContract --private-key $PRIVATE_KEY --rpc-url $COSTON2_RPC_URL --etherscan-api-key $FLARE_API_KEY --broadcast --verify --ffi

Lastly, we define a script that interacts with the above contract. It first reads the ABI encoded proof data, and the contract address, from files. Then, it connects to the above contract at the saved address (this is why we require the interface). With that, it is able to call the getTokenTransfers method of the contract.

script/fdcExample/EVMTransaction.s.sol
contract InteractWithContract is Script {
function run() external {
string memory addressString = vm.readLine(
string.concat(
dirPath,
attestationTypeName,
"_listenerAddress",
".txt"
)
);
address listenerAddress = vm.parseAddress(addressString);
string memory proofString = vm.readLine(
string.concat(dirPath, attestationTypeName, "_proof", ".txt")
);
bytes memory proofBytes = vm.parseBytes(proofString);
IEVMTransaction.Proof memory proof = abi.decode(
proofBytes,
(IEVMTransaction.Proof)
);
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
ITransferEventListener listener = ITransferEventListener(
listenerAddress
);
console.logAddress(address(listener));
listener.collectTransferEvents(proof);
vm.stopBroadcast();
}
}

We run this script with the console command:

forge script script/fdcExample/EVMTransaction.s.sol:InteractWithContract --private-key $PRIVATE_KEY --rpc-url $COSTON2_RPC_URL --etherscan-api-key $FLARE_API_KEY --broadcast --ffi