From f6088eca1d36ee0aa088d996bb5b6bbfc104c226 Mon Sep 17 00:00:00 2001 From: Kodey Kilday-Thomas Date: Wed, 18 Dec 2024 12:21:53 +0000 Subject: [PATCH 01/11] fix: point to 1.3.0 ocr --- .../v1_3_0_zksync/FunctionsCoordinator.sol | 227 ++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 contracts/src/v0.8/functions/v1_3_0_zksync/FunctionsCoordinator.sol diff --git a/contracts/src/v0.8/functions/v1_3_0_zksync/FunctionsCoordinator.sol b/contracts/src/v0.8/functions/v1_3_0_zksync/FunctionsCoordinator.sol new file mode 100644 index 00000000000..66802cc4923 --- /dev/null +++ b/contracts/src/v0.8/functions/v1_3_0_zksync/FunctionsCoordinator.sol @@ -0,0 +1,227 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IFunctionsCoordinator} from "../v1_0_0/interfaces/IFunctionsCoordinator.sol"; +import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol"; + +import {FunctionsBilling, FunctionsBillingConfig} from "./FunctionsBilling.sol"; +import {OCR2Base} from "../v1_3_0/ocr/OCR2Base.sol"; +import {FunctionsResponse} from "../v1_0_0/libraries/FunctionsResponse.sol"; + +/// @title Functions Coordinator contract +/// @notice Contract that nodes of a Decentralized Oracle Network (DON) interact with +contract FunctionsCoordinator is OCR2Base, IFunctionsCoordinator, FunctionsBilling { + using FunctionsResponse for FunctionsResponse.RequestMeta; + using FunctionsResponse for FunctionsResponse.Commitment; + using FunctionsResponse for FunctionsResponse.FulfillResult; + + /// @inheritdoc ITypeAndVersion + // solhint-disable-next-line chainlink-solidity/all-caps-constant-storage-variables + string public constant override typeAndVersion = "Functions Coordinator v1.3.0"; + + event OracleRequest( + bytes32 indexed requestId, + address indexed requestingContract, + address requestInitiator, + uint64 subscriptionId, + address subscriptionOwner, + bytes data, + uint16 dataVersion, + bytes32 flags, + uint64 callbackGasLimit, + FunctionsResponse.Commitment commitment + ); + event OracleResponse(bytes32 indexed requestId, address transmitter); + + error InconsistentReportData(); + error EmptyPublicKey(); + error UnauthorizedPublicKeyChange(); + + bytes private s_donPublicKey; + bytes private s_thresholdPublicKey; + + constructor( + address router, + FunctionsBillingConfig memory config, + address linkToNativeFeed, + address linkToUsdFeed + ) OCR2Base() FunctionsBilling(router, config, linkToNativeFeed, linkToUsdFeed) {} + + /// @inheritdoc IFunctionsCoordinator + function getThresholdPublicKey() external view override returns (bytes memory) { + if (s_thresholdPublicKey.length == 0) { + revert EmptyPublicKey(); + } + return s_thresholdPublicKey; + } + + /// @inheritdoc IFunctionsCoordinator + function setThresholdPublicKey(bytes calldata thresholdPublicKey) external override onlyOwner { + if (thresholdPublicKey.length == 0) { + revert EmptyPublicKey(); + } + s_thresholdPublicKey = thresholdPublicKey; + } + + /// @inheritdoc IFunctionsCoordinator + function getDONPublicKey() external view override returns (bytes memory) { + if (s_donPublicKey.length == 0) { + revert EmptyPublicKey(); + } + return s_donPublicKey; + } + + /// @inheritdoc IFunctionsCoordinator + function setDONPublicKey(bytes calldata donPublicKey) external override onlyOwner { + if (donPublicKey.length == 0) { + revert EmptyPublicKey(); + } + s_donPublicKey = donPublicKey; + } + + /// @dev check if node is in current transmitter list + function _isTransmitter(address node) internal view returns (bool) { + // Bounded by "maxNumOracles" on OCR2Abstract.sol + for (uint256 i = 0; i < s_transmitters.length; ++i) { + if (s_transmitters[i] == node) { + return true; + } + } + return false; + } + + /// @inheritdoc IFunctionsCoordinator + function startRequest( + FunctionsResponse.RequestMeta calldata request + ) external override onlyRouter returns (FunctionsResponse.Commitment memory commitment) { + uint72 operationFee; + (commitment, operationFee) = _startBilling(request); + + emit OracleRequest( + commitment.requestId, + request.requestingContract, + // solhint-disable-next-line avoid-tx-origin + tx.origin, + request.subscriptionId, + request.subscriptionOwner, + request.data, + request.dataVersion, + request.flags, + request.callbackGasLimit, + FunctionsResponse.Commitment({ + coordinator: commitment.coordinator, + client: commitment.client, + subscriptionId: commitment.subscriptionId, + callbackGasLimit: commitment.callbackGasLimit, + estimatedTotalCostJuels: commitment.estimatedTotalCostJuels, + timeoutTimestamp: commitment.timeoutTimestamp, + requestId: commitment.requestId, + donFee: commitment.donFee, + gasOverheadBeforeCallback: commitment.gasOverheadBeforeCallback, + gasOverheadAfterCallback: commitment.gasOverheadAfterCallback, + // The following line is done to use the Coordinator's operationFee in place of the Router's operation fee + // With this in place the Router.adminFee must be set to 0 in the Router. + adminFee: operationFee + }) + ); + + return commitment; + } + + /// @dev DON fees are pooled together. If the OCR configuration is going to change, these need to be distributed. + function _beforeSetConfig(uint8 /* _f */, bytes memory /* _onchainConfig */) internal override { + if (_getTransmitters().length > 0) { + _disperseFeePool(); + } + } + + /// @dev Used by FunctionsBilling.sol + function _getTransmitters() internal view override returns (address[] memory) { + return s_transmitters; + } + + function _beforeTransmit( + bytes calldata report + ) internal view override returns (bool shouldStop, DecodedReport memory decodedReport) { + ( + bytes32[] memory requestIds, + bytes[] memory results, + bytes[] memory errors, + bytes[] memory onchainMetadata, + bytes[] memory offchainMetadata + ) = abi.decode(report, (bytes32[], bytes[], bytes[], bytes[], bytes[])); + uint256 numberOfFulfillments = uint8(requestIds.length); + + if ( + numberOfFulfillments == 0 || + numberOfFulfillments != results.length || + numberOfFulfillments != errors.length || + numberOfFulfillments != onchainMetadata.length || + numberOfFulfillments != offchainMetadata.length + ) { + revert ReportInvalid("Fields must be equal length"); + } + + for (uint256 i = 0; i < numberOfFulfillments; ++i) { + if (_isExistingRequest(requestIds[i])) { + // If there is an existing request, validate report + // Leave shouldStop to default, false + break; + } + if (i == numberOfFulfillments - 1) { + // If the last fulfillment on the report does not exist, then all are duplicates + // Indicate that it's safe to stop to save on the gas of validating the report + shouldStop = true; + } + } + + return ( + shouldStop, + DecodedReport({ + requestIds: requestIds, + results: results, + errors: errors, + onchainMetadata: onchainMetadata, + offchainMetadata: offchainMetadata + }) + ); + } + + /// @dev Report hook called within OCR2Base.sol + function _report(DecodedReport memory decodedReport) internal override { + uint256 numberOfFulfillments = uint8(decodedReport.requestIds.length); + + // Bounded by "MaxRequestBatchSize" on the Job's ReportingPluginConfig + for (uint256 i = 0; i < numberOfFulfillments; ++i) { + FunctionsResponse.FulfillResult result = FunctionsResponse.FulfillResult( + _fulfillAndBill( + decodedReport.requestIds[i], + decodedReport.results[i], + decodedReport.errors[i], + decodedReport.onchainMetadata[i], + decodedReport.offchainMetadata[i] + ) + ); + + // Emit on successfully processing the fulfillment + // In these two fulfillment results the user has been charged + // Otherwise, the DON will re-try + if ( + result == FunctionsResponse.FulfillResult.FULFILLED || + result == FunctionsResponse.FulfillResult.USER_CALLBACK_ERROR + ) { + emit OracleResponse(decodedReport.requestIds[i], msg.sender); + } + } + } + + /// @dev Used in FunctionsBilling.sol + function _onlyOwner() internal view override { + _validateOwnership(); + } + + /// @dev Used in FunctionsBilling.sol + function _owner() internal view override returns (address owner) { + return this.owner(); + } +} From b9a70b8f8e86a5e479dbf99f738135f4089c476a Mon Sep 17 00:00:00 2001 From: Kodey Kilday-Thomas Date: Tue, 10 Dec 2024 11:46:44 +0000 Subject: [PATCH 02/11] chore: add functions router zksync to solhint ignore --- contracts/.solhintignore | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/.solhintignore b/contracts/.solhintignore index 7ae5b10d150..ce7dfa800ba 100644 --- a/contracts/.solhintignore +++ b/contracts/.solhintignore @@ -31,6 +31,7 @@ # Ignore Functions v1.0.0 code that was frozen after audit ./src/v0.8/functions/v1_0_0 +./src/v0.8/functions/v1_3_0_zksync/FunctionsRouter.sol # Test helpers ./src/v0.8/vrf/testhelpers From bbe5bd90c4e18267ed6d210ef784c37db33923ae Mon Sep 17 00:00:00 2001 From: Kodey Kilday-Thomas Date: Tue, 10 Dec 2024 11:52:30 +0000 Subject: [PATCH 03/11] chore: add changeset --- contracts/.changeset/loud-rabbits-chew.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 contracts/.changeset/loud-rabbits-chew.md diff --git a/contracts/.changeset/loud-rabbits-chew.md b/contracts/.changeset/loud-rabbits-chew.md new file mode 100644 index 00000000000..747c715881c --- /dev/null +++ b/contracts/.changeset/loud-rabbits-chew.md @@ -0,0 +1,5 @@ +--- +'@chainlink/contracts': patch +--- + +Added ZKSync support for Functions From 17fa5a61c6c6e51cae4ba0311a96106727370f24 Mon Sep 17 00:00:00 2001 From: Kodey Kilday-Thomas Date: Wed, 11 Dec 2024 12:36:27 +0000 Subject: [PATCH 04/11] chore: removed not modified contracts --- .../v1_3_0_zksync/FunctionsRouter.sol | 584 ++++++++++++++++++ 1 file changed, 584 insertions(+) create mode 100644 contracts/src/v0.8/functions/v1_3_0_zksync/FunctionsRouter.sol diff --git a/contracts/src/v0.8/functions/v1_3_0_zksync/FunctionsRouter.sol b/contracts/src/v0.8/functions/v1_3_0_zksync/FunctionsRouter.sol new file mode 100644 index 00000000000..862817476be --- /dev/null +++ b/contracts/src/v0.8/functions/v1_3_0_zksync/FunctionsRouter.sol @@ -0,0 +1,584 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol"; +import {IFunctionsRouter} from "../v1_0_0/interfaces/IFunctionsRouter.sol"; +import {IFunctionsCoordinator} from "../v1_0_0/interfaces/IFunctionsCoordinator.sol"; +import {IAccessController} from "../../shared/interfaces/IAccessController.sol"; +import {GAS_BOUND_CALLER, IGasBoundCaller} from "./interfaces/zksync/IGasBoundCaller.sol"; + +import {FunctionsSubscriptions} from "../v1_0_0/FunctionsSubscriptions.sol"; +import {FunctionsResponse} from "../v1_0_0/libraries/FunctionsResponse.sol"; +import {ConfirmedOwner} from "../../shared/access/ConfirmedOwner.sol"; + +import {SafeCast} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/math/SafeCast.sol"; +import {Pausable} from "../../vendor/openzeppelin-solidity/v4.8.3/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.3.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; + uint256 g1 = gasleft(); + + 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) + } + } + + // allocate return data memory ahead of time + bytes memory returnData = new bytes(MAX_CALLBACK_RETURN_BYTES); + + // solhint-disable-next-line avoid-low-level-calls + (success, returnData) = GAS_BOUND_CALLER.delegatecall{gas: callbackGasLimit}( + abi.encodeWithSelector(IGasBoundCaller.gasBoundCall.selector, client, callbackGasLimit, encodedCallback) + ); + + uint256 pubdataGasSpent; + if (success) { + (, pubdataGasSpent) = abi.decode(returnData, (bytes, uint256)); + } + gasUsed = g1 - gasleft() + pubdataGasSpent; + + 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(); + } +} From 96cf69c37d18538f12825a0d11fda81ea1900ac5 Mon Sep 17 00:00:00 2001 From: Kodey Kilday-Thomas Date: Wed, 18 Dec 2024 12:02:44 +0000 Subject: [PATCH 05/11] fix: remove l1 fee logic --- .../v1_3_0_zksync/FunctionsBilling.sol | 437 ++++++++++++++++++ 1 file changed, 437 insertions(+) create mode 100644 contracts/src/v0.8/functions/v1_3_0_zksync/FunctionsBilling.sol diff --git a/contracts/src/v0.8/functions/v1_3_0_zksync/FunctionsBilling.sol b/contracts/src/v0.8/functions/v1_3_0_zksync/FunctionsBilling.sol new file mode 100644 index 00000000000..83067d2deb8 --- /dev/null +++ b/contracts/src/v0.8/functions/v1_3_0_zksync/FunctionsBilling.sol @@ -0,0 +1,437 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IFunctionsSubscriptions} from "../v1_0_0/interfaces/IFunctionsSubscriptions.sol"; +import {AggregatorV3Interface} from "../../shared/interfaces/AggregatorV3Interface.sol"; +import {IFunctionsBilling, FunctionsBillingConfig} from "../v1_0_0/interfaces/IFunctionsBilling.sol"; + +import {Routable} from "../v1_0_0/Routable.sol"; +import {FunctionsResponse} from "../v1_0_0/libraries/FunctionsResponse.sol"; + +import {SafeCast} from "../../vendor/openzeppelin-solidity/v4.8.3/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 + + event RequestBilled( + bytes32 indexed requestId, + uint96 juelsPerGas, + uint256 l1FeeShareWei, + uint96 callbackCostJuels, + uint72 donFeeJuels, + uint72 adminFeeJuels, + uint72 operationFeeJuels + ); + + // ================================================================ + // | Request Commitment state | + // ================================================================ + + mapping(bytes32 requestId => bytes32 commitmentHash) private s_requestCommitments; + + event CommitmentDeleted(bytes32 requestId); + + FunctionsBillingConfig private s_config; + + event ConfigUpdated(FunctionsBillingConfig config); + + error UnsupportedRequestDataVersion(); + error InsufficientBalance(); + error InvalidSubscription(); + error UnauthorizedSender(); + error MustBeSubOwner(address owner); + error InvalidLinkWeiPrice(int256 linkWei); + error InvalidUsdLinkPrice(int256 usdLink); + 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; + AggregatorV3Interface private s_linkToUsdFeed; + + // ================================================================ + // | Initialization | + // ================================================================ + constructor( + address router, + FunctionsBillingConfig memory config, + address linkToNativeFeed, + address linkToUsdFeed + ) Routable(router) { + s_linkToNativeFeed = AggregatorV3Interface(linkToNativeFeed); + s_linkToUsdFeed = AggregatorV3Interface(linkToUsdFeed); + + updateConfig(config); + } + + // ================================================================ + // | Configuration | + // ================================================================ + + /// @notice Gets the Chainlink Coordinator's billing configuration + /// @return config + function getConfig() external view returns (FunctionsBillingConfig memory) { + return s_config; + } + + /// @notice Sets the Chainlink Coordinator's billing configuration + /// @param config - See the contents of the FunctionsBillingConfig struct in IFunctionsBilling.sol for more information + function updateConfig(FunctionsBillingConfig memory config) public { + _onlyOwner(); + + s_config = config; + emit ConfigUpdated(config); + } + + // ================================================================ + // | Fee Calculation | + // ================================================================ + + /// @inheritdoc IFunctionsBilling + function getDONFeeJuels(bytes memory /* requestData */) public view override returns (uint72) { + // s_config.donFee is in cents of USD. Get Juel amount then convert to dollars. + return SafeCast.toUint72(_getJuelsFromUsd(s_config.donFeeCentsUsd) / 100); + } + + /// @inheritdoc IFunctionsBilling + function getOperationFeeJuels() public view override returns (uint72) { + // s_config.donFee is in cents of USD. Get Juel amount then convert to dollars. + return SafeCast.toUint72(_getJuelsFromUsd(s_config.operationFeeCentsUsd) / 100); + } + + /// @inheritdoc IFunctionsBilling + function getAdminFeeJuels() public view override returns (uint72) { + return _getRouter().getAdminFee(); + } + + /// @inheritdoc IFunctionsBilling + function getWeiPerUnitLink() public view returns (uint256) { + (, int256 weiPerUnitLink, , uint256 timestamp, ) = s_linkToNativeFeed.latestRoundData(); + // solhint-disable-next-line not-rely-on-time + if (s_config.feedStalenessSeconds < block.timestamp - timestamp && s_config.feedStalenessSeconds > 0) { + return s_config.fallbackNativePerUnitLink; + } + if (weiPerUnitLink <= 0) { + revert InvalidLinkWeiPrice(weiPerUnitLink); + } + return uint256(weiPerUnitLink); + } + + function _getJuelsFromWei(uint256 amountWei) private view returns (uint96) { + // (1e18 juels/link) * wei / (wei/link) = juels + // There are only 1e9*1e18 = 1e27 juels in existence, should not exceed uint96 (2^96 ~ 7e28) + return SafeCast.toUint96((1e18 * amountWei) / getWeiPerUnitLink()); + } + + /// @inheritdoc IFunctionsBilling + function getUsdPerUnitLink() public view returns (uint256, uint8) { + (, int256 usdPerUnitLink, , uint256 timestamp, ) = s_linkToUsdFeed.latestRoundData(); + // solhint-disable-next-line not-rely-on-time + if (s_config.feedStalenessSeconds < block.timestamp - timestamp && s_config.feedStalenessSeconds > 0) { + return (s_config.fallbackUsdPerUnitLink, s_config.fallbackUsdPerUnitLinkDecimals); + } + if (usdPerUnitLink <= 0) { + revert InvalidUsdLinkPrice(usdPerUnitLink); + } + return (uint256(usdPerUnitLink), s_linkToUsdFeed.decimals()); + } + + function _getJuelsFromUsd(uint256 amountUsd) private view returns (uint96) { + (uint256 usdPerLink, uint8 decimals) = getUsdPerUnitLink(); + // (usd) * (10**18 juels/link) * (10**decimals) / (link / usd) = juels + // There are only 1e9*1e18 = 1e27 juels in existence, should not exceed uint96 (2^96 ~ 7e28) + return SafeCast.toUint96((amountUsd * 10 ** (18 + decimals)) / usdPerLink); + } + + // ================================================================ + // | 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 = getAdminFeeJuels(); + uint72 donFee = getDONFeeJuels(data); + uint72 operationFee = getOperationFeeJuels(); + return _calculateCostEstimate(callbackGasLimit, gasPriceWei, donFee, adminFee, operationFee); + } + + /// @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 donFeeJuels, + uint72 adminFeeJuels, + uint72 operationFeeJuels + ) internal view returns (uint96) { + // If gas price is less than the minimum fulfillment gas price, override to using the minimum + if (gasPriceWei < s_config.minimumEstimateGasPriceWei) { + gasPriceWei = s_config.minimumEstimateGasPriceWei; + } + + 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 + + uint256 executionGas = s_config.gasOverheadBeforeCallback + s_config.gasOverheadAfterCallback + callbackGasLimit; + uint96 estimatedGasReimbursementJuels = _getJuelsFromWei(gasPriceWithOverestimation * executionGas); + + uint96 feesJuels = uint96(donFeeJuels) + uint96(adminFeeJuels) + uint96(operationFeeJuels); + + return estimatedGasReimbursementJuels + feesJuels; + } + + // ================================================================ + // | 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, uint72 operationFee) { + // Nodes should support all past versions of the structure + if (request.dataVersion > s_config.maxSupportedRequestDataVersion) { + revert UnsupportedRequestDataVersion(); + } + + uint72 donFee = getDONFeeJuels(request.data); + operationFee = getOperationFeeJuels(); + uint96 estimatedTotalCostJuels = _calculateCostEstimate( + request.callbackGasLimit, + tx.gasprice, + donFee, + request.adminFee, + operationFee + ); + + // Check that subscription can afford the estimated cost + if ((request.availableBalance) < estimatedTotalCostJuels) { + revert InsufficientBalance(); + } + + uint32 timeoutTimestamp = uint32(block.timestamp + s_config.requestTimeoutSeconds); + bytes32 requestId = keccak256( + abi.encode( + address(this), + request.requestingContract, + request.subscriptionId, + request.initiatedRequests + 1, + keccak256(request.data), + request.dataVersion, + request.callbackGasLimit, + estimatedTotalCostJuels, + timeoutTimestamp, + // solhint-disable-next-line avoid-tx-origin + tx.origin + ) + ); + + commitment = FunctionsResponse.Commitment({ + adminFee: request.adminFee, + coordinator: address(this), + client: request.requestingContract, + subscriptionId: request.subscriptionId, + callbackGasLimit: request.callbackGasLimit, + estimatedTotalCostJuels: estimatedTotalCostJuels, + timeoutTimestamp: timeoutTimestamp, + requestId: requestId, + donFee: donFee, + gasOverheadBeforeCallback: s_config.gasOverheadBeforeCallback, + gasOverheadAfterCallback: s_config.gasOverheadAfterCallback + }); + + s_requestCommitments[requestId] = keccak256(abi.encode(commitment)); + + return (commitment, operationFee); + } + + /// @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 + /// @param reportBatchSize the number of fulfillments in the transmitter's report + /// @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 */, + uint8 reportBatchSize + ) internal returns (FunctionsResponse.FulfillResult) { + FunctionsResponse.Commitment memory commitment = abi.decode(onchainMetadata, (FunctionsResponse.Commitment)); + + uint256 gasOverheadWei = (commitment.gasOverheadBeforeCallback + commitment.gasOverheadAfterCallback) * tx.gasprice; + + // Gas overhead without callback + uint96 gasOverheadJuels = _getJuelsFromWei(gasOverheadWei); + uint96 juelsPerGas = _getJuelsFromWei(tx.gasprice); + + // The Functions Router will perform the callback to the client contract + (FunctionsResponse.FulfillResult resultCode, uint96 callbackCostJuels) = _getRouter().fulfill( + response, + err, + juelsPerGas, + // The following line represents: "cost without callback or admin fee, those will be added by the Router" + // But because the _offchain_ Commitment is using operation fee in the place of the admin fee, this now adds admin fee (actually operation fee) + // Admin fee is configured to 0 in the Router + gasOverheadJuels + commitment.donFee + commitment.adminFee, + msg.sender, + FunctionsResponse.Commitment({ + adminFee: 0, // The Router should have adminFee set to 0. If it does not this will cause fulfillments to fail with INVALID_COMMITMENT instead of carrying out incorrect bookkeeping. + coordinator: commitment.coordinator, + client: commitment.client, + subscriptionId: commitment.subscriptionId, + callbackGasLimit: commitment.callbackGasLimit, + estimatedTotalCostJuels: commitment.estimatedTotalCostJuels, + timeoutTimestamp: commitment.timeoutTimestamp, + requestId: commitment.requestId, + donFee: commitment.donFee, + gasOverheadBeforeCallback: commitment.gasOverheadBeforeCallback, + gasOverheadAfterCallback: commitment.gasOverheadAfterCallback + }) + ); + + // The 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; + // Pay the operation fee to the Coordinator owner + s_withdrawableTokens[_owner()] += commitment.adminFee; // OperationFee is used in the slot for Admin Fee in the Offchain Commitment. Admin Fee is set to 0 in the Router (enforced by line 316 in FunctionsBilling.sol). + emit RequestBilled({ + requestId: requestId, + juelsPerGas: juelsPerGas, + l1FeeShareWei: l1FeeShareWei, + callbackCostJuels: callbackCostJuels, + donFeeJuels: commitment.donFee, + // The following two lines are because of OperationFee being used in the Offchain Commitment + adminFeeJuels: 0, + operationFeeJuels: commitment.adminFee + }); + } + 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(); + uint256 numberOfTransmitters = transmitters.length; + if (numberOfTransmitters == 0) { + revert NoTransmittersSet(); + } + uint96 feePoolShare = s_feePool / uint96(numberOfTransmitters); + // Bounded by "maxNumOracles" on OCR2Abstract.sol + for (uint256 i = 0; i < numberOfTransmitters; ++i) { + s_withdrawableTokens[transmitters[i]] += feePoolShare; + } + s_feePool -= feePoolShare * uint96(numberOfTransmitters); + } + + // Overriden in FunctionsCoordinator.sol + function _onlyOwner() internal view virtual; + + // Used in FunctionsCoordinator.sol + function _isExistingRequest(bytes32 requestId) internal view returns (bool) { + return s_requestCommitments[requestId] != bytes32(0); + } + + // Overriden in FunctionsCoordinator.sol + function _owner() internal view virtual returns (address owner); +} From 8b7c57b1de3396656aeeef4edaab9697fc363166 Mon Sep 17 00:00:00 2001 From: Kodey Kilday-Thomas Date: Wed, 18 Dec 2024 12:13:19 +0000 Subject: [PATCH 06/11] fix: unused vars --- .../src/v0.8/functions/v1_3_0_zksync/FunctionsBilling.sol | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/contracts/src/v0.8/functions/v1_3_0_zksync/FunctionsBilling.sol b/contracts/src/v0.8/functions/v1_3_0_zksync/FunctionsBilling.sol index 83067d2deb8..5f9d16ef680 100644 --- a/contracts/src/v0.8/functions/v1_3_0_zksync/FunctionsBilling.sol +++ b/contracts/src/v0.8/functions/v1_3_0_zksync/FunctionsBilling.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.19; import {IFunctionsSubscriptions} from "../v1_0_0/interfaces/IFunctionsSubscriptions.sol"; import {AggregatorV3Interface} from "../../shared/interfaces/AggregatorV3Interface.sol"; -import {IFunctionsBilling, FunctionsBillingConfig} from "../v1_0_0/interfaces/IFunctionsBilling.sol"; +import {IFunctionsBilling, FunctionsBillingConfig} from "../v1_3_0/interfaces/IFunctionsBilling.sol"; import {Routable} from "../v1_0_0/Routable.sol"; import {FunctionsResponse} from "../v1_0_0/libraries/FunctionsResponse.sol"; @@ -278,7 +278,6 @@ abstract contract FunctionsBilling is Routable, IFunctionsBilling { /// @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 - /// @param reportBatchSize the number of fulfillments in the transmitter's report /// @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 @@ -287,8 +286,7 @@ abstract contract FunctionsBilling is Routable, IFunctionsBilling { bytes memory response, bytes memory err, bytes memory onchainMetadata, - bytes memory /* offchainMetadata TODO: use in getDonFee() for dynamic billing */, - uint8 reportBatchSize + bytes memory /* offchainMetadata TODO: use in getDonFee() for dynamic billing */ ) internal returns (FunctionsResponse.FulfillResult) { FunctionsResponse.Commitment memory commitment = abi.decode(onchainMetadata, (FunctionsResponse.Commitment)); @@ -341,7 +339,7 @@ abstract contract FunctionsBilling is Routable, IFunctionsBilling { emit RequestBilled({ requestId: requestId, juelsPerGas: juelsPerGas, - l1FeeShareWei: l1FeeShareWei, + l1FeeShareWei: 0, callbackCostJuels: callbackCostJuels, donFeeJuels: commitment.donFee, // The following two lines are because of OperationFee being used in the Offchain Commitment From 06d118c445371b3dfc5064fe21a353f3ac5dce48 Mon Sep 17 00:00:00 2001 From: Mohamed Mehany <7327188+mohamed-mehany@users.noreply.github.com> Date: Mon, 20 Jan 2025 15:09:53 +0100 Subject: [PATCH 07/11] Adds exact gas bound call implementation for zkSync This commit adds a safe implementation for the gas bound call implementation for zksync. It also adds a custom Functions router contract for zkSync. --- contracts/package.json | 2 +- contracts/pnpm-lock.yaml | 17 +- .../v1_3_0_zksync/FunctionsRouter.sol | 584 ------------------ .../v1_3_0_zksync/ZKSyncFunctionsRouter.sol | 53 ++ .../shared/call/CallWithExactGasZKSync.sol | 118 ++++ 5 files changed, 183 insertions(+), 591 deletions(-) delete mode 100644 contracts/src/v0.8/functions/v1_3_0_zksync/FunctionsRouter.sol create mode 100644 contracts/src/v0.8/functions/v1_3_0_zksync/ZKSyncFunctionsRouter.sol create mode 100644 contracts/src/v0.8/shared/call/CallWithExactGasZKSync.sol diff --git a/contracts/package.json b/contracts/package.json index 1e4da89843a..7f54f0c1649 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -71,7 +71,7 @@ "moment": "^2.30.1", "prettier": "^3.3.3", "prettier-plugin-solidity": "^1.4.1", - "solhint": "^5.0.3", + "solhint": "^5.0.5", "solhint-plugin-chainlink-solidity": "git+https://github.com/smartcontractkit/chainlink-solhint-rules.git#v1.2.1", "solhint-plugin-prettier": "^0.1.0", "ts-node": "^10.9.2", diff --git a/contracts/pnpm-lock.yaml b/contracts/pnpm-lock.yaml index 2ea91943b13..3701a90ccbf 100644 --- a/contracts/pnpm-lock.yaml +++ b/contracts/pnpm-lock.yaml @@ -145,8 +145,8 @@ importers: specifier: ^1.4.1 version: 1.4.1(prettier@3.3.3) solhint: - specifier: ^5.0.3 - version: 5.0.3 + specifier: ^5.0.5 + version: 5.0.5 solhint-plugin-chainlink-solidity: specifier: git+https://github.com/smartcontractkit/chainlink-solhint-rules.git#v1.2.1 version: '@chainlink/solhint-plugin-chainlink-solidity@https://codeload.github.com/smartcontractkit/chainlink-solhint-rules/tar.gz/1b4c0c2663fcd983589d4f33a2e73908624ed43c' @@ -687,6 +687,9 @@ packages: '@solidity-parser/parser@0.18.0': resolution: {integrity: sha512-yfORGUIPgLck41qyN7nbwJRAx17/jAIXCTanHOJZhB6PJ1iAk/84b/xlsVKFSyNyLXIj0dhppoE0+CRws7wlzA==} + '@solidity-parser/parser@0.19.0': + resolution: {integrity: sha512-RV16k/qIxW/wWc+mLzV3ARyKUaMUTBy9tOLMzFhtNSKYeTAanQ3a5MudJKf/8arIFnA2L27SNjarQKmFg0w/jA==} + '@szmarczak/http-timer@5.0.1': resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} engines: {node: '>=14.16'} @@ -2727,8 +2730,8 @@ packages: prettier: ^3.0.0 prettier-plugin-solidity: ^1.0.0 - solhint@5.0.3: - resolution: {integrity: sha512-OLCH6qm/mZTCpplTXzXTJGId1zrtNuDYP5c2e6snIv/hdRVxPfBBz/bAlL91bY/Accavkayp2Zp2BaDSrLVXTQ==} + solhint@5.0.5: + resolution: {integrity: sha512-WrnG6T+/UduuzSWsSOAbfq1ywLUDwNea3Gd5hg6PS+pLUm8lz2ECNr0beX609clBxmDeZ3676AiA9nPDljmbJQ==} hasBin: true solidity-ast@0.4.56: @@ -4102,6 +4105,8 @@ snapshots: '@solidity-parser/parser@0.18.0': {} + '@solidity-parser/parser@0.19.0': {} + '@szmarczak/http-timer@5.0.1': dependencies: defer-to-connect: 2.0.1 @@ -6592,9 +6597,9 @@ snapshots: prettier-linter-helpers: 1.0.0 prettier-plugin-solidity: 1.4.1(prettier@3.3.3) - solhint@5.0.3: + solhint@5.0.5: dependencies: - '@solidity-parser/parser': 0.18.0 + '@solidity-parser/parser': 0.19.0 ajv: 6.12.6 antlr4: 4.13.1-patch-1 ast-parents: 0.0.1 diff --git a/contracts/src/v0.8/functions/v1_3_0_zksync/FunctionsRouter.sol b/contracts/src/v0.8/functions/v1_3_0_zksync/FunctionsRouter.sol deleted file mode 100644 index 862817476be..00000000000 --- a/contracts/src/v0.8/functions/v1_3_0_zksync/FunctionsRouter.sol +++ /dev/null @@ -1,584 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; - -import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol"; -import {IFunctionsRouter} from "../v1_0_0/interfaces/IFunctionsRouter.sol"; -import {IFunctionsCoordinator} from "../v1_0_0/interfaces/IFunctionsCoordinator.sol"; -import {IAccessController} from "../../shared/interfaces/IAccessController.sol"; -import {GAS_BOUND_CALLER, IGasBoundCaller} from "./interfaces/zksync/IGasBoundCaller.sol"; - -import {FunctionsSubscriptions} from "../v1_0_0/FunctionsSubscriptions.sol"; -import {FunctionsResponse} from "../v1_0_0/libraries/FunctionsResponse.sol"; -import {ConfirmedOwner} from "../../shared/access/ConfirmedOwner.sol"; - -import {SafeCast} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/math/SafeCast.sol"; -import {Pausable} from "../../vendor/openzeppelin-solidity/v4.8.3/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.3.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; - uint256 g1 = gasleft(); - - 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) - } - } - - // allocate return data memory ahead of time - bytes memory returnData = new bytes(MAX_CALLBACK_RETURN_BYTES); - - // solhint-disable-next-line avoid-low-level-calls - (success, returnData) = GAS_BOUND_CALLER.delegatecall{gas: callbackGasLimit}( - abi.encodeWithSelector(IGasBoundCaller.gasBoundCall.selector, client, callbackGasLimit, encodedCallback) - ); - - uint256 pubdataGasSpent; - if (success) { - (, pubdataGasSpent) = abi.decode(returnData, (bytes, uint256)); - } - gasUsed = g1 - gasleft() + pubdataGasSpent; - - 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_3_0_zksync/ZKSyncFunctionsRouter.sol b/contracts/src/v0.8/functions/v1_3_0_zksync/ZKSyncFunctionsRouter.sol new file mode 100644 index 00000000000..ab9c6f1d003 --- /dev/null +++ b/contracts/src/v0.8/functions/v1_3_0_zksync/ZKSyncFunctionsRouter.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {FunctionsRouter} from "../v1_0_0/FunctionsRouter.sol"; +import {CallWithExactGasZKSync} from "../../shared/call/CallWithExactGasZKSync.sol"; + +/** + * @title FunctionsRouterZkSync + * @notice Specialized version of FunctionsRouter for zkSync that uses + * CallWithExactGasZKSync to control callback gas usage. + */ +contract ZKSyncFunctionsRouter is FunctionsRouter { + constructor(address linkToken, Config memory config) FunctionsRouter(linkToken, config) {} + + /** + * @dev Override the internal callback function to use CallWithExactGasZKSync + * for controlling and measuring gas usage on zkSync. + */ + function _callback( + bytes32 requestId, + bytes memory response, + bytes memory err, + uint32 callbackGasLimit, + address client + ) private override returns (CallbackResult memory) { + // 1. Check if client code exists + bool destinationNoLongerExists; + assembly { + destinationNoLongerExists := iszero(extcodesize(client)) + } + if (destinationNoLongerExists) { + // If there's no code at `client`, skip the callback + return CallbackResult({success: false, gasUsed: 0, returnData: new bytes(0)}); + } + + // 2. Encode callback to the client + bytes memory encodedCallback = abi.encodeWithSelector( + this.getConfig().handleOracleFulfillmentSelector, + requestId, + response, + err + ); + + // 3. Use our library to enforce an exact gas call + (bool success, uint256 gasUsed, bytes memory returnData) = CallWithExactGasZKSync._callWithExactGas( + client, + callbackGasLimit, + encodedCallback, + MAX_CALLBACK_RETURN_BYTES + ); + return CallbackResult({success: success, gasUsed: gasUsed, returnData: returnData}); + } +} diff --git a/contracts/src/v0.8/shared/call/CallWithExactGasZKSync.sol b/contracts/src/v0.8/shared/call/CallWithExactGasZKSync.sol new file mode 100644 index 00000000000..f826b73a07f --- /dev/null +++ b/contracts/src/v0.8/shared/call/CallWithExactGasZKSync.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {EfficientCall} from "@zksync/contracts/system-contracts/contracts/libraries/EfficientCall.sol"; +import {ISystemContext} from "@zksync/contracts/gas-bound-caller/contracts/ISystemContext.sol"; + +ISystemContext constant SYSTEM_CONTEXT_CONTRACT = ISystemContext(address(0x800b)); + +/** + * @title CallWithExactGasZKSync + * @notice Library that attempts to call a target contract with exactly `gasAmount` gas on zkSync + * and measures how much gas was actually used. + * Implementation based on the GasBoundCaller contract, https://github.com/matter-labs/era-contracts/blob/main/gas-bound-caller/contracts/GasBoundCaller.sol + */ +library CallWithExactGasZKSync { + /// @notice We assume that no more than `CALL_ENTRY_OVERHEAD` ergs are used for the O(1) operations at the start + /// of execution of the contract, such as abi decoding the parameters, jumping to the correct function, etc. + uint256 constant CALL_ENTRY_OVERHEAD = 800; + /// @notice We assume that no more than `CALL_RETURN_OVERHEAD` ergs are used for the O(1) operations at the end of the execution, + /// as such relaying the return. + uint256 constant CALL_RETURN_OVERHEAD = 400; + + /// @notice The function that implements limiting of the total gas expenditure of the call. + /// @dev On Era, the gas for pubdata is charged at the end of the execution of the entire transaction, meaning + /// that if a subcall is not trusted, it can consume lots of pubdata in the process. This function ensures that + /// no more than `_maxTotalGas` will be allowed to be spent by the call. To be sure, this function uses some margin + /// (`CALL_ENTRY_OVERHEAD` + `CALL_RETURN_OVERHEAD`) to ensure that the call will not exceed the limit, so it may + /// actually spend a bit less than `_maxTotalGas` in the end. + /// @dev The entire `gas` passed to this function could be used, regardless + /// of the `_maxTotalGas` parameter. In other words, `max(gas(), _maxTotalGas)` is the maximum amount of gas that can be spent by this function. + /// @dev The function relays the `returndata` returned by the callee. In case the `callee` reverts, it reverts with the same error. + /// @param _to The address of the contract to call. + /// @param _maxtotalgas the maximum amount of gas that can be spent by the call. + /// @param _data The calldata for the call. + /// @param _maxReturnBytes the maximum amount of bytes that can be returned by the call. + function _callWithExactGas( + address _to, + uint256 _maxTotalGas, + bytes calldata _data, + uint16 _maxReturnBytes + ) external payable { + // At the start of the execution we deduce how much gas be spent on things that will be + // paid for later on by the transaction. + // The `expectedForCompute` variable is an upper bound of how much this contract can spend on compute and + // MUST be higher or equal to the `gas` passed into the call. + uint256 expectedForCompute = gasleft() + CALL_ENTRY_OVERHEAD; + + // We expect that the `_maxTotalGas` at least includes the `gas` required for the call. + // This require is more of a safety protection for the users that call this function with incorrect parameters. + // + // Ultimately, the entire `gas` sent to this call can be spent on compute regardless of the `_maxTotalGas` parameter. + require(_maxTotalGas >= gasleft(), "Gas limit is too low"); + + // This is the amount of gas that can be spent *exclusively* on pubdata in addition to the `gas` provided to this function. + uint256 pubdataAllowance = _maxTotalGas > expectedForCompute ? _maxTotalGas - expectedForCompute : 0; + + uint256 pubdataPublishedBefore = SYSTEM_CONTEXT_CONTRACT.getCurrentPubdataSpent(); + + // We never permit system contract calls. + // If the call fails, the `EfficientCall.call` will propagate the revert. + // Since the revert is propagated, the pubdata published wouldn't change and so no + // other checks are needed. + bytes memory returnData = EfficientCall.call({ + _gas: gasleft(), + _address: _to, + _value: msg.value, + _data: _data, + _isSystem: false + }); + + // We will calculate the length of the returndata to be used at the end of the function. + // We need additional `96` bytes to encode the offset `0x40` for the entire pubdata, + // the gas spent on pubdata as well as the length of the original returndata. + // Note, that it has to be padded to the 32 bytes to adhere to proper ABI encoding. + uint256 paddedReturndataLen = returnData.length + 96; + if (paddedReturndataLen % 32 != 0) { + paddedReturndataLen += 32 - (paddedReturndataLen % 32); + } + + // limit our paddedReturndataLen to maxReturnBytes bytes + if (paddedReturndataLen > _maxReturnBytes) { + paddedReturndataLen := _maxReturnBytes; + } + + uint256 pubdataPublishedAfter = SYSTEM_CONTEXT_CONTRACT.getCurrentPubdataSpent(); + + // It is possible that pubdataPublishedAfter < pubdataPublishedBefore if the call, e.g. removes + // some of the previously created state diffs + uint256 pubdataSpent = pubdataPublishedAfter > pubdataPublishedBefore + ? pubdataPublishedAfter - pubdataPublishedBefore + : 0; + + uint256 pubdataGasRate = SYSTEM_CONTEXT_CONTRACT.gasPerPubdataByte(); + + // In case there is an overflow here, the `_maxTotalGas` wouldn't be able to cover it anyway, so + // we don't mind the contract panicking here in case of it. + uint256 pubdataGas = pubdataGasRate * pubdataSpent; + + if (pubdataGas != 0) { + // Here we double check that the additional cost is not higher than the maximum allowed. + // Note, that the `gasleft()` can be spent on pubdata too. + require(pubdataAllowance + gasleft() >= pubdataGas + CALL_RETURN_OVERHEAD, "Not enough gas for pubdata"); + } + + assembly { + // This place does interfere with the memory layout, however, it is done right before + // the `return` statement, so it is safe to do. + // We need to transform `bytes memory returnData` into (bytes memory returndata, gasSpentOnPubdata) + // `bytes memory returnData` is encoded as `length` + `data`. + // We need to prepend it with 0x40 and `pubdataGas`. + // + // It is assumed that the position of returndata is >= 0x40, since 0x40 is the free memory pointer location. + mstore(sub(returnData, 0x40), 0x40) + mstore(sub(returnData, 0x20), pubdataGas) + return(sub(returnData, 0x40), paddedReturndataLen) + } + } +} From 6438140d9b457ad548dfddbf92154e708e04aadb Mon Sep 17 00:00:00 2001 From: Mohamed Mehany <7327188+mohamed-mehany@users.noreply.github.com> Date: Mon, 20 Jan 2025 15:53:41 +0100 Subject: [PATCH 08/11] Bump solidity version --- .../src/v0.8/functions/v1_3_0_zksync/ZKSyncFunctionsRouter.sol | 2 +- contracts/src/v0.8/shared/call/CallWithExactGasZKSync.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/src/v0.8/functions/v1_3_0_zksync/ZKSyncFunctionsRouter.sol b/contracts/src/v0.8/functions/v1_3_0_zksync/ZKSyncFunctionsRouter.sol index ab9c6f1d003..1601e5e588d 100644 --- a/contracts/src/v0.8/functions/v1_3_0_zksync/ZKSyncFunctionsRouter.sol +++ b/contracts/src/v0.8/functions/v1_3_0_zksync/ZKSyncFunctionsRouter.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +pragma solidity ^0.8.24; import {FunctionsRouter} from "../v1_0_0/FunctionsRouter.sol"; import {CallWithExactGasZKSync} from "../../shared/call/CallWithExactGasZKSync.sol"; diff --git a/contracts/src/v0.8/shared/call/CallWithExactGasZKSync.sol b/contracts/src/v0.8/shared/call/CallWithExactGasZKSync.sol index f826b73a07f..b22928b9ed9 100644 --- a/contracts/src/v0.8/shared/call/CallWithExactGasZKSync.sol +++ b/contracts/src/v0.8/shared/call/CallWithExactGasZKSync.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +pragma solidity ^0.8.24; import {EfficientCall} from "@zksync/contracts/system-contracts/contracts/libraries/EfficientCall.sol"; import {ISystemContext} from "@zksync/contracts/gas-bound-caller/contracts/ISystemContext.sol"; From 9c47fddff8f6ec036ef52b411275b299ea8e7d2f Mon Sep 17 00:00:00 2001 From: Mohamed Mehany <7327188+mohamed-mehany@users.noreply.github.com> Date: Mon, 20 Jan 2025 16:03:14 +0100 Subject: [PATCH 09/11] Typo fix --- contracts/src/v0.8/shared/call/CallWithExactGasZKSync.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/v0.8/shared/call/CallWithExactGasZKSync.sol b/contracts/src/v0.8/shared/call/CallWithExactGasZKSync.sol index b22928b9ed9..b1e78f6ea93 100644 --- a/contracts/src/v0.8/shared/call/CallWithExactGasZKSync.sol +++ b/contracts/src/v0.8/shared/call/CallWithExactGasZKSync.sol @@ -79,7 +79,7 @@ library CallWithExactGasZKSync { // limit our paddedReturndataLen to maxReturnBytes bytes if (paddedReturndataLen > _maxReturnBytes) { - paddedReturndataLen := _maxReturnBytes; + paddedReturndataLen = _maxReturnBytes; } uint256 pubdataPublishedAfter = SYSTEM_CONTEXT_CONTRACT.getCurrentPubdataSpent(); From 402840aa099c8ac2f127c526f728a3ded2e1e825 Mon Sep 17 00:00:00 2001 From: Mohamed Mehany <7327188+mohamed-mehany@users.noreply.github.com> Date: Mon, 20 Jan 2025 16:25:27 +0100 Subject: [PATCH 10/11] Minor changes --- contracts/package.json | 2 +- contracts/pnpm-lock.yaml | 2 +- .../v0.8/functions/v1_3_0_zksync/ZKSyncFunctionsRouter.sol | 4 ++-- contracts/src/v0.8/shared/call/CallWithExactGasZKSync.sol | 7 +++++-- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/contracts/package.json b/contracts/package.json index 7f54f0c1649..1e4da89843a 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -71,7 +71,7 @@ "moment": "^2.30.1", "prettier": "^3.3.3", "prettier-plugin-solidity": "^1.4.1", - "solhint": "^5.0.5", + "solhint": "^5.0.3", "solhint-plugin-chainlink-solidity": "git+https://github.com/smartcontractkit/chainlink-solhint-rules.git#v1.2.1", "solhint-plugin-prettier": "^0.1.0", "ts-node": "^10.9.2", diff --git a/contracts/pnpm-lock.yaml b/contracts/pnpm-lock.yaml index 3701a90ccbf..b1a0aa696e5 100644 --- a/contracts/pnpm-lock.yaml +++ b/contracts/pnpm-lock.yaml @@ -145,7 +145,7 @@ importers: specifier: ^1.4.1 version: 1.4.1(prettier@3.3.3) solhint: - specifier: ^5.0.5 + specifier: ^5.0.3 version: 5.0.5 solhint-plugin-chainlink-solidity: specifier: git+https://github.com/smartcontractkit/chainlink-solhint-rules.git#v1.2.1 diff --git a/contracts/src/v0.8/functions/v1_3_0_zksync/ZKSyncFunctionsRouter.sol b/contracts/src/v0.8/functions/v1_3_0_zksync/ZKSyncFunctionsRouter.sol index 1601e5e588d..dd23f669d12 100644 --- a/contracts/src/v0.8/functions/v1_3_0_zksync/ZKSyncFunctionsRouter.sol +++ b/contracts/src/v0.8/functions/v1_3_0_zksync/ZKSyncFunctionsRouter.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; +pragma solidity ^0.8.19; import {FunctionsRouter} from "../v1_0_0/FunctionsRouter.sol"; import {CallWithExactGasZKSync} from "../../shared/call/CallWithExactGasZKSync.sol"; @@ -42,7 +42,7 @@ contract ZKSyncFunctionsRouter is FunctionsRouter { ); // 3. Use our library to enforce an exact gas call - (bool success, uint256 gasUsed, bytes memory returnData) = CallWithExactGasZKSync._callWithExactGas( + (bool success, uint256 gasUsed, bytes memory returnData) = CallWithExactGasZKSync._callWithExactGasSafeReturnData( client, callbackGasLimit, encodedCallback, diff --git a/contracts/src/v0.8/shared/call/CallWithExactGasZKSync.sol b/contracts/src/v0.8/shared/call/CallWithExactGasZKSync.sol index b1e78f6ea93..8a4f0ee788b 100644 --- a/contracts/src/v0.8/shared/call/CallWithExactGasZKSync.sol +++ b/contracts/src/v0.8/shared/call/CallWithExactGasZKSync.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; +pragma solidity ^0.8.19; import {EfficientCall} from "@zksync/contracts/system-contracts/contracts/libraries/EfficientCall.sol"; import {ISystemContext} from "@zksync/contracts/gas-bound-caller/contracts/ISystemContext.sol"; @@ -33,7 +33,10 @@ library CallWithExactGasZKSync { /// @param _maxtotalgas the maximum amount of gas that can be spent by the call. /// @param _data The calldata for the call. /// @param _maxReturnBytes the maximum amount of bytes that can be returned by the call. - function _callWithExactGas( + /// @return success whether the call succeeded + /// @return retData the return data from the call, capped at maxReturnBytes bytes + /// @return gasUsed the gas used by the external call. Does not include the overhead of this function. + function _callWithExactGasSafeReturnData( address _to, uint256 _maxTotalGas, bytes calldata _data, From 72b5787189a9d995b6435dbe55115054a0b975a8 Mon Sep 17 00:00:00 2001 From: Mohamed Mehany <7327188+mohamed-mehany@users.noreply.github.com> Date: Mon, 20 Jan 2025 16:48:30 +0100 Subject: [PATCH 11/11] Adds solidity version 0.8.20 --- contracts/.solhintignore | 3 +-- contracts/hardhat.config.ts | 4 ++++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/contracts/.solhintignore b/contracts/.solhintignore index ce7dfa800ba..32fda0bc31a 100644 --- a/contracts/.solhintignore +++ b/contracts/.solhintignore @@ -31,7 +31,6 @@ # Ignore Functions v1.0.0 code that was frozen after audit ./src/v0.8/functions/v1_0_0 -./src/v0.8/functions/v1_3_0_zksync/FunctionsRouter.sol # Test helpers ./src/v0.8/vrf/testhelpers @@ -44,4 +43,4 @@ ./node_modules/ # Ignore tweaked vendored contracts -./src/v0.8/shared/enumerable/EnumerableSetWithBytes16.sol \ No newline at end of file +./src/v0.8/shared/enumerable/EnumerableSetWithBytes16.sol diff --git a/contracts/hardhat.config.ts b/contracts/hardhat.config.ts index 4a3935475c5..79fe9849df1 100644 --- a/contracts/hardhat.config.ts +++ b/contracts/hardhat.config.ts @@ -71,6 +71,10 @@ let config = { version: '0.8.19', settings: COMPILER_SETTINGS, }, + { + version: '0.8.20', + settings: COMPILER_SETTINGS, + }, { version: '0.8.24', settings: {