1
1
// SPDX-License-Identifier: MIT
2
-
3
2
pragma solidity 0.8.19 ;
4
3
5
4
import {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 " ;
7
5
import {IVerifierFeeManager} from "@chainlink/contracts/src/v0.8/llo-feeds/v0.3.0/interfaces/IVerifierFeeManager.sol " ;
8
6
import {IERC20 } from "@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol " ;
9
7
import {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;
13
11
/**
14
12
* THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE FOR DEMONSTRATION PURPOSES.
15
13
* 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.
16
28
*/
17
29
18
- // Custom interfaces for IVerifierProxy and IFeeManager
30
+ // ────────────────────────────────────────────────────────────────────────────
31
+ // Interfaces
32
+ // ────────────────────────────────────────────────────────────────────────────
33
+
19
34
interface IVerifierProxy {
20
35
/**
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.
25
39
*/
26
40
function verify (
27
41
bytes calldata payload ,
28
42
bytes calldata parameterPayload
29
43
) external payable returns (bytes memory verifierResponse );
30
44
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
- */
37
45
function verifyBulk (
38
46
bytes [] calldata payloads ,
39
47
bytes calldata parameterPayload
@@ -44,14 +52,7 @@ interface IVerifierProxy {
44
52
45
53
interface IFeeManager {
46
54
/**
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
55
56
*/
56
57
function getFeeAndReward (
57
58
address subscriber ,
@@ -66,179 +67,160 @@ interface IFeeManager {
66
67
function i_rewardManager () external view returns (address );
67
68
}
68
69
70
+ // ────────────────────────────────────────────────────────────────────────────
71
+ // Contract
72
+ // ────────────────────────────────────────────────────────────────────────────
73
+
69
74
/**
70
75
* @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.
72
77
*/
73
78
contract 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 );
77
83
84
+ // ----------------- Report schemas -----------------
85
+ // More info: https://docs.chain.link/data-streams/reference/report-schema
78
86
/**
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.
82
89
*/
83
90
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;
93
100
}
94
101
95
102
/**
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).
100
104
*/
101
105
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;
110
114
}
111
115
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;
113
119
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;
116
121
117
- event DecodedPrice (int192 price ); // Event emitted when a report is successfully verified and decoded.
122
+ // ----------------- Events -----------------
123
+ event DecodedPrice (int192 price );
118
124
125
+ // ----------------- Constructor / modifier -----------------
119
126
/**
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
122
129
*/
123
130
constructor (address _verifierProxy ) {
124
- s_owner = msg .sender ;
125
- s_verifierProxy = IVerifierProxy (_verifierProxy);
131
+ i_owner = msg .sender ;
132
+ i_verifierProxy = IVerifierProxy (_verifierProxy);
126
133
}
127
134
128
- /// @notice Checks if the caller is the owner of the contract.
129
135
modifier onlyOwner () {
130
- if (msg .sender != s_owner ) revert NotOwner (msg .sender );
136
+ if (msg .sender != i_owner ) revert NotOwner (msg .sender );
131
137
_;
132
138
}
133
139
140
+ // ----------------- Public API -----------------
141
+
134
142
/**
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.
152
159
*/
153
160
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 ──
164
162
(, bytes memory reportData ) = abi.decode (
165
163
unverifiedReport,
166
164
(bytes32 [3 ], bytes )
167
165
);
168
166
169
- // Extract report version from reportData
170
167
uint16 reportVersion = (uint16 (uint8 (reportData[0 ])) << 8 ) |
171
168
uint16 (uint8 (reportData[1 ]));
169
+ if (reportVersion != 3 && reportVersion != 4 )
170
+ revert InvalidReportVersion (reportVersion);
172
171
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
+ );
177
176
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 ();
180
181
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
+ );
187
187
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
+ }
190
194
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 (
193
197
unverifiedReport,
194
- abi.encode (feeTokenAddress)
198
+ parameterPayload
195
199
);
196
200
197
- // Decode verified report data into the appropriate Report struct based on reportVersion
201
+ // ─── 5. Decode & store price ──
198
202
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);
222
210
}
223
211
}
224
212
225
213
/**
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.
230
217
*/
231
218
function withdrawToken (
232
219
address _beneficiary ,
233
220
address _token
234
- ) public onlyOwner {
235
- // Retrieve the balance of this contract for the specified token
221
+ ) external onlyOwner {
236
222
uint256 amount = IERC20 (_token).balanceOf (address (this ));
237
-
238
- // Revert if there is nothing to withdraw
239
223
if (amount == 0 ) revert NothingToWithdraw ();
240
-
241
- // Transfer the tokens to the beneficiary
242
224
IERC20 (_token).safeTransfer (_beneficiary, amount);
243
225
}
244
226
}
0 commit comments