Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions starter-templates/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ They are more comprehensive than **building-blocks**, and can be adapted into yo
7. **Event Reactor** — [`./event-reactor`](./event-reactor)
Listen for on-chain events (LogTrigger), read contract state, and respond on-chain. Demonstrates event-driven workflows with typed event decoding via generated bindings.

8. **Prediction Market** — [`./prediction-market`](./prediction-market)
Full prediction market lifecycle example with 3 workflows: market creation, resolution using Chainlink BTC/USD Data Feed, and dispute management via LogTrigger.

> Each subdirectory includes its own README with template-specific steps and example logs.

## License
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
kind: starter-template
id: prediction-market-ts
projectDir: .
title: "Prediction Market (Data Feeds)"
description: "Full prediction market lifecycle: create, resolve, and dispute binary markets using Chainlink Data Feeds."
language: typescript
category: workflow
tags:
- prediction-market
- creation
- resolution
- dispute
- cron
- log-trigger
- on-chain-read
- on-chain-write
- data-feeds
workflows:
- dir: market-creation
- dir: market-resolution
- dir: market-dispute
postInit: |
A demo PredictionMarket contract is pre-deployed on Sepolia. See README.md for details.
Run `cd market-creation && bun install && cd ..` for each workflow, then simulate.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.env
.cre_build_tmp.js
416 changes: 416 additions & 0 deletions starter-templates/prediction-market/prediction-market-ts/README.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { parseAbi } from "viem"

export const PredictionMarketAbi = parseAbi([
"function getMarket(uint256 marketId) view returns ((uint256 marketId, string question, uint256 strikePrice, uint256 expirationTime, uint256 disputeDeadline, uint8 status, uint8 outcome, int256 resolutionPrice, uint256 resolvedAt))",
"function getMarketStatus(uint256 marketId) view returns (uint8)",
"function getNextMarketId() view returns (uint256)",
"function isExpired(uint256 marketId) view returns (bool)",
"function isResolvable(uint256 marketId) view returns (bool)",
"function priceFeed() view returns (address)",
"function disputeWindow() view returns (uint256)",
"event MarketCreated(uint256 indexed marketId, string question, uint256 strikePrice, uint256 expirationTime, uint256 disputeDeadline)",
"event MarketResolved(uint256 indexed marketId, uint8 outcome, int256 resolutionPrice, uint256 resolvedAt)",
"event DisputeRaised(uint256 indexed marketId, address indexed disputor, string reason)",
"event DisputeResolved(uint256 indexed marketId, uint8 outcome, int256 newPrice, bool overturned)",
])
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { parseAbi } from "viem"

export const PriceFeedAggregatorAbi = parseAbi([
"function decimals() view returns (uint8)",
"function latestAnswer() view returns (int256)",
"function latestRoundData() view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)",
])
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { PredictionMarketAbi } from "./PredictionMarket"
export { PriceFeedAggregatorAbi } from "./PriceFeedAggregator"
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.4.0) (utils/introspection/IERC165.sol)

pragma solidity >=0.4.16;

