-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathKeystoneReceiver.sol
More file actions
228 lines (197 loc) · 11.2 KB
/
Copy pathKeystoneReceiver.sol
File metadata and controls
228 lines (197 loc) · 11.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
// SPDX-License-Identifier: MIT
pragma solidity 0.8.33;
import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import { Ownable2Step, Ownable } from "@openzeppelin/contracts/access/Ownable2Step.sol";
import { IReceiver } from "./IReceiver.sol";
/// @title KeystoneReceiver - Abstract receiver with optional permission controls
/// @notice Provides flexible, updatable security checks for receiving workflow reports
/// @dev The forwarder address is required at construction time for security.
/// Additional permission fields can be configured using setter functions.
abstract contract KeystoneReceiver is IReceiver, Ownable2Step {
// Required permission field at deployment, configurable after
address private s_forwarderAddress; // If set, only this address can call onReport
// Optional permission fields (all default to zero = disabled)
address private s_expectedAuthor; // If set, only reports from this workflow owner are accepted
bytes10 private s_expectedWorkflowName; // Only validated when s_expectedAuthor is also set
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 InvalidForwarderAddress();
error InvalidSender(address sender, address expected);
error InvalidAuthor(address received, address expected);
error InvalidWorkflowName(bytes10 received, bytes10 expected);
error InvalidWorkflowId(bytes32 received, bytes32 expected);
error WorkflowNameRequiresAuthorValidation();
// 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);
event SecurityWarning(string message);
/// @notice Constructor sets msg.sender as the owner and configures the forwarder address
/// @param _forwarderAddress The address of the Chainlink Forwarder contract (cannot be address(0))
/// @dev The forwarder address is required for security - it ensures only verified reports are processed
constructor(address _forwarderAddress) Ownable(msg.sender) {
if (_forwarderAddress == address(0)) {
revert InvalidForwarderAddress();
}
s_forwarderAddress = _forwarderAddress;
emit ForwarderAddressUpdated(address(0), _forwarderAddress);
}
/// @notice Returns the configured forwarder address
/// @return The forwarder address (address(0) if disabled)
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 (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 (
s_expectedWorkflowId != bytes32(0) || s_expectedAuthor != address(0) || s_expectedWorkflowName != bytes10(0)
) {
(bytes32 workflowId, bytes10 workflowName, address workflowOwner) = _decodeMetadata(metadata);
if (s_expectedWorkflowId != bytes32(0) && workflowId != s_expectedWorkflowId) {
revert InvalidWorkflowId(workflowId, s_expectedWorkflowId);
}
if (s_expectedAuthor != address(0) && workflowOwner != s_expectedAuthor) {
revert InvalidAuthor(workflowOwner, s_expectedAuthor);
}
// ================================================================
// WORKFLOW NAME VALIDATION - REQUIRES AUTHOR VALIDATION
// ================================================================
// Do not rely on workflow name validation alone. Workflow names are unique
// per owner, but not across owners.
// Furthermore, workflow names use 40-bit truncation (bytes10), making collisions possible.
// Therefore, workflow name validation REQUIRES author (workflow owner) validation.
// The code enforces this dependency at runtime.
// ================================================================
if (s_expectedWorkflowName != bytes10(0)) {
// Author must be configured if workflow name is used
if (s_expectedAuthor == address(0)) {
revert WorkflowNameRequiresAuthorValidation();
}
// Validate workflow name matches (author already validated above)
if (workflowName != s_expectedWorkflowName) {
revert InvalidWorkflowName(workflowName, s_expectedWorkflowName);
}
}
}
_processReport(report);
}
/// @notice Updates the forwarder address that is allowed to call onReport
/// @param _forwarder The new forwarder address
/// @dev WARNING: Setting to address(0) disables forwarder validation.
/// This makes your contract INSECURE - anyone can call onReport() with arbitrary data.
/// Only use address(0) if you fully understand the security implications.
function setForwarderAddress(address _forwarder) external onlyOwner {
address previousForwarder = s_forwarderAddress;
// Emit warning if disabling forwarder check
if (_forwarder == address(0)) {
emit SecurityWarning("Forwarder address set to zero - contract is now INSECURE");
}
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 {
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 IMPORTANT: Workflow name validation REQUIRES author validation to be enabled.
/// The workflow name uses only 40-bit truncation, making collision attacks feasible
/// when used alone. However, since workflow names are unique per owner, validating
/// both the name AND the author address provides adequate security.
/// You must call setExpectedAuthor() before or after calling this function.
/// The name is hashed using SHA256 and truncated to bytes10.
function setExpectedWorkflowName(string calldata _name) external onlyOwner {
bytes10 previousName = s_expectedWorkflowName;
if (bytes(_name).length == 0) {
s_expectedWorkflowName = bytes10(0);
emit ExpectedWorkflowNameUpdated(previousName, bytes10(0));
return;
}
// Convert workflow name to bytes10:
// SHA256 hash → hex encode → take first 10 chars → hex encode those chars
bytes32 hash = sha256(bytes(_name));
bytes memory hexString = _bytesToHexString(abi.encodePacked(hash));
bytes memory first10 = new bytes(10);
for (uint256 i = 0; i < 10; i++) {
first10[i] = hexString[i];
}
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 {
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 hexString = new bytes(data.length * 2);
for (uint256 i = 0; i < data.length; i++) {
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 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
function _decodeMetadata(bytes memory metadata)
internal
pure
returns (bytes32 workflowId, bytes10 workflowName, address workflowOwner)
{
// 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)
// - Offset 74, size 20: workflow_owner (address)
assembly {
workflowId := mload(add(metadata, 32))
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
/// @param report The report calldata containing your workflow's encoded data
/// @dev Implement this function with your contract's business logic
function _processReport(bytes calldata report) internal virtual;
/// @inheritdoc IERC165
function supportsInterface(bytes4 interfaceId) public pure virtual override returns (bool) {
return interfaceId == type(IReceiver).interfaceId || interfaceId == type(IERC165).interfaceId;
}
}