|
| 1 | +// SPDX-License-Identifier: MIT |
| 2 | +pragma solidity 0.8.19; |
| 3 | + |
| 4 | +import {Common} from "@chainlink/contracts/src/v0.8/llo-feeds/libraries/Common.sol"; |
| 5 | +import {IVerifierFeeManager} from "@chainlink/contracts/src/v0.8/llo-feeds/v0.3.0/interfaces/IVerifierFeeManager.sol"; |
| 6 | +import {IERC20} from "@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol"; |
| 7 | +import {SafeERC20} from "@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol"; |
| 8 | + |
| 9 | +using SafeERC20 for IERC20; |
| 10 | + |
| 11 | +/** |
| 12 | + * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE FOR DEMONSTRATION PURPOSES. |
| 13 | + * DO NOT USE THIS CODE IN PRODUCTION. |
| 14 | + * |
| 15 | + * This contract can verify Chainlink DataLink reports onchain and pay |
| 16 | + * the verification fee in LINK (when required). |
| 17 | + * |
| 18 | + * - If `VerifierProxy.s_feeManager()` returns a non-zero address, the network |
| 19 | + * expects you to interact with that FeeManager for every verification call: |
| 20 | + * quote fees, approve the RewardManager, then call `verify()`. |
| 21 | + * |
| 22 | + * - If `s_feeManager()` returns the zero address, no FeeManager contract has |
| 23 | + * been deployed on that chain. In that case there is nothing to quote or pay |
| 24 | + * onchain, so the contract skips the fee logic entirely. |
| 25 | + * |
| 26 | + * The `if (address(feeManager) != address(0))` check below chooses the |
| 27 | + * correct path automatically, making the same bytecode usable on any chain. |
| 28 | + */ |
| 29 | + |
| 30 | +// ──────────────────────────────────────────────────────────────────────────── |
| 31 | +// Interfaces |
| 32 | +// ──────────────────────────────────────────────────────────────────────────── |
| 33 | + |
| 34 | +interface IVerifierProxy { |
| 35 | + /** |
| 36 | + * @notice Route a report to the correct verifier and (optionally) bill fees. |
| 37 | + * @param payload Full report payload (header + signed report). |
| 38 | + * @param parameterPayload ABI-encoded fee metadata. |
| 39 | + */ |
| 40 | + function verify( |
| 41 | + bytes calldata payload, |
| 42 | + bytes calldata parameterPayload |
| 43 | + ) external payable returns (bytes memory verifierResponse); |
| 44 | + |
| 45 | + function verifyBulk( |
| 46 | + bytes[] calldata payloads, |
| 47 | + bytes calldata parameterPayload |
| 48 | + ) external payable returns (bytes[] memory verifiedReports); |
| 49 | + |
| 50 | + function s_feeManager() external view returns (IVerifierFeeManager); |
| 51 | +} |
| 52 | + |
| 53 | +interface IFeeManager { |
| 54 | + /** |
| 55 | + * @return fee, reward, totalDiscount |
| 56 | + */ |
| 57 | + function getFeeAndReward( |
| 58 | + address subscriber, |
| 59 | + bytes memory unverifiedReport, |
| 60 | + address quoteAddress |
| 61 | + ) external returns (Common.Asset memory, Common.Asset memory, uint256); |
| 62 | + |
| 63 | + function i_linkAddress() external view returns (address); |
| 64 | + |
| 65 | + function i_nativeAddress() external view returns (address); |
| 66 | + |
| 67 | + function i_rewardManager() external view returns (address); |
| 68 | +} |
| 69 | + |
| 70 | +// ──────────────────────────────────────────────────────────────────────────── |
| 71 | +// Contract |
| 72 | +// ──────────────────────────────────────────────────────────────────────────── |
| 73 | + |
| 74 | +/** |
| 75 | + * @dev This contract implements functionality to verify DataLink reports from |
| 76 | + * the API, with payment in LINK tokens. |
| 77 | + */ |
| 78 | +contract ClientReportsVerifier { |
| 79 | + // ----------------- Errors ----------------- |
| 80 | + error NothingToWithdraw(); |
| 81 | + error NotOwner(address caller); |
| 82 | + error InvalidReportVersion(uint16 version); |
| 83 | + |
| 84 | + // ----------------- Report schemas ----------------- |
| 85 | + // Extract schema version from feed ID (first 2 bytes of the feed ID) |
| 86 | + /** |
| 87 | + * @dev DataLink report schema v3. |
| 88 | + * Prices, bids and asks use 8 or 18 decimals depending on the feed. |
| 89 | + */ |
| 90 | + struct ReportV3 { |
| 91 | + bytes32 feedId; |
| 92 | + uint32 validFromTimestamp; |
| 93 | + uint32 observationsTimestamp; |
| 94 | + uint192 nativeFee; |
| 95 | + uint192 linkFee; |
| 96 | + uint32 expiresAt; |
| 97 | + int192 price; |
| 98 | + int192 bid; |
| 99 | + int192 ask; |
| 100 | + } |
| 101 | + |
| 102 | + /** |
| 103 | + * @dev DataLink report schema v4. |
| 104 | + */ |
| 105 | + struct ReportV4 { |
| 106 | + bytes32 feedId; |
| 107 | + uint32 validFromTimestamp; |
| 108 | + uint32 observationsTimestamp; |
| 109 | + uint192 nativeFee; |
| 110 | + uint192 linkFee; |
| 111 | + uint32 expiresAt; |
| 112 | + int192 price; |
| 113 | + uint32 marketStatus; |
| 114 | + } |
| 115 | + |
| 116 | + // ----------------- Storage ----------------- |
| 117 | + IVerifierProxy public immutable i_verifierProxy; |
| 118 | + address private immutable i_owner; |
| 119 | + |
| 120 | + int192 public lastDecodedPrice; |
| 121 | + |
| 122 | + // ----------------- Events ----------------- |
| 123 | + event DecodedPrice(int192 price); |
| 124 | + |
| 125 | + // ----------------- Constructor / modifier ----------------- |
| 126 | + /** |
| 127 | + * @param _verifierProxy Address of the VerifierProxy on the target network. |
| 128 | + * Addresses: https://docs.chain.link/datalink/pull-delivery/verifier-proxy-addresses |
| 129 | + */ |
| 130 | + constructor(address _verifierProxy) { |
| 131 | + i_owner = msg.sender; |
| 132 | + i_verifierProxy = IVerifierProxy(_verifierProxy); |
| 133 | + } |
| 134 | + |
| 135 | + modifier onlyOwner() { |
| 136 | + if (msg.sender != i_owner) revert NotOwner(msg.sender); |
| 137 | + _; |
| 138 | + } |
| 139 | + |
| 140 | + // ----------------- Public API ----------------- |
| 141 | + |
| 142 | + /** |
| 143 | + * @notice Verify a DataLink report (schema v3 or v4). |
| 144 | + * |
| 145 | + * @dev Steps: |
| 146 | + * 1. Decode the unverified report to get `reportData`. |
| 147 | + * 2. Read the first two bytes → schema version (`0x0003` or `0x0004`). |
| 148 | + * - Revert if the version is unsupported. |
| 149 | + * 3. Fee handling: |
| 150 | + * - Query `s_feeManager()` on the proxy. |
| 151 | + * – Non-zero → quote the fee, approve the RewardManager, |
| 152 | + * ABI-encode the fee token address for `verify()`. |
| 153 | + * – Zero → no FeeManager; skip quoting/approval and pass `""`. |
| 154 | + * 4. Call `VerifierProxy.verify()`. |
| 155 | + * 5. Decode the verified report into the correct struct and emit the price. |
| 156 | + * |
| 157 | + * @param unverifiedReport Full payload returned. |
| 158 | + * @custom:reverts InvalidReportVersion when schema ≠ v3/v4. |
| 159 | + */ |
| 160 | + function verifyReport(bytes memory unverifiedReport) external { |
| 161 | + // ─── 1. & 2. Extract reportData and schema version ── |
| 162 | + (, bytes memory reportData) = abi.decode( |
| 163 | + unverifiedReport, |
| 164 | + (bytes32[3], bytes) |
| 165 | + ); |
| 166 | + |
| 167 | + uint16 reportVersion = (uint16(uint8(reportData[0])) << 8) | |
| 168 | + uint16(uint8(reportData[1])); |
| 169 | + if (reportVersion != 3 && reportVersion != 4) |
| 170 | + revert InvalidReportVersion(reportVersion); |
| 171 | + |
| 172 | + // ─── 3. Fee handling ── |
| 173 | + IFeeManager feeManager = IFeeManager( |
| 174 | + address(i_verifierProxy.s_feeManager()) |
| 175 | + ); |
| 176 | + |
| 177 | + bytes memory parameterPayload; |
| 178 | + if (address(feeManager) != address(0)) { |
| 179 | + // FeeManager exists — always quote & approve |
| 180 | + address feeToken = feeManager.i_linkAddress(); |
| 181 | + |
| 182 | + (Common.Asset memory fee, , ) = feeManager.getFeeAndReward( |
| 183 | + address(this), |
| 184 | + reportData, |
| 185 | + feeToken |
| 186 | + ); |
| 187 | + |
| 188 | + IERC20(feeToken).approve(feeManager.i_rewardManager(), fee.amount); |
| 189 | + parameterPayload = abi.encode(feeToken); |
| 190 | + } else { |
| 191 | + // No FeeManager deployed on this chain |
| 192 | + parameterPayload = bytes(""); |
| 193 | + } |
| 194 | + |
| 195 | + // ─── 4. Verify through the proxy ── |
| 196 | + bytes memory verified = i_verifierProxy.verify( |
| 197 | + unverifiedReport, |
| 198 | + parameterPayload |
| 199 | + ); |
| 200 | + |
| 201 | + // ─── 5. Decode & store price ── |
| 202 | + if (reportVersion == 3) { |
| 203 | + int192 price = abi.decode(verified, (ReportV3)).price; |
| 204 | + lastDecodedPrice = price; |
| 205 | + emit DecodedPrice(price); |
| 206 | + } else { |
| 207 | + int192 price = abi.decode(verified, (ReportV4)).price; |
| 208 | + lastDecodedPrice = price; |
| 209 | + emit DecodedPrice(price); |
| 210 | + } |
| 211 | + } |
| 212 | + |
| 213 | + /** |
| 214 | + * @notice Withdraw all balance of an ERC-20 token held by this contract. |
| 215 | + * @param _beneficiary Address that receives the tokens. |
| 216 | + * @param _token ERC-20 token address. |
| 217 | + */ |
| 218 | + function withdrawToken( |
| 219 | + address _beneficiary, |
| 220 | + address _token |
| 221 | + ) external onlyOwner { |
| 222 | + uint256 amount = IERC20(_token).balanceOf(address(this)); |
| 223 | + if (amount == 0) revert NothingToWithdraw(); |
| 224 | + IERC20(_token).safeTransfer(_beneficiary, amount); |
| 225 | + } |
| 226 | +} |
0 commit comments