/**
* @dev Interface of the ERC-165 standard, as defined in the
* https://eips.ethereum.org/EIPS/eip-165[ERC].
*
* Implementers can declare support of contract interfaces, which can then be
* queried by others ({ERC165Checker}).
*
* For an implementation, see {ERC165}.
*/
interface IERC165 {
/**
* @dev Returns true if this contract implements the interface defined by
* `interfaceId`. See the corresponding
* https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[ERC section]
* to learn more about how these ids are created.
*
* This function call must use less than 30 000 gas.
*/
function supportsInterface(
bytes4 interfaceId
) external view returns (bool);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {IERC165} from "./IERC165.sol";

/// @title IReceiver - receives keystone reports
/// @notice Implementations must support the IReceiver interface through ERC165.
interface IReceiver is IERC165 {
/// @notice Handles incoming keystone reports.
/// @dev If this function call reverts, it can be retried with a higher gas
/// limit. The receiver is responsible for discarding stale reports.
/// @param metadata Report's metadata.
/// @param report Workflow report.
function onReport(
bytes calldata metadata,
bytes calldata report
) external;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {ReceiverTemplate} from "./ReceiverTemplate.sol";

/**
* @title PredictionMarket
* @notice A simple binary prediction market resolved by Chainlink Data Feeds via CRE.
* Markets ask: "Will BTC be above $X by timestamp Y?"
* Resolution reads the on-chain Chainlink BTC/USD price feed — no AI, no off-chain APIs.
*
* Three CRE workflows interact with this contract:
* 1. Creation — opens new markets
* 2. Resolution — settles expired markets using price feed data
* 3. Dispute — re-checks resolution if disputed within the dispute window
*/
contract PredictionMarket is ReceiverTemplate {

// ─── Types ──────────────────────────────────────
enum MarketStatus { Open, Resolved, Disputed, DisputeResolved }
enum Outcome { Unresolved, Yes, No }

struct Market {
uint256 marketId;
string question; // e.g. "Will BTC be above $100,000 by 2026-04-01?"
uint256 strikePrice; // price threshold in feed decimals (e.g. 100000 * 1e8)
uint256 expirationTime; // unix timestamp when market can be resolved
uint256 disputeDeadline; // unix timestamp after which disputes are no longer accepted
MarketStatus status;
Outcome outcome;
int256 resolutionPrice; // the price used to resolve (from Chainlink feed)
uint256 resolvedAt;
}

// ─── State ──────────────────────────────────────
uint256 public nextMarketId;
mapping(uint256 => Market) public markets;
address public priceFeed; // Chainlink BTC/USD aggregator proxy address
uint256 public disputeWindow; // seconds after resolution during which disputes are accepted

// ─── Events ─────────────────────────────────────
event MarketCreated(
uint256 indexed marketId,
string question,
uint256 strikePrice,
uint256 expirationTime,
uint256 disputeDeadline
);
event MarketResolved(
uint256 indexed marketId,
Outcome outcome,
int256 resolutionPrice,
uint256 resolvedAt
);
event DisputeRaised(
uint256 indexed marketId,
address indexed disputor,
string reason
);
event DisputeResolved(
uint256 indexed marketId,
Outcome outcome,
int256 newPrice,
bool overturned
);

// ─── Action Types (decoded from CRE report) ────
uint8 constant ACTION_CREATE = 1;
uint8 constant ACTION_RESOLVE = 2;
uint8 constant ACTION_RESOLVE_DISPUTE = 3;

constructor(
address forwarder,
address _priceFeed,
uint256 _disputeWindow
) ReceiverTemplate(forwarder) {
priceFeed = _priceFeed;
disputeWindow = _disputeWindow;
}

// ─── CRE Entry Point ────────────────────────────

function _processReport(bytes calldata report) internal override {
(uint8 action, bytes memory data) = abi.decode(report, (uint8, bytes));

if (action == ACTION_CREATE) {
_createMarket(data);
} else if (action == ACTION_RESOLVE) {
_resolveMarket(data);
} else if (action == ACTION_RESOLVE_DISPUTE) {
_resolveDispute(data);
} else {
revert("Unknown action");
}
}

// ─── Action: Create Market ──────────────────────

function _createMarket(bytes memory data) internal {
(
string memory question,
uint256 strikePrice,
uint256 expirationTime
) = abi.decode(data, (string, uint256, uint256));

require(expirationTime > block.timestamp, "Expiration must be in the future");

uint256 marketId = nextMarketId++;
uint256 disputeDeadline = expirationTime + disputeWindow;

markets[marketId] = Market({
marketId: marketId,
question: question,
strikePrice: strikePrice,
expirationTime: expirationTime,
disputeDeadline: disputeDeadline,
status: MarketStatus.Open,
outcome: Outcome.Unresolved,
resolutionPrice: 0,
resolvedAt: 0
});

emit MarketCreated(marketId, question, strikePrice, expirationTime, disputeDeadline);
}

// ─── Action: Resolve Market ─────────────────────

function _resolveMarket(bytes memory data) internal {
(uint256 marketId, int256 price) = abi.decode(data, (uint256, int256));

Market storage m = markets[marketId];
require(m.status == MarketStatus.Open, "Market not open");
require(block.timestamp >= m.expirationTime, "Market not expired");

m.outcome = price >= int256(m.strikePrice) ? Outcome.Yes : Outcome.No;
m.resolutionPrice = price;
m.resolvedAt = block.timestamp;
m.status = MarketStatus.Resolved;

emit MarketResolved(marketId, m.outcome, price, block.timestamp);
}

// ─── Action: Resolve Dispute ────────────────────

function _resolveDispute(bytes memory data) internal {
(uint256 marketId, int256 newPrice) = abi.decode(data, (uint256, int256));

Market storage m = markets[marketId];
require(m.status == MarketStatus.Disputed, "Market not disputed");
require(block.timestamp <= m.disputeDeadline, "Dispute window closed");

Outcome newOutcome = newPrice >= int256(m.strikePrice) ? Outcome.Yes : Outcome.No;
bool overturned = newOutcome != m.outcome;

m.outcome = newOutcome;
m.resolutionPrice = newPrice;
m.resolvedAt = block.timestamp;
m.status = MarketStatus.DisputeResolved;

emit DisputeResolved(marketId, newOutcome, newPrice, overturned);
}

// ─── Public: Raise Dispute (called by users, not CRE) ──

function raiseDispute(uint256 marketId, string calldata reason) external {
Market storage m = markets[marketId];
require(m.status == MarketStatus.Resolved, "Market not resolved");
require(block.timestamp <= m.disputeDeadline, "Dispute window closed");

m.status = MarketStatus.Disputed;
emit DisputeRaised(marketId, msg.sender, reason);
}

// ─── View Functions (read by CRE workflows) ─────

function getMarket(uint256 marketId) external view returns (Market memory) {
return markets[marketId];
}

function getMarketStatus(uint256 marketId) external view returns (MarketStatus) {
return markets[marketId].status;
}

function getNextMarketId() external view returns (uint256) {
return nextMarketId;
}

function isExpired(uint256 marketId) external view returns (bool) {
return block.timestamp >= markets[marketId].expirationTime;
}

function isResolvable(uint256 marketId) external view returns (bool) {
Market storage m = markets[marketId];
return m.status == MarketStatus.Open && block.timestamp >= m.expirationTime;
}
}
Loading
Loading