Skip to content

Commit

Permalink
Functions: ARB+OP cost estimation tweaks (#11102)
Browse files Browse the repository at this point in the history
* Functions: ARB+OP gas tweaks

* make wrappers-all

* Updated gas snapshot

* Single line comments

* (refactor): rework ChainSpecificUtil usage to be an additional flat fee, rather than gas price

* (test): Add ChainSpecificUtil foundry tests

* Regenerate geth wrappers

* Regenerate gas snapshot

* Amend L1Fee as gas units, not in wei

* Prettier

* Revert "Amend L1Fee as gas units, not in wei"

This reverts commit 75e47cadbe4a7a58fc731bc57b9dfaa7eb8a7898.

* Denote that _getCurrentTxL1GasFees's return is in Wei

* (refactor) rework FunctionsBilling unit conversion helper to be juels from wei

* Changes from review

* Regenerate gas snapshot

---------

Co-authored-by: Justin Kaseman <justinkaseman@live.com>
  • Loading branch information
Andrei Smirnov and justinkaseman authored Nov 9, 2023
1 parent c2a1b26 commit 0128650
Show file tree
Hide file tree
Showing 11 changed files with 434 additions and 82 deletions.
73 changes: 41 additions & 32 deletions contracts/gas-snapshots/functions.gas-snapshot

Large diffs are not rendered by default.

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

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";

/// @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 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 ------------
/// @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 OVM_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);
OVM_GasPriceOracle private constant OVM_GASPRICEORACLE = OVM_GasPriceOracle(OVM_GASPRICEORACLE_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 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 OVM_GasPriceOracle predeploy
/// @notice and getL1Fee is called to get the fees.
function _getCurrentTxL1GasFees(bytes memory txCallData) internal view returns (uint256 l1FeeWei) {
uint256 chainid = block.chainid;
if (_isArbitrumChainId(chainid)) {
return ARBGAS.getCurrentTxL1GasFees();
} else if (_isOptimismChainId(chainid)) {
return OVM_GASPRICEORACLE.getL1Fee(bytes.concat(txCallData, L1_FEE_DATA_PADDING));
}
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;
}
}
34 changes: 19 additions & 15 deletions contracts/src/v0.8/functions/dev/v1_X/FunctionsBilling.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {FunctionsResponse} from "./libraries/FunctionsResponse.sol";

import {SafeCast} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/utils/math/SafeCast.sol";

import {ChainSpecificUtil} from "./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 {
Expand Down Expand Up @@ -123,10 +125,10 @@ abstract contract FunctionsBilling is Routable, IFunctionsBilling {
return uint256(weiPerUnitLink);
}

function _getJuelsPerGas(uint256 gasPriceWei) private view returns (uint96) {
// (1e18 juels/link) * (wei/gas) / (wei/link) = juels per gas
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 * gasPriceWei) / getWeiPerUnitLink());
return SafeCast.toUint96((1e18 * amountWei) / getWeiPerUnitLink());
}

