@d20dao/vrf-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,33 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.28;
3
+
4
+ import {ID20VRFConsumer, ID20VRFRefundConsumer} from "./interfaces/ID20VRF.sol";
5
+
6
+ /// @notice Every consumer must authenticate callbacks before changing application state.
7
+ abstract contract D20VRFConsumer is ID20VRFConsumer, ID20VRFRefundConsumer {
8
+ address public immutable vrfCoordinator;
9
+ error OnlyCoordinator();
10
+ error InvalidCoordinator();
11
+
12
+ constructor(address coordinator) {
13
+ if (coordinator.code.length == 0) revert InvalidCoordinator();
14
+ vrfCoordinator = coordinator;
15
+ }
16
+
17
+ function rawFulfillRandomness(uint256 requestId, bytes32 randomness) external {
18
+ if (msg.sender != vrfCoordinator) revert OnlyCoordinator();
19
+ _fulfillRandomness(requestId, randomness);
20
+ }
21
+
22
+ function onRefund(uint256 requestId) external {
23
+ if (msg.sender != vrfCoordinator) revert OnlyCoordinator();
24
+ _onRefund(requestId);
25
+ }
26
+
27
+ /// @dev Optional lifecycle notification. Funds may be paid to a different refundAddress
28
+ /// or held as credit. Correlate the request and handle application refunds separately.
29
+ function _onRefund(uint256 requestId) internal virtual {}
30
+
31
+ /// @dev Store randomness only. Keep minting and transfers out of the callback.
32
+ function _fulfillRandomness(uint256 requestId, bytes32 randomness) internal virtual;
33
+ }
@@ -0,0 +1,58 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.28;
3
+
4
+ import {D20VRFConsumer} from "../D20VRFConsumer.sol";
5
+ import {ID20VRF} from "../interfaces/ID20VRF.sol";
6
+
7
+ /// @notice Integration building block, not a replacement for the game's mining/economy contracts.
8
+ /// @dev The actual game validates PoW, locks payment and immutable claim attributes BEFORE
9
+ /// calling _requestForClaim inside that same transaction. No user reveal/contribution call.
10
+ abstract contract MiningRandomnessConsumer is D20VRFConsumer {
11
+ struct ClaimRandomness { uint256 requestId; bytes32 randomness; bool requested; bool ready; }
12
+ mapping(uint256 => ClaimRandomness) public claimRandomness;
13
+ mapping(uint256 => uint256) public requestToClaim;
14
+ mapping(uint256 => bool) private knownRequest;
15
+ event ClaimRandomnessRequested(uint256 indexed claimId, uint256 indexed requestId);
16
+ event ClaimReady(uint256 indexed claimId, uint256 indexed requestId, bytes32 randomness);
17
+ error ClaimAlreadyRequested();
18
+ error UnexpectedCallback();
19
+ error ClaimNotReady();
20
+ error InvalidCandidate();
21
+
22
+ constructor(address coordinator) D20VRFConsumer(coordinator) {}
23
+
24
+ function _requestForClaim(uint256 claimId, bytes32 lockedWork, uint32 callbackGasLimit, address refundAddress)
25
+ internal returns (uint256 requestId)
26
+ {
27
+ if (claimRandomness[claimId].requested) revert ClaimAlreadyRequested();
28
+ claimRandomness[claimId].requested = true;
29
+ ID20VRF rng = ID20VRF(vrfCoordinator);
30
+ requestId = rng.requestRandomness{value: rng.requestFee()}(
31
+ keccak256(abi.encode(claimId, lockedWork)), callbackGasLimit, refundAddress
32
+ );
33
+ claimRandomness[claimId].requestId = requestId;
34
+ requestToClaim[requestId] = claimId;
35
+ knownRequest[requestId] = true;
36
+ emit ClaimRandomnessRequested(claimId, requestId);
37
+ }
38
+
39
+ function _fulfillRandomness(uint256 requestId, bytes32 randomness) internal override {
40
+ if (!knownRequest[requestId]) revert UnexpectedCallback();
41
+ uint256 claimId = requestToClaim[requestId];
42
+ ClaimRandomness storage claim = claimRandomness[claimId];
43
+ if (claim.ready) revert UnexpectedCallback();
44
+ claim.randomness = randomness;
45
+ claim.ready = true;
46
+ emit ClaimReady(claimId, requestId, randomness);
47
+ }
48
+
49
+ function candidateSeed(uint256 claimId, uint8 candidate) public view returns (bytes32) {
50
+ ClaimRandomness storage claim = claimRandomness[claimId];
51
+ if (!claim.ready) revert ClaimNotReady();
52
+ if (candidate >= 3) revert InvalidCandidate();
53
+ return keccak256(abi.encode(
54
+ keccak256("VRF_D20DAO_CARD_V1"), block.chainid, address(this),
55
+ claimId, claim.requestId, claim.randomness, candidate
56
+ ));
57
+ }
58
+ }
@@ -0,0 +1,23 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.28;
3
+
4
+ import {RandomnessMapping} from "../libraries/RandomnessMapping.sol";
5
+
6
+ interface ID20VRF {
7
+ function requestFee() external view returns (uint256);
8
+ function requestRandomness(bytes32 clientSeed, uint32 callbackGasLimit, address _refundAddress)
9
+ external payable returns (uint256 requestId);
10
+ function requestMappedRandomness(
11
+ bytes32 clientSeed, uint32 callbackGasLimit, address _refundAddress, RandomnessMapping.Spec calldata spec
12
+ ) external payable returns (uint256 requestId);
13
+ function getMappedResult(uint256 requestId) external view returns (uint256[] memory);
14
+ }
15
+
16
+ interface ID20VRFConsumer {
17
+ function rawFulfillRandomness(uint256 requestId, bytes32 randomness) external;
18
+ }
19
+
20
+ /// @notice Optional notification after the RNG fee was refunded or recorded as backed credit.
21
+ interface ID20VRFRefundConsumer {
22
+ function onRefund(uint256 requestId) external;
23
+ }
@@ -0,0 +1,41 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.28;
3
+
4
+ import {ID20VRF} from "../interfaces/ID20VRF.sol";
5
+ import {RandomnessMapping as M} from "./RandomnessMapping.sol";
6
+
7
+ /// @notice Built-in request helpers for game contracts. All requests still callback with (id, rawWord).
8
+ /// @dev Usage: using D20VRFRequests for ID20VRF; rng.d20(options). Inspect getMappedResult(id) after fulfillment.
9
+ library D20VRFRequests {
10
+ struct Options { bytes32 clientSeed; uint32 callbackGasLimit; address refundAddress; }
11
+ function diceRoll(ID20VRF rng, uint256 sides, uint32 count, Options memory o) internal returns (uint256) {
12
+ return _send(rng, M.Spec(M.Operation.DiceRoll, 0, sides, count, 0), o);
13
+ }
14
+ function dN(ID20VRF rng, uint256 sides, Options memory o) internal returns (uint256) {
15
+ return diceRoll(rng, sides, 1, o);
16
+ }
17
+ function d20(ID20VRF rng, Options memory o) internal returns (uint256) { return dN(rng, 20, o); }
18
+ function d12(ID20VRF rng, Options memory o) internal returns (uint256) { return dN(rng, 12, o); }
19
+ function d10(ID20VRF rng, Options memory o) internal returns (uint256) { return dN(rng, 10, o); }
20
+ function d8(ID20VRF rng, Options memory o) internal returns (uint256) { return dN(rng, 8, o); }
21
+ function d6(ID20VRF rng, Options memory o) internal returns (uint256) { return dN(rng, 6, o); }
22
+ function d4(ID20VRF rng, Options memory o) internal returns (uint256) { return dN(rng, 4, o); }
23
+ function coinFlip(ID20VRF rng, Options memory o) internal returns (uint256) {
24
+ return _send(rng, M.Spec(M.Operation.CoinFlip, 0, 0, 1, 0), o);
25
+ }
26
+ function numberRange(ID20VRF rng, uint256 min, uint256 max, Options memory o) internal returns (uint256) {
27
+ return _send(rng, M.Spec(M.Operation.NumberRange, min, max, 1, 0), o);
28
+ }
29
+ function chooseOne(ID20VRF rng, uint32 population, Options memory o) internal returns (uint256) {
30
+ return _send(rng, M.Spec(M.Operation.ChooseOne, 0, 0, 1, population), o);
31
+ }
32
+ function chooseMany(ID20VRF rng, uint32 population, uint32 count, Options memory o) internal returns (uint256) {
33
+ return _send(rng, M.Spec(M.Operation.ChooseMany, 0, 0, count, population), o);
34
+ }
35
+ function shuffle(ID20VRF rng, uint32 population, Options memory o) internal returns (uint256) {
36
+ return _send(rng, M.Spec(M.Operation.Shuffle, 0, 0, population, population), o);
37
+ }
38
+ function _send(ID20VRF rng, M.Spec memory spec, Options memory o) private returns (uint256) {
39
+ return rng.requestMappedRandomness{value: rng.requestFee()}(o.clientSeed, o.callbackGasLimit, o.refundAddress, spec);
40
+ }
41
+ }
@@ -0,0 +1,89 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.28;
3
+
4
+ /// @notice Version 1 deterministic, unbiased range sampling and bounded game operations.
5
+ /// @dev Choice/shuffle outputs are indices. Consumers must lock their item list before requesting.
6
+ library RandomnessMapping {
7
+ enum Operation { Raw, DiceRoll, CoinFlip, NumberRange, ChooseOne, ChooseMany, Shuffle }
8
+ struct Spec {
9
+ Operation operation;
10
+ uint256 lower;
11
+ uint256 upper;
12
+ uint32 count;
13
+ uint32 population;
14
+ }
15
+ struct Stream { bytes32 randomness; uint256 cursor; }
16
+ bytes32 internal constant MAP_DOMAIN = keccak256("D20_MAP");
17
+ uint32 internal constant MAX_ITEMS = 256;
18
+ uint32 internal constant MAX_DICE = 128;
19
+ error InvalidMapping();
20
+
21
+ function validate(Spec memory s) internal pure {
22
+ if (s.operation == Operation.Raw) {
23
+ if (s.lower != 0 || s.upper != 0 || s.count != 0 || s.population != 0) revert InvalidMapping();
24
+ } else if (s.operation == Operation.DiceRoll) {
25
+ if (s.lower != 0 || s.upper < 2 || s.count == 0 || s.count > MAX_DICE || s.population != 0)
26
+ revert InvalidMapping();
27
+ } else if (s.operation == Operation.CoinFlip) {
28
+ if (s.lower != 0 || s.upper != 0 || s.count != 1 || s.population != 0) revert InvalidMapping();
29
+ } else if (s.operation == Operation.NumberRange) {
30
+ if (s.lower > s.upper || s.count != 1 || s.population != 0) revert InvalidMapping();
31
+ } else {
32
+ if (s.lower != 0 || s.upper != 0 || s.population == 0 || s.population > MAX_ITEMS)
33
+ revert InvalidMapping();
34
+ if (s.operation == Operation.ChooseOne && s.count != 1) revert InvalidMapping();
35
+ if (s.operation == Operation.ChooseMany && (s.count == 0 || s.count > s.population))
36
+ revert InvalidMapping();
37
+ if (s.operation == Operation.Shuffle && s.count != s.population) revert InvalidMapping();
38
+ }
39
+ }
40
+
41
+ function hash(Spec memory s) internal pure returns (bytes32) {
42
+ return keccak256(abi.encode(s.operation, s.lower, s.upper, s.count, s.population));
43
+ }
44
+
45
+ function map(bytes32 randomness, Spec memory s) internal pure returns (uint256[] memory result) {
46
+ validate(s);
47
+ Stream memory stream = Stream(randomness, 0);
48
+ if (s.operation == Operation.Raw) {
49
+ result = new uint256[](1);
50
+ result[0] = uint256(randomness);
51
+ } else if (s.operation == Operation.DiceRoll) {
52
+ result = new uint256[](s.count);
53
+ for (uint256 i; i < s.count; ++i) result[i] = sample(stream, s.upper) + 1;
54
+ } else if (s.operation == Operation.CoinFlip) {
55
+ result = new uint256[](1);
56
+ result[0] = sample(stream, 2); // 0 = tails, 1 = heads.
57
+ } else if (s.operation == Operation.NumberRange) {
58
+ result = new uint256[](1);
59
+ result[0] = s.lower == 0 && s.upper == type(uint256).max
60
+ ? next(stream) : s.lower + sample(stream, s.upper - s.lower + 1);
61
+ } else if (s.operation == Operation.ChooseOne) {
62
+ result = new uint256[](1);
63
+ result[0] = sample(stream, s.population);
64
+ } else {
65
+ // Partial Fisher-Yates: without-replacement choices, or a full permutation.
66
+ uint256[] memory pool = new uint256[](s.population);
67
+ for (uint256 i; i < s.population; ++i) pool[i] = i;
68
+ result = new uint256[](s.count);
69
+ for (uint256 i; i < s.count; ++i) {
70
+ uint256 j = i + sample(stream, s.population - i);
71
+ (pool[i], pool[j]) = (pool[j], pool[i]);
72
+ result[i] = pool[i];
73
+ }
74
+ }
75
+ }
76
+
77
+ function next(Stream memory stream) internal pure returns (uint256) {
78
+ return uint256(keccak256(abi.encode(MAP_DOMAIN, stream.randomness, stream.cursor++)));
79
+ }
80
+
81
+ /// @dev Reject the short residue prefix instead of introducing modulo bias.
82
+ function sample(Stream memory stream, uint256 bound) internal pure returns (uint256) {
83
+ uint256 threshold;
84
+ unchecked { threshold = (0 - bound) % bound; }
85
+ uint256 word;
86
+ do { word = next(stream); } while (word < threshold);
87
+ return word % bound;
88
+ }
89
+ }