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.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install openclaw-skills-market-configurable-skills
Repository
Skill path: skills/franckstone/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.
Open repositoryBest for
Primary workflow: Ship Full Stack.
Technical facets: Full Stack.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: openclaw.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install market-configurable-skills into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/openclaw/skills before adding market-configurable-skills to shared team environments
- Use market-configurable-skills for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: market-configurable-skills
display_name: GouGouBi Configurable Price Prediction Market Skill
description: >
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.
author: frank
version: 0.1.0
language: en
tags:
- prediction-market
- evm
- contract-call
---
# 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 nonReentrant
```
约束与流程:
- 先调用 `_checkAndExecuteSettlement()`,可能会改变当前轮状态。
- 要求:
- `round > 0 && round <= currentRound`
- `rounds[round].winning != 0`(本轮已结算:YES 赢、NO 赢或平局)
- `rounds[round].poolAmount > 0`
- 调用者在该轮 YES/NO 中至少持有一种代币。
- 分配逻辑:
- 若 `winning == 1`(YES 赢):
- 按 YES 代币在总 YES 供应中的占比分配池中金额。
- 若 `winning == 2`(NO 赢):
- 按 NO 代币在总 NO 供应中的占比分配。
- 若 `winning == 3`(平局/异常):
- 先将调用者持有的 YES/NO 数量相加,按其在总供应量中的占比分配池中金额。
- 所有分配都会从池中扣除 `tokenOut`,并按 `feeRate` 抽取手续费:
- `fee = tokenOut * feeRate / FEE_DENOMINATOR`
- 用户实收 `userAmount = tokenOut - fee`
- 手续费转给 `config.feeRecipient`
- 支持原生币或 ERC20 方式转账。
### 5.4 价格查询与池信息
```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` 返回分数形式的价格(`num/den`),用于前端展示或估算。
- `getPoolInfo` 一次性获取当前轮的池子与价格信息。
- `getRoundInfo` 可查询任意轮的详细数据,包括 YES/NO 代币地址与时间信息。
---
## 6. 在脚本 / 前端中调用示例(ethers.js)
> 示例仅展示调用模式,实际 `FACTORY_ADDRESS` 与市场地址由部署环境决定,请在项目配置中传入。
```ts
import { ethers } from "ethers";
import MarketAbi from "../artifacts/contracts/GouGouBiMarketConfigurable.sol/GouGouBiMarketConfigurable.json";
import FactoryAbi from "../artifacts/contracts/GouGouBiMarketConfigurableFactory.sol/GouGouBiMarketConfigurableFactory.json";
// 由业务方配置
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);
}
// 创建新市场(需要工厂 owner 或白名单地址)
export async function createMarket(
signer: ethers.Signer,
config: {
marketName: string;
uniswapV3Pool: string;
liquidityToken: string; // 原生币市场可传 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,
// 让合约自动推断 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();
// 可以从 MarketCreated 事件中解析新市场地址
return receipt;
}
// 在当前轮买入 YES(ERC20 市场为例)
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();
}
// 在当前轮买入 NO(原生币市场为例)
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();
}
// 赎回某一轮的结算结果
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();
}
```
在前端或自动化脚本中集成时建议:
- **读操作**(`getPoolInfo`、`getRoundInfo`、`priceYes`、`priceNo` 等)使用只读 `provider`。
- **写操作**(`createMarket`、`buyYes`、`buyNo`、`swapYesForNo`、`swapNoForYes`、`redeem`、`resolve`)必须使用具有签名能力的 `signer`。
- ERC20 市场在 `buyYes` / `buyNo` 前要先对市场合约执行一次 `approve`,原生币市场则通过 `value` 附带资金。
- 前端应对 `minYesOut` / `minNoOut` 做合理设置以防滑点过大(例如按预期输出的 95%~99% 设定)。
---
## 7. 与其他 skill 的关系
- 本 `market-configurable-skills` 专注于 **GouGouBi 可配置价格预测市场的具体实现与调用方式**,包括工厂创建、市场配置字段、交易与结算逻辑。
- 若在同一产品中同时使用红包协议或其他合约,可结合对应 skill(如 `giveaway-skills` / `giveaway-protocol`)一起阅读,但它们之间没有直接合约依赖关系,仅在产品层面协同使用。
---
## 8. ABI 信息与精简示例
> 完整 ABI 推荐直接使用编译产物(例如 `artifacts/contracts/GouGouBiMarketConfigurable.sol/GouGouBiMarketConfigurable.json` 和 `...Factory.sol/GouGouBiMarketConfigurableFactory.json` 中的 `abi` 字段)。本节只提供**常用事件与函数的精简片段**,用于快速调试或在无法访问编译产物时临时构造合约实例。
### 8.1 工厂合约 GouGouBiMarketConfigurableFactory ABI 片段
```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 市场合约 GouGouBiMarketConfigurable ABI 片段
```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": []
}
]
```
上述 ABI 片段可以与第 6 节中的 TypeScript 示例直接组合使用,例如:
```ts
import { ethers } from "ethers";
import { factoryFragment, marketFragment } from "./fragments"; // 将上面的 JSON 片段拆分成常量
const factory = new ethers.Contract(FACTORY_ADDRESS, factoryFragment, signerOrProvider);
const market = new ethers.Contract(marketAddress, marketFragment, signerOrProvider);
```
---
## Skill Companion Files
> Additional files collected from the skill directory layout.
### _meta.json
```json
{
"owner": "franckstone",
"slug": "market-configurable-skills",
"displayName": "Market Configurable Skills",
"latest": {
"version": "0.1.0",
"publishedAt": 1772852007416,
"commit": "https://github.com/openclaw/skills/commit/b69d359e016b799d0fa9e0da5337e3444552df30"
},
"history": []
}
```