@@ -80,6 +80,10 @@ import "@pythnetwork/entropy-sdk-solidity/EntropyStatusConstants.sol";
80
80
abstract contract Entropy is IEntropy , EntropyState {
81
81
using ExcessivelySafeCall for address ;
82
82
83
+ uint32 public constant TEN_THOUSAND = 10000 ;
84
+ uint32 public constant MAX_GAS_LIMIT =
85
+ uint32 (type (uint16 ).max) * TEN_THOUSAND;
86
+
83
87
function _initialize (
84
88
address admin ,
85
89
uint128 pythFeeInWei ,
@@ -207,7 +211,8 @@ abstract contract Entropy is IEntropy, EntropyState {
207
211
address provider ,
208
212
bytes32 userCommitment ,
209
213
bool useBlockhash ,
210
- bool isRequestWithCallback
214
+ bool isRequestWithCallback ,
215
+ uint32 callbackGasLimit
211
216
) internal returns (EntropyStructs.Request storage req ) {
212
217
EntropyStructs.ProviderInfo storage providerInfo = _state.providers[
213
218
provider
@@ -222,11 +227,12 @@ abstract contract Entropy is IEntropy, EntropyState {
222
227
providerInfo.sequenceNumber += 1 ;
223
228
224
229
// Check that fees were paid and increment the pyth / provider balances.
225
- uint128 requiredFee = getFee (provider);
230
+ uint128 requiredFee = getFeeForGas (provider, callbackGasLimit );
226
231
if (msg .value < requiredFee) revert EntropyErrors.InsufficientFee ();
227
- providerInfo.accruedFeesInWei += providerInfo.feeInWei;
232
+ uint128 providerFee = getProviderFee (provider, callbackGasLimit);
233
+ providerInfo.accruedFeesInWei += providerFee;
228
234
_state.accruedPythFeesInWei += (SafeCast.toUint128 (msg .value ) -
229
- providerInfo.feeInWei );
235
+ providerFee );
230
236
231
237
// Store the user's commitment so that we can fulfill the request later.
232
238
// Warning: this code needs to overwrite *every* field in the request, because the returned request can be
@@ -251,9 +257,26 @@ abstract contract Entropy is IEntropy, EntropyState {
251
257
252
258
req.blockNumber = SafeCast.toUint64 (block .number );
253
259
req.useBlockhash = useBlockhash;
260
+
254
261
req.callbackStatus = isRequestWithCallback
255
262
? EntropyStatusConstants.CALLBACK_NOT_STARTED
256
263
: EntropyStatusConstants.CALLBACK_NOT_NECESSARY;
264
+ if (providerInfo.defaultGasLimit == 0 ) {
265
+ // Provider doesn't support the new callback failure state flow (toggled by setting the gas limit field).
266
+ // Set gasLimit10k to 0 to disable.
267
+ req.gasLimit10k = 0 ;
268
+ } else {
269
+ // This check does two important things:
270
+ // 1. Providers have a minimum fee set for their defaultGasLimit. If users request less gas than that,
271
+ // they still pay for the full gas limit. So we may as well give them the full limit here.
272
+ // 2. If a provider has a defaultGasLimit != 0, we need to ensure that all requests have a >0 gas limit
273
+ // so that we opt-in to the new callback failure state flow.
274
+ req.gasLimit10k = roundTo10kGas (
275
+ callbackGasLimit < providerInfo.defaultGasLimit
276
+ ? providerInfo.defaultGasLimit
277
+ : callbackGasLimit
278
+ );
279
+ }
257
280
}
258
281
259
282
// As a user, request a random number from `provider`. Prior to calling this method, the user should
@@ -275,7 +298,8 @@ abstract contract Entropy is IEntropy, EntropyState {
275
298
provider,
276
299
userCommitment,
277
300
useBlockHash,
278
- false
301
+ false ,
302
+ 0
279
303
);
280
304
assignedSequenceNumber = req.sequenceNumber;
281
305
emit Requested (req);
@@ -292,6 +316,19 @@ abstract contract Entropy is IEntropy, EntropyState {
292
316
function requestWithCallback (
293
317
address provider ,
294
318
bytes32 userRandomNumber
319
+ ) public payable override returns (uint64 ) {
320
+ return
321
+ requestWithCallbackAndGasLimit (
322
+ provider,
323
+ userRandomNumber,
324
+ 0 // Passing 0 will assign the request the provider's default gas limit
325
+ );
326
+ }
327
+
328
+ function requestWithCallbackAndGasLimit (
329
+ address provider ,
330
+ bytes32 userRandomNumber ,
331
+ uint32 gasLimit
295
332
) public payable override returns (uint64 ) {
296
333
EntropyStructs.Request storage req = requestHelper (
297
334
provider,
@@ -300,7 +337,8 @@ abstract contract Entropy is IEntropy, EntropyState {
300
337
// If we remove the blockHash from this, the provider would have no choice but to provide its committed
301
338
// random number. Hence, useBlockHash is set to false.
302
339
false ,
303
- true
340
+ true ,
341
+ gasLimit
304
342
);
305
343
306
344
emit RequestedWithCallback (
@@ -310,7 +348,6 @@ abstract contract Entropy is IEntropy, EntropyState {
310
348
userRandomNumber,
311
349
req
312
350
);
313
-
314
351
return req.sequenceNumber;
315
352
}
316
353
@@ -493,16 +530,27 @@ abstract contract Entropy is IEntropy, EntropyState {
493
530
494
531
address callAddress = req.requester;
495
532
533
+ // If the request has an explicit gas limit, then run the new callback failure state flow.
534
+ //
496
535
// Requests that haven't been invoked yet will be invoked safely (catching reverts), and
497
536
// any reverts will be reported as an event. Any failing requests move to a failure state
498
537
// at which point they can be recovered. The recovery flow invokes the callback directly
499
538
// (no catching errors) which allows callers to easily see the revert reason.
500
- if (req.callbackStatus == EntropyStatusConstants.CALLBACK_NOT_STARTED) {
539
+ if (
540
+ req.gasLimit10k != 0 &&
541
+ req.callbackStatus == EntropyStatusConstants.CALLBACK_NOT_STARTED
542
+ ) {
501
543
req.callbackStatus = EntropyStatusConstants.CALLBACK_IN_PROGRESS;
502
544
bool success;
503
545
bytes memory ret;
546
+ uint256 startingGas = gasleft ();
504
547
(success, ret) = callAddress.excessivelySafeCall (
505
- gasleft (), // TODO: providers need to be able to configure this in the future.
548
+ // Warning: the provided gas limit below is only an *upper bound* on the gas provided to the call.
549
+ // At most 63/64ths of the current context's gas will be provided to a call, which may be less
550
+ // than the indicated gas limit. (See CALL opcode docs here https://www.evm.codes/?fork=cancun#f1)
551
+ // Consequently, out-of-gas reverts need to be handled carefully to ensure that the callback
552
+ // was truly provided with a sufficient amount of gas.
553
+ uint256 (req.gasLimit10k) * TEN_THOUSAND,
506
554
256 , // copy at most 256 bytes of the return value into ret.
507
555
abi.encodeWithSelector (
508
556
IEntropyConsumer._entropyCallback.selector ,
@@ -522,8 +570,16 @@ abstract contract Entropy is IEntropy, EntropyState {
522
570
randomNumber
523
571
);
524
572
clearRequest (provider, sequenceNumber);
525
- } else if (ret.length > 0 ) {
526
- // Callback reverted for some reason that is *not* out-of-gas.
573
+ } else if (
574
+ ret.length > 0 ||
575
+ (startingGas * 31 ) / 32 >
576
+ uint256 (req.gasLimit10k) * TEN_THOUSAND
577
+ ) {
578
+ // The callback reverted for some reason.
579
+ // If ret.length > 0, then we know the callback manually triggered a revert, so it's safe to mark it as failed.
580
+ // If ret.length == 0, then the callback might have run out of gas (though there are other ways to trigger a revert with ret.length == 0).
581
+ // In this case, ensure that the callback was provided with sufficient gas. Technically, 63/64ths of the startingGas is forwarded,
582
+ // but we're using 31/32 to introduce a margin of safety.
527
583
emit CallbackFailed (
528
584
provider,
529
585
req.requester,
@@ -535,9 +591,13 @@ abstract contract Entropy is IEntropy, EntropyState {
535
591
);
536
592
req.callbackStatus = EntropyStatusConstants.CALLBACK_FAILED;
537
593
} else {
538
- // The callback ran out of gas
539
- // TODO: this case will go away once we add provider gas limits, so we're not putting in a custom error type.
540
- require (false , "provider needs to send more gas " );
594
+ // Callback reverted by (potentially) running out of gas, but the calling context did not have enough gas
595
+ // to run the callback. This is a corner case that can happen due to the nuances of gas passing
596
+ // in calls (see the comment on the call above).
597
+ //
598
+ // (Note that reverting here plays nicely with the estimateGas RPC method, which binary searches for
599
+ // the smallest gas value that causes the transaction to *succeed*. See https://github.com/ethereum/go-ethereum/pull/3587 )
600
+ revert EntropyErrors.InsufficientGas ();
541
601
}
542
602
} else {
543
603
// This case uses the checks-effects-interactions pattern to avoid reentry attacks
@@ -590,7 +650,43 @@ abstract contract Entropy is IEntropy, EntropyState {
590
650
function getFee (
591
651
address provider
592
652
) public view override returns (uint128 feeAmount ) {
593
- return _state.providers[provider].feeInWei + _state.pythFeeInWei;
653
+ return getFeeForGas (provider, 0 );
654
+ }
655
+
656
+ function getFeeForGas (
657
+ address provider ,
658
+ uint32 gasLimit
659
+ ) public view override returns (uint128 feeAmount ) {
660
+ return getProviderFee (provider, gasLimit) + _state.pythFeeInWei;
661
+ }
662
+
663
+ function getProviderFee (
664
+ address providerAddr ,
665
+ uint32 gasLimit
666
+ ) internal view returns (uint128 feeAmount ) {
667
+ EntropyStructs.ProviderInfo memory provider = _state.providers[
668
+ providerAddr
669
+ ];
670
+
671
+ // Providers charge a minimum of their configured feeInWei for every request.
672
+ // Requests using more than the defaultGasLimit get a proportionally scaled fee.
673
+ // This approach may be somewhat simplistic, but it allows us to continue using the
674
+ // existing feeInWei parameter for the callback failure flow instead of defining new
675
+ // configuration values.
676
+ uint32 roundedGasLimit = uint32 (roundTo10kGas (gasLimit)) * TEN_THOUSAND;
677
+ if (
678
+ provider.defaultGasLimit > 0 &&
679
+ roundedGasLimit > provider.defaultGasLimit
680
+ ) {
681
+ // This calculation rounds down the fee, which means that users can get some gas in the callback for free.
682
+ // However, the value of the free gas is < 1 wei, which is insignificant.
683
+ uint128 additionalFee = ((roundedGasLimit -
684
+ provider.defaultGasLimit) * provider.feeInWei) /
685
+ provider.defaultGasLimit;
686
+ return provider.feeInWei + additionalFee;
687
+ } else {
688
+ return provider.feeInWei;
689
+ }
594
690
}
595
691
596
692
function getPythFee () public view returns (uint128 feeAmount ) {
@@ -687,6 +783,24 @@ abstract contract Entropy is IEntropy, EntropyState {
687
783
);
688
784
}
689
785
786
+ // Set the default gas limit for a request.
787
+ function setDefaultGasLimit (uint32 gasLimit ) external override {
788
+ EntropyStructs.ProviderInfo storage provider = _state.providers[
789
+ msg .sender
790
+ ];
791
+ if (provider.sequenceNumber == 0 ) {
792
+ revert EntropyErrors.NoSuchProvider ();
793
+ }
794
+
795
+ // Check that we can round the gas limit into the 10k gas. This reverts
796
+ // if the provided value exceeds the max.
797
+ roundTo10kGas (gasLimit);
798
+
799
+ uint32 oldGasLimit = provider.defaultGasLimit;
800
+ provider.defaultGasLimit = gasLimit;
801
+ emit ProviderDefaultGasLimitUpdated (msg .sender , oldGasLimit, gasLimit);
802
+ }
803
+
690
804
function constructUserCommitment (
691
805
bytes32 userRandomness
692
806
) public pure override returns (bytes32 userCommitment ) {
@@ -703,6 +817,21 @@ abstract contract Entropy is IEntropy, EntropyState {
703
817
);
704
818
}
705
819
820
+ // Rounds the provided quantity of gas into units of 10k gas.
821
+ // If gas is not evenly divisible by 10k, rounds up.
822
+ function roundTo10kGas (uint32 gas ) internal pure returns (uint16 ) {
823
+ if (gas > MAX_GAS_LIMIT) {
824
+ revert EntropyErrors.MaxGasLimitExceeded ();
825
+ }
826
+
827
+ uint32 gas10k = gas / TEN_THOUSAND;
828
+ if (gas10k * TEN_THOUSAND < gas) {
829
+ gas10k += 1 ;
830
+ }
831
+ // Note: safe cast here should never revert due to the if statement above.
832
+ return SafeCast.toUint16 (gas10k);
833
+ }
834
+
706
835
// Create a unique key for an in-flight randomness request. Returns both a long key for use in the requestsOverflow
707
836
// mapping and a short key for use in the requests array.
708
837
function requestKey (
0 commit comments