# Market Configurable Skills

> Call guide and best practices for the configurable crypto price prediction market contracts GouGouBiMarketConfigurable.sol and GouGouBiMarketConfigurableFactory.sol, including factory creation parameters, market configuration fields, core trading/settlement methods, and conventions for calling the contracts from scripts, frontends, or OpenClow workflows via ethers/web3. Use this skill when you need to create new prediction markets, buy YES/NO, swap positions, or redeem settlements.

- Skill: `dvcrn/market-configurable-skills` (Agent Skill)
- Install (CLI): `npx skillmds@latest add dvcrn/market-configurable-skills`
- Raw SKILL.md: https://api.skillmd.com/api/skills/dvcrn/market-configurable-skills/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: dvcrn (https://skillmd.com/u/dvcrn)
- Updated: 2026-09-08
- Page: https://skillmd.com/skills/dvcrn/market-configurable-skills

---


# Configurable Crypto Price Prediction Market Skill

## 1. Protocol and contract overview

- **Market contract**: `contracts/contracts/GouGouBiMarketConfigurable.sol`
- **Factory contract**: `contracts/contracts/GouGouBiMarketConfigurableFactory.sol`
- **Model**: Polymarket-style CPMM YES/NO prediction market. Each round has its own YES/NO outcome tokens and uses a constant product market maker.
- **Key features**:
  - Uses Uniswap V3 pool prices as the oracle and computes time-windowed average prices.
  - Configurable to predict `token0/token1` or the reverse (`reverseOrder`).
  - Supports native coin or arbitrary ERC20 as the liquidity token.
  - Automatically starts a new round on schedule, and supports expiry handling and abnormal price handling (surfaced via events).

> This skill only describes the **contract APIs and calling patterns**. The actual deployment network and addresses should be provided by the product layer (for example via dApp config files or OpenClow workflow parameters).

---

## 2. Factory contract `GouGouBiMarketConfigurableFactory`

### 2.1 Core roles and state

- `owner`: factory admin, manages the creator whitelist.
- `marketImplementation`: implementation contract being cloned (`GouGouBiMarketConfigurable`).
- `isWhitelistedCreator[address]`: whether an address is allowed to call `createMarket`.
- `markets[]`: list of all created market addresses.
- `marketIndex[market]`: index of a market address (starting from 1, 0 means not created by this factory).
- `marketRecords[index]`: creation records (see below).

### 2.2 `MarketRecord` structure (read-only)

For each market created by the factory:

- `market`: market contract address
- `creator`: creator address
- `marketName`: market name
- `uniswapV3Pool`: Uniswap V3 pool address
- `liquidityToken`: liquidity token address, `address(0)` means native coin
- `feeRecipient`: fee recipient address
- `feeRate`: fee rate (denominator 10000)
- `createdAt`: creation timestamp
- The following fields align with `GouGouBiMarketConfigurable.MarketConfig` (see section 3):
  - `tokenDec`
  - `reverseOrder`
  - `settlementInterval`
  - `expiredSeconds`
  - `initialReserve`
  - `priceLookbackSeconds`
  - `imageUrl`
  - `rules`
  - `timezone`
  - `language`
  - `groupUrl`
  - `tags`
  - `predictionToken`
  - `anchorToken`
  - `currencyUnit`

### 2.3 Managing creator whitelist

```solidity
function setCreatorWhitelist(address account, bool allowed) external onlyOwner
function setCreatorWhitelistBatch(address[] calldata accounts, bool allowed) external onlyOwner
```

- Only the factory `owner` can call these.
- Must ensure `account != address(0)`.

### 2.4 Creating a market: `createMarket`

```solidity
function createMarket(GouGouBiMarketConfigurable.MarketConfig memory _config)
    external
    returns (address market)
```

Constraints:

- Only `owner` or an address with `isWhitelistedCreator[msg.sender] == true` can create:
  - `require(msg.sender == owner || isWhitelistedCreator[msg.sender], "NOT_WHITELISTED");`
- Internal flow:
  1. Use `Clones.clone(marketImplementation)` to create a minimal proxy.
  2. Call the new market’s `initialize(_config, msg.sender)` to set config and owner.
  3. Push the market address into `markets[]`, populate `marketRecords`, and update `marketIndex`.
  4. Emit `MarketCreated` event with key configuration info.

Read-only helpers:

```solidity
function getMarkets() external view returns (address[] memory)
function marketCount() external view returns (uint256)
function getMarketRecord(uint256 index) external view returns (MarketRecord memory)
function getMarketRecordsPaginated(uint256 offset, uint256 limit) external view returns (MarketRecord[] memory)
```

---

## 3. Market configuration `MarketConfig` (passed when creating)

The `_config` used by factory `createMarket` is identical to the `MarketConfig` struct inside the market contract:

```solidity
struct MarketConfig {
    string marketName;
    address uniswapV3Pool;
    address liquidityToken;    // address(0) means native coin
    uint8 tokenDec;            // if liquidityToken==0, automatically set to 18 in initialize
    bool reverseOrder;         // price direction: false=Token0/Token1, true=Token1/Token0
    uint256 settlementInterval; // settlement interval in seconds
    uint256 expiredSeconds;     // round expires and is void if not resolved by this time
    uint256 initialReserve;     // initial virtual reserve per round (will be multiplied by 10^decimals)
    uint32 priceLookbackSeconds;// lookback window for average price
    address feeRecipient;       // fee recipient address
    uint256 feeRate;            // fee numerator, denominator 10000
    string imageUrl;
    string rules;
    string timezone;
    string language;
    string groupUrl;
    string[] tags;
    address predictionToken;    // token whose price is being predicted
    address anchorToken;        // quote/anchor token, e.g. USDT, BNB
    string currencyUnit;        // display unit, e.g. "USD", "BNB", "DOGE"
}
```

Key constraints (validated in `initialize`):

- `marketName` must be non-empty: `bytes(_config.marketName).length > 0`
- `uniswapV3Pool != address(0)`
- `settlementInterval > 0`
- `expiredSeconds > 0`
- `0 < initialReserve <= 1_000_000_000`
- `feeRate <= FEE_DENOMINATOR (10000)`
- If `feeRate > 0`, then `feeRecipient != address(0)`; otherwise it will default to `_owner`.
- If `liquidityToken == address(0)`: `tokenDec` is automatically set to 18.
- If `liquidityToken != address(0)`: `tokenDec = IERC20(liquidityToken).decimals()`, requiring `tokenDec <= 18`.
- If `priceLookbackSeconds == 0`: it is automatically set to 300 seconds.

After initialization, the contract immediately calls `_startNewRound()` to open round 1.

---

## 4. Market rounds `RoundInfo` and price settlement

### 4.1 `RoundInfo` structure

```solidity
struct RoundInfo {
    uint8 winning;              // 0=not settled, 1=YES wins, 2=NO wins, 3=draw/exception
    uint256 poolAmount;         // total liquidity token amount in the pool for this round
    uint256 x;                  // CPMM x reserve (YES pool)
    uint256 y;                  // CPMM y reserve (NO pool)
    address yesToken;           // YES outcome token for this round
    address noToken;            // NO outcome token for this round
    uint256 startTime;          // round start time
    uint256 endTime;            // scheduled settlement time
    uint256 expiredTime;        // expiration time (if exceeded, treated as winning=3)
    uint256 startAveragePrice;  // average price at round start
    uint256 endAveragePrice;    // average price at round end / settlement
}
```

- Accessed via the read-only mapping `rounds[round]`.
- Current round index: `currentRound`.

### 4.2 Price fetching: `getAveragePriceFromUniswapV3`

```solidity
function getAveragePriceFromUniswapV3(uint32 startTime, uint32 endTime)
    public
    view
    returns (uint256 averagePrice)
```

- `startTime < endTime`, and `endTime <= block.timestamp`.
- Internally uses Uniswap V3 `observe` + `TickMath` to compute the average price over the interval, represented with `1e18` precision.
- If `reverseOrder == true`, takes the reciprocal of the price (still with `1e18` precision).

### 4.3 Settlement and round transitions

- `_checkAndExecuteSettlement()`: called before every trade/redeem/resolve to:
  - If `block.timestamp > expiredTime`: set `winning = 3`, `endAveragePrice = 0`, emit `AutoResolve` event, and start a new round.
  - If the current round has reached `endTime` and not yet expired: call `_calculateSettlementByPrice()`.
- `_calculateSettlementByPrice()`:
  - Uses `getAveragePriceFromUniswapV3(priceLookbackSeconds, 0)` to get the settlement-time average price.
  - Compares with `startAveragePrice`:
    - equal → `winning = 3` (draw/special situation)
    - settlement price > start price → `winning = 1` (YES wins)
    - settlement price < start price → `winning = 2` (NO wins)
  - Updates `endTime` and `endAveragePrice`, emits `AutoResolve`, then starts a new round.

External callers can trigger the check manually:

```solidity
function resolve() external nonReentrant {
    _checkAndExecuteSettlement();
}
```

---

## 5. Core user interaction methods

### 5.1 Buying YES / NO

#### 5.1.1 `buyYes`

```solidity
function buyYes(uint256 tokenIn, uint256 minYesOut) external payable nonReentrant
```

Semantics:

- For the current `currentRound`:
  - If the market uses the native coin (`liquidityToken == address(0)`):
    - Ignore `tokenIn`, use `msg.value` as the input amount, requiring `msg.value > 0`.
  - If using an ERC20:
    - Require `tokenIn > 0 && msg.value == 0`.
    - Must first call `IERC20(liquidityToken).approve(market, tokenIn)`; the contract uses `safeTransferFrom` internally.
- Contract logic:
  - Add `tokenIn` to `rounds[currentRound].poolAmount`.
  - Mint equal virtual YES/NO positions (`mintedYes = mintedNo = tokenIn`), then use the constant product formula to compute slippage and obtain additional swapped amount.
  - Final YES amount is `totalYesOut = mintedYes + swappedYes`, which must be `>= minYesOut` or the transaction reverts (slippage protection).
  - Use `OutcomeToken(yesToken).mint(msg.sender, totalYesOut)` to issue YES tokens.

#### 5.1.2 `buyNo`

```solidity
function buyNo(uint256 tokenIn, uint256 minNoOut) external payable nonReentrant
```

- Symmetric to `buyYes` but operates on the NO pool, yielding `totalNoOut`, which must be `>= minNoOut`.

### 5.2 Position swaps: `swapYesForNo` / `swapNoForYes`

```solidity
function swapYesForNo(uint256 amountIn, uint256 minAmountOut) external nonReentrant
function swapNoForYes(uint256 amountIn, uint256 minAmountOut) external nonReentrant
```

Common logic:

- Only allowed while `winning == 0` (round not yet settled).
- Caller must already hold the corresponding YES or NO token balance.
- Computes `amountOut` based on the current reserves `x`, `y` and `amountIn`, then checks slippage:
  - `swapYesForNo`: `amountOut = (y * amountIn) / (x + amountIn)`
  - `swapNoForYes`: `amountOut = (x * amountIn) / (y + amountIn)`
- Burns the input tokens via `burn`, mints output tokens via `mint`, and updates `x`, `y`.

### 5.3 Settlement redemption: `redeem`

```solidity
function redeem(uint256 round) external nonReentrant
```

Constraints and flow:

- First calls `_checkAndExecuteSettlement()`, which may change the current round state.
- Requires:
  - `round > 0 && round <= currentRound`
  - `rounds[round].winning != 0` (round already settled: YES wins, NO wins, or draw)
  - `rounds[round].poolAmount > 0`
  - Caller holds at least one of YES/NO tokens for that round.
- Distribution logic:
  - If `winning == 1` (YES wins):
    - Distribute pool amount in proportion to YES token supply shares.
  - If `winning == 2` (NO wins):
    - Distribute in proportion to NO token supply shares.
  - If `winning == 3` (draw/exception):
    - Sum the caller’s YES and NO balances and distribute according to their share of total YES+NO supply.
  - Every payout deducts `tokenOut` from the pool and applies fees by `feeRate`:
    - `fee = tokenOut * feeRate / FEE_DENOMINATOR`
    - User receives `userAmount = tokenOut - fee`
    - Fee is transferred to `config.feeRecipient`
  - Supports both native coin and ERC20 transfers.

### 5.4 Price queries and pool info

```solidity
function priceYes() external view returns (uint256 num, uint256 den)
function priceNo() external view returns (uint256 num, uint256 den)
function getPoolInfo() external view returns (
    uint256 _x, uint256 _y, uint256 _poolAmount,
    uint256 _priceYes, uint256 _priceNo,
    uint256 _currentRound, uint8 _winning
)
function getRoundInfo(uint256 round) external view returns (
    uint8 _winning, uint256 _poolAmount, uint256 _x, uint256 _y,
    address _yesToken, address _noToken,
    uint256 _startTime, uint256 _endTime, uint256 _expiredTime,
    uint256 _startAveragePrice, uint256 _endAveragePrice
)
```

- `priceYes`/`priceNo` return prices as fractions (`num/den`) for UI display or estimation.
- `getPoolInfo` returns current round pool and price information in one call.
- `getRoundInfo` returns detailed data for any round, including YES/NO token addresses and timing info.

---

## 6. Usage examples in scripts / frontends (ethers.js)

> These examples only demonstrate call patterns. The actual `FACTORY_ADDRESS` and market addresses depend on deployment and should be passed via your project config.

```ts
import { ethers } from "ethers";
import MarketAbi from "../artifacts/contracts/GouGouBiMarketConfigurable.sol/GouGouBiMarketConfigurable.json";
import FactoryAbi from "../artifacts/contracts/GouGouBiMarketConfigurableFactory.sol/GouGouBiMarketConfigurableFactory.json";

// Provided by the product side
const FACTORY_ADDRESS = "<REPLACE_WITH_FACTORY_ADDRESS>";

export function getFactory(providerOrSigner: ethers.Signer | ethers.providers.Provider) {
  return new ethers.Contract(FACTORY_ADDRESS, FactoryAbi.abi, providerOrSigner);
}

export function getMarket(
  marketAddress: string,
  providerOrSigner: ethers.Signer | ethers.providers.Provider
) {
  return new ethers.Contract(marketAddress, MarketAbi.abi, providerOrSigner);
}

// Create a new market (requires factory owner or whitelisted address)
export async function createMarket(
  signer: ethers.Signer,
  config: {
    marketName: string;
    uniswapV3Pool: string;
    liquidityToken: string;   // for native-coin markets, pass ethers.constants.AddressZero
    reverseOrder: boolean;
    settlementInterval: number;
    expiredSeconds: number;
    initialReserve: ethers.BigNumberish;
    priceLookbackSeconds?: number;
    feeRecipient?: string;
    feeRate: number;
    imageUrl?: string;
    rules?: string;
    timezone?: string;
    language?: string;
    groupUrl?: string;
    tags?: string[];
    predictionToken: string;
    anchorToken: string;
    currencyUnit: string;
  }
) {
  const factory = getFactory(signer);
  const _config = {
    ...config,
    // Let the contract infer defaults such as tokenDec / priceLookbackSeconds / feeRecipient
    tokenDec: 0,
    priceLookbackSeconds: config.priceLookbackSeconds ?? 0,
    feeRecipient: config.feeRecipient ?? ethers.constants.AddressZero,
    tags: config.tags ?? [],
  };
  const tx = await factory.createMarket(_config);
  const receipt = await tx.wait();
  // You can parse the new market address from the MarketCreated event
  return receipt;
}

// Buy YES in the current round (ERC20 market example)
export async function buyYes(
  signer: ethers.Signer,
  marketAddress: string,
  amountIn: ethers.BigNumberish,
  minYesOut: ethers.BigNumberish
) {
  const market = getMarket(marketAddress, signer);
  const tx = await market.buyYes(amountIn, minYesOut);
  return tx.wait();
}

// Buy NO in the current round (native-coin market example)
export async function buyNoNative(
  signer: ethers.Signer,
  marketAddress: string,
  amountWei: ethers.BigNumberish,
  minNoOut: ethers.BigNumberish
) {
  const market = getMarket(marketAddress, signer);
  const tx = await market.buyNo(0, minNoOut, { value: amountWei });
  return tx.wait();
}

// Redeem settlement for a specific round
export async function redeem(
  signer: ethers.Signer,
  marketAddress: string,
  round: number
) {
  const market = getMarket(marketAddress, signer);
  const tx = await market.redeem(round);
  return tx.wait();
}
```

Recommended integration practices in frontends or automation scripts:

- **Read operations** (`getPoolInfo`, `getRoundInfo`, `priceYes`, `priceNo`, etc.) should use a read-only `provider`.
- **Write operations** (`createMarket`, `buyYes`, `buyNo`, `swapYesForNo`, `swapNoForYes`, `redeem`, `resolve`) must use a `signer` with signing capability.
- For ERC20 markets, call `approve` on the market contract before `buyYes` / `buyNo`; for native-coin markets, attach funds via `value`.
- Frontends should set `minYesOut` / `minNoOut` with reasonable slippage protection (e.g. 95%–99% of expected output).

---

## 7. Relationship with other skills

- This `market-configurable-skills` focuses on the **concrete implementation and calling patterns of the GouGouBi configurable price prediction market**, including factory creation, market configuration fields, and trading/settlement logic.
- If your product also uses the giveaway protocol or other contracts, you can read the corresponding skills (such as `giveaway-skills` / `giveaway-protocol`) together. There is no direct contract-level dependency; they only cooperate at the product level.

---

## 8. ABI information and minimal examples

> For the full ABI, it is recommended to use the compiled artifacts directly (for example, the `abi` fields in `artifacts/contracts/GouGouBiMarketConfigurable.sol/GouGouBiMarketConfigurable.json` and `...Factory.sol/GouGouBiMarketConfigurableFactory.json`). This section only provides **minimal fragments for common events and functions**, for quick debugging or for constructing temporary contract instances when build artifacts are not accessible.

### 8.1 Factory contract `GouGouBiMarketConfigurableFactory` ABI fragments

```json
[
  {
    "type": "event",
    "name": "CreatorWhitelistUpdated",
    "anonymous": false,
    "inputs": [
      { "indexed": true, "name": "account", "type": "address" },
      { "indexed": false, "name": "allowed", "type": "bool" }
    ]
  },
  {
    "type": "event",
    "name": "MarketCreated",
    "anonymous": false,
    "inputs": [
      { "indexed": true, "name": "market", "type": "address" },
      { "indexed": false, "name": "marketName", "type": "string" },
      { "indexed": true, "name": "uniswapV3Pool", "type": "address" },
      { "indexed": true, "name": "creator", "type": "address" },
      { "indexed": false, "name": "liquidityToken", "type": "address" },
      { "indexed": false, "name": "createdAt", "type": "uint256" },
      { "indexed": false, "name": "feeRecipient", "type": "address" },
      { "indexed": false, "name": "feeRate", "type": "uint256" },
      { "indexed": false, "name": "tokenDec", "type": "uint8" },
      { "indexed": false, "name": "reverseOrder", "type": "bool" },
      { "indexed": false, "name": "settlementInterval", "type": "uint256" },
      { "indexed": false, "name": "expiredSeconds", "type": "uint256" },
      { "indexed": false, "name": "initialReserve", "type": "uint256" },
      { "indexed": false, "name": "priceLookbackSeconds", "type": "uint32" },
      { "indexed": false, "name": "imageUrl", "type": "string" },
      { "indexed": false, "name": "rules", "type": "string" },
      { "indexed": false, "name": "timezone", "type": "string" },
      { "indexed": false, "name": "language", "type": "string" },
      { "indexed": false, "name": "groupUrl", "type": "string" },
      { "indexed": false, "name": "tags", "type": "string[]" },
      { "indexed": false, "name": "predictionToken", "type": "address" },
      { "indexed": false, "name": "anchorToken", "type": "address" },
      { "indexed": false, "name": "currencyUnit", "type": "string" }
    ]
  },
  {
    "type": "function",
    "stateMutability": "view",
    "name": "owner",
    "inputs": [],
    "outputs": [{ "type": "address" }]
  },
  {
    "type": "function",
    "stateMutability": "view",
    "name": "marketImplementation",
    "inputs": [],
    "outputs": [{ "type": "address" }]
  },
  {
    "type": "function",
    "stateMutability": "view",
    "name": "isWhitelistedCreator",
    "inputs": [{ "name": "account", "type": "address" }],
    "outputs": [{ "type": "bool" }]
  },
  {
    "type": "function",
    "stateMutability": "nonpayable",
    "name": "setCreatorWhitelist",
    "inputs": [
      { "name": "account", "type": "address" },
      { "name": "allowed", "type": "bool" }
    ],
    "outputs": []
  },
  {
    "type": "function",
    "stateMutability": "nonpayable",
    "name": "setCreatorWhitelistBatch",
    "inputs": [
      { "name": "accounts", "type": "address[]" },
      { "name": "allowed", "type": "bool" }
    ],
    "outputs": []
  },
  {
    "type": "function",
    "stateMutability": "nonpayable",
    "name": "createMarket",
    "inputs": [
      {
        "name": "_config",
        "type": "tuple",
        "components": [
          { "name": "marketName", "type": "string" },
          { "name": "uniswapV3Pool", "type": "address" },
          { "name": "liquidityToken", "type": "address" },
          { "name": "tokenDec", "type": "uint8" },
          { "name": "reverseOrder", "type": "bool" },
          { "name": "settlementInterval", "type": "uint256" },
          { "name": "expiredSeconds", "type": "uint256" },
          { "name": "initialReserve", "type": "uint256" },
          { "name": "priceLookbackSeconds", "type": "uint32" },
          { "name": "feeRecipient", "type": "address" },
          { "name": "feeRate", "type": "uint256" },
          { "name": "imageUrl", "type": "string" },
          { "name": "rules", "type": "string" },
          { "name": "timezone", "type": "string" },
          { "name": "language", "type": "string" },
          { "name": "groupUrl", "type": "string" },
          { "name": "tags", "type": "string[]" },
          { "name": "predictionToken", "type": "address" },
          { "name": "anchorToken", "type": "address" },
          { "name": "currencyUnit", "type": "string" }
        ]
      }
    ],
    "outputs": [{ "type": "address" }]
  },
  {
    "type": "function",
    "stateMutability": "view",
    "name": "getMarkets",
    "inputs": [],
    "outputs": [{ "type": "address[]" }]
  },
  {
    "type": "function",
    "stateMutability": "view",
    "name": "marketCount",
    "inputs": [],
    "outputs": [{ "type": "uint256" }]
  },
  {
    "type": "function",
    "stateMutability": "view",
    "name": "getMarketRecord",
    "inputs": [{ "name": "index", "type": "uint256" }],
    "outputs": [
      {
        "type": "tuple",
        "components": [
          { "name": "market", "type": "address" },
          { "name": "creator", "type": "address" },
          { "name": "marketName", "type": "string" },
          { "name": "uniswapV3Pool", "type": "address" },
          { "name": "liquidityToken", "type": "address" },
          { "name": "feeRecipient", "type": "address" },
          { "name": "feeRate", "type": "uint256" },
          { "name": "createdAt", "type": "uint256" },
          { "name": "tokenDec", "type": "uint8" },
          { "name": "reverseOrder", "type": "bool" },
          { "name": "settlementInterval", "type": "uint256" },
          { "name": "expiredSeconds", "type": "uint256" },
          { "name": "initialReserve", "type": "uint256" },
          { "name": "priceLookbackSeconds", "type": "uint32" },
          { "name": "imageUrl", "type": "string" },
          { "name": "rules", "type": "string" },
          { "name": "timezone", "type": "string" },
          { "name": "language", "type": "string" },
          { "name": "groupUrl", "type": "string" },
          { "name": "tags", "type": "string[]" },
          { "name": "predictionToken", "type": "address" },
          { "name": "anchorToken", "type": "address" },
          { "name": "currencyUnit", "type": "string" }
        ]
      }
    ]
  }
]
```

