@@ -8,7 +8,9 @@ import "@pythnetwork/entropy-sdk-solidity/EntropyEvents.sol";
8
8
import "@pythnetwork/entropy-sdk-solidity/IEntropy.sol " ;
9
9
import "@pythnetwork/entropy-sdk-solidity/IEntropyConsumer.sol " ;
10
10
import "@openzeppelin/contracts/utils/math/SafeCast.sol " ;
11
+ import "@nomad-xyz/excessively-safe-call/src/ExcessivelySafeCall.sol " ;
11
12
import "./EntropyState.sol " ;
13
+ import "@pythnetwork/entropy-sdk-solidity/EntropyStatusConstants.sol " ;
12
14
13
15
// Entropy implements a secure 2-party random number generation procedure. The protocol
14
16
// is an extension of a simple commit/reveal protocol. The original version has the following steps:
@@ -76,6 +78,8 @@ import "./EntropyState.sol";
76
78
// the user is always incentivized to reveal their random number, and that the protocol has an escape hatch for
77
79
// cases where the user chooses not to reveal.
78
80
abstract contract Entropy is IEntropy , EntropyState {
81
+ using ExcessivelySafeCall for address ;
82
+
79
83
function _initialize (
80
84
address admin ,
81
85
uint128 pythFeeInWei ,
@@ -247,7 +251,9 @@ abstract contract Entropy is IEntropy, EntropyState {
247
251
248
252
req.blockNumber = SafeCast.toUint64 (block .number );
249
253
req.useBlockhash = useBlockhash;
250
- req.isRequestWithCallback = isRequestWithCallback;
254
+ req.callbackStatus = isRequestWithCallback
255
+ ? EntropyStatusConstants.CALLBACK_NOT_STARTED
256
+ : EntropyStatusConstants.CALLBACK_NOT_NECESSARY;
251
257
}
252
258
253
259
// As a user, request a random number from `provider`. Prior to calling this method, the user should
@@ -403,7 +409,7 @@ abstract contract Entropy is IEntropy, EntropyState {
403
409
}
404
410
405
411
// Fulfill a request for a random number. This method validates the provided userRandomness and provider's proof
406
- // against the corresponding commitments in the in-flight request. If both values are validated, this function returns
412
+ // against the corresponding commitments in the in-flight request. If both values are validated, this method returns
407
413
// the corresponding random number.
408
414
//
409
415
// Note that this function can only be called once per in-flight request. Calling this function deletes the stored
@@ -423,7 +429,9 @@ abstract contract Entropy is IEntropy, EntropyState {
423
429
sequenceNumber
424
430
);
425
431
426
- if (req.isRequestWithCallback) {
432
+ if (
433
+ req.callbackStatus != EntropyStatusConstants.CALLBACK_NOT_NECESSARY
434
+ ) {
427
435
revert EntropyErrors.InvalidRevealCall ();
428
436
}
429
437
@@ -467,9 +475,14 @@ abstract contract Entropy is IEntropy, EntropyState {
467
475
sequenceNumber
468
476
);
469
477
470
- if (! req.isRequestWithCallback) {
478
+ if (
479
+ ! (req.callbackStatus ==
480
+ EntropyStatusConstants.CALLBACK_NOT_STARTED ||
481
+ req.callbackStatus == EntropyStatusConstants.CALLBACK_FAILED)
482
+ ) {
471
483
revert EntropyErrors.InvalidRevealCall ();
472
484
}
485
+
473
486
bytes32 blockHash;
474
487
bytes32 randomNumber;
475
488
(randomNumber, blockHash) = revealHelper (
@@ -480,26 +493,75 @@ abstract contract Entropy is IEntropy, EntropyState {
480
493
481
494
address callAddress = req.requester;
482
495
483
- emit RevealedWithCallback (
484
- req,
485
- userRandomNumber,
486
- providerRevelation,
487
- randomNumber
488
- );
489
-
490
- clearRequest (provider, sequenceNumber);
491
-
492
- // Check if the callAddress is a contract account.
493
- uint len;
494
- assembly {
495
- len := extcodesize (callAddress)
496
- }
497
- if (len != 0 ) {
498
- IEntropyConsumer (callAddress)._entropyCallback (
499
- sequenceNumber,
500
- provider,
496
+ // Requests that haven't been invoked yet will be invoked safely (catching reverts), and
497
+ // any reverts will be reported as an event. Any failing requests move to a failure state
498
+ // at which point they can be recovered. The recovery flow invokes the callback directly
499
+ // (no catching errors) which allows callers to easily see the revert reason.
500
+ if (req.callbackStatus == EntropyStatusConstants.CALLBACK_NOT_STARTED) {
501
+ req.callbackStatus = EntropyStatusConstants.CALLBACK_IN_PROGRESS;
502
+ bool success;
503
+ bytes memory ret;
504
+ (success, ret) = callAddress.excessivelySafeCall (
505
+ gasleft (), // TODO: providers need to be able to configure this in the future.
506
+ 256 , // copy at most 256 bytes of the return value into ret.
507
+ abi.encodeWithSelector (
508
+ IEntropyConsumer._entropyCallback.selector ,
509
+ sequenceNumber,
510
+ provider,
511
+ randomNumber
512
+ )
513
+ );
514
+ // Reset status to not started here in case the transaction reverts.
515
+ req.callbackStatus = EntropyStatusConstants.CALLBACK_NOT_STARTED;
516
+
517
+ if (success) {
518
+ emit RevealedWithCallback (
519
+ req,
520
+ userRandomNumber,
521
+ providerRevelation,
522
+ randomNumber
523
+ );
524
+ clearRequest (provider, sequenceNumber);
525
+ } else if (ret.length > 0 ) {
526
+ // Callback reverted for some reason that is *not* out-of-gas.
527
+ emit CallbackFailed (
528
+ provider,
529
+ req.requester,
530
+ sequenceNumber,
531
+ userRandomNumber,
532
+ providerRevelation,
533
+ randomNumber,
534
+ ret
535
+ );
536
+ req.callbackStatus = EntropyStatusConstants.CALLBACK_FAILED;
537
+ } 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 " );
541
+ }
542
+ } else {
543
+ // This case uses the checks-effects-interactions pattern to avoid reentry attacks
544
+ emit RevealedWithCallback (
545
+ req,
546
+ userRandomNumber,
547
+ providerRevelation,
501
548
randomNumber
502
549
);
550
+
551
+ clearRequest (provider, sequenceNumber);
552
+
553
+ // Check if the callAddress is a contract account.
554
+ uint len;
555
+ assembly {
556
+ len := extcodesize (callAddress)
557
+ }
558
+ if (len != 0 ) {
559
+ IEntropyConsumer (callAddress)._entropyCallback (
560
+ sequenceNumber,
561
+ provider,
562
+ randomNumber
563
+ );
564
+ }
503
565
}
504
566
}
505
567
0 commit comments