Skip to content

Commit

Permalink
(feat): FUN-1234 Functions USD denominated premium fees (#12104)
Browse files Browse the repository at this point in the history
  • Loading branch information
justinkaseman authored Feb 21, 2024
1 parent 12d4dd6 commit 2cd4bc5
Show file tree
Hide file tree
Showing 16 changed files with 546 additions and 247 deletions.
154 changes: 78 additions & 76 deletions contracts/gas-snapshots/functions.gas-snapshot

Large diffs are not rendered by default.

118 changes: 89 additions & 29 deletions contracts/src/v0.8/functions/dev/v1_X/FunctionsBilling.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ abstract contract FunctionsBilling is Routable, IFunctionsBilling {
uint96 juelsPerGas,
uint256 l1FeeShareWei,
uint96 callbackCostJuels,
uint96 totalCostJuels
uint72 donFeeJuels,
uint72 adminFeeJuels,
uint72 operationFeeJuels
);

// ================================================================
Expand All @@ -47,6 +49,7 @@ abstract contract FunctionsBilling is Routable, IFunctionsBilling {
error UnauthorizedSender();
error MustBeSubOwner(address owner);
error InvalidLinkWeiPrice(int256 linkWei);
error InvalidUsdLinkPrice(int256 usdLink);
error PaymentTooLarge();
error NoTransmittersSet();
error InvalidCalldata();
Expand All @@ -61,12 +64,19 @@ abstract contract FunctionsBilling is Routable, IFunctionsBilling {
uint96 internal s_feePool;

AggregatorV3Interface private s_linkToNativeFeed;
AggregatorV3Interface private s_linkToUsdFeed;

// ================================================================
// | Initialization |
// ================================================================
constructor(address router, FunctionsBillingConfig memory config, address linkToNativeFeed) Routable(router) {
constructor(
address router,
FunctionsBillingConfig memory config,
address linkToNativeFeed,
address linkToUsdFeed
) Routable(router) {
s_linkToNativeFeed = AggregatorV3Interface(linkToNativeFeed);
s_linkToUsdFeed = AggregatorV3Interface(linkToUsdFeed);

updateConfig(config);
}
Expand Down Expand Up @@ -95,22 +105,28 @@ abstract contract FunctionsBilling is Routable, IFunctionsBilling {
// ================================================================

/// @inheritdoc IFunctionsBilling
function getDONFee(bytes memory /* requestData */) public view override returns (uint72) {
return s_config.donFee;
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 getAdminFee() public view override returns (uint72) {
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) {
FunctionsBillingConfig memory config = s_config;
(, int256 weiPerUnitLink, , uint256 timestamp, ) = s_linkToNativeFeed.latestRoundData();
// solhint-disable-next-line not-rely-on-time
if (config.feedStalenessSeconds < block.timestamp - timestamp && config.feedStalenessSeconds > 0) {
return config.fallbackNativePerUnitLink;
if (s_config.feedStalenessSeconds < block.timestamp - timestamp && s_config.feedStalenessSeconds > 0) {
return s_config.fallbackNativePerUnitLink;
}
if (weiPerUnitLink <= 0) {
revert InvalidLinkWeiPrice(weiPerUnitLink);
Expand All @@ -124,6 +140,26 @@ abstract contract FunctionsBilling is Routable, IFunctionsBilling {
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 |
// ================================================================
Expand All @@ -140,9 +176,10 @@ abstract contract FunctionsBilling is Routable, IFunctionsBilling {
if (gasPriceWei > REASONABLE_GAS_PRICE_CEILING) {
revert InvalidCalldata();
}
uint72 adminFee = getAdminFee();
uint72 donFee = getDONFee(data);
return _calculateCostEstimate(callbackGasLimit, gasPriceWei, donFee, adminFee);
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
Expand All @@ -151,8 +188,9 @@ abstract contract FunctionsBilling is Routable, IFunctionsBilling {
function _calculateCostEstimate(
uint32 callbackGasLimit,
uint256 gasPriceWei,
uint72 donFee,
uint72 adminFee
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) {
Expand All @@ -167,7 +205,7 @@ abstract contract FunctionsBilling is Routable, IFunctionsBilling {
uint256 l1FeeWei = ChainSpecificUtil._getCurrentTxL1GasFees(msg.data);
uint96 estimatedGasReimbursementJuels = _getJuelsFromWei((gasPriceWithOverestimation * executionGas) + l1FeeWei);

uint96 feesJuels = uint96(donFee) + uint96(adminFee);
uint96 feesJuels = uint96(donFeeJuels) + uint96(adminFeeJuels) + uint96(operationFeeJuels);

return estimatedGasReimbursementJuels + feesJuels;
}
Expand All @@ -182,28 +220,28 @@ abstract contract FunctionsBilling is Routable, IFunctionsBilling {
/// @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) {
FunctionsBillingConfig memory config = s_config;

) internal returns (FunctionsResponse.Commitment memory commitment, uint72 operationFee) {
// Nodes should support all past versions of the structure
if (request.dataVersion > config.maxSupportedRequestDataVersion) {
if (request.dataVersion > s_config.maxSupportedRequestDataVersion) {
revert UnsupportedRequestDataVersion();
}

uint72 donFee = getDONFee(request.data);
uint72 donFee = getDONFeeJuels(request.data);
operationFee = getOperationFeeJuels();
uint96 estimatedTotalCostJuels = _calculateCostEstimate(
request.callbackGasLimit,
tx.gasprice,
donFee,
request.adminFee
request.adminFee,
operationFee
);

// Check that subscription can afford the estimated cost
if ((request.availableBalance) < estimatedTotalCostJuels) {
revert InsufficientBalance();
}

uint32 timeoutTimestamp = uint32(block.timestamp + config.requestTimeoutSeconds);
uint32 timeoutTimestamp = uint32(block.timestamp + s_config.requestTimeoutSeconds);
bytes32 requestId = keccak256(
abi.encode(
address(this),
Expand All @@ -230,13 +268,13 @@ abstract contract FunctionsBilling is Routable, IFunctionsBilling {
timeoutTimestamp: timeoutTimestamp,
requestId: requestId,
donFee: donFee,
gasOverheadBeforeCallback: config.gasOverheadBeforeCallback,
gasOverheadAfterCallback: config.gasOverheadAfterCallback
gasOverheadBeforeCallback: s_config.gasOverheadBeforeCallback,
gasOverheadAfterCallback: s_config.gasOverheadAfterCallback
});

s_requestCommitments[requestId] = keccak256(abi.encode(commitment));

return 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
Expand Down Expand Up @@ -268,9 +306,24 @@ abstract contract FunctionsBilling is Routable, IFunctionsBilling {
response,
err,
juelsPerGas,
gasOverheadJuels + commitment.donFee, // cost without callback or admin fee, those will be added by the Router
// 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,
commitment
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
Expand All @@ -282,19 +335,23 @@ abstract contract FunctionsBilling is Routable, IFunctionsBilling {
) {
delete s_requestCommitments[requestId];
// Reimburse the transmitter for the fulfillment gas cost
s_withdrawableTokens[msg.sender] = gasOverheadJuels + callbackCostJuels;
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,
totalCostJuels: gasOverheadJuels + callbackCostJuels + commitment.donFee + commitment.adminFee
donFeeJuels: commitment.donFee,
// The following two lines are because of OperationFee being used in the Offchain Commitment
adminFeeJuels: 0,
operationFeeJuels: commitment.adminFee
});
}

return resultCode;
}

Expand Down Expand Up @@ -377,4 +434,7 @@ abstract contract FunctionsBilling is Routable, IFunctionsBilling {
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);
}
36 changes: 28 additions & 8 deletions contracts/src/v0.8/functions/dev/v1_X/FunctionsCoordinator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ contract FunctionsCoordinator is OCR2Base, IFunctionsCoordinator, FunctionsBilli

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

event OracleRequest(
bytes32 indexed requestId,
Expand All @@ -43,8 +43,9 @@ contract FunctionsCoordinator is OCR2Base, IFunctionsCoordinator, FunctionsBilli
constructor(
address router,
FunctionsBillingConfig memory config,
address linkToNativeFeed
) OCR2Base() FunctionsBilling(router, config, linkToNativeFeed) {}
address linkToNativeFeed,
address linkToUsdFeed
) OCR2Base() FunctionsBilling(router, config, linkToNativeFeed, linkToUsdFeed) {}

/// @inheritdoc IFunctionsCoordinator
function getThresholdPublicKey() external view override returns (bytes memory) {
Expand Down Expand Up @@ -80,10 +81,9 @@ contract FunctionsCoordinator is OCR2Base, IFunctionsCoordinator, FunctionsBilli

/// @dev check if node is in current transmitter list
function _isTransmitter(address node) internal view returns (bool) {
address[] memory nodes = s_transmitters;
// Bounded by "maxNumOracles" on OCR2Abstract.sol
for (uint256 i = 0; i < nodes.length; ++i) {
if (nodes[i] == node) {
for (uint256 i = 0; i < s_transmitters.length; ++i) {
if (s_transmitters[i] == node) {
return true;
}
}
Expand All @@ -94,7 +94,8 @@ contract FunctionsCoordinator is OCR2Base, IFunctionsCoordinator, FunctionsBilli
function startRequest(
FunctionsResponse.RequestMeta calldata request
) external override onlyRouter returns (FunctionsResponse.Commitment memory commitment) {
commitment = _startBilling(request);
uint72 operationFee;
(commitment, operationFee) = _startBilling(request);

emit OracleRequest(
commitment.requestId,
Expand All @@ -107,7 +108,21 @@ contract FunctionsCoordinator is OCR2Base, IFunctionsCoordinator, FunctionsBilli
request.dataVersion,
request.flags,
request.callbackGasLimit,
commitment
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;
Expand Down Expand Up @@ -205,4 +220,9 @@ contract FunctionsCoordinator is OCR2Base, IFunctionsCoordinator, FunctionsBilli
function _onlyOwner() internal view override {
_validateOwnership();
}

/// @dev Used in FunctionsBilling.sol
function _owner() internal view override returns (address owner) {
return this.owner();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,23 @@ interface IFunctionsBilling {
/// @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 getDONFee(bytes memory requestCBOR) external view returns (uint72);
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 getAdminFee() external view returns (uint72);
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
Expand Down Expand Up @@ -53,9 +62,12 @@ struct FunctionsBillingConfig {
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.
uint72 donFee; // ║ Additional flat fee (in Juels of LINK) that will be split between Node Operators. Max value is 2^80 - 1 == 1.2m LINK.
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.
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.
}
Loading

0 comments on commit 2cd4bc5

Please sign in to comment.