### 8.2 Market contract `GouGouBiMarketConfigurable` ABI fragments

```json
[
  {
    "type": "event",
    "name": "StartRound",
    "anonymous": false,
    "inputs": [
      { "indexed": true, "name": "round", "type": "uint256" },
      { "indexed": false, "name": "x", "type": "uint256" },
      { "indexed": false, "name": "y", "type": "uint256" },
      { "indexed": false, "name": "startTime", "type": "uint256" },
      { "indexed": false, "name": "endTime", "type": "uint256" },
      { "indexed": false, "name": "expiredTime", "type": "uint256" },
      { "indexed": false, "name": "startAveragePrice", "type": "uint256" },
      { "indexed": false, "name": "endAveragePrice", "type": "uint256" },
      { "indexed": false, "name": "yesToken", "type": "address" },
      { "indexed": false, "name": "noToken", "type": "address" }
    ]
  },
  {
    "type": "event",
    "name": "AutoResolve",
    "anonymous": false,
    "inputs": [
      { "indexed": true, "name": "round", "type": "uint256" },
      { "indexed": false, "name": "winning", "type": "uint8" },
      { "indexed": false, "name": "poolAmount", "type": "uint256" },
      { "indexed": false, "name": "x", "type": "uint256" },
      { "indexed": false, "name": "y", "type": "uint256" },
      { "indexed": false, "name": "startTime", "type": "uint256" },
      { "indexed": false, "name": "endTime", "type": "uint256" },
      { "indexed": false, "name": "endAveragePrice", "type": "uint256" }
    ]
  },
  {
    "type": "event",
    "name": "BuyYes",
    "anonymous": false,
    "inputs": [
      { "indexed": true, "name": "user", "type": "address" },
      { "indexed": true, "name": "round", "type": "uint256" },
      { "indexed": false, "name": "tokenIn", "type": "uint256" },
      { "indexed": false, "name": "yesOut", "type": "uint256" },
      { "indexed": false, "name": "newX", "type": "uint256" },
      { "indexed": false, "name": "newY", "type": "uint256" },
      { "indexed": false, "name": "poolAmount", "type": "uint256" }
    ]
  },
  {
    "type": "event",
    "name": "BuyNo",
    "anonymous": false,
    "inputs": [
      { "indexed": true, "name": "user", "type": "address" },
      { "indexed": true, "name": "round", "type": "uint256" },
      { "indexed": false, "name": "tokenIn", "type": "uint256" },
      { "indexed": false, "name": "noOut", "type": "uint256" },
      { "indexed": false, "name": "newX", "type": "uint256" },
      { "indexed": false, "name": "newY", "type": "uint256" },
      { "indexed": false, "name": "poolAmount", "type": "uint256" }
    ]
  },
  {
    "type": "event",
    "name": "Redeem",
    "anonymous": false,
    "inputs": [
      { "indexed": true, "name": "user", "type": "address" },
      { "indexed": true, "name": "round", "type": "uint256" },
      { "indexed": false, "name": "winning", "type": "uint8" },
      { "indexed": false, "name": "tokensYes", "type": "uint256" },
      { "indexed": false, "name": "tokensNo", "type": "uint256" },
      { "indexed": false, "name": "tokensReceived", "type": "uint256" },
      { "indexed": false, "name": "poolAmount", "type": "uint256" },
      { "indexed": false, "name": "timestamp", "type": "uint256" }
    ]
  },
  {
    "type": "event",
    "name": "SwapYesForNo",
    "anonymous": false,
    "inputs": [
      { "indexed": true, "name": "user", "type": "address" },
      { "indexed": true, "name": "round", "type": "uint256" },
      { "indexed": false, "name": "yesIn", "type": "uint256" },
      { "indexed": false, "name": "noOut", "type": "uint256" },
      { "indexed": false, "name": "newX", "type": "uint256" },
      { "indexed": false, "name": "newY", "type": "uint256" }
    ]
  },
  {
    "type": "event",
    "name": "SwapNoForYes",
    "anonymous": false,
    "inputs": [
      { "indexed": true, "name": "user", "type": "address" },
      { "indexed": true, "name": "round", "type": "uint256" },
      { "indexed": false, "name": "noIn", "type": "uint256" },
      { "indexed": false, "name": "yesOut", "type": "uint256" },
      { "indexed": false, "name": "newX", "type": "uint256" },
      { "indexed": false, "name": "newY", "type": "uint256" }
    ]
  },
  {
    "type": "event",
    "name": "FeeCollected",
    "anonymous": false,
    "inputs": [
      { "indexed": true, "name": "user", "type": "address" },
      { "indexed": true, "name": "round", "type": "uint256" },
      { "indexed": false, "name": "feeAmount", "type": "uint256" },
      { "indexed": false, "name": "feeRecipient", "type": "address" }
    ]
  },
  {
    "type": "function",
    "stateMutability": "view",
    "name": "config",
    "inputs": [],
    "outputs": [
      {
        "type": "tuple",
        "components": [
          { "name": "marketName", "type": "string" },
          { "name": "uniswapV3Pool", "type": "address" },
          { "name": "liquidityToken", "type": "address" },
          { "name": "tokenDec", "type": "uint8" },
          { "name": "reverseOrder", "type": "bool" },
          { "name": "settlementInterval", "type": "uint256" },
          { "name": "expiredSeconds", "type": "uint256" },
          { "name": "initialReserve", "type": "uint256" },
          { "name": "priceLookbackSeconds", "type": "uint32" },
          { "name": "feeRecipient", "type": "address" },
          { "name": "feeRate", "type": "uint256" },
          { "name": "imageUrl", "type": "string" },
          { "name": "rules", "type": "string" },
          { "name": "timezone", "type": "string" },
          { "name": "language", "type": "string" },
          { "name": "groupUrl", "type": "string" },
          { "name": "tags", "type": "string[]" },
          { "name": "predictionToken", "type": "address" },
          { "name": "anchorToken", "type": "address" },
          { "name": "currencyUnit", "type": "string" }
        ]
      }
    ]
  },
  {
    "type": "function",
    "stateMutability": "view",
    "name": "currentRound",
    "inputs": [],
    "outputs": [{ "type": "uint256" }]
  },
  {
    "type": "function",
    "stateMutability": "view",
    "name": "getRoundInfo",
    "inputs": [{ "name": "round", "type": "uint256" }],
    "outputs": [
      {
        "type": "tuple",
        "components": [
          { "name": "winning", "type": "uint8" },
          { "name": "poolAmount", "type": "uint256" },
          { "name": "x", "type": "uint256" },
          { "name": "y", "type": "uint256" },
          { "name": "yesToken", "type": "address" },
          { "name": "noToken", "type": "address" },
          { "name": "startTime", "type": "uint256" },
          { "name": "endTime", "type": "uint256" },
          { "name": "expiredTime", "type": "uint256" },
          { "name": "startAveragePrice", "type": "uint256" },
          { "name": "endAveragePrice", "type": "uint256" }
        ]
      }
    ]
  },
  {
    "type": "function",
    "stateMutability": "view",
    "name": "getPoolInfo",
    "inputs": [],
    "outputs": [
      { "name": "_x", "type": "uint256" },
      { "name": "_y", "type": "uint256" },
      { "name": "_poolAmount", "type": "uint256" },
      { "name": "_priceYes", "type": "uint256" },
      { "name": "_priceNo", "type": "uint256" },
      { "name": "_currentRound", "type": "uint256" },
      { "name": "_winning", "type": "uint8" }
    ]
  },
  {
    "type": "function",
    "stateMutability": "payable",
    "name": "buyYes",
    "inputs": [
      { "name": "tokenIn", "type": "uint256" },
      { "name": "minYesOut", "type": "uint256" }
    ],
    "outputs": []
  },
  {
    "type": "function",
    "stateMutability": "payable",
    "name": "buyNo",
    "inputs": [
      { "name": "tokenIn", "type": "uint256" },
      { "name": "minNoOut", "type": "uint256" }
    ],
    "outputs": []
  },
  {
    "type": "function",
    "stateMutability": "nonpayable",
    "name": "swapYesForNo",
    "inputs": [
      { "name": "amountIn", "type": "uint256" },
      { "name": "minAmountOut", "type": "uint256" }
    ],
    "outputs": []
  },
  {
    "type": "function",
    "stateMutability": "nonpayable",
    "name": "swapNoForYes",
    "inputs": [
      { "name": "amountIn", "type": "uint256" },
      { "name": "minAmountOut", "type": "uint256" }
    ],
    "outputs": []
  },
  {
    "type": "function",
    "stateMutability": "nonpayable",
    "name": "redeem",
    "inputs": [{ "name": "round", "type": "uint256" }],
    "outputs": []
  },
  {
    "type": "function",
    "stateMutability": "nonpayable",
    "name": "resolve",
    "inputs": [],
    "outputs": []
  }
]
```

