Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SECHD-20276] Extend Functions contracts to support ZKSync #15594

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions contracts/.changeset/loud-rabbits-chew.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@chainlink/contracts': patch
---

Added ZKSync support for Functions
1 change: 1 addition & 0 deletions contracts/.solhintignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

# Ignore Functions v1.0.0 code that was frozen after audit
./src/v0.8/functions/v1_0_0
./src/v0.8/functions/v1_3_0_zksync/FunctionsRouter.sol

# Test helpers
./src/v0.8/vrf/testhelpers
Expand Down
440 changes: 440 additions & 0 deletions contracts/src/v0.8/functions/v1_3_0_zksync/FunctionsBilling.sol

Large diffs are not rendered by default.

228 changes: 228 additions & 0 deletions contracts/src/v0.8/functions/v1_3_0_zksync/FunctionsCoordinator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {IFunctionsCoordinator} from "../v1_0_0/interfaces/IFunctionsCoordinator.sol";
import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol";

import {FunctionsBilling, FunctionsBillingConfig} from "./FunctionsBilling.sol";
import {OCR2Base} from "../v1_0_0/ocr/OCR2Base.sol";
import {FunctionsResponse} from "../v1_0_0/libraries/FunctionsResponse.sol";

/// @title Functions Coordinator contract
/// @notice Contract that nodes of a Decentralized Oracle Network (DON) interact with
contract FunctionsCoordinator is OCR2Base, IFunctionsCoordinator, FunctionsBilling {
using FunctionsResponse for FunctionsResponse.RequestMeta;
using FunctionsResponse for FunctionsResponse.Commitment;
using FunctionsResponse for FunctionsResponse.FulfillResult;

/// @inheritdoc ITypeAndVersion
// solhint-disable-next-line chainlink-solidity/all-caps-constant-storage-variables
string public constant override typeAndVersion = "Functions Coordinator v1.3.0";

event OracleRequest(
bytes32 indexed requestId,
address indexed requestingContract,
address requestInitiator,
uint64 subscriptionId,
address subscriptionOwner,
bytes data,
uint16 dataVersion,
bytes32 flags,
uint64 callbackGasLimit,
FunctionsResponse.Commitment commitment
);
event OracleResponse(bytes32 indexed requestId, address transmitter);

error InconsistentReportData();
error EmptyPublicKey();
error UnauthorizedPublicKeyChange();

bytes private s_donPublicKey;
bytes private s_thresholdPublicKey;

constructor(
address router,
FunctionsBillingConfig memory config,
address linkToNativeFeed,
address linkToUsdFeed
) OCR2Base() FunctionsBilling(router, config, linkToNativeFeed, linkToUsdFeed) {}

/// @inheritdoc IFunctionsCoordinator
function getThresholdPublicKey() external view override returns (bytes memory) {
if (s_thresholdPublicKey.length == 0) {
revert EmptyPublicKey();
}
return s_thresholdPublicKey;
}

/// @inheritdoc IFunctionsCoordinator
function setThresholdPublicKey(bytes calldata thresholdPublicKey) external override onlyOwner {
if (thresholdPublicKey.length == 0) {
revert EmptyPublicKey();
}
s_thresholdPublicKey = thresholdPublicKey;
}

/// @inheritdoc IFunctionsCoordinator
function getDONPublicKey() external view override returns (bytes memory) {
if (s_donPublicKey.length == 0) {
revert EmptyPublicKey();
}
return s_donPublicKey;
}

/// @inheritdoc IFunctionsCoordinator
function setDONPublicKey(bytes calldata donPublicKey) external override onlyOwner {
if (donPublicKey.length == 0) {
revert EmptyPublicKey();
}
s_donPublicKey = donPublicKey;
}

/// @dev check if node is in current transmitter list
function _isTransmitter(address node) internal view returns (bool) {
// Bounded by "maxNumOracles" on OCR2Abstract.sol
for (uint256 i = 0; i < s_transmitters.length; ++i) {
if (s_transmitters[i] == node) {
return true;
}
}
return false;
}

/// @inheritdoc IFunctionsCoordinator
function startRequest(
FunctionsResponse.RequestMeta calldata request
) external override onlyRouter returns (FunctionsResponse.Commitment memory commitment) {
uint72 operationFee;
(commitment, operationFee) = _startBilling(request);

emit OracleRequest(
commitment.requestId,
request.requestingContract,
// solhint-disable-next-line avoid-tx-origin
tx.origin,
request.subscriptionId,
request.subscriptionOwner,
request.data,
request.dataVersion,
request.flags,
request.callbackGasLimit,
FunctionsResponse.Commitment({
coordinator: commitment.coordinator,
client: commitment.client,
subscriptionId: commitment.subscriptionId,
callbackGasLimit: commitment.callbackGasLimit,
estimatedTotalCostJuels: commitment.estimatedTotalCostJuels,
timeoutTimestamp: commitment.timeoutTimestamp,
requestId: commitment.requestId,
donFee: commitment.donFee,
gasOverheadBeforeCallback: commitment.gasOverheadBeforeCallback,
gasOverheadAfterCallback: commitment.gasOverheadAfterCallback,
// The following line is done to use the Coordinator's operationFee in place of the Router's operation fee
// With this in place the Router.adminFee must be set to 0 in the Router.
adminFee: operationFee
})
);

return commitment;
}

/// @dev DON fees are pooled together. If the OCR configuration is going to change, these need to be distributed.
function _beforeSetConfig(uint8 /* _f */, bytes memory /* _onchainConfig */) internal override {
if (_getTransmitters().length > 0) {
_disperseFeePool();
}
}

/// @dev Used by FunctionsBilling.sol
function _getTransmitters() internal view override returns (address[] memory) {
return s_transmitters;
}

function _beforeTransmit(
bytes calldata report
) internal view override returns (bool shouldStop, DecodedReport memory decodedReport) {
(
bytes32[] memory requestIds,
bytes[] memory results,
bytes[] memory errors,
bytes[] memory onchainMetadata,
bytes[] memory offchainMetadata
) = abi.decode(report, (bytes32[], bytes[], bytes[], bytes[], bytes[]));
uint256 numberOfFulfillments = uint8(requestIds.length);

if (
numberOfFulfillments == 0 ||
numberOfFulfillments != results.length ||
numberOfFulfillments != errors.length ||
numberOfFulfillments != onchainMetadata.length ||
numberOfFulfillments != offchainMetadata.length
) {
revert ReportInvalid("Fields must be equal length");
}

for (uint256 i = 0; i < numberOfFulfillments; ++i) {
if (_isExistingRequest(requestIds[i])) {
// If there is an existing request, validate report
// Leave shouldStop to default, false
break;
}
if (i == numberOfFulfillments - 1) {
// If the last fulfillment on the report does not exist, then all are duplicates
// Indicate that it's safe to stop to save on the gas of validating the report
shouldStop = true;
}
}

return (
shouldStop,
DecodedReport({
requestIds: requestIds,
results: results,
errors: errors,
onchainMetadata: onchainMetadata,
offchainMetadata: offchainMetadata
})
);
}

/// @dev Report hook called within OCR2Base.sol
function _report(DecodedReport memory decodedReport) internal override {
uint256 numberOfFulfillments = uint8(decodedReport.requestIds.length);

// Bounded by "MaxRequestBatchSize" on the Job's ReportingPluginConfig
for (uint256 i = 0; i < numberOfFulfillments; ++i) {
FunctionsResponse.FulfillResult result = FunctionsResponse.FulfillResult(
_fulfillAndBill(
decodedReport.requestIds[i],
decodedReport.results[i],
decodedReport.errors[i],
decodedReport.onchainMetadata[i],
decodedReport.offchainMetadata[i],
uint8(numberOfFulfillments) // will not exceed "MaxRequestBatchSize" on the Job's ReportingPluginConfig
)
);

// Emit on successfully processing the fulfillment
// In these two fulfillment results the user has been charged
// Otherwise, the DON will re-try
if (
result == FunctionsResponse.FulfillResult.FULFILLED ||
result == FunctionsResponse.FulfillResult.USER_CALLBACK_ERROR
) {
emit OracleResponse(decodedReport.requestIds[i], msg.sender);
}
}
}

/// @dev Used in FunctionsBilling.sol
function _onlyOwner() internal view override {
_validateOwnership();
}

/// @dev Used in FunctionsBilling.sol
function _owner() internal view override returns (address owner) {
return this.owner();
}
}
Loading
Loading