From 0545663e0fd77bb6c7a872078784a1c811edb4d5 Mon Sep 17 00:00:00 2001 From: Michael Buntarman Date: Fri, 24 Apr 2026 10:15:21 +0700 Subject: [PATCH] chore: add eth_truf as order book collateral birdge --- .../migrations/031-order-book-vault.prod.sql | 18 +- .../032-order-book-actions.prod.sql | 45 +++-- .../033-order-book-settlement.prod.sql | 23 ++- .../037-order-book-validation.prod.sql | 18 +- scripts/generate_prod_migrations.py | 158 ++++++++++++------ 5 files changed, 175 insertions(+), 87 deletions(-) diff --git a/internal/migrations/031-order-book-vault.prod.sql b/internal/migrations/031-order-book-vault.prod.sql index ae864d3d..142e2e6d 100644 --- a/internal/migrations/031-order-book-vault.prod.sql +++ b/internal/migrations/031-order-book-vault.prod.sql @@ -26,10 +26,13 @@ PRIVATE { } -- Lock collateral using bridge (user -> network ownedBalance) - if $bridge != 'eth_usdc' { - ERROR('Invalid bridge. Supported: eth_usdc'); + if $bridge = 'eth_usdc' { + eth_usdc.lock($amount); + } else if $bridge = 'eth_truf' { + eth_truf.lock($amount); + } else { + ERROR('Invalid bridge. Supported: eth_usdc, eth_truf'); } - eth_usdc.lock($amount); }; CREATE OR REPLACE ACTION ob_unlock_collateral($bridge TEXT, $user_address TEXT, $amount NUMERIC(78, 0)) @@ -44,8 +47,11 @@ PRIVATE { } -- Unlock collateral using bridge (network ownedBalance -> user) - if $bridge != 'eth_usdc' { - ERROR('Invalid bridge. Supported: eth_usdc'); + if $bridge = 'eth_usdc' { + eth_usdc.unlock($user_address, $amount); + } else if $bridge = 'eth_truf' { + eth_truf.unlock($user_address, $amount); + } else { + ERROR('Invalid bridge. Supported: eth_usdc, eth_truf'); } - eth_usdc.unlock($user_address, $amount); }; diff --git a/internal/migrations/032-order-book-actions.prod.sql b/internal/migrations/032-order-book-actions.prod.sql index 1b7608a6..1e0c6963 100644 --- a/internal/migrations/032-order-book-actions.prod.sql +++ b/internal/migrations/032-order-book-actions.prod.sql @@ -19,8 +19,9 @@ CREATE OR REPLACE ACTION validate_bridge($bridge TEXT) PRIVATE { ERROR('bridge parameter is required'); } - if $bridge != 'eth_usdc' { - ERROR('Invalid bridge. Supported: eth_usdc'); + if $bridge != 'eth_usdc' AND + $bridge != 'eth_truf' { + ERROR('Invalid bridge. Supported: eth_usdc, eth_truf'); } RETURN; @@ -253,9 +254,11 @@ CREATE OR REPLACE ACTION place_buy_order( -- ========================================================================== $caller_balance NUMERIC(78, 0); - $caller_balance := COALESCE(eth_usdc.balance(@caller), 0::NUMERIC(78, 0)); - - if $caller_balance < $collateral_needed { + if $bridge = 'eth_usdc' { + $caller_balance := COALESCE(eth_usdc.balance(@caller), 0::NUMERIC(78, 0)); + } else if $bridge = 'eth_truf' { + $caller_balance := COALESCE(eth_truf.balance(@caller), 0::NUMERIC(78, 0)); + }if $caller_balance < $collateral_needed { -- Note: Division by 10^18 for display purposes (convert wei to TRUF) ERROR('Insufficient balance. Required: ' || $collateral_needed::TEXT || ' wei (' || ($collateral_needed / '1000000000000000000'::NUMERIC(78, 0))::TEXT || ' TRUF)'); @@ -296,9 +299,11 @@ CREATE OR REPLACE ACTION place_buy_order( -- Lock tokens from user to vault (network-owned balance) -- Note: Bridge lock() throws ERROR on failure (insufficient balance, etc.) - eth_usdc.lock($collateral_needed); - - -- Record initial impact (collateral spent) + if $bridge = 'eth_usdc' { + eth_usdc.lock($collateral_needed); + } else if $bridge = 'eth_truf' { + eth_truf.lock($collateral_needed); + }-- Record initial impact (collateral spent) ob_record_tx_impact($participant_id, $outcome, 0::INT8, $collateral_needed, TRUE); -- ========================================================================== @@ -418,9 +423,11 @@ CREATE OR REPLACE ACTION place_split_limit_order( -- ========================================================================== $caller_balance NUMERIC(78, 0); - $caller_balance := COALESCE(eth_usdc.balance(@caller), 0::NUMERIC(78, 0)); - - if $caller_balance < $collateral_needed { + if $bridge = 'eth_usdc' { + $caller_balance := COALESCE(eth_usdc.balance(@caller), 0::NUMERIC(78, 0)); + } else if $bridge = 'eth_truf' { + $caller_balance := COALESCE(eth_truf.balance(@caller), 0::NUMERIC(78, 0)); + }if $caller_balance < $collateral_needed { -- Note: Division by 10^18 for display purposes (convert wei to TRUF) ERROR('Insufficient balance. Required: ' || $collateral_needed::TEXT || ' wei (' || ($collateral_needed / '1000000000000000000'::NUMERIC(78, 0))::TEXT || ' TRUF)'); @@ -461,9 +468,11 @@ CREATE OR REPLACE ACTION place_split_limit_order( -- Lock tokens from user to vault (network-owned balance) -- Note: Bridge lock() throws ERROR on failure (insufficient balance, etc.) - eth_usdc.lock($collateral_needed); - - -- Record initial impacts: + if $bridge = 'eth_usdc' { + eth_usdc.lock($collateral_needed); + } else if $bridge = 'eth_truf' { + eth_truf.lock($collateral_needed); + }-- Record initial impacts: -- Calculate split collateral (50/50 split for YES/NO legs) $collateral_per_leg NUMERIC(78, 0) := $collateral_needed / 2::NUMERIC(78, 0); -- Handle dust: add remainder to YES leg if odd amount @@ -669,9 +678,11 @@ CREATE OR REPLACE ACTION change_bid( if $collateral_delta > $zero { -- New order needs MORE collateral -- Lock additional amount (will ERROR if insufficient balance) - eth_usdc.lock($collateral_delta); - - -- Record initial impact (lock) + if $bridge = 'eth_usdc' { + eth_usdc.lock($collateral_delta); + } else if $bridge = 'eth_truf' { + eth_truf.lock($collateral_delta); + }-- Record initial impact (lock) ob_record_tx_impact($participant_id, $outcome, 0::INT8, $collateral_delta, TRUE); } else if $collateral_delta < $zero { -- New order needs LESS collateral diff --git a/internal/migrations/033-order-book-settlement.prod.sql b/internal/migrations/033-order-book-settlement.prod.sql index 048ba66b..f51efc4b 100644 --- a/internal/migrations/033-order-book-settlement.prod.sql +++ b/internal/migrations/033-order-book-settlement.prod.sql @@ -27,8 +27,11 @@ CREATE OR REPLACE ACTION ob_batch_unlock_collateral( SELECT u.wallet_hex, u.amount FROM UNNEST($wallet_hexes, $amounts) AS u(wallet_hex, amount) { - eth_usdc.unlock($payout.wallet_hex, $payout.amount); - } + if $bridge = 'eth_usdc' { + eth_usdc.unlock($payout.wallet_hex, $payout.amount); + } else if $bridge = 'eth_truf' { + eth_truf.unlock($payout.wallet_hex, $payout.amount); + }} -- 2. Record all impacts using a loop to avoid "unknown variable" in INSERT...SELECT $next_id INT; @@ -100,9 +103,11 @@ CREATE OR REPLACE ACTION distribute_fees( $actual_dp_fees NUMERIC(78, 0) := '0'::NUMERIC(78, 0); if $dp_addr IS NOT NULL AND $infra_share > '0'::NUMERIC(78, 0) { $dp_wallet_hex TEXT := '0x' || encode($dp_addr, 'hex'); - eth_usdc.unlock($dp_wallet_hex, $infra_share); - - $actual_dp_fees := $infra_share; + if $bridge = 'eth_usdc' { + eth_usdc.unlock($dp_wallet_hex, $infra_share); + } else if $bridge = 'eth_truf' { + eth_truf.unlock($dp_wallet_hex, $infra_share); + }$actual_dp_fees := $infra_share; -- Ensure DP has a participant record so the fee is tracked in ob_net_impacts $dp_pid INT; @@ -154,9 +159,11 @@ CREATE OR REPLACE ACTION distribute_fees( -- Skip validators with zero payout (e.g., infra_share < validator_count) if $v_payout > '0'::NUMERIC(78, 0) { -- Unlock funds via bridge - eth_usdc.unlock($v_wallet_hex, $v_payout); - - -- Ensure validator has a participant record + if $bridge = 'eth_usdc' { + eth_usdc.unlock($v_wallet_hex, $v_payout); + } else if $bridge = 'eth_truf' { + eth_truf.unlock($v_wallet_hex, $v_payout); + }-- Ensure validator has a participant record $v_pid INT; for $p in SELECT id FROM ob_participants WHERE wallet_address = $v_wallet_bytes { $v_pid := $p.id; } if $v_pid IS NULL { diff --git a/internal/migrations/037-order-book-validation.prod.sql b/internal/migrations/037-order-book-validation.prod.sql index fb63f0d7..25e00c90 100644 --- a/internal/migrations/037-order-book-validation.prod.sql +++ b/internal/migrations/037-order-book-validation.prod.sql @@ -111,12 +111,18 @@ PUBLIC VIEW RETURNS ( $vault_balance NUMERIC(78, 0) := 0::NUMERIC(78, 0); $row_count INT := 0; - if $bridge != 'eth_usdc' { - ERROR('Invalid bridge. Supported: eth_usdc'); - } - for $info in eth_usdc.info() { - $vault_balance := $info.balance; - $row_count := $row_count + 1; + if $bridge = 'eth_usdc' { + for $info in eth_usdc.info() { + $vault_balance := $info.balance; + $row_count := $row_count + 1; + } + } else if $bridge = 'eth_truf' { + for $info in eth_truf.info() { + $vault_balance := $info.balance; + $row_count := $row_count + 1; + } + } else { + ERROR('Invalid bridge. Supported: eth_usdc, eth_truf'); } -- Validate that bridge returned data (distinguish unavailable from empty vault) diff --git a/scripts/generate_prod_migrations.py b/scripts/generate_prod_migrations.py index 6859c48f..3dcd390c 100644 --- a/scripts/generate_prod_migrations.py +++ b/scripts/generate_prod_migrations.py @@ -150,19 +150,30 @@ def split_actions(sql: str) -> list[tuple[str, str]]: def substitute_tokens(text: str) -> str: """Apply the bridge-name substitutions. + Maps testnet / legacy bridges to their mainnet equivalents: + hoodi_tt2 -> eth_usdc (USDC collateral) + hoodi_tt -> eth_truf (TRUF — fee/collateral) + ethereum_bridge -> eth_truf (legacy mainnet TRUF) + Order matters: `hoodi_tt2` must be replaced before `hoodi_tt`, otherwise the second pass would corrupt the suffix. """ text = text.replace("hoodi_tt2", "eth_usdc") text = text.replace("hoodi_tt", "eth_truf") + text = text.replace("ethereum_bridge", "eth_truf") return text -_DISPATCH_BRANCH_RE = re.compile( - r"if\s+\$bridge\s*=\s*'eth_usdc'\s*\{", +# After substitute_tokens, the only mainnet collateral bridges we want to +# keep in dispatch chains are eth_usdc and eth_truf. Anything else +# (sepolia_bridge, future testnet aliases) gets dropped. +MAINNET_BRIDGES = ("eth_usdc", "eth_truf") + +_BRANCH_HEADER_RE = re.compile( + r"(?:if|else\s+if)\s+\$bridge\s*=\s*'(?P\w+)'\s*\{", ) -_ELSEIF_HEADER_RE = re.compile( - r"\s*else\s+if\s+\$bridge\s*=\s*'(?:sepolia_bridge|ethereum_bridge)'\s*\{", +_IF_BRANCH_HEADER_RE = re.compile( + r"if\s+\$bridge\s*=\s*'(?P\w+)'\s*\{", ) _ELSE_HEADER_RE = re.compile(r"\s*else\s*\{") @@ -187,66 +198,105 @@ def _scan_balanced_block(text: str, start: int) -> int: def collapse_dispatch(action_text: str) -> str: - """Collapse `if $bridge = '' { ... } else if ... else if ... [else { ERROR }]` - chains in `action_text` so only the `eth_usdc` branch remains. - - If the original chain ended in an `else { ERROR(...) }` clause, the - eth_usdc body is prefixed with a guard `if $bridge != 'eth_usdc' { - ERROR(...) }` to preserve the original "reject unknown bridge" - semantics. Otherwise the body is inlined naked, mirroring the - original silent fall-through behavior. - - Multiple dispatch chains in the same action are handled iteratively. + """Walk each `if $bridge = '' { ... } else if ... [else { ... }]` + chain, keep only branches whose `` is a mainnet bridge + (eth_usdc, eth_truf), and drop the rest (sepolia_bridge etc.). + + Behavior summary: + * Single mainnet branch in the chain -> inline body, prefix with a + `if $bridge != '' { ERROR }` guard if the original had a final + `else { ERROR }`. Mirrors original silent-fall-through if no guard. + * Two mainnet branches -> rebuild as + `if … else if … [else { ERROR }]` listing only the mainnet bridges + in the supported-list message. + * No mainnet branches -> drop the entire chain + (shouldn't happen post-substitution, but safe). + + Multiple chains per action are handled iteratively. """ out = [] i = 0 n = len(action_text) while i < n: - m = _DISPATCH_BRANCH_RE.search(action_text, i) + # Find next dispatch CHAIN START (must begin with `if`, not `else if`). + m = _IF_BRANCH_HEADER_RE.search(action_text, i) if not m: out.append(action_text[i:]) break - # Emit everything before the dispatch verbatim. - out.append(action_text[i:m.start()]) - # eth_usdc branch: scan its body. - body_open = m.end() - 1 # index of `{` - body_end = _scan_balanced_block(action_text, body_open) - usdc_body_inner = action_text[body_open + 1:body_end - 1] + # Emit everything before the chain verbatim. + out.append(action_text[i:m.start()]) - # Determine the leading whitespace of the original `if` line so we - # can re-indent the inlined body to match. + # Capture indentation of the `if` line for re-indentation. line_start = action_text.rfind("\n", 0, m.start()) + 1 indent = action_text[line_start:m.start()] - cursor = body_end - # Consume `else if $bridge = 'sepolia_bridge' { ... }` and - # `else if $bridge = 'ethereum_bridge' { ... }` (in either order, - # zero or more times). - while True: - em = _ELSEIF_HEADER_RE.match(action_text, cursor) - if not em: + # Walk the chain, collecting (name, body_inner) for each branch. + branches: list[tuple[str, str]] = [] + cursor = m.start() + while cursor < n: + bm = _BRANCH_HEADER_RE.match(action_text, cursor) + if not bm: + break + name = bm.group("name") + body_open = bm.end() - 1 # the `{` + body_end = _scan_balanced_block(action_text, body_open) + body_inner = action_text[body_open + 1:body_end - 1] + branches.append((name, body_inner)) + cursor = body_end + # Skip whitespace/newlines before the next `else …` or end of chain. + while cursor < n and action_text[cursor] in " \t\n": + cursor += 1 + # If next token isn't `else`, the chain ends here. + if not action_text[cursor:cursor + 4].startswith("else"): break - cursor = _scan_balanced_block(action_text, em.end() - 1) - # Optional final `else { ... }` — typically an ERROR. + # Optional final `else { ... }`. had_final_else = False em = _ELSE_HEADER_RE.match(action_text, cursor) if em: had_final_else = True cursor = _scan_balanced_block(action_text, em.end() - 1) - # Build replacement. - usdc_body = _redent_body(usdc_body_inner, indent) - if had_final_else: - replacement = ( - f"if $bridge != 'eth_usdc' {{\n" - f"{indent} ERROR('Invalid bridge. Supported: eth_usdc');\n" - f"{indent}}}\n" - f"{indent}{usdc_body}" - ) + # Filter to mainnet branches, preserving source order. + mainnet = [(name, body) for name, body in branches if name in MAINNET_BRIDGES] + + if not mainnet: + # All branches were testnet — drop the whole chain. + replacement = "" + elif len(mainnet) == 1: + # Single branch: inline (with optional guard). + name, body_inner = mainnet[0] + body = _redent_body(body_inner, indent) + if had_final_else: + replacement = ( + f"if $bridge != '{name}' {{\n" + f"{indent} ERROR('Invalid bridge. Supported: {name}');\n" + f"{indent}}}\n" + f"{indent}{body}" + ) + else: + replacement = body else: - replacement = usdc_body + # Multiple mainnet branches: rebuild if/else-if chain. + inner_indent = indent + " " + parts: list[str] = [] + for idx, (name, body_inner) in enumerate(mainnet): + body = _redent_body(body_inner, inner_indent) + if idx == 0: + parts.append(f"if $bridge = '{name}' {{") + else: + parts.append(f"{indent}}} else if $bridge = '{name}' {{") + parts.append(f"{inner_indent}{body}") + if had_final_else: + supported = ", ".join(name for name, _ in mainnet) + parts.append(f"{indent}}} else {{") + parts.append(f"{inner_indent}ERROR('Invalid bridge. Supported: {supported}');") + parts.append(f"{indent}}}") + else: + parts.append(f"{indent}}}") + replacement = "\n".join(parts) + out.append(replacement) i = cursor return "".join(out) @@ -290,24 +340,32 @@ def _redent_body(body: str, target_indent: str) -> str: return "\n".join(out_lines) +# After substitute_tokens, the validate_bridge AND chain reads: +# $bridge != 'eth_usdc' AND $bridge != 'sepolia_bridge' AND $bridge != 'eth_truf' +# We want to drop the sepolia_bridge clause and update the matching ERROR +# string so the action accepts both mainnet bridges. _VALIDATE_AND_CHAIN_RE = re.compile( r"\$bridge\s*!=\s*'eth_usdc'\s+AND\s*\n?\s*" r"\$bridge\s*!=\s*'sepolia_bridge'\s+AND\s*\n?\s*" - r"\$bridge\s*!=\s*'ethereum_bridge'", + r"\$bridge\s*!=\s*'eth_truf'", +) +_VALIDATE_AND_REPLACEMENT = ( + "$bridge != 'eth_usdc' AND\n" + " $bridge != 'eth_truf'" ) _SUPPORTED_LIST_RE = re.compile( - r"Supported:\s+eth_usdc,\s+sepolia_bridge,\s+ethereum_bridge", + r"Supported:\s+eth_usdc,\s+sepolia_bridge,\s+eth_truf", ) def collapse_validate_and_chain(text: str) -> str: - """Collapse the `$bridge != 'eth_usdc' AND $bridge != 'sepolia_bridge' - AND $bridge != 'ethereum_bridge'` predicate (used in - `validate_bridge`) to a single inequality, and shrink the matching - "Supported:" ERROR list. Idempotent. + """Collapse the three-clause `$bridge != …` predicate (used in + `validate_bridge`) to drop the sepolia_bridge testnet clause, and + shrink the matching "Supported:" ERROR list to the mainnet bridges + only. Idempotent. """ - text = _VALIDATE_AND_CHAIN_RE.sub("$bridge != 'eth_usdc'", text) - text = _SUPPORTED_LIST_RE.sub("Supported: eth_usdc", text) + text = _VALIDATE_AND_CHAIN_RE.sub(_VALIDATE_AND_REPLACEMENT, text) + text = _SUPPORTED_LIST_RE.sub("Supported: eth_usdc, eth_truf", text) return text