The ABI fragments above can be combined with the TypeScript examples in section 6, for example:

```ts
import { ethers } from "ethers";
import { factoryFragment, marketFragment } from "./fragments"; // export the JSON fragments above as constants

const factory = new ethers.Contract(FACTORY_ADDRESS, factoryFragment, signerOrProvider);
const market = new ethers.Contract(marketAddress, marketFragment, signerOrProvider);
```

---
name: market-configurable-skills
display_name: GouGouBi 可配置价格预测市场 Skill
description: >
  基于 contracts/contracts/GouGouBiMarketConfigurable.sol 与 GouGouBiMarketConfigurableFactory.sol 的加密价格预测市场合约调用说明与最佳实践，包含工厂创建参数、市场配置字段、核心交易/结算方法以及在脚本、前端或 OpenClow 工作流中通过 ethers/web3 调用该合约的规范。当需要创建新预测市场、进行 YES/NO 买入、头寸互换或结算赎回时使用本 skill。
author: frank
version: 0.1.0
language: zh-CN
tags:
  - prediction-market
  - evm
  - contract-call
---

# 可配置加密价格预测市场 Skill

## 1. 协议与合约概览

- **市场合约**：`contracts/contracts/GouGouBiMarketConfigurable.sol`
- **工厂合约**：`contracts/contracts/GouGouBiMarketConfigurableFactory.sol`
- **模式**：Polymarket 风格 CPMM YES/NO 预测市场，每一轮（Round）都有独立的 YES/NO 结果代币，通过常数乘积做市。
- **特点**：
  - 使用 Uniswap V3 池的价格作为预言机，按时间区间计算平均价格。
  - 可配置是否预测 `token0/token1` 还是反向（`reverseOrder`）。
  - 支持原生币或任意 ERC20 作为流动性代币。
  - 自动按周期开启新一轮市场，支持结算过期、价格异常处理（通过事件体现）。

