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/README.md b/README.md index 502ebf5..a179f1f 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" +└─ vrng + ├─ DataTypes - "Data structures for vRNG operations" + ├─ Errors - "Custom error definitions for vRNG consumers" + ├─ VRNGConsumer - "Basic random number consumer contract" + └─ VRNGConsumerAdvanced - "Advanced random number consumer with custom normalization" ``` +> [!NOTE] +> 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/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 diff --git a/src/interfaces/vrng/IVRNGSystem.sol b/src/interfaces/vrng/IVRNGSystem.sol new file mode 100644 index 0000000..a395322 --- /dev/null +++ b/src/interfaces/vrng/IVRNGSystem.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT LICENSE + +pragma solidity ^0.8.0; + +interface IVRNGSystem { + /** + * 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 + */ + function requestRandomNumberWithTraceId(uint256 traceId) external returns (uint256); +} diff --git a/src/interfaces/vrng/IVRNGSystemCallback.sol b/src/interfaces/vrng/IVRNGSystemCallback.sol new file mode 100644 index 0000000..b7e39fd --- /dev/null +++ b/src/interfaces/vrng/IVRNGSystemCallback.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT LICENSE + +pragma solidity ^0.8.0; + +interface IVRNGSystemCallback { + event RandomNumberRequested(uint256 indexed requestId); + event RandomNumberFulfilled(uint256 indexed requestId, uint256 normalizedRandomNumber); + + /** + * 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/vrng/DataTypes.sol b/src/utils/vrng/DataTypes.sol new file mode 100644 index 0000000..b9b8f31 --- /dev/null +++ b/src/utils/vrng/DataTypes.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @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 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 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 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/vrng/VRNGConsumerAdvanced.sol b/src/utils/vrng/VRNGConsumerAdvanced.sol new file mode 100644 index 0000000..561e0be --- /dev/null +++ b/src/utils/vrng/VRNGConsumerAdvanced.sol @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {IVRNGSystemCallback} from "../../interfaces/vrng/IVRNGSystemCallback.sol"; +import {IVRNGSystem} from "../../interfaces/vrng/IVRNGSystem.sol"; +import "./DataTypes.sol"; +import "./Errors.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 `_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 VRNGConsumerStorage { + IVRNGSystem vrng; + mapping(uint256 requestId => VRNGRequest details) requests; + } + + /// @notice The VRNG system contract address + IVRNGSystem public immutable vrng; + + /// @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 == VRNGNormalizationMethod.BALANCED) { + _normalizeRandomNumber = _normalizeRandomNumberHashWithRequestId; + } else if (normalizationMethod == VRNGNormalizationMethod.MOST_NORMALIZED) { + _normalizeRandomNumber = _normalizeRandomNumberMostNormalized; + } + } + + /// @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 { + VRNGConsumerStorage storage $ = _getVRNGStorage(); + require(msg.sender == address($.vrng), VRNGConsumer__OnlyVRNGSystem()); + + VRNGRequest memory request = $.requests[requestId]; + require(request.status == VRNGStatus.REQUESTED, VRNGConsumer__InvalidFulfillment()); + uint256 normalizedRandomNumber = _normalizeRandomNumber(randomNumber, requestId); + + $.requests[requestId] = VRNGRequest({status: VRNGStatus.FULFILLED, randomNumber: normalizedRandomNumber}); + + emit RandomNumberFulfilled(requestId, normalizedRandomNumber); + + _onRandomNumberFulfilled(requestId, normalizedRandomNumber); + } + + /// @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. + /// @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) { + VRNGConsumerStorage storage $ = _getVRNGStorage(); + + if (address($.vrng) == address(0)) { + revert VRNGConsumer__NotInitialized(); + } + + uint256 requestId = $.vrng.requestRandomNumberWithTraceId(traceId); + + 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 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 VRNG request details for a given request ID + /// @param requestId The request ID + /// @return result The VRNG result + function _getVRNGRequest(uint256 requestId) internal view returns (VRNGRequest memory) { + VRNGConsumerStorage storage $ = _getVRNGStorage(); + return $.requests[requestId]; + } + + function _getVRNGStorage() private pure returns (VRNGConsumerStorage storage $) { + assembly { + $.slot := VRNG_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) + { + unchecked { + return uint256(keccak256(abi.encodePacked(blockhash(block.number - (requestId % 256)), randomNumber))); + } + } +} 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/MockVRNGConsumerImplementation.sol b/test/mocks/MockVRNGConsumerImplementation.sol new file mode 100644 index 0000000..c78696a --- /dev/null +++ b/test/mocks/MockVRNGConsumerImplementation.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {VRNGConsumer} from "../../src/utils/vrng/VRNGConsumer.sol"; +import {VRNGNormalizationMethod, VRNGRequest} from "../../src/utils/vrng/DataTypes.sol"; + +contract MockVRNGConsumerImplementation is VRNGConsumer { + mapping(uint256 requestId => uint256 randomNumber) public _requestToRandomNumber; + mapping(uint256 requestId => bool isFulfilled) public _requestToFulfilled; + + 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/MockVRNGSystem.sol b/test/mocks/MockVRNGSystem.sol new file mode 100644 index 0000000..c58d36f --- /dev/null +++ b/test/mocks/MockVRNGSystem.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {IVRNGSystem} from "../../src/interfaces/vrng/IVRNGSystem.sol"; +import {IVRNGSystemCallback} from "../../src/interfaces/vrng/IVRNGSystemCallback.sol"; + +struct VRNGRequest { + uint256 traceId; + uint256 randomNumber; + IVRNGSystemCallback callback; + bool isFulfilled; +} + +contract MockVRNGSystem is IVRNGSystem { + 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/vrng/VRNGConsumerTest.sol b/test/vrng/VRNGConsumerTest.sol new file mode 100644 index 0000000..b4ac7f5 --- /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_uninitializedVrngRevertsOnRequest() public { + // uninitialize the vrng system + vrngConsumer.setVRNG(address(0)); + + vm.expectRevert(VRNGConsumer__NotInitialized.selector); + vrngConsumer.triggerRandomNumberRequest(); + } + + function test_requestRandomNumberCallsVrngSystem() public { + assertEq(vrngSystem.nextRequestId(), 1); + vrngConsumer.triggerRandomNumberRequest(); + assertEq(vrngSystem.nextRequestId(), 2); + } + + function testFuzz_fullfillRandomRequestNotFromVrngSystemReverts(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_duplicateRequestIdFromVrngSystemReverts() 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)) + } + } +}