-
Notifications
You must be signed in to change notification settings - Fork 3
feat: pay out winners on settled markets #1273
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,164 @@ | ||
| /** | ||
| * MIGRATION 033: ORDER BOOK SETTLEMENT | ||
| * | ||
| * Automatic atomic settlement processing: | ||
| * - Bulk delete losing positions (efficient) | ||
| * - Pay winners (shares × $1.00 - 2% redemption fee) | ||
| * - Refund open buy orders (no fee) | ||
| * - Delete all positions atomically | ||
| * - Collect fees in vault (Issue 9 will distribute them) | ||
| * | ||
| * Implementation Note: | ||
| * Uses CTE + ARRAY_AGG to collect all payout data in a single query, then | ||
| * processes payouts via batch unlock. This avoids nested queries in the main | ||
| * settlement action (Kuneiform limitation: cannot call external functions | ||
| * like ethereum_bridge.unlock() inside FOR loops in the same action). | ||
| * | ||
| * The batch unlock helper (ob_batch_unlock_collateral) CAN loop with function | ||
| * calls because it's a separate action called ONCE with all aggregated data. | ||
| * | ||
| * Transaction Atomicity: | ||
| * All Kwil actions execute in a single database transaction. If ANY operation | ||
| * fails (including ethereum_bridge.unlock()), the ENTIRE action rolls back: | ||
| * - Database changes (position deletions, settled flag) are reverted | ||
| * - Blockchain state changes are NOT committed (Kwil's 2-phase approach) | ||
| * - The settled flag remains false, allowing the settlement extension to retry | ||
| * | ||
| * Retry Mechanism: | ||
| * The tn_settlement extension retries failed settlements (3 attempts with backoff). | ||
| * After exhaustion, the market remains unsettled and requires manual intervention | ||
| * or extension restart to resume retries. This is safe because: | ||
| * 1. The settled flag prevents duplicate settlement attempts within a transaction | ||
| * 2. Rollback ensures partial state never persists | ||
| * 3. Position data remains intact for retry attempts | ||
| */ | ||
|
|
||
| -- Batch unlock collateral for multiple wallets | ||
| -- This helper processes all unlocks in a single call, avoiding nested queries in settlement | ||
| CREATE OR REPLACE ACTION ob_batch_unlock_collateral( | ||
| $wallet_addresses TEXT[], | ||
| $amounts NUMERIC(78, 0)[] | ||
| ) PRIVATE { | ||
| -- Validate input arrays have same length | ||
| if COALESCE(array_length($wallet_addresses), 0) != COALESCE(array_length($amounts), 0) { | ||
| ERROR('wallet_addresses and amounts arrays must have the same length'); | ||
| } | ||
|
|
||
| -- Process each unlock (this is the ONLY place we loop with function calls) | ||
| -- This is safe because the settlement action calls THIS function once with all data | ||
| for $payout in | ||
| SELECT wallet, amount | ||
| FROM UNNEST($wallet_addresses, $amounts) AS u(wallet, amount) | ||
| { | ||
| ethereum_bridge.unlock($payout.wallet, $payout.amount); | ||
| } | ||
| }; | ||
|
|
||
| -- Process settlement: Pay winners, refund open buys, collect fees | ||
| CREATE OR REPLACE ACTION process_settlement( | ||
| $query_id INT, | ||
| $winning_outcome BOOL | ||
| ) PRIVATE { | ||
| $redemption_fee_bps INT := 200; -- 2% (200 basis points) | ||
| $total_fees_collected NUMERIC(78, 0) := '0'::NUMERIC(78, 0); | ||
| $one_token NUMERIC(78, 0) := '1000000000000000000'::NUMERIC(78, 0); | ||
|
|
||
| -- Step 1: Bulk delete all losing positions (efficient single operation) | ||
| -- Price semantics: price=0 (holdings), price>0 (open sells), price<0 (open buys) | ||
| -- Deletes losing outcome holdings and sells, which have zero value after settlement | ||
| -- This removes ~50% of positions upfront | ||
| DELETE FROM ob_positions | ||
| WHERE query_id = $query_id | ||
| AND outcome = NOT $winning_outcome | ||
| AND price >= 0; -- Holdings (price=0) and open sells (price>0) only | ||
|
|
||
| -- Step 2: Collect ALL payout data using CTE + ARRAY_AGG (digest pattern!) | ||
| -- Calculate payouts and aggregate into arrays in a SINGLE query | ||
| $wallet_addresses TEXT[]; | ||
| $amounts NUMERIC(78, 0)[]; | ||
|
|
||
| for $result in | ||
| WITH remaining_positions AS ( | ||
| SELECT | ||
| p.participant_id, | ||
| p.outcome, | ||
| p.price, | ||
| p.amount, | ||
| '0x' || encode(part.wallet_address, 'hex') as wallet_address | ||
| FROM ob_positions p | ||
| JOIN ob_participants part ON p.participant_id = part.id | ||
| WHERE p.query_id = $query_id | ||
| ), | ||
| calculated_values AS ( | ||
| SELECT | ||
| wallet_address, | ||
| price, | ||
| -- Pre-calculate all monetary values to avoid CASE type issues | ||
| -- All amounts cast to NUMERIC(78, 0) to match ethereum_bridge.unlock() API | ||
| (amount::NUMERIC(78, 0) * $one_token)::NUMERIC(78, 0) as gross_winner_payout, | ||
| ((amount::NUMERIC(78, 0) * $one_token * $redemption_fee_bps::NUMERIC(78, 0)) / 10000::NUMERIC(78, 0))::NUMERIC(78, 0) as winner_fee, | ||
| ((amount::NUMERIC(78, 0) * abs(price)::NUMERIC(78, 0) * $one_token) / 100::NUMERIC(78, 0))::NUMERIC(78, 0) as refund_amount | ||
| FROM remaining_positions | ||
| ), | ||
| payouts AS ( | ||
| SELECT | ||
| wallet_address, | ||
| -- Remaining positions after Step 1 are: | ||
| -- 1. Winning holdings/sells (price >= 0): Pay shares × $1 - 2% fee | ||
| -- 2. Open buy orders (price < 0): Refund locked collateral, no fee | ||
| CASE | ||
| WHEN price >= 0 THEN | ||
| gross_winner_payout - winner_fee | ||
| ELSE | ||
| refund_amount | ||
| END as payout_amount, | ||
| CASE | ||
| WHEN price >= 0 THEN | ||
| winner_fee | ||
| ELSE | ||
| '0'::NUMERIC(78, 0) | ||
| END as fee_amount | ||
| FROM calculated_values | ||
| ), | ||
| wallet_totals AS ( | ||
| -- Group by wallet to handle multiple positions per user | ||
| SELECT | ||
| wallet_address, | ||
| SUM(payout_amount) as total_payout, | ||
| SUM(fee_amount) as total_fees | ||
| FROM payouts | ||
| GROUP BY wallet_address | ||
| ), | ||
| aggregated AS ( | ||
| SELECT | ||
| ARRAY_AGG(wallet_address ORDER BY wallet_address) as wallets, | ||
| ARRAY_AGG(total_payout::NUMERIC(78, 0) ORDER BY wallet_address) as amounts, | ||
| SUM(total_fees)::NUMERIC(78, 0) as total_fees | ||
| FROM wallet_totals | ||
| ) | ||
| SELECT wallets, amounts, COALESCE(total_fees, 0::NUMERIC(78, 0)) as total_fees | ||
| FROM aggregated | ||
| { | ||
| $wallet_addresses := $result.wallets; | ||
| $amounts := $result.amounts; | ||
| $total_fees_collected := $result.total_fees; | ||
| } | ||
|
|
||
| -- Step 3: Delete all processed positions (set-based, no loop!) | ||
| DELETE FROM ob_positions WHERE query_id = $query_id; | ||
|
|
||
| -- Step 4: Process ALL payouts in a SINGLE batch call (no nested queries!) | ||
| if $wallet_addresses IS NOT NULL AND COALESCE(array_length($wallet_addresses), 0) > 0 { | ||
| ob_batch_unlock_collateral($wallet_addresses, $amounts); | ||
| } | ||
|
|
||
| -- Step 5: Fee distribution (Issue 9 will implement this) | ||
| -- Fees are automatically kept in the vault by deducting from unlocked amounts. | ||
| -- Winners receive (shares × $1 - 2% fee), so 2% remains locked in vault. | ||
| -- | ||
| -- $total_fees_collected tracks the amount for future distribution: | ||
| -- TODO (Issue 9): Uncomment when distribute_fees() is implemented | ||
| -- distribute_fees($query_id, $total_fees_collected); | ||
| -- | ||
| -- Verification: Check vault balance via ethereum_bridge queries | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.