Provably fair

How a result is settled, and how to check it

Every MiniMatch competition is settled by one contract on Base. This page describes exactly what that contract does. Every finished competition has a Verify this competition block on its results page that runs these checks in your browser, reading Base directly.

1The ball is committed before the competition opens

When a competition opens, the contract stores a commitment: a hash of the ball's position and a secret number (the salt).

commitment = keccak256(abi.encodePacked(
  uint256 salt, uint256 ballX, uint256 ballY, uint256 ballRadius))

Positions are whole numbers, in hundred-millionths (1/100,000,000) of the picture's width, measured from its top-left corner. ballRadius is the radius of the disc the winning point is drawn in.

To close the competition, the salt and the ball are sent to the contract (closeCompetition). The contract recomputes the hash and refuses to close if it differs from the commitment, so the ball revealed at the close is the ball committed at the opening.

The commitment covers the ball's position, not the picture. The picture with the ball is published at the close, and the results page draws the revealed ball on it.

2The winning point is drawn by Chainlink VRF

When the competition closes, the contract asks Chainlink VRF for one random number. Chainlink's coordinator contract checks the number's cryptographic proof Onchain and delivers it; MiniMatch's contract accepts it only from that coordinator, stores it, and records it in a RandomnessReceived event.

The contract then turns the number into a point inside the revealed disc (centre x, y, radius r):

for i = 0 … 7:
  h  = keccak256(abi.encodePacked(randomWord, i))
  dx = (h mod 2^128) mod (2r + 1) − r
  dy = (h >> 128)    mod (2r + 1) − r
  if dx² + dy² ≤ r²:  point = (x + dx, y + dy), stop
if no draw landed inside (about 5 times in a million): point = (x, y)

The point is stored by the contract (resolvedPointX, resolvedPointY) and anyone can read it.

3The closest ticket wins

Every ticket's guess is stored by the contract, in the same units as the ball. A ticket's distance to the point is its squared distance:

distance² = (guessX − pointX)² + (guessY − pointY)²

The smallest distance wins. If two tickets are exactly as close, the one with the higher keccak256(abi.encodePacked(randomWord, ticketIndex)) wins.

How the winner is submitted. To keep the cost of a transaction bounded, the contract does not scan every ticket itself. MiniMatch's agent computes the distances from the stored point and submits a shortlist of up to 20 tickets; the contract picks the closest of those, with the tie-break above. The contract therefore guarantees that the winner is the closest ticket of the shortlist. That the shortlist contained the closest ticket of all is what step 3 of the Verify block checks: it reads every ticket from the contract, computes every distance and confirms that the closest of all of them is the winner.

4The prize is paid by the contract

The same transaction that records the winner (submitWinner) pays the prize in USDC to the winner's wallet and the ticket revenue to MiniMatch's treasury, and emits a WinnerResolved event with the winner and the winning distance.

If no winner has been paid 48 hours after the close, anyone can call emergencyRefund on the contract, and every ticket can then be refunded.

Check it yourself

Open any finished competition on Winners and expand Verify this competition. Or read the contract yourself: