11// SPDX-License-Identifier: MIT
2-
32pragma solidity 0.8.19 ;
43
54import {Common} from "@chainlink/contracts/src/v0.8/llo-feeds/libraries/Common.sol " ;
6- import {IRewardManager} from "@chainlink/contracts/src/v0.8/llo-feeds/v0.3.0/interfaces/IRewardManager.sol " ;
75import {IVerifierFeeManager} from "@chainlink/contracts/src/v0.8/llo-feeds/v0.3.0/interfaces/IVerifierFeeManager.sol " ;
86import {IERC20 } from "@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol " ;
97import {SafeERC20} from "@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol " ;
@@ -13,27 +11,37 @@ using SafeERC20 for IERC20;
1311/**
1412 * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE FOR DEMONSTRATION PURPOSES.
1513 * DO NOT USE THIS CODE IN PRODUCTION.
14+ *
15+ * This contract can verify Chainlink Data Streams 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.
1628 */
1729
18- // Custom interfaces for IVerifierProxy and IFeeManager
30+ // ────────────────────────────────────────────────────────────────────────────
31+ // Interfaces
32+ // ────────────────────────────────────────────────────────────────────────────
33+
1934interface IVerifierProxy {
2035 /**
21- * @notice Verifies that the data encoded has been signed correctly by routing to the correct verifier, and bills the user if applicable.
22- * @param payload The encoded data to be verified, including the signed report.
23- * @param parameterPayload Fee metadata for billing. In the current implementation, this consists of the abi-encoded address of the ERC-20 token used for fees.
24- * @return verifierResponse The encoded report from the verifier.
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.
2539 */
2640 function verify (
2741 bytes calldata payload ,
2842 bytes calldata parameterPayload
2943 ) external payable returns (bytes memory verifierResponse );
3044
31- /**
32- * @notice Verifies multiple reports in bulk, ensuring that each is signed correctly, routes them to the appropriate verifier, and handles billing for the verification process.
33- * @param payloads An array of encoded data to be verified, where each entry includes the signed report.
34- * @param parameterPayload Fee metadata for billing. In the current implementation, this consists of the abi-encoded address of the ERC-20 token used for fees.
35- * @return verifiedReports An array of encoded reports returned from the verifier.
36- */
3745 function verifyBulk (
3846 bytes [] calldata payloads ,
3947 bytes calldata parameterPayload
@@ -44,14 +52,7 @@ interface IVerifierProxy {
4452
4553interface IFeeManager {
4654 /**
47- * @notice Calculates the fee and reward associated with verifying a report, including discounts for subscribers.
48- * This function assesses the fee and reward for report verification, applying a discount for recognized subscriber addresses.
49- * @param subscriber The address attempting to verify the report. A discount is applied if this address is recognized as a subscriber.
50- * @param unverifiedReport The report data awaiting verification. The content of this report is used to determine the base fee and reward, before considering subscriber discounts.
51- * @param quoteAddress The payment token address used for quoting fees and rewards.
52- * @return fee The fee assessed for verifying the report, with subscriber discounts applied where applicable.
53- * @return reward The reward allocated to the caller for successfully verifying the report.
54- * @return totalDiscount The total discount amount deducted from the fee for subscribers.
55+ * @return fee, reward, totalDiscount
5556 */
5657 function getFeeAndReward (
5758 address subscriber ,
@@ -66,179 +67,160 @@ interface IFeeManager {
6667 function i_rewardManager () external view returns (address );
6768}
6869
70+ // ────────────────────────────────────────────────────────────────────────────
71+ // Contract
72+ // ────────────────────────────────────────────────────────────────────────────
73+
6974/**
7075 * @dev This contract implements functionality to verify Data Streams reports from
71- * the Streams Direct API or WebSocket connection , with payment in LINK tokens.
76+ * the Data Streams API, with payment in LINK tokens.
7277 */
7378contract ClientReportsVerifier {
74- error NothingToWithdraw (); // Thrown when a withdrawal attempt is made but the contract holds no tokens of the specified type.
75- error NotOwner (address caller ); // Thrown when a caller tries to execute a function that is restricted to the contract's owner.
76- error InvalidReportVersion (uint16 version ); // Thrown when an unsupported report version is provided to verifyReport.
79+ // ----------------- Errors -----------------
80+ error NothingToWithdraw ();
81+ error NotOwner (address caller );
82+ error InvalidReportVersion (uint16 version );
7783
84+ // ----------------- Report schemas -----------------
85+ // More info: https://docs.chain.link/data-streams/reference/report-schema
7886 /**
79- * @dev Represents a data report from a Data Streams stream for v3 schema (crypto streams).
80- * The `price`, `bid`, and `ask` values are carried to either 8 or 18 decimal places, depending on the stream.
81- * For more information, see https://docs.chain.link/data-streams/crypto-streams and https://docs.chain.link/data-streams/reference/report-schema
87+ * @dev Data Streams report schema v3 (crypto streams).
88+ * Prices, bids and asks use 8 or 18 decimals depending on the stream.
8289 */
8390 struct ReportV3 {
84- bytes32 feedId; // The stream ID the report has data for.
85- uint32 validFromTimestamp; // Earliest timestamp for which price is applicable.
86- uint32 observationsTimestamp; // Latest timestamp for which price is applicable.
87- uint192 nativeFee; // Base cost to validate a transaction using the report, denominated in the chain’s native token (e.g., WETH/ETH).
88- uint192 linkFee; // Base cost to validate a transaction using the report, denominated in LINK.
89- uint32 expiresAt; // Latest timestamp where the report can be verified onchain.
90- int192 price; // DON consensus median price (8 or 18 decimals).
91- int192 bid; // Simulated price impact of a buy order up to the X% depth of liquidity utilisation (8 or 18 decimals).
92- int192 ask; // Simulated price impact of a sell order up to the X% depth of liquidity utilisation (8 or 18 decimals).
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;
93100 }
94101
95102 /**
96- * @dev Represents a data report from a Data Streams stream for v4 schema (RWA stream).
97- * The `price` value is carried to either 8 or 18 decimal places, depending on the stream.
98- * The `marketStatus` indicates whether the market is currently open. Possible values: `0` (`Unknown`), `1` (`Closed`), `2` (`Open`).
99- * For more information, see https://docs.chain.link/data-streams/rwa-streams and https://docs.chain.link/data-streams/reference/report-schema-v4
103+ * @dev Data Streams report schema v4 (RWA streams).
100104 */
101105 struct ReportV4 {
102- bytes32 feedId; // The stream ID the report has data for.
103- uint32 validFromTimestamp; // Earliest timestamp for which price is applicable.
104- uint32 observationsTimestamp; // Latest timestamp for which price is applicable.
105- uint192 nativeFee; // Base cost to validate a transaction using the report, denominated in the chain’s native token (e.g., WETH/ETH).
106- uint192 linkFee; // Base cost to validate a transaction using the report, denominated in LINK.
107- uint32 expiresAt; // Latest timestamp where the report can be verified onchain.
108- int192 price; // DON consensus median benchmark price (8 or 18 decimals).
109- uint32 marketStatus; // The DON's consensus on whether the market is currently open.
106+ bytes32 feedId;
107+ uint32 validFromTimestamp;
108+ uint32 observationsTimestamp;
109+ uint192 nativeFee;
110+ uint192 linkFee;
111+ uint32 expiresAt;
112+ int192 price;
113+ uint32 marketStatus;
110114 }
111115
112- IVerifierProxy public s_verifierProxy; // The VerifierProxy contract used for report verification.
116+ // ----------------- Storage -----------------
117+ IVerifierProxy public immutable i_verifierProxy;
118+ address private immutable i_owner;
113119
114- address private s_owner; // The owner of the contract.
115- int192 public lastDecodedPrice; // Stores the last decoded price from a verified report.
120+ int192 public lastDecodedPrice;
116121
117- event DecodedPrice (int192 price ); // Event emitted when a report is successfully verified and decoded.
122+ // ----------------- Events -----------------
123+ event DecodedPrice (int192 price );
118124
125+ // ----------------- Constructor / modifier -----------------
119126 /**
120- * @param _verifierProxy The address of the VerifierProxy contract .
121- * You can find these addresses on https://docs.chain.link/data-streams/crypto-streams.
127+ * @param _verifierProxy Address of the VerifierProxy on the target network .
128+ * Addresses: https://docs.chain.link/data-streams/crypto-streams
122129 */
123130 constructor (address _verifierProxy ) {
124- s_owner = msg .sender ;
125- s_verifierProxy = IVerifierProxy (_verifierProxy);
131+ i_owner = msg .sender ;
132+ i_verifierProxy = IVerifierProxy (_verifierProxy);
126133 }
127134
128- /// @notice Checks if the caller is the owner of the contract.
129135 modifier onlyOwner () {
130- if (msg .sender != s_owner ) revert NotOwner (msg .sender );
136+ if (msg .sender != i_owner ) revert NotOwner (msg .sender );
131137 _;
132138 }
133139
140+ // ----------------- Public API -----------------
141+
134142 /**
135- * @notice Verifies an unverified data report and processes its contents, supporting both v3 and v4 report schemas.
136- * @dev Performs the following steps:
137- * - Decodes the unverified report to extract the report data.
138- * - Extracts the report version by reading the first two bytes of the report data.
139- * - The first two bytes correspond to the schema version encoded in the stream ID.
140- * - Schema version `0x0003` corresponds to report version 3 (for Crypto assets).
141- * - Schema version `0x0004` corresponds to report version 4 (for Real World Assets).
142- * - Validates that the report version is either 3 or 4; reverts with `InvalidReportVersion` otherwise.
143- * - Retrieves the fee manager and reward manager contracts.
144- * - Calculates the fee required for report verification using the fee manager.
145- * - Approves the reward manager to spend the calculated fee amount.
146- * - Verifies the report via the VerifierProxy contract.
147- * - Decodes the verified report data into the appropriate report struct (`ReportV3` or `ReportV4`) based on the report version.
148- * - Emits a `DecodedPrice` event with the price extracted from the verified report.
149- * - Updates the `lastDecodedPrice` state variable with the price from the verified report.
150- * @param unverifiedReport The encoded report data to be verified, including the signed report and metadata.
151- * @custom:reverts InvalidReportVersion(uint8 version) Thrown when an unsupported report version is provided.
143+ * @notice Verify a Data Streams 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 by Streams Direct.
158+ * @custom:reverts InvalidReportVersion when schema ≠ v3/v4.
152159 */
153160 function verifyReport (bytes memory unverifiedReport ) external {
154- // Retrieve fee manager and reward manager
155- IFeeManager feeManager = IFeeManager (
156- address (s_verifierProxy.s_feeManager ())
157- );
158-
159- IRewardManager rewardManager = IRewardManager (
160- address (feeManager.i_rewardManager ())
161- );
162-
163- // Decode unverified report to extract report data
161+ // ─── 1. & 2. Extract reportData and schema version ──
164162 (, bytes memory reportData ) = abi.decode (
165163 unverifiedReport,
166164 (bytes32 [3 ], bytes )
167165 );
168166
169- // Extract report version from reportData
170167 uint16 reportVersion = (uint16 (uint8 (reportData[0 ])) << 8 ) |
171168 uint16 (uint8 (reportData[1 ]));
169+ if (reportVersion != 3 && reportVersion != 4 )
170+ revert InvalidReportVersion (reportVersion);
172171
173- // Validate report version
174- if (reportVersion != 3 && reportVersion != 4 ) {
175- revert InvalidReportVersion ( uint8 (reportVersion));
176- }
172+ // ─── 3. Fee handling ──
173+ IFeeManager feeManager = IFeeManager (
174+ address (i_verifierProxy. s_feeManager ())
175+ );
177176
178- // Set the fee token address (LINK in this case)
179- address feeTokenAddress = feeManager.i_linkAddress ();
177+ bytes memory parameterPayload;
178+ if (address (feeManager) != address (0 )) {
179+ // FeeManager exists — always quote & approve
180+ address feeToken = feeManager.i_linkAddress ();
180181
181- // Calculate the fee required for report verification
182- (Common.Asset memory fee , , ) = feeManager.getFeeAndReward (
183- address (this ),
184- reportData,
185- feeTokenAddress
186- );
182+ (Common.Asset memory fee , , ) = feeManager.getFeeAndReward (
183+ address (this ),
184+ reportData,
185+ feeToken
186+ );
187187
188- // Approve rewardManager to spend this contract's balance in fees
189- IERC20 (feeTokenAddress).approve (address (rewardManager), fee.amount);
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+ }
190194
191- // Verify the report through the VerifierProxy
192- bytes memory verifiedReportData = s_verifierProxy .verify (
195+ // ─── 4. Verify through the proxy ──
196+ bytes memory verified = i_verifierProxy .verify (
193197 unverifiedReport,
194- abi.encode (feeTokenAddress)
198+ parameterPayload
195199 );
196200
197- // Decode verified report data into the appropriate Report struct based on reportVersion
201+ // ─── 5. Decode & store price ──
198202 if (reportVersion == 3 ) {
199- // v3 report schema
200- ReportV3 memory verifiedReport = abi.decode (
201- verifiedReportData,
202- (ReportV3)
203- );
204-
205- // Log price from the verified report
206- emit DecodedPrice (verifiedReport.price);
207-
208- // Store the price from the report
209- lastDecodedPrice = verifiedReport.price;
210- } else if (reportVersion == 4 ) {
211- // v4 report schema
212- ReportV4 memory verifiedReport = abi.decode (
213- verifiedReportData,
214- (ReportV4)
215- );
216-
217- // Log price from the verified report
218- emit DecodedPrice (verifiedReport.price);
219-
220- // Store the price from the report
221- lastDecodedPrice = verifiedReport.price;
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);
222210 }
223211 }
224212
225213 /**
226- * @notice Withdraws all tokens of a specific ERC20 token type to a beneficiary address.
227- * @dev Utilizes SafeERC20's safeTransfer for secure token transfer. Reverts if the contract's balance of the specified token is zero.
228- * @param _beneficiary Address to which the tokens will be sent. Must not be the zero address.
229- * @param _token Address of the ERC20 token to be withdrawn. Must be a valid ERC20 token contract.
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.
230217 */
231218 function withdrawToken (
232219 address _beneficiary ,
233220 address _token
234- ) public onlyOwner {
235- // Retrieve the balance of this contract for the specified token
221+ ) external onlyOwner {
236222 uint256 amount = IERC20 (_token).balanceOf (address (this ));
237-
238- // Revert if there is nothing to withdraw
239223 if (amount == 0 ) revert NothingToWithdraw ();
240-
241- // Transfer the tokens to the beneficiary
242224 IERC20 (_token).safeTransfer (_beneficiary, amount);
243225 }
244226}
0 commit comments