diff --git a/reports/llms-report.json b/reports/llms-report.json index 12f5a2a1310..286d662ad42 100644 --- a/reports/llms-report.json +++ b/reports/llms-report.json @@ -1,22 +1,22 @@ { - "startedAt": "2025-12-09T14:01:39.057Z", + "startedAt": "2025-12-09T18:04:32.521Z", "siteBase": "https://docs.chain.link", "sections": [ { "section": "cre-go", "pagesProcessed": 83, "outputPath": "src/content/cre/llms-full-go.txt", - "bytes": 651615, - "prevBytes": 651595, - "deltaBytes": 20 + "bytes": 655973, + "prevBytes": 651615, + "deltaBytes": 4358 }, { "section": "cre-ts", "pagesProcessed": 78, "outputPath": "src/content/cre/llms-full-ts.txt", - "bytes": 607118, - "prevBytes": 607098, - "deltaBytes": 20 + "bytes": 611476, + "prevBytes": 607118, + "deltaBytes": 4358 }, { "section": "vrf", @@ -86,9 +86,9 @@ "section": "resources", "pagesProcessed": 12, "outputPath": "src/content/resources/llms-full.txt", - "bytes": 338205, + "bytes": 339715, "prevBytes": 338205, - "deltaBytes": 0 + "deltaBytes": 1510 }, { "section": "architecture-overview", @@ -123,5 +123,5 @@ "deltaBytes": 0 } ], - "finishedAt": "2025-12-09T14:01:43.169Z" + "finishedAt": "2025-12-09T18:04:37.177Z" } diff --git a/src/content/cre/getting-started/part-4-writing-onchain-go.mdx b/src/content/cre/getting-started/part-4-writing-onchain-go.mdx index 530d5a03092..c0f777f4439 100644 --- a/src/content/cre/getting-started/part-4-writing-onchain-go.mdx +++ b/src/content/cre/getting-started/part-4-writing-onchain-go.mdx @@ -39,16 +39,16 @@ Here is the source code for the contract so you can see how it works: // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import { IReceiverTemplate } from "./keystone/IReceiverTemplate.sol"; +import { ReceiverTemplate } from "./ReceiverTemplate.sol"; /** * @title CalculatorConsumer (Testing Version) * @notice This contract receives reports from a CRE workflow and stores the results of a calculation onchain. - * @dev This version uses IReceiverTemplate without configuring any security checks, making it compatible + * @dev This version uses ReceiverTemplate without configuring any security checks, making it compatible * with the mock Forwarder used during simulation. All permission fields remain at their default zero * values (disabled). */ -contract CalculatorConsumer is IReceiverTemplate { +contract CalculatorConsumer is ReceiverTemplate { // Struct to hold the data sent in a report from the workflow struct CalculatorResult { uint256 offchainValue; @@ -66,13 +66,13 @@ contract CalculatorConsumer is IReceiverTemplate { /** * @dev The constructor doesn't set any security checks. - * The IReceiverTemplate parent constructor will initialize all permission fields to zero (disabled). + * The ReceiverTemplate parent constructor will initialize all permission fields to zero (disabled). */ constructor() {} /** * @notice Implements the core business logic for processing reports. - * @dev This is called automatically by IReceiverTemplate's onReport function after security checks. + * @dev This is called automatically by ReceiverTemplate's onReport function after security checks. */ function _processReport(bytes calldata report) internal override { // Decode the report bytes into our CalculatorResult struct diff --git a/src/content/cre/getting-started/part-4-writing-onchain-ts.mdx b/src/content/cre/getting-started/part-4-writing-onchain-ts.mdx index 5fbfb1fc583..979ca288de3 100644 --- a/src/content/cre/getting-started/part-4-writing-onchain-ts.mdx +++ b/src/content/cre/getting-started/part-4-writing-onchain-ts.mdx @@ -39,16 +39,16 @@ Here is the source code for the contract so you can see how it works: // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import { IReceiverTemplate } from "./keystone/IReceiverTemplate.sol"; +import { ReceiverTemplate } from "./ReceiverTemplate.sol"; /** * @title CalculatorConsumer (Testing Version) * @notice This contract receives reports from a CRE workflow and stores the results of a calculation onchain. - * @dev This version uses IReceiverTemplate without configuring any security checks, making it compatible + * @dev This version uses ReceiverTemplate without configuring any security checks, making it compatible * with the mock Forwarder used during simulation. All permission fields remain at their default zero * values (disabled). */ -contract CalculatorConsumer is IReceiverTemplate { +contract CalculatorConsumer is ReceiverTemplate { // Struct to hold the data sent in a report from the workflow struct CalculatorResult { uint256 offchainValue; @@ -66,13 +66,13 @@ contract CalculatorConsumer is IReceiverTemplate { /** * @dev The constructor doesn't set any security checks. - * The IReceiverTemplate parent constructor will initialize all permission fields to zero (disabled). + * The ReceiverTemplate parent constructor will initialize all permission fields to zero (disabled). */ constructor() {} /** * @notice Implements the core business logic for processing reports. - * @dev This is called automatically by IReceiverTemplate's onReport function after security checks. + * @dev This is called automatically by ReceiverTemplate's onReport function after security checks. */ function _processReport(bytes calldata report) internal override { // Decode the report bytes into our CalculatorResult struct diff --git a/src/content/cre/guides/workflow/using-evm-client/onchain-write/building-consumer-contracts.mdx b/src/content/cre/guides/workflow/using-evm-client/onchain-write/building-consumer-contracts.mdx index dc21f98ef96..a9570305ce9 100644 --- a/src/content/cre/guides/workflow/using-evm-client/onchain-write/building-consumer-contracts.mdx +++ b/src/content/cre/guides/workflow/using-evm-client/onchain-write/building-consumer-contracts.mdx @@ -18,7 +18,7 @@ This guide explains how to build a consumer contract that can securely receive a - [Core Concepts: The Onchain Data Flow](#1-core-concepts-the-onchain-data-flow) - [The IReceiver Standard](#2-the-ireceiver-standard) -- [Using IReceiverTemplate](#3-using-ireceivertemplate) +- [Using ReceiverTemplate](#3-using-receivertemplate) - [Working with Simulation](#4-working-with-simulation) - [Advanced Usage](#5-advanced-usage-optional) - [Complete Examples](#6-complete-examples) @@ -44,18 +44,18 @@ interface IReceiver is IERC165 { } ``` -- `metadata`: Contains information about the workflow (ID, name, owner). +- `metadata`: Contains information about the workflow (ID, name, owner). This is encoded by the Forwarder using `abi.encodePacked` with the following structure: `bytes32 workflowId`, `bytes10 workflowName`, `address workflowOwner`. - `report`: The raw, ABI-encoded data payload from your workflow. ### 2.2 Support ERC165 Interface Detection [ERC165](https://eips.ethereum.org/EIPS/eip-165) is a standard that allows contracts to publish the interfaces they support. The `KeystoneForwarder` uses this to check if your contract supports the `IReceiver` interface before sending a report. -## 3. Using `IReceiverTemplate` +## 3. Using `ReceiverTemplate` ### 3.1 Overview -While you can implement these standards manually, we provide an abstract contract, `IReceiverTemplate.sol`, that does the heavy lifting for you. Inheriting from it is the recommended best practice. +While you can implement these standards manually, we provide an abstract contract, `ReceiverTemplate.sol`, that does the heavy lifting for you. Inheriting from it is the recommended best practice. **Key features:** @@ -76,15 +76,18 @@ import {IERC165} from "./IERC165.sol"; import {IReceiver} from "./IReceiver.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -/// @title IReceiverTemplate - Abstract receiver with optional permission controls +/// @title ReceiverTemplate - Abstract receiver with optional permission controls /// @notice Provides flexible, updatable security checks for receiving workflow reports /// @dev All permission fields default to zero (disabled). Use setter functions to enable checks. -abstract contract IReceiverTemplate is IReceiver, Ownable { +abstract contract ReceiverTemplate is IReceiver, Ownable { // Optional permission fields (all default to zero = disabled) - address public forwarderAddress; // If set, only this address can call onReport - address public expectedAuthor; // If set, only reports from this workflow owner are accepted - bytes10 public expectedWorkflowName; // If set, only reports with this workflow name are accepted - bytes32 public expectedWorkflowId; // If set, only reports from this specific workflow ID are accepted + address private s_forwarderAddress; // If set, only this address can call onReport + address private s_expectedAuthor; // If set, only reports from this workflow owner are accepted + bytes10 private s_expectedWorkflowName; // If set, only reports with this workflow name are accepted + bytes32 private s_expectedWorkflowId; // If set, only reports from this specific workflow ID are accepted + + // Hex character lookup table for bytes-to-hex conversion + bytes private constant HEX_CHARS = "0123456789abcdef"; // Custom errors error InvalidSender(address sender, address expected); @@ -92,30 +95,60 @@ abstract contract IReceiverTemplate is IReceiver, Ownable { error InvalidWorkflowName(bytes10 received, bytes10 expected); error InvalidWorkflowId(bytes32 received, bytes32 expected); + // Events + event ForwarderAddressUpdated(address indexed previousForwarder, address indexed newForwarder); + event ExpectedAuthorUpdated(address indexed previousAuthor, address indexed newAuthor); + event ExpectedWorkflowNameUpdated(bytes10 indexed previousName, bytes10 indexed newName); + event ExpectedWorkflowIdUpdated(bytes32 indexed previousId, bytes32 indexed newId); + /// @notice Constructor sets msg.sender as the owner /// @dev All permission fields are initialized to zero (disabled by default) constructor() Ownable(msg.sender) {} + /// @notice Returns the configured forwarder address + /// @return The forwarder address (address(0) if not set) + function getForwarderAddress() external view returns (address) { + return s_forwarderAddress; + } + + /// @notice Returns the expected workflow author address + /// @return The expected author address (address(0) if not set) + function getExpectedAuthor() external view returns (address) { + return s_expectedAuthor; + } + + /// @notice Returns the expected workflow name + /// @return The expected workflow name (bytes10(0) if not set) + function getExpectedWorkflowName() external view returns (bytes10) { + return s_expectedWorkflowName; + } + + /// @notice Returns the expected workflow ID + /// @return The expected workflow ID (bytes32(0) if not set) + function getExpectedWorkflowId() external view returns (bytes32) { + return s_expectedWorkflowId; + } + /// @inheritdoc IReceiver /// @dev Performs optional validation checks based on which permission fields are set function onReport(bytes calldata metadata, bytes calldata report) external override { // Security Check 1: Verify caller is the trusted Chainlink Forwarder (if configured) - if (forwarderAddress != address(0) && msg.sender != forwarderAddress) { - revert InvalidSender(msg.sender, forwarderAddress); + if (s_forwarderAddress != address(0) && msg.sender != s_forwarderAddress) { + revert InvalidSender(msg.sender, s_forwarderAddress); } // Security Checks 2-4: Verify workflow identity - ID, owner, and/or name (if any are configured) - if (expectedWorkflowId != bytes32(0) || expectedAuthor != address(0) || expectedWorkflowName != bytes10(0)) { + if (s_expectedWorkflowId != bytes32(0) || s_expectedAuthor != address(0) || s_expectedWorkflowName != bytes10(0)) { (bytes32 workflowId, bytes10 workflowName, address workflowOwner) = _decodeMetadata(metadata); - if (expectedWorkflowId != bytes32(0) && workflowId != expectedWorkflowId) { - revert InvalidWorkflowId(workflowId, expectedWorkflowId); + if (s_expectedWorkflowId != bytes32(0) && workflowId != s_expectedWorkflowId) { + revert InvalidWorkflowId(workflowId, s_expectedWorkflowId); } - if (expectedAuthor != address(0) && workflowOwner != expectedAuthor) { - revert InvalidAuthor(workflowOwner, expectedAuthor); + if (s_expectedAuthor != address(0) && workflowOwner != s_expectedAuthor) { + revert InvalidAuthor(workflowOwner, s_expectedAuthor); } - if (expectedWorkflowName != bytes10(0) && workflowName != expectedWorkflowName) { - revert InvalidWorkflowName(workflowName, expectedWorkflowName); + if (s_expectedWorkflowName != bytes10(0) && workflowName != s_expectedWorkflowName) { + revert InvalidWorkflowName(workflowName, s_expectedWorkflowName); } } @@ -125,21 +158,28 @@ abstract contract IReceiverTemplate is IReceiver, Ownable { /// @notice Updates the forwarder address that is allowed to call onReport /// @param _forwarder The new forwarder address (use address(0) to disable this check) function setForwarderAddress(address _forwarder) external onlyOwner { - forwarderAddress = _forwarder; + address previousForwarder = s_forwarderAddress; + s_forwarderAddress = _forwarder; + emit ForwarderAddressUpdated(previousForwarder, _forwarder); } /// @notice Updates the expected workflow owner address /// @param _author The new expected author address (use address(0) to disable this check) function setExpectedAuthor(address _author) external onlyOwner { - expectedAuthor = _author; + address previousAuthor = s_expectedAuthor; + s_expectedAuthor = _author; + emit ExpectedAuthorUpdated(previousAuthor, _author); } /// @notice Updates the expected workflow name from a plaintext string /// @param _name The workflow name as a string (use empty string "" to disable this check) /// @dev The name is hashed using SHA256 and truncated function setExpectedWorkflowName(string calldata _name) external onlyOwner { + bytes10 previousName = s_expectedWorkflowName; + if (bytes(_name).length == 0) { - expectedWorkflowName = bytes10(0); + s_expectedWorkflowName = bytes10(0); + emit ExpectedWorkflowNameUpdated(previousName, bytes10(0)); return; } @@ -148,35 +188,37 @@ abstract contract IReceiverTemplate is IReceiver, Ownable { bytes32 hash = sha256(bytes(_name)); bytes memory hexString = _bytesToHexString(abi.encodePacked(hash)); bytes memory first10 = new bytes(10); - for (uint i = 0; i < 10; i++) { + for (uint256 i = 0; i < 10; i++) { first10[i] = hexString[i]; } - expectedWorkflowName = bytes10(first10); + s_expectedWorkflowName = bytes10(first10); + emit ExpectedWorkflowNameUpdated(previousName, s_expectedWorkflowName); } /// @notice Updates the expected workflow ID /// @param _id The new expected workflow ID (use bytes32(0) to disable this check) function setExpectedWorkflowId(bytes32 _id) external onlyOwner { - expectedWorkflowId = _id; + bytes32 previousId = s_expectedWorkflowId; + s_expectedWorkflowId = _id; + emit ExpectedWorkflowIdUpdated(previousId, _id); } /// @notice Helper function to convert bytes to hex string /// @param data The bytes to convert /// @return The hex string representation function _bytesToHexString(bytes memory data) private pure returns (bytes memory) { - bytes memory hexChars = "0123456789abcdef"; bytes memory hexString = new bytes(data.length * 2); for (uint256 i = 0; i < data.length; i++) { - hexString[i * 2] = hexChars[uint8(data[i] >> 4)]; - hexString[i * 2 + 1] = hexChars[uint8(data[i] & 0x0f)]; + hexString[i * 2] = HEX_CHARS[uint8(data[i] >> 4)]; + hexString[i * 2 + 1] = HEX_CHARS[uint8(data[i] & 0x0f)]; } return hexString; } /// @notice Extracts all metadata fields from the onReport metadata parameter - /// @param metadata The metadata in bytes format + /// @param metadata The metadata bytes encoded using abi.encodePacked(workflowId, workflowName, workflowOwner) /// @return workflowId The unique identifier of the workflow (bytes32) /// @return workflowName The name of the workflow (bytes10) /// @return workflowOwner The owner address of the workflow @@ -185,7 +227,7 @@ abstract contract IReceiverTemplate is IReceiver, Ownable { pure returns (bytes32 workflowId, bytes10 workflowName, address workflowOwner) { - // Metadata structure: + // Metadata structure (encoded using abi.encodePacked by the Forwarder): // - First 32 bytes: length of the byte array (standard for dynamic bytes) // - Offset 32, size 32: workflow_id (bytes32) // - Offset 64, size 10: workflow_name (bytes10) @@ -195,6 +237,7 @@ abstract contract IReceiverTemplate is IReceiver, Ownable { workflowName := mload(add(metadata, 64)) workflowOwner := shr(mul(12, 8), mload(add(metadata, 74))) } + return (workflowId, workflowName, workflowOwner); } /// @notice Abstract function to process the report data @@ -211,24 +254,24 @@ abstract contract IReceiverTemplate is IReceiver, Ownable { ### 3.3 Quick Start -The simplest way to use `IReceiverTemplate` is to inherit from it and implement the `_processReport` function: +The simplest way to use `ReceiverTemplate` is to inherit from it and implement the `_processReport` function: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; -import { IReceiverTemplate } from "./IReceiverTemplate.sol"; +import { ReceiverTemplate } from "./ReceiverTemplate.sol"; -contract MyConsumer is IReceiverTemplate { - uint256 public storedValue; +contract MyConsumer is ReceiverTemplate { + uint256 public s_storedValue; event ValueUpdated(uint256 newValue); // Simple constructor - no parameters needed - constructor() IReceiverTemplate() {} + constructor() ReceiverTemplate() {} // Implement your business logic here function _processReport(bytes calldata report) internal override { uint256 newValue = abi.decode(report, (uint256)); - storedValue = newValue; + s_storedValue = newValue; emit ValueUpdated(newValue); } } @@ -287,7 +330,7 @@ myConsumer.setExpectedWorkflowName(""); // Empty string disables the check #### How workflow names are encoded -The `workflowName` field in the metadata uses the **`bytes10`** type rather than plaintext strings. When you call `setExpectedWorkflowName("my_workflow")`, the `IReceiverTemplate` automatically encodes it using the same algorithm as the CRE engine: +The `workflowName` field in the metadata uses the **`bytes10`** type rather than plaintext strings. When you call `setExpectedWorkflowName("my_workflow")`, the `ReceiverTemplate` automatically encodes it using the same algorithm as the CRE engine: 1. Compute SHA256 hash of the workflow name 1. Convert hash to hex string (64 characters) @@ -366,25 +409,27 @@ See [Configuring Permissions](#34-configuring-permissions) for complete details. You can override `onReport` to add your own validation logic before or after the standard checks: ```solidity -import { IReceiverTemplate } from "./IReceiverTemplate.sol"; +import { ReceiverTemplate } from "./ReceiverTemplate.sol"; -contract AdvancedConsumer is IReceiverTemplate { - uint256 public minReportInterval = 1 hours; - uint256 public lastReportTime; +contract AdvancedConsumer is ReceiverTemplate { + uint256 private s_minReportInterval = 1 hours; + uint256 private s_lastReportTime; error ReportTooFrequent(uint256 timeSinceLastReport, uint256 minInterval); + event MinReportIntervalUpdated(uint256 previousInterval, uint256 newInterval); + // Add custom validation before parent's checks function onReport(bytes calldata metadata, bytes calldata report) external override { // Custom check: Rate limiting - if (block.timestamp < lastReportTime + minReportInterval) { - revert ReportTooFrequent(block.timestamp - lastReportTime, minReportInterval); + if (block.timestamp < s_lastReportTime + s_minReportInterval) { + revert ReportTooFrequent(block.timestamp - s_lastReportTime, s_minReportInterval); } // Call parent implementation for standard permission checks super.onReport(metadata, report); - lastReportTime = block.timestamp; + s_lastReportTime = block.timestamp; } function _processReport(bytes calldata report) internal override { @@ -393,9 +438,24 @@ contract AdvancedConsumer is IReceiverTemplate { // ... store or process the value ... } - // Allow owner to update rate limit + /// @notice Returns the minimum interval between reports + /// @return The minimum interval in seconds + function getMinReportInterval() external view returns (uint256) { + return s_minReportInterval; + } + + /// @notice Returns the timestamp of the last report + /// @return The last report timestamp + function getLastReportTime() external view returns (uint256) { + return s_lastReportTime; + } + + /// @notice Updates the minimum interval between reports + /// @param _interval The new minimum interval in seconds function setMinReportInterval(uint256 _interval) external onlyOwner { - minReportInterval = _interval; + uint256 previousInterval = s_minReportInterval; + s_minReportInterval = _interval; + emit MinReportIntervalUpdated(previousInterval, _interval); } } ``` @@ -405,8 +465,8 @@ contract AdvancedConsumer is IReceiverTemplate { The `_decodeMetadata` helper function is available for use in your `_processReport` implementation. This allows you to access workflow metadata for custom business logic: ```solidity -contract MetadataAwareConsumer is IReceiverTemplate { - mapping(bytes32 => uint256) public reportCountByWorkflow; +contract MetadataAwareConsumer is ReceiverTemplate { + mapping(bytes32 => uint256) public s_reportCountByWorkflow; function _processReport(bytes calldata report) internal override { // Access the metadata to get workflow ID @@ -414,7 +474,7 @@ contract MetadataAwareConsumer is IReceiverTemplate { (bytes32 workflowId, , ) = _decodeMetadata(metadata); // Use workflow ID in your business logic - reportCountByWorkflow[workflowId]++; + s_reportCountByWorkflow[workflowId]++; // Process the report data uint256 value = abi.decode(report, (uint256)); @@ -432,23 +492,23 @@ contract MetadataAwareConsumer is IReceiverTemplate { ### Example 1: Simple Consumer Contract -This example inherits from `IReceiverTemplate` to store a temperature value. +This example inherits from `ReceiverTemplate` to store a temperature value. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; -import { IReceiverTemplate } from "./IReceiverTemplate.sol"; +import { ReceiverTemplate } from "./ReceiverTemplate.sol"; -contract TemperatureConsumer is IReceiverTemplate { - int256 public currentTemperature; +contract TemperatureConsumer is ReceiverTemplate { + int256 public s_currentTemperature; event TemperatureUpdated(int256 newTemperature); // Simple constructor - no parameters needed - constructor() IReceiverTemplate() {} + constructor() ReceiverTemplate() {} function _processReport(bytes calldata report) internal override { int256 newTemperature = abi.decode(report, (int256)); - currentTemperature = newTemperature; + s_currentTemperature = newTemperature; emit TemperatureUpdated(newTemperature); } } @@ -469,7 +529,7 @@ temperatureConsumer.setExpectedWorkflowId(0xYourWorkflowId...); For more complex scenarios, it's best to separate your Chainlink-aware code from your core business logic. The **Proxy Pattern** is a robust architecture that uses two contracts to achieve this: - **A Logic Contract**: Holds the state and the core functions of your application. It knows nothing about the Forwarder contract or the `onReport` function. -- **A Proxy Contract**: Acts as the secure entry point. It inherits from `IReceiverTemplate` and forwards validated reports to the Logic Contract. +- **A Proxy Contract**: Acts as the secure entry point. It inherits from `ReceiverTemplate` and forwards validated reports to the Logic Contract. This separation makes your business logic more modular and reusable. @@ -489,28 +549,59 @@ contract ReserveManager is Ownable { uint256 btcPrice; } - address public proxyAddress; - uint256 public lastEthPrice; - uint256 public lastBtcPrice; - uint256 public lastUpdateTime; + address private s_proxyAddress; + uint256 private s_lastEthPrice; + uint256 private s_lastBtcPrice; + uint256 private s_lastUpdateTime; event ReservesUpdated(uint256 ethPrice, uint256 btcPrice, uint256 updateTime); + event ProxyAddressUpdated(address indexed previousProxy, address indexed newProxy); modifier onlyProxy() { - require(msg.sender == proxyAddress, "Caller is not the authorized proxy"); + require(msg.sender == s_proxyAddress, "Caller is not the authorized proxy"); _; } constructor() Ownable(msg.sender) {} + /// @notice Returns the proxy address + /// @return The authorized proxy address + function getProxyAddress() external view returns (address) { + return s_proxyAddress; + } + + /// @notice Returns the last ETH price + /// @return The last recorded ETH price + function getLastEthPrice() external view returns (uint256) { + return s_lastEthPrice; + } + + /// @notice Returns the last BTC price + /// @return The last recorded BTC price + function getLastBtcPrice() external view returns (uint256) { + return s_lastBtcPrice; + } + + /// @notice Returns the last update timestamp + /// @return The timestamp of the last update + function getLastUpdateTime() external view returns (uint256) { + return s_lastUpdateTime; + } + + /// @notice Updates the authorized proxy address + /// @param _proxyAddress The new proxy address function setProxyAddress(address _proxyAddress) external onlyOwner { - proxyAddress = _proxyAddress; + address previousProxy = s_proxyAddress; + s_proxyAddress = _proxyAddress; + emit ProxyAddressUpdated(previousProxy, _proxyAddress); } + /// @notice Updates the reserve prices + /// @param data The new reserve data containing ETH and BTC prices function updateReserves(UpdateReserves memory data) external onlyProxy { - lastEthPrice = data.ethPrice; - lastBtcPrice = data.btcPrice; - lastUpdateTime = block.timestamp; + s_lastEthPrice = data.ethPrice; + s_lastBtcPrice = data.btcPrice; + s_lastUpdateTime = block.timestamp; emit ReservesUpdated(data.ethPrice, data.btcPrice, block.timestamp); } } @@ -518,23 +609,29 @@ contract ReserveManager is Ownable { #### The Proxy Contract (`UpdateReservesProxy.sol`) -This contract, our "bouncer", is the only contract that interacts with the Chainlink platform. It inherits `IReceiverTemplate` to validate incoming reports and then calls the `ReserveManager`. +This contract, our "bouncer", is the only contract that interacts with the Chainlink platform. It inherits `ReceiverTemplate` to validate incoming reports and then calls the `ReserveManager`. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import { ReserveManager } from "./ReserveManager.sol"; -import { IReceiverTemplate } from "./keystone/IReceiverTemplate.sol"; +import { ReceiverTemplate } from "./ReceiverTemplate.sol"; -contract UpdateReservesProxy is IReceiverTemplate { - ReserveManager public s_reserveManager; +contract UpdateReservesProxy is ReceiverTemplate { + ReserveManager private s_reserveManager; constructor(address reserveManagerAddress) { s_reserveManager = ReserveManager(reserveManagerAddress); } - /// @inheritdoc IReceiverTemplate + /// @notice Returns the reserve manager contract address + /// @return The ReserveManager contract instance + function getReserveManager() external view returns (ReserveManager) { + return s_reserveManager; + } + + /// @inheritdoc ReceiverTemplate function _processReport(bytes calldata report) internal override { ReserveManager.UpdateReserves memory updateReservesData = abi.decode(report, (ReserveManager.UpdateReserves)); s_reserveManager.updateReserves(updateReservesData); diff --git a/src/content/cre/llms-full-go.txt b/src/content/cre/llms-full-go.txt index 7d6c63e89fd..be883d85541 100644 --- a/src/content/cre/llms-full-go.txt +++ b/src/content/cre/llms-full-go.txt @@ -2344,7 +2344,7 @@ This guide explains how to build a consumer contract that can securely receive a - [Core Concepts: The Onchain Data Flow](#1-core-concepts-the-onchain-data-flow) - [The IReceiver Standard](#2-the-ireceiver-standard) -- [Using IReceiverTemplate](#3-using-ireceivertemplate) +- [Using ReceiverTemplate](#3-using-receivertemplate) - [Working with Simulation](#4-working-with-simulation) - [Advanced Usage](#5-advanced-usage-optional) - [Complete Examples](#6-complete-examples) @@ -2370,18 +2370,18 @@ interface IReceiver is IERC165 { } ``` -- `metadata`: Contains information about the workflow (ID, name, owner). +- `metadata`: Contains information about the workflow (ID, name, owner). This is encoded by the Forwarder using `abi.encodePacked` with the following structure: `bytes32 workflowId`, `bytes10 workflowName`, `address workflowOwner`. - `report`: The raw, ABI-encoded data payload from your workflow. ### 2.2 Support ERC165 Interface Detection [ERC165](https://eips.ethereum.org/EIPS/eip-165) is a standard that allows contracts to publish the interfaces they support. The `KeystoneForwarder` uses this to check if your contract supports the `IReceiver` interface before sending a report. -## 3. Using `IReceiverTemplate` +## 3. Using `ReceiverTemplate` ### 3.1 Overview -While you can implement these standards manually, we provide an abstract contract, `IReceiverTemplate.sol`, that does the heavy lifting for you. Inheriting from it is the recommended best practice. +While you can implement these standards manually, we provide an abstract contract, `ReceiverTemplate.sol`, that does the heavy lifting for you. Inheriting from it is the recommended best practice. **Key features:** @@ -2402,15 +2402,18 @@ import {IERC165} from "./IERC165.sol"; import {IReceiver} from "./IReceiver.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -/// @title IReceiverTemplate - Abstract receiver with optional permission controls +/// @title ReceiverTemplate - Abstract receiver with optional permission controls /// @notice Provides flexible, updatable security checks for receiving workflow reports /// @dev All permission fields default to zero (disabled). Use setter functions to enable checks. -abstract contract IReceiverTemplate is IReceiver, Ownable { +abstract contract ReceiverTemplate is IReceiver, Ownable { // Optional permission fields (all default to zero = disabled) - address public forwarderAddress; // If set, only this address can call onReport - address public expectedAuthor; // If set, only reports from this workflow owner are accepted - bytes10 public expectedWorkflowName; // If set, only reports with this workflow name are accepted - bytes32 public expectedWorkflowId; // If set, only reports from this specific workflow ID are accepted + address private s_forwarderAddress; // If set, only this address can call onReport + address private s_expectedAuthor; // If set, only reports from this workflow owner are accepted + bytes10 private s_expectedWorkflowName; // If set, only reports with this workflow name are accepted + bytes32 private s_expectedWorkflowId; // If set, only reports from this specific workflow ID are accepted + + // Hex character lookup table for bytes-to-hex conversion + bytes private constant HEX_CHARS = "0123456789abcdef"; // Custom errors error InvalidSender(address sender, address expected); @@ -2418,30 +2421,60 @@ abstract contract IReceiverTemplate is IReceiver, Ownable { error InvalidWorkflowName(bytes10 received, bytes10 expected); error InvalidWorkflowId(bytes32 received, bytes32 expected); + // Events + event ForwarderAddressUpdated(address indexed previousForwarder, address indexed newForwarder); + event ExpectedAuthorUpdated(address indexed previousAuthor, address indexed newAuthor); + event ExpectedWorkflowNameUpdated(bytes10 indexed previousName, bytes10 indexed newName); + event ExpectedWorkflowIdUpdated(bytes32 indexed previousId, bytes32 indexed newId); + /// @notice Constructor sets msg.sender as the owner /// @dev All permission fields are initialized to zero (disabled by default) constructor() Ownable(msg.sender) {} + /// @notice Returns the configured forwarder address + /// @return The forwarder address (address(0) if not set) + function getForwarderAddress() external view returns (address) { + return s_forwarderAddress; + } + + /// @notice Returns the expected workflow author address + /// @return The expected author address (address(0) if not set) + function getExpectedAuthor() external view returns (address) { + return s_expectedAuthor; + } + + /// @notice Returns the expected workflow name + /// @return The expected workflow name (bytes10(0) if not set) + function getExpectedWorkflowName() external view returns (bytes10) { + return s_expectedWorkflowName; + } + + /// @notice Returns the expected workflow ID + /// @return The expected workflow ID (bytes32(0) if not set) + function getExpectedWorkflowId() external view returns (bytes32) { + return s_expectedWorkflowId; + } + /// @inheritdoc IReceiver /// @dev Performs optional validation checks based on which permission fields are set function onReport(bytes calldata metadata, bytes calldata report) external override { // Security Check 1: Verify caller is the trusted Chainlink Forwarder (if configured) - if (forwarderAddress != address(0) && msg.sender != forwarderAddress) { - revert InvalidSender(msg.sender, forwarderAddress); + if (s_forwarderAddress != address(0) && msg.sender != s_forwarderAddress) { + revert InvalidSender(msg.sender, s_forwarderAddress); } // Security Checks 2-4: Verify workflow identity - ID, owner, and/or name (if any are configured) - if (expectedWorkflowId != bytes32(0) || expectedAuthor != address(0) || expectedWorkflowName != bytes10(0)) { + if (s_expectedWorkflowId != bytes32(0) || s_expectedAuthor != address(0) || s_expectedWorkflowName != bytes10(0)) { (bytes32 workflowId, bytes10 workflowName, address workflowOwner) = _decodeMetadata(metadata); - if (expectedWorkflowId != bytes32(0) && workflowId != expectedWorkflowId) { - revert InvalidWorkflowId(workflowId, expectedWorkflowId); + if (s_expectedWorkflowId != bytes32(0) && workflowId != s_expectedWorkflowId) { + revert InvalidWorkflowId(workflowId, s_expectedWorkflowId); } - if (expectedAuthor != address(0) && workflowOwner != expectedAuthor) { - revert InvalidAuthor(workflowOwner, expectedAuthor); + if (s_expectedAuthor != address(0) && workflowOwner != s_expectedAuthor) { + revert InvalidAuthor(workflowOwner, s_expectedAuthor); } - if (expectedWorkflowName != bytes10(0) && workflowName != expectedWorkflowName) { - revert InvalidWorkflowName(workflowName, expectedWorkflowName); + if (s_expectedWorkflowName != bytes10(0) && workflowName != s_expectedWorkflowName) { + revert InvalidWorkflowName(workflowName, s_expectedWorkflowName); } } @@ -2451,21 +2484,28 @@ abstract contract IReceiverTemplate is IReceiver, Ownable { /// @notice Updates the forwarder address that is allowed to call onReport /// @param _forwarder The new forwarder address (use address(0) to disable this check) function setForwarderAddress(address _forwarder) external onlyOwner { - forwarderAddress = _forwarder; + address previousForwarder = s_forwarderAddress; + s_forwarderAddress = _forwarder; + emit ForwarderAddressUpdated(previousForwarder, _forwarder); } /// @notice Updates the expected workflow owner address /// @param _author The new expected author address (use address(0) to disable this check) function setExpectedAuthor(address _author) external onlyOwner { - expectedAuthor = _author; + address previousAuthor = s_expectedAuthor; + s_expectedAuthor = _author; + emit ExpectedAuthorUpdated(previousAuthor, _author); } /// @notice Updates the expected workflow name from a plaintext string /// @param _name The workflow name as a string (use empty string "" to disable this check) /// @dev The name is hashed using SHA256 and truncated function setExpectedWorkflowName(string calldata _name) external onlyOwner { + bytes10 previousName = s_expectedWorkflowName; + if (bytes(_name).length == 0) { - expectedWorkflowName = bytes10(0); + s_expectedWorkflowName = bytes10(0); + emit ExpectedWorkflowNameUpdated(previousName, bytes10(0)); return; } @@ -2474,35 +2514,37 @@ abstract contract IReceiverTemplate is IReceiver, Ownable { bytes32 hash = sha256(bytes(_name)); bytes memory hexString = _bytesToHexString(abi.encodePacked(hash)); bytes memory first10 = new bytes(10); - for (uint i = 0; i < 10; i++) { + for (uint256 i = 0; i < 10; i++) { first10[i] = hexString[i]; } - expectedWorkflowName = bytes10(first10); + s_expectedWorkflowName = bytes10(first10); + emit ExpectedWorkflowNameUpdated(previousName, s_expectedWorkflowName); } /// @notice Updates the expected workflow ID /// @param _id The new expected workflow ID (use bytes32(0) to disable this check) function setExpectedWorkflowId(bytes32 _id) external onlyOwner { - expectedWorkflowId = _id; + bytes32 previousId = s_expectedWorkflowId; + s_expectedWorkflowId = _id; + emit ExpectedWorkflowIdUpdated(previousId, _id); } /// @notice Helper function to convert bytes to hex string /// @param data The bytes to convert /// @return The hex string representation function _bytesToHexString(bytes memory data) private pure returns (bytes memory) { - bytes memory hexChars = "0123456789abcdef"; bytes memory hexString = new bytes(data.length * 2); for (uint256 i = 0; i < data.length; i++) { - hexString[i * 2] = hexChars[uint8(data[i] >> 4)]; - hexString[i * 2 + 1] = hexChars[uint8(data[i] & 0x0f)]; + hexString[i * 2] = HEX_CHARS[uint8(data[i] >> 4)]; + hexString[i * 2 + 1] = HEX_CHARS[uint8(data[i] & 0x0f)]; } return hexString; } /// @notice Extracts all metadata fields from the onReport metadata parameter - /// @param metadata The metadata in bytes format + /// @param metadata The metadata bytes encoded using abi.encodePacked(workflowId, workflowName, workflowOwner) /// @return workflowId The unique identifier of the workflow (bytes32) /// @return workflowName The name of the workflow (bytes10) /// @return workflowOwner The owner address of the workflow @@ -2511,7 +2553,7 @@ abstract contract IReceiverTemplate is IReceiver, Ownable { pure returns (bytes32 workflowId, bytes10 workflowName, address workflowOwner) { - // Metadata structure: + // Metadata structure (encoded using abi.encodePacked by the Forwarder): // - First 32 bytes: length of the byte array (standard for dynamic bytes) // - Offset 32, size 32: workflow_id (bytes32) // - Offset 64, size 10: workflow_name (bytes10) @@ -2521,6 +2563,7 @@ abstract contract IReceiverTemplate is IReceiver, Ownable { workflowName := mload(add(metadata, 64)) workflowOwner := shr(mul(12, 8), mload(add(metadata, 74))) } + return (workflowId, workflowName, workflowOwner); } /// @notice Abstract function to process the report data @@ -2537,24 +2580,24 @@ abstract contract IReceiverTemplate is IReceiver, Ownable { ### 3.3 Quick Start -The simplest way to use `IReceiverTemplate` is to inherit from it and implement the `_processReport` function: +The simplest way to use `ReceiverTemplate` is to inherit from it and implement the `_processReport` function: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; -import { IReceiverTemplate } from "./IReceiverTemplate.sol"; +import { ReceiverTemplate } from "./ReceiverTemplate.sol"; -contract MyConsumer is IReceiverTemplate { - uint256 public storedValue; +contract MyConsumer is ReceiverTemplate { + uint256 public s_storedValue; event ValueUpdated(uint256 newValue); // Simple constructor - no parameters needed - constructor() IReceiverTemplate() {} + constructor() ReceiverTemplate() {} // Implement your business logic here function _processReport(bytes calldata report) internal override { uint256 newValue = abi.decode(report, (uint256)); - storedValue = newValue; + s_storedValue = newValue; emit ValueUpdated(newValue); } } @@ -2613,7 +2656,7 @@ myConsumer.setExpectedWorkflowName(""); // Empty string disables the check #### How workflow names are encoded -The `workflowName` field in the metadata uses the **`bytes10`** type rather than plaintext strings. When you call `setExpectedWorkflowName("my_workflow")`, the `IReceiverTemplate` automatically encodes it using the same algorithm as the CRE engine: +The `workflowName` field in the metadata uses the **`bytes10`** type rather than plaintext strings. When you call `setExpectedWorkflowName("my_workflow")`, the `ReceiverTemplate` automatically encodes it using the same algorithm as the CRE engine: 1. Compute SHA256 hash of the workflow name 2. Convert hash to hex string (64 characters) @@ -2692,25 +2735,27 @@ See [Configuring Permissions](#34-configuring-permissions) for complete details. You can override `onReport` to add your own validation logic before or after the standard checks: ```solidity -import { IReceiverTemplate } from "./IReceiverTemplate.sol"; +import { ReceiverTemplate } from "./ReceiverTemplate.sol"; -contract AdvancedConsumer is IReceiverTemplate { - uint256 public minReportInterval = 1 hours; - uint256 public lastReportTime; +contract AdvancedConsumer is ReceiverTemplate { + uint256 private s_minReportInterval = 1 hours; + uint256 private s_lastReportTime; error ReportTooFrequent(uint256 timeSinceLastReport, uint256 minInterval); + event MinReportIntervalUpdated(uint256 previousInterval, uint256 newInterval); + // Add custom validation before parent's checks function onReport(bytes calldata metadata, bytes calldata report) external override { // Custom check: Rate limiting - if (block.timestamp < lastReportTime + minReportInterval) { - revert ReportTooFrequent(block.timestamp - lastReportTime, minReportInterval); + if (block.timestamp < s_lastReportTime + s_minReportInterval) { + revert ReportTooFrequent(block.timestamp - s_lastReportTime, s_minReportInterval); } // Call parent implementation for standard permission checks super.onReport(metadata, report); - lastReportTime = block.timestamp; + s_lastReportTime = block.timestamp; } function _processReport(bytes calldata report) internal override { @@ -2719,9 +2764,24 @@ contract AdvancedConsumer is IReceiverTemplate { // ... store or process the value ... } - // Allow owner to update rate limit + /// @notice Returns the minimum interval between reports + /// @return The minimum interval in seconds + function getMinReportInterval() external view returns (uint256) { + return s_minReportInterval; + } + + /// @notice Returns the timestamp of the last report + /// @return The last report timestamp + function getLastReportTime() external view returns (uint256) { + return s_lastReportTime; + } + + /// @notice Updates the minimum interval between reports + /// @param _interval The new minimum interval in seconds function setMinReportInterval(uint256 _interval) external onlyOwner { - minReportInterval = _interval; + uint256 previousInterval = s_minReportInterval; + s_minReportInterval = _interval; + emit MinReportIntervalUpdated(previousInterval, _interval); } } ``` @@ -2731,8 +2791,8 @@ contract AdvancedConsumer is IReceiverTemplate { The `_decodeMetadata` helper function is available for use in your `_processReport` implementation. This allows you to access workflow metadata for custom business logic: ```solidity -contract MetadataAwareConsumer is IReceiverTemplate { - mapping(bytes32 => uint256) public reportCountByWorkflow; +contract MetadataAwareConsumer is ReceiverTemplate { + mapping(bytes32 => uint256) public s_reportCountByWorkflow; function _processReport(bytes calldata report) internal override { // Access the metadata to get workflow ID @@ -2740,7 +2800,7 @@ contract MetadataAwareConsumer is IReceiverTemplate { (bytes32 workflowId, , ) = _decodeMetadata(metadata); // Use workflow ID in your business logic - reportCountByWorkflow[workflowId]++; + s_reportCountByWorkflow[workflowId]++; // Process the report data uint256 value = abi.decode(report, (uint256)); @@ -2758,23 +2818,23 @@ contract MetadataAwareConsumer is IReceiverTemplate { ### Example 1: Simple Consumer Contract -This example inherits from `IReceiverTemplate` to store a temperature value. +This example inherits from `ReceiverTemplate` to store a temperature value. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; -import { IReceiverTemplate } from "./IReceiverTemplate.sol"; +import { ReceiverTemplate } from "./ReceiverTemplate.sol"; -contract TemperatureConsumer is IReceiverTemplate { - int256 public currentTemperature; +contract TemperatureConsumer is ReceiverTemplate { + int256 public s_currentTemperature; event TemperatureUpdated(int256 newTemperature); // Simple constructor - no parameters needed - constructor() IReceiverTemplate() {} + constructor() ReceiverTemplate() {} function _processReport(bytes calldata report) internal override { int256 newTemperature = abi.decode(report, (int256)); - currentTemperature = newTemperature; + s_currentTemperature = newTemperature; emit TemperatureUpdated(newTemperature); } } @@ -2795,7 +2855,7 @@ temperatureConsumer.setExpectedWorkflowId(0xYourWorkflowId...); For more complex scenarios, it's best to separate your Chainlink-aware code from your core business logic. The **Proxy Pattern** is a robust architecture that uses two contracts to achieve this: - **A Logic Contract**: Holds the state and the core functions of your application. It knows nothing about the Forwarder contract or the `onReport` function. -- **A Proxy Contract**: Acts as the secure entry point. It inherits from `IReceiverTemplate` and forwards validated reports to the Logic Contract. +- **A Proxy Contract**: Acts as the secure entry point. It inherits from `ReceiverTemplate` and forwards validated reports to the Logic Contract. This separation makes your business logic more modular and reusable. @@ -2815,28 +2875,59 @@ contract ReserveManager is Ownable { uint256 btcPrice; } - address public proxyAddress; - uint256 public lastEthPrice; - uint256 public lastBtcPrice; - uint256 public lastUpdateTime; + address private s_proxyAddress; + uint256 private s_lastEthPrice; + uint256 private s_lastBtcPrice; + uint256 private s_lastUpdateTime; event ReservesUpdated(uint256 ethPrice, uint256 btcPrice, uint256 updateTime); + event ProxyAddressUpdated(address indexed previousProxy, address indexed newProxy); modifier onlyProxy() { - require(msg.sender == proxyAddress, "Caller is not the authorized proxy"); + require(msg.sender == s_proxyAddress, "Caller is not the authorized proxy"); _; } constructor() Ownable(msg.sender) {} + /// @notice Returns the proxy address + /// @return The authorized proxy address + function getProxyAddress() external view returns (address) { + return s_proxyAddress; + } + + /// @notice Returns the last ETH price + /// @return The last recorded ETH price + function getLastEthPrice() external view returns (uint256) { + return s_lastEthPrice; + } + + /// @notice Returns the last BTC price + /// @return The last recorded BTC price + function getLastBtcPrice() external view returns (uint256) { + return s_lastBtcPrice; + } + + /// @notice Returns the last update timestamp + /// @return The timestamp of the last update + function getLastUpdateTime() external view returns (uint256) { + return s_lastUpdateTime; + } + + /// @notice Updates the authorized proxy address + /// @param _proxyAddress The new proxy address function setProxyAddress(address _proxyAddress) external onlyOwner { - proxyAddress = _proxyAddress; + address previousProxy = s_proxyAddress; + s_proxyAddress = _proxyAddress; + emit ProxyAddressUpdated(previousProxy, _proxyAddress); } + /// @notice Updates the reserve prices + /// @param data The new reserve data containing ETH and BTC prices function updateReserves(UpdateReserves memory data) external onlyProxy { - lastEthPrice = data.ethPrice; - lastBtcPrice = data.btcPrice; - lastUpdateTime = block.timestamp; + s_lastEthPrice = data.ethPrice; + s_lastBtcPrice = data.btcPrice; + s_lastUpdateTime = block.timestamp; emit ReservesUpdated(data.ethPrice, data.btcPrice, block.timestamp); } } @@ -2844,23 +2935,29 @@ contract ReserveManager is Ownable { #### The Proxy Contract (`UpdateReservesProxy.sol`) -This contract, our "bouncer", is the only contract that interacts with the Chainlink platform. It inherits `IReceiverTemplate` to validate incoming reports and then calls the `ReserveManager`. +This contract, our "bouncer", is the only contract that interacts with the Chainlink platform. It inherits `ReceiverTemplate` to validate incoming reports and then calls the `ReserveManager`. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import { ReserveManager } from "./ReserveManager.sol"; -import { IReceiverTemplate } from "./keystone/IReceiverTemplate.sol"; +import { ReceiverTemplate } from "./ReceiverTemplate.sol"; -contract UpdateReservesProxy is IReceiverTemplate { - ReserveManager public s_reserveManager; +contract UpdateReservesProxy is ReceiverTemplate { + ReserveManager private s_reserveManager; constructor(address reserveManagerAddress) { s_reserveManager = ReserveManager(reserveManagerAddress); } - /// @inheritdoc IReceiverTemplate + /// @notice Returns the reserve manager contract address + /// @return The ReserveManager contract instance + function getReserveManager() external view returns (ReserveManager) { + return s_reserveManager; + } + + /// @inheritdoc ReceiverTemplate function _processReport(bytes calldata report) internal override { ReserveManager.UpdateReserves memory updateReservesData = abi.decode(report, (ReserveManager.UpdateReserves)); s_reserveManager.updateReserves(updateReservesData); @@ -9227,16 +9324,16 @@ Here is the source code for the contract so you can see how it works: // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import { IReceiverTemplate } from "./keystone/IReceiverTemplate.sol"; +import { ReceiverTemplate } from "./ReceiverTemplate.sol"; /** * @title CalculatorConsumer (Testing Version) * @notice This contract receives reports from a CRE workflow and stores the results of a calculation onchain. - * @dev This version uses IReceiverTemplate without configuring any security checks, making it compatible + * @dev This version uses ReceiverTemplate without configuring any security checks, making it compatible * with the mock Forwarder used during simulation. All permission fields remain at their default zero * values (disabled). */ -contract CalculatorConsumer is IReceiverTemplate { +contract CalculatorConsumer is ReceiverTemplate { // Struct to hold the data sent in a report from the workflow struct CalculatorResult { uint256 offchainValue; @@ -9254,13 +9351,13 @@ contract CalculatorConsumer is IReceiverTemplate { /** * @dev The constructor doesn't set any security checks. - * The IReceiverTemplate parent constructor will initialize all permission fields to zero (disabled). + * The ReceiverTemplate parent constructor will initialize all permission fields to zero (disabled). */ constructor() {} /** * @notice Implements the core business logic for processing reports. - * @dev This is called automatically by IReceiverTemplate's onReport function after security checks. + * @dev This is called automatically by ReceiverTemplate's onReport function after security checks. */ function _processReport(bytes calldata report) internal override { // Decode the report bytes into our CalculatorResult struct diff --git a/src/content/cre/llms-full-ts.txt b/src/content/cre/llms-full-ts.txt index 4c686b4fe36..4c79ca12976 100644 --- a/src/content/cre/llms-full-ts.txt +++ b/src/content/cre/llms-full-ts.txt @@ -1933,7 +1933,7 @@ This guide explains how to build a consumer contract that can securely receive a - [Core Concepts: The Onchain Data Flow](#1-core-concepts-the-onchain-data-flow) - [The IReceiver Standard](#2-the-ireceiver-standard) -- [Using IReceiverTemplate](#3-using-ireceivertemplate) +- [Using ReceiverTemplate](#3-using-receivertemplate) - [Working with Simulation](#4-working-with-simulation) - [Advanced Usage](#5-advanced-usage-optional) - [Complete Examples](#6-complete-examples) @@ -1959,18 +1959,18 @@ interface IReceiver is IERC165 { } ``` -- `metadata`: Contains information about the workflow (ID, name, owner). +- `metadata`: Contains information about the workflow (ID, name, owner). This is encoded by the Forwarder using `abi.encodePacked` with the following structure: `bytes32 workflowId`, `bytes10 workflowName`, `address workflowOwner`. - `report`: The raw, ABI-encoded data payload from your workflow. ### 2.2 Support ERC165 Interface Detection [ERC165](https://eips.ethereum.org/EIPS/eip-165) is a standard that allows contracts to publish the interfaces they support. The `KeystoneForwarder` uses this to check if your contract supports the `IReceiver` interface before sending a report. -## 3. Using `IReceiverTemplate` +## 3. Using `ReceiverTemplate` ### 3.1 Overview -While you can implement these standards manually, we provide an abstract contract, `IReceiverTemplate.sol`, that does the heavy lifting for you. Inheriting from it is the recommended best practice. +While you can implement these standards manually, we provide an abstract contract, `ReceiverTemplate.sol`, that does the heavy lifting for you. Inheriting from it is the recommended best practice. **Key features:** @@ -1991,15 +1991,18 @@ import {IERC165} from "./IERC165.sol"; import {IReceiver} from "./IReceiver.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -/// @title IReceiverTemplate - Abstract receiver with optional permission controls +/// @title ReceiverTemplate - Abstract receiver with optional permission controls /// @notice Provides flexible, updatable security checks for receiving workflow reports /// @dev All permission fields default to zero (disabled). Use setter functions to enable checks. -abstract contract IReceiverTemplate is IReceiver, Ownable { +abstract contract ReceiverTemplate is IReceiver, Ownable { // Optional permission fields (all default to zero = disabled) - address public forwarderAddress; // If set, only this address can call onReport - address public expectedAuthor; // If set, only reports from this workflow owner are accepted - bytes10 public expectedWorkflowName; // If set, only reports with this workflow name are accepted - bytes32 public expectedWorkflowId; // If set, only reports from this specific workflow ID are accepted + address private s_forwarderAddress; // If set, only this address can call onReport + address private s_expectedAuthor; // If set, only reports from this workflow owner are accepted + bytes10 private s_expectedWorkflowName; // If set, only reports with this workflow name are accepted + bytes32 private s_expectedWorkflowId; // If set, only reports from this specific workflow ID are accepted + + // Hex character lookup table for bytes-to-hex conversion + bytes private constant HEX_CHARS = "0123456789abcdef"; // Custom errors error InvalidSender(address sender, address expected); @@ -2007,30 +2010,60 @@ abstract contract IReceiverTemplate is IReceiver, Ownable { error InvalidWorkflowName(bytes10 received, bytes10 expected); error InvalidWorkflowId(bytes32 received, bytes32 expected); + // Events + event ForwarderAddressUpdated(address indexed previousForwarder, address indexed newForwarder); + event ExpectedAuthorUpdated(address indexed previousAuthor, address indexed newAuthor); + event ExpectedWorkflowNameUpdated(bytes10 indexed previousName, bytes10 indexed newName); + event ExpectedWorkflowIdUpdated(bytes32 indexed previousId, bytes32 indexed newId); + /// @notice Constructor sets msg.sender as the owner /// @dev All permission fields are initialized to zero (disabled by default) constructor() Ownable(msg.sender) {} + /// @notice Returns the configured forwarder address + /// @return The forwarder address (address(0) if not set) + function getForwarderAddress() external view returns (address) { + return s_forwarderAddress; + } + + /// @notice Returns the expected workflow author address + /// @return The expected author address (address(0) if not set) + function getExpectedAuthor() external view returns (address) { + return s_expectedAuthor; + } + + /// @notice Returns the expected workflow name + /// @return The expected workflow name (bytes10(0) if not set) + function getExpectedWorkflowName() external view returns (bytes10) { + return s_expectedWorkflowName; + } + + /// @notice Returns the expected workflow ID + /// @return The expected workflow ID (bytes32(0) if not set) + function getExpectedWorkflowId() external view returns (bytes32) { + return s_expectedWorkflowId; + } + /// @inheritdoc IReceiver /// @dev Performs optional validation checks based on which permission fields are set function onReport(bytes calldata metadata, bytes calldata report) external override { // Security Check 1: Verify caller is the trusted Chainlink Forwarder (if configured) - if (forwarderAddress != address(0) && msg.sender != forwarderAddress) { - revert InvalidSender(msg.sender, forwarderAddress); + if (s_forwarderAddress != address(0) && msg.sender != s_forwarderAddress) { + revert InvalidSender(msg.sender, s_forwarderAddress); } // Security Checks 2-4: Verify workflow identity - ID, owner, and/or name (if any are configured) - if (expectedWorkflowId != bytes32(0) || expectedAuthor != address(0) || expectedWorkflowName != bytes10(0)) { + if (s_expectedWorkflowId != bytes32(0) || s_expectedAuthor != address(0) || s_expectedWorkflowName != bytes10(0)) { (bytes32 workflowId, bytes10 workflowName, address workflowOwner) = _decodeMetadata(metadata); - if (expectedWorkflowId != bytes32(0) && workflowId != expectedWorkflowId) { - revert InvalidWorkflowId(workflowId, expectedWorkflowId); + if (s_expectedWorkflowId != bytes32(0) && workflowId != s_expectedWorkflowId) { + revert InvalidWorkflowId(workflowId, s_expectedWorkflowId); } - if (expectedAuthor != address(0) && workflowOwner != expectedAuthor) { - revert InvalidAuthor(workflowOwner, expectedAuthor); + if (s_expectedAuthor != address(0) && workflowOwner != s_expectedAuthor) { + revert InvalidAuthor(workflowOwner, s_expectedAuthor); } - if (expectedWorkflowName != bytes10(0) && workflowName != expectedWorkflowName) { - revert InvalidWorkflowName(workflowName, expectedWorkflowName); + if (s_expectedWorkflowName != bytes10(0) && workflowName != s_expectedWorkflowName) { + revert InvalidWorkflowName(workflowName, s_expectedWorkflowName); } } @@ -2040,21 +2073,28 @@ abstract contract IReceiverTemplate is IReceiver, Ownable { /// @notice Updates the forwarder address that is allowed to call onReport /// @param _forwarder The new forwarder address (use address(0) to disable this check) function setForwarderAddress(address _forwarder) external onlyOwner { - forwarderAddress = _forwarder; + address previousForwarder = s_forwarderAddress; + s_forwarderAddress = _forwarder; + emit ForwarderAddressUpdated(previousForwarder, _forwarder); } /// @notice Updates the expected workflow owner address /// @param _author The new expected author address (use address(0) to disable this check) function setExpectedAuthor(address _author) external onlyOwner { - expectedAuthor = _author; + address previousAuthor = s_expectedAuthor; + s_expectedAuthor = _author; + emit ExpectedAuthorUpdated(previousAuthor, _author); } /// @notice Updates the expected workflow name from a plaintext string /// @param _name The workflow name as a string (use empty string "" to disable this check) /// @dev The name is hashed using SHA256 and truncated function setExpectedWorkflowName(string calldata _name) external onlyOwner { + bytes10 previousName = s_expectedWorkflowName; + if (bytes(_name).length == 0) { - expectedWorkflowName = bytes10(0); + s_expectedWorkflowName = bytes10(0); + emit ExpectedWorkflowNameUpdated(previousName, bytes10(0)); return; } @@ -2063,35 +2103,37 @@ abstract contract IReceiverTemplate is IReceiver, Ownable { bytes32 hash = sha256(bytes(_name)); bytes memory hexString = _bytesToHexString(abi.encodePacked(hash)); bytes memory first10 = new bytes(10); - for (uint i = 0; i < 10; i++) { + for (uint256 i = 0; i < 10; i++) { first10[i] = hexString[i]; } - expectedWorkflowName = bytes10(first10); + s_expectedWorkflowName = bytes10(first10); + emit ExpectedWorkflowNameUpdated(previousName, s_expectedWorkflowName); } /// @notice Updates the expected workflow ID /// @param _id The new expected workflow ID (use bytes32(0) to disable this check) function setExpectedWorkflowId(bytes32 _id) external onlyOwner { - expectedWorkflowId = _id; + bytes32 previousId = s_expectedWorkflowId; + s_expectedWorkflowId = _id; + emit ExpectedWorkflowIdUpdated(previousId, _id); } /// @notice Helper function to convert bytes to hex string /// @param data The bytes to convert /// @return The hex string representation function _bytesToHexString(bytes memory data) private pure returns (bytes memory) { - bytes memory hexChars = "0123456789abcdef"; bytes memory hexString = new bytes(data.length * 2); for (uint256 i = 0; i < data.length; i++) { - hexString[i * 2] = hexChars[uint8(data[i] >> 4)]; - hexString[i * 2 + 1] = hexChars[uint8(data[i] & 0x0f)]; + hexString[i * 2] = HEX_CHARS[uint8(data[i] >> 4)]; + hexString[i * 2 + 1] = HEX_CHARS[uint8(data[i] & 0x0f)]; } return hexString; } /// @notice Extracts all metadata fields from the onReport metadata parameter - /// @param metadata The metadata in bytes format + /// @param metadata The metadata bytes encoded using abi.encodePacked(workflowId, workflowName, workflowOwner) /// @return workflowId The unique identifier of the workflow (bytes32) /// @return workflowName The name of the workflow (bytes10) /// @return workflowOwner The owner address of the workflow @@ -2100,7 +2142,7 @@ abstract contract IReceiverTemplate is IReceiver, Ownable { pure returns (bytes32 workflowId, bytes10 workflowName, address workflowOwner) { - // Metadata structure: + // Metadata structure (encoded using abi.encodePacked by the Forwarder): // - First 32 bytes: length of the byte array (standard for dynamic bytes) // - Offset 32, size 32: workflow_id (bytes32) // - Offset 64, size 10: workflow_name (bytes10) @@ -2110,6 +2152,7 @@ abstract contract IReceiverTemplate is IReceiver, Ownable { workflowName := mload(add(metadata, 64)) workflowOwner := shr(mul(12, 8), mload(add(metadata, 74))) } + return (workflowId, workflowName, workflowOwner); } /// @notice Abstract function to process the report data @@ -2126,24 +2169,24 @@ abstract contract IReceiverTemplate is IReceiver, Ownable { ### 3.3 Quick Start -The simplest way to use `IReceiverTemplate` is to inherit from it and implement the `_processReport` function: +The simplest way to use `ReceiverTemplate` is to inherit from it and implement the `_processReport` function: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; -import { IReceiverTemplate } from "./IReceiverTemplate.sol"; +import { ReceiverTemplate } from "./ReceiverTemplate.sol"; -contract MyConsumer is IReceiverTemplate { - uint256 public storedValue; +contract MyConsumer is ReceiverTemplate { + uint256 public s_storedValue; event ValueUpdated(uint256 newValue); // Simple constructor - no parameters needed - constructor() IReceiverTemplate() {} + constructor() ReceiverTemplate() {} // Implement your business logic here function _processReport(bytes calldata report) internal override { uint256 newValue = abi.decode(report, (uint256)); - storedValue = newValue; + s_storedValue = newValue; emit ValueUpdated(newValue); } } @@ -2202,7 +2245,7 @@ myConsumer.setExpectedWorkflowName(""); // Empty string disables the check #### How workflow names are encoded -The `workflowName` field in the metadata uses the **`bytes10`** type rather than plaintext strings. When you call `setExpectedWorkflowName("my_workflow")`, the `IReceiverTemplate` automatically encodes it using the same algorithm as the CRE engine: +The `workflowName` field in the metadata uses the **`bytes10`** type rather than plaintext strings. When you call `setExpectedWorkflowName("my_workflow")`, the `ReceiverTemplate` automatically encodes it using the same algorithm as the CRE engine: 1. Compute SHA256 hash of the workflow name 2. Convert hash to hex string (64 characters) @@ -2281,25 +2324,27 @@ See [Configuring Permissions](#34-configuring-permissions) for complete details. You can override `onReport` to add your own validation logic before or after the standard checks: ```solidity -import { IReceiverTemplate } from "./IReceiverTemplate.sol"; +import { ReceiverTemplate } from "./ReceiverTemplate.sol"; -contract AdvancedConsumer is IReceiverTemplate { - uint256 public minReportInterval = 1 hours; - uint256 public lastReportTime; +contract AdvancedConsumer is ReceiverTemplate { + uint256 private s_minReportInterval = 1 hours; + uint256 private s_lastReportTime; error ReportTooFrequent(uint256 timeSinceLastReport, uint256 minInterval); + event MinReportIntervalUpdated(uint256 previousInterval, uint256 newInterval); + // Add custom validation before parent's checks function onReport(bytes calldata metadata, bytes calldata report) external override { // Custom check: Rate limiting - if (block.timestamp < lastReportTime + minReportInterval) { - revert ReportTooFrequent(block.timestamp - lastReportTime, minReportInterval); + if (block.timestamp < s_lastReportTime + s_minReportInterval) { + revert ReportTooFrequent(block.timestamp - s_lastReportTime, s_minReportInterval); } // Call parent implementation for standard permission checks super.onReport(metadata, report); - lastReportTime = block.timestamp; + s_lastReportTime = block.timestamp; } function _processReport(bytes calldata report) internal override { @@ -2308,9 +2353,24 @@ contract AdvancedConsumer is IReceiverTemplate { // ... store or process the value ... } - // Allow owner to update rate limit + /// @notice Returns the minimum interval between reports + /// @return The minimum interval in seconds + function getMinReportInterval() external view returns (uint256) { + return s_minReportInterval; + } + + /// @notice Returns the timestamp of the last report + /// @return The last report timestamp + function getLastReportTime() external view returns (uint256) { + return s_lastReportTime; + } + + /// @notice Updates the minimum interval between reports + /// @param _interval The new minimum interval in seconds function setMinReportInterval(uint256 _interval) external onlyOwner { - minReportInterval = _interval; + uint256 previousInterval = s_minReportInterval; + s_minReportInterval = _interval; + emit MinReportIntervalUpdated(previousInterval, _interval); } } ``` @@ -2320,8 +2380,8 @@ contract AdvancedConsumer is IReceiverTemplate { The `_decodeMetadata` helper function is available for use in your `_processReport` implementation. This allows you to access workflow metadata for custom business logic: ```solidity -contract MetadataAwareConsumer is IReceiverTemplate { - mapping(bytes32 => uint256) public reportCountByWorkflow; +contract MetadataAwareConsumer is ReceiverTemplate { + mapping(bytes32 => uint256) public s_reportCountByWorkflow; function _processReport(bytes calldata report) internal override { // Access the metadata to get workflow ID @@ -2329,7 +2389,7 @@ contract MetadataAwareConsumer is IReceiverTemplate { (bytes32 workflowId, , ) = _decodeMetadata(metadata); // Use workflow ID in your business logic - reportCountByWorkflow[workflowId]++; + s_reportCountByWorkflow[workflowId]++; // Process the report data uint256 value = abi.decode(report, (uint256)); @@ -2347,23 +2407,23 @@ contract MetadataAwareConsumer is IReceiverTemplate { ### Example 1: Simple Consumer Contract -This example inherits from `IReceiverTemplate` to store a temperature value. +This example inherits from `ReceiverTemplate` to store a temperature value. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; -import { IReceiverTemplate } from "./IReceiverTemplate.sol"; +import { ReceiverTemplate } from "./ReceiverTemplate.sol"; -contract TemperatureConsumer is IReceiverTemplate { - int256 public currentTemperature; +contract TemperatureConsumer is ReceiverTemplate { + int256 public s_currentTemperature; event TemperatureUpdated(int256 newTemperature); // Simple constructor - no parameters needed - constructor() IReceiverTemplate() {} + constructor() ReceiverTemplate() {} function _processReport(bytes calldata report) internal override { int256 newTemperature = abi.decode(report, (int256)); - currentTemperature = newTemperature; + s_currentTemperature = newTemperature; emit TemperatureUpdated(newTemperature); } } @@ -2384,7 +2444,7 @@ temperatureConsumer.setExpectedWorkflowId(0xYourWorkflowId...); For more complex scenarios, it's best to separate your Chainlink-aware code from your core business logic. The **Proxy Pattern** is a robust architecture that uses two contracts to achieve this: - **A Logic Contract**: Holds the state and the core functions of your application. It knows nothing about the Forwarder contract or the `onReport` function. -- **A Proxy Contract**: Acts as the secure entry point. It inherits from `IReceiverTemplate` and forwards validated reports to the Logic Contract. +- **A Proxy Contract**: Acts as the secure entry point. It inherits from `ReceiverTemplate` and forwards validated reports to the Logic Contract. This separation makes your business logic more modular and reusable. @@ -2404,28 +2464,59 @@ contract ReserveManager is Ownable { uint256 btcPrice; } - address public proxyAddress; - uint256 public lastEthPrice; - uint256 public lastBtcPrice; - uint256 public lastUpdateTime; + address private s_proxyAddress; + uint256 private s_lastEthPrice; + uint256 private s_lastBtcPrice; + uint256 private s_lastUpdateTime; event ReservesUpdated(uint256 ethPrice, uint256 btcPrice, uint256 updateTime); + event ProxyAddressUpdated(address indexed previousProxy, address indexed newProxy); modifier onlyProxy() { - require(msg.sender == proxyAddress, "Caller is not the authorized proxy"); + require(msg.sender == s_proxyAddress, "Caller is not the authorized proxy"); _; } constructor() Ownable(msg.sender) {} + /// @notice Returns the proxy address + /// @return The authorized proxy address + function getProxyAddress() external view returns (address) { + return s_proxyAddress; + } + + /// @notice Returns the last ETH price + /// @return The last recorded ETH price + function getLastEthPrice() external view returns (uint256) { + return s_lastEthPrice; + } + + /// @notice Returns the last BTC price + /// @return The last recorded BTC price + function getLastBtcPrice() external view returns (uint256) { + return s_lastBtcPrice; + } + + /// @notice Returns the last update timestamp + /// @return The timestamp of the last update + function getLastUpdateTime() external view returns (uint256) { + return s_lastUpdateTime; + } + + /// @notice Updates the authorized proxy address + /// @param _proxyAddress The new proxy address function setProxyAddress(address _proxyAddress) external onlyOwner { - proxyAddress = _proxyAddress; + address previousProxy = s_proxyAddress; + s_proxyAddress = _proxyAddress; + emit ProxyAddressUpdated(previousProxy, _proxyAddress); } + /// @notice Updates the reserve prices + /// @param data The new reserve data containing ETH and BTC prices function updateReserves(UpdateReserves memory data) external onlyProxy { - lastEthPrice = data.ethPrice; - lastBtcPrice = data.btcPrice; - lastUpdateTime = block.timestamp; + s_lastEthPrice = data.ethPrice; + s_lastBtcPrice = data.btcPrice; + s_lastUpdateTime = block.timestamp; emit ReservesUpdated(data.ethPrice, data.btcPrice, block.timestamp); } } @@ -2433,23 +2524,29 @@ contract ReserveManager is Ownable { #### The Proxy Contract (`UpdateReservesProxy.sol`) -This contract, our "bouncer", is the only contract that interacts with the Chainlink platform. It inherits `IReceiverTemplate` to validate incoming reports and then calls the `ReserveManager`. +This contract, our "bouncer", is the only contract that interacts with the Chainlink platform. It inherits `ReceiverTemplate` to validate incoming reports and then calls the `ReserveManager`. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import { ReserveManager } from "./ReserveManager.sol"; -import { IReceiverTemplate } from "./keystone/IReceiverTemplate.sol"; +import { ReceiverTemplate } from "./ReceiverTemplate.sol"; -contract UpdateReservesProxy is IReceiverTemplate { - ReserveManager public s_reserveManager; +contract UpdateReservesProxy is ReceiverTemplate { + ReserveManager private s_reserveManager; constructor(address reserveManagerAddress) { s_reserveManager = ReserveManager(reserveManagerAddress); } - /// @inheritdoc IReceiverTemplate + /// @notice Returns the reserve manager contract address + /// @return The ReserveManager contract instance + function getReserveManager() external view returns (ReserveManager) { + return s_reserveManager; + } + + /// @inheritdoc ReceiverTemplate function _processReport(bytes calldata report) internal override { ReserveManager.UpdateReserves memory updateReservesData = abi.decode(report, (ReserveManager.UpdateReserves)); s_reserveManager.updateReserves(updateReservesData); @@ -7999,16 +8096,16 @@ Here is the source code for the contract so you can see how it works: // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import { IReceiverTemplate } from "./keystone/IReceiverTemplate.sol"; +import { ReceiverTemplate } from "./ReceiverTemplate.sol"; /** * @title CalculatorConsumer (Testing Version) * @notice This contract receives reports from a CRE workflow and stores the results of a calculation onchain. - * @dev This version uses IReceiverTemplate without configuring any security checks, making it compatible + * @dev This version uses ReceiverTemplate without configuring any security checks, making it compatible * with the mock Forwarder used during simulation. All permission fields remain at their default zero * values (disabled). */ -contract CalculatorConsumer is IReceiverTemplate { +contract CalculatorConsumer is ReceiverTemplate { // Struct to hold the data sent in a report from the workflow struct CalculatorResult { uint256 offchainValue; @@ -8026,13 +8123,13 @@ contract CalculatorConsumer is IReceiverTemplate { /** * @dev The constructor doesn't set any security checks. - * The IReceiverTemplate parent constructor will initialize all permission fields to zero (disabled). + * The ReceiverTemplate parent constructor will initialize all permission fields to zero (disabled). */ constructor() {} /** * @notice Implements the core business logic for processing reports. - * @dev This is called automatically by IReceiverTemplate's onReport function after security checks. + * @dev This is called automatically by ReceiverTemplate's onReport function after security checks. */ function _processReport(bytes calldata report) internal override { // Decode the report bytes into our CalculatorResult struct diff --git a/src/content/resources/llms-full.txt b/src/content/resources/llms-full.txt index dd00f71e5fc..43d5db2917e 100644 --- a/src/content/resources/llms-full.txt +++ b/src/content/resources/llms-full.txt @@ -2023,6 +2023,16 @@ MOVR is used to pay transaction fees on Moonriver Mainnet. | Symbol | LINK | | Decimals | 18 | +### Morph Hoodi Testnet + +| Parameter | Value | +| :-------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Chain ID | `2910` | +| Address |
| +| Name | Chainlink Token on Morph Hoodi Testnet | +| Symbol | LINK | +| Decimals | 18 | + ## Neo X ### Neo X Mainnet