> 本 skill 只描述 **合约 API 与调用模式**，具体部署网络与地址由业务方在上层配置（例如在 dApp 配置文件或 OpenClow workflow 的参数中传入）。

---

## 2. 工厂合约 GouGouBiMarketConfigurableFactory

### 2.1 核心角色与状态

- `owner`：工厂管理员，可管理创建白名单。
- `marketImplementation`：被克隆的逻辑合约实现（`GouGouBiMarketConfigurable`）。
- `isWhitelistedCreator[address]`：是否允许该地址调用 `createMarket`。
- `markets[]`：所有已创建市场地址列表。
- `marketIndex[market]`：市场地址对应的索引（从 1 开始，0 表示不是本工厂创建）。
- `marketRecords[index]`：创建记录（见下）。

### 2.2 MarketRecord 结构（只读）

对应工厂中每个已创建市场：

- `market`：市场合约地址
- `creator`：创建者地址
- `marketName`：市场名称
- `uniswapV3Pool`：Uniswap V3 池地址
- `liquidityToken`：流动性代币地址，`address(0)` 表示原生币
- `feeRecipient`：手续费接收地址
- `feeRate`：手续费率（分母 10000）
- `createdAt`：创建时间戳
- 以下字段与 `GouGouBiMarketConfigurable.MarketConfig` 对齐（见第 3 节）：
  - `tokenDec`
  - `reverseOrder`
  - `settlementInterval`
  - `expiredSeconds`
  - `initialReserve`
  - `priceLookbackSeconds`
  - `imageUrl`
  - `rules`
  - `timezone`
  - `language`
  - `groupUrl`
  - `tags`
  - `predictionToken`
  - `anchorToken`
  - `currencyUnit`

### 2.3 管理白名单

```solidity
function setCreatorWhitelist(address account, bool allowed) external onlyOwner
function setCreatorWhitelistBatch(address[] calldata accounts, bool allowed) external onlyOwner
```

- 只有工厂 `owner` 可调用。
- 需保证 `account != address(0)`。

### 2.4 创建市场 createMarket

```solidity
function createMarket(GouGouBiMarketConfigurable.MarketConfig memory _config)
    external
    returns (address market)
```

约束：

- 仅 `owner` 或 `isWhitelistedCreator[msg.sender] == true` 的地址可创建：
  - `require(msg.sender == owner || isWhitelistedCreator[msg.sender], "NOT_WHITELISTED");`
- 内部流程：
  1. 使用 `Clones.clone(marketImplementation)` 创建最小代理。
  2. 调用新市场的 `initialize(_config, msg.sender)`，设置配置与 owner。
  3. 将市场地址加入 `markets[]`，填充 `marketRecords`，更新 `marketIndex`。
  4. 触发 `MarketCreated` 事件，记录关键配置信息。

读接口：

```solidity
function getMarkets() external view returns (address[] memory)
function marketCount() external view returns (uint256)
function getMarketRecord(uint256 index) external view returns (MarketRecord memory)
function getMarketRecordsPaginated(uint256 offset, uint256 limit) external view returns (MarketRecord[] memory)
```

---

## 3. 市场配置 MarketConfig（创建时传入）

工厂 `createMarket` 的 `_config` 与市场合约中的 `MarketConfig` 完全一致：

```solidity
struct MarketConfig {
    string marketName;
    address uniswapV3Pool;
    address liquidityToken;    // address(0) 表示原生币
    uint8 tokenDec;            // 若 liquidityToken==0，则在 initialize 中自动设为 18
    bool reverseOrder;         // 价格方向：false=Token0/Token1, true=Token1/Token0
    uint256 settlementInterval; // 结算周期秒数
    uint256 expiredSeconds;     // 超时未裁决则作废
    uint256 initialReserve;     // 每轮初始虚拟储备（会乘以 10^decimals）
    uint32 priceLookbackSeconds;// 取价回溯时间，用于平均价
    address feeRecipient;       // 手续费接收地址
    uint256 feeRate;            // 手续费分子，分母 10000
    string imageUrl;
    string rules;
    string timezone;
    string language;
    string groupUrl;
    string[] tags;
    address predictionToken;    // 被预测价格的代币
    address anchorToken;        // 计价/锚定代币，如 USDT、BNB
    string currencyUnit;        // 展示用单位，如 "USD"、"BNB"、"DOGE"
}
```

关键约束（在 `initialize` 中校验）：

