diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..7a0e4c0 Binary files /dev/null and b/.DS_Store differ diff --git a/foundry.toml b/foundry.toml index 2d89352..0410de1 100644 --- a/foundry.toml +++ b/foundry.toml @@ -6,6 +6,14 @@ via_ir = true optimizer = true optimizer_runs = 200 +# RPC endpoints +[rpc_endpoints] +unichain-sepolia = "https://sepolia.unichain.org" +base-sepolia = "${BASE_SEPOLIA_RPC}" +base = "${BASE_RPC}" + # Etherscan API configuration for contract verification [etherscan] unichain-sepolia = { key = "${ETHERSCAN_API_KEY}", url = "https://api-sepolia.uniscan.xyz/api" } +base-sepolia = { key = "${BASESCAN_API_KEY}", url = "https://api-sepolia.basescan.org/api" } +base = { key = "${BASESCAN_API_KEY}", url = "https://api.basescan.org/api" } diff --git a/script/BaseScript.s.sol b/script/BaseScript.s.sol index a4d9a9f..90c8568 100644 --- a/script/BaseScript.s.sol +++ b/script/BaseScript.s.sol @@ -11,7 +11,7 @@ import {console2, Script} from "forge-std/Script.sol"; contract BaseScript is Script { function setUp() public virtual { - vm.createSelectFork("https://sepolia.unichain.org"); + vm.createSelectFork("https://mainnet.base.org"); uint256 deployerPrivateKey = vm.envUint("PRI_KEY"); vm.startBroadcast(deployerPrivateKey); } diff --git a/script/base/00_DeployAll.s.sol b/script/base/00_DeployAll.s.sol new file mode 100644 index 0000000..4ecf737 --- /dev/null +++ b/script/base/00_DeployAll.s.sol @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {console2, Script} from "forge-std/Script.sol"; +import {CTFExchange} from "src/exchange/CTFExchange.sol"; +import {ConditionalTokens} from "src/token/ConditionalTokens.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +// ───────────────────────────────────────────────────────────── +// Inline MockUSDC — open mint, 6 decimals (testnet only!) +// ───────────────────────────────────────────────────────────── +contract MockUSDC is ERC20 { + uint8 private immutable _dec; + + constructor() ERC20("Mock USDC", "mUSDC") { + _dec = 6; + } + + function decimals() public view override returns (uint8) { return _dec; } + + /// @notice Anyone can mint on testnet + function mint(address to, uint256 amount) external { + _mint(to, amount); + } +} + +// ───────────────────────────────────────────────────────────── +/** + * @title DeployAll + * @notice One-shot script: deploys MockUSDC + ConditionalTokens + CTFExchange, + * prepares a prediction market, and funds maker/taker accounts. + * + * Usage: + * forge script script/base/00_DeployAll.s.sol:DeployAll \ + * --rpc-url $BASE_SEPOLIA_RPC \ + * --broadcast \ + * --private-key $PRI_KEY \ + * -vvvv + * + * After running, add the printed values to your .env and then run: + * bash script/base/demo_loop.sh + */ +contract DeployAll is Script { + // ── Config ────────────────────────────────────────────── + uint256 constant FUND_AMOUNT = 10_000 * 1e6; // 10,000 mock USDC + uint256 constant SPLIT_AMOUNT = 5_000 * 1e6; // 5,000 split into YES/NO tokens + + // ── State (set during run) ─────────────────────────────── + address public collateral; + address public ctf; + address public exchange; + bytes32 public conditionId; + uint256 public tokenIdYes; + uint256 public tokenIdNo; + + // Keys + uint256 public deployerKey; + uint256 public makerKey; + uint256 public takerKey; + address public deployer; + address public maker; + address public taker; + + function run() external { + // Load keys + deployerKey = vm.envUint("PRI_KEY"); + makerKey = vm.envOr("MAKER_PRIVATE_KEY", deployerKey); + takerKey = vm.envOr("TAKER_PRIVATE_KEY", deployerKey); + deployer = vm.addr(deployerKey); + maker = vm.addr(makerKey); + taker = vm.addr(takerKey); + + console2.log("\n====================================================="); + console2.log(" Base Sepolia - Prediction Market - Full Deploy"); + console2.log("====================================================="); + console2.log("Chain ID :", block.chainid); + console2.log("Deployer :", deployer); + console2.log("Maker :", maker); + console2.log("Taker :", taker); + + // ── Step 1: Deploy contracts (as deployer) ─────────── + vm.startBroadcast(deployerKey); + + collateral = address(new MockUSDC()); + console2.log("\n[1] MockUSDC :", collateral); + + ctf = address(new ConditionalTokens()); + console2.log("[2] ConditionalTokens:", ctf); + + exchange = address(new CTFExchange(collateral, ctf, address(0), address(0))); + console2.log("[3] CTFExchange :", exchange); + + // Add operator (deployer == matching engine) + CTFExchange(exchange).addOperator(deployer); + console2.log("[4] Operator added :", deployer); + + // Prepare market: "Will ETH reach $10k in 2026?" + bytes32 questionId = keccak256(abi.encodePacked("Will ETH reach $10k in 2026?", uint256(block.chainid))); + ConditionalTokens(ctf).prepareCondition(deployer, questionId, 2); + conditionId = ConditionalTokens(ctf).getConditionId(deployer, questionId, 2); + console2.log("[5] Condition ID :", vm.toString(conditionId)); + + // Compute YES / NO token IDs + bytes32 colIdYes = ConditionalTokens(ctf).getCollectionId(bytes32(0), conditionId, 1); + bytes32 colIdNo = ConditionalTokens(ctf).getCollectionId(bytes32(0), conditionId, 2); + tokenIdYes = ConditionalTokens(ctf).getPositionId(IERC20(collateral), colIdYes); + tokenIdNo = ConditionalTokens(ctf).getPositionId(IERC20(collateral), colIdNo); + console2.log("[6] tokenIdYes :", tokenIdYes); + console2.log(" tokenIdNo :", tokenIdNo); + + // Register tokens on exchange + CTFExchange(exchange).registerToken(tokenIdYes, tokenIdNo, conditionId); + console2.log("[7] Tokens registered on exchange"); + + // Mint mock USDC to maker and taker + MockUSDC(collateral).mint(maker, FUND_AMOUNT); + MockUSDC(collateral).mint(taker, FUND_AMOUNT); + console2.log("[8] Minted", FUND_AMOUNT / 1e6, "mUSDC to maker & taker"); + + vm.stopBroadcast(); + + // ── Step 2: Maker splits USDC -> YES+NO tokens ─────── + vm.startBroadcast(makerKey); + IERC20(collateral).approve(ctf, SPLIT_AMOUNT); + uint256[] memory partition = new uint256[](2); + partition[0] = 1; partition[1] = 2; + ConditionalTokens(ctf).splitPosition( + IERC20(collateral), bytes32(0), conditionId, partition, SPLIT_AMOUNT + ); + // Approve exchange to move maker's outcome tokens + ConditionalTokens(ctf).setApprovalForAll(exchange, true); + vm.stopBroadcast(); + console2.log("[9] Maker split USDC -> YES/NO and approved exchange"); + + // ── Step 3: Taker approves exchange to spend USDC ─── + vm.startBroadcast(takerKey); + IERC20(collateral).approve(exchange, FUND_AMOUNT); + vm.stopBroadcast(); + console2.log("[10] Taker approved exchange for USDC"); + + // ── Print env summary ──────────────────────────────── + console2.log("\n====================================================="); + console2.log(" COPY INTO YOUR .env FILE"); + console2.log("====================================================="); + console2.log("CTF_EXCHANGE=%s", exchange); + console2.log("CTF=%s", ctf); + console2.log("COLLATERAL=%s", collateral); + console2.log("TOKEN_ID_YES=%s", vm.toString(bytes32(tokenIdYes))); + console2.log("TOKEN_ID_NO=%s", vm.toString(bytes32(tokenIdNo))); + console2.log("CONDITION_ID=%s", vm.toString(conditionId)); + console2.log("====================================================="); + console2.log("Next step: bash script/base/demo_loop.sh\n"); + } +} diff --git a/script/base/01_MatchOnce.s.sol b/script/base/01_MatchOnce.s.sol new file mode 100644 index 0000000..8c26bfb --- /dev/null +++ b/script/base/01_MatchOnce.s.sol @@ -0,0 +1,261 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {console2, Script} from "forge-std/Script.sol"; +import {CTFExchange} from "src/exchange/CTFExchange.sol"; +import {ConditionalTokens} from "src/token/ConditionalTokens.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC1155} from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; +import {Order, Side, SignatureType, ORDER_TYPEHASH} from "src/exchange/libraries/OrderStructs.sol"; +import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; + +/** + * @title MatchOnce — Base Sepolia Hackathon Demo + * @notice Sends ONE `matchOrders` transaction to CTFExchange on Base Sepolia. + * Auto-refills maker/taker balances via MockUSDC mint() when running low. + * Called in a loop by demo_loop.sh to simulate continuous matching. + * + * Prerequisites (run 00_DeployAll.s.sol first, then set .env): + * CTF_EXCHANGE, COLLATERAL, TOKEN_ID_YES, CONDITION_ID + * PRI_KEY (operator/deployer) + * MAKER_PRIVATE_KEY, TAKER_PRIVATE_KEY + * + * Single manual run: + * forge script script/base/01_MatchOnce.s.sol:MatchOnce \ + * --rpc-url $BASE_SEPOLIA_RPC \ + * --broadcast \ + * --private-key $PRI_KEY \ + * -vv + * + * Loop (see demo_loop.sh): + * bash script/base/demo_loop.sh + */ +contract MatchOnce is Script { + // ── Exchange params (read from env) ────────────────────── + address public exchange; + uint256 public tokenIdYes; + + // ── Accounts ───────────────────────────────────────────── + uint256 public deployerKey; + uint256 public makerKey; + uint256 public takerKey; + address public maker; + address public taker; + + // ── Order size (realistic small fills for demo) ────────── + /// Sell 100 YES tokens at 0.60 USDC each + uint256 constant FILL_TOKENS = 100 * 1e6; // 100 tokens (1e6 precision) + uint256 constant FILL_USDC = 60 * 1e6; // 60 USDC (6 decimals) + + /// Refill threshold / amount — restock when balance drops below FILL_* * 10 + uint256 constant REFILL_AMOUNT = 10_000 * 1e6; + + // ── Nonce counter (incremented each run via env) ───────── + uint256 public runCounter; + + function run() external { + // ── Read environment ────────────────────────────────── + exchange = vm.envAddress("CTF_EXCHANGE"); + tokenIdYes = vm.envUint("TOKEN_ID_YES"); + deployerKey = vm.envUint("PRI_KEY"); + makerKey = vm.envOr("MAKER_PRIVATE_KEY", deployerKey); + takerKey = vm.envOr("TAKER_PRIVATE_KEY", deployerKey); + maker = vm.addr(makerKey); + taker = vm.addr(takerKey); + runCounter = vm.envOr("RUN_COUNTER", uint256(0)); + + // ── Resolve collateral / CTF from exchange ──────────── + CTFExchange exchangeContract = CTFExchange(exchange); + address collateral = exchangeContract.getCollateral(); + address ctf = exchangeContract.getCtf(); + bytes32 conditionId = vm.envBytes32("CONDITION_ID"); + + // ── Read on-chain nonces ────────────────────────────── + uint256 makerNonce = exchangeContract.nonces(maker); + uint256 takerNonce = exchangeContract.nonces(taker); + + // ── Pre-flight checks + auto-refill ────────────────── + _ensureTakerReady(collateral, exchange, takerKey, taker); + _ensureMakerReady(collateral, ctf, exchange, conditionId, makerKey, maker, tokenIdYes); + + console2.log("\n[MatchOnce] Block #", block.number, " | Run #", runCounter); + console2.log(" Exchange :", exchange); + console2.log(" Maker :", maker); + console2.log(" Taker :", taker); + console2.log(" tokenIdYes:", tokenIdYes); + console2.log(" Price : 0.60 USDC / YES token"); + console2.log(" Fill size :", FILL_TOKENS / 1e6, "tokens"); + + // ── Build maker order (SELL YES tokens) ────────────── + Order memory makerOrder = Order({ + salt: uint256(keccak256(abi.encodePacked(block.timestamp, maker, runCounter, "maker"))), + maker: maker, + signer: maker, + taker: address(0), + tokenId: tokenIdYes, + makerAmount: FILL_TOKENS, // sell 100 YES tokens + takerAmount: FILL_USDC, // receive 60 USDC + expiration: block.timestamp + 1 hours, + nonce: makerNonce, + feeRateBps: 0, + side: Side.SELL, + signatureType: SignatureType.EOA, + signature: "" + }); + + // ── Build taker order (BUY YES tokens) ─────────────── + Order memory takerOrder = Order({ + salt: uint256(keccak256(abi.encodePacked(block.timestamp, taker, runCounter, "taker"))), + maker: taker, + signer: taker, + taker: address(0), + tokenId: tokenIdYes, + makerAmount: FILL_USDC, // pay 60 USDC + takerAmount: FILL_TOKENS, // receive 100 YES tokens + expiration: block.timestamp + 1 hours, + nonce: takerNonce, + feeRateBps: 0, + side: Side.BUY, + signatureType: SignatureType.EOA, + signature: "" + }); + + // ── Sign orders (EIP-712) ───────────────────────────── + bytes32 makerHash = _hashTypedDataV4(_hashOrder(makerOrder)); + bytes32 takerHash = _hashTypedDataV4(_hashOrder(takerOrder)); + + makerOrder.signature = _sign(makerKey, makerHash); + takerOrder.signature = _sign(takerKey, takerHash); + + // ── Broadcast matchOrders as operator ──────────────── + Order[] memory makerOrders = new Order[](1); + uint256[] memory makerFillAmounts = new uint256[](1); + makerOrders[0] = makerOrder; + makerFillAmounts[0] = FILL_TOKENS; + + vm.startBroadcast(deployerKey); + exchangeContract.matchOrders(takerOrder, makerOrders, FILL_USDC, makerFillAmounts); + vm.stopBroadcast(); + + console2.log(" >> matchOrders() SUCCESS"); + } + + // ───────────────────────────────────────────────────────── + // Auto-refill helpers + // ───────────────────────────────────────────────────────── + + /** + * @dev Ensures taker has enough USDC and has approved the exchange. + * Calls MockUSDC.mint() if balance is insufficient (testnet only). + */ + function _ensureTakerReady( + address collateral, + address exch, + uint256 takerPk, + address takerAddr + ) internal { + uint256 bal = IERC20(collateral).balanceOf(takerAddr); + uint256 allw = IERC20(collateral).allowance(takerAddr, exch); + + if (bal < FILL_USDC) { + console2.log(" [auto] taker USDC low, minting", REFILL_AMOUNT); + vm.startBroadcast(takerPk); // mint to self; MockUSDC allows any caller + (bool ok,) = collateral.call( + abi.encodeWithSignature("mint(address,uint256)", takerAddr, REFILL_AMOUNT) + ); + vm.stopBroadcast(); + require(ok, "mint() failed - is COLLATERAL a MockUSDC?"); + } + + if (allw < FILL_USDC) { + console2.log(" [auto] taker allowance low, approving exchange"); + vm.startBroadcast(takerPk); + IERC20(collateral).approve(exch, type(uint256).max); + vm.stopBroadcast(); + } + } + + /** + * @dev Ensures maker has enough YES tokens and CTF approval. + * Mints MockUSDC and splits into YES/NO if YES balance is insufficient. + */ + function _ensureMakerReady( + address collateral, + address ctf, + address exch, + bytes32 conditionId, + uint256 makerPk, + address makerAddr, + uint256 yesId + ) internal { + uint256 yesBal = IERC1155(ctf).balanceOf(makerAddr, yesId); + bool approved = IERC1155(ctf).isApprovedForAll(makerAddr, exch); + + if (yesBal < FILL_TOKENS) { + console2.log(" [auto] maker YES low, minting + splitting", REFILL_AMOUNT); + + // 1. Mint MockUSDC to maker + vm.startBroadcast(makerPk); + (bool ok,) = collateral.call( + abi.encodeWithSignature("mint(address,uint256)", makerAddr, REFILL_AMOUNT) + ); + require(ok, "mint() failed - is COLLATERAL a MockUSDC?"); + + // 2. Approve CTF to spend USDC + IERC20(collateral).approve(ctf, REFILL_AMOUNT); + + // 3. Split USDC -> YES + NO tokens + uint256[] memory partition = new uint256[](2); + partition[0] = 1; // YES + partition[1] = 2; // NO + ConditionalTokens(ctf).splitPosition( + IERC20(collateral), bytes32(0), conditionId, partition, REFILL_AMOUNT + ); + vm.stopBroadcast(); + } + + if (!approved) { + console2.log(" [auto] maker CTF not approved, approving exchange"); + vm.startBroadcast(makerPk); + IERC1155(ctf).setApprovalForAll(exch, true); + vm.stopBroadcast(); + } + } + + // ── Helpers ─────────────────────────────────────────────── + + function _sign(uint256 privateKey, bytes32 finalHash) + internal + pure + returns (bytes memory sig) + { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, finalHash); + sig = abi.encodePacked(r, s, v); + } + + function _hashTypedDataV4(bytes32 structHash) internal view returns (bytes32) { + return MessageHashUtils.toTypedDataHash(_buildDomainSeparator(), structHash); + } + + function _buildDomainSeparator() internal view returns (bytes32) { + return keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes("Polymarket CTF Exchange")), + keccak256(bytes("1")), + block.chainid, + exchange + ) + ); + } + + function _hashOrder(Order memory o) internal pure returns (bytes32) { + return keccak256(abi.encode( + ORDER_TYPEHASH, + o.salt, o.maker, o.signer, o.taker, + o.tokenId, o.makerAmount, o.takerAmount, + o.expiration, o.nonce, o.feeRateBps, + uint8(o.side), uint8(o.signatureType) + )); + } +} diff --git a/script/base/02_DeployBase.s.sol b/script/base/02_DeployBase.s.sol new file mode 100644 index 0000000..8434946 --- /dev/null +++ b/script/base/02_DeployBase.s.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {console2, Script} from "forge-std/Script.sol"; +import {CTFExchange} from "src/exchange/CTFExchange.sol"; +import {ConditionalTokens} from "src/token/ConditionalTokens.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/** + * @title DeployBase + * @notice Deploy ConditionalTokens + CTFExchange on Base Mainnet using real USDC. + * Does NOT split positions — run 03_PrepareMarket.s.sol after this. + * + * Prerequisites: + * PRI_KEY — deployer (also becomes the operator / matching engine) + * + * Usage: + * # Dry-run (no broadcast) + * forge script script/base/02_DeployBase.s.sol:DeployBase \ + * --rpc-url $BASE_RPC \ + * --private-key $PRI_KEY \ + * -vvvv + * + * # Broadcast + verify + * forge script script/base/02_DeployBase.s.sol:DeployBase \ + * --rpc-url $BASE_RPC \ + * --broadcast \ + * --verify \ + * --private-key $PRI_KEY \ + * -vvvv + * + * After running, copy the printed env vars to .env, then run: + * forge script script/base/03_PrepareMarket.s.sol:PrepareMarket ... + * bash script/base/demo_loop.sh + */ +contract DeployBase is Script { + // ── Base Mainnet — real USDC (Circle native) ───────────── + address constant USDC = 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913; + + // ── State (populated during run) ───────────────────────── + address public ctf; + address public exchange; + bytes32 public conditionId; + uint256 public tokenIdYes; + uint256 public tokenIdNo; + + function run() external { + uint256 deployerKey = vm.envUint("PRI_KEY"); + address deployer = vm.addr(deployerKey); + + console2.log("\n====================================================="); + console2.log(" Base Mainnet - Prediction Market - Deploy"); + console2.log("====================================================="); + console2.log("Chain ID :", block.chainid); + console2.log("Deployer :", deployer); + console2.log("Collateral:", USDC); + + vm.startBroadcast(deployerKey); + + // 1. Deploy ConditionalTokens + ctf = address(new ConditionalTokens()); + console2.log("\n[1] ConditionalTokens:", ctf); + + // 2. Deploy CTFExchange (proxyFactory=0, safeFactory=0) + exchange = address(new CTFExchange(USDC, ctf, address(0), address(0))); + console2.log("[2] CTFExchange :", exchange); + + // 3. Add deployer as operator (matching engine) + CTFExchange(exchange).addOperator(deployer); + console2.log("[3] Operator added :", deployer); + + // 4. Prepare prediction market condition + // "Will ETH reach $10k in 2026?" — oracle = deployer for demo + bytes32 questionId = keccak256( + abi.encodePacked("Will ETH reach $10k in 2026?", uint256(block.chainid)) + ); + ConditionalTokens(ctf).prepareCondition(deployer, questionId, 2); + conditionId = ConditionalTokens(ctf).getConditionId(deployer, questionId, 2); + console2.log("[4] conditionId :", vm.toString(conditionId)); + + // 5. Compute YES / NO position IDs + bytes32 colIdYes = ConditionalTokens(ctf).getCollectionId(bytes32(0), conditionId, 1); + bytes32 colIdNo = ConditionalTokens(ctf).getCollectionId(bytes32(0), conditionId, 2); + tokenIdYes = ConditionalTokens(ctf).getPositionId(IERC20(USDC), colIdYes); + tokenIdNo = ConditionalTokens(ctf).getPositionId(IERC20(USDC), colIdNo); + console2.log("[5] tokenIdYes :", tokenIdYes); + console2.log(" tokenIdNo :", tokenIdNo); + + // 6. Register YES/NO token pair on exchange + CTFExchange(exchange).registerToken(tokenIdYes, tokenIdNo, conditionId); + console2.log("[6] Tokens registered on exchange"); + + vm.stopBroadcast(); + + // ── Print env summary ──────────────────────────────── + console2.log("\n====================================================="); + console2.log(" COPY INTO YOUR .env FILE"); + console2.log("====================================================="); + console2.log("CTF_EXCHANGE=%s", exchange); + console2.log("CTF=%s", ctf); + console2.log("COLLATERAL=%s", USDC); + console2.log("TOKEN_ID_YES=%s", vm.toString(bytes32(tokenIdYes))); + console2.log("TOKEN_ID_NO=%s", vm.toString(bytes32(tokenIdNo))); + console2.log("CONDITION_ID=%s", vm.toString(conditionId)); + console2.log("====================================================="); + console2.log("Next step: forge script script/base/03_PrepareMarket.s.sol:PrepareMarket"); + } +} diff --git a/script/base/03_PrepareMarket.s.sol b/script/base/03_PrepareMarket.s.sol new file mode 100644 index 0000000..fd9f708 --- /dev/null +++ b/script/base/03_PrepareMarket.s.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {console2, Script} from "forge-std/Script.sol"; +import {CTFExchange} from "src/exchange/CTFExchange.sol"; +import {ConditionalTokens} from "src/token/ConditionalTokens.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/** + * @title PrepareMarket + * @notice One-time setup: maker splits USDC -> YES/NO tokens, both sides approve exchange. + * Run this ONCE after 02_DeployBase.s.sol. + * + * Prerequisites (.env must contain): + * PRI_KEY — deployer / operator + * MAKER_PRIVATE_KEY — maker account (needs real USDC on Base mainnet) + * TAKER_PRIVATE_KEY — taker account (needs real USDC on Base mainnet) + * CTF_EXCHANGE — from 02_DeployBase output + * CTF — from 02_DeployBase output + * COLLATERAL — 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 + * CONDITION_ID — from 02_DeployBase output + * + * Usage: + * forge script script/base/03_PrepareMarket.s.sol:PrepareMarket \ + * --rpc-url $BASE_RPC \ + * --broadcast \ + * --private-key $PRI_KEY \ + * -vvvv + */ +contract PrepareMarket is Script { + // Amount of USDC to split into YES+NO tokens for the maker + // 10 USDC = 10 * 1e6 (USDC has 6 decimals) + uint256 constant SPLIT_AMOUNT = 10 * 1e6; + + // How much USDC the taker pre-approves exchange to spend + uint256 constant TAKER_APPROVE = 1000 * 1e6; + + function run() external { + // ── Read env ────────────────────────────────────────── + address exchange = vm.envAddress("CTF_EXCHANGE"); + address ctf = vm.envAddress("CTF"); + address collateral = vm.envAddress("COLLATERAL"); + bytes32 conditionId = vm.envBytes32("CONDITION_ID"); + + uint256 makerKey = vm.envOr("MAKER_PRIVATE_KEY", vm.envUint("PRI_KEY")); + uint256 takerKey = vm.envOr("TAKER_PRIVATE_KEY", vm.envUint("PRI_KEY")); + address maker = vm.addr(makerKey); + address taker = vm.addr(takerKey); + + console2.log("\n====================================================="); + console2.log(" Base Mainnet - Prepare Market"); + console2.log("====================================================="); + console2.log("Exchange :", exchange); + console2.log("CTF :", ctf); + console2.log("Collateral :", collateral); + console2.log("Maker :", maker); + console2.log("Taker :", taker); + + // ── Maker: check balance ────────────────────────────── + uint256 makerUsdc = IERC20(collateral).balanceOf(maker); + console2.log("\nMaker USDC balance:", makerUsdc / 1e6, "USDC"); + require(makerUsdc >= SPLIT_AMOUNT, "Maker needs at least 10 USDC on Base mainnet"); + + // ── Maker: approve CTF to spend USDC, then splitPosition ─ + vm.startBroadcast(makerKey); + + IERC20(collateral).approve(ctf, SPLIT_AMOUNT); + console2.log("[1] Maker approved CTF to spend", SPLIT_AMOUNT / 1e6, "USDC"); + + uint256[] memory partition = new uint256[](2); + partition[0] = 1; // YES (index set bit 0) + partition[1] = 2; // NO (index set bit 1) + ConditionalTokens(ctf).splitPosition( + IERC20(collateral), bytes32(0), conditionId, partition, SPLIT_AMOUNT + ); + console2.log("[2] Maker split", SPLIT_AMOUNT / 1e6, "USDC -> YES/NO tokens"); + + // Maker approves exchange to move outcome tokens + ConditionalTokens(ctf).setApprovalForAll(exchange, true); + console2.log("[3] Maker approved exchange for all CTF tokens"); + + vm.stopBroadcast(); + + // ── Taker: approve exchange to spend USDC ───────────── + // (skip if maker == taker, i.e. same PRI_KEY used for both) + if (maker != taker) { + uint256 takerUsdc = IERC20(collateral).balanceOf(taker); + console2.log("\nTaker USDC balance:", takerUsdc / 1e6, "USDC"); + require(takerUsdc > 0, "Taker needs some USDC on Base mainnet"); + + vm.startBroadcast(takerKey); + uint256 approveAmt = takerUsdc < TAKER_APPROVE ? takerUsdc : TAKER_APPROVE; + IERC20(collateral).approve(exchange, approveAmt); + console2.log("[4] Taker approved exchange for", approveAmt / 1e6, "USDC"); + vm.stopBroadcast(); + } else { + // Same account for maker & taker — also approve USDC for the taker side + vm.startBroadcast(makerKey); + IERC20(collateral).approve(exchange, TAKER_APPROVE); + console2.log("[4] Maker/Taker (same acct) approved exchange for USDC"); + vm.stopBroadcast(); + } + + console2.log("\n[SUCCESS] Market is ready for matchOrders!"); + console2.log("Next step: bash script/base/demo_loop.sh"); + } +} diff --git a/script/base/demo_loop.sh b/script/base/demo_loop.sh new file mode 100644 index 0000000..70c4dd8 --- /dev/null +++ b/script/base/demo_loop.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +# ============================================================= +# demo_loop.sh — Base Mainnet Prediction Market Demo +# 发送持续撮合交易,每隔 INTERVAL 秒发一笔 matchOrders 上链 +# +# 前置条件: +# 1. 已在 .env 中设置以下变量 (运行 02_DeployBase.s.sol 后获得): +# PRI_KEY, MAKER_PRIVATE_KEY, TAKER_PRIVATE_KEY +# CTF_EXCHANGE, CTF, COLLATERAL, TOKEN_ID_YES, CONDITION_ID +# BASE_RPC (e.g. https://mainnet.base.org) +# 2. 已安装 Foundry (forge 命令可用) +# +# 运行方式: +# bash script/base/demo_loop.sh +# bash script/base/demo_loop.sh --interval 5 # 每 5 秒一笔 +# bash script/base/demo_loop.sh --count 20 # 只跑 20 笔 +# ============================================================= + +set -euo pipefail + +# ── 加载 .env ───────────────────────────────────────────────── +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" +ENV_FILE="$ROOT_DIR/.env" + +if [[ -f "$ENV_FILE" ]]; then + # shellcheck disable=SC1090 + export $(grep -v '^#' "$ENV_FILE" | xargs) + echo "[demo_loop] Loaded .env from $ENV_FILE" +else + echo "[ERROR] .env not found at $ENV_FILE" + exit 1 +fi + +# ── 参数解析 ────────────────────────────────────────────────── +INTERVAL=10 # seconds between each match tx +MAX_COUNT=0 # 0 = infinite loop + +while [[ $# -gt 0 ]]; do + case "$1" in + --interval|-i) INTERVAL="$2"; shift 2;; + --count|-n) MAX_COUNT="$2"; shift 2;; + *) echo "Unknown arg: $1"; exit 1;; + esac +done + +# ── 验证必需变量 ────────────────────────────────────────────── +REQUIRED_VARS=(PRI_KEY CTF_EXCHANGE TOKEN_ID_YES BASE_RPC) +for var in "${REQUIRED_VARS[@]}"; do + if [[ -z "${!var:-}" ]]; then + echo "[ERROR] Missing required env var: $var" + echo " Run 00_DeployAll.s.sol first and add its output to .env" + exit 1 + fi +done + +# ── 打印 Demo 信息 ──────────────────────────────────────────── +echo "" +echo "╔══════════════════════════════════════════════════════╗" +echo "║ Prediction Market Demo — Base Mainnet ║" +echo "║ Continuous Order Matching Loop ║" +echo "╚══════════════════════════════════════════════════════╝" +echo "" +echo " Exchange : ${CTF_EXCHANGE}" +echo " RPC : ${BASE_RPC}" +echo " Interval : ${INTERVAL}s" +echo " Max count : ${MAX_COUNT} (0 = infinite)" +echo "" +echo " Explorer : https://basescan.org/address/${CTF_EXCHANGE}" +echo "" +echo " Press Ctrl+C to stop" +echo "" + +# ── 主循环 ─────────────────────────────────────────────────── +run=0 +while true; do + run=$((run + 1)) + + if [[ $MAX_COUNT -gt 0 && $run -gt $MAX_COUNT ]]; then + echo "" + echo "[demo_loop] Reached max count ($MAX_COUNT). Stopping." + break + fi + + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "[$(date '+%H:%M:%S')] Run #${run} — Broadcasting matchOrders..." + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + + # Pass run counter so each tx gets a unique salt + RUN_COUNTER=$run forge script \ + "$ROOT_DIR/script/base/01_MatchOnce.s.sol:MatchOnce" \ + --rpc-url "$BASE_RPC" \ + --broadcast \ + --private-key "$PRI_KEY" \ + -vv \ + 2>&1 | tee /tmp/match_run_${run}.log | grep -E "(MatchOnce|matchOrders|SUCCESS|ERROR|error|fail|Block|Run #)" + + echo "" + echo "[$(date '+%H:%M:%S')] ✅ Run #${run} done — sleeping ${INTERVAL}s..." + echo "" + sleep "$INTERVAL" +done diff --git a/script/unichain/05_CTFExchange.s.sol b/script/unichain/05_CTFExchange.s.sol index 79b6308..16effd7 100644 --- a/script/unichain/05_CTFExchange.s.sol +++ b/script/unichain/05_CTFExchange.s.sol @@ -16,7 +16,7 @@ contract DeployCTFExchange is BaseScript { // 1. deploy CTFExchange deployCTF(); - // 2. + } function deployCTF() public { diff --git a/test/CTFExchangeTest.t.sol b/test/CTFExchangeTest.t.sol index 50d8bf1..c9a6f69 100644 --- a/test/CTFExchangeTest.t.sol +++ b/test/CTFExchangeTest.t.sol @@ -23,6 +23,7 @@ contract CTFExchangeTest is Test { bytes32 conditionId; bytes32 collectionId; uint256 tokenId; + uint256 tokenId1; function setUp() public { ctf = new ConditionalTokens(); @@ -38,7 +39,7 @@ contract CTFExchangeTest is Test { tokenId = ctf.getPositionId(IERC20(address(collateral)), collectionId); bytes32 collectionId1 = ctf.getCollectionId(bytes32(0), conditionId, 2); // Outcome 1 - uint256 tokenId1 = ctf.getPositionId(IERC20(address(collateral)), collectionId1); + tokenId1 = ctf.getPositionId(IERC20(address(collateral)), collectionId1); // Register Token Pair exchange.registerToken(tokenId, tokenId1, conditionId); @@ -194,4 +195,92 @@ contract CTFExchangeTest is Test { ctf.splitPosition(IERC20(address(collateral)), bytes32(0), conditionId, partition, mintAmount); vm.stopPrank(); } + + /*////////////////////////////////////////////////////////////// + MATCHING ENGINE INTEGRATION TESTS + //////////////////////////////////////////////////////////////*/ + + function testMatchOrders_Mint() public { + // Taker (Order 1): BUY YES (tokenId) + // Maker (Order 2): BUY NO (tokenId1) + // MatchType.MINT + + uint256 takerAmountCollateral = 60 ether; // Taker pays 60 USDC + uint256 takerWantedShares = 100 ether; // Taker wants 100 YES + + uint256 makerAmountCollateral = 40 ether; // Maker pays 40 USDC + uint256 makerWantedShares = 100 ether; // Maker wants 100 NO + + Order memory takerOrder = Order({ + salt: 10, + maker: taker, + signer: taker, + taker: address(0), + tokenId: tokenId, // YES + makerAmount: takerAmountCollateral, + takerAmount: takerWantedShares, + expiration: block.timestamp + 1000, + nonce: 0, + feeRateBps: 0, + side: Side.BUY, + signatureType: SignatureType.EOA, + signature: "" + }); + + Order memory makerOrder = Order({ + salt: 11, + maker: maker, + signer: maker, + taker: address(0), + tokenId: tokenId1, // NO + makerAmount: makerAmountCollateral, + takerAmount: makerWantedShares, + expiration: block.timestamp + 1000, + nonce: 0, + feeRateBps: 0, + side: Side.BUY, + signatureType: SignatureType.EOA, + signature: "" + }); + + // Sign Taker Order + bytes32 takerHash = exchange.hashOrder(takerOrder); + (uint8 tv, bytes32 tr, bytes32 ts) = vm.sign(takerPk, takerHash); + takerOrder.signature = abi.encodePacked(tr, ts, tv); + + // Sign Maker Order + bytes32 makerHash = exchange.hashOrder(makerOrder); + (uint8 mv, bytes32 mr, bytes32 ms) = vm.sign(makerPk, makerHash); + makerOrder.signature = abi.encodePacked(mr, ms, mv); + + Order[] memory makerOrders = new Order[](1); + makerOrders[0] = makerOrder; + + uint256[] memory makerFillAmounts = new uint256[](1); + makerFillAmounts[0] = makerAmountCollateral; // fill 40 USDC worth of maker order + + uint256 takerCollateralBefore = collateral.balanceOf(taker); + uint256 makerCollateralBefore = collateral.balanceOf(maker); + + // Match Engine acts as Operator + vm.prank(taker); // 'taker' acts as registered operator in setup + exchange.matchOrders( + takerOrder, + makerOrders, + takerAmountCollateral, // fill 60 USDC worth of taker order + makerFillAmounts + ); + + // Verify Balances + // Taker paid 60 USDC, got 100 YES + assertEq(collateral.balanceOf(taker), takerCollateralBefore - 60 ether); + assertEq(ctf.balanceOf(taker, tokenId), 100 ether); + + // Maker paid 40 USDC, got 100 NO + assertEq(collateral.balanceOf(maker), makerCollateralBefore - 40 ether); + assertEq(ctf.balanceOf(maker, tokenId1), 100 ether); + + // Exchange should have 100 USDC locked as collateral for the CTF tokens + assertEq(collateral.balanceOf(address(ctf)), 100 ether); + } }