From e483cf6fd9535f973a53bce0f1ad5593d4420c74 Mon Sep 17 00:00:00 2001 From: RAVI SHANKAR BEJINI <130489622+unspecifiedcoder@users.noreply.github.com> Date: Tue, 7 Apr 2026 22:16:45 +0530 Subject: [PATCH 1/3] Add startup strategy architecture document Comprehensive analysis covering uniqueness assessment, market positioning, billion-dollar angle (protocol + SDK pivot), production architecture, 4-phase execution roadmap, distribution strategy, and risk assessment. --- STRATEGY.md | 245 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100644 STRATEGY.md diff --git a/STRATEGY.md b/STRATEGY.md new file mode 100644 index 0000000..c8d5d13 --- /dev/null +++ b/STRATEGY.md @@ -0,0 +1,245 @@ +# FlashGrid: Startup Strategy Architecture + +## Executive Summary + +FlashGrid is a **state-sharded parallel batch auction engine** on Monad (~6,400 LOC). The core insight — isolating storage per price tick to enable true parallel execution — is a genuine architectural innovation. This document outlines the path from polished hackathon project to category-defining infrastructure. + +**The billion-dollar reframe:** Don't be a prediction market. Be **parallel execution infrastructure** — the protocol + SDK that lets any DeFi team deploy parallel-native matching engines. + +--- + +## 1. Uniqueness Analysis + +### What's Actually Unique + +The storage isolation pattern: mapping each tick to its own `TickState` struct so Monad's parallel execution doesn't hit write conflicts. Most DEX teams on parallel chains are still building traditional AMMs/CLOBs that serialize on shared state. + +**Hidden strengths:** +- The pattern is **generalizable** — any on-chain system with contention hotspots could use tick-sharded state +- The benchmarking infrastructure (200-wallet load test + comparison contract) provides *measurable proof* +- The factory pattern hints at a multi-market platform + +### What's Not Unique + +- Prediction markets exist (Polymarket, Azuro, Overtime) +- Batch auctions exist (CoW Protocol, 1inch Fusion, UniswapX) +- "We're faster on Monad" is a temporary moat +- The settlement logic is simplified — real order books are 100x more complex + +### Dangerous Assumptions + +1. **"Monad will win"** — Entire thesis is Monad-specific +2. **"Parallelism = product"** — Speed is infra advantage, not user-facing feature +3. **"The hard part is the contract"** — The hard part is liquidity bootstrapping and regulatory positioning + +--- + +## 2. Market Positioning + +### Reframe: Parallel-Native DeFi Infrastructure + +Don't compete in "prediction markets." Own the category of **primitives that actually use parallel execution** instead of running legacy code faster. + +### Real Customers + +| Customer | Why They Care | +|----------|---------------| +| **Monad itself** | Needs flagship apps proving parallelism matters | +| **Market makers** | Need infrastructure that doesn't serialize strategies | +| **DeFi protocol teams** | Don't know how to architect for parallelism | +| **Prediction market aggregators** | Need backends for Polymarket-scale on-chain volume | +| **MEV searchers** | Sharded state = new extraction patterns | + +### The Painful Problem at Scale + +Every DeFi protocol on Monad/Sei/MegaETH will hit the same wall: their architecture serializes on shared state, negating parallelism. They need a framework to redesign around. + +--- + +## 3. Billion-Dollar Architecture + +### Protocol + SDK Model (Uniswap Labs playbook) + +``` +1. PROTOCOL LAYER (open, forkable) + - FlashGrid Protocol: state-sharded matching primitives + - Becomes the "ERC-4626 of parallel matching" + +2. SDK/TOOLING LAYER (value capture) + - FlashGrid SDK: deploy parallel-native markets in <100 LOC + - Indexer-as-a-service for sharded state + - Analytics + benchmarking tools + +3. NETWORK EFFECTS + - Shared liquidity across FlashGrid-powered markets + - Cross-market routing + - Composability = lock-in +``` + +--- + +## 4. Production Architecture + +``` +┌─────────────────────────────────────────────────────┐ +│ FlashGrid Protocol │ +├──────────┬──────────┬──────────┬────────────────────┤ +│ Core │ Markets │ Routing │ Settlement │ +│ Engine │ Registry │ Layer │ Engine │ +│ (sharded │ (factory │ (cross- │ (pluggable: │ +│ ticks) │ + meta) │ market) │ pro-rata, CLOB, │ +│ │ │ │ batch auction) │ +├──────────┴──────────┴──────────┴────────────────────┤ +│ Indexer / Event Bus │ +│ PostgreSQL + Redis (not in-memory) │ +├─────────────────────────────────────────────────────┤ +│ SDK / API Layer │ +│ TypeScript SDK │ REST API │ WebSocket Subscriptions│ +├─────────────────────────────────────────────────────┤ +│ Frontend Layer │ +│ Embeddable widgets │ Reference dashboard │ Docs │ +└─────────────────────────────────────────────────────┘ +``` + +### Critical Changes from Current State + +| Current | Production | Why | +|---------|------------|-----| +| In-memory event store | PostgreSQL + Redis | Persistence, horizontal scaling | +| Polling (2s) | WebSocket subscriptions | Real-time UX, lower load | +| Single contract | UUPS upgradeable proxy | Bug fixes without redeployment | +| Hardcoded 20 ticks | Configurable per market | Different markets need different granularity | +| No keepers | Gelato/Chainlink Automation | Permissionless settlement | +| No oracle | Chainlink/Pyth integration | Trustless resolution | +| Mnemonic wallets | Hardware wallet + multisig | Production key management | + +### Security Requirements + +- Formal verification (Halmos/Certora) for matching/settlement +- Explicit `nonReentrant` guards +- MEV protection formalization +- Minimum order sizes (prevent dust order gaming) +- Off-chain API rate limiting + +--- + +## 5. Execution Roadmap + +### v0: Current to MVP (Weeks 1-6) + +**Goal:** Usable prediction market on Monad testnet with real users. + +**Features:** +- Oracle-based market resolution +- Persistent indexer (PostgreSQL) +- WebSocket real-time updates +- Order management (cancel, modify) +- Mobile-responsive dashboard + +**Risks:** Monad testnet instability, oracle integration complexity + +### v1: Product-Market Fit (Months 2-4) + +**Goal:** 1,000 active traders. Prove parallel execution = better UX. + +**Features:** +- Multi-market support (sports, crypto, politics) +- SDK v1: `npm install @flashgrid/sdk` +- Liquidity mining program +- Automated settlement via keepers +- Pluggable settlement strategies + +**Risks:** Liquidity cold-start, Monad mainnet timing, regulatory risk + +### v2: Scale (Months 5-12) + +**Goal:** Default matching infrastructure on Monad. Multi-chain expansion. + +**Features:** +- Formalized, audited FlashGrid Protocol +- Multi-chain (Sei, MegaETH, Aptos) +- Cross-chain liquidity routing +- Institutional API +- Governance token + DAO +- Embeddable widgets + +**Risks:** Multi-chain complexity (3x engineering), governance regulatory scrutiny, forkability + +### v3: Dominance (Year 2+) + +**Goal:** Standard primitive for parallel matching (like Uniswap for AMMs). + +**Features:** +- ERC-standard proposal +- Composable with lending/derivatives +- White-label for TradFi +- Revenue: protocol fees, SDK licensing, managed infra + +**Risks:** Parallel chain adoption uncertainty, TradFi sales cycles, well-funded competitors + +--- + +## 6. Distribution Strategy + +### Phase 1: Ecosystem Alignment +- Monad grants program — become the "reference parallel DeFi app" +- Technical content: publish benchmarks as blog posts and Twitter threads +- Every Monad dev tutorial should reference state-sharding pattern + +### Phase 2: Developer Adoption +- `npx create-flashgrid-market` — trivially easy market deployment +- Hackathon sponsorship with bounties +- Open-source protocol; monetize tooling + +### Phase 3: Trader Acquisition +- High-interest markets (elections, crypto, sports) +- Referral program with fee rebates +- Aggregator integrations + +### Channel Priority + +| Channel | Priority | +|---------|----------| +| Crypto Twitter / technical content | HIGH | +| Monad ecosystem/grants | HIGH | +| Hackathon sponsorship | MEDIUM | +| DeFi aggregator integrations | MEDIUM | +| Conference talks | LOW (early) | + +--- + +## 7. Risk Assessment + +### Why This Might Fail + +1. **Monad dependency is existential** — binary risk that can't be hedged without multi-chain +2. **Parallelism advantage may be invisible to users** — "5x orders/block" ≠ better fills +3. **Liquidity is the only DeFi moat, and there is none** — Polymarket has $500M+ TVL +4. **Prediction markets have severe regulatory risk** — Kalshi spent years on CFTC +5. **Pattern is trivially forkable** — 200-line insight, copyable in a weekend + +### Overestimated + +- Importance of parallelism to end users +- Difficulty of copying the approach +- Monad's timeline and adoption +- Value of benchmarks to traders + +### Underestimated + +- Power of the SDK/protocol play (bigger opportunity than the app) +- Difficulty of liquidity bootstrapping (budget 60% of early effort) +- Regulatory complexity (get legal counsel pre-launch) +- Value of the Monad relationship (reference app = free distribution) + +--- + +## 8. The Strategic Pivot + +**The prediction market is the demo app. The protocol + SDK is the product.** + +This reframing: +- Eliminates prediction market regulatory risk from core business +- Expands TAM from "prediction market users" to "all DeFi on parallel chains" +- Creates defensibility through developer ecosystem +- Aligns with how billion-dollar crypto companies work (Uniswap, Aave, Chainlink — protocols, not apps) From c6bb67dcb2c97e245df725a4bc9ab9e4df64a2d3 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Apr 2026 20:38:50 +0000 Subject: [PATCH 2/3] Production-grade rewrite: fix settlement, add safety, CI, multi-chain, SDK ## Contracts (BREAKING) - Fix broken settlement logic: proper market resolution + winner/loser payouts - Add ReentrancyGuard on withdraw/settlement, Pausable on deposit/orders - Add order cancellation (cancelOrder), market resolution (resolveMarket) - Add MIN_ORDER_SIZE (0.001 ETH) to prevent DoS via dust orders - Losers get unmatched refund, winners get original + pro-rata of losing pool - FlashGridFactory now accepts resolver address for each market ## Tests (800+ LOC) - 45+ test cases including market resolution, payout correctness, edge cases - Fuzz tests: settlement conservation, deposit/withdraw, balance consistency - Reentrancy attack test with malicious contract - Pause/unpause, min order size, order cancellation tests - Stress test: 50 orders per tick settlement ## CI/CD - GitHub Actions workflow: contracts (build, test, fuzz, fmt) + dashboard (typecheck, build) + SDK ## Multi-Chain - New chains.ts config supporting Monad Testnet, Sepolia, Base Sepolia - Refactored wallet.ts, indexer.ts, contract.ts to use dynamic chain config - foundry.toml updated with Sepolia + Base Sepolia RPC endpoints ## SDK (@flashgrid/sdk) - Standalone npm package with types, ABIs, chain config, and FlashGridClient class - Full TypeScript SDK for deploying and interacting with FlashGrid markets https://claude.ai/code/session_01AYqngFhM9R9jAN8zGzLGqk --- .github/workflows/ci.yml | 90 +++++ contracts/foundry.toml | 7 + contracts/script/Deploy.s.sol | 5 +- contracts/src/FlashGrid.sol | 263 ++++++++++--- contracts/src/FlashGridFactory.sol | 32 +- contracts/test/FlashGrid.t.sol | 600 ++++++++++++++++++++++++++--- dashboard/lib/chains.ts | 58 +++ dashboard/lib/contract.ts | 127 +++++- dashboard/lib/indexer.ts | 38 +- dashboard/lib/wallet.ts | 117 +++--- sdk/dist/abis.d.ts | 470 ++++++++++++++++++++++ sdk/dist/abis.d.ts.map | 1 + sdk/dist/abis.js | 82 ++++ sdk/dist/abis.js.map | 1 + sdk/dist/chains.d.ts | 6 + sdk/dist/chains.d.ts.map | 1 + sdk/dist/chains.js | 37 ++ sdk/dist/chains.js.map | 1 + sdk/dist/client.d.ts | 129 +++++++ sdk/dist/client.d.ts.map | 1 + sdk/dist/client.js | 147 +++++++ sdk/dist/client.js.map | 1 + sdk/dist/index.d.ts | 6 + sdk/dist/index.d.ts.map | 1 + sdk/dist/index.js | 6 + sdk/dist/index.js.map | 1 + sdk/dist/types.d.ts | 104 +++++ sdk/dist/types.d.ts.map | 1 + sdk/dist/types.js | 6 + sdk/dist/types.js.map | 1 + sdk/package-lock.json | 258 +++++++++++++ sdk/package.json | 24 ++ sdk/src/abis.ts | 84 ++++ sdk/src/chains.ts | 41 ++ sdk/src/client.ts | 200 ++++++++++ sdk/src/index.ts | 19 + sdk/src/types.ts | 117 ++++++ sdk/tsconfig.json | 18 + 38 files changed, 2872 insertions(+), 229 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 dashboard/lib/chains.ts create mode 100644 sdk/dist/abis.d.ts create mode 100644 sdk/dist/abis.d.ts.map create mode 100644 sdk/dist/abis.js create mode 100644 sdk/dist/abis.js.map create mode 100644 sdk/dist/chains.d.ts create mode 100644 sdk/dist/chains.d.ts.map create mode 100644 sdk/dist/chains.js create mode 100644 sdk/dist/chains.js.map create mode 100644 sdk/dist/client.d.ts create mode 100644 sdk/dist/client.d.ts.map create mode 100644 sdk/dist/client.js create mode 100644 sdk/dist/client.js.map create mode 100644 sdk/dist/index.d.ts create mode 100644 sdk/dist/index.d.ts.map create mode 100644 sdk/dist/index.js create mode 100644 sdk/dist/index.js.map create mode 100644 sdk/dist/types.d.ts create mode 100644 sdk/dist/types.d.ts.map create mode 100644 sdk/dist/types.js create mode 100644 sdk/dist/types.js.map create mode 100644 sdk/package-lock.json create mode 100644 sdk/package.json create mode 100644 sdk/src/abis.ts create mode 100644 sdk/src/chains.ts create mode 100644 sdk/src/client.ts create mode 100644 sdk/src/index.ts create mode 100644 sdk/src/types.ts create mode 100644 sdk/tsconfig.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c184421 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,90 @@ +name: CI + +on: + push: + branches: [main, "feature/**"] + pull_request: + branches: [main] + +jobs: + contracts: + name: Contracts (Build + Test + Fmt) + runs-on: ubuntu-latest + defaults: + run: + working-directory: contracts + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Install dependencies + run: forge install + + - name: Build contracts + run: forge build --sizes + + - name: Run tests + run: forge test -vvv + + - name: Run fuzz tests (extended) + run: forge test --match-test "testFuzz" -vvv --fuzz-runs 500 + + - name: Check formatting + run: forge fmt --check + + dashboard: + name: Dashboard (Lint + Typecheck + Build) + runs-on: ubuntu-latest + defaults: + run: + working-directory: dashboard + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: npm + cache-dependency-path: dashboard/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: TypeScript typecheck + run: npx tsc --noEmit + + - name: Build + run: npm run build + + sdk: + name: SDK (Typecheck + Build) + runs-on: ubuntu-latest + defaults: + run: + working-directory: sdk + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: npm + cache-dependency-path: sdk/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: TypeScript typecheck + run: npx tsc --noEmit + + - name: Build + run: npm run build diff --git a/contracts/foundry.toml b/contracts/foundry.toml index f52b1f1..ec29de2 100644 --- a/contracts/foundry.toml +++ b/contracts/foundry.toml @@ -10,8 +10,15 @@ optimizer_runs = 200 line_length = 120 tab_width = 4 +[profile.default.fuzz] +runs = 256 + [rpc_endpoints] monad_testnet = "https://testnet-rpc.monad.xyz" +sepolia = "${SEPOLIA_RPC_URL}" +base_sepolia = "${BASE_SEPOLIA_RPC_URL}" [etherscan] monad_testnet = { key = "", url = "https://testnet.monadvision.com/api" } +sepolia = { key = "${ETHERSCAN_API_KEY}", url = "https://api-sepolia.etherscan.io/api" } +base_sepolia = { key = "${BASESCAN_API_KEY}", url = "https://api-sepolia.basescan.org/api" } diff --git a/contracts/script/Deploy.s.sol b/contracts/script/Deploy.s.sol index 544a177..e18c881 100644 --- a/contracts/script/Deploy.s.sol +++ b/contracts/script/Deploy.s.sol @@ -8,13 +8,14 @@ import {ParallelBenchmark} from "../src/ParallelBenchmark.sol"; contract DeployScript is Script { function run() external { uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address deployer = vm.addr(deployerPrivateKey); vm.startBroadcast(deployerPrivateKey); // 1. Deploy Factory FlashGridFactory factory = new FlashGridFactory(); console2.log("FlashGridFactory deployed at:", address(factory)); - // 2. Create default market via factory + // 2. Create default market (deployer is resolver) address market = factory.createMarket("Will MON reach $10 by Q2 2025?"); console2.log("FlashGrid market deployed at:", market); @@ -24,10 +25,10 @@ contract DeployScript is Script { vm.stopBroadcast(); - // Output addresses for scripts console2.log("\n=== Deployment Summary ==="); console2.log("Factory: ", address(factory)); console2.log("Market: ", market); + console2.log("Resolver: ", deployer); console2.log("Benchmark: ", address(benchmark)); console2.log("Chain ID: ", block.chainid); } diff --git a/contracts/src/FlashGrid.sol b/contracts/src/FlashGrid.sol index ad10982..4f4b60a 100644 --- a/contracts/src/FlashGrid.sol +++ b/contracts/src/FlashGrid.sol @@ -2,15 +2,43 @@ pragma solidity ^0.8.24; /// @title FlashGrid - Parallel Batch Auction Engine -/// @notice State-sharded order matching engine optimized for Monad's parallel execution -/// @dev Each price tick uses isolated storage slots to enable conflict-free parallel processing +/// @notice State-sharded order matching engine optimized for parallel execution. +/// @dev Each price tick uses isolated storage slots to enable conflict-free parallel processing. +/// Settlement happens after market resolution — winners take the matched pot. contract FlashGrid { + // ═══════════════════════════════════════════════════════════ + // REENTRANCY GUARD + // ═══════════════════════════════════════════════════════════ + + uint256 private constant _NOT_ENTERED = 1; + uint256 private constant _ENTERED = 2; + uint256 private _reentrancyStatus = _NOT_ENTERED; + + modifier nonReentrant() { + require(_reentrancyStatus != _ENTERED, "ReentrancyGuard: reentrant call"); + _reentrancyStatus = _ENTERED; + _; + _reentrancyStatus = _NOT_ENTERED; + } + + // ═══════════════════════════════════════════════════════════ + // PAUSABLE + // ═══════════════════════════════════════════════════════════ + + bool private _paused; + + modifier whenNotPaused() { + require(!_paused, "Pausable: paused"); + _; + } + // ═══════════════════════════════════════════════════════════ // CONSTANTS // ═══════════════════════════════════════════════════════════ - uint8 public constant NUM_TICKS = 20; // Price ticks: 0.05 to 1.00 (step 0.05) - uint32 public constant EPOCH_DURATION = 12; // Blocks per epoch + uint8 public constant NUM_TICKS = 20; + uint32 public constant EPOCH_DURATION = 12; + uint128 public constant MIN_ORDER_SIZE = 0.001 ether; // ═══════════════════════════════════════════════════════════ // TYPES @@ -20,7 +48,7 @@ contract FlashGrid { uint128 totalYesLiquidity; uint128 totalNoLiquidity; uint32 orderCount; - uint32 lastMatchedEpoch; + uint32 lastSettledEpoch; } struct Order { @@ -28,19 +56,21 @@ contract FlashGrid { uint128 amount; bool isYes; uint32 epoch; + bool cancelled; + bool claimed; } // ═══════════════════════════════════════════════════════════ // STORAGE // ═══════════════════════════════════════════════════════════ - /// @notice Isolated tick state - each tick occupies its own storage slot range + /// @notice Isolated tick state — each tick occupies its own storage slot range mapping(uint8 => TickState) public ticks; - /// @notice Orders per tick - isolated arrays, no cross-tick conflicts + /// @notice Orders per tick — isolated arrays, no cross-tick conflicts mapping(uint8 => Order[]) public tickOrders; - /// @notice User balance ledger - only touched on deposit/withdraw + /// @notice User balance ledger — only touched on deposit/withdraw mapping(address => uint256) public balances; /// @notice Current epoch number @@ -51,6 +81,11 @@ contract FlashGrid { address public factory; address public creator; + /// @notice Market resolution state + address public resolver; + bool public resolved; + bool public outcomeYes; + // ═══════════════════════════════════════════════════════════ // EVENTS // ═══════════════════════════════════════════════════════════ @@ -60,10 +95,15 @@ contract FlashGrid { event OrderPlaced( uint8 indexed tick, address indexed maker, uint128 amount, bool isYes, uint32 epoch ); + event OrderCancelled(uint8 indexed tick, address indexed maker, uint256 orderIndex, uint128 amount); event TickSettled( uint8 indexed tick, uint32 epoch, uint128 yesMatched, uint128 noMatched, uint256 clearingPrice ); event EpochCompleted(uint32 epoch, uint256 totalVolume, uint16 ticksActive); + event MarketResolved(bool outcomeYes, address indexed resolvedBy); + event PayoutClaimed(uint8 indexed tick, address indexed maker, uint256 amount); + event Paused(address account); + event Unpaused(address account); // ═══════════════════════════════════════════════════════════ // ERRORS @@ -72,34 +112,68 @@ contract FlashGrid { error InvalidTick(); error InsufficientBalance(); error ZeroAmount(); - error AlreadySettled(); + error OrderTooSmall(); error WithdrawFailed(); + error NotResolver(); + error AlreadyResolved(); + error NotResolved(); + error NotOrderMaker(); + error OrderAlreadyCancelled(); + error OrderAlreadyClaimed(); + error InvalidOrderIndex(); // ═══════════════════════════════════════════════════════════ // CONSTRUCTOR // ═══════════════════════════════════════════════════════════ - constructor(string memory _marketQuestion, address _creator) { + constructor(string memory _marketQuestion, address _creator, address _resolver) { marketQuestion = _marketQuestion; creator = _creator; + resolver = _resolver; factory = msg.sender; currentEpoch = 1; } + // ═══════════════════════════════════════════════════════════ + // MODIFIERS + // ═══════════════════════════════════════════════════════════ + + modifier onlyResolver() { + if (msg.sender != resolver) revert NotResolver(); + _; + } + + // ═══════════════════════════════════════════════════════════ + // ADMIN FUNCTIONS + // ═══════════════════════════════════════════════════════════ + + function pause() external onlyResolver { + _paused = true; + emit Paused(msg.sender); + } + + function unpause() external onlyResolver { + _paused = false; + emit Unpaused(msg.sender); + } + + function paused() external view returns (bool) { + return _paused; + } + // ═══════════════════════════════════════════════════════════ // DEPOSIT / WITHDRAW // ═══════════════════════════════════════════════════════════ - /// @notice Deposit MON to participate in markets - /// @dev Only touches balances[msg.sender] - no conflicts with order placement - function deposit() external payable { + /// @notice Deposit native token to participate in markets + function deposit() external payable whenNotPaused { if (msg.value == 0) revert ZeroAmount(); balances[msg.sender] += msg.value; emit Deposited(msg.sender, msg.value); } /// @notice Withdraw available balance - function withdraw(uint256 amount) external { + function withdraw(uint256 amount) external nonReentrant { if (amount == 0) revert ZeroAmount(); if (balances[msg.sender] < amount) revert InsufficientBalance(); @@ -116,20 +190,17 @@ contract FlashGrid { // ═══════════════════════════════════════════════════════════ /// @notice Place an order at a specific price tick - /// @dev PARALLELISM KEY: Only touches ticks[tick] and tickOrders[tick] - /// Orders at different ticks have ZERO storage conflicts - /// @param tick Price tick index (0-19, representing 0.05 to 1.00) - /// @param amount Order size in wei - /// @param isYes True for YES side, false for NO side - function placeOrder(uint8 tick, uint128 amount, bool isYes) external { + /// @dev PARALLELISM KEY: Only touches ticks[tick] and tickOrders[tick]. + /// Orders at different ticks have ZERO storage conflicts. + function placeOrder(uint8 tick, uint128 amount, bool isYes) external whenNotPaused { if (tick >= NUM_TICKS) revert InvalidTick(); if (amount == 0) revert ZeroAmount(); + if (amount < MIN_ORDER_SIZE) revert OrderTooSmall(); if (balances[msg.sender] < amount) revert InsufficientBalance(); + if (resolved) revert AlreadyResolved(); - // Deduct from balance balances[msg.sender] -= amount; - // Update tick state (isolated storage slot) TickState storage ts = ticks[tick]; if (isYes) { ts.totalYesLiquidity += amount; @@ -138,78 +209,145 @@ contract FlashGrid { } ts.orderCount++; - // Append to tick-specific order array (isolated storage) - tickOrders[tick].push(Order({maker: msg.sender, amount: amount, isYes: isYes, epoch: currentEpoch})); + tickOrders[tick].push(Order({ + maker: msg.sender, + amount: amount, + isYes: isYes, + epoch: currentEpoch, + cancelled: false, + claimed: false + })); emit OrderPlaced(tick, msg.sender, amount, isYes, currentEpoch); } + // ═══════════════════════════════════════════════════════════ + // ORDER CANCELLATION + // ═══════════════════════════════════════════════════════════ + + /// @notice Cancel an open order before market resolution + function cancelOrder(uint8 tick, uint256 orderIndex) external { + if (tick >= NUM_TICKS) revert InvalidTick(); + if (resolved) revert AlreadyResolved(); + + Order[] storage orders = tickOrders[tick]; + if (orderIndex >= orders.length) revert InvalidOrderIndex(); + + Order storage order = orders[orderIndex]; + if (order.maker != msg.sender) revert NotOrderMaker(); + if (order.cancelled) revert OrderAlreadyCancelled(); + + order.cancelled = true; + uint128 refundAmount = order.amount; + + // Update tick state + TickState storage ts = ticks[tick]; + if (order.isYes) { + ts.totalYesLiquidity -= refundAmount; + } else { + ts.totalNoLiquidity -= refundAmount; + } + ts.orderCount--; + + // Refund to balance + balances[msg.sender] += refundAmount; + + emit OrderCancelled(tick, msg.sender, orderIndex, refundAmount); + } + + // ═══════════════════════════════════════════════════════════ + // MARKET RESOLUTION + // ═══════════════════════════════════════════════════════════ + + /// @notice Resolve the market outcome — only callable by resolver + function resolveMarket(bool _outcomeYes) external onlyResolver { + if (resolved) revert AlreadyResolved(); + + resolved = true; + outcomeYes = _outcomeYes; + + emit MarketResolved(_outcomeYes, msg.sender); + } + // ═══════════════════════════════════════════════════════════ // SETTLEMENT // ═══════════════════════════════════════════════════════════ - /// @notice Settle a single tick - matches YES and NO orders - /// @dev PARALLELISM KEY: Each tick settles independently - /// Multiple settleTick() calls for different ticks execute in parallel on Monad - /// @param tick The tick index to settle - function settleTick(uint8 tick) public { + /// @notice Settle a single tick after market resolution + /// @dev PARALLELISM KEY: Each tick settles independently. + /// Winners get their share + the losing side's matched amount. + /// Unmatched orders on the excess side get refunded. + function settleTick(uint8 tick) public nonReentrant { if (tick >= NUM_TICKS) revert InvalidTick(); + if (!resolved) revert NotResolved(); TickState storage ts = ticks[tick]; - if (ts.lastMatchedEpoch >= currentEpoch) revert AlreadySettled(); uint128 yesLiq = ts.totalYesLiquidity; uint128 noLiq = ts.totalNoLiquidity; - - // Match the minimum of YES and NO liquidity uint128 matched = yesLiq < noLiq ? yesLiq : noLiq; - if (matched > 0) { - // Pro-rata settlement: distribute matched amount back to winning side - Order[] storage orders = tickOrders[tick]; - uint256 len = orders.length; + Order[] storage orders = tickOrders[tick]; + uint256 len = orders.length; - for (uint256 i = 0; i < len; i++) { - Order storage order = orders[i]; - if (order.epoch == currentEpoch && order.amount > 0) { - uint128 orderAmt = order.amount; - uint128 pool = order.isYes ? yesLiq : noLiq; + uint128 winPool = outcomeYes ? yesLiq : noLiq; + uint128 losePool = outcomeYes ? noLiq : yesLiq; - // Pro-rata share of matched amount - uint128 share = uint128((uint256(orderAmt) * uint256(matched)) / uint256(pool)); + for (uint256 i = 0; i < len; i++) { + Order storage order = orders[i]; + if (order.cancelled || order.claimed || order.amount == 0) continue; - // Refund unmatched portion + payout - uint128 unmatched = orderAmt - share; - uint128 payout = share * 2; // Winner gets 2x on matched portion + order.claimed = true; + uint128 orderAmt = order.amount; + bool isWinner = (order.isYes == outcomeYes); - balances[order.maker] += uint256(unmatched) + uint256(payout) / 2; - // Note: simplified settlement - in production would track outcomes - order.amount = 0; // Mark as settled + if (isWinner) { + // Winner: gets back full amount + pro-rata share of losing pool + uint128 winnings = 0; + if (winPool > 0 && matched > 0) { + winnings = uint128((uint256(orderAmt) * uint256(losePool)) / uint256(winPool)); + // Cap winnings at matched amount to prevent overflow + if (winnings > matched) winnings = matched; + } + uint256 payout = uint256(orderAmt) + uint256(winnings); + balances[order.maker] += payout; + emit PayoutClaimed(tick, order.maker, payout); + } else { + // Loser: gets back only the unmatched portion + uint128 pool = order.isYes ? yesLiq : noLiq; + uint128 matchedShare = 0; + if (pool > 0) { + matchedShare = uint128((uint256(orderAmt) * uint256(matched)) / uint256(pool)); + } + uint128 unmatched = orderAmt - matchedShare; + if (unmatched > 0) { + balances[order.maker] += uint256(unmatched); + emit PayoutClaimed(tick, order.maker, uint256(unmatched)); } } - - // Emit settlement event - emit TickSettled(tick, currentEpoch, matched, matched, uint256(tick + 1) * 5); } - // Update tick state + // Emit settlement event + uint256 clearingPrice = uint256(tick + 1) * 5; + emit TickSettled(tick, currentEpoch, matched, matched, clearingPrice); + + // Reset tick state ts.totalYesLiquidity = 0; ts.totalNoLiquidity = 0; ts.orderCount = 0; - ts.lastMatchedEpoch = currentEpoch; + ts.lastSettledEpoch = currentEpoch; } /// @notice Settle all ticks and advance epoch - /// @dev On Monad, the internal settleTick calls can execute in parallel - /// since each touches only its own tick's storage function settleAll() external { + if (!resolved) revert NotResolved(); + uint256 totalVolume = 0; uint16 activeTickCount = 0; for (uint8 i = 0; i < NUM_TICKS; i++) { TickState storage ts = ticks[i]; - // Skip ticks with no orders or already settled this epoch - if (ts.orderCount > 0 && ts.lastMatchedEpoch < currentEpoch) { + if (ts.orderCount > 0 && ts.lastSettledEpoch < currentEpoch) { uint128 tickVolume = ts.totalYesLiquidity + ts.totalNoLiquidity; totalVolume += uint256(tickVolume); activeTickCount++; @@ -225,37 +363,34 @@ contract FlashGrid { // VIEW FUNCTIONS // ═══════════════════════════════════════════════════════════ - /// @notice Get tick state for a specific tick function getTickState(uint8 tick) external view - returns (uint128 yesLiquidity, uint128 noLiquidity, uint32 orderCount, uint32 lastMatchedEpoch) + returns (uint128 yesLiquidity, uint128 noLiquidity, uint32 orderCount, uint32 lastSettledEpoch) { if (tick >= NUM_TICKS) revert InvalidTick(); TickState storage ts = ticks[tick]; - return (ts.totalYesLiquidity, ts.totalNoLiquidity, ts.orderCount, ts.lastMatchedEpoch); + return (ts.totalYesLiquidity, ts.totalNoLiquidity, ts.orderCount, ts.lastSettledEpoch); } - /// @notice Get all tick states in one call (for dashboard) function getAllTickStates() external view returns (TickState[20] memory states) { for (uint8 i = 0; i < NUM_TICKS; i++) { states[i] = ticks[i]; } } - /// @notice Get orders for a specific tick function getTickOrders(uint8 tick) external view returns (Order[] memory) { if (tick >= NUM_TICKS) revert InvalidTick(); return tickOrders[tick]; } - /// @notice Get order count for a specific tick function getTickOrderCount(uint8 tick) external view returns (uint256) { if (tick >= NUM_TICKS) revert InvalidTick(); return tickOrders[tick].length; } receive() external payable { + if (msg.value == 0) revert ZeroAmount(); balances[msg.sender] += msg.value; emit Deposited(msg.sender, msg.value); } diff --git a/contracts/src/FlashGridFactory.sol b/contracts/src/FlashGridFactory.sol index f63be60..054813f 100644 --- a/contracts/src/FlashGridFactory.sol +++ b/contracts/src/FlashGridFactory.sol @@ -10,17 +10,14 @@ contract FlashGridFactory { // STORAGE // ═══════════════════════════════════════════════════════════ - /// @notice Registry of all deployed markets address[] public markets; - - /// @notice Mapping from market question hash to deployed address mapping(bytes32 => address) public marketsByHash; // ═══════════════════════════════════════════════════════════ // EVENTS // ═══════════════════════════════════════════════════════════ - event MarketCreated(address indexed market, string question, address indexed creator, uint256 index); + event MarketCreated(address indexed market, string question, address indexed creator, address indexed resolver, uint256 index); // ═══════════════════════════════════════════════════════════ // ERRORS @@ -32,30 +29,43 @@ contract FlashGridFactory { // FUNCTIONS // ═══════════════════════════════════════════════════════════ - /// @notice Deploy a new FlashGrid market - /// @param question The market question (e.g., "Will MON reach $10 by Q2?") + /// @notice Deploy a new FlashGrid market with a dedicated resolver + /// @param question The market question + /// @param _resolver Address authorized to resolve the market outcome /// @return market The address of the deployed FlashGrid contract + function createMarket(string calldata question, address _resolver) external returns (address market) { + bytes32 questionHash = keccak256(abi.encodePacked(question)); + if (marketsByHash[questionHash] != address(0)) revert MarketAlreadyExists(); + + bytes32 salt = questionHash; + FlashGrid grid = new FlashGrid{salt: salt}(question, msg.sender, _resolver); + market = address(grid); + + markets.push(market); + marketsByHash[questionHash] = market; + + emit MarketCreated(market, question, msg.sender, _resolver, markets.length - 1); + } + + /// @notice Deploy a new FlashGrid market (creator is also resolver) function createMarket(string calldata question) external returns (address market) { bytes32 questionHash = keccak256(abi.encodePacked(question)); if (marketsByHash[questionHash] != address(0)) revert MarketAlreadyExists(); - // Deploy with CREATE2 for deterministic addresses bytes32 salt = questionHash; - FlashGrid grid = new FlashGrid{salt: salt}(question, msg.sender); + FlashGrid grid = new FlashGrid{salt: salt}(question, msg.sender, msg.sender); market = address(grid); markets.push(market); marketsByHash[questionHash] = market; - emit MarketCreated(market, question, msg.sender, markets.length - 1); + emit MarketCreated(market, question, msg.sender, msg.sender, markets.length - 1); } - /// @notice Get total number of deployed markets function getMarketCount() external view returns (uint256) { return markets.length; } - /// @notice Get all deployed market addresses function getAllMarkets() external view returns (address[] memory) { return markets; } diff --git a/contracts/test/FlashGrid.t.sol b/contracts/test/FlashGrid.t.sol index ab78ee0..d3d4686 100644 --- a/contracts/test/FlashGrid.t.sol +++ b/contracts/test/FlashGrid.t.sol @@ -14,14 +14,15 @@ contract FlashGridTest is Test { address public alice = makeAddr("alice"); address public bob = makeAddr("bob"); address public charlie = makeAddr("charlie"); + address public resolverAddr; function setUp() public { + resolverAddr = address(this); // test contract is resolver factory = new FlashGridFactory(); address marketAddr = factory.createMarket("Will MON reach $10 by Q2?"); grid = FlashGrid(payable(marketAddr)); benchmark = new ParallelBenchmark(); - // Fund test users vm.deal(alice, 100 ether); vm.deal(bob, 100 ether); vm.deal(charlie, 100 ether); @@ -34,7 +35,6 @@ contract FlashGridTest is Test { function test_Deposit() public { vm.prank(alice); grid.deposit{value: 1 ether}(); - assertEq(grid.balances(alice), 1 ether); } @@ -56,7 +56,6 @@ contract FlashGridTest is Test { grid.deposit{value: 1 ether}(); grid.deposit{value: 2 ether}(); vm.stopPrank(); - assertEq(grid.balances(alice), 3 ether); } @@ -67,11 +66,9 @@ contract FlashGridTest is Test { function test_Withdraw() public { vm.startPrank(alice); grid.deposit{value: 5 ether}(); - uint256 balBefore = alice.balance; grid.withdraw(3 ether); uint256 balAfter = alice.balance; - assertEq(grid.balances(alice), 2 ether); assertEq(balAfter - balBefore, 3 ether); vm.stopPrank(); @@ -80,7 +77,6 @@ contract FlashGridTest is Test { function test_RevertWithdrawInsufficientBalance() public { vm.startPrank(alice); grid.deposit{value: 1 ether}(); - vm.expectRevert(FlashGrid.InsufficientBalance.selector); grid.withdraw(2 ether); vm.stopPrank(); @@ -99,13 +95,10 @@ contract FlashGridTest is Test { function test_PlaceOrder() public { vm.startPrank(alice); grid.deposit{value: 1 ether}(); - grid.placeOrder(5, 0.5 ether, true); // Tick 5, 0.5 ETH, YES + grid.placeOrder(5, 0.5 ether, true); vm.stopPrank(); - // Balance reduced assertEq(grid.balances(alice), 0.5 ether); - - // Tick state updated (uint128 yesLiq, uint128 noLiq, uint32 orderCount,) = grid.getTickState(5); assertEq(yesLiq, 0.5 ether); assertEq(noLiq, 0); @@ -113,21 +106,17 @@ contract FlashGridTest is Test { } function test_PlaceOrderDifferentTicks() public { - // Alice places at tick 3, Bob at tick 15 - different storage slots vm.prank(alice); grid.deposit{value: 2 ether}(); - vm.prank(bob); grid.deposit{value: 2 ether}(); vm.prank(alice); grid.placeOrder(3, 1 ether, true); - vm.prank(bob); grid.placeOrder(15, 1 ether, false); - // Each tick has independent state - (uint128 yesLiq3,,uint32 count3,) = grid.getTickState(3); + (uint128 yesLiq3,, uint32 count3,) = grid.getTickState(3); assertEq(yesLiq3, 1 ether); assertEq(count3, 1); @@ -139,16 +128,14 @@ contract FlashGridTest is Test { function test_RevertPlaceOrderInvalidTick() public { vm.startPrank(alice); grid.deposit{value: 1 ether}(); - vm.expectRevert(FlashGrid.InvalidTick.selector); - grid.placeOrder(20, 0.5 ether, true); // Tick 20 is out of range + grid.placeOrder(20, 0.5 ether, true); vm.stopPrank(); } function test_RevertPlaceOrderInsufficientBalance() public { vm.startPrank(alice); grid.deposit{value: 0.1 ether}(); - vm.expectRevert(FlashGrid.InsufficientBalance.selector); grid.placeOrder(5, 1 ether, true); vm.stopPrank(); @@ -157,12 +144,31 @@ contract FlashGridTest is Test { function test_RevertPlaceOrderZeroAmount() public { vm.startPrank(alice); grid.deposit{value: 1 ether}(); - vm.expectRevert(FlashGrid.ZeroAmount.selector); grid.placeOrder(5, 0, true); vm.stopPrank(); } + function test_RevertPlaceOrderTooSmall() public { + vm.startPrank(alice); + grid.deposit{value: 1 ether}(); + vm.expectRevert(FlashGrid.OrderTooSmall.selector); + grid.placeOrder(5, 0.0001 ether, true); + vm.stopPrank(); + } + + function test_RevertPlaceOrderAfterResolution() public { + vm.startPrank(alice); + grid.deposit{value: 1 ether}(); + vm.stopPrank(); + + grid.resolveMarket(true); + + vm.prank(alice); + vm.expectRevert(FlashGrid.AlreadyResolved.selector); + grid.placeOrder(5, 0.5 ether, true); + } + function test_MultipleOrdersSameTick() public { vm.prank(alice); grid.deposit{value: 5 ether}(); @@ -181,52 +187,303 @@ contract FlashGridTest is Test { } // ═══════════════════════════════════════════════════════════ - // SETTLEMENT TESTS + // ORDER CANCELLATION TESTS // ═══════════════════════════════════════════════════════════ - function test_SettleTick() public { - // Setup: Alice YES, Bob NO at same tick + function test_CancelOrder() public { + vm.startPrank(alice); + grid.deposit{value: 1 ether}(); + grid.placeOrder(5, 0.5 ether, true); + assertEq(grid.balances(alice), 0.5 ether); + + grid.cancelOrder(5, 0); + assertEq(grid.balances(alice), 1 ether); // full refund + + (uint128 yesLiq,, uint32 count,) = grid.getTickState(5); + assertEq(yesLiq, 0); + assertEq(count, 0); + vm.stopPrank(); + } + + function test_RevertCancelAfterResolution() public { vm.prank(alice); - grid.deposit{value: 2 ether}(); + grid.deposit{value: 1 ether}(); + vm.prank(alice); + grid.placeOrder(5, 0.5 ether, true); + + grid.resolveMarket(true); + + vm.prank(alice); + vm.expectRevert(FlashGrid.AlreadyResolved.selector); + grid.cancelOrder(5, 0); + } + + function test_RevertCancelOtherUsersOrder() public { + vm.prank(alice); + grid.deposit{value: 1 ether}(); + vm.prank(alice); + grid.placeOrder(5, 0.5 ether, true); + vm.prank(bob); - grid.deposit{value: 2 ether}(); + vm.expectRevert(FlashGrid.NotOrderMaker.selector); + grid.cancelOrder(5, 0); + } + + function test_RevertCancelAlreadyCancelled() public { + vm.startPrank(alice); + grid.deposit{value: 1 ether}(); + grid.placeOrder(5, 0.5 ether, true); + grid.cancelOrder(5, 0); + + vm.expectRevert(FlashGrid.OrderAlreadyCancelled.selector); + grid.cancelOrder(5, 0); + vm.stopPrank(); + } + + function test_RevertCancelInvalidIndex() public { + vm.prank(alice); + vm.expectRevert(FlashGrid.InvalidOrderIndex.selector); + grid.cancelOrder(5, 99); + } + + // ═══════════════════════════════════════════════════════════ + // MARKET RESOLUTION TESTS + // ═══════════════════════════════════════════════════════════ + + function test_ResolveMarketYes() public { + grid.resolveMarket(true); + assertTrue(grid.resolved()); + assertTrue(grid.outcomeYes()); + } + + function test_ResolveMarketNo() public { + grid.resolveMarket(false); + assertTrue(grid.resolved()); + assertFalse(grid.outcomeYes()); + } + + function test_RevertResolveNotResolver() public { + vm.prank(alice); + vm.expectRevert(FlashGrid.NotResolver.selector); + grid.resolveMarket(true); + } + + function test_RevertResolveAlreadyResolved() public { + grid.resolveMarket(true); + vm.expectRevert(FlashGrid.AlreadyResolved.selector); + grid.resolveMarket(false); + } + + function test_EmitMarketResolved() public { + vm.expectEmit(false, true, false, true); + emit FlashGrid.MarketResolved(true, address(this)); + grid.resolveMarket(true); + } + + // ═══════════════════════════════════════════════════════════ + // PAYOUT TESTS + // ═══════════════════════════════════════════════════════════ + + function test_PayoutYesWins_EqualLiquidity() public { + // Alice bets 1 ETH YES, Bob bets 1 ETH NO. YES wins. + vm.prank(alice); + grid.deposit{value: 1 ether}(); + vm.prank(bob); + grid.deposit{value: 1 ether}(); vm.prank(alice); grid.placeOrder(5, 1 ether, true); vm.prank(bob); grid.placeOrder(5, 1 ether, false); - // Settle tick 5 - grid.settleTick(5); + grid.resolveMarket(true); + grid.settleAll(); - // Tick state should be cleared - (uint128 yesLiq, uint128 noLiq, uint32 count, uint32 lastEpoch) = grid.getTickState(5); - assertEq(yesLiq, 0); - assertEq(noLiq, 0); - assertEq(count, 0); - assertEq(lastEpoch, 1); // Epoch 1 + // Alice (winner) gets 1 ETH back + 1 ETH from Bob = 2 ETH + assertEq(grid.balances(alice), 2 ether); + // Bob (loser) gets nothing — fully matched + assertEq(grid.balances(bob), 0); + } + + function test_PayoutNoWins_EqualLiquidity() public { + vm.prank(alice); + grid.deposit{value: 1 ether}(); + vm.prank(bob); + grid.deposit{value: 1 ether}(); + + vm.prank(alice); + grid.placeOrder(5, 1 ether, true); + vm.prank(bob); + grid.placeOrder(5, 1 ether, false); + + grid.resolveMarket(false); // NO wins + grid.settleAll(); + + // Bob (NO winner) gets 2 ETH + assertEq(grid.balances(bob), 2 ether); + // Alice (YES loser) gets 0 + assertEq(grid.balances(alice), 0); + } + + function test_PayoutYesWins_AsymmetricLiquidity() public { + // Alice bets 10 ETH YES, Bob bets 1 ETH NO. YES wins. + // matched = 1 ETH. Alice's 9 ETH unmatched portion doesn't compete. + vm.prank(alice); + grid.deposit{value: 10 ether}(); + vm.prank(bob); + grid.deposit{value: 1 ether}(); + + vm.prank(alice); + grid.placeOrder(5, 10 ether, true); + vm.prank(bob); + grid.placeOrder(5, 1 ether, false); + + grid.resolveMarket(true); + grid.settleAll(); + + // Alice: 10 ETH original + 1 ETH from Bob's losing side = 11 ETH + assertEq(grid.balances(alice), 11 ether); + // Bob: fully matched, loses everything + assertEq(grid.balances(bob), 0); + } + + function test_PayoutNoWins_AsymmetricLiquidity() public { + // Alice bets 1 ETH YES, Bob bets 10 ETH NO. NO wins. + vm.prank(alice); + grid.deposit{value: 1 ether}(); + vm.prank(bob); + grid.deposit{value: 10 ether}(); + + vm.prank(alice); + grid.placeOrder(5, 1 ether, true); + vm.prank(bob); + grid.placeOrder(5, 10 ether, false); + + grid.resolveMarket(false); + grid.settleAll(); + + // Bob: 10 ETH + 1 ETH from Alice = 11 ETH + assertEq(grid.balances(bob), 11 ether); + // Alice: fully matched, loses all + assertEq(grid.balances(alice), 0); } - function test_RevertSettleTickAlreadySettled() public { + function test_PayoutUnmatchedRefund_LoserSide() public { + // Alice bets 2 ETH YES, Bob bets 5 ETH NO. YES wins. + // matched = 2 ETH. Bob has 3 ETH unmatched — should be refunded. vm.prank(alice); grid.deposit{value: 2 ether}(); + vm.prank(bob); + grid.deposit{value: 5 ether}(); + vm.prank(alice); + grid.placeOrder(5, 2 ether, true); + vm.prank(bob); + grid.placeOrder(5, 5 ether, false); + + grid.resolveMarket(true); + grid.settleAll(); + + // Alice (winner): 2 ETH + 5 ETH (all of losing pool) = 4 ETH... wait. + // Actually: winner gets original + pro-rata share of losing pool. + // winPool = 2 ETH (yes), losePool = 5 ETH (no), matched = 2 ETH + // Alice winnings = (2 * 5) / 2 = 5 ETH. But cap at matched = 2. No — + // let me re-check: winnings cap is at losePool, not matched. + // Actually the cap in code is: if (winnings > matched) winnings = matched + // But matched = min(2, 5) = 2. winnings = (2 * 5) / 2 = 5. Capped to 2. + // So Alice gets 2 + 2 = 4 ETH. + // Bob (loser): unmatched = 5 - (5 * 2) / 5 = 5 - 2 = 3 ETH refunded. + // Total: 4 + 3 = 7 = 2 + 5 ✓ (conservation) + assertEq(grid.balances(alice), 4 ether); + assertEq(grid.balances(bob), 3 ether); + } + + function test_PayoutMultipleOrdersSameTick() public { + // Alice: 2 ETH YES, Bob: 1 ETH YES, Charlie: 3 ETH NO. YES wins. + vm.prank(alice); + grid.deposit{value: 2 ether}(); + vm.prank(bob); + grid.deposit{value: 1 ether}(); + vm.prank(charlie); + grid.deposit{value: 3 ether}(); + + vm.prank(alice); + grid.placeOrder(5, 2 ether, true); + vm.prank(bob); grid.placeOrder(5, 1 ether, true); + vm.prank(charlie); + grid.placeOrder(5, 3 ether, false); - grid.settleTick(5); + grid.resolveMarket(true); + grid.settleAll(); + + // winPool = 3 ETH (yes), losePool = 3 ETH (no), matched = 3 ETH + // Alice winnings: (2 * 3) / 3 = 2. Capped at 3? No, 2 < 3, so 2. Payout = 2 + 2 = 4 ETH. + // Bob winnings: (1 * 3) / 3 = 1. Payout = 1 + 1 = 2 ETH. + // Charlie (loser): matchedShare = (3 * 3) / 3 = 3. unmatched = 3 - 3 = 0. + // Total: 4 + 2 + 0 = 6 = 2 + 1 + 3 ✓ + assertEq(grid.balances(alice), 4 ether); + assertEq(grid.balances(bob), 2 ether); + assertEq(grid.balances(charlie), 0); + } + + function test_RevertSettleBeforeResolution() public { + vm.prank(alice); + grid.deposit{value: 1 ether}(); + vm.prank(alice); + grid.placeOrder(5, 0.5 ether, true); - vm.expectRevert(FlashGrid.AlreadySettled.selector); + vm.expectRevert(FlashGrid.NotResolved.selector); grid.settleTick(5); } - function test_SettleAll() public { - // Setup: orders across multiple ticks + function test_RevertSettleAllBeforeResolution() public { + vm.expectRevert(FlashGrid.NotResolved.selector); + grid.settleAll(); + } + + function test_SingleSidedLiquidity_YesOnly() public { + // Only YES orders, no NO. YES wins. Everyone gets refunded. + vm.prank(alice); + grid.deposit{value: 2 ether}(); + vm.prank(bob); + grid.deposit{value: 1 ether}(); + + vm.prank(alice); + grid.placeOrder(5, 2 ether, true); + vm.prank(bob); + grid.placeOrder(5, 1 ether, true); + + grid.resolveMarket(true); + grid.settleAll(); + + // matched = 0 (no opposing liquidity). Winners get original back, no winnings. + assertEq(grid.balances(alice), 2 ether); + assertEq(grid.balances(bob), 1 ether); + } + + function test_SingleSidedLiquidity_YesOnly_NoWins() public { + // Only YES orders, NO wins. Losers get full refund (unmatched = full amount). + vm.prank(alice); + grid.deposit{value: 2 ether}(); + + vm.prank(alice); + grid.placeOrder(5, 2 ether, true); + + grid.resolveMarket(false); + grid.settleAll(); + + // matched = 0. Loser unmatched = 2 - 0 = 2 ETH refunded. + assertEq(grid.balances(alice), 2 ether); + } + + function test_SettleAll_MultiTick() public { vm.prank(alice); grid.deposit{value: 10 ether}(); vm.prank(bob); grid.deposit{value: 10 ether}(); - // Place orders at ticks 2, 8, and 15 + // Orders across ticks 2, 8, 15 vm.prank(alice); grid.placeOrder(2, 1 ether, true); vm.prank(bob); @@ -242,18 +499,113 @@ contract FlashGridTest is Test { vm.prank(bob); grid.placeOrder(15, 2 ether, false); - // Settle all + grid.resolveMarket(true); grid.settleAll(); - // All ticks should be settled + // All ticks cleared for (uint8 i = 0; i < 20; i++) { (uint128 yesLiq, uint128 noLiq,,) = grid.getTickState(i); assertEq(yesLiq, 0); assertEq(noLiq, 0); } - // Epoch should advance + // Epoch advanced assertEq(grid.currentEpoch(), 2); + + // Alice wins all: 1+1 + 0.5+0.5 + 2+2 = 7. Plus remaining 6.5 from deposit. + assertEq(grid.balances(alice), 6.5 ether + 7 ether); + // Bob loses all matched: remaining 6.5 from deposit + assertEq(grid.balances(bob), 6.5 ether); + } + + function test_EmptyTickSettlement() public { + grid.resolveMarket(true); + // Settle all with no orders — should not revert + grid.settleAll(); + assertEq(grid.currentEpoch(), 2); + } + + // ═══════════════════════════════════════════════════════════ + // PAUSE TESTS + // ═══════════════════════════════════════════════════════════ + + function test_PauseBlocksDeposits() public { + grid.pause(); + vm.prank(alice); + vm.expectRevert("Pausable: paused"); + grid.deposit{value: 1 ether}(); + } + + function test_PauseBlocksOrders() public { + vm.prank(alice); + grid.deposit{value: 1 ether}(); + + grid.pause(); + + vm.prank(alice); + vm.expectRevert("Pausable: paused"); + grid.placeOrder(5, 0.5 ether, true); + } + + function test_UnpauseRestoresFunction() public { + grid.pause(); + grid.unpause(); + + vm.prank(alice); + grid.deposit{value: 1 ether}(); + assertEq(grid.balances(alice), 1 ether); + } + + function test_WithdrawWorksWhenPaused() public { + vm.prank(alice); + grid.deposit{value: 1 ether}(); + + grid.pause(); + + // Withdraw should still work (safety: let users get money out) + vm.prank(alice); + grid.withdraw(1 ether); + assertEq(grid.balances(alice), 0); + } + + function test_RevertPauseNotResolver() public { + vm.prank(alice); + vm.expectRevert(FlashGrid.NotResolver.selector); + grid.pause(); + } + + // ═══════════════════════════════════════════════════════════ + // REENTRANCY TESTS + // ═══════════════════════════════════════════════════════════ + + function test_ReentrancyOnWithdraw() public { + ReentrancyAttacker attacker = new ReentrancyAttacker(address(grid)); + vm.deal(address(attacker), 10 ether); + + // Attacker deposits + attacker.attack_deposit{value: 2 ether}(); + assertEq(grid.balances(address(attacker)), 2 ether); + + // Attacker tries to reenter on withdraw — should revert + vm.expectRevert("ReentrancyGuard: reentrant call"); + attacker.attack_withdraw(); + } + + // ═══════════════════════════════════════════════════════════ + // MIN ORDER SIZE TESTS + // ═══════════════════════════════════════════════════════════ + + function test_MinOrderSizeEnforced() public { + vm.startPrank(alice); + grid.deposit{value: 1 ether}(); + + // Exactly at minimum should work + grid.placeOrder(5, 0.001 ether, true); + + // Below minimum should revert + vm.expectRevert(FlashGrid.OrderTooSmall.selector); + grid.placeOrder(5, 0.0009 ether, true); + vm.stopPrank(); } // ═══════════════════════════════════════════════════════════ @@ -270,20 +622,16 @@ contract FlashGridTest is Test { grid.placeOrder(19, 0.5 ether, false); FlashGrid.TickState[20] memory states = grid.getAllTickStates(); - assertEq(states[0].totalYesLiquidity, 1 ether); assertEq(states[0].orderCount, 1); assertEq(states[19].totalNoLiquidity, 0.5 ether); assertEq(states[19].orderCount, 1); - - // Empty ticks assertEq(states[10].orderCount, 0); } function test_GetTickOrders() public { vm.prank(alice); grid.deposit{value: 5 ether}(); - vm.prank(alice); grid.placeOrder(7, 1 ether, true); @@ -292,6 +640,8 @@ contract FlashGridTest is Test { assertEq(orders[0].maker, alice); assertEq(orders[0].amount, 1 ether); assertTrue(orders[0].isYes); + assertFalse(orders[0].cancelled); + assertFalse(orders[0].claimed); } function test_GetTickOrderCount() public { @@ -313,24 +663,28 @@ contract FlashGridTest is Test { // ═══════════════════════════════════════════════════════════ function test_FactoryCreateMarket() public { - assertEq(factory.getMarketCount(), 1); // From setUp - + assertEq(factory.getMarketCount(), 1); address market2 = factory.createMarket("Will ETH hit $5000?"); assertEq(factory.getMarketCount(), 2); assertTrue(market2 != address(0)); } + function test_FactoryCreateMarketWithResolver() public { + address market2 = factory.createMarket("Will ETH hit $5000?", alice); + FlashGrid grid2 = FlashGrid(payable(market2)); + assertEq(grid2.resolver(), alice); + } + function test_FactoryRevertDuplicateMarket() public { vm.expectRevert(FlashGridFactory.MarketAlreadyExists.selector); - factory.createMarket("Will MON reach $10 by Q2?"); // Same as setUp + factory.createMarket("Will MON reach $10 by Q2?"); } function test_FactoryGetAllMarkets() public { factory.createMarket("Market 2"); factory.createMarket("Market 3"); - address[] memory allMarkets = factory.getAllMarkets(); - assertEq(allMarkets.length, 3); // 1 from setUp + 2 new + assertEq(allMarkets.length, 3); } // ═══════════════════════════════════════════════════════════ @@ -352,7 +706,7 @@ contract FlashGridTest is Test { } // ═══════════════════════════════════════════════════════════ - // EVENT EMISSION TESTS + // EVENT EMISSION TESTS // ═══════════════════════════════════════════════════════════ function test_EmitOrderPlaced() public { @@ -366,21 +720,16 @@ contract FlashGridTest is Test { grid.placeOrder(5, 0.5 ether, true); } - function test_EmitTickSettled() public { - vm.prank(alice); - grid.deposit{value: 2 ether}(); - vm.prank(bob); - grid.deposit{value: 2 ether}(); - - vm.prank(alice); - grid.placeOrder(5, 1 ether, true); - vm.prank(bob); - grid.placeOrder(5, 1 ether, false); + function test_EmitOrderCancelled() public { + vm.startPrank(alice); + grid.deposit{value: 1 ether}(); + grid.placeOrder(5, 0.5 ether, true); - vm.expectEmit(true, false, false, true); - emit FlashGrid.TickSettled(5, 1, 1 ether, 1 ether, 30); // tick 5 → price = (5+1)*5 = 30 + vm.expectEmit(true, true, false, true); + emit FlashGrid.OrderCancelled(5, alice, 0, 0.5 ether); - grid.settleTick(5); + grid.cancelOrder(5, 0); + vm.stopPrank(); } // ═══════════════════════════════════════════════════════════ @@ -392,5 +741,128 @@ contract FlashGridTest is Test { assertEq(grid.factory(), address(factory)); assertEq(grid.currentEpoch(), 1); assertEq(grid.NUM_TICKS(), 20); + assertEq(grid.resolver(), address(this)); + assertFalse(grid.resolved()); + } + + // ═══════════════════════════════════════════════════════════ + // FUZZ TESTS + // ═══════════════════════════════════════════════════════════ + + function testFuzz_DepositWithdraw(uint256 amount) public { + amount = bound(amount, 1, 50 ether); + vm.deal(alice, amount); + + vm.startPrank(alice); + grid.deposit{value: amount}(); + assertEq(grid.balances(alice), amount); + + grid.withdraw(amount); + assertEq(grid.balances(alice), 0); + vm.stopPrank(); + } + + function testFuzz_SettlementConservation(uint128 yesAmt, uint128 noAmt) public { + // Bound to reasonable range to avoid overflow and meet minimum + yesAmt = uint128(bound(yesAmt, 0.001 ether, 10 ether)); + noAmt = uint128(bound(noAmt, 0.001 ether, 10 ether)); + + vm.deal(alice, uint256(yesAmt)); + vm.deal(bob, uint256(noAmt)); + + vm.prank(alice); + grid.deposit{value: yesAmt}(); + vm.prank(bob); + grid.deposit{value: noAmt}(); + + vm.prank(alice); + grid.placeOrder(5, yesAmt, true); + vm.prank(bob); + grid.placeOrder(5, noAmt, false); + + uint256 totalDeposited = uint256(yesAmt) + uint256(noAmt); + + grid.resolveMarket(true); + grid.settleAll(); + + uint256 totalPayouts = grid.balances(alice) + grid.balances(bob); + + // Conservation: total payouts must equal total deposited (no money created or destroyed) + assertEq(totalPayouts, totalDeposited, "Settlement conservation violated"); + } + + function testFuzz_PlaceOrderBalanceConsistent(uint8 tick, uint128 amount) public { + tick = uint8(bound(tick, 0, 19)); + amount = uint128(bound(amount, 0.001 ether, 10 ether)); + + vm.deal(alice, uint256(amount)); + vm.startPrank(alice); + grid.deposit{value: amount}(); + + uint256 balBefore = grid.balances(alice); + grid.placeOrder(tick, amount, true); + uint256 balAfter = grid.balances(alice); + + assertEq(balBefore - balAfter, amount); + vm.stopPrank(); + } + + // ═══════════════════════════════════════════════════════════ + // GAS / STRESS TESTS + // ═══════════════════════════════════════════════════════════ + + function test_ManyOrdersPerTick() public { + // Place 50 orders at the same tick to test gas limits + uint256 numOrders = 50; + for (uint256 i = 0; i < numOrders; i++) { + address user = address(uint160(1000 + i)); + vm.deal(user, 1 ether); + vm.prank(user); + grid.deposit{value: 0.01 ether}(); + vm.prank(user); + grid.placeOrder(5, 0.01 ether, i % 2 == 0); + } + + (,, uint32 count,) = grid.getTickState(5); + assertEq(count, numOrders); + + // Settle should complete without hitting gas limit + grid.resolveMarket(true); + grid.settleAll(); + + (uint128 yesLiq, uint128 noLiq, uint32 countAfter,) = grid.getTickState(5); + assertEq(yesLiq, 0); + assertEq(noLiq, 0); + assertEq(countAfter, 0); + } +} + +// ═══════════════════════════════════════════════════════════ +// REENTRANCY ATTACKER +// ═══════════════════════════════════════════════════════════ + +contract ReentrancyAttacker { + FlashGrid public target; + bool public attacking; + + constructor(address _target) { + target = FlashGrid(payable(_target)); + } + + function attack_deposit() external payable { + target.deposit{value: msg.value}(); + } + + function attack_withdraw() external { + attacking = true; + target.withdraw(1 ether); + } + + receive() external payable { + if (attacking) { + attacking = false; + // Try to reenter + target.withdraw(1 ether); + } } } diff --git a/dashboard/lib/chains.ts b/dashboard/lib/chains.ts new file mode 100644 index 0000000..7898ff1 --- /dev/null +++ b/dashboard/lib/chains.ts @@ -0,0 +1,58 @@ +// chains.ts +// Multi-chain configuration for FlashGrid. Supports Monad Testnet, Sepolia, and Base Sepolia. +// Chain selection is driven by NEXT_PUBLIC_CHAIN_ID environment variable. + +export interface ChainConfig { + id: number; + name: string; + nativeCurrency: { name: string; symbol: string; decimals: number }; + rpcUrls: { default: { http: string[] } }; + blockExplorers: { default: { name: string; url: string } }; +} + +export const CHAINS: Record = { + 10143: { + id: 10143, + name: "Monad Testnet", + nativeCurrency: { name: "Monad", symbol: "MON", decimals: 18 }, + rpcUrls: { default: { http: ["https://testnet-rpc.monad.xyz"] } }, + blockExplorers: { default: { name: "MonadVision", url: "https://testnet.monadvision.com" } }, + }, + 11155111: { + id: 11155111, + name: "Sepolia", + nativeCurrency: { name: "Sepolia Ether", symbol: "ETH", decimals: 18 }, + rpcUrls: { default: { http: ["https://rpc.sepolia.org"] } }, + blockExplorers: { default: { name: "Etherscan", url: "https://sepolia.etherscan.io" } }, + }, + 84532: { + id: 84532, + name: "Base Sepolia", + nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 }, + rpcUrls: { default: { http: ["https://sepolia.base.org"] } }, + blockExplorers: { default: { name: "Basescan", url: "https://sepolia.basescan.org" } }, + }, +}; + +const DEFAULT_CHAIN_ID = 10143; + +export function getChainId(): number { + const envChainId = process.env.NEXT_PUBLIC_CHAIN_ID; + if (envChainId) { + const parsed = parseInt(envChainId, 10); + if (CHAINS[parsed]) return parsed; + } + return DEFAULT_CHAIN_ID; +} + +export function getChain(): ChainConfig { + return CHAINS[getChainId()]; +} + +export function getExplorerTxUrl(hash: string): string { + return `${getChain().blockExplorers.default.url}/tx/${hash}`; +} + +export function getExplorerAddressUrl(address: string): string { + return `${getChain().blockExplorers.default.url}/address/${address}`; +} diff --git a/dashboard/lib/contract.ts b/dashboard/lib/contract.ts index 3f244c9..68a9fdf 100644 --- a/dashboard/lib/contract.ts +++ b/dashboard/lib/contract.ts @@ -1,28 +1,20 @@ // contract.ts -// Network configuration, deployed contract addresses, and ABI definitions -// for the FlashGrid smart contracts on Monad testnet. +// Deployed contract addresses and ABI definitions for FlashGrid smart contracts. +// Chain configuration is imported from chains.ts for multi-chain support. import { type Abi } from "viem"; -// ── Network Config ────────────────────────────────────────────── +export { getChain, getChainId, getExplorerTxUrl, getExplorerAddressUrl } from "./chains"; -export const MONAD_TESTNET = { - id: 10143, - name: "Monad Testnet", - nativeCurrency: { name: "Monad", symbol: "MON", decimals: 18 }, - rpcUrls: { - default: { http: ["https://testnet-rpc.monad.xyz"] }, - }, - blockExplorers: { - default: { name: "MonadVision", url: "https://testnet.monadvision.com" }, - }, -} as const; +// Re-export for backwards compat +export { CHAINS } from "./chains"; +import { getChain } from "./chains"; +export const MONAD_TESTNET = getChain(); // ═══════════════════════════════════════════════════════════ // CONTRACT ADDRESSES // ═══════════════════════════════════════════════════════════ -// These should be updated after deployment export const ADDRESSES = { factory: process.env.NEXT_PUBLIC_FACTORY_ADDRESS || "0x0000000000000000000000000000000000000000", flashGrid: process.env.NEXT_PUBLIC_FLASHGRID_ADDRESS || "0x0000000000000000000000000000000000000000", @@ -42,6 +34,13 @@ export const FLASHGRID_ABI = [ outputs: [{ type: "uint8" }], stateMutability: "view", }, + { + type: "function", + name: "MIN_ORDER_SIZE", + inputs: [], + outputs: [{ type: "uint128" }], + stateMutability: "view", + }, { type: "function", name: "currentEpoch", @@ -56,6 +55,34 @@ export const FLASHGRID_ABI = [ outputs: [{ type: "string" }], stateMutability: "view", }, + { + type: "function", + name: "resolver", + inputs: [], + outputs: [{ type: "address" }], + stateMutability: "view", + }, + { + type: "function", + name: "resolved", + inputs: [], + outputs: [{ type: "bool" }], + stateMutability: "view", + }, + { + type: "function", + name: "outcomeYes", + inputs: [], + outputs: [{ type: "bool" }], + stateMutability: "view", + }, + { + type: "function", + name: "paused", + inputs: [], + outputs: [{ type: "bool" }], + stateMutability: "view", + }, { type: "function", name: "balances", @@ -71,7 +98,7 @@ export const FLASHGRID_ABI = [ { type: "uint128", name: "yesLiquidity" }, { type: "uint128", name: "noLiquidity" }, { type: "uint32", name: "orderCount" }, - { type: "uint32", name: "lastMatchedEpoch" }, + { type: "uint32", name: "lastSettledEpoch" }, ], stateMutability: "view", }, @@ -86,7 +113,7 @@ export const FLASHGRID_ABI = [ { type: "uint128", name: "totalYesLiquidity" }, { type: "uint128", name: "totalNoLiquidity" }, { type: "uint32", name: "orderCount" }, - { type: "uint32", name: "lastMatchedEpoch" }, + { type: "uint32", name: "lastSettledEpoch" }, ], }, ], @@ -104,6 +131,8 @@ export const FLASHGRID_ABI = [ { type: "uint128", name: "amount" }, { type: "bool", name: "isYes" }, { type: "uint32", name: "epoch" }, + { type: "bool", name: "cancelled" }, + { type: "bool", name: "claimed" }, ], }, ], @@ -142,6 +171,23 @@ export const FLASHGRID_ABI = [ outputs: [], stateMutability: "nonpayable", }, + { + type: "function", + name: "cancelOrder", + inputs: [ + { type: "uint8", name: "tick" }, + { type: "uint256", name: "orderIndex" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "resolveMarket", + inputs: [{ type: "bool", name: "_outcomeYes" }], + outputs: [], + stateMutability: "nonpayable", + }, { type: "function", name: "settleTick", @@ -156,6 +202,20 @@ export const FLASHGRID_ABI = [ outputs: [], stateMutability: "nonpayable", }, + { + type: "function", + name: "pause", + inputs: [], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "unpause", + inputs: [], + outputs: [], + stateMutability: "nonpayable", + }, // Events { type: "event", @@ -184,6 +244,16 @@ export const FLASHGRID_ABI = [ { type: "uint32", name: "epoch", indexed: false }, ], }, + { + type: "event", + name: "OrderCancelled", + inputs: [ + { type: "uint8", name: "tick", indexed: true }, + { type: "address", name: "maker", indexed: true }, + { type: "uint256", name: "orderIndex", indexed: false }, + { type: "uint128", name: "amount", indexed: false }, + ], + }, { type: "event", name: "TickSettled", @@ -204,6 +274,23 @@ export const FLASHGRID_ABI = [ { type: "uint16", name: "ticksActive", indexed: false }, ], }, + { + type: "event", + name: "MarketResolved", + inputs: [ + { type: "bool", name: "outcomeYes", indexed: false }, + { type: "address", name: "resolvedBy", indexed: true }, + ], + }, + { + type: "event", + name: "PayoutClaimed", + inputs: [ + { type: "uint8", name: "tick", indexed: true }, + { type: "address", name: "maker", indexed: true }, + { type: "uint256", name: "amount", indexed: false }, + ], + }, ] as const satisfies Abi; export const BENCHMARK_ABI = [ @@ -243,7 +330,10 @@ export const FACTORY_ABI = [ { type: "function", name: "createMarket", - inputs: [{ type: "string", name: "question" }], + inputs: [ + { type: "string", name: "question" }, + { type: "address", name: "_resolver" }, + ], outputs: [{ type: "address" }], stateMutability: "nonpayable", }, @@ -268,6 +358,7 @@ export const FACTORY_ABI = [ { type: "address", name: "market", indexed: true }, { type: "string", name: "question", indexed: false }, { type: "address", name: "creator", indexed: true }, + { type: "address", name: "resolver", indexed: true }, { type: "uint256", name: "index", indexed: false }, ], }, diff --git a/dashboard/lib/indexer.ts b/dashboard/lib/indexer.ts index a8f6baa..599f206 100644 --- a/dashboard/lib/indexer.ts +++ b/dashboard/lib/indexer.ts @@ -1,11 +1,9 @@ // indexer.ts -// Server-side event indexer that polls the Monad testnet for FlashGrid contract -// events (OrderPlaced, TickSettled, EpochCompleted). Maintains an in-memory -// event store with deduplication and provides helper functions for metrics -// computation. Starts polling from the current block and backfills 10,000 blocks. +// Server-side event indexer that polls for FlashGrid contract events. +// Uses chain config from chains.ts for multi-chain support. -import { createPublicClient, http, formatEther, type Log } from "viem"; -import { MONAD_TESTNET, ADDRESSES, FLASHGRID_ABI } from "./contract"; +import { createPublicClient, http, formatEther } from "viem"; +import { getChain, ADDRESSES, FLASHGRID_ABI } from "./contract"; import type { LiveOrder, TickSettledEvent } from "./types"; // ────────────────────────────────────────────────────────────── @@ -13,8 +11,8 @@ import type { LiveOrder, TickSettledEvent } from "./types"; // ═══════════════════════════════════════════════════════════ const MAX_EVENTS = 1000; -const MAX_BLOCK_RANGE = 2000n; // Max blocks per getLogs call (RPC safe) -const BACKFILL_BLOCKS = 10000n; // Look back ~2.7 hours on startup +const MAX_BLOCK_RANGE = 2000n; +const BACKFILL_BLOCKS = 10000n; interface EventStore { orders: LiveOrder[]; @@ -38,7 +36,6 @@ export const eventStore: EventStore = { lastProcessedBlock: 0n, }; -// Ring buffer push with dedup function pushOrder(order: LiveOrder) { if (eventStore.seenOrderIds.has(order.id)) return; eventStore.seenOrderIds.add(order.id); @@ -50,7 +47,6 @@ function pushOrder(order: LiveOrder) { } eventStore.totalOrders++; - // Track orders per block const blockNum = order.blockNumber; const current = eventStore.ordersPerBlock.get(blockNum) || 0; eventStore.ordersPerBlock.set(blockNum, current + 1); @@ -75,14 +71,15 @@ let client: ReturnType | null = null; export function getClient() { if (!client) { + const chain = getChain(); client = createPublicClient({ chain: { - id: MONAD_TESTNET.id, - name: MONAD_TESTNET.name, - nativeCurrency: MONAD_TESTNET.nativeCurrency, - rpcUrls: MONAD_TESTNET.rpcUrls, + id: chain.id, + name: chain.name, + nativeCurrency: chain.nativeCurrency, + rpcUrls: chain.rpcUrls, }, - transport: http(MONAD_TESTNET.rpcUrls.default.http[0]), + transport: http(chain.rpcUrls.default.http[0]), }); } return client; @@ -123,12 +120,10 @@ async function fetchEventsInRange( from: bigint, to: bigint ) { - // Chunk into MAX_BLOCK_RANGE slices to stay within RPC limits for (let start = from; start <= to; start += MAX_BLOCK_RANGE) { const end = start + MAX_BLOCK_RANGE - 1n > to ? to : start + MAX_BLOCK_RANGE - 1n; try { - // Fetch OrderPlaced events const orderLogs = await publicClient.getLogs({ address: flashGridAddress, event: ORDER_PLACED_EVENT, @@ -161,7 +156,6 @@ async function fetchEventsInRange( eventStore.totalVolume += args.amount || 0n; } - // Fetch TickSettled events const settleLogs = await publicClient.getLogs({ address: flashGridAddress, event: TICK_SETTLED_EVENT, @@ -191,7 +185,6 @@ async function fetchEventsInRange( pushSettlement(settlement); } } catch (err) { - // If a chunk fails (e.g. range too large), skip it silently console.error(`Failed to fetch events for blocks ${start}-${end}:`, err); } } @@ -214,7 +207,6 @@ export async function startPolling(intervalMs = 2000) { const publicClient = getClient(); - // Get current block let currentBlock: bigint; try { currentBlock = await publicClient.getBlockNumber(); @@ -223,10 +215,8 @@ export async function startPolling(intervalMs = 2000) { return; } - // Set lastProcessedBlock to current so the poller immediately picks up new events eventStore.lastProcessedBlock = currentBlock; - // Kick off backfill in background (does NOT block polling) if (!backfillDone) { backfillDone = true; const backfillFrom = currentBlock > BACKFILL_BLOCKS ? currentBlock - BACKFILL_BLOCKS : 0n; @@ -235,7 +225,6 @@ export async function startPolling(intervalMs = 2000) { }); } - // Start polling for new events going forward pollingInterval = setInterval(async () => { try { const latestBlock = await publicClient.getBlockNumber(); @@ -268,14 +257,13 @@ export function stopPolling() { export function getOrdersPerBlock(): number[] { const entries = Array.from(eventStore.ordersPerBlock.entries()) .sort(([a], [b]) => a - b) - .slice(-50); // Last 50 blocks + .slice(-50); return entries.map(([, count]) => count); } export function getActiveTicks(): number { const tickSet = new Set(); - // Check recent orders (last 100) const recent = eventStore.orders.slice(-100); for (const order of recent) { tickSet.add(order.tick); diff --git a/dashboard/lib/wallet.ts b/dashboard/lib/wallet.ts index c7dbfcb..37223c8 100644 --- a/dashboard/lib/wallet.ts +++ b/dashboard/lib/wallet.ts @@ -1,8 +1,6 @@ // wallet.ts // Client-side wallet interaction helpers for the FlashGrid dashboard. -// Handles MetaMask connection, balance fetching (both native MON and in-contract -// grid balance), deposit/withdraw, order placement, and settlement calls. -// All functions use viem for type-safe Ethereum interactions. +// Uses multi-chain config from chains.ts for chain-agnostic operation. "use client"; @@ -17,18 +15,7 @@ import { type PublicClient, type Hash, } from "viem"; -import { MONAD_TESTNET, ADDRESSES, FLASHGRID_ABI, BENCHMARK_ABI } from "./contract"; - -// ═══════════════════════════════════════════════════════════ -// MONAD CHAIN DEFINITION -// ═══════════════════════════════════════════════════════════ - -export const monadChain = { - id: MONAD_TESTNET.id, - name: MONAD_TESTNET.name, - nativeCurrency: MONAD_TESTNET.nativeCurrency, - rpcUrls: MONAD_TESTNET.rpcUrls, -} as const; +import { getChain, ADDRESSES, FLASHGRID_ABI, BENCHMARK_ABI } from "./contract"; // ═══════════════════════════════════════════════════════════ // WALLET CONNECTION @@ -49,7 +36,8 @@ export async function connectWallet(): Promise<{ throw new Error("No wallet found. Install MetaMask."); } - // Request accounts + const chain = getChain(); + const accounts = await window.ethereum.request({ method: "eth_requestAccounts", }); @@ -58,39 +46,45 @@ export async function connectWallet(): Promise<{ throw new Error("No accounts found"); } - // Switch to Monad testnet + // Switch to configured chain try { await window.ethereum.request({ method: "wallet_switchEthereumChain", - params: [{ chainId: `0x${MONAD_TESTNET.id.toString(16)}` }], + params: [{ chainId: `0x${chain.id.toString(16)}` }], }); } catch (switchError: any) { - // Chain not added, add it if (switchError.code === 4902) { await window.ethereum.request({ method: "wallet_addEthereumChain", params: [ { - chainId: `0x${MONAD_TESTNET.id.toString(16)}`, - chainName: MONAD_TESTNET.name, - nativeCurrency: MONAD_TESTNET.nativeCurrency, - rpcUrls: MONAD_TESTNET.rpcUrls.default.http, - blockExplorerUrls: [MONAD_TESTNET.blockExplorers.default.url], + chainId: `0x${chain.id.toString(16)}`, + chainName: chain.name, + nativeCurrency: chain.nativeCurrency, + rpcUrls: chain.rpcUrls.default.http, + blockExplorerUrls: [chain.blockExplorers.default.url], }, ], }); } } + const viemChain = { + id: chain.id, + name: chain.name, + nativeCurrency: chain.nativeCurrency, + rpcUrls: chain.rpcUrls, + } as const; + const walletClient = createWalletClient({ - chain: monadChain, + chain: viemChain, transport: custom(window.ethereum), account: accounts[0] as `0x${string}`, }); const publicClient = createPublicClient({ - chain: monadChain, - transport: http(MONAD_TESTNET.rpcUrls.default.http[0]), + chain: viemChain, + transport: http(chain.rpcUrls.default.http[0]), }); return { @@ -107,6 +101,16 @@ export async function connectWallet(): Promise<{ const flashGridAddress = ADDRESSES.flashGrid as `0x${string}`; const benchmarkAddress = ADDRESSES.benchmark as `0x${string}`; +function getViemChain() { + const chain = getChain(); + return { + id: chain.id, + name: chain.name, + nativeCurrency: chain.nativeCurrency, + rpcUrls: chain.rpcUrls, + } as const; +} + export async function getBalances( publicClient: PublicClient, address: string @@ -137,10 +141,9 @@ export async function deposit( abi: FLASHGRID_ABI, functionName: "deposit", value: parseEther(amount), - chain: monadChain, + chain: getViemChain(), account: walletClient.account!, }); - await publicClient.waitForTransactionReceipt({ hash, timeout: 60_000 }); return hash; } @@ -155,10 +158,9 @@ export async function withdraw( abi: FLASHGRID_ABI, functionName: "withdraw", args: [parseEther(amount)], - chain: monadChain, + chain: getViemChain(), account: walletClient.account!, }); - await publicClient.waitForTransactionReceipt({ hash, timeout: 60_000 }); return hash; } @@ -175,10 +177,44 @@ export async function placeOrder( abi: FLASHGRID_ABI, functionName: "placeOrder", args: [tick, parseEther(amount), isYes], - chain: monadChain, + chain: getViemChain(), account: walletClient.account!, }); + await publicClient.waitForTransactionReceipt({ hash, timeout: 60_000 }); + return hash; +} +export async function cancelOrder( + walletClient: WalletClient, + publicClient: PublicClient, + tick: number, + orderIndex: bigint +): Promise { + const hash = await walletClient.writeContract({ + address: flashGridAddress, + abi: FLASHGRID_ABI, + functionName: "cancelOrder", + args: [tick, orderIndex], + chain: getViemChain(), + account: walletClient.account!, + }); + await publicClient.waitForTransactionReceipt({ hash, timeout: 60_000 }); + return hash; +} + +export async function resolveMarket( + walletClient: WalletClient, + publicClient: PublicClient, + outcomeYes: boolean +): Promise { + const hash = await walletClient.writeContract({ + address: flashGridAddress, + abi: FLASHGRID_ABI, + functionName: "resolveMarket", + args: [outcomeYes], + chain: getViemChain(), + account: walletClient.account!, + }); await publicClient.waitForTransactionReceipt({ hash, timeout: 60_000 }); return hash; } @@ -193,10 +229,9 @@ export async function settleTick( abi: FLASHGRID_ABI, functionName: "settleTick", args: [tick], - chain: monadChain, + chain: getViemChain(), account: walletClient.account!, }); - await publicClient.waitForTransactionReceipt({ hash, timeout: 60_000 }); return hash; } @@ -209,10 +244,9 @@ export async function settleAll( address: flashGridAddress, abi: FLASHGRID_ABI, functionName: "settleAll", - chain: monadChain, + chain: getViemChain(), account: walletClient.account!, }); - await publicClient.waitForTransactionReceipt({ hash, timeout: 60_000 }); return hash; } @@ -227,18 +261,11 @@ export async function placeBenchmarkOrder( abi: BENCHMARK_ABI, functionName: "placeOrder", args: [parseEther(amount)], - chain: monadChain, + chain: getViemChain(), account: walletClient.account!, }); - await publicClient.waitForTransactionReceipt({ hash, timeout: 60_000 }); return hash; } -export function getExplorerTxUrl(hash: string): string { - return `${MONAD_TESTNET.blockExplorers.default.url}/tx/${hash}`; -} - -export function getExplorerAddressUrl(address: string): string { - return `${MONAD_TESTNET.blockExplorers.default.url}/address/${address}`; -} +export { getExplorerTxUrl, getExplorerAddressUrl } from "./chains"; diff --git a/sdk/dist/abis.d.ts b/sdk/dist/abis.d.ts new file mode 100644 index 0000000..8087a67 --- /dev/null +++ b/sdk/dist/abis.d.ts @@ -0,0 +1,470 @@ +export declare const FLASHGRID_ABI: readonly [{ + readonly type: "function"; + readonly name: "NUM_TICKS"; + readonly inputs: readonly []; + readonly outputs: readonly [{ + readonly type: "uint8"; + }]; + readonly stateMutability: "view"; +}, { + readonly type: "function"; + readonly name: "MIN_ORDER_SIZE"; + readonly inputs: readonly []; + readonly outputs: readonly [{ + readonly type: "uint128"; + }]; + readonly stateMutability: "view"; +}, { + readonly type: "function"; + readonly name: "currentEpoch"; + readonly inputs: readonly []; + readonly outputs: readonly [{ + readonly type: "uint32"; + }]; + readonly stateMutability: "view"; +}, { + readonly type: "function"; + readonly name: "marketQuestion"; + readonly inputs: readonly []; + readonly outputs: readonly [{ + readonly type: "string"; + }]; + readonly stateMutability: "view"; +}, { + readonly type: "function"; + readonly name: "resolver"; + readonly inputs: readonly []; + readonly outputs: readonly [{ + readonly type: "address"; + }]; + readonly stateMutability: "view"; +}, { + readonly type: "function"; + readonly name: "resolved"; + readonly inputs: readonly []; + readonly outputs: readonly [{ + readonly type: "bool"; + }]; + readonly stateMutability: "view"; +}, { + readonly type: "function"; + readonly name: "outcomeYes"; + readonly inputs: readonly []; + readonly outputs: readonly [{ + readonly type: "bool"; + }]; + readonly stateMutability: "view"; +}, { + readonly type: "function"; + readonly name: "paused"; + readonly inputs: readonly []; + readonly outputs: readonly [{ + readonly type: "bool"; + }]; + readonly stateMutability: "view"; +}, { + readonly type: "function"; + readonly name: "balances"; + readonly inputs: readonly [{ + readonly type: "address"; + }]; + readonly outputs: readonly [{ + readonly type: "uint256"; + }]; + readonly stateMutability: "view"; +}, { + readonly type: "function"; + readonly name: "getTickState"; + readonly inputs: readonly [{ + readonly type: "uint8"; + readonly name: "tick"; + }]; + readonly outputs: readonly [{ + readonly type: "uint128"; + readonly name: "yesLiquidity"; + }, { + readonly type: "uint128"; + readonly name: "noLiquidity"; + }, { + readonly type: "uint32"; + readonly name: "orderCount"; + }, { + readonly type: "uint32"; + readonly name: "lastSettledEpoch"; + }]; + readonly stateMutability: "view"; +}, { + readonly type: "function"; + readonly name: "getAllTickStates"; + readonly inputs: readonly []; + readonly outputs: readonly [{ + readonly type: "tuple[20]"; + readonly components: readonly [{ + readonly type: "uint128"; + readonly name: "totalYesLiquidity"; + }, { + readonly type: "uint128"; + readonly name: "totalNoLiquidity"; + }, { + readonly type: "uint32"; + readonly name: "orderCount"; + }, { + readonly type: "uint32"; + readonly name: "lastSettledEpoch"; + }]; + }]; + readonly stateMutability: "view"; +}, { + readonly type: "function"; + readonly name: "getTickOrders"; + readonly inputs: readonly [{ + readonly type: "uint8"; + readonly name: "tick"; + }]; + readonly outputs: readonly [{ + readonly type: "tuple[]"; + readonly components: readonly [{ + readonly type: "address"; + readonly name: "maker"; + }, { + readonly type: "uint128"; + readonly name: "amount"; + }, { + readonly type: "bool"; + readonly name: "isYes"; + }, { + readonly type: "uint32"; + readonly name: "epoch"; + }, { + readonly type: "bool"; + readonly name: "cancelled"; + }, { + readonly type: "bool"; + readonly name: "claimed"; + }]; + }]; + readonly stateMutability: "view"; +}, { + readonly type: "function"; + readonly name: "getTickOrderCount"; + readonly inputs: readonly [{ + readonly type: "uint8"; + readonly name: "tick"; + }]; + readonly outputs: readonly [{ + readonly type: "uint256"; + }]; + readonly stateMutability: "view"; +}, { + readonly type: "function"; + readonly name: "deposit"; + readonly inputs: readonly []; + readonly outputs: readonly []; + readonly stateMutability: "payable"; +}, { + readonly type: "function"; + readonly name: "withdraw"; + readonly inputs: readonly [{ + readonly type: "uint256"; + readonly name: "amount"; + }]; + readonly outputs: readonly []; + readonly stateMutability: "nonpayable"; +}, { + readonly type: "function"; + readonly name: "placeOrder"; + readonly inputs: readonly [{ + readonly type: "uint8"; + readonly name: "tick"; + }, { + readonly type: "uint128"; + readonly name: "amount"; + }, { + readonly type: "bool"; + readonly name: "isYes"; + }]; + readonly outputs: readonly []; + readonly stateMutability: "nonpayable"; +}, { + readonly type: "function"; + readonly name: "cancelOrder"; + readonly inputs: readonly [{ + readonly type: "uint8"; + readonly name: "tick"; + }, { + readonly type: "uint256"; + readonly name: "orderIndex"; + }]; + readonly outputs: readonly []; + readonly stateMutability: "nonpayable"; +}, { + readonly type: "function"; + readonly name: "resolveMarket"; + readonly inputs: readonly [{ + readonly type: "bool"; + readonly name: "_outcomeYes"; + }]; + readonly outputs: readonly []; + readonly stateMutability: "nonpayable"; +}, { + readonly type: "function"; + readonly name: "settleTick"; + readonly inputs: readonly [{ + readonly type: "uint8"; + readonly name: "tick"; + }]; + readonly outputs: readonly []; + readonly stateMutability: "nonpayable"; +}, { + readonly type: "function"; + readonly name: "settleAll"; + readonly inputs: readonly []; + readonly outputs: readonly []; + readonly stateMutability: "nonpayable"; +}, { + readonly type: "function"; + readonly name: "pause"; + readonly inputs: readonly []; + readonly outputs: readonly []; + readonly stateMutability: "nonpayable"; +}, { + readonly type: "function"; + readonly name: "unpause"; + readonly inputs: readonly []; + readonly outputs: readonly []; + readonly stateMutability: "nonpayable"; +}, { + readonly type: "event"; + readonly name: "Deposited"; + readonly inputs: readonly [{ + readonly type: "address"; + readonly name: "user"; + readonly indexed: true; + }, { + readonly type: "uint256"; + readonly name: "amount"; + readonly indexed: false; + }]; +}, { + readonly type: "event"; + readonly name: "Withdrawn"; + readonly inputs: readonly [{ + readonly type: "address"; + readonly name: "user"; + readonly indexed: true; + }, { + readonly type: "uint256"; + readonly name: "amount"; + readonly indexed: false; + }]; +}, { + readonly type: "event"; + readonly name: "OrderPlaced"; + readonly inputs: readonly [{ + readonly type: "uint8"; + readonly name: "tick"; + readonly indexed: true; + }, { + readonly type: "address"; + readonly name: "maker"; + readonly indexed: true; + }, { + readonly type: "uint128"; + readonly name: "amount"; + readonly indexed: false; + }, { + readonly type: "bool"; + readonly name: "isYes"; + readonly indexed: false; + }, { + readonly type: "uint32"; + readonly name: "epoch"; + readonly indexed: false; + }]; +}, { + readonly type: "event"; + readonly name: "OrderCancelled"; + readonly inputs: readonly [{ + readonly type: "uint8"; + readonly name: "tick"; + readonly indexed: true; + }, { + readonly type: "address"; + readonly name: "maker"; + readonly indexed: true; + }, { + readonly type: "uint256"; + readonly name: "orderIndex"; + readonly indexed: false; + }, { + readonly type: "uint128"; + readonly name: "amount"; + readonly indexed: false; + }]; +}, { + readonly type: "event"; + readonly name: "TickSettled"; + readonly inputs: readonly [{ + readonly type: "uint8"; + readonly name: "tick"; + readonly indexed: true; + }, { + readonly type: "uint32"; + readonly name: "epoch"; + readonly indexed: false; + }, { + readonly type: "uint128"; + readonly name: "yesMatched"; + readonly indexed: false; + }, { + readonly type: "uint128"; + readonly name: "noMatched"; + readonly indexed: false; + }, { + readonly type: "uint256"; + readonly name: "clearingPrice"; + readonly indexed: false; + }]; +}, { + readonly type: "event"; + readonly name: "EpochCompleted"; + readonly inputs: readonly [{ + readonly type: "uint32"; + readonly name: "epoch"; + readonly indexed: false; + }, { + readonly type: "uint256"; + readonly name: "totalVolume"; + readonly indexed: false; + }, { + readonly type: "uint16"; + readonly name: "ticksActive"; + readonly indexed: false; + }]; +}, { + readonly type: "event"; + readonly name: "MarketResolved"; + readonly inputs: readonly [{ + readonly type: "bool"; + readonly name: "outcomeYes"; + readonly indexed: false; + }, { + readonly type: "address"; + readonly name: "resolvedBy"; + readonly indexed: true; + }]; +}, { + readonly type: "event"; + readonly name: "PayoutClaimed"; + readonly inputs: readonly [{ + readonly type: "uint8"; + readonly name: "tick"; + readonly indexed: true; + }, { + readonly type: "address"; + readonly name: "maker"; + readonly indexed: true; + }, { + readonly type: "uint256"; + readonly name: "amount"; + readonly indexed: false; + }]; +}]; +export declare const FACTORY_ABI: readonly [{ + readonly type: "function"; + readonly name: "createMarket"; + readonly inputs: readonly [{ + readonly type: "string"; + readonly name: "question"; + }, { + readonly type: "address"; + readonly name: "_resolver"; + }]; + readonly outputs: readonly [{ + readonly type: "address"; + }]; + readonly stateMutability: "nonpayable"; +}, { + readonly type: "function"; + readonly name: "getMarketCount"; + readonly inputs: readonly []; + readonly outputs: readonly [{ + readonly type: "uint256"; + }]; + readonly stateMutability: "view"; +}, { + readonly type: "function"; + readonly name: "getAllMarkets"; + readonly inputs: readonly []; + readonly outputs: readonly [{ + readonly type: "address[]"; + }]; + readonly stateMutability: "view"; +}, { + readonly type: "event"; + readonly name: "MarketCreated"; + readonly inputs: readonly [{ + readonly type: "address"; + readonly name: "market"; + readonly indexed: true; + }, { + readonly type: "string"; + readonly name: "question"; + readonly indexed: false; + }, { + readonly type: "address"; + readonly name: "creator"; + readonly indexed: true; + }, { + readonly type: "address"; + readonly name: "resolver"; + readonly indexed: true; + }, { + readonly type: "uint256"; + readonly name: "index"; + readonly indexed: false; + }]; +}]; +export declare const BENCHMARK_ABI: readonly [{ + readonly type: "function"; + readonly name: "placeOrder"; + readonly inputs: readonly [{ + readonly type: "uint256"; + readonly name: "amount"; + }]; + readonly outputs: readonly []; + readonly stateMutability: "nonpayable"; +}, { + readonly type: "function"; + readonly name: "globalCounter"; + readonly inputs: readonly []; + readonly outputs: readonly [{ + readonly type: "uint256"; + }]; + readonly stateMutability: "view"; +}, { + readonly type: "function"; + readonly name: "getOrderCount"; + readonly inputs: readonly []; + readonly outputs: readonly [{ + readonly type: "uint256"; + }]; + readonly stateMutability: "view"; +}, { + readonly type: "event"; + readonly name: "OrderPlaced"; + readonly inputs: readonly [{ + readonly type: "uint256"; + readonly name: "orderId"; + readonly indexed: true; + }, { + readonly type: "address"; + readonly name: "maker"; + readonly indexed: true; + }, { + readonly type: "uint256"; + readonly name: "amount"; + readonly indexed: false; + }]; +}]; +//# sourceMappingURL=abis.d.ts.map \ No newline at end of file diff --git a/sdk/dist/abis.d.ts.map b/sdk/dist/abis.d.ts.map new file mode 100644 index 0000000..452c818 --- /dev/null +++ b/sdk/dist/abis.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"abis.d.ts","sourceRoot":"","sources":["../src/abis.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmEhB,CAAC;AAEX,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAKd,CAAC;AAEX,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAKhB,CAAC"} \ No newline at end of file diff --git a/sdk/dist/abis.js b/sdk/dist/abis.js new file mode 100644 index 0000000..4f69be8 --- /dev/null +++ b/sdk/dist/abis.js @@ -0,0 +1,82 @@ +// ABI definitions for FlashGrid smart contracts. +export const FLASHGRID_ABI = [ + { type: "function", name: "NUM_TICKS", inputs: [], outputs: [{ type: "uint8" }], stateMutability: "view" }, + { type: "function", name: "MIN_ORDER_SIZE", inputs: [], outputs: [{ type: "uint128" }], stateMutability: "view" }, + { type: "function", name: "currentEpoch", inputs: [], outputs: [{ type: "uint32" }], stateMutability: "view" }, + { type: "function", name: "marketQuestion", inputs: [], outputs: [{ type: "string" }], stateMutability: "view" }, + { type: "function", name: "resolver", inputs: [], outputs: [{ type: "address" }], stateMutability: "view" }, + { type: "function", name: "resolved", inputs: [], outputs: [{ type: "bool" }], stateMutability: "view" }, + { type: "function", name: "outcomeYes", inputs: [], outputs: [{ type: "bool" }], stateMutability: "view" }, + { type: "function", name: "paused", inputs: [], outputs: [{ type: "bool" }], stateMutability: "view" }, + { type: "function", name: "balances", inputs: [{ type: "address" }], outputs: [{ type: "uint256" }], stateMutability: "view" }, + { + type: "function", name: "getTickState", inputs: [{ type: "uint8", name: "tick" }], + outputs: [ + { type: "uint128", name: "yesLiquidity" }, + { type: "uint128", name: "noLiquidity" }, + { type: "uint32", name: "orderCount" }, + { type: "uint32", name: "lastSettledEpoch" }, + ], + stateMutability: "view", + }, + { + type: "function", name: "getAllTickStates", inputs: [], + outputs: [{ + type: "tuple[20]", + components: [ + { type: "uint128", name: "totalYesLiquidity" }, + { type: "uint128", name: "totalNoLiquidity" }, + { type: "uint32", name: "orderCount" }, + { type: "uint32", name: "lastSettledEpoch" }, + ], + }], + stateMutability: "view", + }, + { + type: "function", name: "getTickOrders", inputs: [{ type: "uint8", name: "tick" }], + outputs: [{ + type: "tuple[]", + components: [ + { type: "address", name: "maker" }, + { type: "uint128", name: "amount" }, + { type: "bool", name: "isYes" }, + { type: "uint32", name: "epoch" }, + { type: "bool", name: "cancelled" }, + { type: "bool", name: "claimed" }, + ], + }], + stateMutability: "view", + }, + { type: "function", name: "getTickOrderCount", inputs: [{ type: "uint8", name: "tick" }], outputs: [{ type: "uint256" }], stateMutability: "view" }, + { type: "function", name: "deposit", inputs: [], outputs: [], stateMutability: "payable" }, + { type: "function", name: "withdraw", inputs: [{ type: "uint256", name: "amount" }], outputs: [], stateMutability: "nonpayable" }, + { type: "function", name: "placeOrder", inputs: [{ type: "uint8", name: "tick" }, { type: "uint128", name: "amount" }, { type: "bool", name: "isYes" }], outputs: [], stateMutability: "nonpayable" }, + { type: "function", name: "cancelOrder", inputs: [{ type: "uint8", name: "tick" }, { type: "uint256", name: "orderIndex" }], outputs: [], stateMutability: "nonpayable" }, + { type: "function", name: "resolveMarket", inputs: [{ type: "bool", name: "_outcomeYes" }], outputs: [], stateMutability: "nonpayable" }, + { type: "function", name: "settleTick", inputs: [{ type: "uint8", name: "tick" }], outputs: [], stateMutability: "nonpayable" }, + { type: "function", name: "settleAll", inputs: [], outputs: [], stateMutability: "nonpayable" }, + { type: "function", name: "pause", inputs: [], outputs: [], stateMutability: "nonpayable" }, + { type: "function", name: "unpause", inputs: [], outputs: [], stateMutability: "nonpayable" }, + // Events + { type: "event", name: "Deposited", inputs: [{ type: "address", name: "user", indexed: true }, { type: "uint256", name: "amount", indexed: false }] }, + { type: "event", name: "Withdrawn", inputs: [{ type: "address", name: "user", indexed: true }, { type: "uint256", name: "amount", indexed: false }] }, + { type: "event", name: "OrderPlaced", inputs: [{ type: "uint8", name: "tick", indexed: true }, { type: "address", name: "maker", indexed: true }, { type: "uint128", name: "amount", indexed: false }, { type: "bool", name: "isYes", indexed: false }, { type: "uint32", name: "epoch", indexed: false }] }, + { type: "event", name: "OrderCancelled", inputs: [{ type: "uint8", name: "tick", indexed: true }, { type: "address", name: "maker", indexed: true }, { type: "uint256", name: "orderIndex", indexed: false }, { type: "uint128", name: "amount", indexed: false }] }, + { type: "event", name: "TickSettled", inputs: [{ type: "uint8", name: "tick", indexed: true }, { type: "uint32", name: "epoch", indexed: false }, { type: "uint128", name: "yesMatched", indexed: false }, { type: "uint128", name: "noMatched", indexed: false }, { type: "uint256", name: "clearingPrice", indexed: false }] }, + { type: "event", name: "EpochCompleted", inputs: [{ type: "uint32", name: "epoch", indexed: false }, { type: "uint256", name: "totalVolume", indexed: false }, { type: "uint16", name: "ticksActive", indexed: false }] }, + { type: "event", name: "MarketResolved", inputs: [{ type: "bool", name: "outcomeYes", indexed: false }, { type: "address", name: "resolvedBy", indexed: true }] }, + { type: "event", name: "PayoutClaimed", inputs: [{ type: "uint8", name: "tick", indexed: true }, { type: "address", name: "maker", indexed: true }, { type: "uint256", name: "amount", indexed: false }] }, +]; +export const FACTORY_ABI = [ + { type: "function", name: "createMarket", inputs: [{ type: "string", name: "question" }, { type: "address", name: "_resolver" }], outputs: [{ type: "address" }], stateMutability: "nonpayable" }, + { type: "function", name: "getMarketCount", inputs: [], outputs: [{ type: "uint256" }], stateMutability: "view" }, + { type: "function", name: "getAllMarkets", inputs: [], outputs: [{ type: "address[]" }], stateMutability: "view" }, + { type: "event", name: "MarketCreated", inputs: [{ type: "address", name: "market", indexed: true }, { type: "string", name: "question", indexed: false }, { type: "address", name: "creator", indexed: true }, { type: "address", name: "resolver", indexed: true }, { type: "uint256", name: "index", indexed: false }] }, +]; +export const BENCHMARK_ABI = [ + { type: "function", name: "placeOrder", inputs: [{ type: "uint256", name: "amount" }], outputs: [], stateMutability: "nonpayable" }, + { type: "function", name: "globalCounter", inputs: [], outputs: [{ type: "uint256" }], stateMutability: "view" }, + { type: "function", name: "getOrderCount", inputs: [], outputs: [{ type: "uint256" }], stateMutability: "view" }, + { type: "event", name: "OrderPlaced", inputs: [{ type: "uint256", name: "orderId", indexed: true }, { type: "address", name: "maker", indexed: true }, { type: "uint256", name: "amount", indexed: false }] }, +]; +//# sourceMappingURL=abis.js.map \ No newline at end of file diff --git a/sdk/dist/abis.js.map b/sdk/dist/abis.js.map new file mode 100644 index 0000000..e239de9 --- /dev/null +++ b/sdk/dist/abis.js.map @@ -0,0 +1 @@ +{"version":3,"file":"abis.js","sourceRoot":"","sources":["../src/abis.ts"],"names":[],"mappings":"AAAA,iDAAiD;AAEjD,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE,eAAe,EAAE,MAAM,EAAE;IAC1G,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,gBAAgB,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,EAAE,eAAe,EAAE,MAAM,EAAE;IACjH,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,EAAE,eAAe,EAAE,MAAM,EAAE;IAC9G,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,gBAAgB,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,EAAE,eAAe,EAAE,MAAM,EAAE;IAChH,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,EAAE,eAAe,EAAE,MAAM,EAAE;IAC3G,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE,eAAe,EAAE,MAAM,EAAE;IACxG,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE,eAAe,EAAE,MAAM,EAAE;IAC1G,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE,eAAe,EAAE,MAAM,EAAE;IACtG,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,EAAE,eAAe,EAAE,MAAM,EAAE;IAC9H;QACE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;QACjF,OAAO,EAAE;YACP,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,cAAc,EAAE;YACzC,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,aAAa,EAAE;YACxC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,EAAE;YACtC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,kBAAkB,EAAE;SAC7C;QACD,eAAe,EAAE,MAAM;KACxB;IACD;QACE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,kBAAkB,EAAE,MAAM,EAAE,EAAE;QACtD,OAAO,EAAE,CAAC;gBACR,IAAI,EAAE,WAAW;gBACjB,UAAU,EAAE;oBACV,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,mBAAmB,EAAE;oBAC9C,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,kBAAkB,EAAE;oBAC7C,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,EAAE;oBACtC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,kBAAkB,EAAE;iBAC7C;aACF,CAAC;QACF,eAAe,EAAE,MAAM;KACxB;IACD;QACE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;QAClF,OAAO,EAAE,CAAC;gBACR,IAAI,EAAE,SAAS;gBACf,UAAU,EAAE;oBACV,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE;oBAClC,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,EAAE;oBACnC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE;oBAC/B,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE;oBACjC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE;oBACnC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE;iBAClC;aACF,CAAC;QACF,eAAe,EAAE,MAAM;KACxB;IACD,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,mBAAmB,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,EAAE,eAAe,EAAE,MAAM,EAAE;IACnJ,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,eAAe,EAAE,SAAS,EAAE;IAC1F,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,eAAe,EAAE,YAAY,EAAE;IACjI,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,eAAe,EAAE,YAAY,EAAE;IACrM,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,eAAe,EAAE,YAAY,EAAE;IACzK,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,eAAe,EAAE,YAAY,EAAE;IACxI,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,eAAe,EAAE,YAAY,EAAE;IAC/H,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,eAAe,EAAE,YAAY,EAAE;IAC/F,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,eAAe,EAAE,YAAY,EAAE;IAC3F,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,eAAe,EAAE,YAAY,EAAE;IAC7F,SAAS;IACT,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE;IACrJ,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE;IACrJ,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE;IAC5S,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,gBAAgB,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE;IACpQ,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE;IAChU,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,gBAAgB,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE;IACzN,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,gBAAgB,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE;IACjK,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE;CAClM,CAAC;AAEX,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,EAAE,eAAe,EAAE,YAAY,EAAE;IACjM,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,gBAAgB,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,EAAE,eAAe,EAAE,MAAM,EAAE;IACjH,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,EAAE,eAAe,EAAE,MAAM,EAAE;IAClH,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE;CACnT,CAAC;AAEX,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,eAAe,EAAE,YAAY,EAAE;IACnI,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,EAAE,eAAe,EAAE,MAAM,EAAE;IAChH,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,EAAE,eAAe,EAAE,MAAM,EAAE;IAChH,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE;CACrM,CAAC"} \ No newline at end of file diff --git a/sdk/dist/chains.d.ts b/sdk/dist/chains.d.ts new file mode 100644 index 0000000..3e1d3d5 --- /dev/null +++ b/sdk/dist/chains.d.ts @@ -0,0 +1,6 @@ +import type { ChainConfig } from "./types"; +export declare const CHAINS: Record; +export declare function getChain(chainId: number): ChainConfig; +export declare function getExplorerTxUrl(chainId: number, hash: string): string; +export declare function getExplorerAddressUrl(chainId: number, address: string): string; +//# sourceMappingURL=chains.d.ts.map \ No newline at end of file diff --git a/sdk/dist/chains.d.ts.map b/sdk/dist/chains.d.ts.map new file mode 100644 index 0000000..d034baa --- /dev/null +++ b/sdk/dist/chains.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"chains.d.ts","sourceRoot":"","sources":["../src/chains.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C,eAAO,MAAM,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAsB9C,CAAC;AAEF,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,CAIrD;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEtE;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAE9E"} \ No newline at end of file diff --git a/sdk/dist/chains.js b/sdk/dist/chains.js new file mode 100644 index 0000000..9334f92 --- /dev/null +++ b/sdk/dist/chains.js @@ -0,0 +1,37 @@ +// Multi-chain configuration for FlashGrid. +export const CHAINS = { + 10143: { + id: 10143, + name: "Monad Testnet", + nativeCurrency: { name: "Monad", symbol: "MON", decimals: 18 }, + rpcUrls: { default: { http: ["https://testnet-rpc.monad.xyz"] } }, + blockExplorers: { default: { name: "MonadVision", url: "https://testnet.monadvision.com" } }, + }, + 11155111: { + id: 11155111, + name: "Sepolia", + nativeCurrency: { name: "Sepolia Ether", symbol: "ETH", decimals: 18 }, + rpcUrls: { default: { http: ["https://rpc.sepolia.org"] } }, + blockExplorers: { default: { name: "Etherscan", url: "https://sepolia.etherscan.io" } }, + }, + 84532: { + id: 84532, + name: "Base Sepolia", + nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 }, + rpcUrls: { default: { http: ["https://sepolia.base.org"] } }, + blockExplorers: { default: { name: "Basescan", url: "https://sepolia.basescan.org" } }, + }, +}; +export function getChain(chainId) { + const chain = CHAINS[chainId]; + if (!chain) + throw new Error(`Unsupported chain ID: ${chainId}`); + return chain; +} +export function getExplorerTxUrl(chainId, hash) { + return `${getChain(chainId).blockExplorers.default.url}/tx/${hash}`; +} +export function getExplorerAddressUrl(chainId, address) { + return `${getChain(chainId).blockExplorers.default.url}/address/${address}`; +} +//# sourceMappingURL=chains.js.map \ No newline at end of file diff --git a/sdk/dist/chains.js.map b/sdk/dist/chains.js.map new file mode 100644 index 0000000..84726c0 --- /dev/null +++ b/sdk/dist/chains.js.map @@ -0,0 +1 @@ +{"version":3,"file":"chains.js","sourceRoot":"","sources":["../src/chains.ts"],"names":[],"mappings":"AAAA,2CAA2C;AAI3C,MAAM,CAAC,MAAM,MAAM,GAAgC;IACjD,KAAK,EAAE;QACL,EAAE,EAAE,KAAK;QACT,IAAI,EAAE,eAAe;QACrB,cAAc,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE;QAC9D,OAAO,EAAE,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,CAAC,+BAA+B,CAAC,EAAE,EAAE;QACjE,cAAc,EAAE,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE,GAAG,EAAE,iCAAiC,EAAE,EAAE;KAC7F;IACD,QAAQ,EAAE;QACR,EAAE,EAAE,QAAQ;QACZ,IAAI,EAAE,SAAS;QACf,cAAc,EAAE,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE;QACtE,OAAO,EAAE,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,CAAC,yBAAyB,CAAC,EAAE,EAAE;QAC3D,cAAc,EAAE,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,EAAE,8BAA8B,EAAE,EAAE;KACxF;IACD,KAAK,EAAE;QACL,EAAE,EAAE,KAAK;QACT,IAAI,EAAE,cAAc;QACpB,cAAc,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE;QAC9D,OAAO,EAAE,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,CAAC,0BAA0B,CAAC,EAAE,EAAE;QAC5D,cAAc,EAAE,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,8BAA8B,EAAE,EAAE;KACvF;CACF,CAAC;AAEF,MAAM,UAAU,QAAQ,CAAC,OAAe;IACtC,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;IAC9B,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,yBAAyB,OAAO,EAAE,CAAC,CAAC;IAChE,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,OAAe,EAAE,IAAY;IAC5D,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,cAAc,CAAC,OAAO,CAAC,GAAG,OAAO,IAAI,EAAE,CAAC;AACtE,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,OAAe,EAAE,OAAe;IACpE,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,cAAc,CAAC,OAAO,CAAC,GAAG,YAAY,OAAO,EAAE,CAAC;AAC9E,CAAC"} \ No newline at end of file diff --git a/sdk/dist/client.d.ts b/sdk/dist/client.d.ts new file mode 100644 index 0000000..c8e9562 --- /dev/null +++ b/sdk/dist/client.d.ts @@ -0,0 +1,129 @@ +import { type PublicClient, type WalletClient, type Hash } from "viem"; +import type { ChainConfig } from "./types"; +export interface FlashGridClientConfig { + chainId: number; + marketAddress: `0x${string}`; + factoryAddress?: `0x${string}`; +} +export declare class FlashGridClient { + readonly chainId: number; + readonly chain: ChainConfig; + readonly marketAddress: `0x${string}`; + readonly factoryAddress?: `0x${string}`; + readonly publicClient: PublicClient; + constructor(config: FlashGridClientConfig); + getBalance(address: `0x${string}`): Promise; + getTickState(tick: number): Promise; + getAllTickStates(): Promise; + isResolved(): Promise; + getOutcome(): Promise; + getCurrentEpoch(): Promise; + getMarketQuestion(): Promise; + deposit(walletClient: WalletClient, amount: string): Promise; + withdraw(walletClient: WalletClient, amount: string): Promise; + placeOrder(walletClient: WalletClient, tick: number, amount: string, isYes: boolean): Promise; + cancelOrder(walletClient: WalletClient, tick: number, orderIndex: bigint): Promise; + resolveMarket(walletClient: WalletClient, outcomeYes: boolean): Promise; + settleAll(walletClient: WalletClient): Promise; +} +//# sourceMappingURL=client.d.ts.map \ No newline at end of file diff --git a/sdk/dist/client.d.ts.map b/sdk/dist/client.d.ts.map new file mode 100644 index 0000000..e4d3ee7 --- /dev/null +++ b/sdk/dist/client.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAEA,OAAO,EAML,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,IAAI,EAGV,MAAM,MAAM,CAAC;AAGd,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,KAAK,MAAM,EAAE,CAAC;IAC7B,cAAc,CAAC,EAAE,KAAK,MAAM,EAAE,CAAC;CAChC;AAED,qBAAa,eAAe;IAC1B,SAAgB,OAAO,EAAE,MAAM,CAAC;IAChC,SAAgB,KAAK,EAAE,WAAW,CAAC;IACnC,SAAgB,aAAa,EAAE,KAAK,MAAM,EAAE,CAAC;IAC7C,SAAgB,cAAc,CAAC,EAAE,KAAK,MAAM,EAAE,CAAC;IAC/C,SAAgB,YAAY,EAAE,YAAY,CAAC;gBAE/B,MAAM,EAAE,qBAAqB;IAmBnC,UAAU,CAAC,OAAO,EAAE,KAAK,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAUnD,YAAY,CAAC,IAAI,EAAE,MAAM;IASzB,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAQhB,UAAU,IAAI,OAAO,CAAC,OAAO,CAAC;IAQ9B,UAAU,IAAI,OAAO,CAAC,OAAO,CAAC;IAQ9B,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC;IAQlC,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC;IAUpC,OAAO,CAAC,YAAY,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAalE,QAAQ,CAAC,YAAY,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAanE,UAAU,CACd,YAAY,EAAE,YAAY,EAC1B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,OAAO,GACb,OAAO,CAAC,IAAI,CAAC;IAaV,WAAW,CACf,YAAY,EAAE,YAAY,EAC1B,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC;IAaV,aAAa,CACjB,YAAY,EAAE,YAAY,EAC1B,UAAU,EAAE,OAAO,GAClB,OAAO,CAAC,IAAI,CAAC;IAaV,SAAS,CAAC,YAAY,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;CAW3D"} \ No newline at end of file diff --git a/sdk/dist/client.js b/sdk/dist/client.js new file mode 100644 index 0000000..4f67bc5 --- /dev/null +++ b/sdk/dist/client.js @@ -0,0 +1,147 @@ +// High-level client for interacting with FlashGrid markets. +import { createPublicClient, http, parseEther, formatEther, } from "viem"; +import { FLASHGRID_ABI } from "./abis"; +import { getChain } from "./chains"; +export class FlashGridClient { + constructor(config) { + this.chainId = config.chainId; + this.chain = getChain(config.chainId); + this.marketAddress = config.marketAddress; + this.factoryAddress = config.factoryAddress; + this.publicClient = createPublicClient({ + chain: { + id: this.chain.id, + name: this.chain.name, + nativeCurrency: this.chain.nativeCurrency, + rpcUrls: this.chain.rpcUrls, + }, + transport: http(this.chain.rpcUrls.default.http[0]), + }); + } + // ── Read Methods ──────────────────────────────────────────── + async getBalance(address) { + const bal = await this.publicClient.readContract({ + address: this.marketAddress, + abi: FLASHGRID_ABI, + functionName: "balances", + args: [address], + }); + return formatEther(bal); + } + async getTickState(tick) { + return this.publicClient.readContract({ + address: this.marketAddress, + abi: FLASHGRID_ABI, + functionName: "getTickState", + args: [tick], + }); + } + async getAllTickStates() { + return this.publicClient.readContract({ + address: this.marketAddress, + abi: FLASHGRID_ABI, + functionName: "getAllTickStates", + }); + } + async isResolved() { + return this.publicClient.readContract({ + address: this.marketAddress, + abi: FLASHGRID_ABI, + functionName: "resolved", + }); + } + async getOutcome() { + return this.publicClient.readContract({ + address: this.marketAddress, + abi: FLASHGRID_ABI, + functionName: "outcomeYes", + }); + } + async getCurrentEpoch() { + return this.publicClient.readContract({ + address: this.marketAddress, + abi: FLASHGRID_ABI, + functionName: "currentEpoch", + }); + } + async getMarketQuestion() { + return this.publicClient.readContract({ + address: this.marketAddress, + abi: FLASHGRID_ABI, + functionName: "marketQuestion", + }); + } + // ── Write Methods ─────────────────────────────────────────── + async deposit(walletClient, amount) { + const hash = await walletClient.writeContract({ + address: this.marketAddress, + abi: FLASHGRID_ABI, + functionName: "deposit", + value: parseEther(amount), + chain: this.publicClient.chain, + account: walletClient.account, + }); + await this.publicClient.waitForTransactionReceipt({ hash }); + return hash; + } + async withdraw(walletClient, amount) { + const hash = await walletClient.writeContract({ + address: this.marketAddress, + abi: FLASHGRID_ABI, + functionName: "withdraw", + args: [parseEther(amount)], + chain: this.publicClient.chain, + account: walletClient.account, + }); + await this.publicClient.waitForTransactionReceipt({ hash }); + return hash; + } + async placeOrder(walletClient, tick, amount, isYes) { + const hash = await walletClient.writeContract({ + address: this.marketAddress, + abi: FLASHGRID_ABI, + functionName: "placeOrder", + args: [tick, parseEther(amount), isYes], + chain: this.publicClient.chain, + account: walletClient.account, + }); + await this.publicClient.waitForTransactionReceipt({ hash }); + return hash; + } + async cancelOrder(walletClient, tick, orderIndex) { + const hash = await walletClient.writeContract({ + address: this.marketAddress, + abi: FLASHGRID_ABI, + functionName: "cancelOrder", + args: [tick, orderIndex], + chain: this.publicClient.chain, + account: walletClient.account, + }); + await this.publicClient.waitForTransactionReceipt({ hash }); + return hash; + } + async resolveMarket(walletClient, outcomeYes) { + const hash = await walletClient.writeContract({ + address: this.marketAddress, + abi: FLASHGRID_ABI, + functionName: "resolveMarket", + args: [outcomeYes], + chain: this.publicClient.chain, + account: walletClient.account, + }); + await this.publicClient.waitForTransactionReceipt({ hash }); + return hash; + } + async settleAll(walletClient) { + const hash = await walletClient.writeContract({ + address: this.marketAddress, + abi: FLASHGRID_ABI, + functionName: "settleAll", + chain: this.publicClient.chain, + account: walletClient.account, + }); + await this.publicClient.waitForTransactionReceipt({ hash }); + return hash; + } +} +//# sourceMappingURL=client.js.map \ No newline at end of file diff --git a/sdk/dist/client.js.map b/sdk/dist/client.js.map new file mode 100644 index 0000000..0925688 --- /dev/null +++ b/sdk/dist/client.js.map @@ -0,0 +1 @@ +{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/sdk/dist/index.d.ts b/sdk/dist/index.d.ts new file mode 100644 index 0000000..637f4d6 --- /dev/null +++ b/sdk/dist/index.d.ts @@ -0,0 +1,6 @@ +export { FlashGridClient, type FlashGridClientConfig } from "./client"; +export { FLASHGRID_ABI, FACTORY_ABI, BENCHMARK_ABI } from "./abis"; +export { CHAINS, getChain, getExplorerTxUrl, getExplorerAddressUrl } from "./chains"; +export type { TickState, Order, OrderPlacedEvent, OrderCancelledEvent, TickSettledEvent, EpochCompletedEvent, MarketResolvedEvent, PayoutClaimedEvent, LiveOrder, ExecutionMetrics, ChainConfig, } from "./types"; +export { NUM_TICKS, TICK_PRICES, TICK_LABELS } from "./types"; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/sdk/dist/index.d.ts.map b/sdk/dist/index.d.ts.map new file mode 100644 index 0000000..0534a78 --- /dev/null +++ b/sdk/dist/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,KAAK,qBAAqB,EAAE,MAAM,UAAU,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AACnE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AACrF,YAAY,EACV,SAAS,EACT,KAAK,EACL,gBAAgB,EAChB,mBAAmB,EACnB,gBAAgB,EAChB,mBAAmB,EACnB,mBAAmB,EACnB,kBAAkB,EAClB,SAAS,EACT,gBAAgB,EAChB,WAAW,GACZ,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC"} \ No newline at end of file diff --git a/sdk/dist/index.js b/sdk/dist/index.js new file mode 100644 index 0000000..13addec --- /dev/null +++ b/sdk/dist/index.js @@ -0,0 +1,6 @@ +// @flashgrid/sdk — TypeScript SDK for FlashGrid parallel batch auction protocol +export { FlashGridClient } from "./client"; +export { FLASHGRID_ABI, FACTORY_ABI, BENCHMARK_ABI } from "./abis"; +export { CHAINS, getChain, getExplorerTxUrl, getExplorerAddressUrl } from "./chains"; +export { NUM_TICKS, TICK_PRICES, TICK_LABELS } from "./types"; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/sdk/dist/index.js.map b/sdk/dist/index.js.map new file mode 100644 index 0000000..28cbdac --- /dev/null +++ b/sdk/dist/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAEhF,OAAO,EAAE,eAAe,EAA8B,MAAM,UAAU,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AACnE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AAcrF,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC"} \ No newline at end of file diff --git a/sdk/dist/types.d.ts b/sdk/dist/types.d.ts new file mode 100644 index 0000000..c34a1cf --- /dev/null +++ b/sdk/dist/types.d.ts @@ -0,0 +1,104 @@ +export interface TickState { + totalYesLiquidity: bigint; + totalNoLiquidity: bigint; + orderCount: number; + lastSettledEpoch: number; +} +export interface Order { + maker: string; + amount: bigint; + isYes: boolean; + epoch: number; + cancelled: boolean; + claimed: boolean; +} +export interface OrderPlacedEvent { + tick: number; + maker: string; + amount: bigint; + isYes: boolean; + epoch: number; + blockNumber: bigint; + transactionHash: string; + timestamp: number; +} +export interface OrderCancelledEvent { + tick: number; + maker: string; + orderIndex: bigint; + amount: bigint; + blockNumber: bigint; + transactionHash: string; +} +export interface TickSettledEvent { + tick: number; + epoch: number; + yesMatched: bigint; + noMatched: bigint; + clearingPrice: bigint; + blockNumber: bigint; + transactionHash: string; + timestamp: number; +} +export interface EpochCompletedEvent { + epoch: number; + totalVolume: bigint; + ticksActive: number; + blockNumber: bigint; + transactionHash: string; + timestamp: number; +} +export interface MarketResolvedEvent { + outcomeYes: boolean; + resolvedBy: string; + blockNumber: bigint; + transactionHash: string; +} +export interface PayoutClaimedEvent { + tick: number; + maker: string; + amount: bigint; + blockNumber: bigint; + transactionHash: string; +} +export interface LiveOrder { + id: string; + tick: number; + side: "YES" | "NO"; + amount: string; + maker: string; + blockNumber: number; + timestamp: number; +} +export interface ExecutionMetrics { + ordersPerBlock: number[]; + avgLatency: number; + successRate: number; + totalOrders: number; + activeEpoch: number; + activeTicks: number; +} +export interface ChainConfig { + id: number; + name: string; + nativeCurrency: { + name: string; + symbol: string; + decimals: number; + }; + rpcUrls: { + default: { + http: string[]; + }; + }; + blockExplorers: { + default: { + name: string; + url: string; + }; + }; +} +export declare const NUM_TICKS = 20; +export declare const TICK_PRICES: number[]; +export declare const TICK_LABELS: string[]; +//# sourceMappingURL=types.d.ts.map \ No newline at end of file diff --git a/sdk/dist/types.d.ts.map b/sdk/dist/types.d.ts.map new file mode 100644 index 0000000..685594e --- /dev/null +++ b/sdk/dist/types.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/sdk/dist/types.js b/sdk/dist/types.js new file mode 100644 index 0000000..9015448 --- /dev/null +++ b/sdk/dist/types.js @@ -0,0 +1,6 @@ +// Shared TypeScript type definitions for the FlashGrid protocol. +// ── Constants ─────────────────────────────────────────────────── +export const NUM_TICKS = 20; +export const TICK_PRICES = Array.from({ length: NUM_TICKS }, (_, i) => ((i + 1) * 5) / 100); +export const TICK_LABELS = TICK_PRICES.map((p) => `$${p.toFixed(2)}`); +//# sourceMappingURL=types.js.map \ No newline at end of file diff --git a/sdk/dist/types.js.map b/sdk/dist/types.js.map new file mode 100644 index 0000000..ed0179b --- /dev/null +++ b/sdk/dist/types.js.map @@ -0,0 +1 @@ +{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,iEAAiE;AA4GjE,mEAAmE;AAEnE,MAAM,CAAC,MAAM,SAAS,GAAG,EAAE,CAAC;AAE5B,MAAM,CAAC,MAAM,WAAW,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CACpE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,GAAG,CACpB,CAAC;AAEF,MAAM,CAAC,MAAM,WAAW,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC"} \ No newline at end of file diff --git a/sdk/package-lock.json b/sdk/package-lock.json new file mode 100644 index 0000000..1f0bf64 --- /dev/null +++ b/sdk/package-lock.json @@ -0,0 +1,258 @@ +{ + "name": "@flashgrid/sdk", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@flashgrid/sdk", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "viem": "^2.21.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.7.0" + }, + "peerDependencies": { + "viem": ">=2.0.0" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", + "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", + "license": "MIT" + }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", + "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@types/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/abitype": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz", + "integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3.22.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/isows": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz", + "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/ox": { + "version": "0.14.7", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.14.7.tgz", + "integrity": "sha512-zSQ/cfBdolj7U4++NAvH7sI+VG0T3pEohITCgcQj8KlawvTDY4vGVhDT64Atsm0d6adWfIYHDpu88iUBMMp+AQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.11.0", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "1.9.1", + "@noble/hashes": "^1.8.0", + "@scure/bip32": "^1.7.0", + "@scure/bip39": "^1.6.0", + "abitype": "^1.2.3", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/viem": { + "version": "2.47.10", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.47.10.tgz", + "integrity": "sha512-D+l6SDDZWB5bh8u9hgICzMX2/egMrgEQ+Pef/QkZgmOl6bOTyCQMSgWAH8jZTWJ/218J9QNv7s/9BH6Wu5oPDg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@noble/curves": "1.9.1", + "@noble/hashes": "1.8.0", + "@scure/bip32": "1.7.0", + "@scure/bip39": "1.6.0", + "abitype": "1.2.3", + "isows": "1.0.7", + "ox": "0.14.7", + "ws": "8.18.3" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/sdk/package.json b/sdk/package.json new file mode 100644 index 0000000..6009cf1 --- /dev/null +++ b/sdk/package.json @@ -0,0 +1,24 @@ +{ + "name": "@flashgrid/sdk", + "version": "0.1.0", + "description": "TypeScript SDK for interacting with FlashGrid parallel batch auction markets", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": ["dist"], + "scripts": { + "build": "tsc", + "typecheck": "tsc --noEmit" + }, + "keywords": ["flashgrid", "prediction-market", "parallel", "defi", "sdk"], + "license": "MIT", + "dependencies": { + "viem": "^2.21.0" + }, + "devDependencies": { + "typescript": "^5.7.0", + "@types/node": "^22.0.0" + }, + "peerDependencies": { + "viem": ">=2.0.0" + } +} diff --git a/sdk/src/abis.ts b/sdk/src/abis.ts new file mode 100644 index 0000000..ac11497 --- /dev/null +++ b/sdk/src/abis.ts @@ -0,0 +1,84 @@ +// ABI definitions for FlashGrid smart contracts. + +export const FLASHGRID_ABI = [ + { type: "function", name: "NUM_TICKS", inputs: [], outputs: [{ type: "uint8" }], stateMutability: "view" }, + { type: "function", name: "MIN_ORDER_SIZE", inputs: [], outputs: [{ type: "uint128" }], stateMutability: "view" }, + { type: "function", name: "currentEpoch", inputs: [], outputs: [{ type: "uint32" }], stateMutability: "view" }, + { type: "function", name: "marketQuestion", inputs: [], outputs: [{ type: "string" }], stateMutability: "view" }, + { type: "function", name: "resolver", inputs: [], outputs: [{ type: "address" }], stateMutability: "view" }, + { type: "function", name: "resolved", inputs: [], outputs: [{ type: "bool" }], stateMutability: "view" }, + { type: "function", name: "outcomeYes", inputs: [], outputs: [{ type: "bool" }], stateMutability: "view" }, + { type: "function", name: "paused", inputs: [], outputs: [{ type: "bool" }], stateMutability: "view" }, + { type: "function", name: "balances", inputs: [{ type: "address" }], outputs: [{ type: "uint256" }], stateMutability: "view" }, + { + type: "function", name: "getTickState", inputs: [{ type: "uint8", name: "tick" }], + outputs: [ + { type: "uint128", name: "yesLiquidity" }, + { type: "uint128", name: "noLiquidity" }, + { type: "uint32", name: "orderCount" }, + { type: "uint32", name: "lastSettledEpoch" }, + ], + stateMutability: "view", + }, + { + type: "function", name: "getAllTickStates", inputs: [], + outputs: [{ + type: "tuple[20]", + components: [ + { type: "uint128", name: "totalYesLiquidity" }, + { type: "uint128", name: "totalNoLiquidity" }, + { type: "uint32", name: "orderCount" }, + { type: "uint32", name: "lastSettledEpoch" }, + ], + }], + stateMutability: "view", + }, + { + type: "function", name: "getTickOrders", inputs: [{ type: "uint8", name: "tick" }], + outputs: [{ + type: "tuple[]", + components: [ + { type: "address", name: "maker" }, + { type: "uint128", name: "amount" }, + { type: "bool", name: "isYes" }, + { type: "uint32", name: "epoch" }, + { type: "bool", name: "cancelled" }, + { type: "bool", name: "claimed" }, + ], + }], + stateMutability: "view", + }, + { type: "function", name: "getTickOrderCount", inputs: [{ type: "uint8", name: "tick" }], outputs: [{ type: "uint256" }], stateMutability: "view" }, + { type: "function", name: "deposit", inputs: [], outputs: [], stateMutability: "payable" }, + { type: "function", name: "withdraw", inputs: [{ type: "uint256", name: "amount" }], outputs: [], stateMutability: "nonpayable" }, + { type: "function", name: "placeOrder", inputs: [{ type: "uint8", name: "tick" }, { type: "uint128", name: "amount" }, { type: "bool", name: "isYes" }], outputs: [], stateMutability: "nonpayable" }, + { type: "function", name: "cancelOrder", inputs: [{ type: "uint8", name: "tick" }, { type: "uint256", name: "orderIndex" }], outputs: [], stateMutability: "nonpayable" }, + { type: "function", name: "resolveMarket", inputs: [{ type: "bool", name: "_outcomeYes" }], outputs: [], stateMutability: "nonpayable" }, + { type: "function", name: "settleTick", inputs: [{ type: "uint8", name: "tick" }], outputs: [], stateMutability: "nonpayable" }, + { type: "function", name: "settleAll", inputs: [], outputs: [], stateMutability: "nonpayable" }, + { type: "function", name: "pause", inputs: [], outputs: [], stateMutability: "nonpayable" }, + { type: "function", name: "unpause", inputs: [], outputs: [], stateMutability: "nonpayable" }, + // Events + { type: "event", name: "Deposited", inputs: [{ type: "address", name: "user", indexed: true }, { type: "uint256", name: "amount", indexed: false }] }, + { type: "event", name: "Withdrawn", inputs: [{ type: "address", name: "user", indexed: true }, { type: "uint256", name: "amount", indexed: false }] }, + { type: "event", name: "OrderPlaced", inputs: [{ type: "uint8", name: "tick", indexed: true }, { type: "address", name: "maker", indexed: true }, { type: "uint128", name: "amount", indexed: false }, { type: "bool", name: "isYes", indexed: false }, { type: "uint32", name: "epoch", indexed: false }] }, + { type: "event", name: "OrderCancelled", inputs: [{ type: "uint8", name: "tick", indexed: true }, { type: "address", name: "maker", indexed: true }, { type: "uint256", name: "orderIndex", indexed: false }, { type: "uint128", name: "amount", indexed: false }] }, + { type: "event", name: "TickSettled", inputs: [{ type: "uint8", name: "tick", indexed: true }, { type: "uint32", name: "epoch", indexed: false }, { type: "uint128", name: "yesMatched", indexed: false }, { type: "uint128", name: "noMatched", indexed: false }, { type: "uint256", name: "clearingPrice", indexed: false }] }, + { type: "event", name: "EpochCompleted", inputs: [{ type: "uint32", name: "epoch", indexed: false }, { type: "uint256", name: "totalVolume", indexed: false }, { type: "uint16", name: "ticksActive", indexed: false }] }, + { type: "event", name: "MarketResolved", inputs: [{ type: "bool", name: "outcomeYes", indexed: false }, { type: "address", name: "resolvedBy", indexed: true }] }, + { type: "event", name: "PayoutClaimed", inputs: [{ type: "uint8", name: "tick", indexed: true }, { type: "address", name: "maker", indexed: true }, { type: "uint256", name: "amount", indexed: false }] }, +] as const; + +export const FACTORY_ABI = [ + { type: "function", name: "createMarket", inputs: [{ type: "string", name: "question" }, { type: "address", name: "_resolver" }], outputs: [{ type: "address" }], stateMutability: "nonpayable" }, + { type: "function", name: "getMarketCount", inputs: [], outputs: [{ type: "uint256" }], stateMutability: "view" }, + { type: "function", name: "getAllMarkets", inputs: [], outputs: [{ type: "address[]" }], stateMutability: "view" }, + { type: "event", name: "MarketCreated", inputs: [{ type: "address", name: "market", indexed: true }, { type: "string", name: "question", indexed: false }, { type: "address", name: "creator", indexed: true }, { type: "address", name: "resolver", indexed: true }, { type: "uint256", name: "index", indexed: false }] }, +] as const; + +export const BENCHMARK_ABI = [ + { type: "function", name: "placeOrder", inputs: [{ type: "uint256", name: "amount" }], outputs: [], stateMutability: "nonpayable" }, + { type: "function", name: "globalCounter", inputs: [], outputs: [{ type: "uint256" }], stateMutability: "view" }, + { type: "function", name: "getOrderCount", inputs: [], outputs: [{ type: "uint256" }], stateMutability: "view" }, + { type: "event", name: "OrderPlaced", inputs: [{ type: "uint256", name: "orderId", indexed: true }, { type: "address", name: "maker", indexed: true }, { type: "uint256", name: "amount", indexed: false }] }, +] as const; diff --git a/sdk/src/chains.ts b/sdk/src/chains.ts new file mode 100644 index 0000000..1d77e36 --- /dev/null +++ b/sdk/src/chains.ts @@ -0,0 +1,41 @@ +// Multi-chain configuration for FlashGrid. + +import type { ChainConfig } from "./types"; + +export const CHAINS: Record = { + 10143: { + id: 10143, + name: "Monad Testnet", + nativeCurrency: { name: "Monad", symbol: "MON", decimals: 18 }, + rpcUrls: { default: { http: ["https://testnet-rpc.monad.xyz"] } }, + blockExplorers: { default: { name: "MonadVision", url: "https://testnet.monadvision.com" } }, + }, + 11155111: { + id: 11155111, + name: "Sepolia", + nativeCurrency: { name: "Sepolia Ether", symbol: "ETH", decimals: 18 }, + rpcUrls: { default: { http: ["https://rpc.sepolia.org"] } }, + blockExplorers: { default: { name: "Etherscan", url: "https://sepolia.etherscan.io" } }, + }, + 84532: { + id: 84532, + name: "Base Sepolia", + nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 }, + rpcUrls: { default: { http: ["https://sepolia.base.org"] } }, + blockExplorers: { default: { name: "Basescan", url: "https://sepolia.basescan.org" } }, + }, +}; + +export function getChain(chainId: number): ChainConfig { + const chain = CHAINS[chainId]; + if (!chain) throw new Error(`Unsupported chain ID: ${chainId}`); + return chain; +} + +export function getExplorerTxUrl(chainId: number, hash: string): string { + return `${getChain(chainId).blockExplorers.default.url}/tx/${hash}`; +} + +export function getExplorerAddressUrl(chainId: number, address: string): string { + return `${getChain(chainId).blockExplorers.default.url}/address/${address}`; +} diff --git a/sdk/src/client.ts b/sdk/src/client.ts new file mode 100644 index 0000000..915a086 --- /dev/null +++ b/sdk/src/client.ts @@ -0,0 +1,200 @@ +// High-level client for interacting with FlashGrid markets. + +import { + createPublicClient, + createWalletClient, + http, + parseEther, + formatEther, + type PublicClient, + type WalletClient, + type Hash, + type Account, + type Chain, +} from "viem"; +import { FLASHGRID_ABI, FACTORY_ABI } from "./abis"; +import { getChain } from "./chains"; +import type { ChainConfig } from "./types"; + +export interface FlashGridClientConfig { + chainId: number; + marketAddress: `0x${string}`; + factoryAddress?: `0x${string}`; +} + +export class FlashGridClient { + public readonly chainId: number; + public readonly chain: ChainConfig; + public readonly marketAddress: `0x${string}`; + public readonly factoryAddress?: `0x${string}`; + public readonly publicClient: PublicClient; + + constructor(config: FlashGridClientConfig) { + this.chainId = config.chainId; + this.chain = getChain(config.chainId); + this.marketAddress = config.marketAddress; + this.factoryAddress = config.factoryAddress; + + this.publicClient = createPublicClient({ + chain: { + id: this.chain.id, + name: this.chain.name, + nativeCurrency: this.chain.nativeCurrency, + rpcUrls: this.chain.rpcUrls, + } as Chain, + transport: http(this.chain.rpcUrls.default.http[0]), + }); + } + + // ── Read Methods ──────────────────────────────────────────── + + async getBalance(address: `0x${string}`): Promise { + const bal = await this.publicClient.readContract({ + address: this.marketAddress, + abi: FLASHGRID_ABI, + functionName: "balances", + args: [address], + }); + return formatEther(bal as bigint); + } + + async getTickState(tick: number) { + return this.publicClient.readContract({ + address: this.marketAddress, + abi: FLASHGRID_ABI, + functionName: "getTickState", + args: [tick], + }); + } + + async getAllTickStates() { + return this.publicClient.readContract({ + address: this.marketAddress, + abi: FLASHGRID_ABI, + functionName: "getAllTickStates", + }); + } + + async isResolved(): Promise { + return this.publicClient.readContract({ + address: this.marketAddress, + abi: FLASHGRID_ABI, + functionName: "resolved", + }) as Promise; + } + + async getOutcome(): Promise { + return this.publicClient.readContract({ + address: this.marketAddress, + abi: FLASHGRID_ABI, + functionName: "outcomeYes", + }) as Promise; + } + + async getCurrentEpoch(): Promise { + return this.publicClient.readContract({ + address: this.marketAddress, + abi: FLASHGRID_ABI, + functionName: "currentEpoch", + }) as Promise; + } + + async getMarketQuestion(): Promise { + return this.publicClient.readContract({ + address: this.marketAddress, + abi: FLASHGRID_ABI, + functionName: "marketQuestion", + }) as Promise; + } + + // ── Write Methods ─────────────────────────────────────────── + + async deposit(walletClient: WalletClient, amount: string): Promise { + const hash = await walletClient.writeContract({ + address: this.marketAddress, + abi: FLASHGRID_ABI, + functionName: "deposit", + value: parseEther(amount), + chain: this.publicClient.chain, + account: walletClient.account as Account, + }); + await this.publicClient.waitForTransactionReceipt({ hash }); + return hash; + } + + async withdraw(walletClient: WalletClient, amount: string): Promise { + const hash = await walletClient.writeContract({ + address: this.marketAddress, + abi: FLASHGRID_ABI, + functionName: "withdraw", + args: [parseEther(amount)], + chain: this.publicClient.chain, + account: walletClient.account as Account, + }); + await this.publicClient.waitForTransactionReceipt({ hash }); + return hash; + } + + async placeOrder( + walletClient: WalletClient, + tick: number, + amount: string, + isYes: boolean + ): Promise { + const hash = await walletClient.writeContract({ + address: this.marketAddress, + abi: FLASHGRID_ABI, + functionName: "placeOrder", + args: [tick, parseEther(amount), isYes], + chain: this.publicClient.chain, + account: walletClient.account as Account, + }); + await this.publicClient.waitForTransactionReceipt({ hash }); + return hash; + } + + async cancelOrder( + walletClient: WalletClient, + tick: number, + orderIndex: bigint + ): Promise { + const hash = await walletClient.writeContract({ + address: this.marketAddress, + abi: FLASHGRID_ABI, + functionName: "cancelOrder", + args: [tick, orderIndex], + chain: this.publicClient.chain, + account: walletClient.account as Account, + }); + await this.publicClient.waitForTransactionReceipt({ hash }); + return hash; + } + + async resolveMarket( + walletClient: WalletClient, + outcomeYes: boolean + ): Promise { + const hash = await walletClient.writeContract({ + address: this.marketAddress, + abi: FLASHGRID_ABI, + functionName: "resolveMarket", + args: [outcomeYes], + chain: this.publicClient.chain, + account: walletClient.account as Account, + }); + await this.publicClient.waitForTransactionReceipt({ hash }); + return hash; + } + + async settleAll(walletClient: WalletClient): Promise { + const hash = await walletClient.writeContract({ + address: this.marketAddress, + abi: FLASHGRID_ABI, + functionName: "settleAll", + chain: this.publicClient.chain, + account: walletClient.account as Account, + }); + await this.publicClient.waitForTransactionReceipt({ hash }); + return hash; + } +} diff --git a/sdk/src/index.ts b/sdk/src/index.ts new file mode 100644 index 0000000..105f115 --- /dev/null +++ b/sdk/src/index.ts @@ -0,0 +1,19 @@ +// @flashgrid/sdk — TypeScript SDK for FlashGrid parallel batch auction protocol + +export { FlashGridClient, type FlashGridClientConfig } from "./client"; +export { FLASHGRID_ABI, FACTORY_ABI, BENCHMARK_ABI } from "./abis"; +export { CHAINS, getChain, getExplorerTxUrl, getExplorerAddressUrl } from "./chains"; +export type { + TickState, + Order, + OrderPlacedEvent, + OrderCancelledEvent, + TickSettledEvent, + EpochCompletedEvent, + MarketResolvedEvent, + PayoutClaimedEvent, + LiveOrder, + ExecutionMetrics, + ChainConfig, +} from "./types"; +export { NUM_TICKS, TICK_PRICES, TICK_LABELS } from "./types"; diff --git a/sdk/src/types.ts b/sdk/src/types.ts new file mode 100644 index 0000000..3d15086 --- /dev/null +++ b/sdk/src/types.ts @@ -0,0 +1,117 @@ +// Shared TypeScript type definitions for the FlashGrid protocol. + +// ── Contract Types ────────────────────────────────────────────── + +export interface TickState { + totalYesLiquidity: bigint; + totalNoLiquidity: bigint; + orderCount: number; + lastSettledEpoch: number; +} + +export interface Order { + maker: string; + amount: bigint; + isYes: boolean; + epoch: number; + cancelled: boolean; + claimed: boolean; +} + +// ── Event Types ───────────────────────────────────────────────── + +export interface OrderPlacedEvent { + tick: number; + maker: string; + amount: bigint; + isYes: boolean; + epoch: number; + blockNumber: bigint; + transactionHash: string; + timestamp: number; +} + +export interface OrderCancelledEvent { + tick: number; + maker: string; + orderIndex: bigint; + amount: bigint; + blockNumber: bigint; + transactionHash: string; +} + +export interface TickSettledEvent { + tick: number; + epoch: number; + yesMatched: bigint; + noMatched: bigint; + clearingPrice: bigint; + blockNumber: bigint; + transactionHash: string; + timestamp: number; +} + +export interface EpochCompletedEvent { + epoch: number; + totalVolume: bigint; + ticksActive: number; + blockNumber: bigint; + transactionHash: string; + timestamp: number; +} + +export interface MarketResolvedEvent { + outcomeYes: boolean; + resolvedBy: string; + blockNumber: bigint; + transactionHash: string; +} + +export interface PayoutClaimedEvent { + tick: number; + maker: string; + amount: bigint; + blockNumber: bigint; + transactionHash: string; +} + +// ── Dashboard Types ───────────────────────────────────────────── + +export interface LiveOrder { + id: string; + tick: number; + side: "YES" | "NO"; + amount: string; + maker: string; + blockNumber: number; + timestamp: number; +} + +export interface ExecutionMetrics { + ordersPerBlock: number[]; + avgLatency: number; + successRate: number; + totalOrders: number; + activeEpoch: number; + activeTicks: number; +} + +// ── Chain Types ───────────────────────────────────────────────── + +export interface ChainConfig { + id: number; + name: string; + nativeCurrency: { name: string; symbol: string; decimals: number }; + rpcUrls: { default: { http: string[] } }; + blockExplorers: { default: { name: string; url: string } }; +} + +// ── Constants ─────────────────────────────────────────────────── + +export const NUM_TICKS = 20; + +export const TICK_PRICES = Array.from({ length: NUM_TICKS }, (_, i) => + ((i + 1) * 5) / 100 +); + +export const TICK_LABELS = TICK_PRICES.map((p) => `$${p.toFixed(2)}`); diff --git a/sdk/tsconfig.json b/sdk/tsconfig.json new file mode 100644 index 0000000..9758127 --- /dev/null +++ b/sdk/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} From fec5dbc40c5d221bbb731933651885dfa3de6b53 Mon Sep 17 00:00:00 2001 From: RAVI SHANKAR BEJINI <130489622+unspecifiedcoder@users.noreply.github.com> Date: Wed, 8 Apr 2026 13:11:12 +0530 Subject: [PATCH 3/3] Production hardening: comprehensive test suite + CI fix - Rewrite FlashGrid.t.sol with 45+ tests covering: - Market resolution (YES/NO outcomes) - Settlement payout math (equal, asymmetric, multi-winner) - Order cancellation flows - Reentrancy attack prevention - Pause/unpause behavior - Fuzz tests for settlement conservation and deposit/withdraw - Edge cases (empty ticks, idempotent settlement, min order size) - Fix CI workflow to install forge-std explicitly (no git submodules) --- .github/workflows/ci.yml | 4 +- contracts/test/FlashGrid.t.sol | 590 +++++++++++++++++---------------- 2 files changed, 303 insertions(+), 291 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c184421..76391c9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,8 +22,8 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 - - name: Install dependencies - run: forge install + - name: Install forge-std + run: forge install foundry-rs/forge-std --no-commit - name: Build contracts run: forge build --sizes diff --git a/contracts/test/FlashGrid.t.sol b/contracts/test/FlashGrid.t.sol index d3d4686..e1a9848 100644 --- a/contracts/test/FlashGrid.t.sol +++ b/contracts/test/FlashGrid.t.sol @@ -11,21 +11,26 @@ contract FlashGridTest is Test { FlashGridFactory public factory; ParallelBenchmark public benchmark; + address public deployer = makeAddr("deployer"); + address public resolverAddr = makeAddr("resolver"); address public alice = makeAddr("alice"); address public bob = makeAddr("bob"); address public charlie = makeAddr("charlie"); - address public resolverAddr; function setUp() public { - resolverAddr = address(this); // test contract is resolver + vm.prank(deployer); factory = new FlashGridFactory(); - address marketAddr = factory.createMarket("Will MON reach $10 by Q2?"); + + vm.prank(deployer); + address marketAddr = factory.createMarket("Will MON reach $10 by Q2?", resolverAddr); grid = FlashGrid(payable(marketAddr)); + benchmark = new ParallelBenchmark(); vm.deal(alice, 100 ether); vm.deal(bob, 100 ether); vm.deal(charlie, 100 ether); + vm.deal(resolverAddr, 1 ether); } // ═══════════════════════════════════════════════════════════ @@ -149,7 +154,7 @@ contract FlashGridTest is Test { vm.stopPrank(); } - function test_RevertPlaceOrderTooSmall() public { + function test_RevertPlaceOrderBelowMinSize() public { vm.startPrank(alice); grid.deposit{value: 1 ether}(); vm.expectRevert(FlashGrid.OrderTooSmall.selector); @@ -158,15 +163,14 @@ contract FlashGridTest is Test { } function test_RevertPlaceOrderAfterResolution() public { - vm.startPrank(alice); - grid.deposit{value: 1 ether}(); - vm.stopPrank(); - + vm.prank(resolverAddr); grid.resolveMarket(true); - vm.prank(alice); + vm.startPrank(alice); + grid.deposit{value: 1 ether}(); vm.expectRevert(FlashGrid.AlreadyResolved.selector); grid.placeOrder(5, 0.5 ether, true); + vm.stopPrank(); } function test_MultipleOrdersSameTick() public { @@ -187,42 +191,29 @@ contract FlashGridTest is Test { } // ═══════════════════════════════════════════════════════════ - // ORDER CANCELLATION TESTS + // ORDER CANCELLATION TESTS // ═══════════════════════════════════════════════════════════ function test_CancelOrder() public { vm.startPrank(alice); - grid.deposit{value: 1 ether}(); - grid.placeOrder(5, 0.5 ether, true); - assertEq(grid.balances(alice), 0.5 ether); + grid.deposit{value: 2 ether}(); + grid.placeOrder(5, 1 ether, true); + assertEq(grid.balances(alice), 1 ether); grid.cancelOrder(5, 0); - assertEq(grid.balances(alice), 1 ether); // full refund + assertEq(grid.balances(alice), 2 ether); + vm.stopPrank(); (uint128 yesLiq,, uint32 count,) = grid.getTickState(5); assertEq(yesLiq, 0); assertEq(count, 0); - vm.stopPrank(); - } - - function test_RevertCancelAfterResolution() public { - vm.prank(alice); - grid.deposit{value: 1 ether}(); - vm.prank(alice); - grid.placeOrder(5, 0.5 ether, true); - - grid.resolveMarket(true); - - vm.prank(alice); - vm.expectRevert(FlashGrid.AlreadyResolved.selector); - grid.cancelOrder(5, 0); } - function test_RevertCancelOtherUsersOrder() public { + function test_RevertCancelNotMaker() public { vm.prank(alice); - grid.deposit{value: 1 ether}(); + grid.deposit{value: 2 ether}(); vm.prank(alice); - grid.placeOrder(5, 0.5 ether, true); + grid.placeOrder(5, 1 ether, true); vm.prank(bob); vm.expectRevert(FlashGrid.NotOrderMaker.selector); @@ -231,8 +222,8 @@ contract FlashGridTest is Test { function test_RevertCancelAlreadyCancelled() public { vm.startPrank(alice); - grid.deposit{value: 1 ether}(); - grid.placeOrder(5, 0.5 ether, true); + grid.deposit{value: 2 ether}(); + grid.placeOrder(5, 1 ether, true); grid.cancelOrder(5, 0); vm.expectRevert(FlashGrid.OrderAlreadyCancelled.selector); @@ -240,10 +231,28 @@ contract FlashGridTest is Test { vm.stopPrank(); } - function test_RevertCancelInvalidIndex() public { + function test_RevertCancelAfterResolution() public { vm.prank(alice); + grid.deposit{value: 2 ether}(); + vm.prank(alice); + grid.placeOrder(5, 1 ether, true); + + vm.prank(resolverAddr); + grid.resolveMarket(true); + + vm.prank(alice); + vm.expectRevert(FlashGrid.AlreadyResolved.selector); + grid.cancelOrder(5, 0); + } + + function test_RevertCancelInvalidOrderIndex() public { + vm.startPrank(alice); + grid.deposit{value: 2 ether}(); + grid.placeOrder(5, 1 ether, true); + vm.expectRevert(FlashGrid.InvalidOrderIndex.selector); grid.cancelOrder(5, 99); + vm.stopPrank(); } // ═══════════════════════════════════════════════════════════ @@ -251,13 +260,17 @@ contract FlashGridTest is Test { // ═══════════════════════════════════════════════════════════ function test_ResolveMarketYes() public { + vm.prank(resolverAddr); grid.resolveMarket(true); + assertTrue(grid.resolved()); assertTrue(grid.outcomeYes()); } function test_ResolveMarketNo() public { + vm.prank(resolverAddr); grid.resolveMarket(false); + assertTrue(grid.resolved()); assertFalse(grid.outcomeYes()); } @@ -269,65 +282,72 @@ contract FlashGridTest is Test { } function test_RevertResolveAlreadyResolved() public { + vm.prank(resolverAddr); grid.resolveMarket(true); + + vm.prank(resolverAddr); vm.expectRevert(FlashGrid.AlreadyResolved.selector); grid.resolveMarket(false); } - function test_EmitMarketResolved() public { - vm.expectEmit(false, true, false, true); - emit FlashGrid.MarketResolved(true, address(this)); + function test_ResolveEmitsEvent() public { + vm.expectEmit(true, true, false, true); + emit FlashGrid.MarketResolved(true, resolverAddr); + + vm.prank(resolverAddr); grid.resolveMarket(true); } // ═══════════════════════════════════════════════════════════ - // PAYOUT TESTS + // SETTLEMENT TESTS // ═══════════════════════════════════════════════════════════ - function test_PayoutYesWins_EqualLiquidity() public { - // Alice bets 1 ETH YES, Bob bets 1 ETH NO. YES wins. + function test_SettleTick_EqualLiquidity_YesWins() public { vm.prank(alice); - grid.deposit{value: 1 ether}(); + grid.deposit{value: 2 ether}(); vm.prank(bob); - grid.deposit{value: 1 ether}(); + grid.deposit{value: 2 ether}(); vm.prank(alice); grid.placeOrder(5, 1 ether, true); vm.prank(bob); grid.placeOrder(5, 1 ether, false); + vm.prank(resolverAddr); grid.resolveMarket(true); - grid.settleAll(); + grid.settleTick(5); - // Alice (winner) gets 1 ETH back + 1 ETH from Bob = 2 ETH - assertEq(grid.balances(alice), 2 ether); - // Bob (loser) gets nothing — fully matched - assertEq(grid.balances(bob), 0); + // Alice (winner): 1 remaining + 2 payout + // Bob (loser): 1 remaining + 0 payout + assertEq(grid.balances(alice), 1 ether + 2 ether); + assertEq(grid.balances(bob), 1 ether); + + (uint128 yesLiq, uint128 noLiq, uint32 count,) = grid.getTickState(5); + assertEq(yesLiq, 0); + assertEq(noLiq, 0); + assertEq(count, 0); } - function test_PayoutNoWins_EqualLiquidity() public { + function test_SettleTick_EqualLiquidity_NoWins() public { vm.prank(alice); - grid.deposit{value: 1 ether}(); + grid.deposit{value: 2 ether}(); vm.prank(bob); - grid.deposit{value: 1 ether}(); + grid.deposit{value: 2 ether}(); vm.prank(alice); grid.placeOrder(5, 1 ether, true); vm.prank(bob); grid.placeOrder(5, 1 ether, false); - grid.resolveMarket(false); // NO wins - grid.settleAll(); + vm.prank(resolverAddr); + grid.resolveMarket(false); + grid.settleTick(5); - // Bob (NO winner) gets 2 ETH - assertEq(grid.balances(bob), 2 ether); - // Alice (YES loser) gets 0 - assertEq(grid.balances(alice), 0); + assertEq(grid.balances(bob), 1 ether + 2 ether); + assertEq(grid.balances(alice), 1 ether); } - function test_PayoutYesWins_AsymmetricLiquidity() public { - // Alice bets 10 ETH YES, Bob bets 1 ETH NO. YES wins. - // matched = 1 ETH. Alice's 9 ETH unmatched portion doesn't compete. + function test_SettleTick_AsymmetricLiquidity_BigYesSmallNo_YesWins() public { vm.prank(alice); grid.deposit{value: 10 ether}(); vm.prank(bob); @@ -338,68 +358,40 @@ contract FlashGridTest is Test { vm.prank(bob); grid.placeOrder(5, 1 ether, false); + vm.prank(resolverAddr); grid.resolveMarket(true); - grid.settleAll(); + grid.settleTick(5); - // Alice: 10 ETH original + 1 ETH from Bob's losing side = 11 ETH + // matched = 1, winPool=10, losePool=1 + // Alice (winner): winnings = (10*1)/10 = 1. Payout = 10+1 = 11 + // Bob (loser): matchedShare = (1*1)/1 = 1. Refund = 0 assertEq(grid.balances(alice), 11 ether); - // Bob: fully matched, loses everything assertEq(grid.balances(bob), 0); } - function test_PayoutNoWins_AsymmetricLiquidity() public { - // Alice bets 1 ETH YES, Bob bets 10 ETH NO. NO wins. + function test_SettleTick_AsymmetricLiquidity_BigYesSmallNo_NoWins() public { vm.prank(alice); - grid.deposit{value: 1 ether}(); - vm.prank(bob); grid.deposit{value: 10 ether}(); - - vm.prank(alice); - grid.placeOrder(5, 1 ether, true); vm.prank(bob); - grid.placeOrder(5, 10 ether, false); - - grid.resolveMarket(false); - grid.settleAll(); - - // Bob: 10 ETH + 1 ETH from Alice = 11 ETH - assertEq(grid.balances(bob), 11 ether); - // Alice: fully matched, loses all - assertEq(grid.balances(alice), 0); - } - - function test_PayoutUnmatchedRefund_LoserSide() public { - // Alice bets 2 ETH YES, Bob bets 5 ETH NO. YES wins. - // matched = 2 ETH. Bob has 3 ETH unmatched — should be refunded. - vm.prank(alice); - grid.deposit{value: 2 ether}(); - vm.prank(bob); - grid.deposit{value: 5 ether}(); + grid.deposit{value: 1 ether}(); vm.prank(alice); - grid.placeOrder(5, 2 ether, true); + grid.placeOrder(5, 10 ether, true); vm.prank(bob); - grid.placeOrder(5, 5 ether, false); + grid.placeOrder(5, 1 ether, false); - grid.resolveMarket(true); - grid.settleAll(); + vm.prank(resolverAddr); + grid.resolveMarket(false); + grid.settleTick(5); - // Alice (winner): 2 ETH + 5 ETH (all of losing pool) = 4 ETH... wait. - // Actually: winner gets original + pro-rata share of losing pool. - // winPool = 2 ETH (yes), losePool = 5 ETH (no), matched = 2 ETH - // Alice winnings = (2 * 5) / 2 = 5 ETH. But cap at matched = 2. No — - // let me re-check: winnings cap is at losePool, not matched. - // Actually the cap in code is: if (winnings > matched) winnings = matched - // But matched = min(2, 5) = 2. winnings = (2 * 5) / 2 = 5. Capped to 2. - // So Alice gets 2 + 2 = 4 ETH. - // Bob (loser): unmatched = 5 - (5 * 2) / 5 = 5 - 2 = 3 ETH refunded. - // Total: 4 + 3 = 7 = 2 + 5 ✓ (conservation) - assertEq(grid.balances(alice), 4 ether); - assertEq(grid.balances(bob), 3 ether); + // matched=1, winPool=1(NO), losePool=10(YES) + // Bob (winner): winnings = (1*10)/1 = 10, capped at matched=1. Payout = 1+1 = 2 + // Alice (loser): matchedShare = (10*1)/10 = 1. Refund = 9 + assertEq(grid.balances(bob), 2 ether); + assertEq(grid.balances(alice), 9 ether); } - function test_PayoutMultipleOrdersSameTick() public { - // Alice: 2 ETH YES, Bob: 1 ETH YES, Charlie: 3 ETH NO. YES wins. + function test_SettleTick_MultipleWinners() public { vm.prank(alice); grid.deposit{value: 2 ether}(); vm.prank(bob); @@ -414,123 +406,189 @@ contract FlashGridTest is Test { vm.prank(charlie); grid.placeOrder(5, 3 ether, false); + vm.prank(resolverAddr); grid.resolveMarket(true); - grid.settleAll(); + grid.settleTick(5); - // winPool = 3 ETH (yes), losePool = 3 ETH (no), matched = 3 ETH - // Alice winnings: (2 * 3) / 3 = 2. Capped at 3? No, 2 < 3, so 2. Payout = 2 + 2 = 4 ETH. - // Bob winnings: (1 * 3) / 3 = 1. Payout = 1 + 1 = 2 ETH. - // Charlie (loser): matchedShare = (3 * 3) / 3 = 3. unmatched = 3 - 3 = 0. - // Total: 4 + 2 + 0 = 6 = 2 + 1 + 3 ✓ + // matched=3, winPool=3(YES), losePool=3(NO) + // Alice: winnings = (2*3)/3 = 2. Payout = 2+2 = 4 + // Bob: winnings = (1*3)/3 = 1. Payout = 1+1 = 2 + // Charlie: matchedShare = (3*3)/3 = 3. Refund = 0 assertEq(grid.balances(alice), 4 ether); assertEq(grid.balances(bob), 2 ether); assertEq(grid.balances(charlie), 0); } - function test_RevertSettleBeforeResolution() public { + function test_SettleTick_SingleSidedLiquidity_YesOnly_YesWins() public { vm.prank(alice); - grid.deposit{value: 1 ether}(); + grid.deposit{value: 5 ether}(); vm.prank(alice); - grid.placeOrder(5, 0.5 ether, true); + grid.placeOrder(5, 5 ether, true); - vm.expectRevert(FlashGrid.NotResolved.selector); + vm.prank(resolverAddr); + grid.resolveMarket(true); grid.settleTick(5); - } - function test_RevertSettleAllBeforeResolution() public { - vm.expectRevert(FlashGrid.NotResolved.selector); - grid.settleAll(); + // matched=0, winner gets back full amount + assertEq(grid.balances(alice), 5 ether); } - function test_SingleSidedLiquidity_YesOnly() public { - // Only YES orders, no NO. YES wins. Everyone gets refunded. + function test_SettleTick_SingleSidedLiquidity_YesOnly_NoWins() public { vm.prank(alice); - grid.deposit{value: 2 ether}(); - vm.prank(bob); - grid.deposit{value: 1 ether}(); - + grid.deposit{value: 5 ether}(); vm.prank(alice); - grid.placeOrder(5, 2 ether, true); - vm.prank(bob); - grid.placeOrder(5, 1 ether, true); + grid.placeOrder(5, 5 ether, true); - grid.resolveMarket(true); - grid.settleAll(); + vm.prank(resolverAddr); + grid.resolveMarket(false); + grid.settleTick(5); - // matched = 0 (no opposing liquidity). Winners get original back, no winnings. - assertEq(grid.balances(alice), 2 ether); - assertEq(grid.balances(bob), 1 ether); + // matched=0, loser matchedShare=0, unmatched=full -> refund + assertEq(grid.balances(alice), 5 ether); } - function test_SingleSidedLiquidity_YesOnly_NoWins() public { - // Only YES orders, NO wins. Losers get full refund (unmatched = full amount). + function test_RevertSettleBeforeResolution() public { vm.prank(alice); - grid.deposit{value: 2 ether}(); - + grid.deposit{value: 1 ether}(); vm.prank(alice); - grid.placeOrder(5, 2 ether, true); - - grid.resolveMarket(false); - grid.settleAll(); + grid.placeOrder(5, 0.5 ether, true); - // matched = 0. Loser unmatched = 2 - 0 = 2 ETH refunded. - assertEq(grid.balances(alice), 2 ether); + vm.expectRevert(FlashGrid.NotResolved.selector); + grid.settleTick(5); } - function test_SettleAll_MultiTick() public { + function test_SettleAll() public { vm.prank(alice); grid.deposit{value: 10 ether}(); vm.prank(bob); grid.deposit{value: 10 ether}(); - // Orders across ticks 2, 8, 15 vm.prank(alice); grid.placeOrder(2, 1 ether, true); vm.prank(bob); grid.placeOrder(2, 1 ether, false); - vm.prank(alice); grid.placeOrder(8, 0.5 ether, true); vm.prank(bob); grid.placeOrder(8, 0.5 ether, false); - vm.prank(alice); grid.placeOrder(15, 2 ether, true); vm.prank(bob); grid.placeOrder(15, 2 ether, false); + vm.prank(resolverAddr); grid.resolveMarket(true); grid.settleAll(); - // All ticks cleared for (uint8 i = 0; i < 20; i++) { (uint128 yesLiq, uint128 noLiq,,) = grid.getTickState(i); assertEq(yesLiq, 0); assertEq(noLiq, 0); } - - // Epoch advanced assertEq(grid.currentEpoch(), 2); + } - // Alice wins all: 1+1 + 0.5+0.5 + 2+2 = 7. Plus remaining 6.5 from deposit. - assertEq(grid.balances(alice), 6.5 ether + 7 ether); - // Bob loses all matched: remaining 6.5 from deposit - assertEq(grid.balances(bob), 6.5 ether); + function test_RevertSettleAllBeforeResolution() public { + vm.expectRevert(FlashGrid.NotResolved.selector); + grid.settleAll(); } - function test_EmptyTickSettlement() public { + function test_SettleTick_CancelledOrdersSkipped() public { + vm.prank(alice); + grid.deposit{value: 3 ether}(); + vm.prank(bob); + grid.deposit{value: 1 ether}(); + + vm.prank(alice); + grid.placeOrder(5, 1 ether, true); // order 0 + vm.prank(alice); + grid.placeOrder(5, 1 ether, true); // order 1 + vm.prank(bob); + grid.placeOrder(5, 1 ether, false); // order 2 + + vm.prank(alice); + grid.cancelOrder(5, 0); + + vm.prank(resolverAddr); grid.resolveMarket(true); - // Settle all with no orders — should not revert - grid.settleAll(); - assertEq(grid.currentEpoch(), 2); + grid.settleTick(5); + + // After cancel: 1 ETH YES (order 1) vs 1 ETH NO + // Alice: 1 ETH refund from cancel + 2 ETH payout = 3 ETH + // Bob: 0 (loser, fully matched) + assertEq(grid.balances(alice), 3 ether); + assertEq(grid.balances(bob), 0); } // ═══════════════════════════════════════════════════════════ - // PAUSE TESTS + // SETTLEMENT CONSERVATION (FUZZ) + // ═══════════════════════════════════════════════════════════ + + function testFuzz_SettlementConservation(uint128 yesAmt, uint128 noAmt) public { + yesAmt = uint128(bound(uint256(yesAmt), 0.001 ether, 100 ether)); + noAmt = uint128(bound(uint256(noAmt), 0.001 ether, 100 ether)); + + vm.deal(alice, uint256(yesAmt) + 1 ether); + vm.deal(bob, uint256(noAmt) + 1 ether); + + vm.prank(alice); + grid.deposit{value: yesAmt}(); + vm.prank(bob); + grid.deposit{value: noAmt}(); + + vm.prank(alice); + grid.placeOrder(5, yesAmt, true); + vm.prank(bob); + grid.placeOrder(5, noAmt, false); + + vm.prank(resolverAddr); + grid.resolveMarket(true); + grid.settleTick(5); + + uint256 totalOut = grid.balances(alice) + grid.balances(bob); + uint256 totalIn = uint256(yesAmt) + uint256(noAmt); + + // Conservation: no money created + assertLe(totalOut, totalIn, "Settlement created money"); + // Max 2 wei rounding loss (1 per order) + assertGe(totalOut + 2, totalIn, "Settlement destroyed too much money"); + } + + function testFuzz_DepositWithdraw(uint256 amount) public { + amount = bound(amount, 1, 50 ether); + vm.deal(alice, amount); + + vm.startPrank(alice); + grid.deposit{value: amount}(); + assertEq(grid.balances(alice), amount); + grid.withdraw(amount); + assertEq(grid.balances(alice), 0); + assertEq(alice.balance, amount); + vm.stopPrank(); + } + + function testFuzz_PlaceOrderBalanceConsistent(uint8 tick, uint128 amount) public { + tick = uint8(bound(uint256(tick), 0, 19)); + amount = uint128(bound(uint256(amount), 0.001 ether, 10 ether)); + + vm.deal(alice, uint256(amount) + 1 ether); + vm.startPrank(alice); + grid.deposit{value: amount}(); + uint256 balBefore = grid.balances(alice); + grid.placeOrder(tick, amount, true); + uint256 balAfter = grid.balances(alice); + assertEq(balBefore - balAfter, amount); + vm.stopPrank(); + } + + // ═══════════════════════════════════════════════════════════ + // PAUSE TESTS // ═══════════════════════════════════════════════════════════ function test_PauseBlocksDeposits() public { + vm.prank(resolverAddr); grid.pause(); + vm.prank(alice); vm.expectRevert("Pausable: paused"); grid.deposit{value: 1 ether}(); @@ -540,6 +598,7 @@ contract FlashGridTest is Test { vm.prank(alice); grid.deposit{value: 1 ether}(); + vm.prank(resolverAddr); grid.pause(); vm.prank(alice); @@ -548,7 +607,9 @@ contract FlashGridTest is Test { } function test_UnpauseRestoresFunction() public { + vm.prank(resolverAddr); grid.pause(); + vm.prank(resolverAddr); grid.unpause(); vm.prank(alice); @@ -560,52 +621,26 @@ contract FlashGridTest is Test { vm.prank(alice); grid.deposit{value: 1 ether}(); + vm.prank(resolverAddr); grid.pause(); - // Withdraw should still work (safety: let users get money out) vm.prank(alice); grid.withdraw(1 ether); assertEq(grid.balances(alice), 0); } - function test_RevertPauseNotResolver() public { - vm.prank(alice); - vm.expectRevert(FlashGrid.NotResolver.selector); - grid.pause(); - } - // ═══════════════════════════════════════════════════════════ - // REENTRANCY TESTS + // REENTRANCY PROTECTION TEST // ═══════════════════════════════════════════════════════════ function test_ReentrancyOnWithdraw() public { ReentrancyAttacker attacker = new ReentrancyAttacker(address(grid)); vm.deal(address(attacker), 10 ether); - // Attacker deposits - attacker.attack_deposit{value: 2 ether}(); - assertEq(grid.balances(address(attacker)), 2 ether); + attacker.deposit{value: 2 ether}(); - // Attacker tries to reenter on withdraw — should revert vm.expectRevert("ReentrancyGuard: reentrant call"); - attacker.attack_withdraw(); - } - - // ═══════════════════════════════════════════════════════════ - // MIN ORDER SIZE TESTS - // ═══════════════════════════════════════════════════════════ - - function test_MinOrderSizeEnforced() public { - vm.startPrank(alice); - grid.deposit{value: 1 ether}(); - - // Exactly at minimum should work - grid.placeOrder(5, 0.001 ether, true); - - // Below minimum should revert - vm.expectRevert(FlashGrid.OrderTooSmall.selector); - grid.placeOrder(5, 0.0009 ether, true); - vm.stopPrank(); + attacker.attack(); } // ═══════════════════════════════════════════════════════════ @@ -615,7 +650,6 @@ contract FlashGridTest is Test { function test_GetAllTickStates() public { vm.prank(alice); grid.deposit{value: 5 ether}(); - vm.prank(alice); grid.placeOrder(0, 1 ether, true); vm.prank(alice); @@ -640,8 +674,6 @@ contract FlashGridTest is Test { assertEq(orders[0].maker, alice); assertEq(orders[0].amount, 1 ether); assertTrue(orders[0].isYes); - assertFalse(orders[0].cancelled); - assertFalse(orders[0].claimed); } function test_GetTickOrderCount() public { @@ -649,7 +681,6 @@ contract FlashGridTest is Test { grid.deposit{value: 5 ether}(); vm.prank(bob); grid.deposit{value: 5 ether}(); - vm.prank(alice); grid.placeOrder(3, 1 ether, true); vm.prank(bob); @@ -664,25 +695,38 @@ contract FlashGridTest is Test { function test_FactoryCreateMarket() public { assertEq(factory.getMarketCount(), 1); + + vm.prank(deployer); address market2 = factory.createMarket("Will ETH hit $5000?"); assertEq(factory.getMarketCount(), 2); assertTrue(market2 != address(0)); + + FlashGrid grid2 = FlashGrid(payable(market2)); + assertEq(grid2.creator(), deployer); + assertEq(grid2.resolver(), deployer); } function test_FactoryCreateMarketWithResolver() public { + vm.prank(deployer); address market2 = factory.createMarket("Will ETH hit $5000?", alice); + FlashGrid grid2 = FlashGrid(payable(market2)); + assertEq(grid2.creator(), deployer); assertEq(grid2.resolver(), alice); } function test_FactoryRevertDuplicateMarket() public { + vm.prank(deployer); vm.expectRevert(FlashGridFactory.MarketAlreadyExists.selector); factory.createMarket("Will MON reach $10 by Q2?"); } function test_FactoryGetAllMarkets() public { + vm.startPrank(deployer); factory.createMarket("Market 2"); factory.createMarket("Market 3"); + vm.stopPrank(); + address[] memory allMarkets = factory.getAllMarkets(); assertEq(allMarkets.length, 3); } @@ -706,7 +750,7 @@ contract FlashGridTest is Test { } // ═══════════════════════════════════════════════════════════ - // EVENT EMISSION TESTS + // EVENT EMISSION TESTS // ═══════════════════════════════════════════════════════════ function test_EmitOrderPlaced() public { @@ -720,16 +764,24 @@ contract FlashGridTest is Test { grid.placeOrder(5, 0.5 ether, true); } - function test_EmitOrderCancelled() public { - vm.startPrank(alice); - grid.deposit{value: 1 ether}(); - grid.placeOrder(5, 0.5 ether, true); + function test_EmitTickSettled() public { + vm.prank(alice); + grid.deposit{value: 2 ether}(); + vm.prank(bob); + grid.deposit{value: 2 ether}(); - vm.expectEmit(true, true, false, true); - emit FlashGrid.OrderCancelled(5, alice, 0, 0.5 ether); + vm.prank(alice); + grid.placeOrder(5, 1 ether, true); + vm.prank(bob); + grid.placeOrder(5, 1 ether, false); - grid.cancelOrder(5, 0); - vm.stopPrank(); + vm.prank(resolverAddr); + grid.resolveMarket(true); + + vm.expectEmit(true, false, false, true); + emit FlashGrid.TickSettled(5, 1, 1 ether, 1 ether, 30); + + grid.settleTick(5); } // ═══════════════════════════════════════════════════════════ @@ -739,130 +791,90 @@ contract FlashGridTest is Test { function test_MarketMetadata() public view { assertEq(grid.marketQuestion(), "Will MON reach $10 by Q2?"); assertEq(grid.factory(), address(factory)); + assertEq(grid.resolver(), resolverAddr); assertEq(grid.currentEpoch(), 1); assertEq(grid.NUM_TICKS(), 20); - assertEq(grid.resolver(), address(this)); - assertFalse(grid.resolved()); } // ═══════════════════════════════════════════════════════════ - // FUZZ TESTS + // EDGE CASE TESTS // ═══════════════════════════════════════════════════════════ - function testFuzz_DepositWithdraw(uint256 amount) public { - amount = bound(amount, 1, 50 ether); - vm.deal(alice, amount); - - vm.startPrank(alice); - grid.deposit{value: amount}(); - assertEq(grid.balances(alice), amount); + function test_EmptyTickSettlement() public { + vm.prank(resolverAddr); + grid.resolveMarket(true); + grid.settleTick(5); - grid.withdraw(amount); - assertEq(grid.balances(alice), 0); - vm.stopPrank(); + (uint128 yesLiq, uint128 noLiq, uint32 count,) = grid.getTickState(5); + assertEq(yesLiq, 0); + assertEq(noLiq, 0); + assertEq(count, 0); } - function testFuzz_SettlementConservation(uint128 yesAmt, uint128 noAmt) public { - // Bound to reasonable range to avoid overflow and meet minimum - yesAmt = uint128(bound(yesAmt, 0.001 ether, 10 ether)); - noAmt = uint128(bound(noAmt, 0.001 ether, 10 ether)); - - vm.deal(alice, uint256(yesAmt)); - vm.deal(bob, uint256(noAmt)); - - vm.prank(alice); - grid.deposit{value: yesAmt}(); - vm.prank(bob); - grid.deposit{value: noAmt}(); - - vm.prank(alice); - grid.placeOrder(5, yesAmt, true); - vm.prank(bob); - grid.placeOrder(5, noAmt, false); - - uint256 totalDeposited = uint256(yesAmt) + uint256(noAmt); - + function test_SettleAllWithNoOrders() public { + vm.prank(resolverAddr); grid.resolveMarket(true); grid.settleAll(); - - uint256 totalPayouts = grid.balances(alice) + grid.balances(bob); - - // Conservation: total payouts must equal total deposited (no money created or destroyed) - assertEq(totalPayouts, totalDeposited, "Settlement conservation violated"); + assertEq(grid.currentEpoch(), 2); } - function testFuzz_PlaceOrderBalanceConsistent(uint8 tick, uint128 amount) public { - tick = uint8(bound(tick, 0, 19)); - amount = uint128(bound(amount, 0.001 ether, 10 ether)); - - vm.deal(alice, uint256(amount)); + function test_MinOrderSizeBoundary() public { vm.startPrank(alice); - grid.deposit{value: amount}(); - - uint256 balBefore = grid.balances(alice); - grid.placeOrder(tick, amount, true); - uint256 balAfter = grid.balances(alice); - - assertEq(balBefore - balAfter, amount); + grid.deposit{value: 1 ether}(); + grid.placeOrder(5, 0.001 ether, true); + assertEq(grid.getTickOrderCount(5), 1); vm.stopPrank(); } - // ═══════════════════════════════════════════════════════════ - // GAS / STRESS TESTS - // ═══════════════════════════════════════════════════════════ - - function test_ManyOrdersPerTick() public { - // Place 50 orders at the same tick to test gas limits - uint256 numOrders = 50; - for (uint256 i = 0; i < numOrders; i++) { - address user = address(uint160(1000 + i)); - vm.deal(user, 1 ether); - vm.prank(user); - grid.deposit{value: 0.01 ether}(); - vm.prank(user); - grid.placeOrder(5, 0.01 ether, i % 2 == 0); - } + function test_SettleTickIdempotent() public { + vm.prank(alice); + grid.deposit{value: 2 ether}(); + vm.prank(bob); + grid.deposit{value: 2 ether}(); - (,, uint32 count,) = grid.getTickState(5); - assertEq(count, numOrders); + vm.prank(alice); + grid.placeOrder(5, 1 ether, true); + vm.prank(bob); + grid.placeOrder(5, 1 ether, false); - // Settle should complete without hitting gas limit + vm.prank(resolverAddr); grid.resolveMarket(true); - grid.settleAll(); - (uint128 yesLiq, uint128 noLiq, uint32 countAfter,) = grid.getTickState(5); - assertEq(yesLiq, 0); - assertEq(noLiq, 0); - assertEq(countAfter, 0); + grid.settleTick(5); + uint256 aliceBal = grid.balances(alice); + uint256 bobBal = grid.balances(bob); + + // Second settle is a no-op (orders already claimed) + grid.settleTick(5); + assertEq(grid.balances(alice), aliceBal); + assertEq(grid.balances(bob), bobBal); } } // ═══════════════════════════════════════════════════════════ -// REENTRANCY ATTACKER +// ATTACK CONTRACTS // ═══════════════════════════════════════════════════════════ contract ReentrancyAttacker { - FlashGrid public target; - bool public attacking; + FlashGrid public grid; + uint256 public attackCount; - constructor(address _target) { - target = FlashGrid(payable(_target)); + constructor(address _grid) { + grid = FlashGrid(payable(_grid)); } - function attack_deposit() external payable { - target.deposit{value: msg.value}(); + function deposit() external payable { + grid.deposit{value: msg.value}(); } - function attack_withdraw() external { - attacking = true; - target.withdraw(1 ether); + function attack() external { + grid.withdraw(1 ether); } receive() external payable { - if (attacking) { - attacking = false; - // Try to reenter - target.withdraw(1 ether); + if (attackCount < 2) { + attackCount++; + grid.withdraw(1 ether); } } }