Market Configurable Skills
Call guide and best practices for the configurable crypto price prediction market contracts GouGouBiMarketConfigurable.sol and GouGouBiMarketConfigurableFact...
Description
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/token1or 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 callcreateMarket.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 addresscreator: creator addressmarketName: market nameuniswapV3Pool: Uniswap V3 pool addressliquidityToken: liquidity token address,address(0)means native coinfeeRecipient: fee recipient addressfeeRate: fee rate (denominator 10000)createdAt: creation timestamp- The following fields align with
GouGouBiMarketConfigurable.MarketConfig(see section 3):tokenDecreverseOrdersettlementIntervalexpiredSecondsinitialReservepriceLookbackSecondsimageUrlrulestimezonelanguagegroupUrltagspredictionTokenanchorTokencurrencyUnit
2.3 Managing creator whitelist
function setCreatorWhitelist(address account, bool allowed) external onlyOwner
function setCreatorWhitelistBatch(address[] calldata accounts, bool allowed) external onlyOwner
- Only the factory
ownercan call these. - Must ensure
account != address(0).
2.4 Creating a market: createMarket
function createMarket(GouGouBiMarketConfigurable.MarketConfig memory _config)
external
returns (address market)
Constraints:
- Only
owneror an address withisWhitelistedCreator[msg.sender] == truecan create:require(msg.sender == owner || isWhitelistedCreator[msg.sender], "NOT_WHITELISTED");
- Internal flow:
- Use
Clones.clone(marketImplementation)to create a minimal proxy. - Call the new market’s
initialize(_config, msg.sender)to set config and owner. - Push the market address into
markets[], populatemarketRecords, and updatemarketIndex. - Emit
MarketCreatedevent with key configuration info.
- Use
Read-only helpers:
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:
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):
marketNamemust be non-empty:bytes(_config.marketName).length > 0uniswapV3Pool != address(0)settlementInterval > 0expiredSeconds > 00 < initialReserve <= 1_000_000_000feeRate <= FEE_DENOMINATOR (10000)- If
feeRate > 0, thenfeeRecipient != address(0); otherwise it will default to_owner. - If
liquidityToken == address(0):tokenDecis automatically set to 18. - If
liquidityToken != address(0):tokenDec = IERC20(liquidityToken).decimals(), requiringtokenDec <= 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
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
function getAveragePriceFromUniswapV3(uint32 startTime, uint32 endTime)
public
view
returns (uint256 averagePrice)
startTime < endTime, andendTime <= block.timestamp.- Internally uses Uniswap V3
observe+TickMathto compute the average price over the interval, represented with1e18precision. - If
reverseOrder == true, takes the reciprocal of the price (still with1e18precision).
4.3 Settlement and round transitions
_checkAndExecuteSettlement(): called before every trade/redeem/resolve to:- If
block.timestamp > expiredTime: setwinning = 3,endAveragePrice = 0, emitAutoResolveevent, and start a new round. - If the current round has reached
endTimeand not yet expired: call_calculateSettlementByPrice().
- If
_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)
- equal →
- Updates
endTimeandendAveragePrice, emitsAutoResolve, then starts a new round.
- Uses
External callers can trigger the check manually:
function resolve() external nonReentrant {
_checkAndExecuteSettlement();
}
5. Core user interaction methods
5.1 Buying YES / NO
5.1.1 buyYes
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, usemsg.valueas the input amount, requiringmsg.value > 0.
- Ignore
- If using an ERC20:
- Require
tokenIn > 0 && msg.value == 0. - Must first call
IERC20(liquidityToken).approve(market, tokenIn); the contract usessafeTransferFrominternally.
- Require
- If the market uses the native coin (
- Contract logic:
- Add
tokenIntorounds[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>= minYesOutor the transaction reverts (slippage protection). - Use
OutcomeToken(yesToken).mint(msg.sender, totalYesOut)to issue YES tokens.
- Add
5.1.2 buyNo
function buyNo(uint256 tokenIn, uint256 minNoOut) external payable nonReentrant
- Symmetric to
buyYesbut operates on the NO pool, yieldingtotalNoOut, which must be>= minNoOut.
5.2 Position swaps: swapYesForNo / swapNoForYes
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
amountOutbased on the current reservesx,yandamountIn, then checks slippage:swapYesForNo:amountOut = (y * amountIn) / (x + amountIn)swapNoForYes:amountOut = (x * amountIn) / (y + amountIn)
- Burns the input tokens via
burn, mints output tokens viamint, and updatesx,y.
5.3 Settlement redemption: redeem
function redeem(uint256 round) external nonReentrant
Constraints and flow:
- First calls
_checkAndExecuteSettlement(), which may change the current round state. - Requires:
round > 0 && round <= currentRoundrounds[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
tokenOutfrom the pool and applies fees byfeeRate:fee = tokenOut * feeRate / FEE_DENOMINATOR- User receives
userAmount = tokenOut - fee - Fee is transferred to
config.feeRecipient
- Supports both native coin and ERC20 transfers.
- If
5.4 Price queries and pool info
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/priceNoreturn prices as fractions (num/den) for UI display or estimation.getPoolInforeturns current round pool and price information in one call.getRoundInforeturns 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_ADDRESSand market addresses depend on deployment and should be passed via your project config.
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-onlyprovider. - Write operations (
createMarket,buyYes,buyNo,swapYesForNo,swapNoForYes,redeem,resolve) must use asignerwith signing capability. - For ERC20 markets, call
approveon the market contract beforebuyYes/buyNo; for native-coin markets, attach funds viavalue. - Frontends should set
minYesOut/minNoOutwith reasonable slippage protection (e.g. 95%–99% of expected output).
7. Relationship with other skills
- This
market-configurable-skillsfocuses 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
abifields inartifacts/contracts/GouGouBiMarketConfigurable.sol/GouGouBiMarketConfigurable.jsonand...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
[
{
"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
[
{
"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:
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 节):tokenDecreverseOrdersettlementIntervalexpiredSecondsinitialReservepriceLookbackSecondsimageUrlrulestimezonelanguagegroupUrltagspredictionTokenanchorTokencurrencyUnit
2.3 管理白名单
function setCreatorWhitelist(address account, bool allowed) external onlyOwner
function setCreatorWhitelistBatch(address[] calldata accounts, bool allowed) external onlyOwner
- 只有工厂
owner可调用。 - 需保证
account != address(0)。
2.4 创建市场 createMarket
function createMarket(GouGouBiMarketConfigurable.MarketConfig memory _config)
external
returns (address market)
约束:
- 仅
owner或isWhitelistedCreator[msg.sender] == true的地址可创建:require(msg.sender == owner || isWhitelistedCreator[msg.sender], "NOT_WHITELISTED");
- 内部流程:
- 使用
Clones.clone(marketImplementation)创建最小代理。 - 调用新市场的
initialize(_config, msg.sender),设置配置与 owner。 - 将市场地址加入
markets[],填充marketRecords,更新marketIndex。 - 触发
MarketCreated事件,记录关键配置信息。
- 使用
读接口:
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 完全一致:
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 > 0uniswapV3Pool != address(0)settlementInterval > 0expiredSeconds > 00 < initialReserve <= 1_000_000_000feeRate <= 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 结构
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
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,然后开启新一轮。
- 通过
外部可主动触发检查:
function resolve() external nonReentrant {
_checkAndExecuteSettlement();
}
5. 用户交互核心方法
5.1 买入 YES / NO
5.1.1 buyYes
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
function buyNo(uint256 tokenIn, uint256 minNoOut) external payable nonReentrant
- 与
buyYes对称,只是对 NO 池进行操作,最终得到totalNoOut,需要满足>= minNoOut。
5.2 头寸互换:swapYesForNo / swapNoForYes
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
function redeem(uint256 round) external nonReentrant
约束与流程:
- 先调用
_checkAndExecuteSettlement(),可能会改变当前轮状态。 - 要求:
round > 0 && round <= currentRoundrounds[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 价格查询与池信息
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与市场地址由部署环境决定,请在项目配置中传入。
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 片段
[
{
"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 片段
[
{
"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 示例直接组合使用,例如:
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);
Reviews (0)
No reviews yet. Be the first to review!
Comments (0)
No comments yet. Be the first to share your thoughts!