Docs/Contracts/Full Vrf

Full VRF Contract

Verifiable randomness on every tick with AutoLoopVRFCompatible — provably fair randomness for dice games, lotteries, and more.


Overview

AutoLoopVRFCompatible provides ECVRF (Elliptic Curve Verifiable Random Function) proof generation and on-chain verification on every tick. The worker generates a cryptographic proof off-chain, and the contract verifies it on-chain before exposing the random value.

Best for: Dice games, lotteries, coin flips, any game that needs provably fair randomness on every single tick.

How VRF Works

  1. Your contract returns shouldProgressLoop() => true with the current _loopID
  2. The worker detects VRF support via ERC-165 (supportsInterface)
  3. The worker generates an ECVRF proof off-chain using its private key and a deterministic seed
  4. The proof is wrapped around your game data and submitted to progressLoop()
  5. VRFVerifier.sol verifies the proof on-chain and outputs a bytes32 random value

The seed is deterministic: keccak256(contractAddress, loopID). Controllers cannot choose or manipulate seeds.

Integration

import "@luckymachines/autoloop/src/AutoLoopVRFCompatible.sol";

contract MyVRFGame is AutoLoopVRFCompatible {
    uint256 public lastRoll;
    uint256 public interval;
    uint256 public lastTimeStamp;
    uint256 private _loopID;

    constructor(uint256 _interval) {
        interval = _interval;
        lastTimeStamp = block.timestamp;
    }

    function shouldProgressLoop()
        external view override
        returns (bool loopIsReady, bytes memory progressWithData)
    {
        loopIsReady = (block.timestamp - lastTimeStamp) > interval;
        progressWithData = abi.encode(_loopID);
    }

    function progressLoop(bytes calldata progressWithData) external override {
        (bytes32 randomness, bytes memory gameData) =
            _verifyAndExtractRandomness(progressWithData, tx.origin);

        uint256 loopID = abi.decode(gameData, (uint256));
        require(loopID == _loopID, "stale");

        lastRoll = (uint256(randomness) % 6) + 1;
        lastTimeStamp = block.timestamp;
        ++_loopID;
    }
}

Register Controller Key

Before a VRF contract accepts proofs from a controller, the controller's public key must be registered:

myVRFGame.registerControllerKey(controllerAddress, pkX, pkY);

Only the controller itself or a contract admin can call this.

VRF Envelope Format

The worker wraps the original progressWithData in a VRF envelope:

abi.encode(
    uint8 vrfVersion,       // 1 = ECVRF-SECP256K1-SHA256-TAI
    uint256[4] proof,       // [gamma_x, gamma_y, c, s]
    uint256[2] uPoint,      // precomputed for fastVerify
    uint256[4] vComponents, // precomputed for fastVerify
    bytes gameData          // original progressWithData
)

Key Functions

_verifyAndExtractRandomness(bytes calldata, address controller)

Verifies the VRF proof and extracts the random value. Returns (bytes32 randomness, bytes memory gameData).

registerControllerKey(address, uint256 pkX, uint256 pkY)

Register a controller's secp256k1 public key. Required before the controller can submit VRF proofs.

computeSeed(uint256 loopID) → bytes

Computes the deterministic seed: keccak256(address(this), loopID).

Gas Cost

MetricValue
Median gas per tick~240,000
Cost per tick (2026)~$0.022
10-min game @ 1 tick/sec~$13.38

VRF adds ~150k gas overhead for on-chain elliptic curve operations. If you don't need randomness on every tick, consider Hybrid VRF to cut costs by ~56%.