-
Notifications
You must be signed in to change notification settings - Fork 1.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
FUN-1234 (feat): Chainlink Functions premium fees in USD denomination #12000
Changes from all commits
b2927a3
84a2675
8aec7fe
9e50fa9
86321d6
97fea36
e26672d
09df392
1ba2c06
f5afdb8
2adfc1b
753122a
688b5f4
bcc39f2
a1d93bc
361893f
4991b13
2c47244
6ace734
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,6 +17,7 @@ import {ChainSpecificUtil} from "./libraries/ChainSpecificUtil.sol"; | |
abstract contract FunctionsBilling is Routable, IFunctionsBilling { | ||
using FunctionsResponse for FunctionsResponse.RequestMeta; | ||
using FunctionsResponse for FunctionsResponse.Commitment; | ||
using FunctionsResponse for FunctionsResponse.CommitmentWithOperationFee; | ||
using FunctionsResponse for FunctionsResponse.FulfillResult; | ||
|
||
uint256 private constant REASONABLE_GAS_PRICE_CEILING = 1_000_000_000_000_000; // 1 million gwei | ||
|
@@ -26,7 +27,9 @@ abstract contract FunctionsBilling is Routable, IFunctionsBilling { | |
uint96 juelsPerGas, | ||
uint256 l1FeeShareWei, | ||
uint96 callbackCostJuels, | ||
uint96 totalCostJuels | ||
uint72 donFeeJuels, | ||
uint72 adminFeeJuels, | ||
uint72 operationFeeJuels | ||
); | ||
|
||
// ================================================================ | ||
|
@@ -47,6 +50,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(); | ||
|
@@ -61,12 +65,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); | ||
} | ||
|
@@ -95,22 +106,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); | ||
|
@@ -124,6 +141,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 | | ||
// ================================================================ | ||
|
@@ -140,9 +177,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 | ||
|
@@ -151,8 +189,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) { | ||
|
@@ -167,7 +206,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; | ||
} | ||
|
@@ -182,28 +221,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.CommitmentWithOperationFee memory commitment) { | ||
// 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); | ||
uint72 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), | ||
|
@@ -220,18 +259,19 @@ abstract contract FunctionsBilling is Routable, IFunctionsBilling { | |
) | ||
); | ||
|
||
commitment = FunctionsResponse.Commitment({ | ||
adminFee: request.adminFee, | ||
commitment = FunctionsResponse.CommitmentWithOperationFee({ | ||
adminFeeJuels: request.adminFee, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since we are deprecating adminFee (right?), could we save all this effort with a new Commitment struct by putting the new operationFee into adminFee field? Still passing a zero to the router so everything matches there. Not the most elegant approach but maybe a lot simpler? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I hadn't considered this. I'd be very curious to know if this is possible as it would be great to eliminate the changes to the log_poller_wrapper. |
||
coordinator: address(this), | ||
client: request.requestingContract, | ||
subscriptionId: request.subscriptionId, | ||
callbackGasLimit: request.callbackGasLimit, | ||
estimatedTotalCostJuels: estimatedTotalCostJuels, | ||
timeoutTimestamp: timeoutTimestamp, | ||
requestId: requestId, | ||
donFee: donFee, | ||
gasOverheadBeforeCallback: config.gasOverheadBeforeCallback, | ||
gasOverheadAfterCallback: config.gasOverheadAfterCallback | ||
donFeeJuels: donFee, | ||
operationFeeJuels: operationFee, | ||
gasOverheadBeforeCallback: s_config.gasOverheadBeforeCallback, | ||
gasOverheadAfterCallback: s_config.gasOverheadAfterCallback | ||
}); | ||
|
||
s_requestCommitments[requestId] = keccak256(abi.encode(commitment)); | ||
|
@@ -255,7 +295,10 @@ abstract contract FunctionsBilling is Routable, IFunctionsBilling { | |
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)); | ||
FunctionsResponse.CommitmentWithOperationFee memory commitment = abi.decode( | ||
onchainMetadata, | ||
(FunctionsResponse.CommitmentWithOperationFee) | ||
); | ||
|
||
uint256 gasOverheadWei = (commitment.gasOverheadBeforeCallback + commitment.gasOverheadAfterCallback) * tx.gasprice; | ||
uint256 l1FeeShareWei = ChainSpecificUtil._getCurrentTxL1GasFees(msg.data) / reportBatchSize; | ||
|
@@ -268,9 +311,21 @@ 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 | ||
gasOverheadJuels + commitment.donFeeJuels + commitment.operationFeeJuels, // cost without callback or admin fee, those will be added by the Router | ||
msg.sender, | ||
commitment | ||
FunctionsResponse.Commitment({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we add a comment here explaining that we re-pack everything into an older Commitment struct compatible with the Router? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good suggestion, I agree that it'll make it more clear |
||
adminFee: commitment.adminFeeJuels, | ||
coordinator: commitment.coordinator, | ||
client: commitment.client, | ||
subscriptionId: commitment.subscriptionId, | ||
callbackGasLimit: commitment.callbackGasLimit, | ||
estimatedTotalCostJuels: commitment.estimatedTotalCostJuels, | ||
timeoutTimestamp: commitment.timeoutTimestamp, | ||
requestId: commitment.requestId, | ||
donFee: commitment.donFeeJuels, | ||
gasOverheadBeforeCallback: commitment.gasOverheadBeforeCallback, | ||
gasOverheadAfterCallback: commitment.gasOverheadAfterCallback | ||
}) | ||
); | ||
|
||
// The router will only pay the DON on successfully processing the fulfillment | ||
|
@@ -282,19 +337,22 @@ 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; | ||
s_feePool += commitment.donFeeJuels; | ||
// Pay the operation fee to the Coordinator owner | ||
s_withdrawableTokens[_owner()] += commitment.operationFeeJuels; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there already a proposal for concrete values of all 3 fees in prod? Can we discuss that in the Config sheet maybe? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's in another PR in tooling. It will be added to the Config sheet once finalized. |
||
emit RequestBilled({ | ||
requestId: requestId, | ||
juelsPerGas: juelsPerGas, | ||
l1FeeShareWei: l1FeeShareWei, | ||
callbackCostJuels: callbackCostJuels, | ||
totalCostJuels: gasOverheadJuels + callbackCostJuels + commitment.donFee + commitment.adminFee | ||
donFeeJuels: commitment.donFeeJuels, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will this affect Atlas or other data collection? Any other event changes that we are coordinating (or will) with other teams?) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It will not. I already double checked that Atlas only uses Router events. |
||
adminFeeJuels: commitment.adminFeeJuels, | ||
operationFeeJuels: commitment.operationFeeJuels | ||
}); | ||
} | ||
|
||
return resultCode; | ||
} | ||
|
||
|
@@ -377,4 +435,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); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I need to work through this math by hand to verify.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I doubled checked. I think this is good.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We also have tests that triple and quadruple check this, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The tests are in this PR, yes. They should also be reviewed.