|
| 1 | +--- |
| 2 | +title: Cross-Chain FDC |
| 3 | +slug: cross-chain-fdc |
| 4 | +authors: [anthonyamadia] |
| 5 | +description: Use the FDC data on a non-Flare, like the XRPL EVM Sidechain. |
| 6 | +tags: [fdc, foundry] |
| 7 | +keywords: |
| 8 | + [ethereum, flare-data-connector, evm, cross-chain, web2json, relay, xrpl] |
| 9 | +sidebar_position: 11 |
| 10 | +--- |
| 11 | + |
| 12 | +import AvailableTestnet from "../_available_testnet.mdx"; |
| 13 | + |
| 14 | +This guide demonstrates a powerful cross-chain workflow using the Flare Data Connector (FDC). |
| 15 | +We will fetch data from a public Web2 API using the [Web2Json](/fdc/attestation-types/web-2-json) attestation type on a Flare network (Coston2) and then use the resulting proof to trigger a state change on a different EVM-compatible blockchain, the XRPL EVM Sidechain Testnet. |
| 16 | + |
| 17 | +The key to this cross-chain interaction is the `Relay` contract. |
| 18 | +For a non-Flare chain to verify FDC proofs, the Merkle root of each finalized voting round on Flare must be transmitted to the target chain. |
| 19 | +This is handled by the `Relay` contract, which must be deployed on the target chain. |
| 20 | +For the XRPL EVM Sidechain, Flare provides a relayer service where a backend script listens for `Relay` transactions on Flare and copies them to the `Relay` contract on the target chain. |
| 21 | +This allows a verifier contract on the target chain to confirm the validity of FDC proofs without trusting a centralized intermediary. |
| 22 | + |
| 23 | +Before running any code, check if the `Relay` contract is already being relayed, or submit a request to the Flare team. |
| 24 | + |
| 25 | +<AvailableTestnet /> |
| 26 | + |
| 27 | +For this example, we will: |
| 28 | + |
| 29 | +1. Deploy a custom verification infrastructure to the XRPL EVM Sidechain Testnet. |
| 30 | +2. Request data about a Star Wars character from the `swapi.info` API on the Coston2 Testnet. |
| 31 | +3. Submit this request to the FDC on Coston2. |
| 32 | +4. Retrieve the finalized proof and use it to call a consumer contract on the XRPL EVM Sidechain, which verifies the proof and stores the character's data on-chain. |
| 33 | + |
| 34 | +:::info |
| 35 | +The code used in this guide is available in the [Flare Foundry starter repository](https://github.com/flare-foundation/flare-foundry-starter). |
| 36 | +::: |
| 37 | + |
| 38 | +### 1. Deploy Infrastructure on Target Chain (XRPL EVM Sidechain) |
| 39 | + |
| 40 | +This workflow requires a one-time deployment of a persistent infrastructure on the target chain. These core contracts manage contract addresses and proof verification. |
| 41 | + |
| 42 | +- **`AddressUpdater`**: A governance-controlled contract that maintains a registry of key contract addresses, such as the `Relay`. |
| 43 | +- **`FdcVerification`**: A custom verification contract that retrieves the trusted Merkle root from the `Relay` to verify FDC proofs on the target chain. The protocol ID (e.g., 200 for Coston2) configured in this contract must match the FDC instance on the source Flare network. |
| 44 | + |
| 45 | +The `DeployInfrastructure` script handles this setup. On a non-Flare chain, the `Relay` address is a known, pre-deployed address that must be provided in your `.env` file. |
| 46 | + |
| 47 | +```solidity title="script/CrossChainFdc.s.sol" |
| 48 | +// Deploys the persistent CORE INFRASTRUCTURE contracts to the target non-Flare chain. |
| 49 | +contract DeployInfrastructure is Script { |
| 50 | + function run() external { |
| 51 | + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); |
| 52 | + address governance = vm.addr(deployerPrivateKey); |
| 53 | +
|
| 54 | + // On a non-Flare chain, the Relay address is a known, pre-deployed address. |
| 55 | + // It must be provided in the .env file. |
| 56 | + address relayAddress = vm.envAddress("RELAY_ADDRESS"); |
| 57 | + console.log("Using Relay address from .env file:", relayAddress); |
| 58 | + require(relayAddress != address(0), "Error: RELAY_ADDRESS not set in .env or invalid."); |
| 59 | +
|
| 60 | + vm.startBroadcast(deployerPrivateKey); |
| 61 | +
|
| 62 | + AddressUpdater addressUpdater = new AddressUpdater(governance); |
| 63 | + // The protocol ID must match the one used by the FDC instance on the Flare network (e.g., 200 for Coston2). |
| 64 | + FdcVerification fdcVerification = new FdcVerification(address(addressUpdater), 200); |
| 65 | +
|
| 66 | + string[] memory names = new string[](2); |
| 67 | + address[] memory addresses = new address[](2); |
| 68 | +
|
| 69 | + names[0] = "Relay"; |
| 70 | + addresses[0] = relayAddress; |
| 71 | +
|
| 72 | + names[1] = "AddressUpdater"; |
| 73 | + addresses[1] = address(addressUpdater); |
| 74 | +
|
| 75 | + addressUpdater.addOrUpdateContractNamesAndAddresses(names, addresses); |
| 76 | +
|
| 77 | + IIAddressUpdatable[] memory contractsToUpdate = new IIAddressUpdatable[](1); |
| 78 | + contractsToUpdate[0] = fdcVerification; |
| 79 | + addressUpdater.updateContractAddresses(contractsToUpdate); |
| 80 | +
|
| 81 | + vm.stopBroadcast(); |
| 82 | +
|
| 83 | + vm.createDir(dirPath, true); |
| 84 | + vm.writeFile(string.concat(dirPath, "addressUpdater.txt"), vm.toString(address(addressUpdater))); |
| 85 | + vm.writeFile(string.concat(dirPath, "fdcVerification.txt"), vm.toString(address(fdcVerification))); |
| 86 | +
|
| 87 | + console.log("\n--- Infrastructure Deployment Complete on Chain ID:", block.chainid, "---"); |
| 88 | + console.log("AddressUpdater: ", address(addressUpdater)); |
| 89 | + console.log("FdcVerification: ", address(fdcVerification)); |
| 90 | + } |
| 91 | +} |
| 92 | +``` |
| 93 | + |
| 94 | +Run the script to deploy the core contracts to the XRPL EVM Sidechain. This only needs to be done once. |
| 95 | + |
| 96 | +```bash |
| 97 | +forge script script/CrossChainFdc.s.sol:DeployInfrastructure --rpc-url $XRPLEVM_RPC_URL_TESTNET --broadcast |
| 98 | +``` |
| 99 | + |
| 100 | +### 2. Prepare and Submit Request on Source Chain (Coston2) |
| 101 | + |
| 102 | +This step, executed by the `PrepareAndSubmitRequest` script, must be run on a Flare network (Coston2). |
| 103 | + |
| 104 | +It prepares the `Web2Json` request off-chain by calling the verifier API and then immediately submits the resulting `abiEncodedRequest` to the FDC on-chain. |
| 105 | + |
| 106 | +The script saves the `abiEncodedRequest` and the resulting `votingRoundId` to files, which will be needed for the final step on the target chain. |
| 107 | + |
| 108 | +```solidity title="script/CrossChainFdc.s.sol" |
| 109 | +// Prepares and submits the FDC request on the Flare Network. |
| 110 | +contract PrepareAndSubmitRequest is Script { |
| 111 | + using Surl for *; |
| 112 | + string public constant SOURCE_NAME = "PublicWeb2"; |
| 113 | +
|
| 114 | + function run() external { |
| 115 | + console.log("--- Preparing and Submitting FDC request on Chain ID:", block.chainid, "---"); |
| 116 | + vm.createDir(dirPath, true); |
| 117 | +
|
| 118 | + string memory attestationType = FdcBase.toUtf8HexString(attestationTypeName); |
| 119 | + string memory sourceId = FdcBase.toUtf8HexString(SOURCE_NAME); |
| 120 | +
|
| 121 | + string memory apiUrl = "https://swapi.info/api/people/3"; // C-3PO |
| 122 | + string memory postProcessJq = '{name: .name, height: .height, mass: .mass, numberOfMovies: .films | length, apiUid: (.url | split(\\"/\\") | .[-1] | tonumber)}'; |
| 123 | + string memory abiSignature = '{\\"components\\":[{\\"internalType\\":\\"string\\",\\"name\\":\\"name\\",\\"type\\":\\"string\\"},{\\"internalType\\":\\"uint256\\",\\"name\\":\\"height\\",\\"type\\":\\"uint256\\"},{\\"internalType\\":\\"uint256\\",\\"name\\":\\"mass\\",\\"type\\":\\"uint256\\"},{\\"internalType\\":\\"uint256\\",\\"name\\":\\"numberOfMovies\\",\\"type\\":\\"uint256\\"},{\\"internalType\\":\\"uint256\\",\\"name\\":\\"apiUid\\",\\"type\\":\\"uint256\\"}],\\"name\\":\\"dto\\",\\"type\\":\\"tuple\\"}'; |
| 124 | + string memory requestBody = string.concat('{"url":"',apiUrl,'","httpMethod":"GET","headers":"{}","queryParams":"{}","body":"{}","postProcessJq":"',postProcessJq,'","abiSignature":"',abiSignature,'"}'); |
| 125 | +
|
| 126 | + // Prepare request off-chain |
| 127 | + (string[] memory headers, string memory body) = FdcBase.prepareAttestationRequest(attestationType, sourceId, requestBody); |
| 128 | + string memory baseUrl = vm.envString("WEB2JSON_VERIFIER_URL_TESTNET"); |
| 129 | + string memory url = string.concat(baseUrl, attestationTypeName, "/prepareRequest"); |
| 130 | + (, bytes memory data) = url.post(headers, body); |
| 131 | + FdcBase.AttestationResponse memory response = FdcBase.parseAttestationRequest(data); |
| 132 | +
|
| 133 | + // Submit request on-chain |
| 134 | + uint256 timestamp = FdcBase.submitAttestationRequest(response.abiEncodedRequest); |
| 135 | + uint256 votingRoundId = FdcBase.calculateRoundId(timestamp); |
| 136 | +
|
| 137 | + // Write data to files for the next step |
| 138 | + FdcBase.writeToFile(dirPath, "abiEncodedRequest.txt", StringsBase.toHexString(response.abiEncodedRequest), true); |
| 139 | + FdcBase.writeToFile(dirPath, "votingRoundId.txt", Strings.toString(votingRoundId), true); |
| 140 | + console.log("Successfully prepared and submitted request. Voting Round ID:", votingRoundId); |
| 141 | + } |
| 142 | +} |
| 143 | +``` |
| 144 | + |
| 145 | +Run the script on **Coston2**: |
| 146 | + |
| 147 | +```bash |
| 148 | +forge script script/CrossChainFdc.s.sol:PrepareAndSubmitRequest --rpc-url $COSTON2_RPC_URL --broadcast --ffi |
| 149 | +``` |
| 150 | + |
| 151 | +### 3. Deliver Proof to Consumer on Target Chain (XRPL EVM Sidechain) |
| 152 | + |
| 153 | +The final script, `DeliverProofToContract`, is executed on the target chain (XRPL EVM Sidechain). |
| 154 | + |
| 155 | +It handles deploying the consumer contract, retrieving the proof, and delivering it for verification. |
| 156 | + |
| 157 | +1. **Deploy Consumer Contract**: It first deploys the `StarWarsCharacterListV3` contract, passing it the address of the `FdcVerification` contract deployed in Step 1. |
| 158 | +2. **Retrieve Proof**: It reads the `abiEncodedRequest` and `votingRoundId` from the files created in Step 2. After waiting for the voting round on Flare to finalize (max. 180 seconds), it polls the Data Availability layer to get the proof. |
| 159 | +3. **Interact with Consumer**: It calls `addCharacter` on the deployed consumer contract, passing the `finalProof`. A `value` of 1 wei is sent to cover the fee required by the `Relay` contract to fetch the Merkle root from the source chain. |
| 160 | + |
| 161 | +```solidity title="script/CrossChainFdc.s.sol" |
| 162 | +// Deploys the consumer, waits, retrieves proof, and delivers it on the target chain. |
| 163 | +contract DeliverProofToContract is Script { |
| 164 | + function run() external { |
| 165 | + console.log("--- Delivering Proof on Chain ID:", block.chainid, "---"); |
| 166 | + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); |
| 167 | +
|
| 168 | + // --- Deploy Consumer Contract --- |
| 169 | + string memory fdcVerificationPath = string.concat(dirPath, "fdcVerification.txt"); |
| 170 | + require(vm.exists(fdcVerificationPath), "Infrastructure not deployed. Run DeployInfrastructure first."); |
| 171 | + address fdcVerificationAddress = vm.parseAddress(vm.readFile(fdcVerificationPath)); |
| 172 | +
|
| 173 | + vm.startBroadcast(deployerPrivateKey); |
| 174 | + StarWarsCharacterListV3 characterList = new StarWarsCharacterListV3(fdcVerificationAddress); |
| 175 | + vm.stopBroadcast(); |
| 176 | + console.log("StarWarsCharacterListV3 consumer deployed to:", address(characterList)); |
| 177 | +
|
| 178 | + // --- Retrieve Proof and Interact --- |
| 179 | + string memory requestHex = vm.readFile(string.concat(dirPath, "abiEncodedRequest.txt")); |
| 180 | + uint256 votingRoundId = FdcBase.stringToUint(vm.readFile(string.concat(dirPath, "votingRoundId.txt"))); |
| 181 | +
|
| 182 | + FdcVerification fdcVerification = FdcVerification(fdcVerificationAddress); |
| 183 | + uint8 protocolId = fdcVerification.fdcProtocolId(); |
| 184 | +
|
| 185 | + bytes memory proofData = FdcBase.retrieveProof(protocolId, requestHex, votingRoundId); |
| 186 | +
|
| 187 | + FdcBase.ParsableProof memory parsedProof = abi.decode(proofData, (FdcBase.ParsableProof)); |
| 188 | + IWeb2Json.Response memory proofResponse = abi.decode(parsedProof.responseHex, (IWeb2Json.Response)); |
| 189 | + IWeb2Json.Proof memory finalProof = IWeb2Json.Proof(parsedProof.proofs, proofResponse); |
| 190 | +
|
| 191 | + console.log("\nDelivering proof to consumer contract..."); |
| 192 | + vm.startBroadcast(deployerPrivateKey); |
| 193 | + characterList.addCharacter{value: 1}(finalProof); |
| 194 | + vm.stopBroadcast(); |
| 195 | + console.log("Proof successfully delivered!"); |
| 196 | +
|
| 197 | + StarWarsCharacter[] memory characters = characterList.getAllCharacters(); |
| 198 | + require(characters.length > 0, "Verification failed: No character was added."); |
| 199 | + console.log("\n--- Character Added Verification ---"); |
| 200 | + console.log("Name:", characters[0].name); |
| 201 | + console.log("BMI:", characters[0].bmi); |
| 202 | + } |
| 203 | +} |
| 204 | +``` |
| 205 | + |
| 206 | +Run the final script on the **XRPL EVM Sidechain**: |
| 207 | + |
| 208 | +```bash |
| 209 | +forge script script/CrossChainFdc.s.sol:DeliverProofToContract --rpc-url $XRPLEVM_RPC_URL_TESTNET --broadcast --ffi |
| 210 | +``` |
0 commit comments