Blockchain / Global / Address
Contract
0x881419C59a5fA95ab5381EfABDc8a9d6f9269F6C
- Balance
- 0 SOMI
- Transactions sent
- 1
- Type
- Contract · 6,248 bytes of code
SpinWheel Verified
- Compiler
- solc 0.8.30+commit.73712a01
- Language
- Solidity
- Optimization
- Yes, 200 runs
- EVM version
- shanghai
- License
- —
- Verified
- 2026-10-12
examples/SpinWheel.sol
and 3 more files
Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
import {VRFConsumerBaseV2Plus} from "../src/VRFConsumerBaseV2Plus.sol";
import {ISomniaVRFCoordinator} from "../src/interfaces/ISomniaVRFCoordinator.sol";
/// @title SpinWheel
/// @notice Permissionless spin-the-wheel registry backed by Somnia VRF.
/// Anyone creates a wheel (labels + weights, all on-chain). Anyone
/// spins it. The coordinator delivers the random word through
/// `fulfillRandomWords`, the winning segment is picked by weighted
/// draw and stored next to the raw word so the result is replayable.
///
/// One consumer contract serves every wheel, so "deploying a wheel" is
/// a single cheap `createWheel` transaction rather than a bytecode
/// deploy (Somnia charges 3125 gas per deployed byte).
contract SpinWheel is VRFConsumerBaseV2Plus {
// ─── Bounds ───────────────────────────────────────────────────────────────
uint8 public constant MIN_SEGMENTS = 2;
uint8 public constant MAX_SEGMENTS = 24;
uint16 public constant MAX_WEIGHT = 10_000;
uint256 public constant MAX_NAME_BYTES = 48;
uint256 public constant MAX_LABEL_BYTES = 32;
uint32 public constant CALLBACK_GAS_LIMIT = 800_000;
uint16 public constant COMMIT_DELAY_BLOCKS = 16;
// ─── Storage ──────────────────────────────────────────────────────────────
struct Wheel {
address creator;
uint64 createdAt;
uint32 spins; // spins requested (landed or pending)
uint32 totalWeight;
string name;
string[] labels;
uint16[] weights;
}
struct Spin {
uint64 wheelId;
address spinner;
bool verifiable;
bool landed;
uint8 segment;
uint256 word;
}
Wheel[] internal _wheels;
mapping(uint256 => Spin) internal _spins; // requestId → spin
mapping(uint256 => uint256[]) internal _spinsOf; // wheelId → requestIds
// ─── Events ───────────────────────────────────────────────────────────────
event WheelCreated(
uint256 indexed wheelId,
address indexed creator,
string name,
string[] labels,
uint16[] weights
);
event SpinRequested(
uint256 indexed wheelId,
uint256 indexed requestId,
address indexed spinner,
bool verifiable
);
event Landed(
uint256 indexed wheelId,
uint256 indexed requestId,
address indexed spinner,
uint8 segment,
string label,
uint256 word
);
// ─── Errors ───────────────────────────────────────────────────────────────
error BadSegmentCount(uint256 provided);
error LengthMismatch();
error NameTooLong();
error LabelTooLong(uint256 index);
error EmptyLabel(uint256 index);
error BadWeight(uint256 index);
error UnknownWheel(uint256 wheelId);
error UnknownSpin(uint256 requestId);
error AlreadyLanded(uint256 requestId);
constructor(address coordinator) VRFConsumerBaseV2Plus(coordinator) {}
// ─── Create ───────────────────────────────────────────────────────────────
/// @notice Create a wheel. Labels and weights are stored on-chain so any
/// observer can replay a spin from its word.
function createWheel(string calldata name, string[] calldata labels, uint16[] calldata weights)
external
returns (uint256 wheelId)
{
uint256 n = labels.length;
if (n < MIN_SEGMENTS || n > MAX_SEGMENTS) revert BadSegmentCount(n);
if (weights.length != n) revert LengthMismatch();
if (bytes(name).length > MAX_NAME_BYTES) revert NameTooLong();
uint32 total;
for (uint256 i = 0; i < n; i++) {
uint256 lb = bytes(labels[i]).length;
if (lb == 0) revert EmptyLabel(i);
if (lb > MAX_LABEL_BYTES) revert LabelTooLong(i);
if (weights[i] == 0 || weights[i] > MAX_WEIGHT) revert BadWeight(i);
total += weights[i];
}
wheelId = _wheels.length;
Wheel storage w = _wheels.push();
w.creator = msg.sender;
w.createdAt = uint64(block.timestamp);
w.totalWeight = total;
w.name = name;
for (uint256 i = 0; i < n; i++) {
w.labels.push(labels[i]);
w.weights.push(weights[i]);
}
emit WheelCreated(wheelId, msg.sender, name, labels, weights);
}
// ─── Spin ─────────────────────────────────────────────────────────────────
/// @notice Request a spin. The consumer pays only tx gas; the coordinator
/// pays for entropy delivery. On mainnet the coordinator rejects
/// `useVerifiableEntropy = false`.
function spin(uint256 wheelId, bool useVerifiableEntropy) external returns (uint256 requestId) {
if (wheelId >= _wheels.length) revert UnknownWheel(wheelId);
requestId = COORDINATOR.requestRandomWords(
ISomniaVRFCoordinator.RandomWordsRequest({
callbackGasLimit: CALLBACK_GAS_LIMIT,
commitDelayBlocks: COMMIT_DELAY_BLOCKS,
numWords: 1,
useVerifiableEntropy: useVerifiableEntropy
})
);
_spins[requestId] = Spin({
wheelId: uint64(wheelId),
spinner: msg.sender,
verifiable: useVerifiableEntropy,
landed: false,
segment: 0,
word: 0
});
_spinsOf[wheelId].push(requestId);
_wheels[wheelId].spins += 1;
emit SpinRequested(wheelId, requestId, msg.sender, useVerifiableEntropy);
}
function fulfillRandomWords(uint256 requestId, uint256[] memory words) internal override {
Spin storage s = _spins[requestId];
if (s.spinner == address(0)) revert UnknownSpin(requestId);
if (s.landed) revert AlreadyLanded(requestId);
Wheel storage w = _wheels[s.wheelId];
uint8 seg = pickSegment(words[0], w.weights, w.totalWeight);
s.landed = true;
s.segment = seg;
s.word = words[0];
emit Landed(s.wheelId, requestId, s.spinner, seg, w.labels[seg], words[0]);
}
/// @notice Weighted draw. Pure so anyone can replay a result off-chain.
function pickSegment(uint256 word, uint16[] memory weights, uint32 totalWeight)
public
pure
returns (uint8)
{
uint256 r = word % totalWeight;
uint256 acc;
for (uint256 i = 0; i < weights.length; i++) {
acc += weights[i];
if (r < acc) return uint8(i);
}
return uint8(weights.length - 1); // unreachable when weights sum to totalWeight
}
// ─── Views ────────────────────────────────────────────────────────────────
function wheelCount() external view returns (uint256) {
return _wheels.length;
}
function getWheel(uint256 wheelId)
external
view
returns (
address creator,
uint64 createdAt,
uint32 spins,
uint32 totalWeight,
string memory name,
string[] memory labels,
uint16[] memory weights
)
{
if (wheelId >= _wheels.length) revert UnknownWheel(wheelId);
Wheel storage w = _wheels[wheelId];
return (w.creator, w.createdAt, w.spins, w.totalWeight, w.name, w.labels, w.weights);
}
function getSpin(uint256 requestId) external view returns (Spin memory) {
return _spins[requestId];
}
function spinsOf(uint256 wheelId) external view returns (uint256[] memory) {
return _spinsOf[wheelId];
}
}
Other files
src/VRFConsumerBaseV2Plus.sol
// SPDX-License-Identifier: MIT pragma solidity ^0.8.30; import {ISomniaVRFCoordinator} from "./interfaces/ISomniaVRFCoordinator.sol"; import {IVRFConsumer} from "./interfaces/IVRFConsumer.sol"; /// @title VRFConsumerBaseV2Plus /// @notice Base contract for VRF consumers on Somnia. Mirrors the shape of /// Chainlink's `VRFConsumerBaseV2Plus` so existing consumer code can /// migrate by changing imports. /// /// @dev Consumers implement `fulfillRandomWords`. The coordinator calls /// `rawFulfillRandomWords`, which access-checks and forwards. abstract contract VRFConsumerBaseV2Plus is IVRFConsumer { error OnlyCoordinatorCanFulfill(address expected, address actual); ISomniaVRFCoordinator public immutable COORDINATOR; constructor(address coordinator) { COORDINATOR = ISomniaVRFCoordinator(coordinator); } /// @notice Called by the coordinator when random words are ready. function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal virtual; /// @inheritdoc IVRFConsumer function rawFulfillRandomWords(uint256 requestId, uint256[] calldata randomWords) external override { if (msg.sender != address(COORDINATOR)) { revert OnlyCoordinatorCanFulfill(address(COORDINATOR), msg.sender); } fulfillRandomWords(requestId, randomWords); } }src/interfaces/ISomniaVRFCoordinator.sol
// SPDX-License-Identifier: MIT pragma solidity ^0.8.30; /// @title ISomniaVRFCoordinator /// @notice Reactivity-native VRF coordinator for Somnia. Consumers request /// random words; the coordinator delivers them via a callback after /// a commit delay. /// /// Two entropy modes: /// /// Mode A — fast (`useVerifiableEntropy = false`) /// Entropy from `blockhash(revealBlock - 1)`. Cheap and low-latency, /// but the block producer for the reveal block can influence it. /// Testnet-grade only. /// /// Mode B — verifiable (`useVerifiableEntropy = true`) /// Entropy mixes drand (League of Entropy publicly-verifiable /// beacon) with the blockhash. The coordinator fires a Somnia /// Agents JSON API Request at reveal time; the resulting seed is /// `keccak256(drand, blockhash, requestId, consumer, revealBlock)`. /// Compromising either drand or the reveal-block producer alone /// does not compromise the result. interface ISomniaVRFCoordinator { struct RandomWordsRequest { uint32 callbackGasLimit; uint16 commitDelayBlocks; uint32 numWords; bool useVerifiableEntropy; } struct Request { address consumer; uint32 callbackGasLimit; uint32 numWords; uint64 revealBlock; bool useVerifiableEntropy; bool fulfilled; bytes32 capturedBlockhash; } event RandomWordsRequested( uint256 indexed requestId, address indexed consumer, uint64 revealBlock, uint32 numWords, uint32 callbackGasLimit, bool useVerifiableEntropy ); event BlockhashCaptured( uint256 indexed requestId, uint64 revealBlock, bytes32 blockhashValue ); event DrandRequested( uint256 indexed requestId, uint256 indexed agentRequestId ); event RandomWordsFulfilled( uint256 indexed requestId, address indexed consumer, uint64 revealBlock, bool callbackSuccess, bool usedVerifiableEntropy ); error CommitDelayTooShort(uint16 provided, uint16 minimum); error CommitDelayTooLong(uint16 provided, uint16 maximum); error CallbackGasTooHigh(uint32 provided, uint32 maximum); error NumWordsOutOfRange(uint32 provided, uint32 minimum, uint32 maximum); error UnknownRequest(uint256 requestId); error UnknownAgentRequest(uint256 agentRequestId); error NotPrecompile(address caller); error NotAgentPlatform(address caller); error DrandAlreadyResolved(uint256 requestId); error DrandResponseFailed(uint8 status); /// @notice Request `numWords` uniformly random uint256 values, delivered via /// the caller's `fulfillRandomWords(requestId, uint256[] words)` callback /// after `commitDelayBlocks` blocks (Mode A) or after the drand round /// following that block resolves (Mode B). function requestRandomWords(RandomWordsRequest calldata req) external returns (uint256 requestId); /// @notice Read a pending or historical request. function getRequest(uint256 requestId) external view returns (Request memory); }src/interfaces/IVRFConsumer.sol
// SPDX-License-Identifier: MIT pragma solidity ^0.8.30; /// @title IVRFConsumer /// @notice Callback interface a VRF consumer must implement. /// Signature mirrors Chainlink's VRFConsumerBaseV2Plus for portability. interface IVRFConsumer { function rawFulfillRandomWords(uint256 requestId, uint256[] calldata randomWords) external; }
ABI · 30 entries
json
[
{
"type": "constructor",
"inputs": [
{
"name": "coordinator",
"type": "address",
"internalType": "address"
}
],
"stateMutability": "nonpayable"
},
{
"name": "AlreadyLanded",
"type": "error",
"inputs": [
{
"name": "requestId",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"name": "BadSegmentCount",
"type": "error",
"inputs": [
{
"name": "provided",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"name": "BadWeight",
"type": "error",
"inputs": [
{
"name": "index",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"name": "EmptyLabel",
"type": "error",
"inputs": [
{
"name": "index",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"name": "LabelTooLong",
"type": "error",
"inputs": [
{
"name": "index",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"name": "LengthMismatch",
"type": "error",
"inputs": []
},
{
"name": "NameTooLong",
"type": "error",
"inputs": []
},
{
"name": "OnlyCoordinatorCanFulfill",
"type": "error",
"inputs": [
{
"name": "expected",
"type": "address",
"internalType": "address"
},
{
"name": "actual",
"type": "address",
"internalType": "address"
}
]
},
{
"name": "UnknownSpin",
"type": "error",
"inputs": [
{
"name": "requestId",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"name": "UnknownWheel",
"type": "error",
"inputs": [
{
"name": "wheelId",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"name": "Landed",
"type": "event",
"inputs": [
{
"name": "wheelId",
"type": "uint256",
"indexed": true,
"internalType": "uint256"
},
{
"name": "requestId",
"type": "uint256",
"indexed": true,
"internalType": "uint256"
},
{
"name": "spinner",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "segment",
"type": "uint8",
"indexed": false,
"internalType": "uint8"
},
{
"name": "label",
"type": "string",
"indexed": false,
"internalType": "string"
},
{
"name": "word",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
}
],
"anonymous": false
},
{
"name": "SpinRequested",
"type": "event",
"inputs": [
{
"name": "wheelId",
"type": "uint256",
"indexed": true,
"internalType": "uint256"
},
{
"name": "requestId",
"type": "uint256",
"indexed": true,
"internalType": "uint256"
},
{
"name": "spinner",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "verifiable",
"type": "bool",
"indexed": false,
"internalType": "bool"
}
],
"anonymous": false
},
{
"name": "WheelCreated",
"type": "event",
"inputs": [
{
"name": "wheelId",
"type": "uint256",
"indexed": true,
"internalType": "uint256"
},
{
"name": "creator",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "name",
"type": "string",
"indexed": false,
"internalType": "string"
},
{
"name": "labels",
"type": "string[]",
"indexed": false,
"internalType": "string[]"
},
{
"name": "weights",
"type": "uint16[]",
"indexed": false,
"internalType": "uint16[]"
}
],
"anonymous": false
},
{
"name": "CALLBACK_GAS_LIMIT",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint32",
"internalType": "uint32"
}
],
"stateMutability": "view"
},
{
"name": "COMMIT_DELAY_BLOCKS",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint16",
"internalType": "uint16"
}
],
"stateMutability": "view"
},
{
"name": "COORDINATOR",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "contract ISomniaVRFCoordinator"
}
],
"stateMutability": "view"
},
{
"name": "MAX_LABEL_BYTES",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"name": "MAX_NAME_BYTES",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"name": "MAX_SEGMENTS",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint8",
"internalType": "uint8"
}
],
"stateMutability": "view"
},
{
"name": "MAX_WEIGHT",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint16",
"internalType": "uint16"
}
],
"stateMutability": "view"
},
{
"name": "MIN_SEGMENTS",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint8",
"internalType": "uint8"
}
],
"stateMutability": "view"
},
{
"name": "createWheel",
"type": "function",
"inputs": [
{
"name": "name",
"type": "string",
"internalType": "string"
},
{
"name": "labels",
"type": "string[]",
"internalType": "string[]"
},
{
"name": "weights",
"type": "uint16[]",
"internalType": "uint16[]"
}
],
"outputs": [
{
"name": "wheelId",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "nonpayable"
},
{
"name": "getSpin",
"type": "function",
"inputs": [
{
"name": "requestId",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "",
"type": "tuple",
"components": [
{
"name": "wheelId",
"type": "uint64",
"internalType": "uint64"
},
{
"name": "spinner",
"type": "address",
"internalType": "address"
},
{
"name": "verifiable",
"type": "bool",
"internalType": "bool"
},
{
"name": "landed",
"type": "bool",
"internalType": "bool"
},
{
"name": "segment",
"type": "uint8",
"internalType": "uint8"
},
{
"name": "word",
"type": "uint256",
"internalType": "uint256"
}
],
"internalType": "struct SpinWheel.Spin"
}
],
"stateMutability": "view"
},
{
"name": "getWheel",
"type": "function",
"inputs": [
{
"name": "wheelId",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "creator",
"type": "address",
"internalType": "address"
},
{
"name": "createdAt",
"type": "uint64",
"internalType": "uint64"
},
{
"name": "spins",
"type": "uint32",
"internalType": "uint32"
},
{
"name": "totalWeight",
"type": "uint32",
"internalType": "uint32"
},
{
"name": "name",
"type": "string",
"internalType": "string"
},
{
"name": "labels",
"type": "string[]",
"internalType": "string[]"
},
{
"name": "weights",
"type": "uint16[]",
"internalType": "uint16[]"
}
],
"stateMutability": "view"
},
{
"name": "pickSegment",
"type": "function",
"inputs": [
{
"name": "word",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "weights",
"type": "uint16[]",
"internalType": "uint16[]"
},
{
"name": "totalWeight",
"type": "uint32",
"internalType": "uint32"
}
],
"outputs": [
{
"name": "",
"type": "uint8",
"internalType": "uint8"
}
],
"stateMutability": "pure"
},
{
"name": "rawFulfillRandomWords",
"type": "function",
"inputs": [
{
"name": "requestId",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "randomWords",
"type": "uint256[]",
"internalType": "uint256[]"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "spin",
"type": "function",
"inputs": [
{
"name": "wheelId",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "useVerifiableEntropy",
"type": "bool",
"internalType": "bool"
}
],
"outputs": [
{
"name": "requestId",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "nonpayable"
},
{
"name": "spinsOf",
"type": "function",
"inputs": [
{
"name": "wheelId",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "",
"type": "uint256[]",
"internalType": "uint256[]"
}
],
"stateMutability": "view"
},
{
"name": "wheelCount",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
}
]