- `marketName` 非空：`bytes(_config.marketName).length > 0`
- `uniswapV3Pool != address(0)`
- `settlementInterval > 0`
- `expiredSeconds > 0`
- `0 < initialReserve <= 1_000_000_000`
- `feeRate <= FEE_DENOMINATOR (10000)`
- 若 `feeRate > 0`，`feeRecipient != address(0)`，否则会默认设置为 `_owner`。
- 若 `liquidityToken == address(0)`：`tokenDec` 自动设为 18。
- 若 `liquidityToken != address(0)`：`tokenDec = IERC20(liquidityToken).decimals()`，要求 `tokenDec <= 18`。
- 若 `priceLookbackSeconds == 0`：自动设为 300 秒。

初始化完成后，合约立即调用 `_startNewRound()` 开启第 1 轮。

---

## 4. 市场轮次 RoundInfo 与价格结算

### 4.1 RoundInfo 结构

```solidity
struct RoundInfo {
    uint8 winning;              // 0=未结算, 1=YES 赢, 2=NO 赢, 3=平局/异常
    uint256 poolAmount;         // 池中当前累计的流动性代币数量
    uint256 x;                  // CPMM x 储备（YES 池）
    uint256 y;                  // CPMM y 储备（NO 池）
    address yesToken;           // 本轮 YES OutcomeToken
    address noToken;            // 本轮 NO OutcomeToken
    uint256 startTime;          // 本轮开始时间
    uint256 endTime;            // 预定结算时间
    uint256 expiredTime;        // 过期时间（超时自动视为 winning=3）
    uint256 startAveragePrice;  // 轮次开始时平均价格
    uint256 endAveragePrice;    // 轮次结束/结算时平均价格
}
```

- 通过 `rounds[round]` 只读映射访问。
- 当前轮次索引：`currentRound`。

### 4.2 价格获取：getAveragePriceFromUniswapV3

```solidity
function getAveragePriceFromUniswapV3(uint32 startTime, uint32 endTime)
    public
    view
    returns (uint256 averagePrice)
```

- `startTime > endTime`，且 `endTime <= block.timestamp`。
- 内部使用 Uniswap V3 `observe` + `TickMath` 计算对应区间的平均价格，并用 `1e18` 精度表示。
- 若 `reverseOrder == true`，会对价格取倒数（同样保持 1e18 精度）。

### 4.3 结算与轮次切换

- `_checkAndExecuteSettlement()`：在每次交易/赎回/resolve 前调用，用于：
  - 若 `block.timestamp > expiredTime`：将本轮 `winning = 3`，`endAveragePrice = 0`，触发 `AutoResolve` 事件，并开启新一轮。
  - 若当前轮已到达 `endTime` 且未过期：调用 `_calculateSettlementByPrice()`。
- `_calculateSettlementByPrice()`：
  - 通过 `getAveragePriceFromUniswapV3(priceLookbackSeconds, 0)` 获取结算时平均价。
  - 与 `startAveragePrice` 比较：
    - 相等 → `winning = 3`（平局/特殊情况）
    - 结算价 > 起始价 → `winning = 1`（YES 赢）
    - 结算价 < 起始价 → `winning = 2`（NO 赢）
  - 更新 `endTime`、`endAveragePrice`，触发 `AutoResolve`，然后开启新一轮。

外部可主动触发检查：

```solidity
function resolve() external nonReentrant {
    _checkAndExecuteSettlement();
}
```

---

## 5. 用户交互核心方法

### 5.1 买入 YES / NO

#### 5.1.1 buyYes

```solidity
function buyYes(uint256 tokenIn, uint256 minYesOut) external payable nonReentrant
```

语义：

- 对当前轮 `currentRound`：
  - 若市场为原生币（`liquidityToken == address(0)`）：
    - 忽略 `tokenIn` 参数，使用 `msg.value` 作为投入金额，要求 `msg.value > 0`。
  - 若为 ERC20：
    - 要求 `tokenIn > 0 && msg.value == 0`。
    - 需提前 `IERC20(liquidityToken).approve(market, tokenIn)`，合约内部使用 `safeTransferFrom`。
- 合约逻辑：
  - 将 `tokenIn` 记入 `rounds[currentRound].poolAmount`。
  - 铸造等量 YES/NO 虚拟头寸（`mintedYes = mintedNo = tokenIn`），通过常数乘积公式计算价格滑点，得到额外的 swapped 数量。
  - 最终得到的 YES 数量 `totalYesOut = mintedYes + swappedYes`，必须 `>= minYesOut`，否则交易回滚（滑点保护）。
  - 使用 `OutcomeToken(yesToken).mint(msg.sender, totalYesOut)` 发放 YES 代币。

#### 5.1.2 buyNo

```solidity
function buyNo(uint256 tokenIn, uint256 minNoOut) external payable nonReentrant
```

- 与 `buyYes` 对称，只是对 NO 池进行操作，最终得到 `totalNoOut`，需要满足 `>= minNoOut`。

### 5.2 头寸互换：swapYesForNo / swapNoForYes

```solidity
function swapYesForNo(uint256 amountIn, uint256 minAmountOut) external nonReentrant
function swapNoForYes(uint256 amountIn, uint256 minAmountOut) external nonReentrant
```

通用逻辑要点：

- 仅当本轮 `winning == 0`（未结算）时允许互换。
- 需先持有对应的 YES 或 NO 代币余额。
- 根据当前池子 `x`、`y` 以及输入数量 `amountIn` 计算 `amountOut`，并进行滑点检查：
  - `swapYesForNo`：`amountOut = (y * amountIn) / (x + amountIn)`
  - `swapNoForYes`：`amountOut = (x * amountIn) / (y + amountIn)`
- 使用 `burn` 销毁输入代币，`mint` 发放输出代币，并更新 `x`、`y`。

### 5.3 赎回结算：redeem

```solidity
function redeem(uint256 round) external nonReentra

…(truncated)
