From b484ff9bf281f3e0b0a8e04e02637fdf395827e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Coffee=E2=98=95=EF=B8=8F?= Date: Tue, 1 Jul 2025 21:44:21 -0400 Subject: [PATCH 01/10] feat: add abstract VRF consumer --- src/interfaces/vrf/IVRFSystem.sol | 13 ++ src/interfaces/vrf/IVRFSystemCallback.sol | 13 ++ src/utils/vrf/DataTypes.sol | 19 +++ src/utils/vrf/VRFConsumer.sol | 14 +++ src/utils/vrf/VRFConsumerAdvanced.sol | 145 ++++++++++++++++++++++ 5 files changed, 204 insertions(+) create mode 100644 src/interfaces/vrf/IVRFSystem.sol create mode 100644 src/interfaces/vrf/IVRFSystemCallback.sol create mode 100644 src/utils/vrf/DataTypes.sol create mode 100644 src/utils/vrf/VRFConsumer.sol create mode 100644 src/utils/vrf/VRFConsumerAdvanced.sol diff --git a/src/interfaces/vrf/IVRFSystem.sol b/src/interfaces/vrf/IVRFSystem.sol new file mode 100644 index 0000000..2369330 --- /dev/null +++ b/src/interfaces/vrf/IVRFSystem.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT LICENSE + +pragma solidity ^0.8.0; + +interface IVRFSystem { + /** + * Starts a VRF random number request + * + * @param traceId Optional Id to use when tracing the request + * @return requestId for the random number, will be passed to the callback contract + */ + function requestRandomNumberWithTraceId(uint256 traceId) external returns (uint256); +} diff --git a/src/interfaces/vrf/IVRFSystemCallback.sol b/src/interfaces/vrf/IVRFSystemCallback.sol new file mode 100644 index 0000000..d265acb --- /dev/null +++ b/src/interfaces/vrf/IVRFSystemCallback.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT LICENSE + +pragma solidity ^0.8.0; + +interface IVRFSystemCallback { + /** + * Callback for when a Random Number is delivered + * + * @param requestId Id of the request + * @param randomNumber Random number that was generated by the Verified Random Number Generator Tool + */ + function randomNumberCallback(uint256 requestId, uint256 randomNumber) external; +} diff --git a/src/utils/vrf/DataTypes.sol b/src/utils/vrf/DataTypes.sol new file mode 100644 index 0000000..82f6dec --- /dev/null +++ b/src/utils/vrf/DataTypes.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +enum VRFStatus { + NONE, + REQUESTED, + FULFILLED +} + +struct VRFResult { + VRFStatus status; + uint256 randomNumber; +} + +enum VRFNormalizationMethod { + MOST_EFFICIENT, + BALANCED, + MOST_NORMALIZED +} diff --git a/src/utils/vrf/VRFConsumer.sol b/src/utils/vrf/VRFConsumer.sol new file mode 100644 index 0000000..a921790 --- /dev/null +++ b/src/utils/vrf/VRFConsumer.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT LICENSE + +pragma solidity ^0.8.26; + +import {VRFConsumerAdvanced} from "./VRFConsumerAdvanced.sol"; +import {VRFNormalizationMethod} from "./DataTypes.sol"; + +/// @title VRFConsumer +/// @author Abstract (https://github.com/Abstract-Foundation/absmate/blob/main/src/utils/VRFConsumer.sol) +/// @notice A simple VRF consumer contract for requesting randomness from Proof of Play VRF. +/// @dev Must initialize via `_setVrf` function before requesting randomness. +abstract contract VRFConsumer is VRFConsumerAdvanced { + constructor() VRFConsumerAdvanced(VRFNormalizationMethod.BALANCED) {} +} diff --git a/src/utils/vrf/VRFConsumerAdvanced.sol b/src/utils/vrf/VRFConsumerAdvanced.sol new file mode 100644 index 0000000..9ca2413 --- /dev/null +++ b/src/utils/vrf/VRFConsumerAdvanced.sol @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {IVRFSystemCallback} from "../../interfaces/vrf/IVRFSystemCallback.sol"; +import {IVRFSystem} from "../../interfaces/vrf/IVRFSystem.sol"; +import {VRFResult, VRFStatus, VRFNormalizationMethod} from "./DataTypes.sol"; + +/// @title VRFConsumerAdvanced +/// @author Abstract (https://github.com/Abstract-Foundation/absmate/blob/main/src/utils/VRFConsumerAdvanced.sol) +/// @notice A VRF consumer contract for requesting randomness from Proof of Play VRF. +/// @dev Allows configuration of the randomness normalization method to one of three presets. +/// Must initialize via `_setVrf` function before requesting randomness. +abstract contract VRFConsumerAdvanced is IVRFSystemCallback { + // keccak256(abi.encode(uint256(keccak256("absmate.vrf.consumer.storage")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant VRF_STORAGE_LOCATION = 0xa2e33039c2b06aa64552c3b67ad94d03188260355f4bb3d7095983e910872300; + + /// @dev The function used to normalize the drand random number + function(uint256,uint256) internal returns(uint256) internal immutable _normalizeRandomNumber; + + struct VRFConsumerStorage { + IVRFSystem vrf; + mapping(uint256 requestId => VRFResult result) results; + } + + /// @notice The VRF system contract address + IVRFSystem public immutable vrf; + + error NotInitialized(); + error UnsupportedChain(); + error InvalidFulfillment(); + error InvalidRequest(); + error OnlyVRFSystem(); + error UnsupportedNormalizationMethod(); + + constructor(VRFNormalizationMethod normalizationMethod) { + if (normalizationMethod == VRFNormalizationMethod.MOST_EFFICIENT) { + _normalizeRandomNumber = _normalizeRandomNumberHyperEfficient; + } else if (normalizationMethod == VRFNormalizationMethod.BALANCED) { + _normalizeRandomNumber = _normalizeRandomNumberHashWithRequestId; + } else if (normalizationMethod == VRFNormalizationMethod.MOST_NORMALIZED) { + _normalizeRandomNumber = _normalizeRandomNumberMostNormalized; + } else { + revert UnsupportedNormalizationMethod(); + } + } + + /// @notice Callback for VRF system. Not user callable. + /// @dev Callback function for the VRF system, normalizes the random number and calls the + /// _onRandomNumberFulfilled function with the normalized randomness + /// @param requestId The request ID + /// @param randomNumber The random number + function randomNumberCallback(uint256 requestId, uint256 randomNumber) external { + VRFConsumerStorage storage $ = _getVRFStorage(); + require(msg.sender == address($.vrf), OnlyVRFSystem()); + + VRFResult memory result = $.results[requestId]; + require(result.status == VRFStatus.REQUESTED, InvalidFulfillment()); + uint256 normalizedRandomNumber = _normalizeRandomNumber(randomNumber, requestId); + + $.results[requestId] = VRFResult({status: VRFStatus.FULFILLED, randomNumber: normalizedRandomNumber}); + + _onRandomNumberFulfilled(requestId, normalizedRandomNumber); + } + + /// @dev Set the VRF system contract address. Must be initialized before requesting randomness. + /// @param _vrf The VRF system contract address + function _setVrf(address _vrf) internal { + VRFConsumerStorage storage $ = _getVRFStorage(); + $.vrf = IVRFSystem(_vrf); + } + + /// @dev Request a random number. Guards against duplicate requests. + /// @return requestId The request ID + function _requestRandomNumber() internal returns (uint256) { + return _requestRandomNumber(0); + } + + /// @dev Request a random number with a trace ID. Guards against duplicate requests. + /// @param traceId The trace ID + /// @return requestId The request ID + function _requestRandomNumber(uint256 traceId) internal returns (uint256) { + VRFConsumerStorage storage $ = _getVRFStorage(); + + if (address($.vrf) == address(0)) { + revert NotInitialized(); + } + + uint256 requestId = $.vrf.requestRandomNumberWithTraceId(traceId); + + VRFResult storage result = $.results[requestId]; + require(result.status == VRFStatus.NONE, InvalidRequest()); + result.status = VRFStatus.REQUESTED; + return requestId; + } + + /// @dev Callback function for the VRF system. Override to handle randomness. + /// @param requestId The request ID + /// @param randomNumber The random number + function _onRandomNumberFulfilled(uint256 requestId, uint256 randomNumber) internal virtual; + + /// @dev Get the VRF result for a given request ID + /// @param requestId The request ID + /// @return result The VRF result + function _getVrfResult(uint256 requestId) internal view returns (VRFResult memory) { + VRFConsumerStorage storage $ = _getVRFStorage(); + return $.results[requestId]; + } + + function _getVRFStorage() private pure returns (VRFConsumerStorage storage $) { + assembly { + $.slot := VRF_STORAGE_LOCATION + } + } + + /// @dev Most efficient, but least normalized method of normalization - uses requestId + number + function _normalizeRandomNumberHyperEfficient(uint256 randomNumber, uint256 requestId) + private + pure + returns (uint256) + { + // allow overflow here in case of a very large requestId and randomness + unchecked { + return requestId + randomNumber; + } + } + + /// @dev Hash with requestId - balance of efficiency and normalization + function _normalizeRandomNumberHashWithRequestId(uint256 randomNumber, uint256 requestId) + private + pure + returns (uint256) + { + return uint256(keccak256(abi.encodePacked(requestId, randomNumber))); + } + + /// @dev Most expensive, but most normalized method of normalization - hash of encoded blockhash + /// from pseudo random block number derived via requestId + function _normalizeRandomNumberMostNormalized(uint256 randomNumber, uint256 requestId) + private + view + returns (uint256) + { + return uint256(keccak256(abi.encodePacked(blockhash(block.number - (requestId % 256)), randomNumber))); + } +} From 9acc5ed8395f25861a7e2c0cf67bb55f9218ef45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Coffee=E2=98=95=EF=B8=8F?= Date: Tue, 1 Jul 2025 23:09:51 -0400 Subject: [PATCH 02/10] vrf tests; docs and typing --- .gitignore | 2 + src/interfaces/vrf/IVRFSystemCallback.sol | 3 + src/utils/vrf/DataTypes.sol | 15 +- src/utils/vrf/Errors.sol | 15 ++ src/utils/vrf/VRFConsumerAdvanced.sol | 47 ++--- .../MockVRFConsumerAdvancedImplementation.sol | 29 +++ test/mocks/MockVRFConsumerImplementation.sol | 27 +++ test/mocks/MockVRFSystem.sol | 25 +++ test/vrf/VRFConsumerTest.sol | 169 ++++++++++++++++++ 9 files changed, 308 insertions(+), 24 deletions(-) create mode 100644 src/utils/vrf/Errors.sol create mode 100644 test/mocks/MockVRFConsumerAdvancedImplementation.sol create mode 100644 test/mocks/MockVRFConsumerImplementation.sol create mode 100644 test/mocks/MockVRFSystem.sol create mode 100644 test/vrf/VRFConsumerTest.sol diff --git a/.gitignore b/.gitignore index 16d545b..18390bb 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ docs/ # Dotenv file .env + +.DS_Store \ No newline at end of file diff --git a/src/interfaces/vrf/IVRFSystemCallback.sol b/src/interfaces/vrf/IVRFSystemCallback.sol index d265acb..8c00b52 100644 --- a/src/interfaces/vrf/IVRFSystemCallback.sol +++ b/src/interfaces/vrf/IVRFSystemCallback.sol @@ -3,6 +3,9 @@ pragma solidity ^0.8.0; interface IVRFSystemCallback { + event RandomNumberRequested(uint256 indexed requestId); + event RandomNumberFulfilled(uint256 indexed requestId, uint256 normalizedRandomNumber); + /** * Callback for when a Random Number is delivered * diff --git a/src/utils/vrf/DataTypes.sol b/src/utils/vrf/DataTypes.sol index 82f6dec..d8367a4 100644 --- a/src/utils/vrf/DataTypes.sol +++ b/src/utils/vrf/DataTypes.sol @@ -1,17 +1,30 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; +/// @dev VRF statuses +/// @dev NONE: No VRF request has been made +/// @dev REQUESTED: VRF request has been made, and is pending fulfillment +/// @dev FULFILLED: The VRF request has been fulfilled with a random number enum VRFStatus { NONE, REQUESTED, FULFILLED } -struct VRFResult { +/// @dev VRF request details +/// @param status The status of the VRF request, see `VRFStatus` +/// @param randomNumber The random number returned by the VRF system +struct VRFRequest { VRFStatus status; uint256 randomNumber; } +/// @dev VRF normalization methods +/// @dev MOST_EFFICIENT: The most efficient normalization method - uses requestId + randomNumber +/// @dev BALANCED: Normalization method balanced for gas efficiency and normalization - hash of +/// encoded requestId and randomNumber +/// @dev MOST_NORMALIZED: The most normalized normalization method - uses hash of encoded pseudo +/// random block hash and random number enum VRFNormalizationMethod { MOST_EFFICIENT, BALANCED, diff --git a/src/utils/vrf/Errors.sol b/src/utils/vrf/Errors.sol new file mode 100644 index 0000000..1104322 --- /dev/null +++ b/src/utils/vrf/Errors.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @dev VRF Consumer is not initialized - must be initialized before requesting randomness +error VRFConsumer__NotInitialized(); + +/// @dev VRF request has not been made - request ID must be recieved before a fulfullment callback +/// can be processed +error VRFConsumer__InvalidFulfillment(); + +/// @dev VRF request id is invalid. Request id must be unique. +error VRFConsumer__InvalidRequestId(); + +/// @dev Call can only be made by the VRF system +error VRFConsumer__OnlyVRFSystem(); diff --git a/src/utils/vrf/VRFConsumerAdvanced.sol b/src/utils/vrf/VRFConsumerAdvanced.sol index 9ca2413..b93f8c9 100644 --- a/src/utils/vrf/VRFConsumerAdvanced.sol +++ b/src/utils/vrf/VRFConsumerAdvanced.sol @@ -3,7 +3,8 @@ pragma solidity ^0.8.26; import {IVRFSystemCallback} from "../../interfaces/vrf/IVRFSystemCallback.sol"; import {IVRFSystem} from "../../interfaces/vrf/IVRFSystem.sol"; -import {VRFResult, VRFStatus, VRFNormalizationMethod} from "./DataTypes.sol"; +import "./DataTypes.sol"; +import "./Errors.sol"; /// @title VRFConsumerAdvanced /// @author Abstract (https://github.com/Abstract-Foundation/absmate/blob/main/src/utils/VRFConsumerAdvanced.sol) @@ -19,19 +20,14 @@ abstract contract VRFConsumerAdvanced is IVRFSystemCallback { struct VRFConsumerStorage { IVRFSystem vrf; - mapping(uint256 requestId => VRFResult result) results; + mapping(uint256 requestId => VRFRequest details) requests; } /// @notice The VRF system contract address IVRFSystem public immutable vrf; - error NotInitialized(); - error UnsupportedChain(); - error InvalidFulfillment(); - error InvalidRequest(); - error OnlyVRFSystem(); - error UnsupportedNormalizationMethod(); - + /// @dev Create a new VRF consumer with the specified normalization method. + /// @param normalizationMethod The normalization method to use. See `VRFNormalizationMethod` for more details. constructor(VRFNormalizationMethod normalizationMethod) { if (normalizationMethod == VRFNormalizationMethod.MOST_EFFICIENT) { _normalizeRandomNumber = _normalizeRandomNumberHyperEfficient; @@ -39,8 +35,6 @@ abstract contract VRFConsumerAdvanced is IVRFSystemCallback { _normalizeRandomNumber = _normalizeRandomNumberHashWithRequestId; } else if (normalizationMethod == VRFNormalizationMethod.MOST_NORMALIZED) { _normalizeRandomNumber = _normalizeRandomNumberMostNormalized; - } else { - revert UnsupportedNormalizationMethod(); } } @@ -51,13 +45,15 @@ abstract contract VRFConsumerAdvanced is IVRFSystemCallback { /// @param randomNumber The random number function randomNumberCallback(uint256 requestId, uint256 randomNumber) external { VRFConsumerStorage storage $ = _getVRFStorage(); - require(msg.sender == address($.vrf), OnlyVRFSystem()); + require(msg.sender == address($.vrf), VRFConsumer__OnlyVRFSystem()); - VRFResult memory result = $.results[requestId]; - require(result.status == VRFStatus.REQUESTED, InvalidFulfillment()); + VRFRequest memory request = $.requests[requestId]; + require(request.status == VRFStatus.REQUESTED, VRFConsumer__InvalidFulfillment()); uint256 normalizedRandomNumber = _normalizeRandomNumber(randomNumber, requestId); - $.results[requestId] = VRFResult({status: VRFStatus.FULFILLED, randomNumber: normalizedRandomNumber}); + $.requests[requestId] = VRFRequest({status: VRFStatus.FULFILLED, randomNumber: normalizedRandomNumber}); + + emit RandomNumberFulfilled(requestId, normalizedRandomNumber); _onRandomNumberFulfilled(requestId, normalizedRandomNumber); } @@ -82,14 +78,17 @@ abstract contract VRFConsumerAdvanced is IVRFSystemCallback { VRFConsumerStorage storage $ = _getVRFStorage(); if (address($.vrf) == address(0)) { - revert NotInitialized(); + revert VRFConsumer__NotInitialized(); } uint256 requestId = $.vrf.requestRandomNumberWithTraceId(traceId); - VRFResult storage result = $.results[requestId]; - require(result.status == VRFStatus.NONE, InvalidRequest()); - result.status = VRFStatus.REQUESTED; + VRFRequest storage request = $.requests[requestId]; + require(request.status == VRFStatus.NONE, VRFConsumer__InvalidRequestId()); + request.status = VRFStatus.REQUESTED; + + emit RandomNumberRequested(requestId); + return requestId; } @@ -98,12 +97,12 @@ abstract contract VRFConsumerAdvanced is IVRFSystemCallback { /// @param randomNumber The random number function _onRandomNumberFulfilled(uint256 requestId, uint256 randomNumber) internal virtual; - /// @dev Get the VRF result for a given request ID + /// @dev Get the VRF request details for a given request ID /// @param requestId The request ID /// @return result The VRF result - function _getVrfResult(uint256 requestId) internal view returns (VRFResult memory) { + function _getVrfRequest(uint256 requestId) internal view returns (VRFRequest memory) { VRFConsumerStorage storage $ = _getVRFStorage(); - return $.results[requestId]; + return $.requests[requestId]; } function _getVRFStorage() private pure returns (VRFConsumerStorage storage $) { @@ -140,6 +139,8 @@ abstract contract VRFConsumerAdvanced is IVRFSystemCallback { view returns (uint256) { - return uint256(keccak256(abi.encodePacked(blockhash(block.number - (requestId % 256)), randomNumber))); + unchecked { + return uint256(keccak256(abi.encodePacked(blockhash(block.number - (requestId % 256)), randomNumber))); + } } } diff --git a/test/mocks/MockVRFConsumerAdvancedImplementation.sol b/test/mocks/MockVRFConsumerAdvancedImplementation.sol new file mode 100644 index 0000000..2224625 --- /dev/null +++ b/test/mocks/MockVRFConsumerAdvancedImplementation.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {VRFConsumerAdvanced} from "../../src/utils/vrf/VRFConsumerAdvanced.sol"; +import {VRFNormalizationMethod, VRFRequest} from "../../src/utils/vrf/DataTypes.sol"; + +contract MockVRFConsumerAdvancedImplementation is VRFConsumerAdvanced { + mapping(uint256 requestId => uint256 randomNumber) public _requestToRandomNumber; + mapping(uint256 requestId => bool isFulfilled) public _requestToFulfilled; + + constructor(VRFNormalizationMethod normalizationMethod) VRFConsumerAdvanced(normalizationMethod) {} + + function setVrf(address vrfSystem) public { + _setVrf(vrfSystem); + } + + function triggerRandomNumberRequest() public { + _requestRandomNumber(); + } + + function getVrfRequest(uint256 requestId) public view returns (VRFRequest memory) { + return _getVrfRequest(requestId); + } + + function _onRandomNumberFulfilled(uint256 requestId, uint256 randomNumber) internal override { + _requestToRandomNumber[requestId] = randomNumber; + _requestToFulfilled[requestId] = true; + } +} diff --git a/test/mocks/MockVRFConsumerImplementation.sol b/test/mocks/MockVRFConsumerImplementation.sol new file mode 100644 index 0000000..a8b4e6d --- /dev/null +++ b/test/mocks/MockVRFConsumerImplementation.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {VRFConsumer} from "../../src/utils/vrf/VRFConsumer.sol"; +import {VRFNormalizationMethod, VRFRequest} from "../../src/utils/vrf/DataTypes.sol"; + +contract MockVRFConsumerImplementation is VRFConsumer { + mapping(uint256 requestId => uint256 randomNumber) public _requestToRandomNumber; + mapping(uint256 requestId => bool isFulfilled) public _requestToFulfilled; + + function setVrf(address vrfSystem) public { + _setVrf(vrfSystem); + } + + function triggerRandomNumberRequest() public { + _requestRandomNumber(); + } + + function getVrfRequest(uint256 requestId) public view returns (VRFRequest memory) { + return _getVrfRequest(requestId); + } + + function _onRandomNumberFulfilled(uint256 requestId, uint256 randomNumber) internal override { + _requestToRandomNumber[requestId] = randomNumber; + _requestToFulfilled[requestId] = true; + } +} diff --git a/test/mocks/MockVRFSystem.sol b/test/mocks/MockVRFSystem.sol new file mode 100644 index 0000000..ab39dff --- /dev/null +++ b/test/mocks/MockVRFSystem.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {IVRFSystem} from "../../src/interfaces/vrf/IVRFSystem.sol"; +import {IVRFSystemCallback} from "../../src/interfaces/vrf/IVRFSystemCallback.sol"; + +struct VRFRequest { + uint256 traceId; + uint256 randomNumber; + IVRFSystemCallback callback; + bool isFulfilled; +} + +contract MockVRFSystem is IVRFSystem { + uint256 public nextRequestId = 1; + + function setNextRequestId(uint256 requestId) external { + nextRequestId = requestId; + } + + function requestRandomNumberWithTraceId(uint256) external returns (uint256) { + uint256 requestId = nextRequestId++; + return requestId; + } +} diff --git a/test/vrf/VRFConsumerTest.sol b/test/vrf/VRFConsumerTest.sol new file mode 100644 index 0000000..97ee5fa --- /dev/null +++ b/test/vrf/VRFConsumerTest.sol @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {MockVRFSystem} from "../mocks/MockVRFSystem.sol"; +import {MockVRFConsumerImplementation} from "../mocks/MockVRFConsumerImplementation.sol"; +import {MockVRFConsumerAdvancedImplementation} from "../mocks/MockVRFConsumerAdvancedImplementation.sol"; +import {Test} from "forge-std/Test.sol"; +import {VRFConsumer} from "../../src/utils/vrf/VRFConsumer.sol"; +import {VRFConsumerAdvanced} from "../../src/utils/vrf/VRFConsumerAdvanced.sol"; +import {VRFRequest, VRFNormalizationMethod} from "../../src/utils/vrf/DataTypes.sol"; +import "../../src/utils/vrf/Errors.sol"; +import {TestBase} from "../TestBase.sol"; +import {console} from "forge-std/console.sol"; + +contract VRFConsumerTest is TestBase { + mapping(uint256 randomNumber => bool seen) private _randomNumberSeen; + + MockVRFSystem public vrfSystem; + MockVRFConsumerImplementation public vrfConsumer; + MockVRFConsumerAdvancedImplementation public vrfConsumerMostNormalized; + MockVRFConsumerAdvancedImplementation public vrfConsumerMostEfficient; + + function setUp() public { + vrfSystem = new MockVRFSystem(); + vrfConsumer = new MockVRFConsumerImplementation(); + vrfConsumer.setVrf(address(vrfSystem)); + vrfConsumerMostNormalized = new MockVRFConsumerAdvancedImplementation(VRFNormalizationMethod.MOST_NORMALIZED); + vrfConsumerMostNormalized.setVrf(address(vrfSystem)); + vrfConsumerMostEfficient = new MockVRFConsumerAdvancedImplementation(VRFNormalizationMethod.MOST_EFFICIENT); + vrfConsumerMostEfficient.setVrf(address(vrfSystem)); + } + + function test_uninitializedVrfRevertsOnRequest() public { + // uninitialize the vrf system + vrfConsumer.setVrf(address(0)); + + vm.expectRevert(VRFConsumer__NotInitialized.selector); + vrfConsumer.triggerRandomNumberRequest(); + } + + function test_requestRandomNumberCallsVrfSystem() public { + assertEq(vrfSystem.nextRequestId(), 1); + vrfConsumer.triggerRandomNumberRequest(); + assertEq(vrfSystem.nextRequestId(), 2); + } + + function testFuzz_fullfillRandomRequestNotFromVrfSystemReverts(address sender, uint256 randomNumber) public { + vm.assume(sender != address(vrfSystem)); + vrfConsumer.triggerRandomNumberRequest(); + + vm.prank(sender); + vm.expectRevert(VRFConsumer__OnlyVRFSystem.selector); + vrfConsumer.randomNumberCallback(0, randomNumber); + } + + function testFuzz_fullfillRandomRequestNotRequestedReverts(uint256 requestId, uint256 randomNumber) public { + vm.prank(address(vrfSystem)); + vm.expectRevert(VRFConsumer__InvalidFulfillment.selector); + vrfConsumer.randomNumberCallback(requestId, randomNumber); + } + + function testFuzz_fulfillRandomRequestAlreadyFulfilledReverts(uint256 randomNumber1, uint256 randomNumber2) + public + { + uint256 requestId = vrfSystem.nextRequestId(); + vrfConsumer.triggerRandomNumberRequest(); + + vm.prank(address(vrfSystem)); + vrfConsumer.randomNumberCallback(requestId, randomNumber1); + + vm.prank(address(vrfSystem)); + vm.expectRevert(VRFConsumer__InvalidFulfillment.selector); + vrfConsumer.randomNumberCallback(requestId, randomNumber2); + } + + function test_duplicateRequestIdFromVrfSystemReverts() public { + uint256 requestId = vrfSystem.nextRequestId(); + + vrfConsumer.triggerRandomNumberRequest(); + vrfSystem.setNextRequestId(requestId); + + vm.expectRevert(VRFConsumer__InvalidRequestId.selector); + vrfConsumer.triggerRandomNumberRequest(); + } + + function testFuzz_vrfResultIsNormalizedDefaultNormalization(uint256 randomNumber) public { + uint256 requestId = vrfSystem.nextRequestId(); + for (uint256 i = 0; i < 10; i++) { + vrfConsumer.triggerRandomNumberRequest(); + } + for (uint256 i = requestId; i < requestId + 10; i++) { + vm.prank(address(vrfSystem)); + vrfConsumer.randomNumberCallback(i, randomNumber); + } + for (uint256 i = requestId; i < requestId + 10; i++) { + VRFRequest memory result = vrfConsumer.getVrfRequest(i); + assertNotEq(result.randomNumber, randomNumber); + assertFalse(_randomNumberSeen[result.randomNumber]); + _randomNumberSeen[result.randomNumber] = true; + } + } + + function testFuzz_vrfResultIsNormalizedEfficientNormalizationMethod(uint256 randomNumber) public { + uint256 requestId = vrfSystem.nextRequestId(); + for (uint256 i = 0; i < 10; i++) { + vrfConsumerMostEfficient.triggerRandomNumberRequest(); + } + for (uint256 i = requestId; i < requestId + 10; i++) { + vm.prank(address(vrfSystem)); + vrfConsumerMostEfficient.randomNumberCallback(i, randomNumber); + } + for (uint256 i = requestId; i < requestId + 10; i++) { + VRFRequest memory result = vrfConsumerMostEfficient.getVrfRequest(i); + assertNotEq(result.randomNumber, randomNumber); + assertFalse(_randomNumberSeen[result.randomNumber]); + _randomNumberSeen[result.randomNumber] = true; + } + } + + function testFuzz_vrfResultIsNormalizedMostNormalizedMethod(uint256 randomNumber) public { + // most normalized method uses blockhash based on a pseudo random block number in the last + // 256 blocks, so we need to roll forward to ensure there are at least 256 blocks available + // to get a blockhash. + vm.roll(256); + + uint256 requestId = vrfSystem.nextRequestId(); + for (uint256 i = 0; i < 10; i++) { + vrfConsumerMostNormalized.triggerRandomNumberRequest(); + } + for (uint256 i = requestId; i < requestId + 10; i++) { + vm.prank(address(vrfSystem)); + vrfConsumerMostNormalized.randomNumberCallback(i, randomNumber); + } + for (uint256 i = requestId; i < requestId + 10; i++) { + VRFRequest memory result = vrfConsumerMostNormalized.getVrfRequest(i); + assertNotEq(result.randomNumber, randomNumber); + assertFalse(_randomNumberSeen[result.randomNumber]); + _randomNumberSeen[result.randomNumber] = true; + } + } + + function testFuzz_canCreateConsumerWithAnyNormalizationMethod(uint8 normalizationMethod) public { + vm.assume(normalizationMethod <= uint8(type(VRFNormalizationMethod).max)); + + address result = _assemblyCreate(normalizationMethod); + + // Deployment should be successful. + assertNotEq(result, address(0)); + } + + function testFuzz_cannotCreateConsumerWithInvalidNormalizationMethod(uint8 normalizationMethod) public { + vm.assume(normalizationMethod > uint8(type(VRFNormalizationMethod).max)); + + address result = _assemblyCreate(normalizationMethod); + + // Deployment should be failed. + assertEq(result, address(0)); + } + + function _assemblyCreate(uint8 normalizationMethod) internal returns (address result) { + bytes memory code = + abi.encodePacked(type(MockVRFConsumerAdvancedImplementation).creationCode, abi.encode(normalizationMethod)); + + // Deploy via assembly to avoid enum revert inside the test code + assembly { + result := create(0, add(code, 0x20), mload(code)) + } + } +} From 64cbc0ad24dd8eb66735141584e61195374190e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Coffee=E2=98=95=EF=B8=8F?= Date: Tue, 1 Jul 2025 23:18:24 -0400 Subject: [PATCH 03/10] fix tests for evm matrix --- .github/workflows/test.yml | 1 - foundry.toml | 2 ++ src/utils/LibEVM.sol | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9911525..608b561 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,7 +45,6 @@ jobs: id: build - name: Run Forge tests - if: ${{ matrix.mode == 'zksync' }} env: RPC_URL: ${{ matrix.chain == 'testnet' && secrets.TESTNET_RPC_URL || secrets.MAINNET_RPC_URL }} run: | diff --git a/foundry.toml b/foundry.toml index 6d85595..cdbb270 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,6 +2,8 @@ src = "src" out = "out" libs = ["lib"] +# These tests can only be run in zksync mode +no_match_contract = "(LibAGWTest|LibEVMTest)" [profile.default.zksync] enable_eravm_extensions = true diff --git a/src/utils/LibEVM.sol b/src/utils/LibEVM.sol index 2e9fa10..b86c563 100644 --- a/src/utils/LibEVM.sol +++ b/src/utils/LibEVM.sol @@ -5,7 +5,7 @@ import {ACCOUNT_CODE_STORAGE_SYSTEM_CONTRACT} from "era-contracts/system-contrac import {Utils} from "era-contracts/system-contracts/libraries/Utils.sol"; /// @notice Library for detecting EVM comaptibility of addresses -// @author Abstract (https://github.com/Abstract-Foundation/absmate/blob/main/src/utils/LibEVM.sol) +/// @author Abstract (https://github.com/Abstract-Foundation/absmate/blob/main/src/utils/LibEVM.sol) library LibEVM { /// @dev returns true if the address is EVM compatible. Empty addresses are assumed to be EOAs /// but could potentially be undeployed zkvm contracts so this should be used with caution. From 926062d599be1b9c44f8a84a872ea2801f573cff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Coffee=E2=98=95=EF=B8=8F?= Date: Tue, 1 Jul 2025 23:29:14 -0400 Subject: [PATCH 04/10] gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 18390bb..b53795c 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,4 @@ docs/ # Dotenv file .env -.DS_Store \ No newline at end of file +.DS_Store From de3fe40017535bf8c76ed337bb8ebec6c2e53e02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Coffee=E2=98=95=EF=B8=8F?= Date: Tue, 1 Jul 2025 23:29:28 -0400 Subject: [PATCH 05/10] nit: whitespace --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index b53795c..1c5937f 100644 --- a/.gitignore +++ b/.gitignore @@ -13,5 +13,4 @@ docs/ # Dotenv file .env - .DS_Store From b38ca27ca1d536ea68211512ce1235914225d69c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Coffee=E2=98=95=EF=B8=8F?= Date: Wed, 2 Jul 2025 13:13:49 -0400 Subject: [PATCH 06/10] update readme --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 502ebf5..9b76a18 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,16 @@ The Solidity smart contracts are located in the `src` directory. utils ├─ LibAGW - "Utilities for AGW smart accounts" ├─ LibClone — "Clones for ZKsync" -└─ LibEVM — "Detection of EVM bytecode contracts" +├─ LibEVM — "Detection of EVM bytecode contracts" +└─ vrf + ├─ DataTypes - "Data structures for VRF operations" + ├─ Errors - "Custom error definitions for VRF" + ├─ VRFConsumer - "Basic VRF consumer contract" + └─ VRFConsumerAdvanced - "Advanced VRF consumer with custom normalization" ``` +> [!NOTE] +> Using VRF contracts requires a subscription to [Proof of Play VRF](https://docs.proofofplay.com/proof-of-play/vrf/introduction). Make sure you have an active subscription before implementing VRF functionality. + ## Contributing From 4683c6182eb2c0262850233457a966ab929b3b5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Coffee=E2=98=95=EF=B8=8F?= Date: Wed, 2 Jul 2025 13:20:34 -0400 Subject: [PATCH 07/10] use "vRNG" in docs --- README.md | 10 +++++----- src/utils/vrf/VRFConsumer.sol | 2 +- src/utils/vrf/VRFConsumerAdvanced.sol | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 9b76a18..e933380 100644 --- a/README.md +++ b/README.md @@ -27,13 +27,13 @@ utils ├─ LibClone — "Clones for ZKsync" ├─ LibEVM — "Detection of EVM bytecode contracts" └─ vrf - ├─ DataTypes - "Data structures for VRF operations" - ├─ Errors - "Custom error definitions for VRF" - ├─ VRFConsumer - "Basic VRF consumer contract" - └─ VRFConsumerAdvanced - "Advanced VRF consumer with custom normalization" + ├─ DataTypes - "Data structures for vRNG operations" + ├─ Errors - "Custom error definitions for vRNG consumers" + ├─ VRFConsumer - "Basic random number consumer contract" + └─ VRFConsumerAdvanced - "Advanced random number consumer with custom normalization" ``` > [!NOTE] -> Using VRF contracts requires a subscription to [Proof of Play VRF](https://docs.proofofplay.com/proof-of-play/vrf/introduction). Make sure you have an active subscription before implementing VRF functionality. +> Using `vrf` contracts requires a subscription to [Proof of Play vRNG](https://docs.proofofplay.com/introduction). Make sure you have an active subscription before implementing vRNG functionality. ## Contributing diff --git a/src/utils/vrf/VRFConsumer.sol b/src/utils/vrf/VRFConsumer.sol index a921790..e5a37a8 100644 --- a/src/utils/vrf/VRFConsumer.sol +++ b/src/utils/vrf/VRFConsumer.sol @@ -7,7 +7,7 @@ import {VRFNormalizationMethod} from "./DataTypes.sol"; /// @title VRFConsumer /// @author Abstract (https://github.com/Abstract-Foundation/absmate/blob/main/src/utils/VRFConsumer.sol) -/// @notice A simple VRF consumer contract for requesting randomness from Proof of Play VRF. +/// @notice A simple consumer contract for requesting randomness from Proof of Play vRNG. (https://docs.proofofplay.com/services/vrng/about) /// @dev Must initialize via `_setVrf` function before requesting randomness. abstract contract VRFConsumer is VRFConsumerAdvanced { constructor() VRFConsumerAdvanced(VRFNormalizationMethod.BALANCED) {} diff --git a/src/utils/vrf/VRFConsumerAdvanced.sol b/src/utils/vrf/VRFConsumerAdvanced.sol index b93f8c9..00f20b8 100644 --- a/src/utils/vrf/VRFConsumerAdvanced.sol +++ b/src/utils/vrf/VRFConsumerAdvanced.sol @@ -8,7 +8,7 @@ import "./Errors.sol"; /// @title VRFConsumerAdvanced /// @author Abstract (https://github.com/Abstract-Foundation/absmate/blob/main/src/utils/VRFConsumerAdvanced.sol) -/// @notice A VRF consumer contract for requesting randomness from Proof of Play VRF. +/// @notice A consumer contract for requesting randomness from Proof of Play vRNG. (https://docs.proofofplay.com/services/vrng/about) /// @dev Allows configuration of the randomness normalization method to one of three presets. /// Must initialize via `_setVrf` function before requesting randomness. abstract contract VRFConsumerAdvanced is IVRFSystemCallback { From 38b6bc65c1d47722e7dc59ebf871543a734fb017 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Coffee=E2=98=95=EF=B8=8F?= Date: Wed, 2 Jul 2025 13:33:35 -0400 Subject: [PATCH 08/10] vrf -> vrng --- README.md | 8 +- .../IVRFSystem.sol => vrng/IVRNGSystem.sol} | 4 +- .../IVRNGSystemCallback.sol} | 2 +- src/utils/vrf/Errors.sol | 15 -- src/utils/vrf/VRFConsumer.sol | 14 -- src/utils/{vrf => vrng}/DataTypes.sol | 24 +-- src/utils/vrng/Errors.sol | 15 ++ src/utils/vrng/VRNGConsumer.sol | 14 ++ .../VRNGConsumerAdvanced.sol} | 90 +++++----- .../MockVRFConsumerAdvancedImplementation.sol | 29 --- ...MockVRNGConsumerAdvancedImplementation.sol | 29 +++ ...sol => MockVRNGConsumerImplementation.sol} | 14 +- .../{MockVRFSystem.sol => MockVRNGSystem.sol} | 10 +- test/vrf/VRFConsumerTest.sol | 169 ------------------ test/vrng/VRNGConsumerTest.sol | 169 ++++++++++++++++++ 15 files changed, 303 insertions(+), 303 deletions(-) rename src/interfaces/{vrf/IVRFSystem.sol => vrng/IVRNGSystem.sol} (83%) rename src/interfaces/{vrf/IVRFSystemCallback.sol => vrng/IVRNGSystemCallback.sol} (94%) delete mode 100644 src/utils/vrf/Errors.sol delete mode 100644 src/utils/vrf/VRFConsumer.sol rename src/utils/{vrf => vrng}/DataTypes.sol (51%) create mode 100644 src/utils/vrng/Errors.sol create mode 100644 src/utils/vrng/VRNGConsumer.sol rename src/utils/{vrf/VRFConsumerAdvanced.sol => vrng/VRNGConsumerAdvanced.sol} (53%) delete mode 100644 test/mocks/MockVRFConsumerAdvancedImplementation.sol create mode 100644 test/mocks/MockVRNGConsumerAdvancedImplementation.sol rename test/mocks/{MockVRFConsumerImplementation.sol => MockVRNGConsumerImplementation.sol} (55%) rename test/mocks/{MockVRFSystem.sol => MockVRNGSystem.sol} (63%) delete mode 100644 test/vrf/VRFConsumerTest.sol create mode 100644 test/vrng/VRNGConsumerTest.sol diff --git a/README.md b/README.md index e933380..a179f1f 100644 --- a/README.md +++ b/README.md @@ -26,14 +26,14 @@ utils ├─ LibAGW - "Utilities for AGW smart accounts" ├─ LibClone — "Clones for ZKsync" ├─ LibEVM — "Detection of EVM bytecode contracts" -└─ vrf +└─ vrng ├─ DataTypes - "Data structures for vRNG operations" ├─ Errors - "Custom error definitions for vRNG consumers" - ├─ VRFConsumer - "Basic random number consumer contract" - └─ VRFConsumerAdvanced - "Advanced random number consumer with custom normalization" + ├─ VRNGConsumer - "Basic random number consumer contract" + └─ VRNGConsumerAdvanced - "Advanced random number consumer with custom normalization" ``` > [!NOTE] -> Using `vrf` contracts requires a subscription to [Proof of Play vRNG](https://docs.proofofplay.com/introduction). Make sure you have an active subscription before implementing vRNG functionality. +> Using `vrng` contracts requires a subscription to [Proof of Play vRNG](https://docs.proofofplay.com/introduction). Make sure you have an active subscription before implementing vRNG functionality. ## Contributing diff --git a/src/interfaces/vrf/IVRFSystem.sol b/src/interfaces/vrng/IVRNGSystem.sol similarity index 83% rename from src/interfaces/vrf/IVRFSystem.sol rename to src/interfaces/vrng/IVRNGSystem.sol index 2369330..a395322 100644 --- a/src/interfaces/vrf/IVRFSystem.sol +++ b/src/interfaces/vrng/IVRNGSystem.sol @@ -2,9 +2,9 @@ pragma solidity ^0.8.0; -interface IVRFSystem { +interface IVRNGSystem { /** - * Starts a VRF random number request + * Starts a VRNG random number request * * @param traceId Optional Id to use when tracing the request * @return requestId for the random number, will be passed to the callback contract diff --git a/src/interfaces/vrf/IVRFSystemCallback.sol b/src/interfaces/vrng/IVRNGSystemCallback.sol similarity index 94% rename from src/interfaces/vrf/IVRFSystemCallback.sol rename to src/interfaces/vrng/IVRNGSystemCallback.sol index 8c00b52..b7e39fd 100644 --- a/src/interfaces/vrf/IVRFSystemCallback.sol +++ b/src/interfaces/vrng/IVRNGSystemCallback.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.0; -interface IVRFSystemCallback { +interface IVRNGSystemCallback { event RandomNumberRequested(uint256 indexed requestId); event RandomNumberFulfilled(uint256 indexed requestId, uint256 normalizedRandomNumber); diff --git a/src/utils/vrf/Errors.sol b/src/utils/vrf/Errors.sol deleted file mode 100644 index 1104322..0000000 --- a/src/utils/vrf/Errors.sol +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -/// @dev VRF Consumer is not initialized - must be initialized before requesting randomness -error VRFConsumer__NotInitialized(); - -/// @dev VRF request has not been made - request ID must be recieved before a fulfullment callback -/// can be processed -error VRFConsumer__InvalidFulfillment(); - -/// @dev VRF request id is invalid. Request id must be unique. -error VRFConsumer__InvalidRequestId(); - -/// @dev Call can only be made by the VRF system -error VRFConsumer__OnlyVRFSystem(); diff --git a/src/utils/vrf/VRFConsumer.sol b/src/utils/vrf/VRFConsumer.sol deleted file mode 100644 index e5a37a8..0000000 --- a/src/utils/vrf/VRFConsumer.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: MIT LICENSE - -pragma solidity ^0.8.26; - -import {VRFConsumerAdvanced} from "./VRFConsumerAdvanced.sol"; -import {VRFNormalizationMethod} from "./DataTypes.sol"; - -/// @title VRFConsumer -/// @author Abstract (https://github.com/Abstract-Foundation/absmate/blob/main/src/utils/VRFConsumer.sol) -/// @notice A simple consumer contract for requesting randomness from Proof of Play vRNG. (https://docs.proofofplay.com/services/vrng/about) -/// @dev Must initialize via `_setVrf` function before requesting randomness. -abstract contract VRFConsumer is VRFConsumerAdvanced { - constructor() VRFConsumerAdvanced(VRFNormalizationMethod.BALANCED) {} -} diff --git a/src/utils/vrf/DataTypes.sol b/src/utils/vrng/DataTypes.sol similarity index 51% rename from src/utils/vrf/DataTypes.sol rename to src/utils/vrng/DataTypes.sol index d8367a4..b9b8f31 100644 --- a/src/utils/vrf/DataTypes.sol +++ b/src/utils/vrng/DataTypes.sol @@ -1,31 +1,31 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -/// @dev VRF statuses -/// @dev NONE: No VRF request has been made -/// @dev REQUESTED: VRF request has been made, and is pending fulfillment -/// @dev FULFILLED: The VRF request has been fulfilled with a random number -enum VRFStatus { +/// @dev VRNG statuses +/// @dev NONE: No VRNG request has been made +/// @dev REQUESTED: VRNG request has been made, and is pending fulfillment +/// @dev FULFILLED: The VRNG request has been fulfilled with a random number +enum VRNGStatus { NONE, REQUESTED, FULFILLED } -/// @dev VRF request details -/// @param status The status of the VRF request, see `VRFStatus` -/// @param randomNumber The random number returned by the VRF system -struct VRFRequest { - VRFStatus status; +/// @dev VRNG request details +/// @param status The status of the VRNG request, see `VRNGStatus` +/// @param randomNumber The random number returned by the VRNG system +struct VRNGRequest { + VRNGStatus status; uint256 randomNumber; } -/// @dev VRF normalization methods +/// @dev VRNG normalization methods /// @dev MOST_EFFICIENT: The most efficient normalization method - uses requestId + randomNumber /// @dev BALANCED: Normalization method balanced for gas efficiency and normalization - hash of /// encoded requestId and randomNumber /// @dev MOST_NORMALIZED: The most normalized normalization method - uses hash of encoded pseudo /// random block hash and random number -enum VRFNormalizationMethod { +enum VRNGNormalizationMethod { MOST_EFFICIENT, BALANCED, MOST_NORMALIZED diff --git a/src/utils/vrng/Errors.sol b/src/utils/vrng/Errors.sol new file mode 100644 index 0000000..67e25f8 --- /dev/null +++ b/src/utils/vrng/Errors.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @dev VRNG Consumer is not initialized - must be initialized before requesting randomness +error VRNGConsumer__NotInitialized(); + +/// @dev VRNG request has not been made - request ID must be recieved before a fulfullment callback +/// can be processed +error VRNGConsumer__InvalidFulfillment(); + +/// @dev VRNG request id is invalid. Request id must be unique. +error VRNGConsumer__InvalidRequestId(); + +/// @dev Call can only be made by the VRNG system +error VRNGConsumer__OnlyVRNGSystem(); diff --git a/src/utils/vrng/VRNGConsumer.sol b/src/utils/vrng/VRNGConsumer.sol new file mode 100644 index 0000000..46824c8 --- /dev/null +++ b/src/utils/vrng/VRNGConsumer.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT LICENSE + +pragma solidity ^0.8.26; + +import {VRNGConsumerAdvanced} from "./VRNGConsumerAdvanced.sol"; +import {VRNGNormalizationMethod} from "./DataTypes.sol"; + +/// @title VRNGConsumer +/// @author Abstract (https://github.com/Abstract-Foundation/absmate/blob/main/src/utils/VRNGConsumer.sol) +/// @notice A simple consumer contract for requesting randomness from Proof of Play vRNG. (https://docs.proofofplay.com/services/vrng/about) +/// @dev Must initialize via `_setVRNG` function before requesting randomness. +abstract contract VRNGConsumer is VRNGConsumerAdvanced { + constructor() VRNGConsumerAdvanced(VRNGNormalizationMethod.BALANCED) {} +} diff --git a/src/utils/vrf/VRFConsumerAdvanced.sol b/src/utils/vrng/VRNGConsumerAdvanced.sol similarity index 53% rename from src/utils/vrf/VRFConsumerAdvanced.sol rename to src/utils/vrng/VRNGConsumerAdvanced.sol index 00f20b8..561e0be 100644 --- a/src/utils/vrf/VRFConsumerAdvanced.sol +++ b/src/utils/vrng/VRNGConsumerAdvanced.sol @@ -1,68 +1,68 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; -import {IVRFSystemCallback} from "../../interfaces/vrf/IVRFSystemCallback.sol"; -import {IVRFSystem} from "../../interfaces/vrf/IVRFSystem.sol"; +import {IVRNGSystemCallback} from "../../interfaces/vrng/IVRNGSystemCallback.sol"; +import {IVRNGSystem} from "../../interfaces/vrng/IVRNGSystem.sol"; import "./DataTypes.sol"; import "./Errors.sol"; -/// @title VRFConsumerAdvanced -/// @author Abstract (https://github.com/Abstract-Foundation/absmate/blob/main/src/utils/VRFConsumerAdvanced.sol) +/// @title VRNGConsumerAdvanced +/// @author Abstract (https://github.com/Abstract-Foundation/absmate/blob/main/src/utils/VRNGConsumerAdvanced.sol) /// @notice A consumer contract for requesting randomness from Proof of Play vRNG. (https://docs.proofofplay.com/services/vrng/about) /// @dev Allows configuration of the randomness normalization method to one of three presets. -/// Must initialize via `_setVrf` function before requesting randomness. -abstract contract VRFConsumerAdvanced is IVRFSystemCallback { - // keccak256(abi.encode(uint256(keccak256("absmate.vrf.consumer.storage")) - 1)) & ~bytes32(uint256(0xff)) - bytes32 private constant VRF_STORAGE_LOCATION = 0xa2e33039c2b06aa64552c3b67ad94d03188260355f4bb3d7095983e910872300; +/// Must initialize via `_setVRNG` function before requesting randomness. +abstract contract VRNGConsumerAdvanced is IVRNGSystemCallback { + // keccak256(abi.encode(uint256(keccak256("absmate.vrng.consumer.storage")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant VRNG_STORAGE_LOCATION = 0xfc4de942100e62e9eb61034c75124e3689e7605ae081e19c59907d5c442ea700; /// @dev The function used to normalize the drand random number function(uint256,uint256) internal returns(uint256) internal immutable _normalizeRandomNumber; - struct VRFConsumerStorage { - IVRFSystem vrf; - mapping(uint256 requestId => VRFRequest details) requests; + struct VRNGConsumerStorage { + IVRNGSystem vrng; + mapping(uint256 requestId => VRNGRequest details) requests; } - /// @notice The VRF system contract address - IVRFSystem public immutable vrf; + /// @notice The VRNG system contract address + IVRNGSystem public immutable vrng; - /// @dev Create a new VRF consumer with the specified normalization method. - /// @param normalizationMethod The normalization method to use. See `VRFNormalizationMethod` for more details. - constructor(VRFNormalizationMethod normalizationMethod) { - if (normalizationMethod == VRFNormalizationMethod.MOST_EFFICIENT) { + /// @dev Create a new VRNG consumer with the specified normalization method. + /// @param normalizationMethod The normalization method to use. See `VRNGNormalizationMethod` for more details. + constructor(VRNGNormalizationMethod normalizationMethod) { + if (normalizationMethod == VRNGNormalizationMethod.MOST_EFFICIENT) { _normalizeRandomNumber = _normalizeRandomNumberHyperEfficient; - } else if (normalizationMethod == VRFNormalizationMethod.BALANCED) { + } else if (normalizationMethod == VRNGNormalizationMethod.BALANCED) { _normalizeRandomNumber = _normalizeRandomNumberHashWithRequestId; - } else if (normalizationMethod == VRFNormalizationMethod.MOST_NORMALIZED) { + } else if (normalizationMethod == VRNGNormalizationMethod.MOST_NORMALIZED) { _normalizeRandomNumber = _normalizeRandomNumberMostNormalized; } } - /// @notice Callback for VRF system. Not user callable. - /// @dev Callback function for the VRF system, normalizes the random number and calls the + /// @notice Callback for VRNG system. Not user callable. + /// @dev Callback function for the VRNG system, normalizes the random number and calls the /// _onRandomNumberFulfilled function with the normalized randomness /// @param requestId The request ID /// @param randomNumber The random number function randomNumberCallback(uint256 requestId, uint256 randomNumber) external { - VRFConsumerStorage storage $ = _getVRFStorage(); - require(msg.sender == address($.vrf), VRFConsumer__OnlyVRFSystem()); + VRNGConsumerStorage storage $ = _getVRNGStorage(); + require(msg.sender == address($.vrng), VRNGConsumer__OnlyVRNGSystem()); - VRFRequest memory request = $.requests[requestId]; - require(request.status == VRFStatus.REQUESTED, VRFConsumer__InvalidFulfillment()); + VRNGRequest memory request = $.requests[requestId]; + require(request.status == VRNGStatus.REQUESTED, VRNGConsumer__InvalidFulfillment()); uint256 normalizedRandomNumber = _normalizeRandomNumber(randomNumber, requestId); - $.requests[requestId] = VRFRequest({status: VRFStatus.FULFILLED, randomNumber: normalizedRandomNumber}); + $.requests[requestId] = VRNGRequest({status: VRNGStatus.FULFILLED, randomNumber: normalizedRandomNumber}); emit RandomNumberFulfilled(requestId, normalizedRandomNumber); _onRandomNumberFulfilled(requestId, normalizedRandomNumber); } - /// @dev Set the VRF system contract address. Must be initialized before requesting randomness. - /// @param _vrf The VRF system contract address - function _setVrf(address _vrf) internal { - VRFConsumerStorage storage $ = _getVRFStorage(); - $.vrf = IVRFSystem(_vrf); + /// @dev Set the VRNG system contract address. Must be initialized before requesting randomness. + /// @param _vrng The VRNG system contract address + function _setVRNG(address _vrng) internal { + VRNGConsumerStorage storage $ = _getVRNGStorage(); + $.vrng = IVRNGSystem(_vrng); } /// @dev Request a random number. Guards against duplicate requests. @@ -75,39 +75,39 @@ abstract contract VRFConsumerAdvanced is IVRFSystemCallback { /// @param traceId The trace ID /// @return requestId The request ID function _requestRandomNumber(uint256 traceId) internal returns (uint256) { - VRFConsumerStorage storage $ = _getVRFStorage(); + VRNGConsumerStorage storage $ = _getVRNGStorage(); - if (address($.vrf) == address(0)) { - revert VRFConsumer__NotInitialized(); + if (address($.vrng) == address(0)) { + revert VRNGConsumer__NotInitialized(); } - uint256 requestId = $.vrf.requestRandomNumberWithTraceId(traceId); + uint256 requestId = $.vrng.requestRandomNumberWithTraceId(traceId); - VRFRequest storage request = $.requests[requestId]; - require(request.status == VRFStatus.NONE, VRFConsumer__InvalidRequestId()); - request.status = VRFStatus.REQUESTED; + VRNGRequest storage request = $.requests[requestId]; + require(request.status == VRNGStatus.NONE, VRNGConsumer__InvalidRequestId()); + request.status = VRNGStatus.REQUESTED; emit RandomNumberRequested(requestId); return requestId; } - /// @dev Callback function for the VRF system. Override to handle randomness. + /// @dev Callback function for the VRNG system. Override to handle randomness. /// @param requestId The request ID /// @param randomNumber The random number function _onRandomNumberFulfilled(uint256 requestId, uint256 randomNumber) internal virtual; - /// @dev Get the VRF request details for a given request ID + /// @dev Get the VRNG request details for a given request ID /// @param requestId The request ID - /// @return result The VRF result - function _getVrfRequest(uint256 requestId) internal view returns (VRFRequest memory) { - VRFConsumerStorage storage $ = _getVRFStorage(); + /// @return result The VRNG result + function _getVRNGRequest(uint256 requestId) internal view returns (VRNGRequest memory) { + VRNGConsumerStorage storage $ = _getVRNGStorage(); return $.requests[requestId]; } - function _getVRFStorage() private pure returns (VRFConsumerStorage storage $) { + function _getVRNGStorage() private pure returns (VRNGConsumerStorage storage $) { assembly { - $.slot := VRF_STORAGE_LOCATION + $.slot := VRNG_STORAGE_LOCATION } } diff --git a/test/mocks/MockVRFConsumerAdvancedImplementation.sol b/test/mocks/MockVRFConsumerAdvancedImplementation.sol deleted file mode 100644 index 2224625..0000000 --- a/test/mocks/MockVRFConsumerAdvancedImplementation.sol +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.26; - -import {VRFConsumerAdvanced} from "../../src/utils/vrf/VRFConsumerAdvanced.sol"; -import {VRFNormalizationMethod, VRFRequest} from "../../src/utils/vrf/DataTypes.sol"; - -contract MockVRFConsumerAdvancedImplementation is VRFConsumerAdvanced { - mapping(uint256 requestId => uint256 randomNumber) public _requestToRandomNumber; - mapping(uint256 requestId => bool isFulfilled) public _requestToFulfilled; - - constructor(VRFNormalizationMethod normalizationMethod) VRFConsumerAdvanced(normalizationMethod) {} - - function setVrf(address vrfSystem) public { - _setVrf(vrfSystem); - } - - function triggerRandomNumberRequest() public { - _requestRandomNumber(); - } - - function getVrfRequest(uint256 requestId) public view returns (VRFRequest memory) { - return _getVrfRequest(requestId); - } - - function _onRandomNumberFulfilled(uint256 requestId, uint256 randomNumber) internal override { - _requestToRandomNumber[requestId] = randomNumber; - _requestToFulfilled[requestId] = true; - } -} diff --git a/test/mocks/MockVRNGConsumerAdvancedImplementation.sol b/test/mocks/MockVRNGConsumerAdvancedImplementation.sol new file mode 100644 index 0000000..c598fca --- /dev/null +++ b/test/mocks/MockVRNGConsumerAdvancedImplementation.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {VRNGConsumerAdvanced} from "../../src/utils/vrng/VRNGConsumerAdvanced.sol"; +import {VRNGNormalizationMethod, VRNGRequest} from "../../src/utils/vrng/DataTypes.sol"; + +contract MockVRNGConsumerAdvancedImplementation is VRNGConsumerAdvanced { + mapping(uint256 requestId => uint256 randomNumber) public _requestToRandomNumber; + mapping(uint256 requestId => bool isFulfilled) public _requestToFulfilled; + + constructor(VRNGNormalizationMethod normalizationMethod) VRNGConsumerAdvanced(normalizationMethod) {} + + function setVRNG(address vrngSystem) public { + _setVRNG(vrngSystem); + } + + function triggerRandomNumberRequest() public { + _requestRandomNumber(); + } + + function getVRNGRequest(uint256 requestId) public view returns (VRNGRequest memory) { + return _getVRNGRequest(requestId); + } + + function _onRandomNumberFulfilled(uint256 requestId, uint256 randomNumber) internal override { + _requestToRandomNumber[requestId] = randomNumber; + _requestToFulfilled[requestId] = true; + } +} diff --git a/test/mocks/MockVRFConsumerImplementation.sol b/test/mocks/MockVRNGConsumerImplementation.sol similarity index 55% rename from test/mocks/MockVRFConsumerImplementation.sol rename to test/mocks/MockVRNGConsumerImplementation.sol index a8b4e6d..c78696a 100644 --- a/test/mocks/MockVRFConsumerImplementation.sol +++ b/test/mocks/MockVRNGConsumerImplementation.sol @@ -1,23 +1,23 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; -import {VRFConsumer} from "../../src/utils/vrf/VRFConsumer.sol"; -import {VRFNormalizationMethod, VRFRequest} from "../../src/utils/vrf/DataTypes.sol"; +import {VRNGConsumer} from "../../src/utils/vrng/VRNGConsumer.sol"; +import {VRNGNormalizationMethod, VRNGRequest} from "../../src/utils/vrng/DataTypes.sol"; -contract MockVRFConsumerImplementation is VRFConsumer { +contract MockVRNGConsumerImplementation is VRNGConsumer { mapping(uint256 requestId => uint256 randomNumber) public _requestToRandomNumber; mapping(uint256 requestId => bool isFulfilled) public _requestToFulfilled; - function setVrf(address vrfSystem) public { - _setVrf(vrfSystem); + function setVRNG(address vrngSystem) public { + _setVRNG(vrngSystem); } function triggerRandomNumberRequest() public { _requestRandomNumber(); } - function getVrfRequest(uint256 requestId) public view returns (VRFRequest memory) { - return _getVrfRequest(requestId); + function getVRNGRequest(uint256 requestId) public view returns (VRNGRequest memory) { + return _getVRNGRequest(requestId); } function _onRandomNumberFulfilled(uint256 requestId, uint256 randomNumber) internal override { diff --git a/test/mocks/MockVRFSystem.sol b/test/mocks/MockVRNGSystem.sol similarity index 63% rename from test/mocks/MockVRFSystem.sol rename to test/mocks/MockVRNGSystem.sol index ab39dff..c58d36f 100644 --- a/test/mocks/MockVRFSystem.sol +++ b/test/mocks/MockVRNGSystem.sol @@ -1,17 +1,17 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; -import {IVRFSystem} from "../../src/interfaces/vrf/IVRFSystem.sol"; -import {IVRFSystemCallback} from "../../src/interfaces/vrf/IVRFSystemCallback.sol"; +import {IVRNGSystem} from "../../src/interfaces/vrng/IVRNGSystem.sol"; +import {IVRNGSystemCallback} from "../../src/interfaces/vrng/IVRNGSystemCallback.sol"; -struct VRFRequest { +struct VRNGRequest { uint256 traceId; uint256 randomNumber; - IVRFSystemCallback callback; + IVRNGSystemCallback callback; bool isFulfilled; } -contract MockVRFSystem is IVRFSystem { +contract MockVRNGSystem is IVRNGSystem { uint256 public nextRequestId = 1; function setNextRequestId(uint256 requestId) external { diff --git a/test/vrf/VRFConsumerTest.sol b/test/vrf/VRFConsumerTest.sol deleted file mode 100644 index 97ee5fa..0000000 --- a/test/vrf/VRFConsumerTest.sol +++ /dev/null @@ -1,169 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.26; - -import {MockVRFSystem} from "../mocks/MockVRFSystem.sol"; -import {MockVRFConsumerImplementation} from "../mocks/MockVRFConsumerImplementation.sol"; -import {MockVRFConsumerAdvancedImplementation} from "../mocks/MockVRFConsumerAdvancedImplementation.sol"; -import {Test} from "forge-std/Test.sol"; -import {VRFConsumer} from "../../src/utils/vrf/VRFConsumer.sol"; -import {VRFConsumerAdvanced} from "../../src/utils/vrf/VRFConsumerAdvanced.sol"; -import {VRFRequest, VRFNormalizationMethod} from "../../src/utils/vrf/DataTypes.sol"; -import "../../src/utils/vrf/Errors.sol"; -import {TestBase} from "../TestBase.sol"; -import {console} from "forge-std/console.sol"; - -contract VRFConsumerTest is TestBase { - mapping(uint256 randomNumber => bool seen) private _randomNumberSeen; - - MockVRFSystem public vrfSystem; - MockVRFConsumerImplementation public vrfConsumer; - MockVRFConsumerAdvancedImplementation public vrfConsumerMostNormalized; - MockVRFConsumerAdvancedImplementation public vrfConsumerMostEfficient; - - function setUp() public { - vrfSystem = new MockVRFSystem(); - vrfConsumer = new MockVRFConsumerImplementation(); - vrfConsumer.setVrf(address(vrfSystem)); - vrfConsumerMostNormalized = new MockVRFConsumerAdvancedImplementation(VRFNormalizationMethod.MOST_NORMALIZED); - vrfConsumerMostNormalized.setVrf(address(vrfSystem)); - vrfConsumerMostEfficient = new MockVRFConsumerAdvancedImplementation(VRFNormalizationMethod.MOST_EFFICIENT); - vrfConsumerMostEfficient.setVrf(address(vrfSystem)); - } - - function test_uninitializedVrfRevertsOnRequest() public { - // uninitialize the vrf system - vrfConsumer.setVrf(address(0)); - - vm.expectRevert(VRFConsumer__NotInitialized.selector); - vrfConsumer.triggerRandomNumberRequest(); - } - - function test_requestRandomNumberCallsVrfSystem() public { - assertEq(vrfSystem.nextRequestId(), 1); - vrfConsumer.triggerRandomNumberRequest(); - assertEq(vrfSystem.nextRequestId(), 2); - } - - function testFuzz_fullfillRandomRequestNotFromVrfSystemReverts(address sender, uint256 randomNumber) public { - vm.assume(sender != address(vrfSystem)); - vrfConsumer.triggerRandomNumberRequest(); - - vm.prank(sender); - vm.expectRevert(VRFConsumer__OnlyVRFSystem.selector); - vrfConsumer.randomNumberCallback(0, randomNumber); - } - - function testFuzz_fullfillRandomRequestNotRequestedReverts(uint256 requestId, uint256 randomNumber) public { - vm.prank(address(vrfSystem)); - vm.expectRevert(VRFConsumer__InvalidFulfillment.selector); - vrfConsumer.randomNumberCallback(requestId, randomNumber); - } - - function testFuzz_fulfillRandomRequestAlreadyFulfilledReverts(uint256 randomNumber1, uint256 randomNumber2) - public - { - uint256 requestId = vrfSystem.nextRequestId(); - vrfConsumer.triggerRandomNumberRequest(); - - vm.prank(address(vrfSystem)); - vrfConsumer.randomNumberCallback(requestId, randomNumber1); - - vm.prank(address(vrfSystem)); - vm.expectRevert(VRFConsumer__InvalidFulfillment.selector); - vrfConsumer.randomNumberCallback(requestId, randomNumber2); - } - - function test_duplicateRequestIdFromVrfSystemReverts() public { - uint256 requestId = vrfSystem.nextRequestId(); - - vrfConsumer.triggerRandomNumberRequest(); - vrfSystem.setNextRequestId(requestId); - - vm.expectRevert(VRFConsumer__InvalidRequestId.selector); - vrfConsumer.triggerRandomNumberRequest(); - } - - function testFuzz_vrfResultIsNormalizedDefaultNormalization(uint256 randomNumber) public { - uint256 requestId = vrfSystem.nextRequestId(); - for (uint256 i = 0; i < 10; i++) { - vrfConsumer.triggerRandomNumberRequest(); - } - for (uint256 i = requestId; i < requestId + 10; i++) { - vm.prank(address(vrfSystem)); - vrfConsumer.randomNumberCallback(i, randomNumber); - } - for (uint256 i = requestId; i < requestId + 10; i++) { - VRFRequest memory result = vrfConsumer.getVrfRequest(i); - assertNotEq(result.randomNumber, randomNumber); - assertFalse(_randomNumberSeen[result.randomNumber]); - _randomNumberSeen[result.randomNumber] = true; - } - } - - function testFuzz_vrfResultIsNormalizedEfficientNormalizationMethod(uint256 randomNumber) public { - uint256 requestId = vrfSystem.nextRequestId(); - for (uint256 i = 0; i < 10; i++) { - vrfConsumerMostEfficient.triggerRandomNumberRequest(); - } - for (uint256 i = requestId; i < requestId + 10; i++) { - vm.prank(address(vrfSystem)); - vrfConsumerMostEfficient.randomNumberCallback(i, randomNumber); - } - for (uint256 i = requestId; i < requestId + 10; i++) { - VRFRequest memory result = vrfConsumerMostEfficient.getVrfRequest(i); - assertNotEq(result.randomNumber, randomNumber); - assertFalse(_randomNumberSeen[result.randomNumber]); - _randomNumberSeen[result.randomNumber] = true; - } - } - - function testFuzz_vrfResultIsNormalizedMostNormalizedMethod(uint256 randomNumber) public { - // most normalized method uses blockhash based on a pseudo random block number in the last - // 256 blocks, so we need to roll forward to ensure there are at least 256 blocks available - // to get a blockhash. - vm.roll(256); - - uint256 requestId = vrfSystem.nextRequestId(); - for (uint256 i = 0; i < 10; i++) { - vrfConsumerMostNormalized.triggerRandomNumberRequest(); - } - for (uint256 i = requestId; i < requestId + 10; i++) { - vm.prank(address(vrfSystem)); - vrfConsumerMostNormalized.randomNumberCallback(i, randomNumber); - } - for (uint256 i = requestId; i < requestId + 10; i++) { - VRFRequest memory result = vrfConsumerMostNormalized.getVrfRequest(i); - assertNotEq(result.randomNumber, randomNumber); - assertFalse(_randomNumberSeen[result.randomNumber]); - _randomNumberSeen[result.randomNumber] = true; - } - } - - function testFuzz_canCreateConsumerWithAnyNormalizationMethod(uint8 normalizationMethod) public { - vm.assume(normalizationMethod <= uint8(type(VRFNormalizationMethod).max)); - - address result = _assemblyCreate(normalizationMethod); - - // Deployment should be successful. - assertNotEq(result, address(0)); - } - - function testFuzz_cannotCreateConsumerWithInvalidNormalizationMethod(uint8 normalizationMethod) public { - vm.assume(normalizationMethod > uint8(type(VRFNormalizationMethod).max)); - - address result = _assemblyCreate(normalizationMethod); - - // Deployment should be failed. - assertEq(result, address(0)); - } - - function _assemblyCreate(uint8 normalizationMethod) internal returns (address result) { - bytes memory code = - abi.encodePacked(type(MockVRFConsumerAdvancedImplementation).creationCode, abi.encode(normalizationMethod)); - - // Deploy via assembly to avoid enum revert inside the test code - assembly { - result := create(0, add(code, 0x20), mload(code)) - } - } -} diff --git a/test/vrng/VRNGConsumerTest.sol b/test/vrng/VRNGConsumerTest.sol new file mode 100644 index 0000000..30b72ab --- /dev/null +++ b/test/vrng/VRNGConsumerTest.sol @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {MockVRNGSystem} from "../mocks/MockVRNGSystem.sol"; +import {MockVRNGConsumerImplementation} from "../mocks/MockVRNGConsumerImplementation.sol"; +import {MockVRNGConsumerAdvancedImplementation} from "../mocks/MockVRNGConsumerAdvancedImplementation.sol"; +import {Test} from "forge-std/Test.sol"; +import {VRNGConsumer} from "../../src/utils/vrng/VRNGConsumer.sol"; +import {VRNGConsumerAdvanced} from "../../src/utils/vrng/VRNGConsumerAdvanced.sol"; +import {VRNGRequest, VRNGNormalizationMethod} from "../../src/utils/vrng/DataTypes.sol"; +import "../../src/utils/vrng/Errors.sol"; +import {TestBase} from "../TestBase.sol"; +import {console} from "forge-std/console.sol"; + +contract VRNGConsumerTest is TestBase { + mapping(uint256 randomNumber => bool seen) private _randomNumberSeen; + + MockVRNGSystem public vrngSystem; + MockVRNGConsumerImplementation public vrngConsumer; + MockVRNGConsumerAdvancedImplementation public vrngConsumerMostNormalized; + MockVRNGConsumerAdvancedImplementation public vrngConsumerMostEfficient; + + function setUp() public { + vrngSystem = new MockVRNGSystem(); + vrngConsumer = new MockVRNGConsumerImplementation(); + vrngConsumer.setVRNG(address(vrngSystem)); + vrngConsumerMostNormalized = new MockVRNGConsumerAdvancedImplementation(VRNGNormalizationMethod.MOST_NORMALIZED); + vrngConsumerMostNormalized.setVRNG(address(vrngSystem)); + vrngConsumerMostEfficient = new MockVRNGConsumerAdvancedImplementation(VRNGNormalizationMethod.MOST_EFFICIENT); + vrngConsumerMostEfficient.setVRNG(address(vrngSystem)); + } + + function test_uninitializedVrfRevertsOnRequest() public { + // uninitialize the vrng system + vrngConsumer.setVRNG(address(0)); + + vm.expectRevert(VRNGConsumer__NotInitialized.selector); + vrngConsumer.triggerRandomNumberRequest(); + } + + function test_requestRandomNumberCallsVrfSystem() public { + assertEq(vrngSystem.nextRequestId(), 1); + vrngConsumer.triggerRandomNumberRequest(); + assertEq(vrngSystem.nextRequestId(), 2); + } + + function testFuzz_fullfillRandomRequestNotFromVrfSystemReverts(address sender, uint256 randomNumber) public { + vm.assume(sender != address(vrngSystem)); + vrngConsumer.triggerRandomNumberRequest(); + + vm.prank(sender); + vm.expectRevert(VRNGConsumer__OnlyVRNGSystem.selector); + vrngConsumer.randomNumberCallback(0, randomNumber); + } + + function testFuzz_fullfillRandomRequestNotRequestedReverts(uint256 requestId, uint256 randomNumber) public { + vm.prank(address(vrngSystem)); + vm.expectRevert(VRNGConsumer__InvalidFulfillment.selector); + vrngConsumer.randomNumberCallback(requestId, randomNumber); + } + + function testFuzz_fulfillRandomRequestAlreadyFulfilledReverts(uint256 randomNumber1, uint256 randomNumber2) + public + { + uint256 requestId = vrngSystem.nextRequestId(); + vrngConsumer.triggerRandomNumberRequest(); + + vm.prank(address(vrngSystem)); + vrngConsumer.randomNumberCallback(requestId, randomNumber1); + + vm.prank(address(vrngSystem)); + vm.expectRevert(VRNGConsumer__InvalidFulfillment.selector); + vrngConsumer.randomNumberCallback(requestId, randomNumber2); + } + + function test_duplicateRequestIdFromVrfSystemReverts() public { + uint256 requestId = vrngSystem.nextRequestId(); + + vrngConsumer.triggerRandomNumberRequest(); + vrngSystem.setNextRequestId(requestId); + + vm.expectRevert(VRNGConsumer__InvalidRequestId.selector); + vrngConsumer.triggerRandomNumberRequest(); + } + + function testFuzz_vrngResultIsNormalizedDefaultNormalization(uint256 randomNumber) public { + uint256 requestId = vrngSystem.nextRequestId(); + for (uint256 i = 0; i < 10; i++) { + vrngConsumer.triggerRandomNumberRequest(); + } + for (uint256 i = requestId; i < requestId + 10; i++) { + vm.prank(address(vrngSystem)); + vrngConsumer.randomNumberCallback(i, randomNumber); + } + for (uint256 i = requestId; i < requestId + 10; i++) { + VRNGRequest memory result = vrngConsumer.getVRNGRequest(i); + assertNotEq(result.randomNumber, randomNumber); + assertFalse(_randomNumberSeen[result.randomNumber]); + _randomNumberSeen[result.randomNumber] = true; + } + } + + function testFuzz_vrngResultIsNormalizedEfficientNormalizationMethod(uint256 randomNumber) public { + uint256 requestId = vrngSystem.nextRequestId(); + for (uint256 i = 0; i < 10; i++) { + vrngConsumerMostEfficient.triggerRandomNumberRequest(); + } + for (uint256 i = requestId; i < requestId + 10; i++) { + vm.prank(address(vrngSystem)); + vrngConsumerMostEfficient.randomNumberCallback(i, randomNumber); + } + for (uint256 i = requestId; i < requestId + 10; i++) { + VRNGRequest memory result = vrngConsumerMostEfficient.getVRNGRequest(i); + assertNotEq(result.randomNumber, randomNumber); + assertFalse(_randomNumberSeen[result.randomNumber]); + _randomNumberSeen[result.randomNumber] = true; + } + } + + function testFuzz_vrngResultIsNormalizedMostNormalizedMethod(uint256 randomNumber) public { + // most normalized method uses blockhash based on a pseudo random block number in the last + // 256 blocks, so we need to roll forward to ensure there are at least 256 blocks available + // to get a blockhash. + vm.roll(256); + + uint256 requestId = vrngSystem.nextRequestId(); + for (uint256 i = 0; i < 10; i++) { + vrngConsumerMostNormalized.triggerRandomNumberRequest(); + } + for (uint256 i = requestId; i < requestId + 10; i++) { + vm.prank(address(vrngSystem)); + vrngConsumerMostNormalized.randomNumberCallback(i, randomNumber); + } + for (uint256 i = requestId; i < requestId + 10; i++) { + VRNGRequest memory result = vrngConsumerMostNormalized.getVRNGRequest(i); + assertNotEq(result.randomNumber, randomNumber); + assertFalse(_randomNumberSeen[result.randomNumber]); + _randomNumberSeen[result.randomNumber] = true; + } + } + + function testFuzz_canCreateConsumerWithAnyNormalizationMethod(uint8 normalizationMethod) public { + vm.assume(normalizationMethod <= uint8(type(VRNGNormalizationMethod).max)); + + address result = _assemblyCreate(normalizationMethod); + + // Deployment should be successful. + assertNotEq(result, address(0)); + } + + function testFuzz_cannotCreateConsumerWithInvalidNormalizationMethod(uint8 normalizationMethod) public { + vm.assume(normalizationMethod > uint8(type(VRNGNormalizationMethod).max)); + + address result = _assemblyCreate(normalizationMethod); + + // Deployment should be failed. + assertEq(result, address(0)); + } + + function _assemblyCreate(uint8 normalizationMethod) internal returns (address result) { + bytes memory code = + abi.encodePacked(type(MockVRNGConsumerAdvancedImplementation).creationCode, abi.encode(normalizationMethod)); + + // Deploy via assembly to avoid enum revert inside the test code + assembly { + result := create(0, add(code, 0x20), mload(code)) + } + } +} From aa6575aa101fcdeb7e68c76667fd8d37ae37064c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Coffee=E2=98=95=EF=B8=8F?= Date: Wed, 2 Jul 2025 13:35:06 -0400 Subject: [PATCH 09/10] test names --- test/vrng/VRNGConsumerTest.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/vrng/VRNGConsumerTest.sol b/test/vrng/VRNGConsumerTest.sol index 30b72ab..b4ac7f5 100644 --- a/test/vrng/VRNGConsumerTest.sol +++ b/test/vrng/VRNGConsumerTest.sol @@ -30,7 +30,7 @@ contract VRNGConsumerTest is TestBase { vrngConsumerMostEfficient.setVRNG(address(vrngSystem)); } - function test_uninitializedVrfRevertsOnRequest() public { + function test_uninitializedVrngRevertsOnRequest() public { // uninitialize the vrng system vrngConsumer.setVRNG(address(0)); @@ -38,13 +38,13 @@ contract VRNGConsumerTest is TestBase { vrngConsumer.triggerRandomNumberRequest(); } - function test_requestRandomNumberCallsVrfSystem() public { + function test_requestRandomNumberCallsVrngSystem() public { assertEq(vrngSystem.nextRequestId(), 1); vrngConsumer.triggerRandomNumberRequest(); assertEq(vrngSystem.nextRequestId(), 2); } - function testFuzz_fullfillRandomRequestNotFromVrfSystemReverts(address sender, uint256 randomNumber) public { + function testFuzz_fullfillRandomRequestNotFromVrngSystemReverts(address sender, uint256 randomNumber) public { vm.assume(sender != address(vrngSystem)); vrngConsumer.triggerRandomNumberRequest(); @@ -73,7 +73,7 @@ contract VRNGConsumerTest is TestBase { vrngConsumer.randomNumberCallback(requestId, randomNumber2); } - function test_duplicateRequestIdFromVrfSystemReverts() public { + function test_duplicateRequestIdFromVrngSystemReverts() public { uint256 requestId = vrngSystem.nextRequestId(); vrngConsumer.triggerRandomNumberRequest(); From ae120a42309be5608d0ac33fbad52aff7f237384 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Coffee=E2=98=95=EF=B8=8F?= Date: Wed, 2 Jul 2025 13:46:11 -0400 Subject: [PATCH 10/10] adjust wf --- .github/workflows/test.yml | 1 + foundry.toml | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1924340..a68fbbc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -52,5 +52,6 @@ jobs: env: RPC_URL: ${{ matrix.chain == 'testnet' && secrets.TESTNET_RPC_URL || secrets.MAINNET_RPC_URL }} run: | + FOUNDRY_PROFILE=${{ matrix.mode == 'zksync' && 'zksync' || env.FOUNDRY_PROFILE }} forge test -vvv ${{ matrix.mode == 'zksync' && '--zksync' || '' }} id: test diff --git a/foundry.toml b/foundry.toml index cdbb270..5e05ba8 100644 --- a/foundry.toml +++ b/foundry.toml @@ -8,4 +8,8 @@ no_match_contract = "(LibAGWTest|LibEVMTest)" [profile.default.zksync] enable_eravm_extensions = true +[profile.zksync] +# use to unset the no_match_contract from default profile +no_match_contract = "_" + # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options