From a9ed79bae152f9d1251b28a5a9b3101739a5015d Mon Sep 17 00:00:00 2001 From: Morgan Kuphal Date: Thu, 16 May 2024 16:41:33 -0500 Subject: [PATCH 1/9] Changed L1 fee estimation logic for L2s --- .../functions/v1_1_0/FunctionsBilling.sol | 7 +- .../v1_1_0/libraries/ChainSpecificUtil.sol | 44 +++++-- .../functions/v1_1_0/libraries/ISemver.sol | 13 ++ .../functions/v1_1_0/libraries/L1Block.sol | 122 ++++++++++++++++++ 4 files changed, 171 insertions(+), 15 deletions(-) create mode 100644 contracts/src/v0.8/functions/v1_1_0/libraries/ISemver.sol create mode 100644 contracts/src/v0.8/functions/v1_1_0/libraries/L1Block.sol diff --git a/contracts/src/v0.8/functions/v1_1_0/FunctionsBilling.sol b/contracts/src/v0.8/functions/v1_1_0/FunctionsBilling.sol index ff345003741..0656a492304 100644 --- a/contracts/src/v0.8/functions/v1_1_0/FunctionsBilling.sol +++ b/contracts/src/v0.8/functions/v1_1_0/FunctionsBilling.sol @@ -50,7 +50,8 @@ abstract contract FunctionsBilling is Routable, IFunctionsBilling { uint40 minimumEstimateGasPriceWei; // ║ The lowest amount of wei that will be used as the tx.gasprice when estimating the cost to fulfill the request uint16 maxSupportedRequestDataVersion; // ═══════╝ The highest support request data version supported by the node. All lower versions should also be supported. uint224 fallbackNativePerUnitLink; // ═══════════╗ Fallback NATIVE CURRENCY / LINK conversion rate if the data feed is stale - uint32 requestTimeoutSeconds; // ════════════════╝ How many seconds it takes before we consider a request to be timed out + uint32 requestTimeoutSeconds; // ║ How many seconds it takes before we consider a request to be timed out + uint24 transmitTxSizeBytes; // ══════════════════╝ The size of the transmit transaction in bytes used to calculate the L1 data fee on L2s } Config private s_config; @@ -180,7 +181,7 @@ abstract contract FunctionsBilling is Routable, IFunctionsBilling { /// @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; - uint256 l1FeeWei = ChainSpecificUtil._getCurrentTxL1GasFees(msg.data); + uint256 l1FeeWei = ChainSpecificUtil._getL1DataGasCostUpperLimit(s_config.transmitTxSizeBytes); uint96 estimatedGasReimbursementJuels = _getJuelsFromWei((gasPriceWithOverestimation * executionGas) + l1FeeWei); uint96 feesJuels = uint96(donFee) + uint96(adminFee); @@ -274,7 +275,7 @@ abstract contract FunctionsBilling is Routable, IFunctionsBilling { FunctionsResponse.Commitment memory commitment = abi.decode(onchainMetadata, (FunctionsResponse.Commitment)); uint256 gasOverheadWei = (commitment.gasOverheadBeforeCallback + commitment.gasOverheadAfterCallback) * tx.gasprice; - uint256 l1FeeShareWei = ChainSpecificUtil._getCurrentTxL1GasFees(msg.data) / reportBatchSize; + uint256 l1FeeShareWei = ChainSpecificUtil._getL1DataGasCostUpperLimit(msg.data) / reportBatchSize; // Gas overhead without callback uint96 gasOverheadJuels = _getJuelsFromWei(gasOverheadWei + l1FeeShareWei); uint96 juelsPerGas = _getJuelsFromWei(tx.gasprice); diff --git a/contracts/src/v0.8/functions/v1_1_0/libraries/ChainSpecificUtil.sol b/contracts/src/v0.8/functions/v1_1_0/libraries/ChainSpecificUtil.sol index 68d346e676d..b950f2957b6 100644 --- a/contracts/src/v0.8/functions/v1_1_0/libraries/ChainSpecificUtil.sol +++ b/contracts/src/v0.8/functions/v1_1_0/libraries/ChainSpecificUtil.sol @@ -2,13 +2,14 @@ pragma solidity ^0.8.19; import {ArbGasInfo} from "../../../vendor/@arbitrum/nitro-contracts/src/precompiles/ArbGasInfo.sol"; -import {GasPriceOracle} from "../../../vendor/@eth-optimism/contracts-bedrock/v0.16.2/src/L2/GasPriceOracle.sol"; +import {L1Block} from "./L1Block.sol"; /// @dev A library that abstracts out opcodes that behave differently across chains. /// @dev The methods below return values that are pertinent to the given chain. library ChainSpecificUtil { // ------------ Start Arbitrum Constants ------------ - + /// @dev ARB_L1_FEE_DATA_PADDING_SIZE is the L1 data padding for Optimism + uint256 private const ARB_L1_FEE_DATA_PADDING_SIZE = 140; /// @dev ARBGAS_ADDR is the address of the ArbGasInfo precompile on Arbitrum. /// @dev reference: https://github.com/OffchainLabs/nitro/blob/v2.0.14/contracts/src/precompiles/ArbGasInfo.sol#L10 address private constant ARBGAS_ADDR = address(0x000000000000000000000000000000000000006C); @@ -21,13 +22,12 @@ library ChainSpecificUtil { // ------------ End Arbitrum Constants ------------ // ------------ Start Optimism Constants ------------ - /// @dev L1_FEE_DATA_PADDING includes 35 bytes for L1 data padding for Optimism - bytes internal constant L1_FEE_DATA_PADDING = - "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; - /// @dev OVM_GASPRICEORACLE_ADDR is the address of the GasPriceOracle precompile on Optimism. - /// @dev reference: https://community.optimism.io/docs/developers/build/transaction-fees/#estimating-the-l1-data-fee - address private constant OVM_GASPRICEORACLE_ADDR = address(0x420000000000000000000000000000000000000F); - GasPriceOracle private constant OVM_GASPRICEORACLE = GasPriceOracle(OVM_GASPRICEORACLE_ADDR); + /// @dev OP_L1_FEE_DATA_PADDING_SIZE is the L1 data padding for Optimism + uint256 private const OP_L1_FEE_DATA_PADDING_SIZE = 35; + /// @dev L1BLOCK_ADDR is the address of the L1Block precompile on Optimism. + /// @dev Reference for calculating the gas for the Ecotone upgrade: https://docs.optimism.io/stack/transactions/fees#ecotone + address private constant L1BLOCK_ADDR = address(0x4200000000000000000000000000000000000015); + L1Block private constant L1BLOCK = L1Block(L1BLOCK_ADDR); uint256 private constant OP_MAINNET_CHAIN_ID = 10; uint256 private constant OP_GOERLI_CHAIN_ID = 420; @@ -46,12 +46,13 @@ library ChainSpecificUtil { /// @notice On Arbitrum, the provided calldata is not used to calculate the fees. /// @notice On Optimism, the provided calldata is passed to the GasPriceOracle predeploy /// @notice and getL1Fee is called to get the fees. - function _getCurrentTxL1GasFees(bytes memory txCallData) internal view returns (uint256 l1FeeWei) { + function _getL1DataGasCostUpperLimit(bytes memory dataSizeBytes) internal view returns (uint256 l1FeeWei) { uint256 chainid = block.chainid; if (_isArbitrumChainId(chainid)) { - return ARBGAS.getCurrentTxL1GasFees(); + (, uint256 l1PricePerByte, , , , ) = ARBGAS.getPricesInWei(); + return l1PricePerByte * (calldataSizeBytes + ARB_L1_FEE_DATA_PADDING_SIZE); } else if (_isOptimismChainId(chainid)) { - return OVM_GASPRICEORACLE.getL1Fee(bytes.concat(txCallData, L1_FEE_DATA_PADDING)); + return _calculateOptimismL1FeeUpperLimit(dataSizeBytes + OP_L1_FEE_DATA_PADDING_SIZE); } return 0; } @@ -75,4 +76,23 @@ library ChainSpecificUtil { chainId == BASE_GOERLI_CHAIN_ID || chainId == BASE_SEPOLIA_CHAIN_ID; } + + function _calculateOptimismL1FeeUpperLimit(uint256 dataSizeBytes) internal view returns (uint256) { + // from: https://docs.optimism.io/stack/transactions/fees#ecotone + // l1_data_fee = tx_compressed_size * weighted_gas_price + // weighted_gas_price = 16*base_fee_scalar*base_fee + blob_base_fee_scalar*blob_base_fee + // tx_compressed_size = [(count_zero_bytes(tx_data)*4 + count_non_zero_bytes(tx_data)*16)] / 16 + // note we conservatively assume all non-zero bytes, therefore: + // tx_compressed_size = tx_data_size_bytes + // l1_data_fee = tx_data_size_bytes * weighted_gas_price + uint256 l1BaseFeeWei = L1BLOCK.baseFee(); + uint256 l1BaseFeeScalar = L1BLOCK.baseFeeScalar(); + uint256 l1BlobBaseFeeWei = L1BLOCK.blobBaseFee(); + uint256 l1BlobBaseFeeScalar = L1BLOCK.blobBaseFeeScalar(); + + uint256 weightedGasPrice = 16 * l1BaseFeeScalar * l1BaseFee + l1BlobBaseFeeScalar * l1BlobBaseFeeWei; + + uint256 l1DataFeeWei = weightedGasPrice * dataSizeBytes; + return l1DataFeeWei; + } } diff --git a/contracts/src/v0.8/functions/v1_1_0/libraries/ISemver.sol b/contracts/src/v0.8/functions/v1_1_0/libraries/ISemver.sol new file mode 100644 index 00000000000..ae9569a0505 --- /dev/null +++ b/contracts/src/v0.8/functions/v1_1_0/libraries/ISemver.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @title ISemver +/// @notice ISemver is a simple contract for ensuring that contracts are +/// versioned using semantic versioning. +interface ISemver { + /// @notice Getter for the semantic version of the contract. This is not + /// meant to be used onchain but instead meant to be used by offchain + /// tooling. + /// @return Semver contract version as a string. + function version() external view returns (string memory); +} \ No newline at end of file diff --git a/contracts/src/v0.8/functions/v1_1_0/libraries/L1Block.sol b/contracts/src/v0.8/functions/v1_1_0/libraries/L1Block.sol new file mode 100644 index 00000000000..2a3acc6550f --- /dev/null +++ b/contracts/src/v0.8/functions/v1_1_0/libraries/L1Block.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Copied from the implementation contract for https://optimistic.etherscan.io/address/0x4200000000000000000000000000000000000015#readProxyContract +// https://optimistic.etherscan.io/address/0x07dbe8500fc591d1852b76fee44d5a05e13097ff#code + +import { ISemver } from "./ISemver.sol"; + +/// @custom:proxied +/// @custom:predeploy 0x4200000000000000000000000000000000000015 +/// @title L1Block +/// @notice The L1Block predeploy gives users access to information about the last known L1 block. +/// Values within this contract are updated once per epoch (every L1 block) and can only be +/// set by the "depositor" account, a special system address. Depositor account transactions +/// are created by the protocol whenever we move to a new epoch. +contract L1Block is ISemver { + /// @notice Address of the special depositor account. + address public constant DEPOSITOR_ACCOUNT = 0xDeaDDEaDDeAdDeAdDEAdDEaddeAddEAdDEAd0001; + + /// @notice The latest L1 block number known by the L2 system. + uint64 public number; + + /// @notice The latest L1 timestamp known by the L2 system. + uint64 public timestamp; + + /// @notice The latest L1 base fee. + uint256 public basefee; + + /// @notice The latest L1 blockhash. + bytes32 public hash; + + /// @notice The number of L2 blocks in the same epoch. + uint64 public sequenceNumber; + + /// @notice The scalar value applied to the L1 blob base fee portion of the blob-capable L1 cost func. + uint32 public blobBaseFeeScalar; + + /// @notice The scalar value applied to the L1 base fee portion of the blob-capable L1 cost func. + uint32 public baseFeeScalar; + + /// @notice The versioned hash to authenticate the batcher by. + bytes32 public batcherHash; + + /// @notice The overhead value applied to the L1 portion of the transaction fee. + /// @custom:legacy + uint256 public l1FeeOverhead; + + /// @notice The scalar value applied to the L1 portion of the transaction fee. + /// @custom:legacy + uint256 public l1FeeScalar; + + /// @notice The latest L1 blob base fee. + uint256 public blobBaseFee; + + /// @custom:semver 1.2.0 + string public constant version = "1.2.0"; + + /// @custom:legacy + /// @notice Updates the L1 block values. + /// @param _number L1 blocknumber. + /// @param _timestamp L1 timestamp. + /// @param _basefee L1 basefee. + /// @param _hash L1 blockhash. + /// @param _sequenceNumber Number of L2 blocks since epoch start. + /// @param _batcherHash Versioned hash to authenticate batcher by. + /// @param _l1FeeOverhead L1 fee overhead. + /// @param _l1FeeScalar L1 fee scalar. + function setL1BlockValues( + uint64 _number, + uint64 _timestamp, + uint256 _basefee, + bytes32 _hash, + uint64 _sequenceNumber, + bytes32 _batcherHash, + uint256 _l1FeeOverhead, + uint256 _l1FeeScalar + ) + external + { + require(msg.sender == DEPOSITOR_ACCOUNT, "L1Block: only the depositor account can set L1 block values"); + + number = _number; + timestamp = _timestamp; + basefee = _basefee; + hash = _hash; + sequenceNumber = _sequenceNumber; + batcherHash = _batcherHash; + l1FeeOverhead = _l1FeeOverhead; + l1FeeScalar = _l1FeeScalar; + } + + /// @notice Updates the L1 block values for an Ecotone upgraded chain. + /// Params are packed and passed in as raw msg.data instead of ABI to reduce calldata size. + /// Params are expected to be in the following order: + /// 1. _baseFeeScalar L1 base fee scalar + /// 2. _blobBaseFeeScalar L1 blob base fee scalar + /// 3. _sequenceNumber Number of L2 blocks since epoch start. + /// 4. _timestamp L1 timestamp. + /// 5. _number L1 blocknumber. + /// 6. _basefee L1 base fee. + /// 7. _blobBaseFee L1 blob base fee. + /// 8. _hash L1 blockhash. + /// 9. _batcherHash Versioned hash to authenticate batcher by. + function setL1BlockValuesEcotone() external { + assembly { + // Revert if the caller is not the depositor account. + if xor(caller(), DEPOSITOR_ACCOUNT) { + mstore(0x00, 0x3cc50b45) // 0x3cc50b45 is the 4-byte selector of "NotDepositor()" + revert(0x1C, 0x04) // returns the stored 4-byte selector from above + } + let data := calldataload(4) + // sequencenum (uint64), blobBaseFeeScalar (uint32), baseFeeScalar (uint32) + sstore(sequenceNumber.slot, shr(128, calldataload(4))) + // number (uint64) and timestamp (uint64) + sstore(number.slot, shr(128, calldataload(20))) + sstore(basefee.slot, calldataload(36)) // uint256 + sstore(blobBaseFee.slot, calldataload(68)) // uint256 + sstore(hash.slot, calldataload(100)) // bytes32 + sstore(batcherHash.slot, calldataload(132)) // bytes32 + } + } +} \ No newline at end of file From e8d83e2b25bed60b4cea1f816e2f8a10f32d2f7a Mon Sep 17 00:00:00 2001 From: Morgan Kuphal Date: Thu, 16 May 2024 16:45:40 -0500 Subject: [PATCH 2/9] Added source --- .../src/v0.8/functions/v1_1_0/libraries/ChainSpecificUtil.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/src/v0.8/functions/v1_1_0/libraries/ChainSpecificUtil.sol b/contracts/src/v0.8/functions/v1_1_0/libraries/ChainSpecificUtil.sol index b950f2957b6..a8465a6077c 100644 --- a/contracts/src/v0.8/functions/v1_1_0/libraries/ChainSpecificUtil.sol +++ b/contracts/src/v0.8/functions/v1_1_0/libraries/ChainSpecificUtil.sol @@ -49,6 +49,7 @@ library ChainSpecificUtil { function _getL1DataGasCostUpperLimit(bytes memory dataSizeBytes) internal view returns (uint256 l1FeeWei) { uint256 chainid = block.chainid; if (_isArbitrumChainId(chainid)) { + // https://docs.arbitrum.io/build-decentralized-apps/how-to-estimate-gas#where-do-we-get-all-this-information-from (, uint256 l1PricePerByte, , , , ) = ARBGAS.getPricesInWei(); return l1PricePerByte * (calldataSizeBytes + ARB_L1_FEE_DATA_PADDING_SIZE); } else if (_isOptimismChainId(chainid)) { From a62f5d99720a9fcdf9d0a6d229f214756038a9f1 Mon Sep 17 00:00:00 2001 From: Morgan Kuphal Date: Thu, 16 May 2024 16:48:20 -0500 Subject: [PATCH 3/9] removed duplicate comment --- .../src/v0.8/functions/v1_1_0/libraries/ChainSpecificUtil.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/src/v0.8/functions/v1_1_0/libraries/ChainSpecificUtil.sol b/contracts/src/v0.8/functions/v1_1_0/libraries/ChainSpecificUtil.sol index a8465a6077c..473d5636c03 100644 --- a/contracts/src/v0.8/functions/v1_1_0/libraries/ChainSpecificUtil.sol +++ b/contracts/src/v0.8/functions/v1_1_0/libraries/ChainSpecificUtil.sol @@ -25,7 +25,6 @@ library ChainSpecificUtil { /// @dev OP_L1_FEE_DATA_PADDING_SIZE is the L1 data padding for Optimism uint256 private const OP_L1_FEE_DATA_PADDING_SIZE = 35; /// @dev L1BLOCK_ADDR is the address of the L1Block precompile on Optimism. - /// @dev Reference for calculating the gas for the Ecotone upgrade: https://docs.optimism.io/stack/transactions/fees#ecotone address private constant L1BLOCK_ADDR = address(0x4200000000000000000000000000000000000015); L1Block private constant L1BLOCK = L1Block(L1BLOCK_ADDR); From 5b3e6eb546c67881f8562c51f7c9b239a8e90b35 Mon Sep 17 00:00:00 2001 From: Morgan Kuphal Date: Fri, 17 May 2024 14:26:20 -0500 Subject: [PATCH 4/9] Functions v1_3_1 --- .../functions/v1_1_0/FunctionsBilling.sol | 7 +- .../v1_1_0/libraries/ChainSpecificUtil.sol | 44 +- .../functions/v1_3_1/FunctionsBilling.sol | 440 ++++++++++++++++++ .../functions/v1_3_1/FunctionsCoordinator.sol | 228 +++++++++ .../v1_3_1/interfaces/IFunctionsBilling.sol | 74 +++ .../v1_3_1/libraries/ChainSpecificUtil.sol | 79 ++++ .../v0.17.1/src/L2}/L1Block.sol | 5 +- .../v0.17.1/src/universal}/ISemver.sol | 0 8 files changed, 837 insertions(+), 40 deletions(-) create mode 100644 contracts/src/v0.8/functions/v1_3_1/FunctionsBilling.sol create mode 100644 contracts/src/v0.8/functions/v1_3_1/FunctionsCoordinator.sol create mode 100644 contracts/src/v0.8/functions/v1_3_1/interfaces/IFunctionsBilling.sol create mode 100644 contracts/src/v0.8/functions/v1_3_1/libraries/ChainSpecificUtil.sol rename contracts/src/v0.8/{functions/v1_1_0/libraries => vendor/@eth-optimism/contracts-bedrock/v0.17.1/src/L2}/L1Block.sol (94%) rename contracts/src/v0.8/{functions/v1_1_0/libraries => vendor/@eth-optimism/contracts-bedrock/v0.17.1/src/universal}/ISemver.sol (100%) diff --git a/contracts/src/v0.8/functions/v1_1_0/FunctionsBilling.sol b/contracts/src/v0.8/functions/v1_1_0/FunctionsBilling.sol index 0656a492304..ff345003741 100644 --- a/contracts/src/v0.8/functions/v1_1_0/FunctionsBilling.sol +++ b/contracts/src/v0.8/functions/v1_1_0/FunctionsBilling.sol @@ -50,8 +50,7 @@ abstract contract FunctionsBilling is Routable, IFunctionsBilling { uint40 minimumEstimateGasPriceWei; // ║ The lowest amount of wei that will be used as the tx.gasprice when estimating the cost to fulfill the request uint16 maxSupportedRequestDataVersion; // ═══════╝ The highest support request data version supported by the node. All lower versions should also be supported. uint224 fallbackNativePerUnitLink; // ═══════════╗ Fallback NATIVE CURRENCY / LINK conversion rate if the data feed is stale - uint32 requestTimeoutSeconds; // ║ How many seconds it takes before we consider a request to be timed out - uint24 transmitTxSizeBytes; // ══════════════════╝ The size of the transmit transaction in bytes used to calculate the L1 data fee on L2s + uint32 requestTimeoutSeconds; // ════════════════╝ How many seconds it takes before we consider a request to be timed out } Config private s_config; @@ -181,7 +180,7 @@ abstract contract FunctionsBilling is Routable, IFunctionsBilling { /// @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; - uint256 l1FeeWei = ChainSpecificUtil._getL1DataGasCostUpperLimit(s_config.transmitTxSizeBytes); + uint256 l1FeeWei = ChainSpecificUtil._getCurrentTxL1GasFees(msg.data); uint96 estimatedGasReimbursementJuels = _getJuelsFromWei((gasPriceWithOverestimation * executionGas) + l1FeeWei); uint96 feesJuels = uint96(donFee) + uint96(adminFee); @@ -275,7 +274,7 @@ abstract contract FunctionsBilling is Routable, IFunctionsBilling { FunctionsResponse.Commitment memory commitment = abi.decode(onchainMetadata, (FunctionsResponse.Commitment)); uint256 gasOverheadWei = (commitment.gasOverheadBeforeCallback + commitment.gasOverheadAfterCallback) * tx.gasprice; - uint256 l1FeeShareWei = ChainSpecificUtil._getL1DataGasCostUpperLimit(msg.data) / reportBatchSize; + uint256 l1FeeShareWei = ChainSpecificUtil._getCurrentTxL1GasFees(msg.data) / reportBatchSize; // Gas overhead without callback uint96 gasOverheadJuels = _getJuelsFromWei(gasOverheadWei + l1FeeShareWei); uint96 juelsPerGas = _getJuelsFromWei(tx.gasprice); diff --git a/contracts/src/v0.8/functions/v1_1_0/libraries/ChainSpecificUtil.sol b/contracts/src/v0.8/functions/v1_1_0/libraries/ChainSpecificUtil.sol index 473d5636c03..68d346e676d 100644 --- a/contracts/src/v0.8/functions/v1_1_0/libraries/ChainSpecificUtil.sol +++ b/contracts/src/v0.8/functions/v1_1_0/libraries/ChainSpecificUtil.sol @@ -2,14 +2,13 @@ pragma solidity ^0.8.19; import {ArbGasInfo} from "../../../vendor/@arbitrum/nitro-contracts/src/precompiles/ArbGasInfo.sol"; -import {L1Block} from "./L1Block.sol"; +import {GasPriceOracle} from "../../../vendor/@eth-optimism/contracts-bedrock/v0.16.2/src/L2/GasPriceOracle.sol"; /// @dev A library that abstracts out opcodes that behave differently across chains. /// @dev The methods below return values that are pertinent to the given chain. library ChainSpecificUtil { // ------------ Start Arbitrum Constants ------------ - /// @dev ARB_L1_FEE_DATA_PADDING_SIZE is the L1 data padding for Optimism - uint256 private const ARB_L1_FEE_DATA_PADDING_SIZE = 140; + /// @dev ARBGAS_ADDR is the address of the ArbGasInfo precompile on Arbitrum. /// @dev reference: https://github.com/OffchainLabs/nitro/blob/v2.0.14/contracts/src/precompiles/ArbGasInfo.sol#L10 address private constant ARBGAS_ADDR = address(0x000000000000000000000000000000000000006C); @@ -22,11 +21,13 @@ library ChainSpecificUtil { // ------------ End Arbitrum Constants ------------ // ------------ Start Optimism Constants ------------ - /// @dev OP_L1_FEE_DATA_PADDING_SIZE is the L1 data padding for Optimism - uint256 private const OP_L1_FEE_DATA_PADDING_SIZE = 35; - /// @dev L1BLOCK_ADDR is the address of the L1Block precompile on Optimism. - address private constant L1BLOCK_ADDR = address(0x4200000000000000000000000000000000000015); - L1Block private constant L1BLOCK = L1Block(L1BLOCK_ADDR); + /// @dev L1_FEE_DATA_PADDING includes 35 bytes for L1 data padding for Optimism + bytes internal constant L1_FEE_DATA_PADDING = + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; + /// @dev OVM_GASPRICEORACLE_ADDR is the address of the GasPriceOracle precompile on Optimism. + /// @dev reference: https://community.optimism.io/docs/developers/build/transaction-fees/#estimating-the-l1-data-fee + address private constant OVM_GASPRICEORACLE_ADDR = address(0x420000000000000000000000000000000000000F); + GasPriceOracle private constant OVM_GASPRICEORACLE = GasPriceOracle(OVM_GASPRICEORACLE_ADDR); uint256 private constant OP_MAINNET_CHAIN_ID = 10; uint256 private constant OP_GOERLI_CHAIN_ID = 420; @@ -45,14 +46,12 @@ library ChainSpecificUtil { /// @notice On Arbitrum, the provided calldata is not used to calculate the fees. /// @notice On Optimism, the provided calldata is passed to the GasPriceOracle predeploy /// @notice and getL1Fee is called to get the fees. - function _getL1DataGasCostUpperLimit(bytes memory dataSizeBytes) internal view returns (uint256 l1FeeWei) { + function _getCurrentTxL1GasFees(bytes memory txCallData) internal view returns (uint256 l1FeeWei) { uint256 chainid = block.chainid; if (_isArbitrumChainId(chainid)) { - // https://docs.arbitrum.io/build-decentralized-apps/how-to-estimate-gas#where-do-we-get-all-this-information-from - (, uint256 l1PricePerByte, , , , ) = ARBGAS.getPricesInWei(); - return l1PricePerByte * (calldataSizeBytes + ARB_L1_FEE_DATA_PADDING_SIZE); + return ARBGAS.getCurrentTxL1GasFees(); } else if (_isOptimismChainId(chainid)) { - return _calculateOptimismL1FeeUpperLimit(dataSizeBytes + OP_L1_FEE_DATA_PADDING_SIZE); + return OVM_GASPRICEORACLE.getL1Fee(bytes.concat(txCallData, L1_FEE_DATA_PADDING)); } return 0; } @@ -76,23 +75,4 @@ library ChainSpecificUtil { chainId == BASE_GOERLI_CHAIN_ID || chainId == BASE_SEPOLIA_CHAIN_ID; } - - function _calculateOptimismL1FeeUpperLimit(uint256 dataSizeBytes) internal view returns (uint256) { - // from: https://docs.optimism.io/stack/transactions/fees#ecotone - // l1_data_fee = tx_compressed_size * weighted_gas_price - // weighted_gas_price = 16*base_fee_scalar*base_fee + blob_base_fee_scalar*blob_base_fee - // tx_compressed_size = [(count_zero_bytes(tx_data)*4 + count_non_zero_bytes(tx_data)*16)] / 16 - // note we conservatively assume all non-zero bytes, therefore: - // tx_compressed_size = tx_data_size_bytes - // l1_data_fee = tx_data_size_bytes * weighted_gas_price - uint256 l1BaseFeeWei = L1BLOCK.baseFee(); - uint256 l1BaseFeeScalar = L1BLOCK.baseFeeScalar(); - uint256 l1BlobBaseFeeWei = L1BLOCK.blobBaseFee(); - uint256 l1BlobBaseFeeScalar = L1BLOCK.blobBaseFeeScalar(); - - uint256 weightedGasPrice = 16 * l1BaseFeeScalar * l1BaseFee + l1BlobBaseFeeScalar * l1BlobBaseFeeWei; - - uint256 l1DataFeeWei = weightedGasPrice * dataSizeBytes; - return l1DataFeeWei; - } } diff --git a/contracts/src/v0.8/functions/v1_3_1/FunctionsBilling.sol b/contracts/src/v0.8/functions/v1_3_1/FunctionsBilling.sol new file mode 100644 index 00000000000..8d5a61a4102 --- /dev/null +++ b/contracts/src/v0.8/functions/v1_3_1/FunctionsBilling.sol @@ -0,0 +1,440 @@ +// 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 "./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"; + +import {ChainSpecificUtil} from "../v1_1_0/libraries/ChainSpecificUtil.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; + uint256 l1FeeWei = ChainSpecificUtil._getL1FeeUpperLimit(s_config.transmitTxSizeBytes); + uint96 estimatedGasReimbursementJuels = _getJuelsFromWei((gasPriceWithOverestimation * executionGas) + l1FeeWei); + + 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; + uint256 l1FeeShareWei = ChainSpecificUtil._getL1FeeUpperLimit(msg.data.length) / reportBatchSize; + // Gas overhead without callback + uint96 gasOverheadJuels = _getJuelsFromWei(gasOverheadWei + l1FeeShareWei); + 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); +} diff --git a/contracts/src/v0.8/functions/v1_3_1/FunctionsCoordinator.sol b/contracts/src/v0.8/functions/v1_3_1/FunctionsCoordinator.sol new file mode 100644 index 00000000000..06b135c5eb4 --- /dev/null +++ b/contracts/src/v0.8/functions/v1_3_1/FunctionsCoordinator.sol @@ -0,0 +1,228 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IFunctionsCoordinator} from "../v1_0_0/interfaces/IFunctionsCoordinator.sol"; +import {ITypeAndVersion} from "../../shared/interfaces/ITypeAndVersion.sol"; + +import {FunctionsBilling, FunctionsBillingConfig} from "./FunctionsBilling.sol"; +import {OCR2Base} from "../v1_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.1"; + + event OracleRequest( + bytes32 indexed requestId, + address indexed requestingContract, + address requestInitiator, + uint64 subscriptionId, + address subscriptionOwner, + bytes data, + uint16 dataVersion, + bytes32 flags, + uint64 callbackGasLimit, + FunctionsResponse.Commitment commitment + ); + event OracleResponse(bytes32 indexed requestId, address transmitter); + + error InconsistentReportData(); + error EmptyPublicKey(); + error UnauthorizedPublicKeyChange(); + + bytes private s_donPublicKey; + bytes private s_thresholdPublicKey; + + constructor( + address router, + FunctionsBillingConfig memory config, + address linkToNativeFeed, + address linkToUsdFeed + ) OCR2Base() FunctionsBilling(router, config, linkToNativeFeed, linkToUsdFeed) {} + + /// @inheritdoc IFunctionsCoordinator + function getThresholdPublicKey() external view override returns (bytes memory) { + if (s_thresholdPublicKey.length == 0) { + revert EmptyPublicKey(); + } + return s_thresholdPublicKey; + } + + /// @inheritdoc IFunctionsCoordinator + function setThresholdPublicKey(bytes calldata thresholdPublicKey) external override onlyOwner { + if (thresholdPublicKey.length == 0) { + revert EmptyPublicKey(); + } + s_thresholdPublicKey = thresholdPublicKey; + } + + /// @inheritdoc IFunctionsCoordinator + function getDONPublicKey() external view override returns (bytes memory) { + if (s_donPublicKey.length == 0) { + revert EmptyPublicKey(); + } + return s_donPublicKey; + } + + /// @inheritdoc IFunctionsCoordinator + function setDONPublicKey(bytes calldata donPublicKey) external override onlyOwner { + if (donPublicKey.length == 0) { + revert EmptyPublicKey(); + } + s_donPublicKey = donPublicKey; + } + + /// @dev check if node is in current transmitter list + function _isTransmitter(address node) internal view returns (bool) { + // Bounded by "maxNumOracles" on OCR2Abstract.sol + for (uint256 i = 0; i < s_transmitters.length; ++i) { + if (s_transmitters[i] == node) { + return true; + } + } + return false; + } + + /// @inheritdoc IFunctionsCoordinator + function startRequest( + FunctionsResponse.RequestMeta calldata request + ) external override onlyRouter returns (FunctionsResponse.Commitment memory commitment) { + uint72 operationFee; + (commitment, operationFee) = _startBilling(request); + + emit OracleRequest( + commitment.requestId, + request.requestingContract, + // solhint-disable-next-line avoid-tx-origin + tx.origin, + request.subscriptionId, + request.subscriptionOwner, + request.data, + request.dataVersion, + request.flags, + request.callbackGasLimit, + FunctionsResponse.Commitment({ + coordinator: commitment.coordinator, + client: commitment.client, + subscriptionId: commitment.subscriptionId, + callbackGasLimit: commitment.callbackGasLimit, + estimatedTotalCostJuels: commitment.estimatedTotalCostJuels, + timeoutTimestamp: commitment.timeoutTimestamp, + requestId: commitment.requestId, + donFee: commitment.donFee, + gasOverheadBeforeCallback: commitment.gasOverheadBeforeCallback, + gasOverheadAfterCallback: commitment.gasOverheadAfterCallback, + // The following line is done to use the Coordinator's operationFee in place of the Router's operation fee + // With this in place the Router.adminFee must be set to 0 in the Router. + adminFee: operationFee + }) + ); + + return commitment; + } + + /// @dev DON fees are pooled together. If the OCR configuration is going to change, these need to be distributed. + function _beforeSetConfig(uint8 /* _f */, bytes memory /* _onchainConfig */) internal override { + if (_getTransmitters().length > 0) { + _disperseFeePool(); + } + } + + /// @dev Used by FunctionsBilling.sol + function _getTransmitters() internal view override returns (address[] memory) { + return s_transmitters; + } + + function _beforeTransmit( + bytes calldata report + ) internal view override returns (bool shouldStop, DecodedReport memory decodedReport) { + ( + bytes32[] memory requestIds, + bytes[] memory results, + bytes[] memory errors, + bytes[] memory onchainMetadata, + bytes[] memory offchainMetadata + ) = abi.decode(report, (bytes32[], bytes[], bytes[], bytes[], bytes[])); + uint256 numberOfFulfillments = uint8(requestIds.length); + + if ( + numberOfFulfillments == 0 || + numberOfFulfillments != results.length || + numberOfFulfillments != errors.length || + numberOfFulfillments != onchainMetadata.length || + numberOfFulfillments != offchainMetadata.length + ) { + revert ReportInvalid("Fields must be equal length"); + } + + for (uint256 i = 0; i < numberOfFulfillments; ++i) { + if (_isExistingRequest(requestIds[i])) { + // If there is an existing request, validate report + // Leave shouldStop to default, false + break; + } + if (i == numberOfFulfillments - 1) { + // If the last fulfillment on the report does not exist, then all are duplicates + // Indicate that it's safe to stop to save on the gas of validating the report + shouldStop = true; + } + } + + return ( + shouldStop, + DecodedReport({ + requestIds: requestIds, + results: results, + errors: errors, + onchainMetadata: onchainMetadata, + offchainMetadata: offchainMetadata + }) + ); + } + + /// @dev Report hook called within OCR2Base.sol + function _report(DecodedReport memory decodedReport) internal override { + uint256 numberOfFulfillments = uint8(decodedReport.requestIds.length); + + // Bounded by "MaxRequestBatchSize" on the Job's ReportingPluginConfig + for (uint256 i = 0; i < numberOfFulfillments; ++i) { + FunctionsResponse.FulfillResult result = FunctionsResponse.FulfillResult( + _fulfillAndBill( + decodedReport.requestIds[i], + decodedReport.results[i], + decodedReport.errors[i], + decodedReport.onchainMetadata[i], + decodedReport.offchainMetadata[i], + uint8(numberOfFulfillments) // will not exceed "MaxRequestBatchSize" on the Job's ReportingPluginConfig + ) + ); + + // Emit on successfully processing the fulfillment + // In these two fulfillment results the user has been charged + // Otherwise, the DON will re-try + if ( + result == FunctionsResponse.FulfillResult.FULFILLED || + result == FunctionsResponse.FulfillResult.USER_CALLBACK_ERROR + ) { + emit OracleResponse(decodedReport.requestIds[i], msg.sender); + } + } + } + + /// @dev Used in FunctionsBilling.sol + function _onlyOwner() internal view override { + _validateOwnership(); + } + + /// @dev Used in FunctionsBilling.sol + function _owner() internal view override returns (address owner) { + return this.owner(); + } +} diff --git a/contracts/src/v0.8/functions/v1_3_1/interfaces/IFunctionsBilling.sol b/contracts/src/v0.8/functions/v1_3_1/interfaces/IFunctionsBilling.sol new file mode 100644 index 00000000000..66a9f24577f --- /dev/null +++ b/contracts/src/v0.8/functions/v1_3_1/interfaces/IFunctionsBilling.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +/// @title Chainlink Functions DON billing interface. +interface IFunctionsBilling { + /// @notice Return the current conversion from WEI of ETH to LINK from the configured Chainlink data feed + /// @return weiPerUnitLink - The amount of WEI in one LINK + function getWeiPerUnitLink() external view returns (uint256); + + /// @notice Return the current conversion from LINK to USD from the configured Chainlink data feed + /// @return weiPerUnitLink - The amount of USD that one LINK is worth + /// @return decimals - The number of decimals that should be represented in the price feed's response + function getUsdPerUnitLink() external view returns (uint256, uint8); + + /// @notice Determine the fee that will be split between Node Operators for servicing a request + /// @param requestCBOR - CBOR encoded Chainlink Functions request data, use FunctionsRequest library to encode a request + /// @return fee - Cost in Juels (1e18) of LINK + function getDONFeeJuels(bytes memory requestCBOR) external view returns (uint72); + + /// @notice Determine the fee that will be paid to the Coordinator owner for operating the network + /// @return fee - Cost in Juels (1e18) of LINK + function getOperationFeeJuels() external view returns (uint72); + + /// @notice Determine the fee that will be paid to the Router owner for operating the network + /// @return fee - Cost in Juels (1e18) of LINK + function getAdminFeeJuels() external view returns (uint72); + + /// @notice Estimate the total cost that will be charged to a subscription to make a request: transmitter gas re-reimbursement, plus DON fee, plus Registry fee + /// @param - subscriptionId An identifier of the billing account + /// @param - data Encoded Chainlink Functions request data, use FunctionsClient API to encode a request + /// @param - callbackGasLimit Gas limit for the fulfillment callback + /// @param - gasPriceWei The blockchain's gas price to estimate with + /// @return - billedCost Cost in Juels (1e18) of LINK + function estimateCost( + uint64 subscriptionId, + bytes calldata data, + uint32 callbackGasLimit, + uint256 gasPriceWei + ) external view returns (uint96); + + /// @notice Remove a request commitment that the Router has determined to be stale + /// @param requestId - The request ID to remove + function deleteCommitment(bytes32 requestId) external; + + /// @notice Oracle withdraw LINK earned through fulfilling requests + /// @notice If amount is 0 the full balance will be withdrawn + /// @param recipient where to send the funds + /// @param amount amount to withdraw + function oracleWithdraw(address recipient, uint96 amount) external; + + /// @notice Withdraw all LINK earned by Oracles through fulfilling requests + /// @dev transmitter addresses must support LINK tokens to avoid tokens from getting stuck as oracleWithdrawAll() calls will forward tokens directly to transmitters + function oracleWithdrawAll() external; +} + +// ================================================================ +// | Configuration state | +// ================================================================ + +struct FunctionsBillingConfig { + uint32 fulfillmentGasPriceOverEstimationBP; // ══╗ Percentage of gas price overestimation to account for changes in gas price between request and response. Held as basis points (one hundredth of 1 percentage point) + uint32 feedStalenessSeconds; // ║ How long before we consider the feed price to be stale and fallback to fallbackNativePerUnitLink. + uint32 gasOverheadBeforeCallback; // ║ Represents the average gas execution cost before the fulfillment callback. This amount is always billed for every request. + uint32 gasOverheadAfterCallback; // ║ Represents the average gas execution cost after the fulfillment callback. This amount is always billed for every request. + uint40 minimumEstimateGasPriceWei; // ║ The lowest amount of wei that will be used as the tx.gasprice when estimating the cost to fulfill the request + uint16 maxSupportedRequestDataVersion; // ║ The highest support request data version supported by the node. All lower versions should also be supported. + uint64 fallbackUsdPerUnitLink; // ║ Fallback LINK / USD conversion rate if the data feed is stale + uint8 fallbackUsdPerUnitLinkDecimals; // ════════╝ Fallback LINK / USD conversion rate decimal places if the data feed is stale + uint224 fallbackNativePerUnitLink; // ═══════════╗ Fallback NATIVE CURRENCY / LINK conversion rate if the data feed is stale + uint32 requestTimeoutSeconds; // ════════════════╝ How many seconds it takes before we consider a request to be timed out + uint16 donFeeCentsUsd; // ═══════════════════════════════╗ Additional flat fee (denominated in cents of USD, paid as LINK) that will be split between Node Operators. + uint16 operationFeeCentsUsd; // ║ Additional flat fee (denominated in cents of USD, paid as LINK) that will be paid to the owner of the Coordinator contract. + uint24 transmitTxSizeBytes; // ══════════════════════════╝ The size of the transmit transaction in bytes assuming a single 256 byte response payload. Used to estimate L1 cost for fulfillments on L2 chains. +} diff --git a/contracts/src/v0.8/functions/v1_3_1/libraries/ChainSpecificUtil.sol b/contracts/src/v0.8/functions/v1_3_1/libraries/ChainSpecificUtil.sol new file mode 100644 index 00000000000..0f625dd9151 --- /dev/null +++ b/contracts/src/v0.8/functions/v1_3_1/libraries/ChainSpecificUtil.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {ArbGasInfo} from "../../../vendor/@arbitrum/nitro-contracts/src/precompiles/ArbGasInfo.sol"; +import {L1Block} from "../../../vendor/@eth-optimism/contracts-bedrock/v0.17.1/src/L2/L1Block.sol"; + +/// @dev A library that abstracts out opcodes that behave differently across chains. +/// @dev The methods below return values that are pertinent to the given chain. +library ChainSpecificUtil { + // ------------ Start Arbitrum Constants ------------ + /// @dev ARB_L1_FEE_DATA_PADDING_SIZE is the L1 data padding for Optimism + uint256 private const ARB_L1_FEE_DATA_PADDING_SIZE = 140; + /// @dev ARBGAS_ADDR is the address of the ArbGasInfo precompile on Arbitrum. + address private constant ARBGAS_ADDR = address(0x000000000000000000000000000000000000006C); + ArbGasInfo private constant ARBGAS = ArbGasInfo(ARBGAS_ADDR); + + uint256 private constant ARB_MAINNET_CHAIN_ID = 42161; + uint256 private constant ARB_GOERLI_TESTNET_CHAIN_ID = 421613; + uint256 private constant ARB_SEPOLIA_TESTNET_CHAIN_ID = 421614; + // ------------ End Arbitrum Constants ------------ + + // ------------ Start Optimism Constants ------------ + /// @dev OP_L1_FEE_DATA_PADDING_SIZE is the L1 data padding for Optimism + uint256 private const OP_L1_FEE_DATA_PADDING_SIZE = 35; + /// @dev L1BLOCK_ADDR is the address of the L1Block precompile on Optimism. + address private constant L1BLOCK_ADDR = address(0x4200000000000000000000000000000000000015); + L1Block private constant L1BLOCK = L1Block(L1BLOCK_ADDR); + + uint256 private constant OP_MAINNET_CHAIN_ID = 10; + uint256 private constant OP_GOERLI_CHAIN_ID = 420; + uint256 private constant OP_SEPOLIA_CHAIN_ID = 11155420; + + /// @dev Base is a OP stack based rollup and follows the same L1 pricing logic as Optimism. + uint256 private constant BASE_MAINNET_CHAIN_ID = 8453; + uint256 private constant BASE_GOERLI_CHAIN_ID = 84531; + uint256 private constant BASE_SEPOLIA_CHAIN_ID = 84532; + // ------------ End Optimism Constants ------------ + + /// @notice Returns the L1 fees in wei that will be paid for L2 chains + /// @notice based on the size of the transaction data and the current gas conditions. + function _getL1FeeUpperLimit(uint256 dataSizeBytes) internal view returns (uint256 l1FeeWei) { + uint256 chainid = block.chainid; + if (_isArbitrumChainId(chainid)) { + // https://docs.arbitrum.io/build-decentralized-apps/how-to-estimate-gas#where-do-we-get-all-this-information-from + (, uint256 l1PricePerByte, , , , ) = ARBGAS.getPricesInWei(); + return l1PricePerByte * (dataSizeBytes + ARB_L1_FEE_DATA_PADDING_SIZE); + } else if (_isOptimismChainId(chainid)) { + // https://docs.optimism.io/stack/transactions/fees#ecotone + // note we conservatively assume all non-zero bytes: tx_compressed_size = tx_data_size_bytes + uint256 l1BaseFeeWei = L1BLOCK.baseFee(); + uint256 l1BaseFeeScalar = L1BLOCK.baseFeeScalar(); + uint256 l1BlobBaseFeeWei = L1BLOCK.blobBaseFee(); + uint256 l1BlobBaseFeeScalar = L1BLOCK.blobBaseFeeScalar(); + uint256 weightedGasPrice = 16 * l1BaseFeeScalar * l1BaseFee + l1BlobBaseFeeScalar * l1BlobBaseFeeWei; + return weightedGasPrice * dataSizeBytes; + } + return 0; + } + + /// @notice Return true if and only if the provided chain ID is an Arbitrum chain ID. + function _isArbitrumChainId(uint256 chainId) internal pure returns (bool) { + return + chainId == ARB_MAINNET_CHAIN_ID || + chainId == ARB_GOERLI_TESTNET_CHAIN_ID || + chainId == ARB_SEPOLIA_TESTNET_CHAIN_ID; + } + + /// @notice Return true if and only if the provided chain ID is an Optimism (or Base) chain ID. + /// @notice Note that optimism chain id's are also OP stack chain id's. + function _isOptimismChainId(uint256 chainId) internal pure returns (bool) { + return + chainId == OP_MAINNET_CHAIN_ID || + chainId == OP_GOERLI_CHAIN_ID || + chainId == OP_SEPOLIA_CHAIN_ID || + chainId == BASE_MAINNET_CHAIN_ID || + chainId == BASE_GOERLI_CHAIN_ID || + chainId == BASE_SEPOLIA_CHAIN_ID; + } +} diff --git a/contracts/src/v0.8/functions/v1_1_0/libraries/L1Block.sol b/contracts/src/v0.8/vendor/@eth-optimism/contracts-bedrock/v0.17.1/src/L2/L1Block.sol similarity index 94% rename from contracts/src/v0.8/functions/v1_1_0/libraries/L1Block.sol rename to contracts/src/v0.8/vendor/@eth-optimism/contracts-bedrock/v0.17.1/src/L2/L1Block.sol index 2a3acc6550f..b2ba1ba36db 100644 --- a/contracts/src/v0.8/functions/v1_1_0/libraries/L1Block.sol +++ b/contracts/src/v0.8/vendor/@eth-optimism/contracts-bedrock/v0.17.1/src/L2/L1Block.sol @@ -1,10 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Copied from the implementation contract for https://optimistic.etherscan.io/address/0x4200000000000000000000000000000000000015#readProxyContract -// https://optimistic.etherscan.io/address/0x07dbe8500fc591d1852b76fee44d5a05e13097ff#code - -import { ISemver } from "./ISemver.sol"; +import { ISemver } from "../universal/ISemver.sol"; /// @custom:proxied /// @custom:predeploy 0x4200000000000000000000000000000000000015 diff --git a/contracts/src/v0.8/functions/v1_1_0/libraries/ISemver.sol b/contracts/src/v0.8/vendor/@eth-optimism/contracts-bedrock/v0.17.1/src/universal/ISemver.sol similarity index 100% rename from contracts/src/v0.8/functions/v1_1_0/libraries/ISemver.sol rename to contracts/src/v0.8/vendor/@eth-optimism/contracts-bedrock/v0.17.1/src/universal/ISemver.sol From 99a1b2ed0d76e6ee9c6193e813df5b063c06a64f Mon Sep 17 00:00:00 2001 From: Morgan Kuphal Date: Fri, 17 May 2024 14:37:20 -0500 Subject: [PATCH 5/9] Include l1Fee when calculating overestimate --- .../src/v0.8/functions/v1_3_1/FunctionsBilling.sol | 12 ++++++------ .../functions/v1_3_1/libraries/ChainSpecificUtil.sol | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/contracts/src/v0.8/functions/v1_3_1/FunctionsBilling.sol b/contracts/src/v0.8/functions/v1_3_1/FunctionsBilling.sol index 8d5a61a4102..81361fa981b 100644 --- a/contracts/src/v0.8/functions/v1_3_1/FunctionsBilling.sol +++ b/contracts/src/v0.8/functions/v1_3_1/FunctionsBilling.sol @@ -10,7 +10,7 @@ import {FunctionsResponse} from "../v1_0_0/libraries/FunctionsResponse.sol"; import {SafeCast} from "../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/math/SafeCast.sol"; -import {ChainSpecificUtil} from "../v1_1_0/libraries/ChainSpecificUtil.sol"; +import {ChainSpecificUtil} from "./libraries/ChainSpecificUtil.sol"; /// @title Functions Billing contract /// @notice Contract that calculates payment from users to the nodes of the Decentralized Oracle Network (DON). @@ -197,13 +197,13 @@ abstract contract FunctionsBilling is Routable, IFunctionsBilling { 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; uint256 l1FeeWei = ChainSpecificUtil._getL1FeeUpperLimit(s_config.transmitTxSizeBytes); - uint96 estimatedGasReimbursementJuels = _getJuelsFromWei((gasPriceWithOverestimation * executionGas) + l1FeeWei); + + uint256 totalFeeWei = (gasPriceWei * executionGas) + l1FeeWei; + uint256 totalFeeWeiWithOverestimate = totalFee + ((totalFee * s_config.fulfillmentGasPriceOverEstimationBP) / 10_000); + + uint96 estimatedGasReimbursementJuels = _getJuelsFromWei(totalFeeWeiWithOverestimate); uint96 feesJuels = uint96(donFeeJuels) + uint96(adminFeeJuels) + uint96(operationFeeJuels); diff --git a/contracts/src/v0.8/functions/v1_3_1/libraries/ChainSpecificUtil.sol b/contracts/src/v0.8/functions/v1_3_1/libraries/ChainSpecificUtil.sol index 0f625dd9151..12d945cea92 100644 --- a/contracts/src/v0.8/functions/v1_3_1/libraries/ChainSpecificUtil.sol +++ b/contracts/src/v0.8/functions/v1_3_1/libraries/ChainSpecificUtil.sol @@ -36,7 +36,7 @@ library ChainSpecificUtil { uint256 private constant BASE_SEPOLIA_CHAIN_ID = 84532; // ------------ End Optimism Constants ------------ - /// @notice Returns the L1 fees in wei that will be paid for L2 chains + /// @notice Returns the upper limit estimate of the L1 fees in wei that will be paid for L2 chains /// @notice based on the size of the transaction data and the current gas conditions. function _getL1FeeUpperLimit(uint256 dataSizeBytes) internal view returns (uint256 l1FeeWei) { uint256 chainid = block.chainid; From e093ced6180957e1d15bfe72872949e074ff9113 Mon Sep 17 00:00:00 2001 From: Morgan Kuphal Date: Fri, 17 May 2024 14:59:32 -0500 Subject: [PATCH 6/9] reduce diff --- .../src/v0.8/functions/v1_3_1/libraries/ChainSpecificUtil.sol | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contracts/src/v0.8/functions/v1_3_1/libraries/ChainSpecificUtil.sol b/contracts/src/v0.8/functions/v1_3_1/libraries/ChainSpecificUtil.sol index 12d945cea92..eae4075e5b1 100644 --- a/contracts/src/v0.8/functions/v1_3_1/libraries/ChainSpecificUtil.sol +++ b/contracts/src/v0.8/functions/v1_3_1/libraries/ChainSpecificUtil.sol @@ -11,12 +11,14 @@ library ChainSpecificUtil { /// @dev ARB_L1_FEE_DATA_PADDING_SIZE is the L1 data padding for Optimism uint256 private const ARB_L1_FEE_DATA_PADDING_SIZE = 140; /// @dev ARBGAS_ADDR is the address of the ArbGasInfo precompile on Arbitrum. + /// @dev reference: https://github.com/OffchainLabs/nitro/blob/v2.0.14/contracts/src/precompiles/ArbGasInfo.sol#L10 address private constant ARBGAS_ADDR = address(0x000000000000000000000000000000000000006C); ArbGasInfo private constant ARBGAS = ArbGasInfo(ARBGAS_ADDR); uint256 private constant ARB_MAINNET_CHAIN_ID = 42161; uint256 private constant ARB_GOERLI_TESTNET_CHAIN_ID = 421613; uint256 private constant ARB_SEPOLIA_TESTNET_CHAIN_ID = 421614; + // ------------ End Arbitrum Constants ------------ // ------------ Start Optimism Constants ------------ @@ -34,6 +36,7 @@ library ChainSpecificUtil { uint256 private constant BASE_MAINNET_CHAIN_ID = 8453; uint256 private constant BASE_GOERLI_CHAIN_ID = 84531; uint256 private constant BASE_SEPOLIA_CHAIN_ID = 84532; + // ------------ End Optimism Constants ------------ /// @notice Returns the upper limit estimate of the L1 fees in wei that will be paid for L2 chains From 126118e656da78676c4ab375e3405023d9a7a7ca Mon Sep 17 00:00:00 2001 From: Morgan Kuphal Date: Fri, 17 May 2024 15:12:58 -0500 Subject: [PATCH 7/9] Added to dev/v1_X --- .../functions/dev/v1_X/FunctionsBilling.sol | 21 ++++------ .../dev/v1_X/FunctionsCoordinator.sol | 2 +- .../dev/v1_X/interfaces/IFunctionsBilling.sol | 5 ++- .../dev/v1_X/libraries/ChainSpecificUtil.sol | 39 +++++++++++-------- 4 files changed, 34 insertions(+), 33 deletions(-) diff --git a/contracts/src/v0.8/functions/dev/v1_X/FunctionsBilling.sol b/contracts/src/v0.8/functions/dev/v1_X/FunctionsBilling.sol index c973f55a715..9be1c9707e5 100644 --- a/contracts/src/v0.8/functions/dev/v1_X/FunctionsBilling.sol +++ b/contracts/src/v0.8/functions/dev/v1_X/FunctionsBilling.sol @@ -124,7 +124,6 @@ abstract contract FunctionsBilling is Routable, IFunctionsBilling { /// @inheritdoc IFunctionsBilling function getWeiPerUnitLink() public view returns (uint256) { (, int256 weiPerUnitLink, , uint256 timestamp, ) = s_linkToNativeFeed.latestRoundData(); - // Only fallback if feedStalenessSeconds is set // solhint-disable-next-line not-rely-on-time if (s_config.feedStalenessSeconds < block.timestamp - timestamp && s_config.feedStalenessSeconds > 0) { return s_config.fallbackNativePerUnitLink; @@ -144,7 +143,6 @@ abstract contract FunctionsBilling is Routable, IFunctionsBilling { /// @inheritdoc IFunctionsBilling function getUsdPerUnitLink() public view returns (uint256, uint8) { (, int256 usdPerUnitLink, , uint256 timestamp, ) = s_linkToUsdFeed.latestRoundData(); - // Only fallback if feedStalenessSeconds is set // 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); @@ -199,13 +197,14 @@ abstract contract FunctionsBilling is Routable, IFunctionsBilling { 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; - uint256 l1FeeWei = ChainSpecificUtil._getCurrentTxL1GasFees(msg.data); - uint96 estimatedGasReimbursementJuels = _getJuelsFromWei((gasPriceWithOverestimation * executionGas) + l1FeeWei); + uint256 l1FeeWei = ChainSpecificUtil._getL1FeeUpperLimit(s_config.transmitTxSizeBytes); + + uint256 totalFeeWei = (gasPriceWei * executionGas) + l1FeeWei; + // Basis Points are 1/100th of 1%, divide by 10_000 to bring back to original units + uint256 totalFeeWeiWithOverestimate = totalFee + ((totalFee * s_config.fulfillmentGasPriceOverEstimationBP) / 10_000); + + uint96 estimatedGasReimbursementJuels = _getJuelsFromWei(totalFeeWeiWithOverestimate); uint96 feesJuels = uint96(donFeeJuels) + uint96(adminFeeJuels) + uint96(operationFeeJuels); @@ -298,7 +297,7 @@ abstract contract FunctionsBilling is Routable, IFunctionsBilling { FunctionsResponse.Commitment memory commitment = abi.decode(onchainMetadata, (FunctionsResponse.Commitment)); uint256 gasOverheadWei = (commitment.gasOverheadBeforeCallback + commitment.gasOverheadAfterCallback) * tx.gasprice; - uint256 l1FeeShareWei = ChainSpecificUtil._getCurrentTxL1GasFees(msg.data) / reportBatchSize; + uint256 l1FeeShareWei = ChainSpecificUtil._getL1FeeUpperLimit(msg.data.length) / reportBatchSize; // Gas overhead without callback uint96 gasOverheadJuels = _getJuelsFromWei(gasOverheadWei + l1FeeShareWei); uint96 juelsPerGas = _getJuelsFromWei(tx.gasprice); @@ -422,10 +421,6 @@ abstract contract FunctionsBilling is Routable, IFunctionsBilling { revert NoTransmittersSet(); } uint96 feePoolShare = s_feePool / uint96(numberOfTransmitters); - if (feePoolShare == 0) { - // Dust cannot be evenly distributed to all transmitters - return; - } // Bounded by "maxNumOracles" on OCR2Abstract.sol for (uint256 i = 0; i < numberOfTransmitters; ++i) { s_withdrawableTokens[transmitters[i]] += feePoolShare; diff --git a/contracts/src/v0.8/functions/dev/v1_X/FunctionsCoordinator.sol b/contracts/src/v0.8/functions/dev/v1_X/FunctionsCoordinator.sol index f0bec7c3e9c..69d6f3a7b0a 100644 --- a/contracts/src/v0.8/functions/dev/v1_X/FunctionsCoordinator.sol +++ b/contracts/src/v0.8/functions/dev/v1_X/FunctionsCoordinator.sol @@ -16,7 +16,7 @@ contract FunctionsCoordinator is OCR2Base, IFunctionsCoordinator, FunctionsBilli using FunctionsResponse for FunctionsResponse.FulfillResult; /// @inheritdoc ITypeAndVersion - string public constant override typeAndVersion = "Functions Coordinator v1.3.0"; + string public constant override typeAndVersion = "Functions Coordinator v1.3.1"; event OracleRequest( bytes32 indexed requestId, diff --git a/contracts/src/v0.8/functions/dev/v1_X/interfaces/IFunctionsBilling.sol b/contracts/src/v0.8/functions/dev/v1_X/interfaces/IFunctionsBilling.sol index ecf15c68f0d..66a9f24577f 100644 --- a/contracts/src/v0.8/functions/dev/v1_X/interfaces/IFunctionsBilling.sol +++ b/contracts/src/v0.8/functions/dev/v1_X/interfaces/IFunctionsBilling.sol @@ -59,7 +59,7 @@ interface IFunctionsBilling { struct FunctionsBillingConfig { uint32 fulfillmentGasPriceOverEstimationBP; // ══╗ Percentage of gas price overestimation to account for changes in gas price between request and response. Held as basis points (one hundredth of 1 percentage point) - uint32 feedStalenessSeconds; // ║ How long before we consider the feed price to be stale and fallback to fallbackNativePerUnitLink. Default of 0 means no fallback. + uint32 feedStalenessSeconds; // ║ How long before we consider the feed price to be stale and fallback to fallbackNativePerUnitLink. uint32 gasOverheadBeforeCallback; // ║ Represents the average gas execution cost before the fulfillment callback. This amount is always billed for every request. uint32 gasOverheadAfterCallback; // ║ Represents the average gas execution cost after the fulfillment callback. This amount is always billed for every request. uint40 minimumEstimateGasPriceWei; // ║ The lowest amount of wei that will be used as the tx.gasprice when estimating the cost to fulfill the request @@ -69,5 +69,6 @@ struct FunctionsBillingConfig { uint224 fallbackNativePerUnitLink; // ═══════════╗ Fallback NATIVE CURRENCY / LINK conversion rate if the data feed is stale uint32 requestTimeoutSeconds; // ════════════════╝ How many seconds it takes before we consider a request to be timed out uint16 donFeeCentsUsd; // ═══════════════════════════════╗ Additional flat fee (denominated in cents of USD, paid as LINK) that will be split between Node Operators. - uint16 operationFeeCentsUsd; // ═════════════════════════╝ Additional flat fee (denominated in cents of USD, paid as LINK) that will be paid to the owner of the Coordinator contract. + uint16 operationFeeCentsUsd; // ║ Additional flat fee (denominated in cents of USD, paid as LINK) that will be paid to the owner of the Coordinator contract. + uint24 transmitTxSizeBytes; // ══════════════════════════╝ The size of the transmit transaction in bytes assuming a single 256 byte response payload. Used to estimate L1 cost for fulfillments on L2 chains. } diff --git a/contracts/src/v0.8/functions/dev/v1_X/libraries/ChainSpecificUtil.sol b/contracts/src/v0.8/functions/dev/v1_X/libraries/ChainSpecificUtil.sol index 574d1bf1645..b6d9ae32418 100644 --- a/contracts/src/v0.8/functions/dev/v1_X/libraries/ChainSpecificUtil.sol +++ b/contracts/src/v0.8/functions/dev/v1_X/libraries/ChainSpecificUtil.sol @@ -2,13 +2,15 @@ pragma solidity ^0.8.19; import {ArbGasInfo} from "../../../../vendor/@arbitrum/nitro-contracts/src/precompiles/ArbGasInfo.sol"; -import {GasPriceOracle} from "../../../../vendor/@eth-optimism/contracts-bedrock/v0.16.2/src/L2/GasPriceOracle.sol"; +import {L1Block} from "../../../../vendor/@eth-optimism/contracts-bedrock/v0.17.1/src/L2/L1Block.sol"; /// @dev A library that abstracts out opcodes that behave differently across chains. /// @dev The methods below return values that are pertinent to the given chain. library ChainSpecificUtil { // ------------ Start Arbitrum Constants ------------ + /// @dev ARB_L1_FEE_DATA_PADDING_SIZE is the L1 data padding for Optimism + uint256 private const ARB_L1_FEE_DATA_PADDING_SIZE = 140; /// @dev ARBGAS_ADDR is the address of the ArbGasInfo precompile on Arbitrum. /// @dev reference: https://github.com/OffchainLabs/nitro/blob/v2.0.14/contracts/src/precompiles/ArbGasInfo.sol#L10 address private constant ARBGAS_ADDR = address(0x000000000000000000000000000000000000006C); @@ -21,13 +23,11 @@ library ChainSpecificUtil { // ------------ End Arbitrum Constants ------------ // ------------ Start Optimism Constants ------------ - /// @dev L1_FEE_DATA_PADDING includes 35 bytes for L1 data padding for Optimism - bytes internal constant L1_FEE_DATA_PADDING = - "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; - /// @dev OVM_GASPRICEORACLE_ADDR is the address of the GasPriceOracle precompile on Optimism. - /// @dev reference: https://community.optimism.io/docs/developers/build/transaction-fees/#estimating-the-l1-data-fee - address private constant OVM_GASPRICEORACLE_ADDR = address(0x420000000000000000000000000000000000000F); - GasPriceOracle private constant OVM_GASPRICEORACLE = GasPriceOracle(OVM_GASPRICEORACLE_ADDR); + /// @dev OP_L1_FEE_DATA_PADDING_SIZE is the L1 data padding for Optimism + uint256 private const OP_L1_FEE_DATA_PADDING_SIZE = 35; + /// @dev L1BLOCK_ADDR is the address of the L1Block precompile on Optimism. + address private constant L1BLOCK_ADDR = address(0x4200000000000000000000000000000000000015); + L1Block private constant L1BLOCK = L1Block(L1BLOCK_ADDR); uint256 private constant OP_MAINNET_CHAIN_ID = 10; uint256 private constant OP_GOERLI_CHAIN_ID = 420; @@ -40,18 +40,23 @@ library ChainSpecificUtil { // ------------ End Optimism Constants ------------ - /// @notice Returns the L1 fees in wei that will be paid for the current transaction, given any calldata - /// @notice for the current transaction. - /// @notice When on a known Arbitrum chain, it uses ArbGas.getCurrentTxL1GasFees to get the fees. - /// @notice On Arbitrum, the provided calldata is not used to calculate the fees. - /// @notice On Optimism, the provided calldata is passed to the GasPriceOracle predeploy - /// @notice and getL1Fee is called to get the fees. - function _getCurrentTxL1GasFees(bytes memory txCallData) internal view returns (uint256 l1FeeWei) { + /// @notice Returns the upper limit estimate of the L1 fees in wei that will be paid for L2 chains + /// @notice based on the size of the transaction data and the current gas conditions. + function _getL1FeeUpperLimit(uint256 dataSizeBytes) internal view returns (uint256 l1FeeWei) { uint256 chainid = block.chainid; if (_isArbitrumChainId(chainid)) { - return ARBGAS.getCurrentTxL1GasFees(); + // https://docs.arbitrum.io/build-decentralized-apps/how-to-estimate-gas#where-do-we-get-all-this-information-from + (, uint256 l1PricePerByte, , , , ) = ARBGAS.getPricesInWei(); + return l1PricePerByte * (dataSizeBytes + ARB_L1_FEE_DATA_PADDING_SIZE); } else if (_isOptimismChainId(chainid)) { - return OVM_GASPRICEORACLE.getL1Fee(bytes.concat(txCallData, L1_FEE_DATA_PADDING)); + // https://docs.optimism.io/stack/transactions/fees#ecotone + // note we conservatively assume all non-zero bytes: tx_compressed_size = tx_data_size_bytes + uint256 l1BaseFeeWei = L1BLOCK.baseFee(); + uint256 l1BaseFeeScalar = L1BLOCK.baseFeeScalar(); + uint256 l1BlobBaseFeeWei = L1BLOCK.blobBaseFee(); + uint256 l1BlobBaseFeeScalar = L1BLOCK.blobBaseFeeScalar(); + uint256 weightedGasPrice = 16 * l1BaseFeeScalar * l1BaseFee + l1BlobBaseFeeScalar * l1BlobBaseFeeWei; + return weightedGasPrice * dataSizeBytes; } return 0; } From 402afb7d4cd3a235baa094a08792bc054b7fdd66 Mon Sep 17 00:00:00 2001 From: Morgan Kuphal Date: Fri, 17 May 2024 15:20:28 -0500 Subject: [PATCH 8/9] added trailing newlines --- .../@eth-optimism/contracts-bedrock/v0.17.1/src/L2/L1Block.sol | 2 +- .../contracts-bedrock/v0.17.1/src/universal/ISemver.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/src/v0.8/vendor/@eth-optimism/contracts-bedrock/v0.17.1/src/L2/L1Block.sol b/contracts/src/v0.8/vendor/@eth-optimism/contracts-bedrock/v0.17.1/src/L2/L1Block.sol index b2ba1ba36db..e047e980a30 100644 --- a/contracts/src/v0.8/vendor/@eth-optimism/contracts-bedrock/v0.17.1/src/L2/L1Block.sol +++ b/contracts/src/v0.8/vendor/@eth-optimism/contracts-bedrock/v0.17.1/src/L2/L1Block.sol @@ -116,4 +116,4 @@ contract L1Block is ISemver { sstore(batcherHash.slot, calldataload(132)) // bytes32 } } -} \ No newline at end of file +} diff --git a/contracts/src/v0.8/vendor/@eth-optimism/contracts-bedrock/v0.17.1/src/universal/ISemver.sol b/contracts/src/v0.8/vendor/@eth-optimism/contracts-bedrock/v0.17.1/src/universal/ISemver.sol index ae9569a0505..22a5ddebdaf 100644 --- a/contracts/src/v0.8/vendor/@eth-optimism/contracts-bedrock/v0.17.1/src/universal/ISemver.sol +++ b/contracts/src/v0.8/vendor/@eth-optimism/contracts-bedrock/v0.17.1/src/universal/ISemver.sol @@ -10,4 +10,4 @@ interface ISemver { /// tooling. /// @return Semver contract version as a string. function version() external view returns (string memory); -} \ No newline at end of file +} From 43ae0093529a8c1fbfdd17745b2f8d0d5a7761e7 Mon Sep 17 00:00:00 2001 From: Morgan Kuphal Date: Fri, 17 May 2024 20:39:16 -0500 Subject: [PATCH 9/9] tests wip --- .../functions/dev/v1_X/FunctionsBilling.sol | 2 +- .../dev/v1_X/libraries/ChainSpecificUtil.sol | 8 +- .../tests/v1_X/ChainSpecificUtil.t.sol | 103 +++++++++++++----- .../tests/v1_X/FunctionsBilling.t.sol | 3 +- .../tests/v1_X/FunctionsCoordinator.t.sol | 2 +- .../src/v0.8/functions/tests/v1_X/Setup.t.sol | 3 +- .../v0.17.1/src/L2/L1Block.sol | 2 +- 7 files changed, 85 insertions(+), 38 deletions(-) diff --git a/contracts/src/v0.8/functions/dev/v1_X/FunctionsBilling.sol b/contracts/src/v0.8/functions/dev/v1_X/FunctionsBilling.sol index 9be1c9707e5..d653a9bcf9d 100644 --- a/contracts/src/v0.8/functions/dev/v1_X/FunctionsBilling.sol +++ b/contracts/src/v0.8/functions/dev/v1_X/FunctionsBilling.sol @@ -202,7 +202,7 @@ abstract contract FunctionsBilling is Routable, IFunctionsBilling { uint256 totalFeeWei = (gasPriceWei * executionGas) + l1FeeWei; // Basis Points are 1/100th of 1%, divide by 10_000 to bring back to original units - uint256 totalFeeWeiWithOverestimate = totalFee + ((totalFee * s_config.fulfillmentGasPriceOverEstimationBP) / 10_000); + uint256 totalFeeWeiWithOverestimate = totalFeeWei + ((totalFeeWei * s_config.fulfillmentGasPriceOverEstimationBP) / 10_000); uint96 estimatedGasReimbursementJuels = _getJuelsFromWei(totalFeeWeiWithOverestimate); diff --git a/contracts/src/v0.8/functions/dev/v1_X/libraries/ChainSpecificUtil.sol b/contracts/src/v0.8/functions/dev/v1_X/libraries/ChainSpecificUtil.sol index b6d9ae32418..36f54e3b2eb 100644 --- a/contracts/src/v0.8/functions/dev/v1_X/libraries/ChainSpecificUtil.sol +++ b/contracts/src/v0.8/functions/dev/v1_X/libraries/ChainSpecificUtil.sol @@ -10,7 +10,7 @@ library ChainSpecificUtil { // ------------ Start Arbitrum Constants ------------ /// @dev ARB_L1_FEE_DATA_PADDING_SIZE is the L1 data padding for Optimism - uint256 private const ARB_L1_FEE_DATA_PADDING_SIZE = 140; + uint256 private constant ARB_L1_FEE_DATA_PADDING_SIZE = 140; /// @dev ARBGAS_ADDR is the address of the ArbGasInfo precompile on Arbitrum. /// @dev reference: https://github.com/OffchainLabs/nitro/blob/v2.0.14/contracts/src/precompiles/ArbGasInfo.sol#L10 address private constant ARBGAS_ADDR = address(0x000000000000000000000000000000000000006C); @@ -24,7 +24,7 @@ library ChainSpecificUtil { // ------------ Start Optimism Constants ------------ /// @dev OP_L1_FEE_DATA_PADDING_SIZE is the L1 data padding for Optimism - uint256 private const OP_L1_FEE_DATA_PADDING_SIZE = 35; + uint256 private constant OP_L1_FEE_DATA_PADDING_SIZE = 35; /// @dev L1BLOCK_ADDR is the address of the L1Block precompile on Optimism. address private constant L1BLOCK_ADDR = address(0x4200000000000000000000000000000000000015); L1Block private constant L1BLOCK = L1Block(L1BLOCK_ADDR); @@ -51,11 +51,11 @@ library ChainSpecificUtil { } else if (_isOptimismChainId(chainid)) { // https://docs.optimism.io/stack/transactions/fees#ecotone // note we conservatively assume all non-zero bytes: tx_compressed_size = tx_data_size_bytes - uint256 l1BaseFeeWei = L1BLOCK.baseFee(); + uint256 l1BaseFeeWei = L1BLOCK.basefee(); uint256 l1BaseFeeScalar = L1BLOCK.baseFeeScalar(); uint256 l1BlobBaseFeeWei = L1BLOCK.blobBaseFee(); uint256 l1BlobBaseFeeScalar = L1BLOCK.blobBaseFeeScalar(); - uint256 weightedGasPrice = 16 * l1BaseFeeScalar * l1BaseFee + l1BlobBaseFeeScalar * l1BlobBaseFeeWei; + uint256 weightedGasPrice = 16 * l1BaseFeeScalar * l1BaseFeeWei + l1BlobBaseFeeScalar * l1BlobBaseFeeWei; return weightedGasPrice * dataSizeBytes; } return 0; diff --git a/contracts/src/v0.8/functions/tests/v1_X/ChainSpecificUtil.t.sol b/contracts/src/v0.8/functions/tests/v1_X/ChainSpecificUtil.t.sol index 5384a66d912..f137e41d9b0 100644 --- a/contracts/src/v0.8/functions/tests/v1_X/ChainSpecificUtil.t.sol +++ b/contracts/src/v0.8/functions/tests/v1_X/ChainSpecificUtil.t.sol @@ -11,22 +11,29 @@ import {FunctionsResponse} from "../../dev/v1_X/libraries/FunctionsResponse.sol" import {FunctionsFulfillmentSetup} from "./Setup.t.sol"; import {ArbGasInfo} from "../../../vendor/@arbitrum/nitro-contracts/src/precompiles/ArbGasInfo.sol"; -import {OVM_GasPriceOracle} from "../../../vendor/@eth-optimism/contracts/v0.8.9/contracts/L2/predeploys/OVM_GasPriceOracle.sol"; +import {L1Block} from "../../../vendor/@eth-optimism/contracts-bedrock/v0.17.1/src/L2/L1Block.sol"; -/// @notice #_getCurrentTxL1GasFees Arbitrum +/// @notice #_getL1FeeUpperLimit Arbitrum /// @dev Arbitrum gas formula = L2 Gas Price * (Gas used on L2 + Extra Buffer for L1 cost) /// @dev where Extra Buffer for L1 cost = (L1 Estimated Cost / L2 Gas Price) -contract ChainSpecificUtil__getCurrentTxL1GasFees_Arbitrum is FunctionsFulfillmentSetup { +contract ChainSpecificUtil__getL1FeeUpperLimit_Arbitrum is FunctionsFulfillmentSetup { address private constant ARBGAS_ADDR = address(0x000000000000000000000000000000000000006C); uint256 private constant L1_FEE_WEI = 15_818_209_764_247; uint96 l1FeeJuels = uint96((1e18 * L1_FEE_WEI) / uint256(LINK_ETH_RATE)); function setUp() public virtual override { - vm.mockCall(ARBGAS_ADDR, abi.encodeWithSelector(ArbGasInfo.getCurrentTxL1GasFees.selector), abi.encode(L1_FEE_WEI)); + uint256 unused = 0; + uint256 gasPerL1CalldataByte = 4; + vm.mockCall( + ARBGAS_ADDR, + abi.encodeWithSelector( + ArbGasInfo.getPricesInWei.selector), + abi.encode(unused, gasPerL1CalldataByte, unused, unused, unused, unused) + ); } - function test__getCurrentTxL1GasFees_SuccessWhenArbitrumMainnet() public { + function test__getL1FeeUpperLimit_SuccessWhenArbitrumMainnet() public { // Set the chainID vm.chainId(42161); @@ -43,7 +50,7 @@ contract ChainSpecificUtil__getCurrentTxL1GasFees_Arbitrum is FunctionsFulfillme assertEq(s_responses[1].totalCostJuels, expectedTotalCostJuels); } - function test__getCurrentTxL1GasFees_SuccessWhenArbitrumGoerli() public { + function test__getL1FeeUpperLimit_SuccessWhenArbitrumGoerli() public { // Set the chainID vm.chainId(421613); @@ -60,7 +67,7 @@ contract ChainSpecificUtil__getCurrentTxL1GasFees_Arbitrum is FunctionsFulfillme assertEq(s_responses[1].totalCostJuels, expectedTotalCostJuels); } - function test__getCurrentTxL1GasFees_SuccessWhenArbitrumSepolia() public { + function test__getL1FeeUpperLimit_SuccessWhenArbitrumSepolia() public { // Set the chainID vm.chainId(421614); @@ -78,24 +85,43 @@ contract ChainSpecificUtil__getCurrentTxL1GasFees_Arbitrum is FunctionsFulfillme } } -/// @notice #_getCurrentTxL1GasFees Optimism +/// @notice #_getL1FeeUpperLimit Optimism /// @dev Optimism gas formula = ((l2_base_fee + l2_priority_fee) * l2_gas_used) + L1 data fee /// @dev where L1 data fee = l1_gas_price * ((count_zero_bytes(tx_data) * 4 + count_non_zero_bytes(tx_data) * 16) + fixed_overhead + noncalldata_gas) * dynamic_overhead -contract ChainSpecificUtil__getCurrentTxL1GasFees_Optimism is FunctionsFulfillmentSetup { - address private constant OVM_GASPRICEORACLE_ADDR = address(0x420000000000000000000000000000000000000F); - uint256 private constant L1_FEE_WEI = 15_818_209_764_247; +contract ChainSpecificUtil__getL1FeeUpperLimit_Optimism is FunctionsFulfillmentSetup { + address private constant L1BLOCK_ADDR = address(0x4200000000000000000000000000000000000015); + L1Block private constant L1BLOCK = L1Block(L1BLOCK_ADDR); + uint256 private constant L1_BASE_FEE_WEI = 15_818_209; + uint256 private constant L1_BASE_FEE_SCALAR = 2; + uint256 private constant L1_BLOB_BASE_FEE_WEI = 15_818_209; + uint256 private constant L1_BLOB_BASE_FEE_SCALAR = 2; - uint96 l1FeeJuels = uint96((1e18 * L1_FEE_WEI) / uint256(LINK_ETH_RATE)); + uint96 l1FeeJuels = uint96((1e18 * L1_BASE_FEE_WEI) / uint256(LINK_ETH_RATE)); function setUp() public virtual override { vm.mockCall( - OVM_GASPRICEORACLE_ADDR, - abi.encodeWithSelector(OVM_GasPriceOracle.getL1Fee.selector), - abi.encode(L1_FEE_WEI) + L1BLOCK_ADDR, + abi.encodeWithSelector(L1BLOCK.basefee.selector), + abi.encode(L1_BASE_FEE_WEI) + ); + vm.mockCall( + L1BLOCK_ADDR, + abi.encodeWithSelector(L1BLOCK.baseFeeScalar.selector), + abi.encode(L1_BASE_FEE_SCALAR) + ); + vm.mockCall( + L1BLOCK_ADDR, + abi.encodeWithSelector(L1BLOCK.blobBaseFee.selector), + abi.encode(L1_BLOB_BASE_FEE_WEI) + ); + vm.mockCall( + L1BLOCK_ADDR, + abi.encodeWithSelector(L1BLOCK.blobBaseFeeScalar.selector), + abi.encode(L1_BLOB_BASE_FEE_SCALAR) ); } - function test__getCurrentTxL1GasFees_SuccessWhenOptimismMainnet() public { + function test__getL1FeeUpperLimit_SuccessWhenOptimismMainnet() public { // Set the chainID vm.chainId(10); @@ -112,7 +138,7 @@ contract ChainSpecificUtil__getCurrentTxL1GasFees_Optimism is FunctionsFulfillme assertEq(s_responses[1].totalCostJuels, expectedTotalCostJuels); } - function test__getCurrentTxL1GasFees_SuccessWhenOptimismGoerli() public { + function test__getL1FeeUpperLimit_SuccessWhenOptimismGoerli() public { // Set the chainID vm.chainId(420); @@ -129,7 +155,7 @@ contract ChainSpecificUtil__getCurrentTxL1GasFees_Optimism is FunctionsFulfillme assertEq(s_responses[1].totalCostJuels, expectedTotalCostJuels); } - function test__getCurrentTxL1GasFees_SuccessWhenOptimismSepolia() public { + function test__getL1FeeUpperLimit_SuccessWhenOptimismSepolia() public { // Set the chainID vm.chainId(11155420); @@ -147,24 +173,43 @@ contract ChainSpecificUtil__getCurrentTxL1GasFees_Optimism is FunctionsFulfillme } } -/// @notice #_getCurrentTxL1GasFees Base +/// @notice #_getL1FeeUpperLimit Base /// @dev Base gas formula uses Optimism formula = ((l2_base_fee + l2_priority_fee) * l2_gas_used) + L1 data fee /// @dev where L1 data fee = l1_gas_price * ((count_zero_bytes(tx_data) * 4 + count_non_zero_bytes(tx_data) * 16) + fixed_overhead + noncalldata_gas) * dynamic_overhead -contract ChainSpecificUtil__getCurrentTxL1GasFees_Base is FunctionsFulfillmentSetup { - address private constant OVM_GASPRICEORACLE_ADDR = address(0x420000000000000000000000000000000000000F); - uint256 private constant L1_FEE_WEI = 15_818_209_764_247; +contract ChainSpecificUtil__getL1FeeUpperLimit_Base is FunctionsFulfillmentSetup { + address private constant L1BLOCK_ADDR = address(0x4200000000000000000000000000000000000015); + L1Block private constant L1BLOCK = L1Block(L1BLOCK_ADDR); + uint256 private constant L1_BASE_FEE_WEI = 15_818_209; + uint256 private constant L1_BASE_FEE_SCALAR = 2; + uint256 private constant L1_BLOB_BASE_FEE_WEI = 15_818_209; + uint256 private constant L1_BLOB_BASE_FEE_SCALAR = 2; - uint96 l1FeeJuels = uint96((1e18 * L1_FEE_WEI) / uint256(LINK_ETH_RATE)); + uint96 l1FeeJuels = uint96((1e18 * L1_BASE_FEE_WEI) / uint256(LINK_ETH_RATE)); function setUp() public virtual override { vm.mockCall( - OVM_GASPRICEORACLE_ADDR, - abi.encodeWithSelector(OVM_GasPriceOracle.getL1Fee.selector), - abi.encode(L1_FEE_WEI) + L1BLOCK_ADDR, + abi.encodeWithSelector(L1BLOCK.basefee.selector), + abi.encode(L1_BASE_FEE_WEI) + ); + vm.mockCall( + L1BLOCK_ADDR, + abi.encodeWithSelector(L1BLOCK.baseFeeScalar.selector), + abi.encode(L1_BASE_FEE_SCALAR) + ); + vm.mockCall( + L1BLOCK_ADDR, + abi.encodeWithSelector(L1BLOCK.blobBaseFee.selector), + abi.encode(L1_BLOB_BASE_FEE_WEI) + ); + vm.mockCall( + L1BLOCK_ADDR, + abi.encodeWithSelector(L1BLOCK.blobBaseFeeScalar.selector), + abi.encode(L1_BLOB_BASE_FEE_SCALAR) ); } - function test__getCurrentTxL1GasFees_SuccessWhenBaseMainnet() public { + function test__getL1FeeUpperLimit_SuccessWhenBaseMainnet() public { // Set the chainID vm.chainId(8453); @@ -181,7 +226,7 @@ contract ChainSpecificUtil__getCurrentTxL1GasFees_Base is FunctionsFulfillmentSe assertEq(s_responses[1].totalCostJuels, expectedTotalCostJuels); } - function test__getCurrentTxL1GasFees_SuccessWhenBaseGoerli() public { + function test__getL1FeeUpperLimit_SuccessWhenBaseGoerli() public { // Set the chainID vm.chainId(84531); @@ -198,7 +243,7 @@ contract ChainSpecificUtil__getCurrentTxL1GasFees_Base is FunctionsFulfillmentSe assertEq(s_responses[1].totalCostJuels, expectedTotalCostJuels); } - function test__getCurrentTxL1GasFees_SuccessWhenBaseSepolia() public { + function test__getL1FeeUpperLimit_SuccessWhenBaseSepolia() public { // Set the chainID vm.chainId(84532); diff --git a/contracts/src/v0.8/functions/tests/v1_X/FunctionsBilling.t.sol b/contracts/src/v0.8/functions/tests/v1_X/FunctionsBilling.t.sol index 774fe4e875c..b66c1e42f90 100644 --- a/contracts/src/v0.8/functions/tests/v1_X/FunctionsBilling.t.sol +++ b/contracts/src/v0.8/functions/tests/v1_X/FunctionsBilling.t.sol @@ -59,7 +59,8 @@ contract FunctionsBilling_UpdateConfig is FunctionsRouterSetup { fallbackNativePerUnitLink: getCoordinatorConfig().fallbackNativePerUnitLink * 2, fallbackUsdPerUnitLink: getCoordinatorConfig().fallbackUsdPerUnitLink * 2, fallbackUsdPerUnitLinkDecimals: getCoordinatorConfig().fallbackUsdPerUnitLinkDecimals * 2, - minimumEstimateGasPriceWei: getCoordinatorConfig().minimumEstimateGasPriceWei * 2 + minimumEstimateGasPriceWei: getCoordinatorConfig().minimumEstimateGasPriceWei * 2, + transmitTxSizeBytes: getCoordinatorConfig().transmitTxSizeBytes * 2 }); } diff --git a/contracts/src/v0.8/functions/tests/v1_X/FunctionsCoordinator.t.sol b/contracts/src/v0.8/functions/tests/v1_X/FunctionsCoordinator.t.sol index 3944c5873b1..03d9f645146 100644 --- a/contracts/src/v0.8/functions/tests/v1_X/FunctionsCoordinator.t.sol +++ b/contracts/src/v0.8/functions/tests/v1_X/FunctionsCoordinator.t.sol @@ -14,7 +14,7 @@ import {FunctionsRouterSetup, FunctionsDONSetup, FunctionsSubscriptionSetup} fro /// @notice #constructor contract FunctionsCoordinator_Constructor is FunctionsRouterSetup { function test_Constructor_Success() public { - assertEq(s_functionsCoordinator.typeAndVersion(), "Functions Coordinator v1.3.0"); + assertEq(s_functionsCoordinator.typeAndVersion(), "Functions Coordinator v1.3.1"); assertEq(s_functionsCoordinator.owner(), OWNER_ADDRESS); } } diff --git a/contracts/src/v0.8/functions/tests/v1_X/Setup.t.sol b/contracts/src/v0.8/functions/tests/v1_X/Setup.t.sol index 32dc53e42ab..eaae8b1ab2e 100644 --- a/contracts/src/v0.8/functions/tests/v1_X/Setup.t.sol +++ b/contracts/src/v0.8/functions/tests/v1_X/Setup.t.sol @@ -93,7 +93,8 @@ contract FunctionsRouterSetup is BaseTest { fallbackNativePerUnitLink: 5000000000000000, fallbackUsdPerUnitLink: 1400000000, fallbackUsdPerUnitLinkDecimals: 8, - minimumEstimateGasPriceWei: 1000000000 // 1 gwei + minimumEstimateGasPriceWei: 1000000000, // 1 gwei + transmitTxSizeBytes: 5000 }); } diff --git a/contracts/src/v0.8/vendor/@eth-optimism/contracts-bedrock/v0.17.1/src/L2/L1Block.sol b/contracts/src/v0.8/vendor/@eth-optimism/contracts-bedrock/v0.17.1/src/L2/L1Block.sol index e047e980a30..133d2f7ad16 100644 --- a/contracts/src/v0.8/vendor/@eth-optimism/contracts-bedrock/v0.17.1/src/L2/L1Block.sol +++ b/contracts/src/v0.8/vendor/@eth-optimism/contracts-bedrock/v0.17.1/src/L2/L1Block.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.15; +pragma solidity ^0.8.15; import { ISemver } from "../universal/ISemver.sol";