Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
13 changes: 13 additions & 0 deletions src/interfaces/vrng/IVRNGSystem.sol
Original file line number Diff line number Diff line change
@@ -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);
}
16 changes: 16 additions & 0 deletions src/interfaces/vrng/IVRNGSystemCallback.sol
Original file line number Diff line number Diff line change
@@ -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;
}
32 changes: 32 additions & 0 deletions src/utils/vrng/DataTypes.sol
Original file line number Diff line number Diff line change
@@ -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
}
15 changes: 15 additions & 0 deletions src/utils/vrng/Errors.sol
Original file line number Diff line number Diff line change
@@ -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();
14 changes: 14 additions & 0 deletions src/utils/vrng/VRNGConsumer.sol
Original file line number Diff line number Diff line change
@@ -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) {}
}
146 changes: 146 additions & 0 deletions src/utils/vrng/VRNGConsumerAdvanced.sol
Original file line number Diff line number Diff line change
@@ -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)));
}
}
}
29 changes: 29 additions & 0 deletions test/mocks/MockVRNGConsumerAdvancedImplementation.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
27 changes: 27 additions & 0 deletions test/mocks/MockVRNGConsumerImplementation.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
25 changes: 25 additions & 0 deletions test/mocks/MockVRNGSystem.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading