Smart Contract: PredictionMarket.sol
Agora vamos fazer o deploy do smart contract com o qual nosso workflow CRE vai interagir.
Como Funciona
Nosso mercado de previsão suporta quatro ações principais:
┌─────────────────────────────────────────────────────────────────────────┐
│ FLUXO DO MERCADO DE PREVISÃO │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. CRIAR MERCADO │
│ Qualquer pessoa cria um mercado com uma pergunta Sim/Não │
│ Exemplo: "A Argentina venceu a Copa do Mundo de 2022?" │
│ │
│ 2. PREVER │
│ Usuários apostam ETH em Sim ou Não │
│ → Fundos vão para o Pool Sim ou Pool Não │
│ │
│ 3. SOLICITAR LIQUIDAÇÃO │
│ Qualquer pessoa pode solicitar a liquidação │
│ → Emite evento SettlementRequested │
│ → CRE Log Trigger detecta o evento │
│ → CRE consulta o Gemini AI para obter a resposta │
│ → CRE escreve o resultado de volta via onReport() │
│ │
│ 4. RESGATAR GANHOS │
│ Vencedores resgatam sua parte do pool total │
│ → Sua aposta * (Pool Total / Pool Vencedor) │
│ │
└─────────────────────────────────────────────────────────────────────────┘
O Código do Contrato
Crie src/PredictionMarket.sol com o código do contrato mostrado abaixo:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import {ReceiverTemplate} from "./interfaces/ReceiverTemplate.sol";
/// @title PredictionMarket
/// @notice A simplified prediction market for CRE bootcamp.
contract PredictionMarket is ReceiverTemplate {
error MarketDoesNotExist();
error MarketAlreadySettled();
error MarketNotSettled();
error AlreadyPredicted();
error InvalidAmount();
error NothingToClaim();
error AlreadyClaimed();
error TransferFailed();
event MarketCreated(uint256 indexed marketId, string question, address creator);
event PredictionMade(uint256 indexed marketId, address indexed predictor, Prediction prediction, uint256 amount);
event SettlementRequested(uint256 indexed marketId, string question);
event MarketSettled(uint256 indexed marketId, Prediction outcome, uint16 confidence);
event WinningsClaimed(uint256 indexed marketId, address indexed claimer, uint256 amount);
enum Prediction {
Yes,
No
}
struct Market {
address creator;
uint48 createdAt;
uint48 settledAt;
bool settled;
uint16 confidence;
Prediction outcome;
uint256 totalYesPool;
uint256 totalNoPool;
string question;
}
struct UserPrediction {
uint256 amount;
Prediction prediction;
bool claimed;
}
uint256 internal nextMarketId;
mapping(uint256 marketId => Market market) internal markets;
mapping(uint256 marketId => mapping(address user => UserPrediction)) internal predictions;
/// @notice Constructor sets the Chainlink Forwarder address for security
/// @param _forwarderAddress The address of the Chainlink KeystoneForwarder contract
/// @dev For Sepolia testnet, use: 0x15fc6ae953e024d975e77382eeec56a9101f9f88
constructor(address _forwarderAddress) ReceiverTemplate(_forwarderAddress) {}
// ================================================================
// │ Create market │
// ================================================================
/// @notice Create a new prediction market.
/// @param question The question for the market.
/// @return marketId The ID of the newly created market.
function createMarket(string memory question) public returns (uint256 marketId) {
marketId = nextMarketId++;
markets[marketId] = Market({
creator: msg.sender,
createdAt: uint48(block.timestamp),
settledAt: 0,
settled: false,
confidence: 0,
outcome: Prediction.Yes,
totalYesPool: 0,
totalNoPool: 0,
question: question
});
emit MarketCreated(marketId, question, msg.sender);
}
// ================================================================
// │ Predict │
// ================================================================
/// @notice Make a prediction on a market.
/// @param marketId The ID of the market.
/// @param prediction The prediction (Yes or No).
function predict(uint256 marketId, Prediction prediction) external payable {
Market memory m = markets[marketId];
if (m.creator == address(0)) revert MarketDoesNotExist();
if (m.settled) revert MarketAlreadySettled();
if (msg.value == 0) revert InvalidAmount();
UserPrediction memory userPred = predictions[marketId][msg.sender];
if (userPred.amount != 0) revert AlreadyPredicted();
predictions[marketId][msg.sender] = UserPrediction({
amount: msg.value,
prediction: prediction,
claimed: false
});
if (prediction == Prediction.Yes) {
markets[marketId].totalYesPool += msg.value;
} else {
markets[marketId].totalNoPool += msg.value;
}
emit PredictionMade(marketId, msg.sender, prediction, msg.value);
}
// ================================================================
// │ Request settlement │
// ================================================================
/// @notice Request settlement for a market.
/// @dev Emits SettlementRequested event for CRE Log Trigger.
/// @param marketId The ID of the market to settle.
function requestSettlement(uint256 marketId) external {
Market memory m = markets[marketId];
if (m.creator == address(0)) revert MarketDoesNotExist();
if (m.settled) revert MarketAlreadySettled();
emit SettlementRequested(marketId, m.question);
}
// ================================================================
// │ Market settlement by CRE │
// ================================================================
/// @notice Settles a market from a CRE report with AI-determined outcome.
/// @dev Called via onReport → _processReport when prefix byte is 0x01.
/// @param report ABI-encoded (uint256 marketId, Prediction outcome, uint16 confidence)
function _settleMarket(bytes calldata report) internal {
(uint256 marketId, Prediction outcome, uint16 confidence) = abi.decode(
report,
(uint256, Prediction, uint16)
);
Market memory m = markets[marketId];
if (m.creator == address(0)) revert MarketDoesNotExist();
if (m.settled) revert MarketAlreadySettled();
markets[marketId].settled = true;
markets[marketId].confidence = confidence;
markets[marketId].settledAt = uint48(block.timestamp);
markets[marketId].outcome = outcome;
emit MarketSettled(marketId, outcome, confidence);
}
// ================================================================
// │ CRE Entry Point │
// ================================================================
/// @inheritdoc ReceiverTemplate
/// @dev Routes to either market creation or settlement based on prefix byte.
/// - No prefix → Create market (Day 1)
/// - Prefix 0x01 → Settle market (Day 2)
function _processReport(bytes calldata report) internal override {
if (report.length > 0 && report[0] == 0x01) {
_settleMarket(report[1:]);
} else {
string memory question = abi.decode(report, (string));
createMarket(question);
}
}
// ================================================================
// │ Claim winnings │
// ================================================================
/// @notice Claim winnings after market settlement.
/// @param marketId The ID of the market.
function claim(uint256 marketId) external {
Market memory m = markets[marketId];
if (m.creator == address(0)) revert MarketDoesNotExist();
if (!m.settled) revert MarketNotSettled();
UserPrediction memory userPred = predictions[marketId][msg.sender];
if (userPred.amount == 0) revert NothingToClaim();
if (userPred.claimed) revert AlreadyClaimed();
if (userPred.prediction != m.outcome) revert NothingToClaim();
predictions[marketId][msg.sender].claimed = true;
uint256 totalPool = m.totalYesPool + m.totalNoPool;
uint256 winningPool = m.outcome == Prediction.Yes ? m.totalYesPool : m.totalNoPool;
uint256 payout = (userPred.amount * totalPool) / winningPool;
(bool success,) = msg.sender.call{value: payout}("");
if (!success) revert TransferFailed();
emit WinningsClaimed(marketId, msg.sender, payout);
}
// ================================================================
// │ Getters │
// ================================================================
/// @notice Get market details.
/// @param marketId The ID of the market.
function getMarket(uint256 marketId) external view returns (Market memory) {
return markets[marketId];
}
/// @notice Get user's prediction for a market.
/// @param marketId The ID of the market.
/// @param user The user's address.
function getPrediction(uint256 marketId, address user) external view returns (UserPrediction memory) {
return predictions[marketId][user];
}
}
Pontos-Chave de Integração com CRE
1. O Evento SettlementRequested
event SettlementRequested(uint256 indexed marketId, string question);
Este evento é o que o Log Trigger do CRE escuta. Quando emitido, o CRE executa automaticamente o workflow de liquidação.
2. A Função onReport
O contrato base ReceiverTemplate lida com onReport() automaticamente, incluindo verificações de segurança para garantir que apenas o KeystoneForwarder confiável da Chainlink possa chamá-lo. Seu contrato só precisa implementar _processReport() para lidar com os dados decodificados do relatório.
O CRE chama onReport() via o KeystoneForwarder para entregar os resultados de liquidação. O report contém (marketId, outcome, confidence) codificados em ABI.