@@ -8,7 +8,9 @@ import "@pythnetwork/entropy-sdk-solidity/EntropyEvents.sol";
88import "@pythnetwork/entropy-sdk-solidity/IEntropy.sol " ;
99import "@pythnetwork/entropy-sdk-solidity/IEntropyConsumer.sol " ;
1010import "@openzeppelin/contracts/utils/math/SafeCast.sol " ;
11+ import "@nomad-xyz/excessively-safe-call/src/ExcessivelySafeCall.sol " ;
1112import "./EntropyState.sol " ;
13+ import "@pythnetwork/entropy-sdk-solidity/EntropyStatusConstants.sol " ;
1214
1315// Entropy implements a secure 2-party random number generation procedure. The protocol
1416// is an extension of a simple commit/reveal protocol. The original version has the following steps:
@@ -76,6 +78,8 @@ import "./EntropyState.sol";
7678// the user is always incentivized to reveal their random number, and that the protocol has an escape hatch for
7779// cases where the user chooses not to reveal.
7880abstract contract Entropy is IEntropy , EntropyState {
81+ using ExcessivelySafeCall for address ;
82+
7983 function _initialize (
8084 address admin ,
8185 uint128 pythFeeInWei ,
@@ -247,7 +251,9 @@ abstract contract Entropy is IEntropy, EntropyState {
247251
248252 req.blockNumber = SafeCast.toUint64 (block .number );
249253 req.useBlockhash = useBlockhash;
250- req.isRequestWithCallback = isRequestWithCallback;
254+ req.callbackStatus = isRequestWithCallback
255+ ? EntropyStatusConstants.CALLBACK_NOT_STARTED
256+ : EntropyStatusConstants.CALLBACK_NOT_NECESSARY;
251257 }
252258
253259 // 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 {
403409 }
404410
405411 // 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
407413 // the corresponding random number.
408414 //
409415 // 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 {
423429 sequenceNumber
424430 );
425431
426- if (req.isRequestWithCallback) {
432+ if (
433+ req.callbackStatus != EntropyStatusConstants.CALLBACK_NOT_NECESSARY
434+ ) {
427435 revert EntropyErrors.InvalidRevealCall ();
428436 }
429437
@@ -467,9 +475,14 @@ abstract contract Entropy is IEntropy, EntropyState {
467475 sequenceNumber
468476 );
469477
470- if (! req.isRequestWithCallback) {
478+ if (
479+ ! (req.callbackStatus ==
480+ EntropyStatusConstants.CALLBACK_NOT_STARTED ||
481+ req.callbackStatus == EntropyStatusConstants.CALLBACK_FAILED)
482+ ) {
471483 revert EntropyErrors.InvalidRevealCall ();
472484 }
485+
473486 bytes32 blockHash;
474487 bytes32 randomNumber;
475488 (randomNumber, blockHash) = revealHelper (
@@ -480,26 +493,75 @@ abstract contract Entropy is IEntropy, EntropyState {
480493
481494 address callAddress = req.requester;
482495
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,
501548 randomNumber
502549 );
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+ }
503565 }
504566 }
505567
0 commit comments