Skip to content

Commit 52be242

Browse files
authored
feat: add abstract vRNG consumer (#2)
* feat: add abstract VRF consumer * vrf tests; docs and typing * fix tests for evm matrix * gitignore * nit: whitespace * update readme * use "vRNG" in docs * vrf -> vrng * test names * adjust wf
1 parent 5700ff4 commit 52be242

13 files changed

Lines changed: 500 additions & 1 deletion

.github/workflows/test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,5 +52,6 @@ jobs:
5252
env:
5353
RPC_URL: ${{ matrix.chain == 'testnet' && secrets.TESTNET_RPC_URL || secrets.MAINNET_RPC_URL }}
5454
run: |
55+
FOUNDRY_PROFILE=${{ matrix.mode == 'zksync' && 'zksync' || env.FOUNDRY_PROFILE }}
5556
forge test -vvv ${{ matrix.mode == 'zksync' && '--zksync' || '' }}
5657
id: test

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,16 @@ The Solidity smart contracts are located in the `src` directory.
2525
utils
2626
├─ LibAGW - "Utilities for AGW smart accounts"
2727
├─ LibClone — "Clones for ZKsync"
28-
└─ LibEVM — "Detection of EVM bytecode contracts"
28+
├─ LibEVM — "Detection of EVM bytecode contracts"
29+
└─ vrng
30+
├─ DataTypes - "Data structures for vRNG operations"
31+
├─ Errors - "Custom error definitions for vRNG consumers"
32+
├─ VRNGConsumer - "Basic random number consumer contract"
33+
└─ VRNGConsumerAdvanced - "Advanced random number consumer with custom normalization"
2934
```
35+
> [!NOTE]
36+
> 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.
37+
3038

3139
## Contributing
3240

foundry.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,8 @@ no_match_contract = "(LibAGWTest|LibEVMTest)"
88
[profile.default.zksync]
99
enable_eravm_extensions = true
1010

11+
[profile.zksync]
12+
# use to unset the no_match_contract from default profile
13+
no_match_contract = "_"
14+
1115
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// SPDX-License-Identifier: MIT LICENSE
2+
3+
pragma solidity ^0.8.0;
4+
5+
interface IVRNGSystem {
6+
/**
7+
* Starts a VRNG random number request
8+
*
9+
* @param traceId Optional Id to use when tracing the request
10+
* @return requestId for the random number, will be passed to the callback contract
11+
*/
12+
function requestRandomNumberWithTraceId(uint256 traceId) external returns (uint256);
13+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// SPDX-License-Identifier: MIT LICENSE
2+
3+
pragma solidity ^0.8.0;
4+
5+
interface IVRNGSystemCallback {
6+
event RandomNumberRequested(uint256 indexed requestId);
7+
event RandomNumberFulfilled(uint256 indexed requestId, uint256 normalizedRandomNumber);
8+
9+
/**
10+
* Callback for when a Random Number is delivered
11+
*
12+
* @param requestId Id of the request
13+
* @param randomNumber Random number that was generated by the Verified Random Number Generator Tool
14+
*/
15+
function randomNumberCallback(uint256 requestId, uint256 randomNumber) external;
16+
}

src/utils/vrng/DataTypes.sol

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.0;
3+
4+
/// @dev VRNG statuses
5+
/// @dev NONE: No VRNG request has been made
6+
/// @dev REQUESTED: VRNG request has been made, and is pending fulfillment
7+
/// @dev FULFILLED: The VRNG request has been fulfilled with a random number
8+
enum VRNGStatus {
9+
NONE,
10+
REQUESTED,
11+
FULFILLED
12+
}
13+
14+
/// @dev VRNG request details
15+
/// @param status The status of the VRNG request, see `VRNGStatus`
16+
/// @param randomNumber The random number returned by the VRNG system
17+
struct VRNGRequest {
18+
VRNGStatus status;
19+
uint256 randomNumber;
20+
}
21+
22+
/// @dev VRNG normalization methods
23+
/// @dev MOST_EFFICIENT: The most efficient normalization method - uses requestId + randomNumber
24+
/// @dev BALANCED: Normalization method balanced for gas efficiency and normalization - hash of
25+
/// encoded requestId and randomNumber
26+
/// @dev MOST_NORMALIZED: The most normalized normalization method - uses hash of encoded pseudo
27+
/// random block hash and random number
28+
enum VRNGNormalizationMethod {
29+
MOST_EFFICIENT,
30+
BALANCED,
31+
MOST_NORMALIZED
32+
}

src/utils/vrng/Errors.sol

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.0;
3+
4+
/// @dev VRNG Consumer is not initialized - must be initialized before requesting randomness
5+
error VRNGConsumer__NotInitialized();
6+
7+
/// @dev VRNG request has not been made - request ID must be recieved before a fulfullment callback
8+
/// can be processed
9+
error VRNGConsumer__InvalidFulfillment();
10+
11+
/// @dev VRNG request id is invalid. Request id must be unique.
12+
error VRNGConsumer__InvalidRequestId();
13+
14+
/// @dev Call can only be made by the VRNG system
15+
error VRNGConsumer__OnlyVRNGSystem();

src/utils/vrng/VRNGConsumer.sol

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// SPDX-License-Identifier: MIT LICENSE
2+
3+
pragma solidity ^0.8.26;
4+
5+
import {VRNGConsumerAdvanced} from "./VRNGConsumerAdvanced.sol";
6+
import {VRNGNormalizationMethod} from "./DataTypes.sol";
7+
8+
/// @title VRNGConsumer
9+
/// @author Abstract (https://github.com/Abstract-Foundation/absmate/blob/main/src/utils/VRNGConsumer.sol)
10+
/// @notice A simple consumer contract for requesting randomness from Proof of Play vRNG. (https://docs.proofofplay.com/services/vrng/about)
11+
/// @dev Must initialize via `_setVRNG` function before requesting randomness.
12+
abstract contract VRNGConsumer is VRNGConsumerAdvanced {
13+
constructor() VRNGConsumerAdvanced(VRNGNormalizationMethod.BALANCED) {}
14+
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.26;
3+
4+
import {IVRNGSystemCallback} from "../../interfaces/vrng/IVRNGSystemCallback.sol";
5+
import {IVRNGSystem} from "../../interfaces/vrng/IVRNGSystem.sol";
6+
import "./DataTypes.sol";
7+
import "./Errors.sol";
8+
9+
/// @title VRNGConsumerAdvanced
10+
/// @author Abstract (https://github.com/Abstract-Foundation/absmate/blob/main/src/utils/VRNGConsumerAdvanced.sol)
11+
/// @notice A consumer contract for requesting randomness from Proof of Play vRNG. (https://docs.proofofplay.com/services/vrng/about)
12+
/// @dev Allows configuration of the randomness normalization method to one of three presets.
13+
/// Must initialize via `_setVRNG` function before requesting randomness.
14+
abstract contract VRNGConsumerAdvanced is IVRNGSystemCallback {
15+
// keccak256(abi.encode(uint256(keccak256("absmate.vrng.consumer.storage")) - 1)) & ~bytes32(uint256(0xff))
16+
bytes32 private constant VRNG_STORAGE_LOCATION = 0xfc4de942100e62e9eb61034c75124e3689e7605ae081e19c59907d5c442ea700;
17+
18+
/// @dev The function used to normalize the drand random number
19+
function(uint256,uint256) internal returns(uint256) internal immutable _normalizeRandomNumber;
20+
21+
struct VRNGConsumerStorage {
22+
IVRNGSystem vrng;
23+
mapping(uint256 requestId => VRNGRequest details) requests;
24+
}
25+
26+
/// @notice The VRNG system contract address
27+
IVRNGSystem public immutable vrng;
28+
29+
/// @dev Create a new VRNG consumer with the specified normalization method.
30+
/// @param normalizationMethod The normalization method to use. See `VRNGNormalizationMethod` for more details.
31+
constructor(VRNGNormalizationMethod normalizationMethod) {
32+
if (normalizationMethod == VRNGNormalizationMethod.MOST_EFFICIENT) {
33+
_normalizeRandomNumber = _normalizeRandomNumberHyperEfficient;
34+
} else if (normalizationMethod == VRNGNormalizationMethod.BALANCED) {
35+
_normalizeRandomNumber = _normalizeRandomNumberHashWithRequestId;
36+
} else if (normalizationMethod == VRNGNormalizationMethod.MOST_NORMALIZED) {
37+
_normalizeRandomNumber = _normalizeRandomNumberMostNormalized;
38+
}
39+
}
40+
41+
/// @notice Callback for VRNG system. Not user callable.
42+
/// @dev Callback function for the VRNG system, normalizes the random number and calls the
43+
/// _onRandomNumberFulfilled function with the normalized randomness
44+
/// @param requestId The request ID
45+
/// @param randomNumber The random number
46+
function randomNumberCallback(uint256 requestId, uint256 randomNumber) external {
47+
VRNGConsumerStorage storage $ = _getVRNGStorage();
48+
require(msg.sender == address($.vrng), VRNGConsumer__OnlyVRNGSystem());
49+
50+
VRNGRequest memory request = $.requests[requestId];
51+
require(request.status == VRNGStatus.REQUESTED, VRNGConsumer__InvalidFulfillment());
52+
uint256 normalizedRandomNumber = _normalizeRandomNumber(randomNumber, requestId);
53+
54+
$.requests[requestId] = VRNGRequest({status: VRNGStatus.FULFILLED, randomNumber: normalizedRandomNumber});
55+
56+
emit RandomNumberFulfilled(requestId, normalizedRandomNumber);
57+
58+
_onRandomNumberFulfilled(requestId, normalizedRandomNumber);
59+
}
60+
61+
/// @dev Set the VRNG system contract address. Must be initialized before requesting randomness.
62+
/// @param _vrng The VRNG system contract address
63+
function _setVRNG(address _vrng) internal {
64+
VRNGConsumerStorage storage $ = _getVRNGStorage();
65+
$.vrng = IVRNGSystem(_vrng);
66+
}
67+
68+
/// @dev Request a random number. Guards against duplicate requests.
69+
/// @return requestId The request ID
70+
function _requestRandomNumber() internal returns (uint256) {
71+
return _requestRandomNumber(0);
72+
}
73+
74+
/// @dev Request a random number with a trace ID. Guards against duplicate requests.
75+
/// @param traceId The trace ID
76+
/// @return requestId The request ID
77+
function _requestRandomNumber(uint256 traceId) internal returns (uint256) {
78+
VRNGConsumerStorage storage $ = _getVRNGStorage();
79+
80+
if (address($.vrng) == address(0)) {
81+
revert VRNGConsumer__NotInitialized();
82+
}
83+
84+
uint256 requestId = $.vrng.requestRandomNumberWithTraceId(traceId);
85+
86+
VRNGRequest storage request = $.requests[requestId];
87+
require(request.status == VRNGStatus.NONE, VRNGConsumer__InvalidRequestId());
88+
request.status = VRNGStatus.REQUESTED;
89+
90+
emit RandomNumberRequested(requestId);
91+
92+
return requestId;
93+
}
94+
95+
/// @dev Callback function for the VRNG system. Override to handle randomness.
96+
/// @param requestId The request ID
97+
/// @param randomNumber The random number
98+
function _onRandomNumberFulfilled(uint256 requestId, uint256 randomNumber) internal virtual;
99+
100+
/// @dev Get the VRNG request details for a given request ID
101+
/// @param requestId The request ID
102+
/// @return result The VRNG result
103+
function _getVRNGRequest(uint256 requestId) internal view returns (VRNGRequest memory) {
104+
VRNGConsumerStorage storage $ = _getVRNGStorage();
105+
return $.requests[requestId];
106+
}
107+
108+
function _getVRNGStorage() private pure returns (VRNGConsumerStorage storage $) {
109+
assembly {
110+
$.slot := VRNG_STORAGE_LOCATION
111+
}
112+
}
113+
114+
/// @dev Most efficient, but least normalized method of normalization - uses requestId + number
115+
function _normalizeRandomNumberHyperEfficient(uint256 randomNumber, uint256 requestId)
116+
private
117+
pure
118+
returns (uint256)
119+
{
120+
// allow overflow here in case of a very large requestId and randomness
121+
unchecked {
122+
return requestId + randomNumber;
123+
}
124+
}
125+
126+
/// @dev Hash with requestId - balance of efficiency and normalization
127+
function _normalizeRandomNumberHashWithRequestId(uint256 randomNumber, uint256 requestId)
128+
private
129+
pure
130+
returns (uint256)
131+
{
132+
return uint256(keccak256(abi.encodePacked(requestId, randomNumber)));
133+
}
134+
135+
/// @dev Most expensive, but most normalized method of normalization - hash of encoded blockhash
136+
/// from pseudo random block number derived via requestId
137+
function _normalizeRandomNumberMostNormalized(uint256 randomNumber, uint256 requestId)
138+
private
139+
view
140+
returns (uint256)
141+
{
142+
unchecked {
143+
return uint256(keccak256(abi.encodePacked(blockhash(block.number - (requestId % 256)), randomNumber)));
144+
}
145+
}
146+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.26;
3+
4+
import {VRNGConsumerAdvanced} from "../../src/utils/vrng/VRNGConsumerAdvanced.sol";
5+
import {VRNGNormalizationMethod, VRNGRequest} from "../../src/utils/vrng/DataTypes.sol";
6+
7+
contract MockVRNGConsumerAdvancedImplementation is VRNGConsumerAdvanced {
8+
mapping(uint256 requestId => uint256 randomNumber) public _requestToRandomNumber;
9+
mapping(uint256 requestId => bool isFulfilled) public _requestToFulfilled;
10+
11+
constructor(VRNGNormalizationMethod normalizationMethod) VRNGConsumerAdvanced(normalizationMethod) {}
12+
13+
function setVRNG(address vrngSystem) public {
14+
_setVRNG(vrngSystem);
15+
}
16+
17+
function triggerRandomNumberRequest() public {
18+
_requestRandomNumber();
19+
}
20+
21+
function getVRNGRequest(uint256 requestId) public view returns (VRNGRequest memory) {
22+
return _getVRNGRequest(requestId);
23+
}
24+
25+
function _onRandomNumberFulfilled(uint256 requestId, uint256 randomNumber) internal override {
26+
_requestToRandomNumber[requestId] = randomNumber;
27+
_requestToFulfilled[requestId] = true;
28+
}
29+
}

0 commit comments

Comments
 (0)