// ================================================================
Expand Down Expand Up @@ -159,8 +161,6 @@ abstract contract FunctionsBilling is Routable, IFunctionsBilling {
uint72 donFee,
uint72 adminFee
) internal view returns (uint96) {
uint256 executionGas = s_config.gasOverheadBeforeCallback + s_config.gasOverheadAfterCallback + callbackGasLimit;

// 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;
Expand All @@ -170,11 +170,13 @@ abstract contract FunctionsBilling is Routable, IFunctionsBilling {
((gasPriceWei * s_config.fulfillmentGasPriceOverEstimationBP) / 10_000);
/// @NOTE: Basis Points are 1/100th of 1%, divide by 10_000 to bring back to original units

uint96 juelsPerGas = _getJuelsPerGas(gasPriceWithOverestimation);
uint256 estimatedGasReimbursement = juelsPerGas * executionGas;
uint96 fees = uint96(donFee) + uint96(adminFee);
uint256 executionGas = s_config.gasOverheadBeforeCallback + s_config.gasOverheadAfterCallback + callbackGasLimit;
uint256 l1FeeWei = ChainSpecificUtil._getCurrentTxL1GasFees(msg.data);
uint96 estimatedGasReimbursementJuels = _getJuelsFromWei((gasPriceWithOverestimation * executionGas) + l1FeeWei);

uint96 feesJuels = uint96(donFee) + uint96(adminFee);

return SafeCast.toUint96(estimatedGasReimbursement + fees);
return estimatedGasReimbursementJuels + feesJuels;
}

// ================================================================
Expand Down Expand Up @@ -248,6 +250,7 @@ abstract contract FunctionsBilling is Routable, IFunctionsBilling {
/// @param requestId identifier for the request that was generated by the Registry in the beginBilling commitment
/// @param response response data from DON consensus
/// @param err error from DON consensus
/// @param reportBatchSize the number of fulfillments in the transmitter's report
/// @return result fulfillment result
/// @dev Only callable by a node that has been approved on the Coordinator
/// @dev simulated offchain to determine if sufficient balance is present to fulfill the request
Expand All @@ -256,21 +259,22 @@ abstract contract FunctionsBilling is Routable, IFunctionsBilling {
bytes memory response,
bytes memory err,
bytes memory onchainMetadata,
bytes memory /* offchainMetadata TODO: use in getDonFee() for dynamic billing */
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));

uint96 juelsPerGas = _getJuelsPerGas(tx.gasprice);
uint256 gasOverheadWei = (commitment.gasOverheadBeforeCallback + commitment.gasOverheadAfterCallback) * tx.gasprice;
uint256 l1FeeShareWei = ChainSpecificUtil._getCurrentTxL1GasFees(msg.data) / reportBatchSize;
// Gas overhead without callback
uint96 gasOverheadJuels = juelsPerGas *
(commitment.gasOverheadBeforeCallback + commitment.gasOverheadAfterCallback);
uint96 gasOverheadJuels = _getJuelsFromWei(gasOverheadWei + l1FeeShareWei);

// The Functions Router will perform the callback to the client contract
(FunctionsResponse.FulfillResult resultCode, uint96 callbackCostJuels) = _getRouter().fulfill(
response,
err,
juelsPerGas,
gasOverheadJuels + commitment.donFee, // costWithoutFulfillment
_getJuelsFromWei(tx.gasprice), // Juels Per Gas conversion rate
gasOverheadJuels + commitment.donFee, // cost without callback or admin fee, those will be added by the Router
msg.sender,
commitment
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,14 @@ contract FunctionsCoordinator is OCR2Base, IFunctionsCoordinator, FunctionsBilli
// Bounded by "MaxRequestBatchSize" on the Job's ReportingPluginConfig
for (uint256 i = 0; i < requestIds.length; ++i) {
FunctionsResponse.FulfillResult result = FunctionsResponse.FulfillResult(
_fulfillAndBill(requestIds[i], results[i], errors[i], onchainMetadata[i], offchainMetadata[i])
_fulfillAndBill(
requestIds[i],
results[i],
errors[i],
onchainMetadata[i],
offchainMetadata[i],
uint8(requestIds.length) // will not exceed "MaxRequestBatchSize" on the Job's ReportingPluginConfig
)
);

// Emit on successfully processing the fulfillment
Expand Down
217 changes: 217 additions & 0 deletions contracts/src/v0.8/functions/tests/v1_X/ChainSpecificUtil.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {BaseTest} from "./BaseTest.t.sol";
import {FunctionsClient} from "../../dev/v1_X/FunctionsClient.sol";
import {FunctionsRouter} from "../../dev/v1_X/FunctionsRouter.sol";
import {FunctionsSubscriptions} from "../../dev/v1_X/FunctionsSubscriptions.sol";
import {FunctionsRequest} from "../../dev/v1_X/libraries/FunctionsRequest.sol";
import {FunctionsResponse} from "../../dev/v1_X/libraries/FunctionsResponse.sol";

import {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";

/// @notice #_getCurrentTxL1GasFees 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 {
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));
}

function test__getCurrentTxL1GasFees_SuccessWhenArbitrumMainnet() public {
// Set the chainID
vm.chainId(42161);

// Setup sends and fulfills request #1
FunctionsFulfillmentSetup.setUp();

// Check request cost estimate
uint96 expectedEstimatedTotalCostJuels = _getExpectedCostEstimate(s_requests[1].requestData.callbackGasLimit) +
l1FeeJuels;
assertEq(s_requests[1].commitment.estimatedTotalCostJuels, expectedEstimatedTotalCostJuels);

// Check response actual cost
uint96 expectedTotalCostJuels = _getExpectedCost(5416) + l1FeeJuels;
assertEq(s_responses[1].totalCostJuels, expectedTotalCostJuels);
}

function test__getCurrentTxL1GasFees_SuccessWhenArbitrumGoerli() public {
// Set the chainID
vm.chainId(421613);

// Setup sends and fulfills request #1
FunctionsFulfillmentSetup.setUp();

// Check request cost estimate
uint96 expectedEstimatedTotalCostJuels = _getExpectedCostEstimate(s_requests[1].requestData.callbackGasLimit) +
l1FeeJuels;
assertEq(s_requests[1].commitment.estimatedTotalCostJuels, expectedEstimatedTotalCostJuels);

// Check response actual cost
uint96 expectedTotalCostJuels = _getExpectedCost(5416) + l1FeeJuels;
assertEq(s_responses[1].totalCostJuels, expectedTotalCostJuels);
}

function test__getCurrentTxL1GasFees_SuccessWhenArbitrumSepolia() public {
// Set the chainID
vm.chainId(421614);

// Setup sends and fulfills request #1
FunctionsFulfillmentSetup.setUp();

// Check request cost estimate
uint96 expectedEstimatedTotalCostJuels = _getExpectedCostEstimate(s_requests[1].requestData.callbackGasLimit) +
l1FeeJuels;
assertEq(s_requests[1].commitment.estimatedTotalCostJuels, expectedEstimatedTotalCostJuels);

// Check response actual cost
uint96 expectedTotalCostJuels = _getExpectedCost(5416) + l1FeeJuels;
assertEq(s_responses[1].totalCostJuels, expectedTotalCostJuels);
}
}

/// @notice #_getCurrentTxL1GasFees 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;

uint96 l1FeeJuels = uint96((1e18 * L1_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)
);
}

function test__getCurrentTxL1GasFees_SuccessWhenOptimismMainnet() public {
// Set the chainID
vm.chainId(10);

// Setup sends and fulfills request #1
FunctionsFulfillmentSetup.setUp();

// Check request cost estimate
uint96 expectedEstimatedTotalCostJuels = _getExpectedCostEstimate(s_requests[1].requestData.callbackGasLimit) +
l1FeeJuels;
assertEq(s_requests[1].commitment.estimatedTotalCostJuels, expectedEstimatedTotalCostJuels);

// Check response actual cost
uint96 expectedTotalCostJuels = _getExpectedCost(5416) + l1FeeJuels;
assertEq(s_responses[1].totalCostJuels, expectedTotalCostJuels);
}

function test__getCurrentTxL1GasFees_SuccessWhenOptimismGoerli() public {
// Set the chainID
vm.chainId(420);

// Setup sends and fulfills request #1
FunctionsFulfillmentSetup.setUp();

// Check request cost estimate
uint96 expectedEstimatedTotalCostJuels = _getExpectedCostEstimate(s_requests[1].requestData.callbackGasLimit) +
l1FeeJuels;
assertEq(s_requests[1].commitment.estimatedTotalCostJuels, expectedEstimatedTotalCostJuels);

// Check response actual cost
uint96 expectedTotalCostJuels = _getExpectedCost(5416) + l1FeeJuels;
assertEq(s_responses[1].totalCostJuels, expectedTotalCostJuels);
}

function test__getCurrentTxL1GasFees_SuccessWhenOptimismSepolia() public {
// Set the chainID
vm.chainId(11155420);

// Setup sends and fulfills request #1
FunctionsFulfillmentSetup.setUp();

// Check request cost estimate
uint96 expectedEstimatedTotalCostJuels = _getExpectedCostEstimate(s_requests[1].requestData.callbackGasLimit) +
l1FeeJuels;
assertEq(s_requests[1].commitment.estimatedTotalCostJuels, expectedEstimatedTotalCostJuels);

// Check response actual cost
uint96 expectedTotalCostJuels = _getExpectedCost(5416) + l1FeeJuels;
assertEq(s_responses[1].totalCostJuels, expectedTotalCostJuels);
}
}

/// @notice #_getCurrentTxL1GasFees 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;

uint96 l1FeeJuels = uint96((1e18 * L1_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)
);
}

function test__getCurrentTxL1GasFees_SuccessWhenBaseMainnet() public {
// Set the chainID
vm.chainId(8453);

// Setup sends and fulfills request #1
FunctionsFulfillmentSetup.setUp();

// Check request cost estimate
uint96 expectedEstimatedTotalCostJuels = _getExpectedCostEstimate(s_requests[1].requestData.callbackGasLimit) +
l1FeeJuels;
assertEq(s_requests[1].commitment.estimatedTotalCostJuels, expectedEstimatedTotalCostJuels);

// Check response actual cost
uint96 expectedTotalCostJuels = _getExpectedCost(5416) + l1FeeJuels;
assertEq(s_responses[1].totalCostJuels, expectedTotalCostJuels);
}

function test__getCurrentTxL1GasFees_SuccessWhenBaseGoerli() public {
// Set the chainID
vm.chainId(84531);

// Setup sends and fulfills request #1
FunctionsFulfillmentSetup.setUp();

// Check request cost estimate
uint96 expectedEstimatedTotalCostJuels = _getExpectedCostEstimate(s_requests[1].requestData.callbackGasLimit) +
l1FeeJuels;
assertEq(s_requests[1].commitment.estimatedTotalCostJuels, expectedEstimatedTotalCostJuels);

// Check response actual cost
uint96 expectedTotalCostJuels = _getExpectedCost(5416) + l1FeeJuels;
assertEq(s_responses[1].totalCostJuels, expectedTotalCostJuels);
}

function test__getCurrentTxL1GasFees_SuccessWhenBaseSepolia() public {
// Set the chainID
vm.chainId(84532);

// Setup sends and fulfills request #1
FunctionsFulfillmentSetup.setUp();

// Check request cost estimate
uint96 expectedEstimatedTotalCostJuels = _getExpectedCostEstimate(s_requests[1].requestData.callbackGasLimit) +
l1FeeJuels;
assertEq(s_requests[1].commitment.estimatedTotalCostJuels, expectedEstimatedTotalCostJuels);

// Check response actual cost
uint96 expectedTotalCostJuels = _getExpectedCost(5416) + l1FeeJuels;
assertEq(s_responses[1].totalCostJuels, expectedTotalCostJuels);
}
}
Loading

0 comments on commit 0128650

Please sign in to comment.