From a466aeae2363c4d1ca669e9c7bc55dfb76d936e3 Mon Sep 17 00:00:00 2001 From: Justin Kaseman Date: Mon, 16 Oct 2023 11:18:01 -0400 Subject: [PATCH] (chore): Move Functions v1.0.0 to production (#10941) * Move Functions v0 code out of dev folder * Rename Functions dev folder 1_X to reflect next version * Add PR number * Soothe linter errors for Functions 1.X --- contracts/CHANGELOG.md | 1 + .../scripts/native_solc_compile_all_functions | 16 +- .../dev/{v1_0_0 => v1_X}/FunctionsBilling.sol | 1 - .../functions/dev/v1_X/FunctionsClient.sol | 62 ++ .../{v1_0_0 => v1_X}/FunctionsCoordinator.sol | 3 +- .../dev/{v1_0_0 => v1_X}/FunctionsRouter.sol | 2 + .../FunctionsSubscriptions.sol | 1 - .../dev/{v1_0_0 => v1_X}/Routable.sol | 0 .../accessControl/TermsOfServiceAllowList.sol | 1 + .../interfaces/ITermsOfServiceAllowList.sol | 0 .../v1_X/example/FunctionsClientExample.sol | 69 +++ .../interfaces/IFunctionsBilling.sol | 0 .../interfaces/IFunctionsClient.sol | 0 .../interfaces/IFunctionsCoordinator.sol | 0 .../interfaces/IFunctionsRouter.sol | 0 .../interfaces/IFunctionsSubscriptions.sol | 0 .../interfaces/IOwnableFunctionsRouter.sol | 0 .../dev/v1_X/libraries/FunctionsRequest.sol | 155 +++++ .../libraries/FunctionsResponse.sol | 0 .../mocks/FunctionsV1EventsMock.sol | 0 .../functions/dev/v1_X/ocr/OCR2Abstract.sol | 103 +++ .../v0.8/functions/dev/v1_X/ocr/OCR2Base.sol | 371 +++++++++++ .../tests/{v1_0_0 => v1_X}/BaseTest.t.sol | 0 .../{v1_0_0 => v1_X}/FunctionsBilling.t.sol | 14 +- .../{v1_0_0 => v1_X}/FunctionsClient.t.sol | 12 +- .../FunctionsCoordinator.t.sol | 8 +- .../{v1_0_0 => v1_X}/FunctionsRequest.t.sol | 2 +- .../{v1_0_0 => v1_X}/FunctionsRouter.t.sol | 80 +-- .../FunctionsSubscriptions.t.sol | 6 +- .../FunctionsTermsOfServiceAllowList.t.sol | 2 +- .../tests/{v1_0_0 => v1_X}/Gas.t.sol | 16 +- .../tests/{v1_0_0 => v1_X}/OCR2.t.sol | 0 .../tests/{v1_0_0 => v1_X}/README.md | 2 +- .../tests/{v1_0_0 => v1_X}/Setup.t.sol | 8 +- .../testhelpers/FunctionsClientTestHelper.sol | 28 +- .../FunctionsClientUpgradeHelper.sol | 42 +- .../FunctionsClientWithEmptyCallback.sol | 10 +- .../FunctionsCoordinatorTestHelper.sol | 4 +- .../testhelpers/FunctionsLoadTestClient.sol | 22 +- .../testhelpers/FunctionsTestHelper.sol | 12 +- contracts/src/v0.8/functions/v1_0_0/.gitkeep | 0 .../functions/v1_0_0/FunctionsBilling.sol | 372 +++++++++++ .../{dev => }/v1_0_0/FunctionsClient.sol | 0 .../functions/v1_0_0/FunctionsCoordinator.sol | 176 ++++++ .../v0.8/functions/v1_0_0/FunctionsRouter.sol | 585 ++++++++++++++++++ .../v1_0_0/FunctionsSubscriptions.sol | 552 +++++++++++++++++ .../src/v0.8/functions/v1_0_0/Routable.sol | 45 ++ .../accessControl/TermsOfServiceAllowList.sol | 142 +++++ .../interfaces/ITermsOfServiceAllowList.sol | 40 ++ .../v1_0_0/example/FunctionsClientExample.sol | 2 +- .../v1_0_0/interfaces/IFunctionsBilling.sol | 44 ++ .../v1_0_0/interfaces/IFunctionsClient.sol | 13 + .../interfaces/IFunctionsCoordinator.sol | 37 ++ .../v1_0_0/interfaces/IFunctionsRouter.sol | 109 ++++ .../interfaces/IFunctionsSubscriptions.sol | 140 +++++ .../interfaces/IOwnableFunctionsRouter.sol | 10 + .../v1_0_0/libraries/FunctionsRequest.sol | 2 +- .../v1_0_0/libraries/FunctionsResponse.sol | 44 ++ .../v1_0_0/mocks/FunctionsV1EventsMock.sol | 181 ++++++ .../{dev => }/v1_0_0/ocr/OCR2Abstract.sol | 2 +- .../{dev => }/v1_0_0/ocr/OCR2Base.sol | 7 +- .../test/v0.8/functions/v1/Functions.test.ts | 2 +- .../v0.8/functions/v1/FunctionsClient.test.ts | 2 +- contracts/test/v0.8/functions/v1/utils.ts | 10 +- ...rapper-dependency-versions-do-not-edit.txt | 16 +- core/gethwrappers/functions/go_generate.go | 16 +- integration-tests/load/functions/README.md | 2 +- 67 files changed, 3426 insertions(+), 178 deletions(-) rename contracts/src/v0.8/functions/dev/{v1_0_0 => v1_X}/FunctionsBilling.sol (99%) create mode 100644 contracts/src/v0.8/functions/dev/v1_X/FunctionsClient.sol rename contracts/src/v0.8/functions/dev/{v1_0_0 => v1_X}/FunctionsCoordinator.sol (97%) rename contracts/src/v0.8/functions/dev/{v1_0_0 => v1_X}/FunctionsRouter.sol (99%) rename contracts/src/v0.8/functions/dev/{v1_0_0 => v1_X}/FunctionsSubscriptions.sol (99%) rename contracts/src/v0.8/functions/dev/{v1_0_0 => v1_X}/Routable.sol (100%) rename contracts/src/v0.8/functions/dev/{v1_0_0 => v1_X}/accessControl/TermsOfServiceAllowList.sol (98%) rename contracts/src/v0.8/functions/dev/{v1_0_0 => v1_X}/accessControl/interfaces/ITermsOfServiceAllowList.sol (100%) create mode 100644 contracts/src/v0.8/functions/dev/v1_X/example/FunctionsClientExample.sol rename contracts/src/v0.8/functions/dev/{v1_0_0 => v1_X}/interfaces/IFunctionsBilling.sol (100%) rename contracts/src/v0.8/functions/dev/{v1_0_0 => v1_X}/interfaces/IFunctionsClient.sol (100%) rename contracts/src/v0.8/functions/dev/{v1_0_0 => v1_X}/interfaces/IFunctionsCoordinator.sol (100%) rename contracts/src/v0.8/functions/dev/{v1_0_0 => v1_X}/interfaces/IFunctionsRouter.sol (100%) rename contracts/src/v0.8/functions/dev/{v1_0_0 => v1_X}/interfaces/IFunctionsSubscriptions.sol (100%) rename contracts/src/v0.8/functions/dev/{v1_0_0 => v1_X}/interfaces/IOwnableFunctionsRouter.sol (100%) create mode 100644 contracts/src/v0.8/functions/dev/v1_X/libraries/FunctionsRequest.sol rename contracts/src/v0.8/functions/dev/{v1_0_0 => v1_X}/libraries/FunctionsResponse.sol (100%) rename contracts/src/v0.8/functions/dev/{v1_0_0 => v1_X}/mocks/FunctionsV1EventsMock.sol (100%) create mode 100644 contracts/src/v0.8/functions/dev/v1_X/ocr/OCR2Abstract.sol create mode 100644 contracts/src/v0.8/functions/dev/v1_X/ocr/OCR2Base.sol rename contracts/src/v0.8/functions/tests/{v1_0_0 => v1_X}/BaseTest.t.sol (100%) rename contracts/src/v0.8/functions/tests/{v1_0_0 => v1_X}/FunctionsBilling.t.sol (93%) rename contracts/src/v0.8/functions/tests/{v1_0_0 => v1_X}/FunctionsClient.t.sol (78%) rename contracts/src/v0.8/functions/tests/{v1_0_0 => v1_X}/FunctionsCoordinator.t.sol (84%) rename contracts/src/v0.8/functions/tests/{v1_0_0 => v1_X}/FunctionsRequest.t.sol (94%) rename contracts/src/v0.8/functions/tests/{v1_0_0 => v1_X}/FunctionsRouter.t.sol (95%) rename contracts/src/v0.8/functions/tests/{v1_0_0 => v1_X}/FunctionsSubscriptions.t.sol (99%) rename contracts/src/v0.8/functions/tests/{v1_0_0 => v1_X}/FunctionsTermsOfServiceAllowList.t.sol (99%) rename contracts/src/v0.8/functions/tests/{v1_0_0 => v1_X}/Gas.t.sol (95%) rename contracts/src/v0.8/functions/tests/{v1_0_0 => v1_X}/OCR2.t.sol (100%) rename contracts/src/v0.8/functions/tests/{v1_0_0 => v1_X}/README.md (84%) rename contracts/src/v0.8/functions/tests/{v1_0_0 => v1_X}/Setup.t.sol (98%) rename contracts/src/v0.8/functions/tests/{v1_0_0 => v1_X}/testhelpers/FunctionsClientTestHelper.sol (77%) rename contracts/src/v0.8/functions/tests/{v1_0_0 => v1_X}/testhelpers/FunctionsClientUpgradeHelper.sol (74%) rename contracts/src/v0.8/functions/tests/{v1_0_0 => v1_X}/testhelpers/FunctionsClientWithEmptyCallback.sol (66%) rename contracts/src/v0.8/functions/tests/{v1_0_0 => v1_X}/testhelpers/FunctionsCoordinatorTestHelper.sol (85%) rename contracts/src/v0.8/functions/tests/{v1_0_0 => v1_X}/testhelpers/FunctionsLoadTestClient.sol (83%) rename contracts/src/v0.8/functions/tests/{v1_0_0 => v1_X}/testhelpers/FunctionsTestHelper.sol (81%) delete mode 100644 contracts/src/v0.8/functions/v1_0_0/.gitkeep create mode 100644 contracts/src/v0.8/functions/v1_0_0/FunctionsBilling.sol rename contracts/src/v0.8/functions/{dev => }/v1_0_0/FunctionsClient.sol (100%) create mode 100644 contracts/src/v0.8/functions/v1_0_0/FunctionsCoordinator.sol create mode 100644 contracts/src/v0.8/functions/v1_0_0/FunctionsRouter.sol create mode 100644 contracts/src/v0.8/functions/v1_0_0/FunctionsSubscriptions.sol create mode 100644 contracts/src/v0.8/functions/v1_0_0/Routable.sol create mode 100644 contracts/src/v0.8/functions/v1_0_0/accessControl/TermsOfServiceAllowList.sol create mode 100644 contracts/src/v0.8/functions/v1_0_0/accessControl/interfaces/ITermsOfServiceAllowList.sol rename contracts/src/v0.8/functions/{dev => }/v1_0_0/example/FunctionsClientExample.sol (97%) create mode 100644 contracts/src/v0.8/functions/v1_0_0/interfaces/IFunctionsBilling.sol create mode 100644 contracts/src/v0.8/functions/v1_0_0/interfaces/IFunctionsClient.sol create mode 100644 contracts/src/v0.8/functions/v1_0_0/interfaces/IFunctionsCoordinator.sol create mode 100644 contracts/src/v0.8/functions/v1_0_0/interfaces/IFunctionsRouter.sol create mode 100644 contracts/src/v0.8/functions/v1_0_0/interfaces/IFunctionsSubscriptions.sol create mode 100644 contracts/src/v0.8/functions/v1_0_0/interfaces/IOwnableFunctionsRouter.sol rename contracts/src/v0.8/functions/{dev => }/v1_0_0/libraries/FunctionsRequest.sol (98%) create mode 100644 contracts/src/v0.8/functions/v1_0_0/libraries/FunctionsResponse.sol create mode 100644 contracts/src/v0.8/functions/v1_0_0/mocks/FunctionsV1EventsMock.sol rename contracts/src/v0.8/functions/{dev => }/v1_0_0/ocr/OCR2Abstract.sol (98%) rename contracts/src/v0.8/functions/{dev => }/v1_0_0/ocr/OCR2Base.sol (95%) diff --git a/contracts/CHANGELOG.md b/contracts/CHANGELOG.md index 5f06c21c250..83c2208910f 100644 --- a/contracts/CHANGELOG.md +++ b/contracts/CHANGELOG.md @@ -4,6 +4,7 @@ - Moved `VRFCoordinatorV2Mock.sol` to src/v0.8/vrf/mocks - Moved `VRFCoordinatorMock.sol` to src/v0.8/vrf/mocks +- Release Functions v1.0.0 contracts. Start dev folder for v1.X (#10941) ### Removed diff --git a/contracts/scripts/native_solc_compile_all_functions b/contracts/scripts/native_solc_compile_all_functions index 1dc2a0776f3..4b353b36901 100755 --- a/contracts/scripts/native_solc_compile_all_functions +++ b/contracts/scripts/native_solc_compile_all_functions @@ -51,15 +51,15 @@ solc-select install $SOLC_VERSION solc-select use $SOLC_VERSION export SOLC_VERSION=$SOLC_VERSION -compileContract v1_0_0 dev/v1_0_0/libraries/FunctionsRequest.sol -compileContract v1_0_0 dev/v1_0_0/FunctionsRouter.sol -compileContract v1_0_0 dev/v1_0_0/FunctionsCoordinator.sol -compileContract v1_0_0 dev/v1_0_0/accessControl/TermsOfServiceAllowList.sol -compileContract v1_0_0 dev/v1_0_0/example/FunctionsClientExample.sol +compileContract v1_X dev/v1_X/libraries/FunctionsRequest.sol +compileContract v1_X dev/v1_X/FunctionsRouter.sol +compileContract v1_X dev/v1_X/FunctionsCoordinator.sol +compileContract v1_X dev/v1_X/accessControl/TermsOfServiceAllowList.sol +compileContract v1_X dev/v1_X/example/FunctionsClientExample.sol # Test helpers -compileContract v1_0_0 tests/v1_0_0/testhelpers/FunctionsCoordinatorTestHelper.sol -compileContract v1_0_0 tests/v1_0_0/testhelpers/FunctionsLoadTestClient.sol +compileContract v1_X tests/v1_X/testhelpers/FunctionsCoordinatorTestHelper.sol +compileContract v1_X tests/v1_X/testhelpers/FunctionsLoadTestClient.sol # Mocks -compileContract v1_0_0 dev/v1_0_0/mocks/FunctionsV1EventsMock.sol +compileContract v1_X dev/v1_X/mocks/FunctionsV1EventsMock.sol diff --git a/contracts/src/v0.8/functions/dev/v1_0_0/FunctionsBilling.sol b/contracts/src/v0.8/functions/dev/v1_X/FunctionsBilling.sol similarity index 99% rename from contracts/src/v0.8/functions/dev/v1_0_0/FunctionsBilling.sol rename to contracts/src/v0.8/functions/dev/v1_X/FunctionsBilling.sol index c616f7355d2..c87e00bb871 100644 --- a/contracts/src/v0.8/functions/dev/v1_0_0/FunctionsBilling.sol +++ b/contracts/src/v0.8/functions/dev/v1_X/FunctionsBilling.sol @@ -12,7 +12,6 @@ import {SafeCast} from "../../../vendor/openzeppelin-solidity/v4.8.0/contracts/u /// @title Functions Billing contract /// @notice Contract that calculates payment from users to the nodes of the Decentralized Oracle Network (DON). -/// @dev THIS CONTRACT HAS NOT GONE THROUGH ANY SECURITY REVIEW. DO NOT USE IN PROD. abstract contract FunctionsBilling is Routable, IFunctionsBilling { using FunctionsResponse for FunctionsResponse.RequestMeta; using FunctionsResponse for FunctionsResponse.Commitment; diff --git a/contracts/src/v0.8/functions/dev/v1_X/FunctionsClient.sol b/contracts/src/v0.8/functions/dev/v1_X/FunctionsClient.sol new file mode 100644 index 00000000000..6d033d4b235 --- /dev/null +++ b/contracts/src/v0.8/functions/dev/v1_X/FunctionsClient.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IFunctionsRouter} from "./interfaces/IFunctionsRouter.sol"; +import {IFunctionsClient} from "./interfaces/IFunctionsClient.sol"; + +import {FunctionsRequest} from "./libraries/FunctionsRequest.sol"; + +/// @title The Chainlink Functions client contract +/// @notice Contract developers can inherit this contract in order to make Chainlink Functions requests +abstract contract FunctionsClient is IFunctionsClient { + using FunctionsRequest for FunctionsRequest.Request; + + IFunctionsRouter internal immutable i_router; + + event RequestSent(bytes32 indexed id); + event RequestFulfilled(bytes32 indexed id); + + error OnlyRouterCanFulfill(); + + constructor(address router) { + i_router = IFunctionsRouter(router); + } + + /// @notice Sends a Chainlink Functions request + /// @param data The CBOR encoded bytes data for a Functions request + /// @param subscriptionId The subscription ID that will be charged to service the request + /// @param callbackGasLimit the amount of gas that will be available for the fulfillment callback + /// @return requestId The generated request ID for this request + function _sendRequest( + bytes memory data, + uint64 subscriptionId, + uint32 callbackGasLimit, + bytes32 donId + ) internal returns (bytes32) { + bytes32 requestId = i_router.sendRequest( + subscriptionId, + data, + FunctionsRequest.REQUEST_DATA_VERSION, + callbackGasLimit, + donId + ); + emit RequestSent(requestId); + return requestId; + } + + /// @notice User defined function to handle a response from the DON + /// @param requestId The request ID, returned by sendRequest() + /// @param response Aggregated response from the execution of the user's source code + /// @param err Aggregated error from the execution of the user code or from the execution pipeline + /// @dev Either response or error parameter will be set, but never both + function _fulfillRequest(bytes32 requestId, bytes memory response, bytes memory err) internal virtual; + + /// @inheritdoc IFunctionsClient + function handleOracleFulfillment(bytes32 requestId, bytes memory response, bytes memory err) external override { + if (msg.sender != address(i_router)) { + revert OnlyRouterCanFulfill(); + } + _fulfillRequest(requestId, response, err); + emit RequestFulfilled(requestId); + } +} diff --git a/contracts/src/v0.8/functions/dev/v1_0_0/FunctionsCoordinator.sol b/contracts/src/v0.8/functions/dev/v1_X/FunctionsCoordinator.sol similarity index 97% rename from contracts/src/v0.8/functions/dev/v1_0_0/FunctionsCoordinator.sol rename to contracts/src/v0.8/functions/dev/v1_X/FunctionsCoordinator.sol index daafe593a7b..b9070826987 100644 --- a/contracts/src/v0.8/functions/dev/v1_0_0/FunctionsCoordinator.sol +++ b/contracts/src/v0.8/functions/dev/v1_X/FunctionsCoordinator.sol @@ -10,13 +10,13 @@ import {FunctionsResponse} from "./libraries/FunctionsResponse.sol"; /// @title Functions Coordinator contract /// @notice Contract that nodes of a Decentralized Oracle Network (DON) interact with -/// @dev THIS CONTRACT HAS NOT GONE THROUGH ANY SECURITY REVIEW. DO NOT USE IN PROD. 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.0.0"; event OracleRequest( @@ -99,6 +99,7 @@ contract FunctionsCoordinator is OCR2Base, IFunctionsCoordinator, FunctionsBilli emit OracleRequest( commitment.requestId, request.requestingContract, + // solhint-disable-next-line avoid-tx-origin tx.origin, request.subscriptionId, request.subscriptionOwner, diff --git a/contracts/src/v0.8/functions/dev/v1_0_0/FunctionsRouter.sol b/contracts/src/v0.8/functions/dev/v1_X/FunctionsRouter.sol similarity index 99% rename from contracts/src/v0.8/functions/dev/v1_0_0/FunctionsRouter.sol rename to contracts/src/v0.8/functions/dev/v1_X/FunctionsRouter.sol index 9051fd26d44..8daa821bb54 100644 --- a/contracts/src/v0.8/functions/dev/v1_0_0/FunctionsRouter.sol +++ b/contracts/src/v0.8/functions/dev/v1_X/FunctionsRouter.sol @@ -18,6 +18,7 @@ contract FunctionsRouter is IFunctionsRouter, FunctionsSubscriptions, Pausable, using FunctionsResponse for FunctionsResponse.Commitment; using FunctionsResponse for FunctionsResponse.FulfillResult; + // solhint-disable-next-line chainlink-solidity/all-caps-constant-storage-variables string public constant override typeAndVersion = "Functions Router v1.0.0"; // We limit return data to a selector plus 4 words. This is to avoid @@ -285,6 +286,7 @@ contract FunctionsRouter is IFunctionsRouter, FunctionsSubscriptions, Pausable, subscriptionId: subscriptionId, subscriptionOwner: subscription.owner, requestingContract: msg.sender, + // solhint-disable-next-line avoid-tx-origin requestInitiator: tx.origin, data: data, dataVersion: dataVersion, diff --git a/contracts/src/v0.8/functions/dev/v1_0_0/FunctionsSubscriptions.sol b/contracts/src/v0.8/functions/dev/v1_X/FunctionsSubscriptions.sol similarity index 99% rename from contracts/src/v0.8/functions/dev/v1_0_0/FunctionsSubscriptions.sol rename to contracts/src/v0.8/functions/dev/v1_X/FunctionsSubscriptions.sol index 9a390a93fce..86e762e39c8 100644 --- a/contracts/src/v0.8/functions/dev/v1_0_0/FunctionsSubscriptions.sol +++ b/contracts/src/v0.8/functions/dev/v1_X/FunctionsSubscriptions.sol @@ -12,7 +12,6 @@ import {SafeERC20} from "../../../vendor/openzeppelin-solidity/v4.8.0/contracts/ /// @title Functions Subscriptions contract /// @notice Contract that coordinates payment from users to the nodes of the Decentralized Oracle Network (DON). -/// @dev THIS CONTRACT HAS NOT GONE THROUGH ANY SECURITY REVIEW. DO NOT USE IN PROD. abstract contract FunctionsSubscriptions is IFunctionsSubscriptions, IERC677Receiver { using SafeERC20 for IERC20; using FunctionsResponse for FunctionsResponse.Commitment; diff --git a/contracts/src/v0.8/functions/dev/v1_0_0/Routable.sol b/contracts/src/v0.8/functions/dev/v1_X/Routable.sol similarity index 100% rename from contracts/src/v0.8/functions/dev/v1_0_0/Routable.sol rename to contracts/src/v0.8/functions/dev/v1_X/Routable.sol diff --git a/contracts/src/v0.8/functions/dev/v1_0_0/accessControl/TermsOfServiceAllowList.sol b/contracts/src/v0.8/functions/dev/v1_X/accessControl/TermsOfServiceAllowList.sol similarity index 98% rename from contracts/src/v0.8/functions/dev/v1_0_0/accessControl/TermsOfServiceAllowList.sol rename to contracts/src/v0.8/functions/dev/v1_X/accessControl/TermsOfServiceAllowList.sol index c4bd524b522..fbeef3c298a 100644 --- a/contracts/src/v0.8/functions/dev/v1_0_0/accessControl/TermsOfServiceAllowList.sol +++ b/contracts/src/v0.8/functions/dev/v1_X/accessControl/TermsOfServiceAllowList.sol @@ -16,6 +16,7 @@ contract TermsOfServiceAllowList is ITermsOfServiceAllowList, IAccessController, using EnumerableSet for EnumerableSet.AddressSet; /// @inheritdoc ITypeAndVersion + // solhint-disable-next-line chainlink-solidity/all-caps-constant-storage-variables string public constant override typeAndVersion = "Functions Terms of Service Allow List v1.0.0"; EnumerableSet.AddressSet private s_allowedSenders; diff --git a/contracts/src/v0.8/functions/dev/v1_0_0/accessControl/interfaces/ITermsOfServiceAllowList.sol b/contracts/src/v0.8/functions/dev/v1_X/accessControl/interfaces/ITermsOfServiceAllowList.sol similarity index 100% rename from contracts/src/v0.8/functions/dev/v1_0_0/accessControl/interfaces/ITermsOfServiceAllowList.sol rename to contracts/src/v0.8/functions/dev/v1_X/accessControl/interfaces/ITermsOfServiceAllowList.sol diff --git a/contracts/src/v0.8/functions/dev/v1_X/example/FunctionsClientExample.sol b/contracts/src/v0.8/functions/dev/v1_X/example/FunctionsClientExample.sol new file mode 100644 index 00000000000..abb8569b1e6 --- /dev/null +++ b/contracts/src/v0.8/functions/dev/v1_X/example/FunctionsClientExample.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {FunctionsClient} from "../FunctionsClient.sol"; +import {ConfirmedOwner} from "../../../../shared/access/ConfirmedOwner.sol"; +import {FunctionsRequest} from "../libraries/FunctionsRequest.sol"; + +/// @title Chainlink Functions example Client contract implementation +contract FunctionsClientExample is FunctionsClient, ConfirmedOwner { + using FunctionsRequest for FunctionsRequest.Request; + + uint32 public constant MAX_CALLBACK_GAS = 70_000; + + bytes32 public s_lastRequestId; + bytes32 public s_lastResponse; + bytes32 public s_lastError; + uint32 public s_lastResponseLength; + uint32 public s_lastErrorLength; + + error UnexpectedRequestID(bytes32 requestId); + + constructor(address router) FunctionsClient(router) ConfirmedOwner(msg.sender) {} + + /// @notice Send a simple request + /// @param source JavaScript source code + /// @param encryptedSecretsReferences Encrypted secrets payload + /// @param args List of arguments accessible from within the source code + /// @param subscriptionId Billing ID + function sendRequest( + string calldata source, + bytes calldata encryptedSecretsReferences, + string[] calldata args, + uint64 subscriptionId, + bytes32 jobId + ) external onlyOwner { + FunctionsRequest.Request memory req; + req._initializeRequestForInlineJavaScript(source); + if (encryptedSecretsReferences.length > 0) req._addSecretsReference(encryptedSecretsReferences); + if (args.length > 0) req._setArgs(args); + s_lastRequestId = _sendRequest(req._encodeCBOR(), subscriptionId, MAX_CALLBACK_GAS, jobId); + } + + /// @notice Store latest result/error + /// @param requestId The request ID, returned by sendRequest() + /// @param response Aggregated response from the user code + /// @param err Aggregated error from the user code or from the execution pipeline + /// @dev Either response or error parameter will be set, but never both + function _fulfillRequest(bytes32 requestId, bytes memory response, bytes memory err) internal override { + if (s_lastRequestId != requestId) { + revert UnexpectedRequestID(requestId); + } + // Save only the first 32 bytes of response/error to always fit within MAX_CALLBACK_GAS + s_lastResponse = _bytesToBytes32(response); + s_lastResponseLength = uint32(response.length); + s_lastError = _bytesToBytes32(err); + s_lastErrorLength = uint32(err.length); + } + + function _bytesToBytes32(bytes memory b) private pure returns (bytes32 out) { + uint256 maxLen = 32; + if (b.length < 32) { + maxLen = b.length; + } + for (uint256 i = 0; i < maxLen; ++i) { + out |= bytes32(b[i]) >> (i * 8); + } + return out; + } +} diff --git a/contracts/src/v0.8/functions/dev/v1_0_0/interfaces/IFunctionsBilling.sol b/contracts/src/v0.8/functions/dev/v1_X/interfaces/IFunctionsBilling.sol similarity index 100% rename from contracts/src/v0.8/functions/dev/v1_0_0/interfaces/IFunctionsBilling.sol rename to contracts/src/v0.8/functions/dev/v1_X/interfaces/IFunctionsBilling.sol diff --git a/contracts/src/v0.8/functions/dev/v1_0_0/interfaces/IFunctionsClient.sol b/contracts/src/v0.8/functions/dev/v1_X/interfaces/IFunctionsClient.sol similarity index 100% rename from contracts/src/v0.8/functions/dev/v1_0_0/interfaces/IFunctionsClient.sol rename to contracts/src/v0.8/functions/dev/v1_X/interfaces/IFunctionsClient.sol diff --git a/contracts/src/v0.8/functions/dev/v1_0_0/interfaces/IFunctionsCoordinator.sol b/contracts/src/v0.8/functions/dev/v1_X/interfaces/IFunctionsCoordinator.sol similarity index 100% rename from contracts/src/v0.8/functions/dev/v1_0_0/interfaces/IFunctionsCoordinator.sol rename to contracts/src/v0.8/functions/dev/v1_X/interfaces/IFunctionsCoordinator.sol diff --git a/contracts/src/v0.8/functions/dev/v1_0_0/interfaces/IFunctionsRouter.sol b/contracts/src/v0.8/functions/dev/v1_X/interfaces/IFunctionsRouter.sol similarity index 100% rename from contracts/src/v0.8/functions/dev/v1_0_0/interfaces/IFunctionsRouter.sol rename to contracts/src/v0.8/functions/dev/v1_X/interfaces/IFunctionsRouter.sol diff --git a/contracts/src/v0.8/functions/dev/v1_0_0/interfaces/IFunctionsSubscriptions.sol b/contracts/src/v0.8/functions/dev/v1_X/interfaces/IFunctionsSubscriptions.sol similarity index 100% rename from contracts/src/v0.8/functions/dev/v1_0_0/interfaces/IFunctionsSubscriptions.sol rename to contracts/src/v0.8/functions/dev/v1_X/interfaces/IFunctionsSubscriptions.sol diff --git a/contracts/src/v0.8/functions/dev/v1_0_0/interfaces/IOwnableFunctionsRouter.sol b/contracts/src/v0.8/functions/dev/v1_X/interfaces/IOwnableFunctionsRouter.sol similarity index 100% rename from contracts/src/v0.8/functions/dev/v1_0_0/interfaces/IOwnableFunctionsRouter.sol rename to contracts/src/v0.8/functions/dev/v1_X/interfaces/IOwnableFunctionsRouter.sol diff --git a/contracts/src/v0.8/functions/dev/v1_X/libraries/FunctionsRequest.sol b/contracts/src/v0.8/functions/dev/v1_X/libraries/FunctionsRequest.sol new file mode 100644 index 00000000000..4e3134dc127 --- /dev/null +++ b/contracts/src/v0.8/functions/dev/v1_X/libraries/FunctionsRequest.sol @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {CBOR} from "../../../../vendor/solidity-cborutils/v2.0.0/CBOR.sol"; + +/// @title Library for encoding the input data of a Functions request into CBOR +library FunctionsRequest { + using CBOR for CBOR.CBORBuffer; + + uint16 public constant REQUEST_DATA_VERSION = 1; + uint256 internal constant DEFAULT_BUFFER_SIZE = 256; + + enum Location { + Inline, // Provided within the Request + Remote, // Hosted through remote location that can be accessed through a provided URL + DONHosted // Hosted on the DON's storage + } + + enum CodeLanguage { + JavaScript + // In future version we may add other languages + } + + struct Request { + Location codeLocation; // ════════════╸ The location of the source code that will be executed on each node in the DON + Location secretsLocation; // ═════════╸ The location of secrets that will be passed into the source code. *Only Remote secrets are supported + CodeLanguage language; // ════════════╸ The coding language that the source code is written in + string source; // ════════════════════╸ Raw source code for Request.codeLocation of Location.Inline, URL for Request.codeLocation of Location.Remote, or slot decimal number for Request.codeLocation of Location.DONHosted + bytes encryptedSecretsReference; // ══╸ Encrypted URLs for Request.secretsLocation of Location.Remote (use addSecretsReference()), or CBOR encoded slotid+version for Request.secretsLocation of Location.DONHosted (use addDONHostedSecrets()) + string[] args; // ════════════════════╸ String arguments that will be passed into the source code + bytes[] bytesArgs; // ════════════════╸ Bytes arguments that will be passed into the source code + } + + error EmptySource(); + error EmptySecrets(); + error EmptyArgs(); + error NoInlineSecrets(); + + /// @notice Encodes a Request to CBOR encoded bytes + /// @param self The request to encode + /// @return CBOR encoded bytes + function _encodeCBOR(Request memory self) internal pure returns (bytes memory) { + CBOR.CBORBuffer memory buffer = CBOR.create(DEFAULT_BUFFER_SIZE); + + buffer.writeString("codeLocation"); + buffer.writeUInt256(uint256(self.codeLocation)); + + buffer.writeString("language"); + buffer.writeUInt256(uint256(self.language)); + + buffer.writeString("source"); + buffer.writeString(self.source); + + if (self.args.length > 0) { + buffer.writeString("args"); + buffer.startArray(); + for (uint256 i = 0; i < self.args.length; ++i) { + buffer.writeString(self.args[i]); + } + buffer.endSequence(); + } + + if (self.encryptedSecretsReference.length > 0) { + if (self.secretsLocation == Location.Inline) { + revert NoInlineSecrets(); + } + buffer.writeString("secretsLocation"); + buffer.writeUInt256(uint256(self.secretsLocation)); + buffer.writeString("secrets"); + buffer.writeBytes(self.encryptedSecretsReference); + } + + if (self.bytesArgs.length > 0) { + buffer.writeString("bytesArgs"); + buffer.startArray(); + for (uint256 i = 0; i < self.bytesArgs.length; ++i) { + buffer.writeBytes(self.bytesArgs[i]); + } + buffer.endSequence(); + } + + return buffer.buf.buf; + } + + /// @notice Initializes a Chainlink Functions Request + /// @dev Sets the codeLocation and code on the request + /// @param self The uninitialized request + /// @param codeLocation The user provided source code location + /// @param language The programming language of the user code + /// @param source The user provided source code or a url + function _initializeRequest( + Request memory self, + Location codeLocation, + CodeLanguage language, + string memory source + ) internal pure { + if (bytes(source).length == 0) revert EmptySource(); + + self.codeLocation = codeLocation; + self.language = language; + self.source = source; + } + + /// @notice Initializes a Chainlink Functions Request + /// @dev Simplified version of initializeRequest for PoC + /// @param self The uninitialized request + /// @param javaScriptSource The user provided JS code (must not be empty) + function _initializeRequestForInlineJavaScript(Request memory self, string memory javaScriptSource) internal pure { + _initializeRequest(self, Location.Inline, CodeLanguage.JavaScript, javaScriptSource); + } + + /// @notice Adds Remote user encrypted secrets to a Request + /// @param self The initialized request + /// @param encryptedSecretsReference Encrypted comma-separated string of URLs pointing to off-chain secrets + function _addSecretsReference(Request memory self, bytes memory encryptedSecretsReference) internal pure { + if (encryptedSecretsReference.length == 0) revert EmptySecrets(); + + self.secretsLocation = Location.Remote; + self.encryptedSecretsReference = encryptedSecretsReference; + } + + /// @notice Adds DON-hosted secrets reference to a Request + /// @param self The initialized request + /// @param slotID Slot ID of the user's secrets hosted on DON + /// @param version User data version (for the slotID) + function _addDONHostedSecrets(Request memory self, uint8 slotID, uint64 version) internal pure { + CBOR.CBORBuffer memory buffer = CBOR.create(DEFAULT_BUFFER_SIZE); + + buffer.writeString("slotID"); + buffer.writeUInt64(slotID); + buffer.writeString("version"); + buffer.writeUInt64(version); + + self.secretsLocation = Location.DONHosted; + self.encryptedSecretsReference = buffer.buf.buf; + } + + /// @notice Sets args for the user run function + /// @param self The initialized request + /// @param args The array of string args (must not be empty) + function _setArgs(Request memory self, string[] memory args) internal pure { + if (args.length == 0) revert EmptyArgs(); + + self.args = args; + } + + /// @notice Sets bytes args for the user run function + /// @param self The initialized request + /// @param args The array of bytes args (must not be empty) + function _setBytesArgs(Request memory self, bytes[] memory args) internal pure { + if (args.length == 0) revert EmptyArgs(); + + self.bytesArgs = args; + } +} diff --git a/contracts/src/v0.8/functions/dev/v1_0_0/libraries/FunctionsResponse.sol b/contracts/src/v0.8/functions/dev/v1_X/libraries/FunctionsResponse.sol similarity index 100% rename from contracts/src/v0.8/functions/dev/v1_0_0/libraries/FunctionsResponse.sol rename to contracts/src/v0.8/functions/dev/v1_X/libraries/FunctionsResponse.sol diff --git a/contracts/src/v0.8/functions/dev/v1_0_0/mocks/FunctionsV1EventsMock.sol b/contracts/src/v0.8/functions/dev/v1_X/mocks/FunctionsV1EventsMock.sol similarity index 100% rename from contracts/src/v0.8/functions/dev/v1_0_0/mocks/FunctionsV1EventsMock.sol rename to contracts/src/v0.8/functions/dev/v1_X/mocks/FunctionsV1EventsMock.sol diff --git a/contracts/src/v0.8/functions/dev/v1_X/ocr/OCR2Abstract.sol b/contracts/src/v0.8/functions/dev/v1_X/ocr/OCR2Abstract.sol new file mode 100644 index 00000000000..77cc9502171 --- /dev/null +++ b/contracts/src/v0.8/functions/dev/v1_X/ocr/OCR2Abstract.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {ITypeAndVersion} from "../../../../shared/interfaces/ITypeAndVersion.sol"; + +abstract contract OCR2Abstract is ITypeAndVersion { + // Maximum number of oracles the offchain reporting protocol is designed for + uint256 internal constant MAX_NUM_ORACLES = 31; + + /** + * @notice triggers a new run of the offchain reporting protocol + * @param previousConfigBlockNumber block in which the previous config was set, to simplify historic analysis + * @param configDigest configDigest of this configuration + * @param configCount ordinal number of this config setting among all config settings over the life of this contract + * @param signers ith element is address ith oracle uses to sign a report + * @param transmitters ith element is address ith oracle uses to transmit a report via the transmit method + * @param f maximum number of faulty/dishonest oracles the protocol can tolerate while still working correctly + * @param onchainConfig serialized configuration used by the contract (and possibly oracles) + * @param offchainConfigVersion version of the serialization format used for "offchainConfig" parameter + * @param offchainConfig serialized configuration used by the oracles exclusively and only passed through the contract + */ + event ConfigSet( + uint32 previousConfigBlockNumber, + bytes32 configDigest, + uint64 configCount, + address[] signers, + address[] transmitters, + uint8 f, + bytes onchainConfig, + uint64 offchainConfigVersion, + bytes offchainConfig + ); + + /** + * @notice sets offchain reporting protocol configuration incl. participating oracles + * @param signers addresses with which oracles sign the reports + * @param transmitters addresses oracles use to transmit the reports + * @param f number of faulty oracles the system can tolerate + * @param onchainConfig serialized configuration used by the contract (and possibly oracles) + * @param offchainConfigVersion version number for offchainEncoding schema + * @param offchainConfig serialized configuration used by the oracles exclusively and only passed through the contract + */ + function setConfig( + address[] memory signers, + address[] memory transmitters, + uint8 f, + bytes memory onchainConfig, + uint64 offchainConfigVersion, + bytes memory offchainConfig + ) external virtual; + + /** + * @notice information about current offchain reporting protocol configuration + * @return configCount ordinal number of current config, out of all configs applied to this contract so far + * @return blockNumber block at which this config was set + * @return configDigest domain-separation tag for current config (see _configDigestFromConfigData) + */ + function latestConfigDetails() + external + view + virtual + returns (uint32 configCount, uint32 blockNumber, bytes32 configDigest); + + /** + * @notice optionally emited to indicate the latest configDigest and epoch for + which a report was successfully transmited. Alternatively, the contract may + use latestConfigDigestAndEpoch with scanLogs set to false. + */ + event Transmitted(bytes32 configDigest, uint32 epoch); + + /** + * @notice optionally returns the latest configDigest and epoch for which a + report was successfully transmitted. Alternatively, the contract may return + scanLogs set to true and use Transmitted events to provide this information + to offchain watchers. + * @return scanLogs indicates whether to rely on the configDigest and epoch + returned or whether to scan logs for the Transmitted event instead. + * @return configDigest + * @return epoch + */ + function latestConfigDigestAndEpoch() + external + view + virtual + returns (bool scanLogs, bytes32 configDigest, uint32 epoch); + + /** + * @notice transmit is called to post a new report to the contract + * @param report serialized report, which the signatures are signing. + * @param rs ith element is the R components of the ith signature on report. Must have at most maxNumOracles entries + * @param ss ith element is the S components of the ith signature on report. Must have at most maxNumOracles entries + * @param rawVs ith element is the the V component of the ith signature + */ + function transmit( + // NOTE: If these parameters are changed, expectedMsgDataLength and/or + // TRANSMIT_MSGDATA_CONSTANT_LENGTH_COMPONENT need to be changed accordingly + bytes32[3] calldata reportContext, + bytes calldata report, + bytes32[] calldata rs, + bytes32[] calldata ss, + bytes32 rawVs // signatures + ) external virtual; +} diff --git a/contracts/src/v0.8/functions/dev/v1_X/ocr/OCR2Base.sol b/contracts/src/v0.8/functions/dev/v1_X/ocr/OCR2Base.sol new file mode 100644 index 00000000000..dd9ea84a519 --- /dev/null +++ b/contracts/src/v0.8/functions/dev/v1_X/ocr/OCR2Base.sol @@ -0,0 +1,371 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {ConfirmedOwner} from "../../../../shared/access/ConfirmedOwner.sol"; +import {OCR2Abstract} from "./OCR2Abstract.sol"; + +/** + * @notice Onchain verification of reports from the offchain reporting protocol + * @dev For details on its operation, see the offchain reporting protocol design + * doc, which refers to this contract as simply the "contract". + */ +abstract contract OCR2Base is ConfirmedOwner, OCR2Abstract { + error ReportInvalid(); + error InvalidConfig(string message); + + bool internal immutable i_uniqueReports; + + constructor(bool uniqueReports) ConfirmedOwner(msg.sender) { + i_uniqueReports = uniqueReports; + } + + // solhint-disable-next-line chainlink-solidity/all-caps-constant-storage-variables + uint256 private constant maxUint32 = (1 << 32) - 1; + + // incremented each time a new config is posted. This count is incorporated + // into the config digest, to prevent replay attacks. + uint32 internal s_configCount; + uint32 internal s_latestConfigBlockNumber; // makes it easier for offchain systems + // to extract config from logs. + + // Storing these fields used on the hot path in a ConfigInfo variable reduces the + // retrieval of all of them to a single SLOAD. If any further fields are + // added, make sure that storage of the struct still takes at most 32 bytes. + struct ConfigInfo { + bytes32 latestConfigDigest; + uint8 f; // TODO: could be optimized by squeezing into one slot + uint8 n; + } + ConfigInfo internal s_configInfo; + + // Used for s_oracles[a].role, where a is an address, to track the purpose + // of the address, or to indicate that the address is unset. + enum Role { + // No oracle role has been set for address a + Unset, + // Signing address for the s_oracles[a].index'th oracle. I.e., report + // signatures from this oracle should ecrecover back to address a. + Signer, + // Transmission address for the s_oracles[a].index'th oracle. I.e., if a + // report is received by OCR2Aggregator.transmit in which msg.sender is + // a, it is attributed to the s_oracles[a].index'th oracle. + Transmitter + } + + struct Oracle { + uint8 index; // Index of oracle in s_signers/s_transmitters + Role role; // Role of the address which mapped to this struct + } + + mapping(address signerOrTransmitter => Oracle) internal s_oracles; + + // s_signers contains the signing address of each oracle + address[] internal s_signers; + + // s_transmitters contains the transmission address of each oracle, + // i.e. the address the oracle actually sends transactions to the contract from + address[] internal s_transmitters; + + /* + * Config logic + */ + + // Reverts transaction if config args are invalid + modifier checkConfigValid( + uint256 numSigners, + uint256 numTransmitters, + uint256 f + ) { + if (numSigners > MAX_NUM_ORACLES) revert InvalidConfig("too many signers"); + if (f == 0) revert InvalidConfig("f must be positive"); + if (numSigners != numTransmitters) revert InvalidConfig("oracle addresses out of registration"); + if (numSigners <= 3 * f) revert InvalidConfig("faulty-oracle f too high"); + _; + } + + struct SetConfigArgs { + address[] signers; + address[] transmitters; + uint8 f; + bytes onchainConfig; + uint64 offchainConfigVersion; + bytes offchainConfig; + } + + /// @inheritdoc OCR2Abstract + function latestConfigDigestAndEpoch() + external + view + virtual + override + returns (bool scanLogs, bytes32 configDigest, uint32 epoch) + { + return (true, bytes32(0), uint32(0)); + } + + /** + * @notice sets offchain reporting protocol configuration incl. participating oracles + * @param _signers addresses with which oracles sign the reports + * @param _transmitters addresses oracles use to transmit the reports + * @param _f number of faulty oracles the system can tolerate + * @param _onchainConfig encoded on-chain contract configuration + * @param _offchainConfigVersion version number for offchainEncoding schema + * @param _offchainConfig encoded off-chain oracle configuration + */ + function setConfig( + address[] memory _signers, + address[] memory _transmitters, + uint8 _f, + bytes memory _onchainConfig, + uint64 _offchainConfigVersion, + bytes memory _offchainConfig + ) external override checkConfigValid(_signers.length, _transmitters.length, _f) onlyOwner { + SetConfigArgs memory args = SetConfigArgs({ + signers: _signers, + transmitters: _transmitters, + f: _f, + onchainConfig: _onchainConfig, + offchainConfigVersion: _offchainConfigVersion, + offchainConfig: _offchainConfig + }); + + _beforeSetConfig(args.f, args.onchainConfig); + + while (s_signers.length != 0) { + // remove any old signer/transmitter addresses + uint256 lastIdx = s_signers.length - 1; + address signer = s_signers[lastIdx]; + address transmitter = s_transmitters[lastIdx]; + delete s_oracles[signer]; + delete s_oracles[transmitter]; + s_signers.pop(); + s_transmitters.pop(); + } + + // Bounded by MAX_NUM_ORACLES in OCR2Abstract.sol + for (uint256 i = 0; i < args.signers.length; i++) { + // add new signer/transmitter addresses + // solhint-disable-next-line custom-errors + require(s_oracles[args.signers[i]].role == Role.Unset, "repeated signer address"); + s_oracles[args.signers[i]] = Oracle(uint8(i), Role.Signer); + // solhint-disable-next-line custom-errors + require(s_oracles[args.transmitters[i]].role == Role.Unset, "repeated transmitter address"); + s_oracles[args.transmitters[i]] = Oracle(uint8(i), Role.Transmitter); + s_signers.push(args.signers[i]); + s_transmitters.push(args.transmitters[i]); + } + s_configInfo.f = args.f; + uint32 previousConfigBlockNumber = s_latestConfigBlockNumber; + s_latestConfigBlockNumber = uint32(block.number); + s_configCount += 1; + { + s_configInfo.latestConfigDigest = _configDigestFromConfigData( + block.chainid, + address(this), + s_configCount, + args.signers, + args.transmitters, + args.f, + args.onchainConfig, + args.offchainConfigVersion, + args.offchainConfig + ); + } + s_configInfo.n = uint8(args.signers.length); + + emit ConfigSet( + previousConfigBlockNumber, + s_configInfo.latestConfigDigest, + s_configCount, + args.signers, + args.transmitters, + args.f, + args.onchainConfig, + args.offchainConfigVersion, + args.offchainConfig + ); + } + + function _configDigestFromConfigData( + uint256 _chainId, + address _contractAddress, + uint64 _configCount, + address[] memory _signers, + address[] memory _transmitters, + uint8 _f, + bytes memory _onchainConfig, + uint64 _encodedConfigVersion, + bytes memory _encodedConfig + ) internal pure returns (bytes32) { + uint256 h = uint256( + keccak256( + abi.encode( + _chainId, + _contractAddress, + _configCount, + _signers, + _transmitters, + _f, + _onchainConfig, + _encodedConfigVersion, + _encodedConfig + ) + ) + ); + uint256 prefixMask = type(uint256).max << (256 - 16); // 0xFFFF00..00 + uint256 prefix = 0x0001 << (256 - 16); // 0x000100..00 + return bytes32((prefix & prefixMask) | (h & ~prefixMask)); + } + + /** + * @notice information about current offchain reporting protocol configuration + * @return configCount ordinal number of current config, out of all configs applied to this contract so far + * @return blockNumber block at which this config was set + * @return configDigest domain-separation tag for current config (see __configDigestFromConfigData) + */ + function latestConfigDetails() + external + view + override + returns (uint32 configCount, uint32 blockNumber, bytes32 configDigest) + { + return (s_configCount, s_latestConfigBlockNumber, s_configInfo.latestConfigDigest); + } + + /** + * @return list of addresses permitted to transmit reports to this contract + * @dev The list will match the order used to specify the transmitter during setConfig + */ + function transmitters() external view returns (address[] memory) { + return s_transmitters; + } + + function _beforeSetConfig(uint8 _f, bytes memory _onchainConfig) internal virtual; + + /** + * @dev hook called after the report has been fully validated + * for the extending contract to handle additional logic, such as oracle payment + * @param initialGas the amount of gas before validation + * @param transmitter the address of the account that submitted the report + * @param signers the addresses of all signing accounts + * @param report serialized report + */ + function _report( + uint256 initialGas, + address transmitter, + uint8 signerCount, + address[MAX_NUM_ORACLES] memory signers, + bytes calldata report + ) internal virtual; + + // The constant-length components of the msg.data sent to transmit. + // See the "If we wanted to call sam" example on for example reasoning + // https://solidity.readthedocs.io/en/v0.7.2/abi-spec.html + uint16 private constant TRANSMIT_MSGDATA_CONSTANT_LENGTH_COMPONENT = + 4 + // function selector + 32 * + 3 + // 3 words containing reportContext + 32 + // word containing start location of abiencoded report value + 32 + // word containing location start of abiencoded rs value + 32 + // word containing start location of abiencoded ss value + 32 + // rawVs value + 32 + // word containing length of report + 32 + // word containing length rs + 32 + // word containing length of ss + 0; // placeholder + + function _requireExpectedMsgDataLength( + bytes calldata report, + bytes32[] calldata rs, + bytes32[] calldata ss + ) private pure { + // calldata will never be big enough to make this overflow + uint256 expected = uint256(TRANSMIT_MSGDATA_CONSTANT_LENGTH_COMPONENT) + + report.length + // one byte pure entry in _report + rs.length * + 32 + // 32 bytes per entry in _rs + ss.length * + 32 + // 32 bytes per entry in _ss + 0; // placeholder + // solhint-disable-next-line custom-errors + require(msg.data.length == expected, "calldata length mismatch"); + } + + /** + * @notice transmit is called to post a new report to the contract + * @param report serialized report, which the signatures are signing. + * @param rs ith element is the R components of the ith signature on report. Must have at most maxNumOracles entries + * @param ss ith element is the S components of the ith signature on report. Must have at most maxNumOracles entries + * @param rawVs ith element is the the V component of the ith signature + */ + function transmit( + // NOTE: If these parameters are changed, expectedMsgDataLength and/or + // TRANSMIT_MSGDATA_CONSTANT_LENGTH_COMPONENT need to be changed accordingly + bytes32[3] calldata reportContext, + bytes calldata report, + bytes32[] calldata rs, + bytes32[] calldata ss, + bytes32 rawVs // signatures + ) external override { + uint256 initialGas = gasleft(); // This line must come first + + { + // reportContext consists of: + // reportContext[0]: ConfigDigest + // reportContext[1]: 27 byte padding, 4-byte epoch and 1-byte round + // reportContext[2]: ExtraHash + bytes32 configDigest = reportContext[0]; + uint32 epochAndRound = uint32(uint256(reportContext[1])); + + emit Transmitted(configDigest, uint32(epochAndRound >> 8)); + + ConfigInfo memory configInfo = s_configInfo; + // solhint-disable-next-line custom-errors + require(configInfo.latestConfigDigest == configDigest, "configDigest mismatch"); + + _requireExpectedMsgDataLength(report, rs, ss); + + uint256 expectedNumSignatures; + if (i_uniqueReports) { + expectedNumSignatures = (configInfo.n + configInfo.f) / 2 + 1; + } else { + expectedNumSignatures = configInfo.f + 1; + } + + // solhint-disable-next-line custom-errors + require(rs.length == expectedNumSignatures, "wrong number of signatures"); + // solhint-disable-next-line custom-errors + require(rs.length == ss.length, "signatures out of registration"); + + Oracle memory transmitter = s_oracles[msg.sender]; + // solhint-disable-next-line custom-errors + require( // Check that sender is authorized to report + transmitter.role == Role.Transmitter && msg.sender == s_transmitters[transmitter.index], + "unauthorized transmitter" + ); + } + + address[MAX_NUM_ORACLES] memory signed; + uint8 signerCount = 0; + + { + // Verify signatures attached to report + bytes32 h = keccak256(abi.encodePacked(keccak256(report), reportContext)); + + Oracle memory o; + // Bounded by MAX_NUM_ORACLES in OCR2Abstract.sol + for (uint256 i = 0; i < rs.length; ++i) { + address signer = ecrecover(h, uint8(rawVs[i]) + 27, rs[i], ss[i]); + o = s_oracles[signer]; + // solhint-disable-next-line custom-errors + require(o.role == Role.Signer, "address not authorized to sign"); + // solhint-disable-next-line custom-errors + require(signed[o.index] == address(0), "non-unique signature"); + signed[o.index] = signer; + signerCount += 1; + } + } + + _report(initialGas, msg.sender, signerCount, signed, report); + } +} diff --git a/contracts/src/v0.8/functions/tests/v1_0_0/BaseTest.t.sol b/contracts/src/v0.8/functions/tests/v1_X/BaseTest.t.sol similarity index 100% rename from contracts/src/v0.8/functions/tests/v1_0_0/BaseTest.t.sol rename to contracts/src/v0.8/functions/tests/v1_X/BaseTest.t.sol diff --git a/contracts/src/v0.8/functions/tests/v1_0_0/FunctionsBilling.t.sol b/contracts/src/v0.8/functions/tests/v1_X/FunctionsBilling.t.sol similarity index 93% rename from contracts/src/v0.8/functions/tests/v1_0_0/FunctionsBilling.t.sol rename to contracts/src/v0.8/functions/tests/v1_X/FunctionsBilling.t.sol index a6fb5c2702f..7c63e66d9b1 100644 --- a/contracts/src/v0.8/functions/tests/v1_0_0/FunctionsBilling.t.sol +++ b/contracts/src/v0.8/functions/tests/v1_X/FunctionsBilling.t.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import {FunctionsCoordinator} from "../../dev/v1_0_0/FunctionsCoordinator.sol"; -import {FunctionsBilling} from "../../dev/v1_0_0/FunctionsBilling.sol"; -import {FunctionsRequest} from "../../dev/v1_0_0/libraries/FunctionsRequest.sol"; +import {FunctionsCoordinator} from "../../dev/v1_X/FunctionsCoordinator.sol"; +import {FunctionsBilling} from "../../dev/v1_X/FunctionsBilling.sol"; +import {FunctionsRequest} from "../../dev/v1_X/libraries/FunctionsRequest.sol"; import {FunctionsRouterSetup, FunctionsSubscriptionSetup, FunctionsMultipleFulfillmentsSetup} from "./Setup.t.sol"; @@ -72,13 +72,13 @@ contract FunctionsBilling_EstimateCost is FunctionsSubscriptionSetup { // Build minimal valid request data string memory sourceCode = "return 'hello world';"; FunctionsRequest.Request memory request; - FunctionsRequest.initializeRequest( + FunctionsRequest._initializeRequest( request, FunctionsRequest.Location.Inline, FunctionsRequest.CodeLanguage.JavaScript, sourceCode ); - bytes memory requestData = FunctionsRequest.encodeCBOR(request); + bytes memory requestData = FunctionsRequest._encodeCBOR(request); uint32 callbackGasLimit = 5_500; uint256 gasPriceWei = REASONABLE_GAS_PRICE_CEILING + 1; @@ -92,13 +92,13 @@ contract FunctionsBilling_EstimateCost is FunctionsSubscriptionSetup { // Build minimal valid request data string memory sourceCode = "return 'hello world';"; FunctionsRequest.Request memory request; - FunctionsRequest.initializeRequest( + FunctionsRequest._initializeRequest( request, FunctionsRequest.Location.Inline, FunctionsRequest.CodeLanguage.JavaScript, sourceCode ); - bytes memory requestData = FunctionsRequest.encodeCBOR(request); + bytes memory requestData = FunctionsRequest._encodeCBOR(request); uint32 callbackGasLimit = 5_500; uint256 gasPriceWei = 1; diff --git a/contracts/src/v0.8/functions/tests/v1_0_0/FunctionsClient.t.sol b/contracts/src/v0.8/functions/tests/v1_X/FunctionsClient.t.sol similarity index 78% rename from contracts/src/v0.8/functions/tests/v1_0_0/FunctionsClient.t.sol rename to contracts/src/v0.8/functions/tests/v1_X/FunctionsClient.t.sol index 10177d8ed7f..d6a3be16847 100644 --- a/contracts/src/v0.8/functions/tests/v1_0_0/FunctionsClient.t.sol +++ b/contracts/src/v0.8/functions/tests/v1_X/FunctionsClient.t.sol @@ -2,10 +2,10 @@ pragma solidity ^0.8.19; import {BaseTest} from "./BaseTest.t.sol"; -import {FunctionsRouter} from "../../dev/v1_0_0/FunctionsRouter.sol"; -import {FunctionsSubscriptions} from "../../dev/v1_0_0/FunctionsSubscriptions.sol"; -import {FunctionsRequest} from "../../dev/v1_0_0/libraries/FunctionsRequest.sol"; -import {FunctionsResponse} from "../../dev/v1_0_0/libraries/FunctionsResponse.sol"; +import {FunctionsRouter} from "../../dev/v1_X/FunctionsRouter.sol"; +import {FunctionsSubscriptions} from "../../dev/v1_X/FunctionsSubscriptions.sol"; +import {FunctionsRequest} from "../../dev/v1_X/libraries/FunctionsRequest.sol"; +import {FunctionsResponse} from "../../dev/v1_X/libraries/FunctionsResponse.sol"; import {FunctionsSubscriptionSetup} from "./Setup.t.sol"; @@ -22,13 +22,13 @@ contract FunctionsClient__SendRequest is FunctionsSubscriptionSetup { // Build minimal valid request data string memory sourceCode = "return 'hello world';"; FunctionsRequest.Request memory request; - FunctionsRequest.initializeRequest( + FunctionsRequest._initializeRequest( request, FunctionsRequest.Location.Inline, FunctionsRequest.CodeLanguage.JavaScript, sourceCode ); - bytes memory requestData = FunctionsRequest.encodeCBOR(request); + bytes memory requestData = FunctionsRequest._encodeCBOR(request); uint8 MAX_CALLBACK_GAS_LIMIT_FLAGS_INDEX = 0; bytes32 subscriptionFlags = s_functionsRouter.getFlags(s_subscriptionId); diff --git a/contracts/src/v0.8/functions/tests/v1_0_0/FunctionsCoordinator.t.sol b/contracts/src/v0.8/functions/tests/v1_X/FunctionsCoordinator.t.sol similarity index 84% rename from contracts/src/v0.8/functions/tests/v1_0_0/FunctionsCoordinator.t.sol rename to contracts/src/v0.8/functions/tests/v1_X/FunctionsCoordinator.t.sol index 174397ca8b5..95e955b4c88 100644 --- a/contracts/src/v0.8/functions/tests/v1_0_0/FunctionsCoordinator.t.sol +++ b/contracts/src/v0.8/functions/tests/v1_X/FunctionsCoordinator.t.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import {FunctionsCoordinator} from "../../dev/v1_0_0/FunctionsCoordinator.sol"; -import {FunctionsBilling} from "../../dev/v1_0_0/FunctionsBilling.sol"; -import {FunctionsRequest} from "../../dev/v1_0_0/libraries/FunctionsRequest.sol"; -import {FunctionsRouter} from "../../dev/v1_0_0/FunctionsRouter.sol"; +import {FunctionsCoordinator} from "../../dev/v1_X/FunctionsCoordinator.sol"; +import {FunctionsBilling} from "../../dev/v1_X/FunctionsBilling.sol"; +import {FunctionsRequest} from "../../dev/v1_X/libraries/FunctionsRequest.sol"; +import {FunctionsRouter} from "../../dev/v1_X/FunctionsRouter.sol"; import {FunctionsRouterSetup} from "./Setup.t.sol"; diff --git a/contracts/src/v0.8/functions/tests/v1_0_0/FunctionsRequest.t.sol b/contracts/src/v0.8/functions/tests/v1_X/FunctionsRequest.t.sol similarity index 94% rename from contracts/src/v0.8/functions/tests/v1_0_0/FunctionsRequest.t.sol rename to contracts/src/v0.8/functions/tests/v1_X/FunctionsRequest.t.sol index c14bdca5ecb..5457a221b61 100644 --- a/contracts/src/v0.8/functions/tests/v1_0_0/FunctionsRequest.t.sol +++ b/contracts/src/v0.8/functions/tests/v1_X/FunctionsRequest.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import {FunctionsRequest} from "../../dev/v1_0_0/libraries/FunctionsRequest.sol"; +import {FunctionsRequest} from "../../dev/v1_X/libraries/FunctionsRequest.sol"; import {Test} from "forge-std/Test.sol"; diff --git a/contracts/src/v0.8/functions/tests/v1_0_0/FunctionsRouter.t.sol b/contracts/src/v0.8/functions/tests/v1_X/FunctionsRouter.t.sol similarity index 95% rename from contracts/src/v0.8/functions/tests/v1_0_0/FunctionsRouter.t.sol rename to contracts/src/v0.8/functions/tests/v1_X/FunctionsRouter.t.sol index 6c07158377c..628c945d652 100644 --- a/contracts/src/v0.8/functions/tests/v1_0_0/FunctionsRouter.t.sol +++ b/contracts/src/v0.8/functions/tests/v1_X/FunctionsRouter.t.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import {FunctionsRouter} from "../../dev/v1_0_0/FunctionsRouter.sol"; -import {FunctionsSubscriptions} from "../../dev/v1_0_0/FunctionsSubscriptions.sol"; -import {FunctionsCoordinator} from "../../dev/v1_0_0/FunctionsCoordinator.sol"; -import {FunctionsBilling} from "../../dev/v1_0_0/FunctionsBilling.sol"; -import {FunctionsRequest} from "../../dev/v1_0_0/libraries/FunctionsRequest.sol"; -import {FunctionsResponse} from "../../dev/v1_0_0/libraries/FunctionsResponse.sol"; +import {FunctionsRouter} from "../../dev/v1_X/FunctionsRouter.sol"; +import {FunctionsSubscriptions} from "../../dev/v1_X/FunctionsSubscriptions.sol"; +import {FunctionsCoordinator} from "../../dev/v1_X/FunctionsCoordinator.sol"; +import {FunctionsBilling} from "../../dev/v1_X/FunctionsBilling.sol"; +import {FunctionsRequest} from "../../dev/v1_X/libraries/FunctionsRequest.sol"; +import {FunctionsResponse} from "../../dev/v1_X/libraries/FunctionsResponse.sol"; import {FunctionsCoordinatorTestHelper} from "./testhelpers/FunctionsCoordinatorTestHelper.sol"; import {FunctionsClientTestHelper} from "./testhelpers/FunctionsClientTestHelper.sol"; @@ -206,13 +206,13 @@ contract FunctionsRouter_SendRequest is FunctionsSubscriptionSetup { // Build minimal valid request data string memory sourceCode = "return 'hello world';"; FunctionsRequest.Request memory request; - FunctionsRequest.initializeRequest( + FunctionsRequest._initializeRequest( request, FunctionsRequest.Location.Inline, FunctionsRequest.CodeLanguage.JavaScript, sourceCode ); - bytes memory requestData = FunctionsRequest.encodeCBOR(request); + bytes memory requestData = FunctionsRequest._encodeCBOR(request); bytes32 invalidDonId = bytes32("this does not exist"); @@ -230,13 +230,13 @@ contract FunctionsRouter_SendRequest is FunctionsSubscriptionSetup { // Build minimal valid request data string memory sourceCode = "return 'hello world';"; FunctionsRequest.Request memory request; - FunctionsRequest.initializeRequest( + FunctionsRequest._initializeRequest( request, FunctionsRequest.Location.Inline, FunctionsRequest.CodeLanguage.JavaScript, sourceCode ); - bytes memory requestData = FunctionsRequest.encodeCBOR(request); + bytes memory requestData = FunctionsRequest._encodeCBOR(request); bytes32 incorrectDonId = s_functionsRouter.getAllowListId(); @@ -257,13 +257,13 @@ contract FunctionsRouter_SendRequest is FunctionsSubscriptionSetup { // Build minimal valid request data string memory sourceCode = "return 'hello world';"; FunctionsRequest.Request memory request; - FunctionsRequest.initializeRequest( + FunctionsRequest._initializeRequest( request, FunctionsRequest.Location.Inline, FunctionsRequest.CodeLanguage.JavaScript, sourceCode ); - bytes memory requestData = FunctionsRequest.encodeCBOR(request); + bytes memory requestData = FunctionsRequest._encodeCBOR(request); vm.expectRevert("Pausable: paused"); s_functionsRouter.sendRequest(s_subscriptionId, requestData, FunctionsRequest.REQUEST_DATA_VERSION, 5000, s_donId); @@ -273,13 +273,13 @@ contract FunctionsRouter_SendRequest is FunctionsSubscriptionSetup { // Build minimal valid request data string memory sourceCode = "return 'hello world';"; FunctionsRequest.Request memory request; - FunctionsRequest.initializeRequest( + FunctionsRequest._initializeRequest( request, FunctionsRequest.Location.Inline, FunctionsRequest.CodeLanguage.JavaScript, sourceCode ); - bytes memory requestData = FunctionsRequest.encodeCBOR(request); + bytes memory requestData = FunctionsRequest._encodeCBOR(request); uint64 invalidSubscriptionId = 123456789; @@ -300,13 +300,13 @@ contract FunctionsRouter_SendRequest is FunctionsSubscriptionSetup { // Build minimal valid request data string memory sourceCode = "return 'hello world';"; FunctionsRequest.Request memory request; - FunctionsRequest.initializeRequest( + FunctionsRequest._initializeRequest( request, FunctionsRequest.Location.Inline, FunctionsRequest.CodeLanguage.JavaScript, sourceCode ); - bytes memory requestData = FunctionsRequest.encodeCBOR(request); + bytes memory requestData = FunctionsRequest._encodeCBOR(request); vm.expectRevert(FunctionsSubscriptions.InvalidConsumer.selector); s_functionsRouter.sendRequest(s_subscriptionId, requestData, FunctionsRequest.REQUEST_DATA_VERSION, 5000, s_donId); @@ -316,13 +316,13 @@ contract FunctionsRouter_SendRequest is FunctionsSubscriptionSetup { // Build minimal valid request data string memory sourceCode = "return 'hello world';"; FunctionsRequest.Request memory request; - FunctionsRequest.initializeRequest( + FunctionsRequest._initializeRequest( request, FunctionsRequest.Location.Inline, FunctionsRequest.CodeLanguage.JavaScript, sourceCode ); - bytes memory requestData = FunctionsRequest.encodeCBOR(request); + bytes memory requestData = FunctionsRequest._encodeCBOR(request); uint8 MAX_CALLBACK_GAS_LIMIT_FLAGS_INDEX = 0; bytes32 subscriptionFlags = s_functionsRouter.getFlags(s_subscriptionId); @@ -364,13 +364,13 @@ contract FunctionsRouter_SendRequest is FunctionsSubscriptionSetup { // Build minimal valid request data string memory sourceCode = "return 'hello world';"; FunctionsRequest.Request memory request; - FunctionsRequest.initializeRequest( + FunctionsRequest._initializeRequest( request, FunctionsRequest.Location.Inline, FunctionsRequest.CodeLanguage.JavaScript, sourceCode ); - bytes memory requestData = FunctionsRequest.encodeCBOR(request); + bytes memory requestData = FunctionsRequest._encodeCBOR(request); uint32 callbackGasLimit = 5000; vm.expectRevert(FunctionsBilling.InsufficientBalance.selector); @@ -388,14 +388,14 @@ contract FunctionsRouter_SendRequest is FunctionsSubscriptionSetup { // Build minimal valid request data string memory sourceCode = "return 'hello world';"; FunctionsRequest.Request memory request; - FunctionsRequest.initializeRequest( + FunctionsRequest._initializeRequest( request, FunctionsRequest.Location.Inline, FunctionsRequest.CodeLanguage.JavaScript, sourceCode ); uint32 callbackGasLimit = 5_000; - bytes memory requestData = FunctionsRequest.encodeCBOR(request); + bytes memory requestData = FunctionsRequest._encodeCBOR(request); // Send a first request that will remain pending bytes32 requestId = s_functionsRouter.sendRequest( @@ -454,13 +454,13 @@ contract FunctionsRouter_SendRequest is FunctionsSubscriptionSetup { // Build minimal valid request data string memory sourceCode = "return 'hello world';"; FunctionsRequest.Request memory request; - FunctionsRequest.initializeRequest( + FunctionsRequest._initializeRequest( request, FunctionsRequest.Location.Inline, FunctionsRequest.CodeLanguage.JavaScript, sourceCode ); - bytes memory requestData = FunctionsRequest.encodeCBOR(request); + bytes memory requestData = FunctionsRequest._encodeCBOR(request); uint32 callbackGasLimit = 5000; @@ -542,13 +542,13 @@ contract FunctionsRouter_SendRequestToProposed is FunctionsSubscriptionSetup { // Build minimal valid request data string memory sourceCode = "return 'hello world';"; FunctionsRequest.Request memory request; - FunctionsRequest.initializeRequest( + FunctionsRequest._initializeRequest( request, FunctionsRequest.Location.Inline, FunctionsRequest.CodeLanguage.JavaScript, sourceCode ); - bytes memory requestData = FunctionsRequest.encodeCBOR(request); + bytes memory requestData = FunctionsRequest._encodeCBOR(request); bytes32 invalidDonId = bytes32("this does not exist"); @@ -566,13 +566,13 @@ contract FunctionsRouter_SendRequestToProposed is FunctionsSubscriptionSetup { // Build minimal valid request data string memory sourceCode = "return 'hello world';"; FunctionsRequest.Request memory request; - FunctionsRequest.initializeRequest( + FunctionsRequest._initializeRequest( request, FunctionsRequest.Location.Inline, FunctionsRequest.CodeLanguage.JavaScript, sourceCode ); - bytes memory requestData = FunctionsRequest.encodeCBOR(request); + bytes memory requestData = FunctionsRequest._encodeCBOR(request); bytes32 incorrectDonId = s_functionsRouter.getAllowListId(); @@ -593,13 +593,13 @@ contract FunctionsRouter_SendRequestToProposed is FunctionsSubscriptionSetup { // Build minimal valid request data string memory sourceCode = "return 'hello world';"; FunctionsRequest.Request memory request; - FunctionsRequest.initializeRequest( + FunctionsRequest._initializeRequest( request, FunctionsRequest.Location.Inline, FunctionsRequest.CodeLanguage.JavaScript, sourceCode ); - bytes memory requestData = FunctionsRequest.encodeCBOR(request); + bytes memory requestData = FunctionsRequest._encodeCBOR(request); vm.expectRevert("Pausable: paused"); s_functionsRouter.sendRequestToProposed( @@ -615,13 +615,13 @@ contract FunctionsRouter_SendRequestToProposed is FunctionsSubscriptionSetup { // Build minimal valid request data string memory sourceCode = "return 'hello world';"; FunctionsRequest.Request memory request; - FunctionsRequest.initializeRequest( + FunctionsRequest._initializeRequest( request, FunctionsRequest.Location.Inline, FunctionsRequest.CodeLanguage.JavaScript, sourceCode ); - bytes memory requestData = FunctionsRequest.encodeCBOR(request); + bytes memory requestData = FunctionsRequest._encodeCBOR(request); uint64 invalidSubscriptionId = 123456789; @@ -642,13 +642,13 @@ contract FunctionsRouter_SendRequestToProposed is FunctionsSubscriptionSetup { // Build minimal valid request data string memory sourceCode = "return 'hello world';"; FunctionsRequest.Request memory request; - FunctionsRequest.initializeRequest( + FunctionsRequest._initializeRequest( request, FunctionsRequest.Location.Inline, FunctionsRequest.CodeLanguage.JavaScript, sourceCode ); - bytes memory requestData = FunctionsRequest.encodeCBOR(request); + bytes memory requestData = FunctionsRequest._encodeCBOR(request); vm.expectRevert(FunctionsSubscriptions.InvalidConsumer.selector); s_functionsRouter.sendRequestToProposed( @@ -664,13 +664,13 @@ contract FunctionsRouter_SendRequestToProposed is FunctionsSubscriptionSetup { // Build minimal valid request data string memory sourceCode = "return 'hello world';"; FunctionsRequest.Request memory request; - FunctionsRequest.initializeRequest( + FunctionsRequest._initializeRequest( request, FunctionsRequest.Location.Inline, FunctionsRequest.CodeLanguage.JavaScript, sourceCode ); - bytes memory requestData = FunctionsRequest.encodeCBOR(request); + bytes memory requestData = FunctionsRequest._encodeCBOR(request); uint8 MAX_CALLBACK_GAS_LIMIT_FLAGS_INDEX = 0; bytes32 subscriptionFlags = s_functionsRouter.getFlags(s_subscriptionId); @@ -712,13 +712,13 @@ contract FunctionsRouter_SendRequestToProposed is FunctionsSubscriptionSetup { // Build minimal valid request data string memory sourceCode = "return 'hello world';"; FunctionsRequest.Request memory request; - FunctionsRequest.initializeRequest( + FunctionsRequest._initializeRequest( request, FunctionsRequest.Location.Inline, FunctionsRequest.CodeLanguage.JavaScript, sourceCode ); - bytes memory requestData = FunctionsRequest.encodeCBOR(request); + bytes memory requestData = FunctionsRequest._encodeCBOR(request); uint32 callbackGasLimit = 5000; vm.expectRevert(FunctionsBilling.InsufficientBalance.selector); @@ -749,13 +749,13 @@ contract FunctionsRouter_SendRequestToProposed is FunctionsSubscriptionSetup { // Build minimal valid request data string memory sourceCode = "return 'hello world';"; FunctionsRequest.Request memory request; - FunctionsRequest.initializeRequest( + FunctionsRequest._initializeRequest( request, FunctionsRequest.Location.Inline, FunctionsRequest.CodeLanguage.JavaScript, sourceCode ); - bytes memory requestData = FunctionsRequest.encodeCBOR(request); + bytes memory requestData = FunctionsRequest._encodeCBOR(request); uint32 callbackGasLimit = 5000; diff --git a/contracts/src/v0.8/functions/tests/v1_0_0/FunctionsSubscriptions.t.sol b/contracts/src/v0.8/functions/tests/v1_X/FunctionsSubscriptions.t.sol similarity index 99% rename from contracts/src/v0.8/functions/tests/v1_0_0/FunctionsSubscriptions.t.sol rename to contracts/src/v0.8/functions/tests/v1_X/FunctionsSubscriptions.t.sol index ea5ec0dd683..93203b02be8 100644 --- a/contracts/src/v0.8/functions/tests/v1_0_0/FunctionsSubscriptions.t.sol +++ b/contracts/src/v0.8/functions/tests/v1_X/FunctionsSubscriptions.t.sol @@ -2,9 +2,9 @@ pragma solidity ^0.8.19; import {BaseTest} from "./BaseTest.t.sol"; -import {FunctionsRouter} from "../../dev/v1_0_0/FunctionsRouter.sol"; -import {FunctionsSubscriptions} from "../../dev/v1_0_0/FunctionsSubscriptions.sol"; -import {FunctionsResponse} from "../../dev/v1_0_0/libraries/FunctionsResponse.sol"; +import {FunctionsRouter} from "../../dev/v1_X/FunctionsRouter.sol"; +import {FunctionsSubscriptions} from "../../dev/v1_X/FunctionsSubscriptions.sol"; +import {FunctionsResponse} from "../../dev/v1_X/libraries/FunctionsResponse.sol"; import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.0/contracts/token/ERC20/IERC20.sol"; diff --git a/contracts/src/v0.8/functions/tests/v1_0_0/FunctionsTermsOfServiceAllowList.t.sol b/contracts/src/v0.8/functions/tests/v1_X/FunctionsTermsOfServiceAllowList.t.sol similarity index 99% rename from contracts/src/v0.8/functions/tests/v1_0_0/FunctionsTermsOfServiceAllowList.t.sol rename to contracts/src/v0.8/functions/tests/v1_X/FunctionsTermsOfServiceAllowList.t.sol index d8f03ba3ebf..4ab3daab0aa 100644 --- a/contracts/src/v0.8/functions/tests/v1_0_0/FunctionsTermsOfServiceAllowList.t.sol +++ b/contracts/src/v0.8/functions/tests/v1_X/FunctionsTermsOfServiceAllowList.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import {TermsOfServiceAllowList} from "../../dev/v1_0_0/accessControl/TermsOfServiceAllowList.sol"; +import {TermsOfServiceAllowList} from "../../dev/v1_X/accessControl/TermsOfServiceAllowList.sol"; import {FunctionsClientTestHelper} from "./testhelpers/FunctionsClientTestHelper.sol"; import {FunctionsRoutesSetup, FunctionsOwnerAcceptTermsOfServiceSetup} from "./Setup.t.sol"; diff --git a/contracts/src/v0.8/functions/tests/v1_0_0/Gas.t.sol b/contracts/src/v0.8/functions/tests/v1_X/Gas.t.sol similarity index 95% rename from contracts/src/v0.8/functions/tests/v1_0_0/Gas.t.sol rename to contracts/src/v0.8/functions/tests/v1_X/Gas.t.sol index f28a8e6defe..55ab3810b41 100644 --- a/contracts/src/v0.8/functions/tests/v1_0_0/Gas.t.sol +++ b/contracts/src/v0.8/functions/tests/v1_X/Gas.t.sol @@ -2,10 +2,10 @@ pragma solidity ^0.8.19; import {BaseTest} from "./BaseTest.t.sol"; -import {FunctionsRouter} from "../../dev/v1_0_0/FunctionsRouter.sol"; -import {FunctionsSubscriptions} from "../../dev/v1_0_0/FunctionsSubscriptions.sol"; -import {FunctionsRequest} from "../../dev/v1_0_0/libraries/FunctionsRequest.sol"; -import {FunctionsResponse} from "../../dev/v1_0_0/libraries/FunctionsResponse.sol"; +import {FunctionsRouter} from "../../dev/v1_X/FunctionsRouter.sol"; +import {FunctionsSubscriptions} from "../../dev/v1_X/FunctionsSubscriptions.sol"; +import {FunctionsRequest} from "../../dev/v1_X/libraries/FunctionsRequest.sol"; +import {FunctionsResponse} from "../../dev/v1_X/libraries/FunctionsResponse.sol"; import {FunctionsClientTestHelper} from "./testhelpers/FunctionsClientTestHelper.sol"; import {FunctionsRoutesSetup, FunctionsOwnerAcceptTermsOfServiceSetup, FunctionsSubscriptionSetup, FunctionsClientRequestSetup} from "./Setup.t.sol"; @@ -102,13 +102,13 @@ contract Gas_SendRequest is FunctionsSubscriptionSetup { // Create minimum viable request data FunctionsRequest.Request memory minimalRequest; string memory minimalSourceCode = "return Functions.encodeString('hello world');"; - FunctionsRequest.initializeRequest( + FunctionsRequest._initializeRequest( minimalRequest, FunctionsRequest.Location.Inline, FunctionsRequest.CodeLanguage.JavaScript, minimalSourceCode ); - s_minimalRequestData = FunctionsRequest.encodeCBOR(minimalRequest); + s_minimalRequestData = FunctionsRequest._encodeCBOR(minimalRequest); } { @@ -117,13 +117,13 @@ contract Gas_SendRequest is FunctionsSubscriptionSetup { // Create maximum viable request data - 30 KB encoded data string memory maximalSourceCode = _makeStringOfBytesSize(29_898); // CBOR size without source code is 102 bytes - FunctionsRequest.initializeRequest( + FunctionsRequest._initializeRequest( maxmimalRequest, FunctionsRequest.Location.Inline, FunctionsRequest.CodeLanguage.JavaScript, maximalSourceCode ); - s_maximalRequestData = FunctionsRequest.encodeCBOR(maxmimalRequest); + s_maximalRequestData = FunctionsRequest._encodeCBOR(maxmimalRequest); assertEq(s_maximalRequestData.length, 30_000); } } diff --git a/contracts/src/v0.8/functions/tests/v1_0_0/OCR2.t.sol b/contracts/src/v0.8/functions/tests/v1_X/OCR2.t.sol similarity index 100% rename from contracts/src/v0.8/functions/tests/v1_0_0/OCR2.t.sol rename to contracts/src/v0.8/functions/tests/v1_X/OCR2.t.sol diff --git a/contracts/src/v0.8/functions/tests/v1_0_0/README.md b/contracts/src/v0.8/functions/tests/v1_X/README.md similarity index 84% rename from contracts/src/v0.8/functions/tests/v1_0_0/README.md rename to contracts/src/v0.8/functions/tests/v1_X/README.md index 42aa6ed2b7b..6400a28dc79 100644 --- a/contracts/src/v0.8/functions/tests/v1_0_0/README.md +++ b/contracts/src/v0.8/functions/tests/v1_X/README.md @@ -12,7 +12,7 @@ forge test -vv To run a specific file use: ``` -forge test -vv --mp src/v0.8/functions/tests/v1_0_0/[File Name].t.sol +forge test -vv --mp src/v0.8/functions/tests/v1_X/[File Name].t.sol ``` To see coverage: diff --git a/contracts/src/v0.8/functions/tests/v1_0_0/Setup.t.sol b/contracts/src/v0.8/functions/tests/v1_X/Setup.t.sol similarity index 98% rename from contracts/src/v0.8/functions/tests/v1_0_0/Setup.t.sol rename to contracts/src/v0.8/functions/tests/v1_X/Setup.t.sol index 5226af19d96..3bbd6187a94 100644 --- a/contracts/src/v0.8/functions/tests/v1_0_0/Setup.t.sol +++ b/contracts/src/v0.8/functions/tests/v1_X/Setup.t.sol @@ -2,12 +2,12 @@ pragma solidity ^0.8.19; import {BaseTest} from "./BaseTest.t.sol"; -import {FunctionsRouter} from "../../dev/v1_0_0/FunctionsRouter.sol"; +import {FunctionsRouter} from "../../dev/v1_X/FunctionsRouter.sol"; import {FunctionsCoordinatorTestHelper} from "./testhelpers/FunctionsCoordinatorTestHelper.sol"; -import {FunctionsBilling} from "../../dev/v1_0_0/FunctionsBilling.sol"; -import {FunctionsResponse} from "../../dev/v1_0_0/libraries/FunctionsResponse.sol"; +import {FunctionsBilling} from "../../dev/v1_X/FunctionsBilling.sol"; +import {FunctionsResponse} from "../../dev/v1_X/libraries/FunctionsResponse.sol"; import {MockV3Aggregator} from "../../../tests/MockV3Aggregator.sol"; -import {TermsOfServiceAllowList} from "../../dev/v1_0_0/accessControl/TermsOfServiceAllowList.sol"; +import {TermsOfServiceAllowList} from "../../dev/v1_X/accessControl/TermsOfServiceAllowList.sol"; import {FunctionsClientUpgradeHelper} from "./testhelpers/FunctionsClientUpgradeHelper.sol"; import {MockLinkToken} from "../../../mocks/MockLinkToken.sol"; diff --git a/contracts/src/v0.8/functions/tests/v1_0_0/testhelpers/FunctionsClientTestHelper.sol b/contracts/src/v0.8/functions/tests/v1_X/testhelpers/FunctionsClientTestHelper.sol similarity index 77% rename from contracts/src/v0.8/functions/tests/v1_0_0/testhelpers/FunctionsClientTestHelper.sol rename to contracts/src/v0.8/functions/tests/v1_X/testhelpers/FunctionsClientTestHelper.sol index 57129d0153f..bca0f0a3fa2 100644 --- a/contracts/src/v0.8/functions/tests/v1_0_0/testhelpers/FunctionsClientTestHelper.sol +++ b/contracts/src/v0.8/functions/tests/v1_X/testhelpers/FunctionsClientTestHelper.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.6; -import {ITermsOfServiceAllowList} from "../../../dev/v1_0_0/accessControl/interfaces/ITermsOfServiceAllowList.sol"; -import {IFunctionsSubscriptions} from "../../../dev/v1_0_0/interfaces/IFunctionsSubscriptions.sol"; +import {ITermsOfServiceAllowList} from "../../../dev/v1_X/accessControl/interfaces/ITermsOfServiceAllowList.sol"; +import {IFunctionsSubscriptions} from "../../../dev/v1_X/interfaces/IFunctionsSubscriptions.sol"; -import {FunctionsRequest} from "../../../dev/v1_0_0/libraries/FunctionsRequest.sol"; -import {FunctionsClient} from "../../../dev/v1_0_0/FunctionsClient.sol"; +import {FunctionsRequest} from "../../../dev/v1_X/libraries/FunctionsRequest.sol"; +import {FunctionsClient} from "../../../dev/v1_X/FunctionsClient.sol"; contract FunctionsClientTestHelper is FunctionsClient { using FunctionsRequest for FunctionsRequest.Request; @@ -34,12 +34,12 @@ contract FunctionsClientTestHelper is FunctionsClient { uint32 callbackGasLimit ) public returns (bytes32 requestId) { FunctionsRequest.Request memory req; - req.initializeRequestForInlineJavaScript(source); - if (secrets.length > 0) req.addSecretsReference(secrets); - if (args.length > 0) req.setArgs(args); - if (bytesArgs.length > 0) req.setBytesArgs(bytesArgs); + req._initializeRequestForInlineJavaScript(source); + if (secrets.length > 0) req._addSecretsReference(secrets); + if (args.length > 0) req._setArgs(args); + if (bytesArgs.length > 0) req._setBytesArgs(bytesArgs); - return _sendRequest(FunctionsRequest.encodeCBOR(req), subscriptionId, callbackGasLimit, donId); + return _sendRequest(FunctionsRequest._encodeCBOR(req), subscriptionId, callbackGasLimit, donId); } function sendSimpleRequestWithJavaScript( @@ -49,8 +49,8 @@ contract FunctionsClientTestHelper is FunctionsClient { uint32 callbackGasLimit ) public returns (bytes32 requestId) { FunctionsRequest.Request memory request; - request.initializeRequestForInlineJavaScript(sourceCode); - bytes memory requestData = FunctionsRequest.encodeCBOR(request); + request._initializeRequestForInlineJavaScript(sourceCode); + bytes memory requestData = FunctionsRequest._encodeCBOR(request); requestId = _sendRequest(requestData, subscriptionId, callbackGasLimit, donId); emit SendRequestInvoked(requestId, sourceCode, subscriptionId); } @@ -62,8 +62,8 @@ contract FunctionsClientTestHelper is FunctionsClient { ) public returns (bytes32 requestId) { FunctionsRequest.Request memory request; uint32 callbackGasLimit = 20_000; - request.initializeRequestForInlineJavaScript(sourceCode); - bytes memory requestData = FunctionsRequest.encodeCBOR(request); + request._initializeRequestForInlineJavaScript(sourceCode); + bytes memory requestData = FunctionsRequest._encodeCBOR(request); requestId = i_router.sendRequestToProposed( subscriptionId, requestData, @@ -85,7 +85,7 @@ contract FunctionsClientTestHelper is FunctionsClient { IFunctionsSubscriptions(address(i_router)).acceptSubscriptionOwnerTransfer(subscriptionId); } - function fulfillRequest(bytes32 requestId, bytes memory response, bytes memory err) internal override { + function _fulfillRequest(bytes32 requestId, bytes memory response, bytes memory err) internal override { if (s_revertFulfillRequest) { revert(s_revertFulfillRequestMessage); } diff --git a/contracts/src/v0.8/functions/tests/v1_0_0/testhelpers/FunctionsClientUpgradeHelper.sol b/contracts/src/v0.8/functions/tests/v1_X/testhelpers/FunctionsClientUpgradeHelper.sol similarity index 74% rename from contracts/src/v0.8/functions/tests/v1_0_0/testhelpers/FunctionsClientUpgradeHelper.sol rename to contracts/src/v0.8/functions/tests/v1_X/testhelpers/FunctionsClientUpgradeHelper.sol index 4cfa41d1e46..a52c3009927 100644 --- a/contracts/src/v0.8/functions/tests/v1_0_0/testhelpers/FunctionsClientUpgradeHelper.sol +++ b/contracts/src/v0.8/functions/tests/v1_X/testhelpers/FunctionsClientUpgradeHelper.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import {FunctionsRequest} from "../../../dev/v1_0_0/libraries/FunctionsRequest.sol"; -import {FunctionsClient} from "../../../dev/v1_0_0/FunctionsClient.sol"; +import {FunctionsRequest} from "../../../dev/v1_X/libraries/FunctionsRequest.sol"; +import {FunctionsClient} from "../../../dev/v1_X/FunctionsClient.sol"; import {ConfirmedOwner} from "../../../../shared/access/ConfirmedOwner.sol"; contract FunctionsClientUpgradeHelper is FunctionsClient, ConfirmedOwner { @@ -33,12 +33,12 @@ contract FunctionsClientUpgradeHelper is FunctionsClient, ConfirmedOwner { uint32 callbackGasLimit ) public onlyOwner returns (bytes32) { FunctionsRequest.Request memory req; - req.initializeRequestForInlineJavaScript(source); - if (secrets.length > 0) req.addSecretsReference(secrets); - if (args.length > 0) req.setArgs(args); - if (bytesArgs.length > 0) req.setBytesArgs(bytesArgs); + req._initializeRequestForInlineJavaScript(source); + if (secrets.length > 0) req._addSecretsReference(secrets); + if (args.length > 0) req._setArgs(args); + if (bytesArgs.length > 0) req._setBytesArgs(bytesArgs); - return _sendRequest(FunctionsRequest.encodeCBOR(req), subscriptionId, callbackGasLimit, donId); + return _sendRequest(FunctionsRequest._encodeCBOR(req), subscriptionId, callbackGasLimit, donId); } function sendRequestBytes( @@ -63,12 +63,12 @@ contract FunctionsClientUpgradeHelper is FunctionsClient, ConfirmedOwner { uint32 callbackGasLimit ) public onlyOwner returns (bytes32) { FunctionsRequest.Request memory req; - req.initializeRequestForInlineJavaScript(source); - req.addDONHostedSecrets(slotId, slotVersion); + req._initializeRequestForInlineJavaScript(source); + req._addDONHostedSecrets(slotId, slotVersion); - if (args.length > 0) req.setArgs(args); + if (args.length > 0) req._setArgs(args); - return _sendRequest(FunctionsRequest.encodeCBOR(req), subscriptionId, callbackGasLimit, donId); + return _sendRequest(FunctionsRequest._encodeCBOR(req), subscriptionId, callbackGasLimit, donId); } // @notice Sends a Chainlink Functions request @@ -114,12 +114,12 @@ contract FunctionsClientUpgradeHelper is FunctionsClient, ConfirmedOwner { uint32 callbackGasLimit ) public onlyOwner returns (bytes32) { FunctionsRequest.Request memory req; - req.initializeRequestForInlineJavaScript(source); - if (secrets.length > 0) req.addSecretsReference(secrets); - if (args.length > 0) req.setArgs(args); - if (bytesArgs.length > 0) req.setBytesArgs(bytesArgs); + req._initializeRequestForInlineJavaScript(source); + if (secrets.length > 0) req._addSecretsReference(secrets); + if (args.length > 0) req._setArgs(args); + if (bytesArgs.length > 0) req._setBytesArgs(bytesArgs); - return _sendRequestToProposed(FunctionsRequest.encodeCBOR(req), subscriptionId, callbackGasLimit, donId); + return _sendRequestToProposed(FunctionsRequest._encodeCBOR(req), subscriptionId, callbackGasLimit, donId); } /** @@ -135,12 +135,12 @@ contract FunctionsClientUpgradeHelper is FunctionsClient, ConfirmedOwner { uint32 callbackGasLimit ) public onlyOwner returns (bytes32) { FunctionsRequest.Request memory req; - req.initializeRequestForInlineJavaScript(source); - req.addDONHostedSecrets(slotId, slotVersion); + req._initializeRequestForInlineJavaScript(source); + req._addDONHostedSecrets(slotId, slotVersion); - if (args.length > 0) req.setArgs(args); + if (args.length > 0) req._setArgs(args); - return _sendRequestToProposed(FunctionsRequest.encodeCBOR(req), subscriptionId, callbackGasLimit, donId); + return _sendRequestToProposed(FunctionsRequest._encodeCBOR(req), subscriptionId, callbackGasLimit, donId); } /** @@ -151,7 +151,7 @@ contract FunctionsClientUpgradeHelper is FunctionsClient, ConfirmedOwner { * @param err Aggregated error from the user code or from the execution pipeline * Either response or error parameter will be set, but never both */ - function fulfillRequest(bytes32 requestId, bytes memory response, bytes memory err) internal override { + function _fulfillRequest(bytes32 requestId, bytes memory response, bytes memory err) internal override { emit ResponseReceived(requestId, response, err); } } diff --git a/contracts/src/v0.8/functions/tests/v1_0_0/testhelpers/FunctionsClientWithEmptyCallback.sol b/contracts/src/v0.8/functions/tests/v1_X/testhelpers/FunctionsClientWithEmptyCallback.sol similarity index 66% rename from contracts/src/v0.8/functions/tests/v1_0_0/testhelpers/FunctionsClientWithEmptyCallback.sol rename to contracts/src/v0.8/functions/tests/v1_X/testhelpers/FunctionsClientWithEmptyCallback.sol index 5feede895f0..362b21d89ba 100644 --- a/contracts/src/v0.8/functions/tests/v1_0_0/testhelpers/FunctionsClientWithEmptyCallback.sol +++ b/contracts/src/v0.8/functions/tests/v1_X/testhelpers/FunctionsClientWithEmptyCallback.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.6; -import {FunctionsRequest} from "../../../dev/v1_0_0/libraries/FunctionsRequest.sol"; -import {FunctionsClient} from "../../../dev/v1_0_0/FunctionsClient.sol"; +import {FunctionsRequest} from "../../../dev/v1_X/libraries/FunctionsRequest.sol"; +import {FunctionsClient} from "../../../dev/v1_X/FunctionsClient.sol"; contract FunctionsClientWithEmptyCallback is FunctionsClient { using FunctionsRequest for FunctionsRequest.Request; @@ -19,13 +19,13 @@ contract FunctionsClientWithEmptyCallback is FunctionsClient { uint32 callbackGasLimit ) public returns (bytes32 requestId) { FunctionsRequest.Request memory request; - request.initializeRequestForInlineJavaScript(sourceCode); - bytes memory requestData = FunctionsRequest.encodeCBOR(request); + request._initializeRequestForInlineJavaScript(sourceCode); + bytes memory requestData = FunctionsRequest._encodeCBOR(request); requestId = _sendRequest(requestData, subscriptionId, callbackGasLimit, donId); emit SendRequestInvoked(requestId, sourceCode, subscriptionId); } - function fulfillRequest(bytes32 /*requestId*/, bytes memory /*response*/, bytes memory /*err*/) internal override { + function _fulfillRequest(bytes32 /*requestId*/, bytes memory /*response*/, bytes memory /*err*/) internal override { // Do nothing } } diff --git a/contracts/src/v0.8/functions/tests/v1_0_0/testhelpers/FunctionsCoordinatorTestHelper.sol b/contracts/src/v0.8/functions/tests/v1_X/testhelpers/FunctionsCoordinatorTestHelper.sol similarity index 85% rename from contracts/src/v0.8/functions/tests/v1_0_0/testhelpers/FunctionsCoordinatorTestHelper.sol rename to contracts/src/v0.8/functions/tests/v1_X/testhelpers/FunctionsCoordinatorTestHelper.sol index bf55258fbdb..1d883b3b29a 100644 --- a/contracts/src/v0.8/functions/tests/v1_0_0/testhelpers/FunctionsCoordinatorTestHelper.sol +++ b/contracts/src/v0.8/functions/tests/v1_X/testhelpers/FunctionsCoordinatorTestHelper.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.6; -import {FunctionsCoordinator} from "../../../dev/v1_0_0/FunctionsCoordinator.sol"; -import {FunctionsBilling} from "../../../dev/v1_0_0/FunctionsBilling.sol"; +import {FunctionsCoordinator} from "../../../dev/v1_X/FunctionsCoordinator.sol"; +import {FunctionsBilling} from "../../../dev/v1_X/FunctionsBilling.sol"; contract FunctionsCoordinatorTestHelper is FunctionsCoordinator { constructor( diff --git a/contracts/src/v0.8/functions/tests/v1_0_0/testhelpers/FunctionsLoadTestClient.sol b/contracts/src/v0.8/functions/tests/v1_X/testhelpers/FunctionsLoadTestClient.sol similarity index 83% rename from contracts/src/v0.8/functions/tests/v1_0_0/testhelpers/FunctionsLoadTestClient.sol rename to contracts/src/v0.8/functions/tests/v1_X/testhelpers/FunctionsLoadTestClient.sol index c31962ba1f4..1623fc5e124 100644 --- a/contracts/src/v0.8/functions/tests/v1_0_0/testhelpers/FunctionsLoadTestClient.sol +++ b/contracts/src/v0.8/functions/tests/v1_X/testhelpers/FunctionsLoadTestClient.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import {FunctionsClient} from "../../../dev/v1_0_0/FunctionsClient.sol"; +import {FunctionsClient} from "../../../dev/v1_X/FunctionsClient.sol"; import {ConfirmedOwner} from "../../../../shared/access/ConfirmedOwner.sol"; -import {FunctionsRequest} from "../../../dev/v1_0_0/libraries/FunctionsRequest.sol"; +import {FunctionsRequest} from "../../../dev/v1_X/libraries/FunctionsRequest.sol"; /** * @title Chainlink Functions load test client implementation @@ -41,12 +41,12 @@ contract FunctionsLoadTestClient is FunctionsClient, ConfirmedOwner { bytes32 donId ) external onlyOwner { FunctionsRequest.Request memory req; - req.initializeRequestForInlineJavaScript(source); - if (encryptedSecretsReferences.length > 0) req.addSecretsReference(encryptedSecretsReferences); - if (args.length > 0) req.setArgs(args); + req._initializeRequestForInlineJavaScript(source); + if (encryptedSecretsReferences.length > 0) req._addSecretsReference(encryptedSecretsReferences); + if (args.length > 0) req._setArgs(args); uint i = 0; for (i = 0; i < times; i++) { - lastRequestID = _sendRequest(req.encodeCBOR(), subscriptionId, MAX_CALLBACK_GAS, donId); + lastRequestID = _sendRequest(req._encodeCBOR(), subscriptionId, MAX_CALLBACK_GAS, donId); totalRequests += 1; } } @@ -71,12 +71,12 @@ contract FunctionsLoadTestClient is FunctionsClient, ConfirmedOwner { bytes32 donId ) public onlyOwner { FunctionsRequest.Request memory req; - req.initializeRequestForInlineJavaScript(source); - req.addDONHostedSecrets(slotId, slotVersion); - if (args.length > 0) req.setArgs(args); + req._initializeRequestForInlineJavaScript(source); + req._addDONHostedSecrets(slotId, slotVersion); + if (args.length > 0) req._setArgs(args); uint i = 0; for (i = 0; i < times; i++) { - lastRequestID = _sendRequest(req.encodeCBOR(), subscriptionId, MAX_CALLBACK_GAS, donId); + lastRequestID = _sendRequest(req._encodeCBOR(), subscriptionId, MAX_CALLBACK_GAS, donId); totalRequests += 1; } } @@ -135,7 +135,7 @@ contract FunctionsLoadTestClient is FunctionsClient, ConfirmedOwner { * @param err Aggregated error from the user code or from the execution pipeline * Either response or error parameter will be set, but never both */ - function fulfillRequest(bytes32 requestId, bytes memory response, bytes memory err) internal override { + function _fulfillRequest(bytes32 requestId, bytes memory response, bytes memory err) internal override { lastRequestID = requestId; lastResponse = response; lastError = err; diff --git a/contracts/src/v0.8/functions/tests/v1_0_0/testhelpers/FunctionsTestHelper.sol b/contracts/src/v0.8/functions/tests/v1_X/testhelpers/FunctionsTestHelper.sol similarity index 81% rename from contracts/src/v0.8/functions/tests/v1_0_0/testhelpers/FunctionsTestHelper.sol rename to contracts/src/v0.8/functions/tests/v1_X/testhelpers/FunctionsTestHelper.sol index 9ab5386dda7..e8e74e3ed74 100644 --- a/contracts/src/v0.8/functions/tests/v1_0_0/testhelpers/FunctionsTestHelper.sol +++ b/contracts/src/v0.8/functions/tests/v1_X/testhelpers/FunctionsTestHelper.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.6; -import {FunctionsRequest} from "../../../dev/v1_0_0/libraries/FunctionsRequest.sol"; +import {FunctionsRequest} from "../../../dev/v1_X/libraries/FunctionsRequest.sol"; contract FunctionsTestHelper { using FunctionsRequest for FunctionsRequest.Request; @@ -11,25 +11,25 @@ contract FunctionsTestHelper { event RequestData(bytes data); function closeEvent() public { - emit RequestData(s_req.encodeCBOR()); + emit RequestData(s_req._encodeCBOR()); } function initializeRequestForInlineJavaScript(string memory sourceCode) public { FunctionsRequest.Request memory r; - r.initializeRequestForInlineJavaScript(sourceCode); + r._initializeRequestForInlineJavaScript(sourceCode); storeRequest(r); } function addSecretsReference(bytes memory secrets) public { FunctionsRequest.Request memory r = s_req; - r.addSecretsReference(secrets); + r._addSecretsReference(secrets); storeRequest(r); } function addEmptyArgs() public pure { FunctionsRequest.Request memory r; string[] memory args; - r.setArgs(args); + r._setArgs(args); } function addTwoArgs(string memory arg1, string memory arg2) public { @@ -37,7 +37,7 @@ contract FunctionsTestHelper { args[0] = arg1; args[1] = arg2; FunctionsRequest.Request memory r = s_req; - r.setArgs(args); + r._setArgs(args); storeRequest(r); } diff --git a/contracts/src/v0.8/functions/v1_0_0/.gitkeep b/contracts/src/v0.8/functions/v1_0_0/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/contracts/src/v0.8/functions/v1_0_0/FunctionsBilling.sol b/contracts/src/v0.8/functions/v1_0_0/FunctionsBilling.sol new file mode 100644 index 00000000000..1f99903c5a2 --- /dev/null +++ b/contracts/src/v0.8/functions/v1_0_0/FunctionsBilling.sol @@ -0,0 +1,372 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IFunctionsSubscriptions} from "./interfaces/IFunctionsSubscriptions.sol"; +import {AggregatorV3Interface} from "../../interfaces/AggregatorV3Interface.sol"; +import {IFunctionsBilling} from "./interfaces/IFunctionsBilling.sol"; + +import {Routable} from "./Routable.sol"; +import {FunctionsResponse} from "./libraries/FunctionsResponse.sol"; + +import {SafeCast} from "../../vendor/openzeppelin-solidity/v4.8.0/contracts/utils/math/SafeCast.sol"; + +/// @title Functions Billing contract +/// @notice Contract that calculates payment from users to the nodes of the Decentralized Oracle Network (DON). +abstract contract FunctionsBilling is Routable, IFunctionsBilling { + using FunctionsResponse for FunctionsResponse.RequestMeta; + using FunctionsResponse for FunctionsResponse.Commitment; + using FunctionsResponse for FunctionsResponse.FulfillResult; + + uint256 private constant REASONABLE_GAS_PRICE_CEILING = 1_000_000_000_000_000; // 1 million gwei + // ================================================================ + // | Request Commitment state | + // ================================================================ + + mapping(bytes32 requestId => bytes32 commitmentHash) private s_requestCommitments; + + event CommitmentDeleted(bytes32 requestId); + + // ================================================================ + // | Configuration state | + // ================================================================ + + struct Config { + uint32 fulfillmentGasPriceOverEstimationBP; // ══╗ Percentage of gas price overestimation to account for changes in gas price between request and response. Held as basis points (one hundredth of 1 percentage point) + uint32 feedStalenessSeconds; // ║ How long before we consider the feed price to be stale and fallback to fallbackNativePerUnitLink. + uint32 gasOverheadBeforeCallback; // ║ Represents the average gas execution cost before the fulfillment callback. This amount is always billed for every request. + uint32 gasOverheadAfterCallback; // ║ Represents the average gas execution cost after the fulfillment callback. This amount is always billed for every request. + uint32 requestTimeoutSeconds; // ║ How many seconds it takes before we consider a request to be timed out + uint72 donFee; // ║ Additional flat fee (in Juels of LINK) that will be split between Node Operators. Max value is 2^80 - 1 == 1.2m LINK. + uint16 maxSupportedRequestDataVersion; // ═══════╝ The highest support request data version supported by the node. All lower versions should also be supported. + uint224 fallbackNativePerUnitLink; // ═══════════╸ fallback NATIVE CURRENCY / LINK conversion rate if the data feed is stale + } + + Config private s_config; + + event ConfigUpdated(Config config); + + error UnsupportedRequestDataVersion(); + error InsufficientBalance(); + error InvalidSubscription(); + error UnauthorizedSender(); + error MustBeSubOwner(address owner); + error InvalidLinkWeiPrice(int256 linkWei); + error PaymentTooLarge(); + error NoTransmittersSet(); + error InvalidCalldata(); + + // ================================================================ + // | Balance state | + // ================================================================ + + mapping(address transmitter => uint96 balanceJuelsLink) private s_withdrawableTokens; + // Pool together collected DON fees + // Disperse them on withdrawal or change in OCR configuration + uint96 internal s_feePool; + + AggregatorV3Interface private s_linkToNativeFeed; + + // ================================================================ + // | Initialization | + // ================================================================ + constructor(address router, Config memory config, address linkToNativeFeed) Routable(router) { + s_linkToNativeFeed = AggregatorV3Interface(linkToNativeFeed); + + updateConfig(config); + } + + // ================================================================ + // | Configuration | + // ================================================================ + + /// @notice Gets the Chainlink Coordinator's billing configuration + /// @return config + function getConfig() external view returns (Config memory) { + return s_config; + } + + /// @notice Sets the Chainlink Coordinator's billing configuration + /// @param config - See the contents of the Config struct in IFunctionsBilling.Config for more information + function updateConfig(Config memory config) public { + _onlyOwner(); + + s_config = config; + emit ConfigUpdated(config); + } + + // ================================================================ + // | Fee Calculation | + // ================================================================ + + /// @inheritdoc IFunctionsBilling + function getDONFee(bytes memory /* requestData */) public view override returns (uint72) { + return s_config.donFee; + } + + /// @inheritdoc IFunctionsBilling + function getAdminFee() public view override returns (uint72) { + return _getRouter().getAdminFee(); + } + + /// @inheritdoc IFunctionsBilling + function getWeiPerUnitLink() public view returns (uint256) { + Config memory config = s_config; + (, int256 weiPerUnitLink, , uint256 timestamp, ) = s_linkToNativeFeed.latestRoundData(); + // solhint-disable-next-line not-rely-on-time + if (config.feedStalenessSeconds < block.timestamp - timestamp && config.feedStalenessSeconds > 0) { + return config.fallbackNativePerUnitLink; + } + if (weiPerUnitLink <= 0) { + revert InvalidLinkWeiPrice(weiPerUnitLink); + } + return uint256(weiPerUnitLink); + } + + function _getJuelsPerGas(uint256 gasPriceWei) private view returns (uint96) { + // (1e18 juels/link) * (wei/gas) / (wei/link) = juels per gas + // There are only 1e9*1e18 = 1e27 juels in existence, should not exceed uint96 (2^96 ~ 7e28) + return SafeCast.toUint96((1e18 * gasPriceWei) / getWeiPerUnitLink()); + } + + // ================================================================ + // | Cost Estimation | + // ================================================================ + + /// @inheritdoc IFunctionsBilling + function estimateCost( + uint64 subscriptionId, + bytes calldata data, + uint32 callbackGasLimit, + uint256 gasPriceWei + ) external view override returns (uint96) { + _getRouter().isValidCallbackGasLimit(subscriptionId, callbackGasLimit); + // Reasonable ceilings to prevent integer overflows + if (gasPriceWei > REASONABLE_GAS_PRICE_CEILING) { + revert InvalidCalldata(); + } + uint72 adminFee = getAdminFee(); + uint72 donFee = getDONFee(data); + return _calculateCostEstimate(callbackGasLimit, gasPriceWei, donFee, adminFee); + } + + /// @notice Estimate the cost in Juels of LINK + // that will be charged to a subscription to fulfill a Functions request + // Gas Price can be overestimated to account for flucuations between request and response time + function _calculateCostEstimate( + uint32 callbackGasLimit, + uint256 gasPriceWei, + uint72 donFee, + uint72 adminFee + ) internal view returns (uint96) { + uint256 executionGas = s_config.gasOverheadBeforeCallback + s_config.gasOverheadAfterCallback + callbackGasLimit; + + uint256 gasPriceWithOverestimation = gasPriceWei + + ((gasPriceWei * s_config.fulfillmentGasPriceOverEstimationBP) / 10_000); + /// @NOTE: Basis Points are 1/100th of 1%, divide by 10_000 to bring back to original units + + uint96 juelsPerGas = _getJuelsPerGas(gasPriceWithOverestimation); + uint256 estimatedGasReimbursement = juelsPerGas * executionGas; + uint96 fees = uint96(donFee) + uint96(adminFee); + + return SafeCast.toUint96(estimatedGasReimbursement + fees); + } + + // ================================================================ + // | Billing | + // ================================================================ + + /// @notice Initiate the billing process for an Functions request + /// @dev Only callable by the Functions Router + /// @param request - Chainlink Functions request data, see FunctionsResponse.RequestMeta for the structure + /// @return commitment - The parameters of the request that must be held consistent at response time + function _startBilling( + FunctionsResponse.RequestMeta memory request + ) internal returns (FunctionsResponse.Commitment memory commitment) { + Config memory config = s_config; + + // Nodes should support all past versions of the structure + if (request.dataVersion > config.maxSupportedRequestDataVersion) { + revert UnsupportedRequestDataVersion(); + } + + uint72 donFee = getDONFee(request.data); + uint96 estimatedTotalCostJuels = _calculateCostEstimate( + request.callbackGasLimit, + tx.gasprice, + donFee, + request.adminFee + ); + + // Check that subscription can afford the estimated cost + if ((request.availableBalance) < estimatedTotalCostJuels) { + revert InsufficientBalance(); + } + + bytes32 requestId = _computeRequestId( + address(this), + request.requestingContract, + request.subscriptionId, + request.initiatedRequests + 1 + ); + + commitment = FunctionsResponse.Commitment({ + adminFee: request.adminFee, + coordinator: address(this), + client: request.requestingContract, + subscriptionId: request.subscriptionId, + callbackGasLimit: request.callbackGasLimit, + estimatedTotalCostJuels: estimatedTotalCostJuels, + timeoutTimestamp: uint32(block.timestamp + config.requestTimeoutSeconds), + requestId: requestId, + donFee: donFee, + gasOverheadBeforeCallback: config.gasOverheadBeforeCallback, + gasOverheadAfterCallback: config.gasOverheadAfterCallback + }); + + s_requestCommitments[requestId] = keccak256(abi.encode(commitment)); + + return commitment; + } + + /// @notice Generate a keccak hash request ID + /// @dev uses the number of requests that the consumer of a subscription has sent as a nonce + function _computeRequestId( + address don, + address client, + uint64 subscriptionId, + uint64 nonce + ) private pure returns (bytes32) { + return keccak256(abi.encode(don, client, subscriptionId, nonce)); + } + + /// @notice Finalize billing process for an Functions request by sending a callback to the Client contract and then charging the subscription + /// @param requestId identifier for the request that was generated by the Registry in the beginBilling commitment + /// @param response response data from DON consensus + /// @param err error from DON consensus + /// @return result fulfillment result + /// @dev Only callable by a node that has been approved on the Coordinator + /// @dev simulated offchain to determine if sufficient balance is present to fulfill the request + function _fulfillAndBill( + bytes32 requestId, + bytes memory response, + bytes memory err, + bytes memory onchainMetadata, + bytes memory /* offchainMetadata TODO: use in getDonFee() for dynamic billing */ + ) internal returns (FunctionsResponse.FulfillResult) { + FunctionsResponse.Commitment memory commitment = abi.decode(onchainMetadata, (FunctionsResponse.Commitment)); + + if (s_requestCommitments[requestId] == bytes32(0)) { + return FunctionsResponse.FulfillResult.INVALID_REQUEST_ID; + } + + if (s_requestCommitments[requestId] != keccak256(abi.encode(commitment))) { + return FunctionsResponse.FulfillResult.INVALID_COMMITMENT; + } + + uint96 juelsPerGas = _getJuelsPerGas(tx.gasprice); + // Gas overhead without callback + uint96 gasOverheadJuels = juelsPerGas * + (commitment.gasOverheadBeforeCallback + commitment.gasOverheadAfterCallback); + + // The Functions Router will perform the callback to the client contract + (FunctionsResponse.FulfillResult resultCode, uint96 callbackCostJuels) = _getRouter().fulfill( + response, + err, + juelsPerGas, + gasOverheadJuels + commitment.donFee, // costWithoutFulfillment + msg.sender, + commitment + ); + + // The router will only pay the DON on successfully processing the fulfillment + // In these two fulfillment results the user has been charged + // Otherwise, the Coordinator should hold on to the request commitment + if ( + resultCode == FunctionsResponse.FulfillResult.FULFILLED || + resultCode == FunctionsResponse.FulfillResult.USER_CALLBACK_ERROR + ) { + delete s_requestCommitments[requestId]; + // Reimburse the transmitter for the fulfillment gas cost + s_withdrawableTokens[msg.sender] = gasOverheadJuels + callbackCostJuels; + // Put donFee into the pool of fees, to be split later + // Saves on storage writes that would otherwise be charged to the user + s_feePool += commitment.donFee; + } + + return resultCode; + } + + // ================================================================ + // | Request Timeout | + // ================================================================ + + /// @inheritdoc IFunctionsBilling + /// @dev Only callable by the Router + /// @dev Used by FunctionsRouter.sol during timeout of a request + function deleteCommitment(bytes32 requestId) external override onlyRouter { + // Delete commitment + delete s_requestCommitments[requestId]; + emit CommitmentDeleted(requestId); + } + + // ================================================================ + // | Fund withdrawal | + // ================================================================ + + /// @inheritdoc IFunctionsBilling + function oracleWithdraw(address recipient, uint96 amount) external { + _disperseFeePool(); + + if (amount == 0) { + amount = s_withdrawableTokens[msg.sender]; + } else if (s_withdrawableTokens[msg.sender] < amount) { + revert InsufficientBalance(); + } + s_withdrawableTokens[msg.sender] -= amount; + IFunctionsSubscriptions(address(_getRouter())).oracleWithdraw(recipient, amount); + } + + /// @inheritdoc IFunctionsBilling + /// @dev Only callable by the Coordinator owner + function oracleWithdrawAll() external { + _onlyOwner(); + _disperseFeePool(); + + address[] memory transmitters = _getTransmitters(); + + // Bounded by "maxNumOracles" on OCR2Abstract.sol + for (uint256 i = 0; i < transmitters.length; ++i) { + uint96 balance = s_withdrawableTokens[transmitters[i]]; + if (balance > 0) { + s_withdrawableTokens[transmitters[i]] = 0; + IFunctionsSubscriptions(address(_getRouter())).oracleWithdraw(transmitters[i], balance); + } + } + } + + // Overriden in FunctionsCoordinator, which has visibility into transmitters + function _getTransmitters() internal view virtual returns (address[] memory); + + // DON fees are collected into a pool s_feePool + // When OCR configuration changes, or any oracle withdraws, this must be dispersed + function _disperseFeePool() internal { + if (s_feePool == 0) { + return; + } + // All transmitters are assumed to also be observers + // Pay out the DON fee to all transmitters + address[] memory transmitters = _getTransmitters(); + if (transmitters.length == 0) { + revert NoTransmittersSet(); + } + uint96 feePoolShare = s_feePool / uint96(transmitters.length); + // Bounded by "maxNumOracles" on OCR2Abstract.sol + for (uint256 i = 0; i < transmitters.length; ++i) { + s_withdrawableTokens[transmitters[i]] += feePoolShare; + } + s_feePool -= feePoolShare * uint96(transmitters.length); + } + + // Overriden in FunctionsCoordinator.sol + function _onlyOwner() internal view virtual; +} diff --git a/contracts/src/v0.8/functions/dev/v1_0_0/FunctionsClient.sol b/contracts/src/v0.8/functions/v1_0_0/FunctionsClient.sol similarity index 100% rename from contracts/src/v0.8/functions/dev/v1_0_0/FunctionsClient.sol rename to contracts/src/v0.8/functions/v1_0_0/FunctionsClient.sol diff --git a/contracts/src/v0.8/functions/v1_0_0/FunctionsCoordinator.sol b/contracts/src/v0.8/functions/v1_0_0/FunctionsCoordinator.sol new file mode 100644 index 00000000000..1488bc45888 --- /dev/null +++ b/contracts/src/v0.8/functions/v1_0_0/FunctionsCoordinator.sol @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IFunctionsCoordinator} from "./interfaces/IFunctionsCoordinator.sol"; +import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol"; + +import {FunctionsBilling} from "./FunctionsBilling.sol"; +import {OCR2Base} from "./ocr/OCR2Base.sol"; +import {FunctionsResponse} from "./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 + string public constant override typeAndVersion = "Functions Coordinator v1.0.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, + Config memory config, + address linkToNativeFeed + ) OCR2Base(true) FunctionsBilling(router, config, linkToNativeFeed) {} + + /// @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) { + address[] memory nodes = s_transmitters; + // Bounded by "maxNumOracles" on OCR2Abstract.sol + for (uint256 i = 0; i < nodes.length; ++i) { + if (nodes[i] == node) { + return true; + } + } + return false; + } + + /// @inheritdoc IFunctionsCoordinator + function startRequest( + FunctionsResponse.RequestMeta calldata request + ) external override onlyRouter returns (FunctionsResponse.Commitment memory commitment) { + commitment = _startBilling(request); + + emit OracleRequest( + commitment.requestId, + request.requestingContract, + tx.origin, + request.subscriptionId, + request.subscriptionOwner, + request.data, + request.dataVersion, + request.flags, + request.callbackGasLimit, + commitment + ); + + 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; + } + + /// @dev Report hook called within OCR2Base.sol + function _report( + uint256 /*initialGas*/, + address /*transmitter*/, + uint8 /*signerCount*/, + address[MAX_NUM_ORACLES] memory /*signers*/, + bytes calldata report + ) internal override { + bytes32[] memory requestIds; + bytes[] memory results; + bytes[] memory errors; + bytes[] memory onchainMetadata; + bytes[] memory offchainMetadata; + (requestIds, results, errors, onchainMetadata, offchainMetadata) = abi.decode( + report, + (bytes32[], bytes[], bytes[], bytes[], bytes[]) + ); + + if ( + requestIds.length == 0 || + requestIds.length != results.length || + requestIds.length != errors.length || + requestIds.length != onchainMetadata.length || + requestIds.length != offchainMetadata.length + ) { + revert ReportInvalid(); + } + + // Bounded by "MaxRequestBatchSize" on the Job's ReportingPluginConfig + for (uint256 i = 0; i < requestIds.length; ++i) { + FunctionsResponse.FulfillResult result = FunctionsResponse.FulfillResult( + _fulfillAndBill(requestIds[i], results[i], errors[i], onchainMetadata[i], offchainMetadata[i]) + ); + + // 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(requestIds[i], msg.sender); + } + } + } + + /// @dev Used in FunctionsBilling.sol + function _onlyOwner() internal view override { + _validateOwnership(); + } +} diff --git a/contracts/src/v0.8/functions/v1_0_0/FunctionsRouter.sol b/contracts/src/v0.8/functions/v1_0_0/FunctionsRouter.sol new file mode 100644 index 00000000000..dad50b042b9 --- /dev/null +++ b/contracts/src/v0.8/functions/v1_0_0/FunctionsRouter.sol @@ -0,0 +1,585 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol"; +import {IFunctionsRouter} from "./interfaces/IFunctionsRouter.sol"; +import {IFunctionsCoordinator} from "./interfaces/IFunctionsCoordinator.sol"; +import {IAccessController} from "../../shared/interfaces/IAccessController.sol"; + +import {FunctionsSubscriptions} from "./FunctionsSubscriptions.sol"; +import {FunctionsResponse} from "./libraries/FunctionsResponse.sol"; +import {ConfirmedOwner} from "../../shared/access/ConfirmedOwner.sol"; + +import {SafeCast} from "../../vendor/openzeppelin-solidity/v4.8.0/contracts/utils/math/SafeCast.sol"; +import {Pausable} from "../../vendor/openzeppelin-solidity/v4.8.0/contracts/security/Pausable.sol"; + +contract FunctionsRouter is IFunctionsRouter, FunctionsSubscriptions, Pausable, ITypeAndVersion, ConfirmedOwner { + using FunctionsResponse for FunctionsResponse.RequestMeta; + using FunctionsResponse for FunctionsResponse.Commitment; + using FunctionsResponse for FunctionsResponse.FulfillResult; + + string public constant override typeAndVersion = "Functions Router v1.0.0"; + + // We limit return data to a selector plus 4 words. This is to avoid + // malicious contracts from returning large amounts of data and causing + // repeated out-of-gas scenarios. + uint16 public constant MAX_CALLBACK_RETURN_BYTES = 4 + 4 * 32; + uint8 private constant MAX_CALLBACK_GAS_LIMIT_FLAGS_INDEX = 0; + + event RequestStart( + bytes32 indexed requestId, + bytes32 indexed donId, + uint64 indexed subscriptionId, + address subscriptionOwner, + address requestingContract, + address requestInitiator, + bytes data, + uint16 dataVersion, + uint32 callbackGasLimit, + uint96 estimatedTotalCostJuels + ); + + event RequestProcessed( + bytes32 indexed requestId, + uint64 indexed subscriptionId, + uint96 totalCostJuels, + address transmitter, + FunctionsResponse.FulfillResult resultCode, + bytes response, + bytes err, + bytes callbackReturnData + ); + + event RequestNotProcessed( + bytes32 indexed requestId, + address coordinator, + address transmitter, + FunctionsResponse.FulfillResult resultCode + ); + + error EmptyRequestData(); + error OnlyCallableFromCoordinator(); + error SenderMustAcceptTermsOfService(address sender); + error InvalidGasFlagValue(uint8 value); + error GasLimitTooBig(uint32 limit); + error DuplicateRequestId(bytes32 requestId); + + struct CallbackResult { + bool success; // ══════╸ Whether the callback succeeded or not + uint256 gasUsed; // ═══╸ The amount of gas consumed during the callback + bytes returnData; // ══╸ The return of the callback function + } + + // ================================================================ + // | Route state | + // ================================================================ + + mapping(bytes32 id => address routableContract) private s_route; + + error RouteNotFound(bytes32 id); + + // Identifier for the route to the Terms of Service Allow List + bytes32 private s_allowListId; + + // ================================================================ + // | Configuration state | + // ================================================================ + struct Config { + uint16 maxConsumersPerSubscription; // ═════════╗ Maximum number of consumers which can be added to a single subscription. This bound ensures we are able to loop over all subscription consumers as needed, without exceeding gas limits. Should a user require more consumers, they can use multiple subscriptions. + uint72 adminFee; // ║ Flat fee (in Juels of LINK) that will be paid to the Router owner for operation of the network + bytes4 handleOracleFulfillmentSelector; // ║ The function selector that is used when calling back to the Client contract + uint16 gasForCallExactCheck; // ════════════════╝ Used during calling back to the client. Ensures we have at least enough gas to be able to revert if gasAmount > 63//64*gas available. + uint32[] maxCallbackGasLimits; // ══════════════╸ List of max callback gas limits used by flag with GAS_FLAG_INDEX + uint16 subscriptionDepositMinimumRequests; //═══╗ Amount of requests that must be completed before the full subscription balance will be released when closing a subscription account. + uint72 subscriptionDepositJuels; // ════════════╝ Amount of subscription funds that are held as a deposit until Config.subscriptionDepositMinimumRequests are made using the subscription. + } + + Config private s_config; + + event ConfigUpdated(Config); + + // ================================================================ + // | Proposal state | + // ================================================================ + + uint8 private constant MAX_PROPOSAL_SET_LENGTH = 8; + + struct ContractProposalSet { + bytes32[] ids; // ══╸ The IDs that key into the routes that will be modified if the update is applied + address[] to; // ═══╸ The address of the contracts that the route will point to if the updated is applied + } + ContractProposalSet private s_proposedContractSet; + + event ContractProposed( + bytes32 proposedContractSetId, + address proposedContractSetFromAddress, + address proposedContractSetToAddress + ); + + event ContractUpdated(bytes32 id, address from, address to); + + error InvalidProposal(); + error IdentifierIsReserved(bytes32 id); + + // ================================================================ + // | Initialization | + // ================================================================ + + constructor( + address linkToken, + Config memory config + ) FunctionsSubscriptions(linkToken) ConfirmedOwner(msg.sender) Pausable() { + // Set the intial configuration + updateConfig(config); + } + + // ================================================================ + // | Configuration | + // ================================================================ + + /// @notice The identifier of the route to retrieve the address of the access control contract + // The access control contract controls which accounts can manage subscriptions + /// @return id - bytes32 id that can be passed to the "getContractById" of the Router + function getConfig() external view returns (Config memory) { + return s_config; + } + + /// @notice The router configuration + function updateConfig(Config memory config) public onlyOwner { + s_config = config; + emit ConfigUpdated(config); + } + + /// @inheritdoc IFunctionsRouter + function isValidCallbackGasLimit(uint64 subscriptionId, uint32 callbackGasLimit) public view { + uint8 callbackGasLimitsIndexSelector = uint8(getFlags(subscriptionId)[MAX_CALLBACK_GAS_LIMIT_FLAGS_INDEX]); + if (callbackGasLimitsIndexSelector >= s_config.maxCallbackGasLimits.length) { + revert InvalidGasFlagValue(callbackGasLimitsIndexSelector); + } + uint32 maxCallbackGasLimit = s_config.maxCallbackGasLimits[callbackGasLimitsIndexSelector]; + if (callbackGasLimit > maxCallbackGasLimit) { + revert GasLimitTooBig(maxCallbackGasLimit); + } + } + + /// @inheritdoc IFunctionsRouter + function getAdminFee() external view override returns (uint72) { + return s_config.adminFee; + } + + /// @inheritdoc IFunctionsRouter + function getAllowListId() external view override returns (bytes32) { + return s_allowListId; + } + + /// @inheritdoc IFunctionsRouter + function setAllowListId(bytes32 allowListId) external override onlyOwner { + s_allowListId = allowListId; + } + + /// @dev Used within FunctionsSubscriptions.sol + function _getMaxConsumers() internal view override returns (uint16) { + return s_config.maxConsumersPerSubscription; + } + + /// @dev Used within FunctionsSubscriptions.sol + function _getSubscriptionDepositDetails() internal view override returns (uint16, uint72) { + return (s_config.subscriptionDepositMinimumRequests, s_config.subscriptionDepositJuels); + } + + // ================================================================ + // | Requests | + // ================================================================ + + /// @inheritdoc IFunctionsRouter + function sendRequest( + uint64 subscriptionId, + bytes calldata data, + uint16 dataVersion, + uint32 callbackGasLimit, + bytes32 donId + ) external override returns (bytes32) { + IFunctionsCoordinator coordinator = IFunctionsCoordinator(getContractById(donId)); + return _sendRequest(donId, coordinator, subscriptionId, data, dataVersion, callbackGasLimit); + } + + /// @inheritdoc IFunctionsRouter + function sendRequestToProposed( + uint64 subscriptionId, + bytes calldata data, + uint16 dataVersion, + uint32 callbackGasLimit, + bytes32 donId + ) external override returns (bytes32) { + IFunctionsCoordinator coordinator = IFunctionsCoordinator(getProposedContractById(donId)); + return _sendRequest(donId, coordinator, subscriptionId, data, dataVersion, callbackGasLimit); + } + + function _sendRequest( + bytes32 donId, + IFunctionsCoordinator coordinator, + uint64 subscriptionId, + bytes memory data, + uint16 dataVersion, + uint32 callbackGasLimit + ) private returns (bytes32) { + _whenNotPaused(); + _isExistingSubscription(subscriptionId); + _isAllowedConsumer(msg.sender, subscriptionId); + isValidCallbackGasLimit(subscriptionId, callbackGasLimit); + + if (data.length == 0) { + revert EmptyRequestData(); + } + + Subscription memory subscription = getSubscription(subscriptionId); + Consumer memory consumer = getConsumer(msg.sender, subscriptionId); + uint72 adminFee = s_config.adminFee; + + // Forward request to DON + FunctionsResponse.Commitment memory commitment = coordinator.startRequest( + FunctionsResponse.RequestMeta({ + requestingContract: msg.sender, + data: data, + subscriptionId: subscriptionId, + dataVersion: dataVersion, + flags: getFlags(subscriptionId), + callbackGasLimit: callbackGasLimit, + adminFee: adminFee, + initiatedRequests: consumer.initiatedRequests, + completedRequests: consumer.completedRequests, + availableBalance: subscription.balance - subscription.blockedBalance, + subscriptionOwner: subscription.owner + }) + ); + + // Do not allow setting a comittment for a requestId that already exists + if (s_requestCommitments[commitment.requestId] != bytes32(0)) { + revert DuplicateRequestId(commitment.requestId); + } + + // Store a commitment about the request + s_requestCommitments[commitment.requestId] = keccak256( + abi.encode( + FunctionsResponse.Commitment({ + adminFee: adminFee, + coordinator: address(coordinator), + client: msg.sender, + subscriptionId: subscriptionId, + callbackGasLimit: callbackGasLimit, + estimatedTotalCostJuels: commitment.estimatedTotalCostJuels, + timeoutTimestamp: commitment.timeoutTimestamp, + requestId: commitment.requestId, + donFee: commitment.donFee, + gasOverheadBeforeCallback: commitment.gasOverheadBeforeCallback, + gasOverheadAfterCallback: commitment.gasOverheadAfterCallback + }) + ) + ); + + _markRequestInFlight(msg.sender, subscriptionId, commitment.estimatedTotalCostJuels); + + emit RequestStart({ + requestId: commitment.requestId, + donId: donId, + subscriptionId: subscriptionId, + subscriptionOwner: subscription.owner, + requestingContract: msg.sender, + requestInitiator: tx.origin, + data: data, + dataVersion: dataVersion, + callbackGasLimit: callbackGasLimit, + estimatedTotalCostJuels: commitment.estimatedTotalCostJuels + }); + + return commitment.requestId; + } + + // ================================================================ + // | Responses | + // ================================================================ + + /// @inheritdoc IFunctionsRouter + function fulfill( + bytes memory response, + bytes memory err, + uint96 juelsPerGas, + uint96 costWithoutCallback, + address transmitter, + FunctionsResponse.Commitment memory commitment + ) external override returns (FunctionsResponse.FulfillResult resultCode, uint96) { + _whenNotPaused(); + + if (msg.sender != commitment.coordinator) { + revert OnlyCallableFromCoordinator(); + } + + { + bytes32 commitmentHash = s_requestCommitments[commitment.requestId]; + + if (commitmentHash == bytes32(0)) { + resultCode = FunctionsResponse.FulfillResult.INVALID_REQUEST_ID; + emit RequestNotProcessed(commitment.requestId, commitment.coordinator, transmitter, resultCode); + return (resultCode, 0); + } + + if (keccak256(abi.encode(commitment)) != commitmentHash) { + resultCode = FunctionsResponse.FulfillResult.INVALID_COMMITMENT; + emit RequestNotProcessed(commitment.requestId, commitment.coordinator, transmitter, resultCode); + return (resultCode, 0); + } + + // Check that the transmitter has supplied enough gas for the callback to succeed + if (gasleft() < commitment.callbackGasLimit + commitment.gasOverheadAfterCallback) { + resultCode = FunctionsResponse.FulfillResult.INSUFFICIENT_GAS_PROVIDED; + emit RequestNotProcessed(commitment.requestId, commitment.coordinator, transmitter, resultCode); + return (resultCode, 0); + } + } + + { + uint96 callbackCost = juelsPerGas * SafeCast.toUint96(commitment.callbackGasLimit); + uint96 totalCostJuels = commitment.adminFee + costWithoutCallback + callbackCost; + + // Check that the subscription can still afford to fulfill the request + if (totalCostJuels > getSubscription(commitment.subscriptionId).balance) { + resultCode = FunctionsResponse.FulfillResult.SUBSCRIPTION_BALANCE_INVARIANT_VIOLATION; + emit RequestNotProcessed(commitment.requestId, commitment.coordinator, transmitter, resultCode); + return (resultCode, 0); + } + + // Check that the cost has not exceeded the quoted cost + if (totalCostJuels > commitment.estimatedTotalCostJuels) { + resultCode = FunctionsResponse.FulfillResult.COST_EXCEEDS_COMMITMENT; + emit RequestNotProcessed(commitment.requestId, commitment.coordinator, transmitter, resultCode); + return (resultCode, 0); + } + } + + delete s_requestCommitments[commitment.requestId]; + + CallbackResult memory result = _callback( + commitment.requestId, + response, + err, + commitment.callbackGasLimit, + commitment.client + ); + + resultCode = result.success + ? FunctionsResponse.FulfillResult.FULFILLED + : FunctionsResponse.FulfillResult.USER_CALLBACK_ERROR; + + Receipt memory receipt = _pay( + commitment.subscriptionId, + commitment.estimatedTotalCostJuels, + commitment.client, + commitment.adminFee, + juelsPerGas, + SafeCast.toUint96(result.gasUsed), + costWithoutCallback + ); + + emit RequestProcessed({ + requestId: commitment.requestId, + subscriptionId: commitment.subscriptionId, + totalCostJuels: receipt.totalCostJuels, + transmitter: transmitter, + resultCode: resultCode, + response: response, + err: err, + callbackReturnData: result.returnData + }); + + return (resultCode, receipt.callbackGasCostJuels); + } + + function _callback( + bytes32 requestId, + bytes memory response, + bytes memory err, + uint32 callbackGasLimit, + address client + ) private returns (CallbackResult memory) { + bool destinationNoLongerExists; + assembly { + // solidity calls check that a contract actually exists at the destination, so we do the same + destinationNoLongerExists := iszero(extcodesize(client)) + } + if (destinationNoLongerExists) { + // Return without attempting callback + // The subscription will still be charged to reimburse transmitter's gas overhead + return CallbackResult({success: false, gasUsed: 0, returnData: new bytes(0)}); + } + + bytes memory encodedCallback = abi.encodeWithSelector( + s_config.handleOracleFulfillmentSelector, + requestId, + response, + err + ); + + uint16 gasForCallExactCheck = s_config.gasForCallExactCheck; + + // Call with explicitly the amount of callback gas requested + // Important to not let them exhaust the gas budget and avoid payment. + // NOTE: that callWithExactGas will revert if we do not have sufficient gas + // to give the callee their requested amount. + + bool success; + uint256 gasUsed; + // allocate return data memory ahead of time + bytes memory returnData = new bytes(MAX_CALLBACK_RETURN_BYTES); + + assembly { + let g := gas() + // Compute g -= gasForCallExactCheck and check for underflow + // The gas actually passed to the callee is _min(gasAmount, 63//64*gas available). + // We want to ensure that we revert if gasAmount > 63//64*gas available + // as we do not want to provide them with less, however that check itself costs + // gas. gasForCallExactCheck ensures we have at least enough gas to be able + // to revert if gasAmount > 63//64*gas available. + if lt(g, gasForCallExactCheck) { + revert(0, 0) + } + g := sub(g, gasForCallExactCheck) + // if g - g//64 <= gasAmount, revert + // (we subtract g//64 because of EIP-150) + if iszero(gt(sub(g, div(g, 64)), callbackGasLimit)) { + revert(0, 0) + } + // call and report whether we succeeded + // call(gas,addr,value,argsOffset,argsLength,retOffset,retLength) + let gasBeforeCall := gas() + success := call(callbackGasLimit, client, 0, add(encodedCallback, 0x20), mload(encodedCallback), 0, 0) + gasUsed := sub(gasBeforeCall, gas()) + + // limit our copy to MAX_CALLBACK_RETURN_BYTES bytes + let toCopy := returndatasize() + if gt(toCopy, MAX_CALLBACK_RETURN_BYTES) { + toCopy := MAX_CALLBACK_RETURN_BYTES + } + // Store the length of the copied bytes + mstore(returnData, toCopy) + // copy the bytes from returnData[0:_toCopy] + returndatacopy(add(returnData, 0x20), 0, toCopy) + } + + return CallbackResult({success: success, gasUsed: gasUsed, returnData: returnData}); + } + + // ================================================================ + // | Route methods | + // ================================================================ + + /// @inheritdoc IFunctionsRouter + function getContractById(bytes32 id) public view override returns (address) { + address currentImplementation = s_route[id]; + if (currentImplementation == address(0)) { + revert RouteNotFound(id); + } + return currentImplementation; + } + + /// @inheritdoc IFunctionsRouter + function getProposedContractById(bytes32 id) public view override returns (address) { + // Iterations will not exceed MAX_PROPOSAL_SET_LENGTH + for (uint8 i = 0; i < s_proposedContractSet.ids.length; ++i) { + if (id == s_proposedContractSet.ids[i]) { + return s_proposedContractSet.to[i]; + } + } + revert RouteNotFound(id); + } + + // ================================================================ + // | Contract Proposal methods | + // ================================================================ + + /// @inheritdoc IFunctionsRouter + function getProposedContractSet() external view override returns (bytes32[] memory, address[] memory) { + return (s_proposedContractSet.ids, s_proposedContractSet.to); + } + + /// @inheritdoc IFunctionsRouter + function proposeContractsUpdate( + bytes32[] memory proposedContractSetIds, + address[] memory proposedContractSetAddresses + ) external override onlyOwner { + // IDs and addresses arrays must be of equal length and must not exceed the max proposal length + uint256 idsArrayLength = proposedContractSetIds.length; + if (idsArrayLength != proposedContractSetAddresses.length || idsArrayLength > MAX_PROPOSAL_SET_LENGTH) { + revert InvalidProposal(); + } + + // NOTE: iterations of this loop will not exceed MAX_PROPOSAL_SET_LENGTH + for (uint256 i = 0; i < idsArrayLength; ++i) { + bytes32 id = proposedContractSetIds[i]; + address proposedContract = proposedContractSetAddresses[i]; + if ( + proposedContract == address(0) || // The Proposed address must be a valid address + s_route[id] == proposedContract // The Proposed address must point to a different address than what is currently set + ) { + revert InvalidProposal(); + } + + emit ContractProposed({ + proposedContractSetId: id, + proposedContractSetFromAddress: s_route[id], + proposedContractSetToAddress: proposedContract + }); + } + + s_proposedContractSet = ContractProposalSet({ids: proposedContractSetIds, to: proposedContractSetAddresses}); + } + + /// @inheritdoc IFunctionsRouter + function updateContracts() external override onlyOwner { + // Iterations will not exceed MAX_PROPOSAL_SET_LENGTH + for (uint256 i = 0; i < s_proposedContractSet.ids.length; ++i) { + bytes32 id = s_proposedContractSet.ids[i]; + address to = s_proposedContractSet.to[i]; + emit ContractUpdated({id: id, from: s_route[id], to: to}); + s_route[id] = to; + } + + delete s_proposedContractSet; + } + + // ================================================================ + // | Modifiers | + // ================================================================ + // Favoring internal functions over actual modifiers to reduce contract size + + /// @dev Used within FunctionsSubscriptions.sol + function _whenNotPaused() internal view override { + _requireNotPaused(); + } + + /// @dev Used within FunctionsSubscriptions.sol + function _onlyRouterOwner() internal view override { + _validateOwnership(); + } + + /// @dev Used within FunctionsSubscriptions.sol + function _onlySenderThatAcceptedToS() internal view override { + address currentImplementation = s_route[s_allowListId]; + if (currentImplementation == address(0)) { + // If not set, ignore this check, allow all access + return; + } + if (!IAccessController(currentImplementation).hasAccess(msg.sender, new bytes(0))) { + revert SenderMustAcceptTermsOfService(msg.sender); + } + } + + /// @inheritdoc IFunctionsRouter + function pause() external override onlyOwner { + _pause(); + } + + /// @inheritdoc IFunctionsRouter + function unpause() external override onlyOwner { + _unpause(); + } +} diff --git a/contracts/src/v0.8/functions/v1_0_0/FunctionsSubscriptions.sol b/contracts/src/v0.8/functions/v1_0_0/FunctionsSubscriptions.sol new file mode 100644 index 00000000000..7fa2368c46d --- /dev/null +++ b/contracts/src/v0.8/functions/v1_0_0/FunctionsSubscriptions.sol @@ -0,0 +1,552 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IFunctionsSubscriptions} from "./interfaces/IFunctionsSubscriptions.sol"; +import {IERC677Receiver} from "../../shared/interfaces/IERC677Receiver.sol"; +import {IFunctionsBilling} from "./interfaces/IFunctionsBilling.sol"; + +import {FunctionsResponse} from "./libraries/FunctionsResponse.sol"; + +import {IERC20} from "../../vendor/openzeppelin-solidity/v4.8.0/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "../../vendor/openzeppelin-solidity/v4.8.0/contracts/token/ERC20/utils/SafeERC20.sol"; + +/// @title Functions Subscriptions contract +/// @notice Contract that coordinates payment from users to the nodes of the Decentralized Oracle Network (DON). +abstract contract FunctionsSubscriptions is IFunctionsSubscriptions, IERC677Receiver { + using SafeERC20 for IERC20; + using FunctionsResponse for FunctionsResponse.Commitment; + + // ================================================================ + // | Balance state | + // ================================================================ + // link token address + IERC20 internal immutable i_linkToken; + + // s_totalLinkBalance tracks the total LINK sent to/from + // this contract through onTokenTransfer, cancelSubscription and oracleWithdraw. + // A discrepancy with this contract's LINK balance indicates that someone + // sent tokens using transfer and so we may need to use recoverFunds. + uint96 private s_totalLinkBalance; + + /// @dev NOP balances are held as a single amount. The breakdown is held by the Coordinator. + mapping(address coordinator => uint96 balanceJuelsLink) private s_withdrawableTokens; + + // ================================================================ + // | Subscription state | + // ================================================================ + // Keep a count of the number of subscriptions so that its possible to + // loop through all the current subscriptions via .getSubscription(). + uint64 private s_currentSubscriptionId; + + mapping(uint64 subscriptionId => Subscription) private s_subscriptions; + + // Maintains the list of keys in s_consumers. + // We do this for 2 reasons: + // 1. To be able to clean up all keys from s_consumers when canceling a subscription. + // 2. To be able to return the list of all consumers in getSubscription. + // Note that we need the s_consumers map to be able to directly check if a + // consumer is valid without reading all the consumers from storage. + mapping(address consumer => mapping(uint64 subscriptionId => Consumer)) private s_consumers; + + event SubscriptionCreated(uint64 indexed subscriptionId, address owner); + event SubscriptionFunded(uint64 indexed subscriptionId, uint256 oldBalance, uint256 newBalance); + event SubscriptionConsumerAdded(uint64 indexed subscriptionId, address consumer); + event SubscriptionConsumerRemoved(uint64 indexed subscriptionId, address consumer); + event SubscriptionCanceled(uint64 indexed subscriptionId, address fundsRecipient, uint256 fundsAmount); + event SubscriptionOwnerTransferRequested(uint64 indexed subscriptionId, address from, address to); + event SubscriptionOwnerTransferred(uint64 indexed subscriptionId, address from, address to); + + error TooManyConsumers(uint16 maximumConsumers); + error InsufficientBalance(uint96 currentBalanceJuels); + error InvalidConsumer(); + error CannotRemoveWithPendingRequests(); + error InvalidSubscription(); + error OnlyCallableFromLink(); + error InvalidCalldata(); + error MustBeSubscriptionOwner(); + error TimeoutNotExceeded(); + error MustBeProposedOwner(address proposedOwner); + event FundsRecovered(address to, uint256 amount); + + // ================================================================ + // | Request state | + // ================================================================ + + mapping(bytes32 requestId => bytes32 commitmentHash) internal s_requestCommitments; + + struct Receipt { + uint96 callbackGasCostJuels; + uint96 totalCostJuels; + } + + event RequestTimedOut(bytes32 indexed requestId); + + // ================================================================ + // | Initialization | + // ================================================================ + constructor(address link) { + i_linkToken = IERC20(link); + } + + // ================================================================ + // | Request/Response | + // ================================================================ + + /// @notice Sets a request as in-flight + /// @dev Only callable within the Router + function _markRequestInFlight(address client, uint64 subscriptionId, uint96 estimatedTotalCostJuels) internal { + // Earmark subscription funds + s_subscriptions[subscriptionId].blockedBalance += estimatedTotalCostJuels; + + // Increment sent requests + s_consumers[client][subscriptionId].initiatedRequests += 1; + } + + /// @notice Moves funds from one subscription account to another. + /// @dev Only callable by the Coordinator contract that is saved in the request commitment + function _pay( + uint64 subscriptionId, + uint96 estimatedTotalCostJuels, + address client, + uint96 adminFee, + uint96 juelsPerGas, + uint96 gasUsed, + uint96 costWithoutCallbackJuels + ) internal returns (Receipt memory) { + uint96 callbackGasCostJuels = juelsPerGas * gasUsed; + uint96 totalCostJuels = costWithoutCallbackJuels + adminFee + callbackGasCostJuels; + + if ( + s_subscriptions[subscriptionId].balance < totalCostJuels || + s_subscriptions[subscriptionId].blockedBalance < estimatedTotalCostJuels + ) { + revert InsufficientBalance(s_subscriptions[subscriptionId].balance); + } + + // Charge the subscription + s_subscriptions[subscriptionId].balance -= totalCostJuels; + + // Unblock earmarked funds + s_subscriptions[subscriptionId].blockedBalance -= estimatedTotalCostJuels; + + // Pay the DON's fees and gas reimbursement + s_withdrawableTokens[msg.sender] += costWithoutCallbackJuels + callbackGasCostJuels; + + // Pay out the administration fee + s_withdrawableTokens[address(this)] += adminFee; + + // Increment finished requests + s_consumers[client][subscriptionId].completedRequests += 1; + + return Receipt({callbackGasCostJuels: callbackGasCostJuels, totalCostJuels: totalCostJuels}); + } + + // ================================================================ + // | Owner methods | + // ================================================================ + + /// @inheritdoc IFunctionsSubscriptions + function ownerCancelSubscription(uint64 subscriptionId) external override { + _onlyRouterOwner(); + _isExistingSubscription(subscriptionId); + _cancelSubscriptionHelper(subscriptionId, s_subscriptions[subscriptionId].owner, false); + } + + /// @inheritdoc IFunctionsSubscriptions + function recoverFunds(address to) external override { + _onlyRouterOwner(); + uint256 externalBalance = i_linkToken.balanceOf(address(this)); + uint256 internalBalance = uint256(s_totalLinkBalance); + if (internalBalance < externalBalance) { + uint256 amount = externalBalance - internalBalance; + i_linkToken.safeTransfer(to, amount); + emit FundsRecovered(to, amount); + } + // If the balances are equal, nothing to be done. + } + + // ================================================================ + // | Fund withdrawal | + // ================================================================ + + /// @inheritdoc IFunctionsSubscriptions + function oracleWithdraw(address recipient, uint96 amount) external override { + _whenNotPaused(); + + if (amount == 0) { + revert InvalidCalldata(); + } + uint96 currentBalance = s_withdrawableTokens[msg.sender]; + if (currentBalance < amount) { + revert InsufficientBalance(currentBalance); + } + s_withdrawableTokens[msg.sender] -= amount; + s_totalLinkBalance -= amount; + i_linkToken.safeTransfer(recipient, amount); + } + + /// @notice Owner withdraw LINK earned through admin fees + /// @notice If amount is 0 the full balance will be withdrawn + /// @param recipient where to send the funds + /// @param amount amount to withdraw + function ownerWithdraw(address recipient, uint96 amount) external { + _onlyRouterOwner(); + if (amount == 0) { + amount = s_withdrawableTokens[address(this)]; + } + uint96 currentBalance = s_withdrawableTokens[address(this)]; + if (currentBalance < amount) { + revert InsufficientBalance(currentBalance); + } + s_withdrawableTokens[address(this)] -= amount; + s_totalLinkBalance -= amount; + + i_linkToken.safeTransfer(recipient, amount); + } + + // ================================================================ + // | TransferAndCall Deposit helper | + // ================================================================ + + // This function is to be invoked when using LINK.transferAndCall + /// @dev Note to fund the subscription, use transferAndCall. For example + /// @dev LINKTOKEN.transferAndCall( + /// @dev address(ROUTER), + /// @dev amount, + /// @dev abi.encode(subscriptionId)); + function onTokenTransfer(address /* sender */, uint256 amount, bytes calldata data) external override { + _whenNotPaused(); + if (msg.sender != address(i_linkToken)) { + revert OnlyCallableFromLink(); + } + if (data.length != 32) { + revert InvalidCalldata(); + } + uint64 subscriptionId = abi.decode(data, (uint64)); + if (s_subscriptions[subscriptionId].owner == address(0)) { + revert InvalidSubscription(); + } + // We do not check that the msg.sender is the subscription owner, + // anyone can fund a subscription. + uint256 oldBalance = s_subscriptions[subscriptionId].balance; + s_subscriptions[subscriptionId].balance += uint96(amount); + s_totalLinkBalance += uint96(amount); + emit SubscriptionFunded(subscriptionId, oldBalance, oldBalance + amount); + } + + // ================================================================ + // | Subscription management | + // ================================================================ + + /// @inheritdoc IFunctionsSubscriptions + function getTotalBalance() external view override returns (uint96) { + return s_totalLinkBalance; + } + + /// @inheritdoc IFunctionsSubscriptions + function getSubscriptionCount() external view override returns (uint64) { + return s_currentSubscriptionId; + } + + /// @inheritdoc IFunctionsSubscriptions + function getSubscription(uint64 subscriptionId) public view override returns (Subscription memory) { + _isExistingSubscription(subscriptionId); + return s_subscriptions[subscriptionId]; + } + + /// @inheritdoc IFunctionsSubscriptions + function getSubscriptionsInRange( + uint64 subscriptionIdStart, + uint64 subscriptionIdEnd + ) external view override returns (Subscription[] memory subscriptions) { + if ( + subscriptionIdStart > subscriptionIdEnd || + subscriptionIdEnd > s_currentSubscriptionId || + s_currentSubscriptionId == 0 + ) { + revert InvalidCalldata(); + } + + subscriptions = new Subscription[]((subscriptionIdEnd - subscriptionIdStart) + 1); + for (uint256 i = 0; i <= subscriptionIdEnd - subscriptionIdStart; ++i) { + subscriptions[i] = s_subscriptions[uint64(subscriptionIdStart + i)]; + } + + return subscriptions; + } + + /// @inheritdoc IFunctionsSubscriptions + function getConsumer(address client, uint64 subscriptionId) public view override returns (Consumer memory) { + return s_consumers[client][subscriptionId]; + } + + /// @dev Used within this file & FunctionsRouter.sol + function _isExistingSubscription(uint64 subscriptionId) internal view { + if (s_subscriptions[subscriptionId].owner == address(0)) { + revert InvalidSubscription(); + } + } + + /// @dev Used within FunctionsRouter.sol + function _isAllowedConsumer(address client, uint64 subscriptionId) internal view { + if (!s_consumers[client][subscriptionId].allowed) { + revert InvalidConsumer(); + } + } + + /// @inheritdoc IFunctionsSubscriptions + function createSubscription() external override returns (uint64 subscriptionId) { + _whenNotPaused(); + _onlySenderThatAcceptedToS(); + + subscriptionId = ++s_currentSubscriptionId; + s_subscriptions[subscriptionId] = Subscription({ + balance: 0, + blockedBalance: 0, + owner: msg.sender, + proposedOwner: address(0), + consumers: new address[](0), + flags: bytes32(0) + }); + + emit SubscriptionCreated(subscriptionId, msg.sender); + + return subscriptionId; + } + + /// @inheritdoc IFunctionsSubscriptions + function createSubscriptionWithConsumer(address consumer) external override returns (uint64 subscriptionId) { + _whenNotPaused(); + _onlySenderThatAcceptedToS(); + + subscriptionId = ++s_currentSubscriptionId; + s_subscriptions[subscriptionId] = Subscription({ + balance: 0, + blockedBalance: 0, + owner: msg.sender, + proposedOwner: address(0), + consumers: new address[](0), + flags: bytes32(0) + }); + + s_subscriptions[subscriptionId].consumers.push(consumer); + s_consumers[consumer][subscriptionId].allowed = true; + + emit SubscriptionCreated(subscriptionId, msg.sender); + emit SubscriptionConsumerAdded(subscriptionId, consumer); + + return subscriptionId; + } + + /// @inheritdoc IFunctionsSubscriptions + function proposeSubscriptionOwnerTransfer(uint64 subscriptionId, address newOwner) external override { + _whenNotPaused(); + _onlySubscriptionOwner(subscriptionId); + _onlySenderThatAcceptedToS(); + + if (newOwner == address(0) || s_subscriptions[subscriptionId].proposedOwner == newOwner) { + revert InvalidCalldata(); + } + + s_subscriptions[subscriptionId].proposedOwner = newOwner; + emit SubscriptionOwnerTransferRequested(subscriptionId, msg.sender, newOwner); + } + + /// @inheritdoc IFunctionsSubscriptions + function acceptSubscriptionOwnerTransfer(uint64 subscriptionId) external override { + _whenNotPaused(); + _onlySenderThatAcceptedToS(); + + address previousOwner = s_subscriptions[subscriptionId].owner; + address proposedOwner = s_subscriptions[subscriptionId].proposedOwner; + if (proposedOwner != msg.sender) { + revert MustBeProposedOwner(proposedOwner); + } + s_subscriptions[subscriptionId].owner = msg.sender; + s_subscriptions[subscriptionId].proposedOwner = address(0); + emit SubscriptionOwnerTransferred(subscriptionId, previousOwner, msg.sender); + } + + /// @inheritdoc IFunctionsSubscriptions + function removeConsumer(uint64 subscriptionId, address consumer) external override { + _whenNotPaused(); + _onlySubscriptionOwner(subscriptionId); + _onlySenderThatAcceptedToS(); + + Consumer memory consumerData = s_consumers[consumer][subscriptionId]; + _isAllowedConsumer(consumer, subscriptionId); + if (consumerData.initiatedRequests != consumerData.completedRequests) { + revert CannotRemoveWithPendingRequests(); + } + // Note bounded by config.maxConsumers + address[] memory consumers = s_subscriptions[subscriptionId].consumers; + for (uint256 i = 0; i < consumers.length; ++i) { + if (consumers[i] == consumer) { + // Storage write to preserve last element + s_subscriptions[subscriptionId].consumers[i] = consumers[consumers.length - 1]; + // Storage remove last element + s_subscriptions[subscriptionId].consumers.pop(); + break; + } + } + delete s_consumers[consumer][subscriptionId]; + emit SubscriptionConsumerRemoved(subscriptionId, consumer); + } + + /// @dev Overriden in FunctionsRouter.sol + function _getMaxConsumers() internal view virtual returns (uint16); + + /// @inheritdoc IFunctionsSubscriptions + function addConsumer(uint64 subscriptionId, address consumer) external override { + _whenNotPaused(); + _onlySubscriptionOwner(subscriptionId); + _onlySenderThatAcceptedToS(); + + // Already maxed, cannot add any more consumers. + uint16 maximumConsumers = _getMaxConsumers(); + if (s_subscriptions[subscriptionId].consumers.length >= maximumConsumers) { + revert TooManyConsumers(maximumConsumers); + } + if (s_consumers[consumer][subscriptionId].allowed) { + // Idempotence - do nothing if already added. + // Ensures uniqueness in s_subscriptions[subscriptionId].consumers. + return; + } + + s_consumers[consumer][subscriptionId].allowed = true; + s_subscriptions[subscriptionId].consumers.push(consumer); + + emit SubscriptionConsumerAdded(subscriptionId, consumer); + } + + /// @dev Overriden in FunctionsRouter.sol + function _getSubscriptionDepositDetails() internal virtual returns (uint16, uint72); + + function _cancelSubscriptionHelper(uint64 subscriptionId, address toAddress, bool checkDepositRefundability) private { + Subscription memory subscription = s_subscriptions[subscriptionId]; + uint96 balance = subscription.balance; + uint64 completedRequests = 0; + + // NOTE: loop iterations are bounded by config.maxConsumers + // If no consumers, does nothing. + for (uint256 i = 0; i < subscription.consumers.length; ++i) { + address consumer = subscription.consumers[i]; + completedRequests += s_consumers[consumer][subscriptionId].completedRequests; + delete s_consumers[consumer][subscriptionId]; + } + delete s_subscriptions[subscriptionId]; + + (uint16 subscriptionDepositMinimumRequests, uint72 subscriptionDepositJuels) = _getSubscriptionDepositDetails(); + + // If subscription has not made enough requests, deposit will be forfeited + if (checkDepositRefundability && completedRequests < subscriptionDepositMinimumRequests) { + uint96 deposit = subscriptionDepositJuels > balance ? balance : subscriptionDepositJuels; + if (deposit > 0) { + s_withdrawableTokens[address(this)] += deposit; + balance -= deposit; + } + } + + if (balance > 0) { + s_totalLinkBalance -= balance; + i_linkToken.safeTransfer(toAddress, uint256(balance)); + } + emit SubscriptionCanceled(subscriptionId, toAddress, balance); + } + + /// @inheritdoc IFunctionsSubscriptions + function cancelSubscription(uint64 subscriptionId, address to) external override { + _whenNotPaused(); + _onlySubscriptionOwner(subscriptionId); + _onlySenderThatAcceptedToS(); + + if (pendingRequestExists(subscriptionId)) { + revert CannotRemoveWithPendingRequests(); + } + + _cancelSubscriptionHelper(subscriptionId, to, true); + } + + /// @inheritdoc IFunctionsSubscriptions + function pendingRequestExists(uint64 subscriptionId) public view override returns (bool) { + address[] memory consumers = s_subscriptions[subscriptionId].consumers; + // NOTE: loop iterations are bounded by config.maxConsumers + for (uint256 i = 0; i < consumers.length; ++i) { + Consumer memory consumer = s_consumers[consumers[i]][subscriptionId]; + if (consumer.initiatedRequests != consumer.completedRequests) { + return true; + } + } + return false; + } + + /// @inheritdoc IFunctionsSubscriptions + function setFlags(uint64 subscriptionId, bytes32 flags) external override { + _onlyRouterOwner(); + _isExistingSubscription(subscriptionId); + s_subscriptions[subscriptionId].flags = flags; + } + + /// @inheritdoc IFunctionsSubscriptions + function getFlags(uint64 subscriptionId) public view returns (bytes32) { + return s_subscriptions[subscriptionId].flags; + } + + // ================================================================ + // | Request Timeout | + // ================================================================ + + /// @inheritdoc IFunctionsSubscriptions + function timeoutRequests(FunctionsResponse.Commitment[] calldata requestsToTimeoutByCommitment) external override { + _whenNotPaused(); + + for (uint256 i = 0; i < requestsToTimeoutByCommitment.length; ++i) { + FunctionsResponse.Commitment memory request = requestsToTimeoutByCommitment[i]; + bytes32 requestId = request.requestId; + uint64 subscriptionId = request.subscriptionId; + + // Check that request ID is valid + if (keccak256(abi.encode(request)) != s_requestCommitments[requestId]) { + revert InvalidCalldata(); + } + + // Check that request has exceeded allowed request time + if (block.timestamp < request.timeoutTimestamp) { + revert TimeoutNotExceeded(); + } + + // Notify the Coordinator that the request should no longer be fulfilled + IFunctionsBilling(request.coordinator).deleteCommitment(requestId); + // Release the subscription's balance that had been earmarked for the request + s_subscriptions[subscriptionId].blockedBalance -= request.estimatedTotalCostJuels; + s_consumers[request.client][subscriptionId].completedRequests += 1; + // Delete commitment within Router state + delete s_requestCommitments[requestId]; + + emit RequestTimedOut(requestId); + } + } + + // ================================================================ + // | Modifiers | + // ================================================================ + + function _onlySubscriptionOwner(uint64 subscriptionId) internal view { + address owner = s_subscriptions[subscriptionId].owner; + if (owner == address(0)) { + revert InvalidSubscription(); + } + if (msg.sender != owner) { + revert MustBeSubscriptionOwner(); + } + } + + /// @dev Overriden in FunctionsRouter.sol + function _onlySenderThatAcceptedToS() internal virtual; + + /// @dev Overriden in FunctionsRouter.sol + function _onlyRouterOwner() internal virtual; + + /// @dev Overriden in FunctionsRouter.sol + function _whenNotPaused() internal virtual; +} diff --git a/contracts/src/v0.8/functions/v1_0_0/Routable.sol b/contracts/src/v0.8/functions/v1_0_0/Routable.sol new file mode 100644 index 00000000000..6c11d4d6180 --- /dev/null +++ b/contracts/src/v0.8/functions/v1_0_0/Routable.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol"; +import {IOwnableFunctionsRouter} from "./interfaces/IOwnableFunctionsRouter.sol"; + +/// @title This abstract should be inherited by contracts that will be used +/// as the destinations to a route (id=>contract) on the Router. +/// It provides a Router getter and modifiers. +abstract contract Routable is ITypeAndVersion { + IOwnableFunctionsRouter private immutable i_router; + + error RouterMustBeSet(); + error OnlyCallableByRouter(); + error OnlyCallableByRouterOwner(); + + /// @dev Initializes the contract. + constructor(address router) { + if (router == address(0)) { + revert RouterMustBeSet(); + } + i_router = IOwnableFunctionsRouter(router); + } + + /// @notice Return the Router + function _getRouter() internal view returns (IOwnableFunctionsRouter router) { + return i_router; + } + + /// @notice Reverts if called by anyone other than the router. + modifier onlyRouter() { + if (msg.sender != address(i_router)) { + revert OnlyCallableByRouter(); + } + _; + } + + /// @notice Reverts if called by anyone other than the router owner. + modifier onlyRouterOwner() { + if (msg.sender != i_router.owner()) { + revert OnlyCallableByRouterOwner(); + } + _; + } +} diff --git a/contracts/src/v0.8/functions/v1_0_0/accessControl/TermsOfServiceAllowList.sol b/contracts/src/v0.8/functions/v1_0_0/accessControl/TermsOfServiceAllowList.sol new file mode 100644 index 00000000000..a7b13577842 --- /dev/null +++ b/contracts/src/v0.8/functions/v1_0_0/accessControl/TermsOfServiceAllowList.sol @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {ITermsOfServiceAllowList} from "./interfaces/ITermsOfServiceAllowList.sol"; +import {IAccessController} from "../../../shared/interfaces/IAccessController.sol"; +import {ITypeAndVersion} from "../../../shared/interfaces/ITypeAndVersion.sol"; + +import {ConfirmedOwner} from "../../../shared/access/ConfirmedOwner.sol"; + +import {Address} from "../../../vendor/openzeppelin-solidity/v4.8.0/contracts/utils/Address.sol"; +import {EnumerableSet} from "../../../vendor/openzeppelin-solidity/v4.8.0/contracts/utils/structs/EnumerableSet.sol"; + +/// @notice A contract to handle access control of subscription management dependent on signing a Terms of Service +contract TermsOfServiceAllowList is ITermsOfServiceAllowList, IAccessController, ITypeAndVersion, ConfirmedOwner { + using Address for address; + using EnumerableSet for EnumerableSet.AddressSet; + + /// @inheritdoc ITypeAndVersion + string public constant override typeAndVersion = "Functions Terms of Service Allow List v1.0.0"; + + EnumerableSet.AddressSet private s_allowedSenders; + mapping(address => bool) private s_blockedSenders; + + event AddedAccess(address user); + event BlockedAccess(address user); + event UnblockedAccess(address user); + + error InvalidSignature(); + error InvalidUsage(); + error RecipientIsBlocked(); + + // ================================================================ + // | Configuration state | + // ================================================================ + struct Config { + bool enabled; // ═════════════╗ When enabled, access will be checked against s_allowedSenders. When disabled, all access will be allowed. + address signerPublicKey; // ══╝ The key pair that needs to sign the acceptance data + } + + Config private s_config; + + event ConfigUpdated(Config config); + + // ================================================================ + // | Initialization | + // ================================================================ + + constructor(Config memory config) ConfirmedOwner(msg.sender) { + updateConfig(config); + } + + // ================================================================ + // | Configuration | + // ================================================================ + + /// @notice Gets the contracts's configuration + /// @return config + function getConfig() external view returns (Config memory) { + return s_config; + } + + /// @notice Sets the contracts's configuration + /// @param config - See the contents of the TermsOfServiceAllowList.Config struct for more information + function updateConfig(Config memory config) public onlyOwner { + s_config = config; + emit ConfigUpdated(config); + } + + // ================================================================ + // | Allow methods | + // ================================================================ + + /// @inheritdoc ITermsOfServiceAllowList + function getMessage(address acceptor, address recipient) public pure override returns (bytes32) { + return keccak256(abi.encodePacked(acceptor, recipient)); + } + + /// @inheritdoc ITermsOfServiceAllowList + function acceptTermsOfService(address acceptor, address recipient, bytes32 r, bytes32 s, uint8 v) external override { + if (s_blockedSenders[recipient]) { + revert RecipientIsBlocked(); + } + + // Validate that the signature is correct and the correct data has been signed + bytes32 prefixedMessage = keccak256( + abi.encodePacked("\x19Ethereum Signed Message:\n32", getMessage(acceptor, recipient)) + ); + if (ecrecover(prefixedMessage, v, r, s) != s_config.signerPublicKey) { + revert InvalidSignature(); + } + + // If contract, validate that msg.sender == recipient + // This is to prevent EoAs from claiming contracts that they are not in control of + // If EoA, validate that msg.sender == acceptor == recipient + // This is to prevent EoAs from accepting for other EoAs + if (msg.sender != recipient || (msg.sender != acceptor && !msg.sender.isContract())) { + revert InvalidUsage(); + } + + // Add recipient to the allow list + s_allowedSenders.add(recipient); + emit AddedAccess(recipient); + } + + /// @inheritdoc ITermsOfServiceAllowList + function getAllAllowedSenders() external view override returns (address[] memory) { + return s_allowedSenders.values(); + } + + /// @inheritdoc IAccessController + function hasAccess(address user, bytes calldata /* data */) external view override returns (bool) { + if (!s_config.enabled) { + return true; + } + return s_allowedSenders.contains(user); + } + + // ================================================================ + // | Block methods | + // ================================================================ + + /// @inheritdoc ITermsOfServiceAllowList + function isBlockedSender(address sender) external view override returns (bool) { + if (!s_config.enabled) { + return false; + } + return s_blockedSenders[sender]; + } + + /// @inheritdoc ITermsOfServiceAllowList + function blockSender(address sender) external override onlyOwner { + s_allowedSenders.remove(sender); + s_blockedSenders[sender] = true; + emit BlockedAccess(sender); + } + + /// @inheritdoc ITermsOfServiceAllowList + function unblockSender(address sender) external override onlyOwner { + s_blockedSenders[sender] = false; + emit UnblockedAccess(sender); + } +} diff --git a/contracts/src/v0.8/functions/v1_0_0/accessControl/interfaces/ITermsOfServiceAllowList.sol b/contracts/src/v0.8/functions/v1_0_0/accessControl/interfaces/ITermsOfServiceAllowList.sol new file mode 100644 index 00000000000..af4daa18bc3 --- /dev/null +++ b/contracts/src/v0.8/functions/v1_0_0/accessControl/interfaces/ITermsOfServiceAllowList.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +/// @notice A contract to handle access control of subscription management dependent on signing a Terms of Service +interface ITermsOfServiceAllowList { + /// @notice Return the message data for the proof given to accept the Terms of Service + /// @param acceptor - The wallet address that has accepted the Terms of Service on the UI + /// @param recipient - The recipient address that the acceptor is taking responsibility for + /// @return Hash of the message data + function getMessage(address acceptor, address recipient) external pure returns (bytes32); + + /// @notice Check if the address is blocked for usage + /// @param sender The transaction sender's address + /// @return True or false + function isBlockedSender(address sender) external returns (bool); + + /// @notice Get a list of all allowed senders + /// @dev WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + /// to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + /// this function has an unbounded cost, and using it as part of a state-changing function may render the function + /// uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + /// @return addresses - all allowed addresses + function getAllAllowedSenders() external view returns (address[] memory); + + /// @notice Allows access to the sender based on acceptance of the Terms of Service + /// @param acceptor - The wallet address that has accepted the Terms of Service on the UI + /// @param recipient - The recipient address that the acceptor is taking responsibility for + /// @param r - ECDSA signature r data produced by the Chainlink Functions Subscription UI + /// @param s - ECDSA signature s produced by the Chainlink Functions Subscription UI + /// @param v - ECDSA signature v produced by the Chainlink Functions Subscription UI + function acceptTermsOfService(address acceptor, address recipient, bytes32 r, bytes32 s, uint8 v) external; + + /// @notice Removes a sender's access if already authorized, and disallows re-accepting the Terms of Service + /// @param sender - Address of the sender to block + function blockSender(address sender) external; + + /// @notice Re-allows a previously blocked sender to accept the Terms of Service + /// @param sender - Address of the sender to unblock + function unblockSender(address sender) external; +} diff --git a/contracts/src/v0.8/functions/dev/v1_0_0/example/FunctionsClientExample.sol b/contracts/src/v0.8/functions/v1_0_0/example/FunctionsClientExample.sol similarity index 97% rename from contracts/src/v0.8/functions/dev/v1_0_0/example/FunctionsClientExample.sol rename to contracts/src/v0.8/functions/v1_0_0/example/FunctionsClientExample.sol index f0f154c07b2..8d1cf36c6c4 100644 --- a/contracts/src/v0.8/functions/dev/v1_0_0/example/FunctionsClientExample.sol +++ b/contracts/src/v0.8/functions/v1_0_0/example/FunctionsClientExample.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.19; import {FunctionsClient} from "../FunctionsClient.sol"; -import {ConfirmedOwner} from "../../../../shared/access/ConfirmedOwner.sol"; +import {ConfirmedOwner} from "../../../shared/access/ConfirmedOwner.sol"; import {FunctionsRequest} from "../libraries/FunctionsRequest.sol"; /// @title Chainlink Functions example Client contract implementation diff --git a/contracts/src/v0.8/functions/v1_0_0/interfaces/IFunctionsBilling.sol b/contracts/src/v0.8/functions/v1_0_0/interfaces/IFunctionsBilling.sol new file mode 100644 index 00000000000..6291d05e57c --- /dev/null +++ b/contracts/src/v0.8/functions/v1_0_0/interfaces/IFunctionsBilling.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +/// @title Chainlink Functions DON billing interface. +interface IFunctionsBilling { + /// @notice Return the current conversion from WEI of ETH to LINK from the configured Chainlink data feed + /// @return weiPerUnitLink - The amount of WEI in one LINK + function getWeiPerUnitLink() external view returns (uint256); + + /// @notice Determine the fee that will be split between Node Operators for servicing a request + /// @param requestCBOR - CBOR encoded Chainlink Functions request data, use FunctionsRequest library to encode a request + /// @return fee - Cost in Juels (1e18) of LINK + function getDONFee(bytes memory requestCBOR) external view returns (uint72); + + /// @notice Determine the fee that will be paid to the Router owner for operating the network + /// @return fee - Cost in Juels (1e18) of LINK + function getAdminFee() external view returns (uint72); + + /// @notice Estimate the total cost that will be charged to a subscription to make a request: transmitter gas re-reimbursement, plus DON fee, plus Registry fee + /// @param - subscriptionId An identifier of the billing account + /// @param - data Encoded Chainlink Functions request data, use FunctionsClient API to encode a request + /// @param - callbackGasLimit Gas limit for the fulfillment callback + /// @param - gasPriceWei The blockchain's gas price to estimate with + /// @return - billedCost Cost in Juels (1e18) of LINK + function estimateCost( + uint64 subscriptionId, + bytes calldata data, + uint32 callbackGasLimit, + uint256 gasPriceWei + ) external view returns (uint96); + + /// @notice Remove a request commitment that the Router has determined to be stale + /// @param requestId - The request ID to remove + function deleteCommitment(bytes32 requestId) external; + + /// @notice Oracle withdraw LINK earned through fulfilling requests + /// @notice If amount is 0 the full balance will be withdrawn + /// @param recipient where to send the funds + /// @param amount amount to withdraw + function oracleWithdraw(address recipient, uint96 amount) external; + + /// @notice Withdraw all LINK earned by Oracles through fulfilling requests + function oracleWithdrawAll() external; +} diff --git a/contracts/src/v0.8/functions/v1_0_0/interfaces/IFunctionsClient.sol b/contracts/src/v0.8/functions/v1_0_0/interfaces/IFunctionsClient.sol new file mode 100644 index 00000000000..f28a41666b5 --- /dev/null +++ b/contracts/src/v0.8/functions/v1_0_0/interfaces/IFunctionsClient.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +/// @title Chainlink Functions client interface. +interface IFunctionsClient { + /// @notice Chainlink Functions response handler called by the Functions Router + /// during fullilment from the designated transmitter node in an OCR round. + /// @param requestId The requestId returned by FunctionsClient.sendRequest(). + /// @param response Aggregated response from the request's source code. + /// @param err Aggregated error either from the request's source code or from the execution pipeline. + /// @dev Either response or error parameter will be set, but never both. + function handleOracleFulfillment(bytes32 requestId, bytes memory response, bytes memory err) external; +} diff --git a/contracts/src/v0.8/functions/v1_0_0/interfaces/IFunctionsCoordinator.sol b/contracts/src/v0.8/functions/v1_0_0/interfaces/IFunctionsCoordinator.sol new file mode 100644 index 00000000000..4e2bd703dc4 --- /dev/null +++ b/contracts/src/v0.8/functions/v1_0_0/interfaces/IFunctionsCoordinator.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {FunctionsResponse} from "../libraries/FunctionsResponse.sol"; + +/// @title Chainlink Functions DON Coordinator interface. +interface IFunctionsCoordinator { + /// @notice Returns the DON's threshold encryption public key used to encrypt secrets + /// @dev All nodes on the DON have separate key shares of the threshold decryption key + /// and nodes must participate in a threshold decryption OCR round to decrypt secrets + /// @return thresholdPublicKey the DON's threshold encryption public key + function getThresholdPublicKey() external view returns (bytes memory); + + /// @notice Sets the DON's threshold encryption public key used to encrypt secrets + /// @dev Used to rotate the key + /// @param thresholdPublicKey The new public key + function setThresholdPublicKey(bytes calldata thresholdPublicKey) external; + + /// @notice Returns the DON's secp256k1 public key that is used to encrypt secrets + /// @dev All nodes on the DON have the corresponding private key + /// needed to decrypt the secrets encrypted with the public key + /// @return publicKey the DON's public key + function getDONPublicKey() external view returns (bytes memory); + + /// @notice Sets DON's secp256k1 public key used to encrypt secrets + /// @dev Used to rotate the key + /// @param donPublicKey The new public key + function setDONPublicKey(bytes calldata donPublicKey) external; + + /// @notice Receives a request to be emitted to the DON for processing + /// @param request The request metadata + /// @dev see the struct for field descriptions + /// @return commitment - The parameters of the request that must be held consistent at response time + function startRequest( + FunctionsResponse.RequestMeta calldata request + ) external returns (FunctionsResponse.Commitment memory commitment); +} diff --git a/contracts/src/v0.8/functions/v1_0_0/interfaces/IFunctionsRouter.sol b/contracts/src/v0.8/functions/v1_0_0/interfaces/IFunctionsRouter.sol new file mode 100644 index 00000000000..5f93aac873e --- /dev/null +++ b/contracts/src/v0.8/functions/v1_0_0/interfaces/IFunctionsRouter.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {FunctionsResponse} from "../libraries/FunctionsResponse.sol"; + +/// @title Chainlink Functions Router interface. +interface IFunctionsRouter { + /// @notice The identifier of the route to retrieve the address of the access control contract + /// The access control contract controls which accounts can manage subscriptions + /// @return id - bytes32 id that can be passed to the "getContractById" of the Router + function getAllowListId() external view returns (bytes32); + + /// @notice Set the identifier of the route to retrieve the address of the access control contract + /// The access control contract controls which accounts can manage subscriptions + function setAllowListId(bytes32 allowListId) external; + + /// @notice Get the flat fee (in Juels of LINK) that will be paid to the Router owner for operation of the network + /// @return adminFee + function getAdminFee() external view returns (uint72 adminFee); + + /// @notice Sends a request using the provided subscriptionId + /// @param subscriptionId - A unique subscription ID allocated by billing system, + /// a client can make requests from different contracts referencing the same subscription + /// @param data - CBOR encoded Chainlink Functions request data, use FunctionsClient API to encode a request + /// @param dataVersion - Gas limit for the fulfillment callback + /// @param callbackGasLimit - Gas limit for the fulfillment callback + /// @param donId - An identifier used to determine which route to send the request along + /// @return requestId - A unique request identifier + function sendRequest( + uint64 subscriptionId, + bytes calldata data, + uint16 dataVersion, + uint32 callbackGasLimit, + bytes32 donId + ) external returns (bytes32); + + /// @notice Sends a request to the proposed contracts + /// @param subscriptionId - A unique subscription ID allocated by billing system, + /// a client can make requests from different contracts referencing the same subscription + /// @param data - CBOR encoded Chainlink Functions request data, use FunctionsClient API to encode a request + /// @param dataVersion - Gas limit for the fulfillment callback + /// @param callbackGasLimit - Gas limit for the fulfillment callback + /// @param donId - An identifier used to determine which route to send the request along + /// @return requestId - A unique request identifier + function sendRequestToProposed( + uint64 subscriptionId, + bytes calldata data, + uint16 dataVersion, + uint32 callbackGasLimit, + bytes32 donId + ) external returns (bytes32); + + /// @notice Fulfill the request by: + /// - calling back the data that the Oracle returned to the client contract + /// - pay the DON for processing the request + /// @dev Only callable by the Coordinator contract that is saved in the commitment + /// @param response response data from DON consensus + /// @param err error from DON consensus + /// @param juelsPerGas - current rate of juels/gas + /// @param costWithoutFulfillment - The cost of processing the request (in Juels of LINK ), without fulfillment + /// @param transmitter - The Node that transmitted the OCR report + /// @param commitment - The parameters of the request that must be held consistent between request and response time + /// @return fulfillResult - + /// @return callbackGasCostJuels - + function fulfill( + bytes memory response, + bytes memory err, + uint96 juelsPerGas, + uint96 costWithoutFulfillment, + address transmitter, + FunctionsResponse.Commitment memory commitment + ) external returns (FunctionsResponse.FulfillResult, uint96); + + /// @notice Validate requested gas limit is below the subscription max. + /// @param subscriptionId subscription ID + /// @param callbackGasLimit desired callback gas limit + function isValidCallbackGasLimit(uint64 subscriptionId, uint32 callbackGasLimit) external view; + + /// @notice Get the current contract given an ID + /// @param id A bytes32 identifier for the route + /// @return contract The current contract address + function getContractById(bytes32 id) external view returns (address); + + /// @notice Get the proposed next contract given an ID + /// @param id A bytes32 identifier for the route + /// @return contract The current or proposed contract address + function getProposedContractById(bytes32 id) external view returns (address); + + /// @notice Return the latest proprosal set + /// @return ids The identifiers of the contracts to update + /// @return to The addresses of the contracts that will be updated to + function getProposedContractSet() external view returns (bytes32[] memory, address[] memory); + + /// @notice Proposes one or more updates to the contract routes + /// @dev Only callable by owner + function proposeContractsUpdate(bytes32[] memory proposalSetIds, address[] memory proposalSetAddresses) external; + + /// @notice Updates the current contract routes to the proposed contracts + /// @dev Only callable by owner + function updateContracts() external; + + /// @dev Puts the system into an emergency stopped state. + /// @dev Only callable by owner + function pause() external; + + /// @dev Takes the system out of an emergency stopped state. + /// @dev Only callable by owner + function unpause() external; +} diff --git a/contracts/src/v0.8/functions/v1_0_0/interfaces/IFunctionsSubscriptions.sol b/contracts/src/v0.8/functions/v1_0_0/interfaces/IFunctionsSubscriptions.sol new file mode 100644 index 00000000000..eafd6f4fe99 --- /dev/null +++ b/contracts/src/v0.8/functions/v1_0_0/interfaces/IFunctionsSubscriptions.sol @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {FunctionsResponse} from "../libraries/FunctionsResponse.sol"; + +/// @title Chainlink Functions Subscription interface. +interface IFunctionsSubscriptions { + struct Subscription { + uint96 balance; // ═════════╗ Common LINK balance that is controlled by the Router to be used for all consumer requests. + address owner; // ══════════╝ The owner can fund/withdraw/cancel the subscription. + uint96 blockedBalance; // ══╗ LINK balance that is reserved to pay for pending consumer requests. + address proposedOwner; // ══╝ For safely transferring sub ownership. + address[] consumers; // ════╸ Client contracts that can use the subscription + bytes32 flags; // ══════════╸ Per-subscription flags + } + + struct Consumer { + bool allowed; // ══════════════╗ Owner can fund/withdraw/cancel the sub. + uint64 initiatedRequests; // ║ The number of requests that have been started + uint64 completedRequests; // ══╝ The number of requests that have successfully completed or timed out + } + + /// @notice Get details about a subscription. + /// @param subscriptionId - the ID of the subscription + /// @return subscription - see IFunctionsSubscriptions.Subscription for more information on the structure + function getSubscription(uint64 subscriptionId) external view returns (Subscription memory); + + /// @notice Retrieve details about multiple subscriptions using an inclusive range + /// @param subscriptionIdStart - the ID of the subscription to start the range at + /// @param subscriptionIdEnd - the ID of the subscription to end the range at + /// @return subscriptions - see IFunctionsSubscriptions.Subscription for more information on the structure + function getSubscriptionsInRange( + uint64 subscriptionIdStart, + uint64 subscriptionIdEnd + ) external view returns (Subscription[] memory); + + /// @notice Get details about a consumer of a subscription. + /// @param client - the consumer contract address + /// @param subscriptionId - the ID of the subscription + /// @return consumer - see IFunctionsSubscriptions.Consumer for more information on the structure + function getConsumer(address client, uint64 subscriptionId) external view returns (Consumer memory); + + /// @notice Get details about the total amount of LINK within the system + /// @return totalBalance - total Juels of LINK held by the contract + function getTotalBalance() external view returns (uint96); + + /// @notice Get details about the total number of subscription accounts + /// @return count - total number of subscriptions in the system + function getSubscriptionCount() external view returns (uint64); + + /// @notice Time out all expired requests: unlocks funds and removes the ability for the request to be fulfilled + /// @param requestsToTimeoutByCommitment - A list of request commitments to time out + /// @dev The commitment can be found on the "OracleRequest" event created when sending the request. + function timeoutRequests(FunctionsResponse.Commitment[] calldata requestsToTimeoutByCommitment) external; + + /// @notice Oracle withdraw LINK earned through fulfilling requests + /// @notice If amount is 0 the full balance will be withdrawn + /// @notice Both signing and transmitting wallets will have a balance to withdraw + /// @param recipient where to send the funds + /// @param amount amount to withdraw + function oracleWithdraw(address recipient, uint96 amount) external; + + /// @notice Owner cancel subscription, sends remaining link directly to the subscription owner. + /// @dev Only callable by the Router Owner + /// @param subscriptionId subscription id + /// @dev notably can be called even if there are pending requests, outstanding ones may fail onchain + function ownerCancelSubscription(uint64 subscriptionId) external; + + /// @notice Recover link sent with transfer instead of transferAndCall. + /// @dev Only callable by the Router Owner + /// @param to address to send link to + function recoverFunds(address to) external; + + /// @notice Create a new subscription. + /// @return subscriptionId - A unique subscription id. + /// @dev You can manage the consumer set dynamically with addConsumer/removeConsumer. + /// @dev Note to fund the subscription, use transferAndCall. For example + /// @dev LINKTOKEN.transferAndCall( + /// @dev address(ROUTER), + /// @dev amount, + /// @dev abi.encode(subscriptionId)); + function createSubscription() external returns (uint64); + + /// @notice Create a new subscription and add a consumer. + /// @return subscriptionId - A unique subscription id. + /// @dev You can manage the consumer set dynamically with addConsumer/removeConsumer. + /// @dev Note to fund the subscription, use transferAndCall. For example + /// @dev LINKTOKEN.transferAndCall( + /// @dev address(ROUTER), + /// @dev amount, + /// @dev abi.encode(subscriptionId)); + function createSubscriptionWithConsumer(address consumer) external returns (uint64 subscriptionId); + + /// @notice Propose a new owner for a subscription. + /// @dev Only callable by the Subscription's owner + /// @param subscriptionId - ID of the subscription + /// @param newOwner - proposed new owner of the subscription + function proposeSubscriptionOwnerTransfer(uint64 subscriptionId, address newOwner) external; + + /// @notice Accept an ownership transfer. + /// @param subscriptionId - ID of the subscription + /// @dev will revert if original owner of subscriptionId has not requested that msg.sender become the new owner. + function acceptSubscriptionOwnerTransfer(uint64 subscriptionId) external; + + /// @notice Remove a consumer from a Chainlink Functions subscription. + /// @dev Only callable by the Subscription's owner + /// @param subscriptionId - ID of the subscription + /// @param consumer - Consumer to remove from the subscription + function removeConsumer(uint64 subscriptionId, address consumer) external; + + /// @notice Add a consumer to a Chainlink Functions subscription. + /// @dev Only callable by the Subscription's owner + /// @param subscriptionId - ID of the subscription + /// @param consumer - New consumer which can use the subscription + function addConsumer(uint64 subscriptionId, address consumer) external; + + /// @notice Cancel a subscription + /// @dev Only callable by the Subscription's owner + /// @param subscriptionId - ID of the subscription + /// @param to - Where to send the remaining LINK to + function cancelSubscription(uint64 subscriptionId, address to) external; + + /// @notice Check to see if there exists a request commitment for all consumers for a given sub. + /// @param subscriptionId - ID of the subscription + /// @return true if there exists at least one unfulfilled request for the subscription, false otherwise. + /// @dev Looping is bounded to MAX_CONSUMERS*(number of DONs). + /// @dev Used to disable subscription canceling while outstanding request are present. + function pendingRequestExists(uint64 subscriptionId) external view returns (bool); + + /// @notice Set subscription specific flags for a subscription. + /// Each byte of the flag is used to represent a resource tier that the subscription can utilize. + /// @param subscriptionId - ID of the subscription + /// @param flags - desired flag values + function setFlags(uint64 subscriptionId, bytes32 flags) external; + + /// @notice Get flags for a given subscription. + /// @param subscriptionId - ID of the subscription + /// @return flags - current flag values + function getFlags(uint64 subscriptionId) external view returns (bytes32); +} diff --git a/contracts/src/v0.8/functions/v1_0_0/interfaces/IOwnableFunctionsRouter.sol b/contracts/src/v0.8/functions/v1_0_0/interfaces/IOwnableFunctionsRouter.sol new file mode 100644 index 00000000000..89eb48022be --- /dev/null +++ b/contracts/src/v0.8/functions/v1_0_0/interfaces/IOwnableFunctionsRouter.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IFunctionsRouter} from "./IFunctionsRouter.sol"; +import {IOwnable} from "../../../shared/interfaces/IOwnable.sol"; + +/// @title Chainlink Functions Router interface with Ownability. +interface IOwnableFunctionsRouter is IOwnable, IFunctionsRouter { + +} diff --git a/contracts/src/v0.8/functions/dev/v1_0_0/libraries/FunctionsRequest.sol b/contracts/src/v0.8/functions/v1_0_0/libraries/FunctionsRequest.sol similarity index 98% rename from contracts/src/v0.8/functions/dev/v1_0_0/libraries/FunctionsRequest.sol rename to contracts/src/v0.8/functions/v1_0_0/libraries/FunctionsRequest.sol index efc3a811e6a..ef697b5e809 100644 --- a/contracts/src/v0.8/functions/dev/v1_0_0/libraries/FunctionsRequest.sol +++ b/contracts/src/v0.8/functions/v1_0_0/libraries/FunctionsRequest.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import {CBOR} from "../../../../vendor/solidity-cborutils/v2.0.0/CBOR.sol"; +import {CBOR} from "../../../vendor/solidity-cborutils/v2.0.0/CBOR.sol"; /// @title Library for encoding the input data of a Functions request into CBOR library FunctionsRequest { diff --git a/contracts/src/v0.8/functions/v1_0_0/libraries/FunctionsResponse.sol b/contracts/src/v0.8/functions/v1_0_0/libraries/FunctionsResponse.sol new file mode 100644 index 00000000000..65fad665d69 --- /dev/null +++ b/contracts/src/v0.8/functions/v1_0_0/libraries/FunctionsResponse.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +/// @title Library of types that are used for fulfillment of a Functions request +library FunctionsResponse { + // Used to send request information from the Router to the Coordinator + struct RequestMeta { + bytes data; // ══════════════════╸ CBOR encoded Chainlink Functions request data, use FunctionsRequest library to encode a request + bytes32 flags; // ═══════════════╸ Per-subscription flags + address requestingContract; // ══╗ The client contract that is sending the request + uint96 availableBalance; // ═════╝ Common LINK balance of the subscription that is controlled by the Router to be used for all consumer requests. + uint72 adminFee; // ═════════════╗ Flat fee (in Juels of LINK) that will be paid to the Router Owner for operation of the network + uint64 subscriptionId; // ║ Identifier of the billing subscription that will be charged for the request + uint64 initiatedRequests; // ║ The number of requests that have been started + uint32 callbackGasLimit; // ║ The amount of gas that the callback to the consuming contract will be given + uint16 dataVersion; // ══════════╝ The version of the structure of the CBOR encoded request data + uint64 completedRequests; // ════╗ The number of requests that have successfully completed or timed out + address subscriptionOwner; // ═══╝ The owner of the billing subscription + } + + enum FulfillResult { + FULFILLED, // 0 + USER_CALLBACK_ERROR, // 1 + INVALID_REQUEST_ID, // 2 + COST_EXCEEDS_COMMITMENT, // 3 + INSUFFICIENT_GAS_PROVIDED, // 4 + SUBSCRIPTION_BALANCE_INVARIANT_VIOLATION, // 5 + INVALID_COMMITMENT // 6 + } + + struct Commitment { + bytes32 requestId; // ═════════════════╸ A unique identifier for a Chainlink Functions request + address coordinator; // ═══════════════╗ The Coordinator contract that manages the DON that is servicing a request + uint96 estimatedTotalCostJuels; // ════╝ The maximum cost in Juels (1e18) of LINK that will be charged to fulfill a request + address client; // ════════════════════╗ The client contract that sent the request + uint64 subscriptionId; // ║ Identifier of the billing subscription that will be charged for the request + uint32 callbackGasLimit; // ═══════════╝ The amount of gas that the callback to the consuming contract will be given + uint72 adminFee; // ═══════════════════╗ Flat fee (in Juels of LINK) that will be paid to the Router Owner for operation of the network + uint72 donFee; // ║ Fee (in Juels of LINK) that will be split between Node Operators for servicing a request + uint40 gasOverheadBeforeCallback; // ║ Represents the average gas execution cost before the fulfillment callback. + uint40 gasOverheadAfterCallback; // ║ Represents the average gas execution cost after the fulfillment callback. + uint32 timeoutTimestamp; // ═══════════╝ The timestamp at which a request will be eligible to be timed out + } +} diff --git a/contracts/src/v0.8/functions/v1_0_0/mocks/FunctionsV1EventsMock.sol b/contracts/src/v0.8/functions/v1_0_0/mocks/FunctionsV1EventsMock.sol new file mode 100644 index 00000000000..68b51f89019 --- /dev/null +++ b/contracts/src/v0.8/functions/v1_0_0/mocks/FunctionsV1EventsMock.sol @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +contract FunctionsV1EventsMock { + struct Config { + uint16 maxConsumersPerSubscription; + uint72 adminFee; + bytes4 handleOracleFulfillmentSelector; + uint16 gasForCallExactCheck; + uint32[] maxCallbackGasLimits; + } + event ConfigUpdated(Config param1); + event ContractProposed( + bytes32 proposedContractSetId, + address proposedContractSetFromAddress, + address proposedContractSetToAddress + ); + event ContractUpdated(bytes32 id, address from, address to); + event FundsRecovered(address to, uint256 amount); + event OwnershipTransferRequested(address indexed from, address indexed to); + event OwnershipTransferred(address indexed from, address indexed to); + event Paused(address account); + event RequestNotProcessed(bytes32 indexed requestId, address coordinator, address transmitter, uint8 resultCode); + event RequestProcessed( + bytes32 indexed requestId, + uint64 indexed subscriptionId, + uint96 totalCostJuels, + address transmitter, + uint8 resultCode, + bytes response, + bytes err, + bytes callbackReturnData + ); + event RequestStart( + bytes32 indexed requestId, + bytes32 indexed donId, + uint64 indexed subscriptionId, + address subscriptionOwner, + address requestingContract, + address requestInitiator, + bytes data, + uint16 dataVersion, + uint32 callbackGasLimit, + uint96 estimatedTotalCostJuels + ); + event RequestTimedOut(bytes32 indexed requestId); + event SubscriptionCanceled(uint64 indexed subscriptionId, address fundsRecipient, uint256 fundsAmount); + event SubscriptionConsumerAdded(uint64 indexed subscriptionId, address consumer); + event SubscriptionConsumerRemoved(uint64 indexed subscriptionId, address consumer); + event SubscriptionCreated(uint64 indexed subscriptionId, address owner); + event SubscriptionFunded(uint64 indexed subscriptionId, uint256 oldBalance, uint256 newBalance); + event SubscriptionOwnerTransferRequested(uint64 indexed subscriptionId, address from, address to); + event SubscriptionOwnerTransferred(uint64 indexed subscriptionId, address from, address to); + event Unpaused(address account); + + function emitConfigUpdated(Config memory param1) public { + emit ConfigUpdated(param1); + } + + function emitContractProposed( + bytes32 proposedContractSetId, + address proposedContractSetFromAddress, + address proposedContractSetToAddress + ) public { + emit ContractProposed(proposedContractSetId, proposedContractSetFromAddress, proposedContractSetToAddress); + } + + function emitContractUpdated(bytes32 id, address from, address to) public { + emit ContractUpdated(id, from, to); + } + + function emitFundsRecovered(address to, uint256 amount) public { + emit FundsRecovered(to, amount); + } + + function emitOwnershipTransferRequested(address from, address to) public { + emit OwnershipTransferRequested(from, to); + } + + function emitOwnershipTransferred(address from, address to) public { + emit OwnershipTransferred(from, to); + } + + function emitPaused(address account) public { + emit Paused(account); + } + + function emitRequestNotProcessed( + bytes32 requestId, + address coordinator, + address transmitter, + uint8 resultCode + ) public { + emit RequestNotProcessed(requestId, coordinator, transmitter, resultCode); + } + + function emitRequestProcessed( + bytes32 requestId, + uint64 subscriptionId, + uint96 totalCostJuels, + address transmitter, + uint8 resultCode, + bytes memory response, + bytes memory err, + bytes memory callbackReturnData + ) public { + emit RequestProcessed( + requestId, + subscriptionId, + totalCostJuels, + transmitter, + resultCode, + response, + err, + callbackReturnData + ); + } + + function emitRequestStart( + bytes32 requestId, + bytes32 donId, + uint64 subscriptionId, + address subscriptionOwner, + address requestingContract, + address requestInitiator, + bytes memory data, + uint16 dataVersion, + uint32 callbackGasLimit, + uint96 estimatedTotalCostJuels + ) public { + emit RequestStart( + requestId, + donId, + subscriptionId, + subscriptionOwner, + requestingContract, + requestInitiator, + data, + dataVersion, + callbackGasLimit, + estimatedTotalCostJuels + ); + } + + function emitRequestTimedOut(bytes32 requestId) public { + emit RequestTimedOut(requestId); + } + + function emitSubscriptionCanceled(uint64 subscriptionId, address fundsRecipient, uint256 fundsAmount) public { + emit SubscriptionCanceled(subscriptionId, fundsRecipient, fundsAmount); + } + + function emitSubscriptionConsumerAdded(uint64 subscriptionId, address consumer) public { + emit SubscriptionConsumerAdded(subscriptionId, consumer); + } + + function emitSubscriptionConsumerRemoved(uint64 subscriptionId, address consumer) public { + emit SubscriptionConsumerRemoved(subscriptionId, consumer); + } + + function emitSubscriptionCreated(uint64 subscriptionId, address owner) public { + emit SubscriptionCreated(subscriptionId, owner); + } + + function emitSubscriptionFunded(uint64 subscriptionId, uint256 oldBalance, uint256 newBalance) public { + emit SubscriptionFunded(subscriptionId, oldBalance, newBalance); + } + + function emitSubscriptionOwnerTransferRequested(uint64 subscriptionId, address from, address to) public { + emit SubscriptionOwnerTransferRequested(subscriptionId, from, to); + } + + function emitSubscriptionOwnerTransferred(uint64 subscriptionId, address from, address to) public { + emit SubscriptionOwnerTransferred(subscriptionId, from, to); + } + + function emitUnpaused(address account) public { + emit Unpaused(account); + } +} diff --git a/contracts/src/v0.8/functions/dev/v1_0_0/ocr/OCR2Abstract.sol b/contracts/src/v0.8/functions/v1_0_0/ocr/OCR2Abstract.sol similarity index 98% rename from contracts/src/v0.8/functions/dev/v1_0_0/ocr/OCR2Abstract.sol rename to contracts/src/v0.8/functions/v1_0_0/ocr/OCR2Abstract.sol index ea4503a8b7d..09c4a825f44 100644 --- a/contracts/src/v0.8/functions/dev/v1_0_0/ocr/OCR2Abstract.sol +++ b/contracts/src/v0.8/functions/v1_0_0/ocr/OCR2Abstract.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import {ITypeAndVersion} from "../../../../shared/interfaces/ITypeAndVersion.sol"; +import {ITypeAndVersion} from "../../../shared/interfaces/ITypeAndVersion.sol"; abstract contract OCR2Abstract is ITypeAndVersion { // Maximum number of oracles the offchain reporting protocol is designed for diff --git a/contracts/src/v0.8/functions/dev/v1_0_0/ocr/OCR2Base.sol b/contracts/src/v0.8/functions/v1_0_0/ocr/OCR2Base.sol similarity index 95% rename from contracts/src/v0.8/functions/dev/v1_0_0/ocr/OCR2Base.sol rename to contracts/src/v0.8/functions/v1_0_0/ocr/OCR2Base.sol index 013cefe82b9..ba671d4468b 100644 --- a/contracts/src/v0.8/functions/dev/v1_0_0/ocr/OCR2Base.sol +++ b/contracts/src/v0.8/functions/v1_0_0/ocr/OCR2Base.sol @@ -1,18 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import {ConfirmedOwner} from "../../../../shared/access/ConfirmedOwner.sol"; +import {ConfirmedOwner} from "../../../shared/access/ConfirmedOwner.sol"; import {OCR2Abstract} from "./OCR2Abstract.sol"; /** * @notice Onchain verification of reports from the offchain reporting protocol - * @dev THIS CONTRACT HAS NOT GONE THROUGH ANY SECURITY REVIEW. DO NOT USE IN PROD. * @dev For details on its operation, see the offchain reporting protocol design * doc, which refers to this contract as simply the "contract". - * @dev This contract is meant to aid rapid development of new applications based on OCR2. - * However, for actual production contracts, it is expected that most of the logic of this contract - * will be folded directly into the application contract. Inheritance prevents us from doing lots - * of juicy storage layout optimizations, leading to a substantial increase in gas cost. */ abstract contract OCR2Base is ConfirmedOwner, OCR2Abstract { error ReportInvalid(); diff --git a/contracts/test/v0.8/functions/v1/Functions.test.ts b/contracts/test/v0.8/functions/v1/Functions.test.ts index 4de281c0b74..14a68c211b9 100644 --- a/contracts/test/v0.8/functions/v1/Functions.test.ts +++ b/contracts/test/v0.8/functions/v1/Functions.test.ts @@ -17,7 +17,7 @@ let roles: Roles before(async () => { roles = (await getUsers()).roles concreteFunctionsTestHelperFactory = await ethers.getContractFactory( - 'src/v0.8/functions/tests/v1_0_0/testhelpers/FunctionsTestHelper.sol:FunctionsTestHelper', + 'src/v0.8/functions/tests/v1_X/testhelpers/FunctionsTestHelper.sol:FunctionsTestHelper', roles.defaultAccount, ) }) diff --git a/contracts/test/v0.8/functions/v1/FunctionsClient.test.ts b/contracts/test/v0.8/functions/v1/FunctionsClient.test.ts index dad78769b75..826953fb2c4 100644 --- a/contracts/test/v0.8/functions/v1/FunctionsClient.test.ts +++ b/contracts/test/v0.8/functions/v1/FunctionsClient.test.ts @@ -178,7 +178,7 @@ describe('Faulty Functions Client', () => { it('can complete requests with an empty callback', async () => { const clientWithEmptyCallbackTestHelperFactory = await ethers.getContractFactory( - 'src/v0.8/functions/tests/v1_0_0/testhelpers/FunctionsClientWithEmptyCallback.sol:FunctionsClientWithEmptyCallback', + 'src/v0.8/functions/tests/v1_X/testhelpers/FunctionsClientWithEmptyCallback.sol:FunctionsClientWithEmptyCallback', roles.consumer, ) diff --git a/contracts/test/v0.8/functions/v1/utils.ts b/contracts/test/v0.8/functions/v1/utils.ts index 98f6143dee3..0ed82eb104e 100644 --- a/contracts/test/v0.8/functions/v1/utils.ts +++ b/contracts/test/v0.8/functions/v1/utils.ts @@ -52,7 +52,7 @@ export const encodeReport = async ( offchainMetadata: string, ) => { const functionsResponse = await ethers.getContractFactory( - 'src/v0.8/functions/dev/v1_0_0/FunctionsCoordinator.sol:FunctionsCoordinator', + 'src/v0.8/functions/dev/v1_X/FunctionsCoordinator.sol:FunctionsCoordinator', ) const onchainMetadataBytes = functionsResponse.interface._abiCoder.encode( [ @@ -130,19 +130,19 @@ export async function setupRolesAndFactories(): Promise<{ }> { const roles = (await getUsers()).roles const functionsRouterFactory = await ethers.getContractFactory( - 'src/v0.8/functions/dev/v1_0_0/FunctionsRouter.sol:FunctionsRouter', + 'src/v0.8/functions/dev/v1_X/FunctionsRouter.sol:FunctionsRouter', roles.defaultAccount, ) const functionsCoordinatorFactory = await ethers.getContractFactory( - 'src/v0.8/functions/tests/v1_0_0/testhelpers/FunctionsCoordinatorTestHelper.sol:FunctionsCoordinatorTestHelper', + 'src/v0.8/functions/tests/v1_X/testhelpers/FunctionsCoordinatorTestHelper.sol:FunctionsCoordinatorTestHelper', roles.defaultAccount, ) const accessControlFactory = await ethers.getContractFactory( - 'src/v0.8/functions/dev/v1_0_0/accessControl/TermsOfServiceAllowList.sol:TermsOfServiceAllowList', + 'src/v0.8/functions/dev/v1_X/accessControl/TermsOfServiceAllowList.sol:TermsOfServiceAllowList', roles.defaultAccount, ) const clientTestHelperFactory = await ethers.getContractFactory( - 'src/v0.8/functions/tests/v1_0_0/testhelpers/FunctionsClientTestHelper.sol:FunctionsClientTestHelper', + 'src/v0.8/functions/tests/v1_X/testhelpers/FunctionsClientTestHelper.sol:FunctionsClientTestHelper', roles.consumer, ) const linkTokenFactory = await ethers.getContractFactory( diff --git a/core/gethwrappers/functions/generation/generated-wrapper-dependency-versions-do-not-edit.txt b/core/gethwrappers/functions/generation/generated-wrapper-dependency-versions-do-not-edit.txt index 6b77b889e3b..0ea738ec38d 100644 --- a/core/gethwrappers/functions/generation/generated-wrapper-dependency-versions-do-not-edit.txt +++ b/core/gethwrappers/functions/generation/generated-wrapper-dependency-versions-do-not-edit.txt @@ -1,14 +1,14 @@ GETH_VERSION: 1.12.0 -functions: ../../../contracts/solc/v0.8.19/functions/v1_0_0/FunctionsRequest.abi ../../../contracts/solc/v0.8.19/functions/v1_0_0/FunctionsRequest.bin 3c972870b0afeb6d73a29ebb182f24956a2cebb127b21c4f867d1ecf19a762db -functions_allow_list: ../../../contracts/solc/v0.8.19/functions/v1_0_0/TermsOfServiceAllowList.abi ../../../contracts/solc/v0.8.19/functions/v1_0_0/TermsOfServiceAllowList.bin b2697ad4dfece903a1d34028826a017fa445eb3cd984006f1734fa9d47836ca0 +functions: ../../../contracts/solc/v0.8.19/functions/v1_X/FunctionsRequest.abi ../../../contracts/solc/v0.8.19/functions/v1_X/FunctionsRequest.bin 3c972870b0afeb6d73a29ebb182f24956a2cebb127b21c4f867d1ecf19a762db +functions_allow_list: ../../../contracts/solc/v0.8.19/functions/v1_X/TermsOfServiceAllowList.abi ../../../contracts/solc/v0.8.19/functions/v1_X/TermsOfServiceAllowList.bin b2697ad4dfece903a1d34028826a017fa445eb3cd984006f1734fa9d47836ca0 functions_billing_registry_events_mock: ../../../contracts/solc/v0.8.6/functions/v0_0_0/FunctionsBillingRegistryEventsMock.abi ../../../contracts/solc/v0.8.6/functions/v0_0_0/FunctionsBillingRegistryEventsMock.bin 50deeb883bd9c3729702be335c0388f9d8553bab4be5e26ecacac496a89e2b77 -functions_client: ../../../contracts/solc/v0.8.19/functions/v1_0_0/FunctionsClient.abi ../../../contracts/solc/v0.8.19/functions/v1_0_0/FunctionsClient.bin 2368f537a04489c720a46733f8596c4fc88a31062ecfa966d05f25dd98608aca -functions_client_example: ../../../contracts/solc/v0.8.19/functions/v1_0_0/FunctionsClientExample.abi ../../../contracts/solc/v0.8.19/functions/v1_0_0/FunctionsClientExample.bin abf32e69f268f40e8530eb8d8e96bf310b798a4c0049a58022d9d2fb527b601b -functions_coordinator: ../../../contracts/solc/v0.8.19/functions/v1_0_0/FunctionsCoordinator.abi ../../../contracts/solc/v0.8.19/functions/v1_0_0/FunctionsCoordinator.bin 21bd322caf977c4802d2c17419b57487cca438c7c5fafc52a9a9e1c9f4a72289 -functions_load_test_client: ../../../contracts/solc/v0.8.19/functions/v1_0_0/FunctionsLoadTestClient.abi ../../../contracts/solc/v0.8.19/functions/v1_0_0/FunctionsLoadTestClient.bin c8dbbd5ebb34435800d6674700068837c3a252db60046a14b0e61e829db517de +functions_client: ../../../contracts/solc/v0.8.19/functions/v1_X/FunctionsClient.abi ../../../contracts/solc/v0.8.19/functions/v1_X/FunctionsClient.bin 2368f537a04489c720a46733f8596c4fc88a31062ecfa966d05f25dd98608aca +functions_client_example: ../../../contracts/solc/v0.8.19/functions/v1_X/FunctionsClientExample.abi ../../../contracts/solc/v0.8.19/functions/v1_X/FunctionsClientExample.bin abf32e69f268f40e8530eb8d8e96bf310b798a4c0049a58022d9d2fb527b601b +functions_coordinator: ../../../contracts/solc/v0.8.19/functions/v1_X/FunctionsCoordinator.abi ../../../contracts/solc/v0.8.19/functions/v1_X/FunctionsCoordinator.bin 21bd322caf977c4802d2c17419b57487cca438c7c5fafc52a9a9e1c9f4a72289 +functions_load_test_client: ../../../contracts/solc/v0.8.19/functions/v1_X/FunctionsLoadTestClient.abi ../../../contracts/solc/v0.8.19/functions/v1_X/FunctionsLoadTestClient.bin c8dbbd5ebb34435800d6674700068837c3a252db60046a14b0e61e829db517de functions_oracle_events_mock: ../../../contracts/solc/v0.8.6/functions/v0_0_0/FunctionsOracleEventsMock.abi ../../../contracts/solc/v0.8.6/functions/v0_0_0/FunctionsOracleEventsMock.bin 3ca70f966f8fe751987f0ccb50bebb6aa5be77e4a9f835d1ae99e0e9bfb7d52c -functions_router: ../../../contracts/solc/v0.8.19/functions/v1_0_0/FunctionsRouter.abi ../../../contracts/solc/v0.8.19/functions/v1_0_0/FunctionsRouter.bin 9dedd3a36043605fd9bedf821e7ec5b4281a5c7ae2e4a1955f37aff8ba13519f -functions_v1_events_mock: ../../../contracts/solc/v0.8.19/functions/v1_0_0/FunctionsV1EventsMock.abi ../../../contracts/solc/v0.8.19/functions/v1_0_0/FunctionsV1EventsMock.bin 0f0ba42e0cc33c7abc8b8fd4fdfce903748a169886dd5f16cfdd56e75bcf708d +functions_router: ../../../contracts/solc/v0.8.19/functions/v1_X/FunctionsRouter.abi ../../../contracts/solc/v0.8.19/functions/v1_X/FunctionsRouter.bin 9dedd3a36043605fd9bedf821e7ec5b4281a5c7ae2e4a1955f37aff8ba13519f +functions_v1_events_mock: ../../../contracts/solc/v0.8.19/functions/v1_X/FunctionsV1EventsMock.abi ../../../contracts/solc/v0.8.19/functions/v1_X/FunctionsV1EventsMock.bin 0f0ba42e0cc33c7abc8b8fd4fdfce903748a169886dd5f16cfdd56e75bcf708d ocr2dr: ../../../contracts/solc/v0.8.6/functions/v0_0_0/Functions.abi ../../../contracts/solc/v0.8.6/functions/v0_0_0/Functions.bin d9a794b33f47cc57563d216f7cf3a612309fc3062356a27e30005cf1d59e449d ocr2dr_client: ../../../contracts/solc/v0.8.6/functions/v0_0_0/FunctionsClient.abi ../../../contracts/solc/v0.8.6/functions/v0_0_0/FunctionsClient.bin 84aa63f9dbc5c7eac240db699b09e613ca4c6cd56dab10bdc25b02461b717e21 ocr2dr_client_example: ../../../contracts/solc/v0.8.6/functions/v0_0_0/FunctionsClientExample.abi ../../../contracts/solc/v0.8.6/functions/v0_0_0/FunctionsClientExample.bin a978d9b52a5a2da19eef0975979de256e62980a0cfb3084fe6d66a351b4ef534 diff --git a/core/gethwrappers/functions/go_generate.go b/core/gethwrappers/functions/go_generate.go index 2cb6c4ffaec..9c389a699e4 100644 --- a/core/gethwrappers/functions/go_generate.go +++ b/core/gethwrappers/functions/go_generate.go @@ -12,11 +12,11 @@ package gethwrappers //go:generate go run ../generation/generate/wrap.go ../../../contracts/solc/v0.8.6/functions/v0_0_0/FunctionsBillingRegistryWithInit.abi ../../../contracts/solc/v0.8.6/functions/v0_0_0/FunctionsBillingRegistryWithInit.bin OCR2DRRegistry ocr2dr_registry // Version 1 (Mainnet Preview) -//go:generate go run ../generation/generate/wrap.go ../../../contracts/solc/v0.8.19/functions/v1_0_0/FunctionsRequest.abi ../../../contracts/solc/v0.8.19/functions/v1_0_0/FunctionsRequest.bin Functions functions -//go:generate go run ../generation/generate/wrap.go ../../../contracts/solc/v0.8.19/functions/v1_0_0/FunctionsClient.abi ../../../contracts/solc/v0.8.19/functions/v1_0_0/FunctionsClient.bin FunctionsClient functions_client -//go:generate go run ../generation/generate/wrap.go ../../../contracts/solc/v0.8.19/functions/v1_0_0/FunctionsClientExample.abi ../../../contracts/solc/v0.8.19/functions/v1_0_0/FunctionsClientExample.bin FunctionsClientExample functions_client_example -//go:generate go run ../generation/generate/wrap.go ../../../contracts/solc/v0.8.19/functions/v1_0_0/FunctionsLoadTestClient.abi ../../../contracts/solc/v0.8.19/functions/v1_0_0/FunctionsLoadTestClient.bin FunctionsLoadTestClient functions_load_test_client -//go:generate go run ../generation/generate/wrap.go ../../../contracts/solc/v0.8.19/functions/v1_0_0/FunctionsCoordinator.abi ../../../contracts/solc/v0.8.19/functions/v1_0_0/FunctionsCoordinator.bin FunctionsCoordinator functions_coordinator -//go:generate go run ../generation/generate/wrap.go ../../../contracts/solc/v0.8.19/functions/v1_0_0/FunctionsRouter.abi ../../../contracts/solc/v0.8.19/functions/v1_0_0/FunctionsRouter.bin FunctionsRouter functions_router -//go:generate go run ../generation/generate/wrap.go ../../../contracts/solc/v0.8.19/functions/v1_0_0/TermsOfServiceAllowList.abi ../../../contracts/solc/v0.8.19/functions/v1_0_0/TermsOfServiceAllowList.bin TermsOfServiceAllowList functions_allow_list -//go:generate go run ../generation/generate/wrap.go ../../../contracts/solc/v0.8.19/functions/v1_0_0/FunctionsV1EventsMock.abi ../../../contracts/solc/v0.8.19/functions/v1_0_0/FunctionsV1EventsMock.bin FunctionsV1EventsMock functions_v1_events_mock +//go:generate go run ../generation/generate/wrap.go ../../../contracts/solc/v0.8.19/functions/v1_X/FunctionsRequest.abi ../../../contracts/solc/v0.8.19/functions/v1_X/FunctionsRequest.bin Functions functions +//go:generate go run ../generation/generate/wrap.go ../../../contracts/solc/v0.8.19/functions/v1_X/FunctionsClient.abi ../../../contracts/solc/v0.8.19/functions/v1_X/FunctionsClient.bin FunctionsClient functions_client +//go:generate go run ../generation/generate/wrap.go ../../../contracts/solc/v0.8.19/functions/v1_X/FunctionsClientExample.abi ../../../contracts/solc/v0.8.19/functions/v1_X/FunctionsClientExample.bin FunctionsClientExample functions_client_example +//go:generate go run ../generation/generate/wrap.go ../../../contracts/solc/v0.8.19/functions/v1_X/FunctionsLoadTestClient.abi ../../../contracts/solc/v0.8.19/functions/v1_X/FunctionsLoadTestClient.bin FunctionsLoadTestClient functions_load_test_client +//go:generate go run ../generation/generate/wrap.go ../../../contracts/solc/v0.8.19/functions/v1_X/FunctionsCoordinator.abi ../../../contracts/solc/v0.8.19/functions/v1_X/FunctionsCoordinator.bin FunctionsCoordinator functions_coordinator +//go:generate go run ../generation/generate/wrap.go ../../../contracts/solc/v0.8.19/functions/v1_X/FunctionsRouter.abi ../../../contracts/solc/v0.8.19/functions/v1_X/FunctionsRouter.bin FunctionsRouter functions_router +//go:generate go run ../generation/generate/wrap.go ../../../contracts/solc/v0.8.19/functions/v1_X/TermsOfServiceAllowList.abi ../../../contracts/solc/v0.8.19/functions/v1_X/TermsOfServiceAllowList.bin TermsOfServiceAllowList functions_allow_list +//go:generate go run ../generation/generate/wrap.go ../../../contracts/solc/v0.8.19/functions/v1_X/FunctionsV1EventsMock.abi ../../../contracts/solc/v0.8.19/functions/v1_X/FunctionsV1EventsMock.bin FunctionsV1EventsMock functions_v1_events_mock diff --git a/integration-tests/load/functions/README.md b/integration-tests/load/functions/README.md index 65a87b897ac..bcdbe92d524 100644 --- a/integration-tests/load/functions/README.md +++ b/integration-tests/load/functions/README.md @@ -18,7 +18,7 @@ All tests are split by network and in 3 groups: - Secrets decoding payload only - Realistic payload with args/http/secrets -Load test client is [here](../../../contracts/src/v0.8/functions/tests/v1_0_0/testhelpers/FunctionsLoadTestClient.sol) +Load test client is [here](../../../contracts/src/v0.8/functions/tests/v1_X/testhelpers/FunctionsLoadTestClient.sol) Load is controlled with 2 params: - RPS