Create a prediction dApp that uses real-time sports data to trigger automatic payouts using CCIP, Functions, and Automation.

Chainlink in action

Rugby predictions demo

See how Chainlink CCIP, Functions, and Automation combine to power a prediction dApp that uses real-time sports data to trigger automatic payouts.

Demo
Architecture
Code
Tip: Use the toggle to see the different layers of the demo
Autoplay video here

pragma solidity ^0.8.7;

import {ResultsConsumer} from "./ResultsConsumer.sol";
import {NativeTokenSender} from "./ccip/NativeTokenSender.sol";
import {AutomationCompatibleInterface} from "@chainlink/contracts/src/v0.8/AutomationCompatible.sol";

struct Config {
  address oracle;
  address ccipRouter;
  address link;
  address weth9Token;
  address exchangeToken;
  address uniswapV3Router;
  uint64 subscriptionId;
  uint64 destinationChainSelector;
  uint32 gasLimit;
  bytes secrets;
  string source;
}

contract SportsPredictionGame is ResultsConsumer, NativeTokenSender, AutomationCompatibleInterface {
  uint256 private constant MIN_WAGER = 0.00001 ether;
  uint256 private constant MAX_WAGER = 0.01 ether;
  uint256 private constant GAME_RESOLVE_DELAY = 2 hours;

  mapping(uint256 => Game) private games;
  mapping(address => mapping(uint256 => Prediction[])) private predictions;

  mapping(uint256 => bytes32) private pendingRequests;

  uint256[] private activeGames;
  uint256[] private resolvedGames;

  struct Game {
    uint256 sportId;
    uint256 externalId;
    uint256 timestamp;
    uint256 homeWagerAmount;
    uint256 awayWagerAmount;
    bool resolved;
    Result result;
  }

  struct Prediction {
    uint256 gameId;
    Result result;
    uint256 amount;
    bool claimed;
  }

  enum Result {
    None,
    Home,
    Away 
  }

  event GameRegistered(uint256 indexed gameId);
  event GameResolved(uint256 indexed gameId, Result result);
  event Predicted(address indexed user, uint256 indexed gameId, Result result, uint256 amount);
  event Claimed(address indexed user, uint256 indexed gameId, uint256 amount);

  error GameAlreadyRegistered();
  error TimestampInPast();
  error GameNotRegistered();
  error GameIsResolved();
  error GameAlreadyStarted();
  error InsufficientValue();
  error ValueTooHigh();
  error InvalidResult();
  error GameNotResolved();
  error GameNotReadyToResolve();
  error ResolveAlreadyRequested();
  error NothingToClaim();

  constructor(
    Config memory config
  )
    ResultsConsumer(config.oracle, config.subscriptionId, config.source, config.secrets, config.gasLimit)
    NativeTokenSender(
      config.ccipRouter,
      config.link,
      config.weth9Token,
      config.exchangeToken,
      config.uniswapV3Router,
      config.destinationChainSelector
    )
  {}

  function predict(uint256 gameId, Result result) public payable {
    Game memory game = games[gameId];
    uint256 wagerAmount = msg.value;

    if (game.externalId == 0) revert GameNotRegistered();
    if (game.resolved) revert GameIsResolved();
    if (game.timestamp < block.timestamp) revert GameAlreadyStarted();
    if (wagerAmount < MIN_WAGER) revert InsufficientValue();
    if (wagerAmount > MAX_WAGER) revert ValueTooHigh();

    if (result == Result.Home) games[gameId].homeWagerAmount += wagerAmount;
    else if (result == Result.Away) games[gameId].awayWagerAmount += wagerAmount;
    else revert InvalidResult();

    predictions[msg.sender][gameId].push(Prediction(gameId, result, wagerAmount, false));
    emit Predicted(msg.sender, gameId, result, wagerAmount);
  }

  function registerAndPredict(uint256 sportId, uint256 externalId, uint256 timestamp, Result result) external payable {
    uint256 gameId = _registerGame(sportId, externalId, timestamp);
    predict(gameId, result);
  }

  function claim(uint256 gameId, bool transfer) external {
    Game memory game = games[gameId];
    address user = msg.sender;

    if (!game.resolved) revert GameNotResolved();

    uint256 totalWinnings = 0;
    Prediction[] memory userPredictions = predictions[user][gameId];
    for (uint256 i = 0; i < userPredictions.length; i++) {
      Prediction memory prediction = userPredictions[i];
      if (prediction.claimed) continue;
      if (game.result == Result.None) {
        totalWinnings += prediction.amount;
      } else if (prediction.result == game.result) {
        uint256 winnings = calculateWinnings(gameId, prediction.amount, prediction.result);
        totalWinnings += winnings;
      }
      predictions[user][gameId][i].claimed = true;
    }

    if (totalWinnings == 0) revert NothingToClaim();

    if (transfer) {
      _sendTransferRequest(user, totalWinnings);
    } else {
      payable(user).transfer(totalWinnings);
    }

    emit Claimed(user, gameId, totalWinnings);
  }

  function _registerGame(uint256 sportId, uint256 externalId, uint256 timestamp) internal returns (uint256 gameId) {
    gameId = getGameId(sportId, externalId);

    if (games[gameId].externalId != 0) revert GameAlreadyRegistered();
    if (timestamp < block.timestamp) revert TimestampInPast();

    games[gameId] = Game(sportId, externalId, timestamp, 0, 0, false, Result.None);
    activeGames.push(gameId);

    emit GameRegistered(gameId);
  }

  function _requestResolve(uint256 gameId) internal {
    Game memory game = games[gameId];

    if (pendingRequests[gameId] != 0) revert ResolveAlreadyRequested();
    if (game.externalId == 0) revert GameNotRegistered();
    if (game.resolved) revert GameIsResolved();
    if (!readyToResolve(gameId)) revert GameNotReadyToResolve();

    pendingRequests[gameId] = _requestResult(game.sportId, game.externalId);
  }

  function _processResult(uint256 sportId, uint256 externalId, bytes memory response) internal override {
    uint256 gameId = getGameId(sportId, externalId);
    Result result = Result(uint256(bytes32(response)));
    _resolveGame(gameId, result);
  }

  function _resolveGame(uint256 gameId, Result result) internal {
    games[gameId].result = result;
    games[gameId].resolved = true;

    resolvedGames.push(gameId);
    _removeFromActiveGames(gameId);

    emit GameResolved(gameId, result);
  }

  function _removeFromActiveGames(uint256 gameId) internal {
    uint256 index;
    for (uint256 i = 0; i < activeGames.length; i++) {
      if (activeGames[i] == gameId) {
        index = i;
        break;
      }
    }
    for (uint256 i = index; i < activeGames.length - 1; i++) {
      activeGames[i] = activeGames[i + 1];
    }
    activeGames.pop();
  }

  function getGameId(uint256 sportId, uint256 externalId) public pure returns (uint256) {
    return (sportId << 128) | externalId;
  }

  function getGame(uint256 gameId) external view returns (Game memory) {
    return games[gameId];
  }

  function getActiveGames() public view returns (Game[] memory) {
    Game[] memory activeGamesArray = new Game[](activeGames.length);
    for (uint256 i = 0; i < activeGames.length; i++) {
      activeGamesArray[i] = games[activeGames[i]];
    }
    return activeGamesArray;
  }

  function getActivePredictions(address user) external view returns (Prediction[] memory) {
    uint256 totalPredictions = 0;
    for (uint256 i = 0; i < activeGames.length; i++) {
      totalPredictions += predictions[user][activeGames[i]].length;
    }
    uint256 index = 0;
    Prediction[] memory userPredictions = new Prediction[](totalPredictions);
    for (uint256 i = 0; i < activeGames.length; i++) {
      Prediction[] memory gamePredictions = predictions[user][activeGames[i]];
      for (uint256 j = 0; j < gamePredictions.length; j++) {
        userPredictions[index] = gamePredictions[j];
        index++;
      }
    }
    return userPredictions;
  }

  function getPastPredictions(address user) external view returns (Prediction[] memory) {
    uint256 totalPredictions = 0;
    for (uint256 i = 0; i < resolvedGames.length; i++) {
      totalPredictions += predictions[user][resolvedGames[i]].length;
    }
    uint256 index = 0;
    Prediction[] memory userPredictions = new Prediction[](totalPredictions);
    for (uint256 i = 0; i < resolvedGames.length; i++) {
      Prediction[] memory gamePredictions = predictions[user][resolvedGames[i]];
      for (uint256 j = 0; j < gamePredictions.length; j++) {
        userPredictions[index] = gamePredictions[j];
        index++;
      }
    }
    return userPredictions;
  }

  function isPredictionCorrect(address user, uint256 gameId, uint32 predictionIdx) external view returns (bool) {
    Game memory game = games[gameId];
    if (!game.resolved) return false;
    Prediction memory prediction = predictions[user][gameId][predictionIdx];
    return prediction.result == game.result;
  }

  function calculateWinnings(uint256 gameId, uint256 wager, Result result) public view returns (uint256) {
    Game memory game = games[gameId];
    uint256 totalWager = game.homeWagerAmount + game.awayWagerAmount;
    uint256 winnings = (wager * totalWager) / (result == Result.Home ? game.homeWagerAmount : game.awayWagerAmount);
    return winnings;
  }

  function readyToResolve(uint256 gameId) public view returns (bool) {
    return games[gameId].timestamp + GAME_RESOLVE_DELAY < block.timestamp;
  }

  function checkUpkeep(bytes memory) public view override returns (bool, bytes memory) {
    Game[] memory activeGamesArray = getActiveGames();
    for (uint256 i = 0; i < activeGamesArray.length; i++) {
      uint256 gameId = getGameId(activeGamesArray[i].sportId, activeGamesArray[i].externalId);
      if (readyToResolve(gameId) && pendingRequests[gameId] == 0) {
        return (true, abi.encodePacked(gameId));
      }
    }
    return (false, "");
  }

  function performUpkeep(bytes calldata data) external override {
    uint256 gameId = abi.decode(data, (uint256));
    _requestResolve(gameId);
  }

  function deletePendingRequest(uint256 gameId) external onlyOwner {
    delete pendingRequests[gameId];
  }
}

Use Cases

DeFi

DeFi

NFTs

NFTs

dApps

dApps

Real Estate

Real Estate

Climate Change

Climate Change

Gaming

Gaming

Smart Contracts

Smart Contracts

Insurance

Insurance

Upcoming Events

0
See All
No items found.
No items found.

Get the latest Chainlink content straight to your inbox.