From 613e9218ff738e8d933b8e5fb0a1e6b3b5a5d93a Mon Sep 17 00:00:00 2001 From: walerikus <35748353+walerikus@users.noreply.github.com> Date: Sun, 28 Jun 2026 12:25:34 +0200 Subject: [PATCH] feat(build-b): B3/B4/B5 lock-and-mint tUSDt validated (real Tron lock + reconciliation) Build B Flavor 2 (the credit/debit lock-and-mint), all 7 checks PASS on real coins across both chains: - B3 lock+mint: Alice locks 5 real USDT in TronUSDTVault; bridge oracle attests; reused DeviantWrapMintIOU mints exactly 5,000,000 tUSDt (covenant-enforced 1:1) - B5a over-mint: replaying the attestation is rejected (outpoint-bound) - B4 redeem: return tUSDt on BCH -> operator releases the USDT on Tron - B5b reconcile: sum(tUSDt circulating) == USDT locked at both states (5:5, 0:0) Upgrades chipnet-btc-iou-demo (simulated lock) to a REAL Tron lock with on-chain cross-chain reconciliation. Vault uses balance-delta moves (Tether gotcha). v1 witness = single oracle/operator (trust-minimized); B6 = bonded M-of-N. contracts/tron/TronUSDTVault.sol scripts/tron-bch-lock-mint.mjs Co-Authored-By: Claude Opus 4.8 --- .../ASM DEX/build-B_usdt-tron-bch_scope.md | 24 +- contracts/tron/TronUSDTVault.sol | 62 +++++ scripts/tron-bch-lock-mint.mjs | 219 ++++++++++++++++++ 3 files changed, 302 insertions(+), 3 deletions(-) create mode 100644 contracts/tron/TronUSDTVault.sol create mode 100644 scripts/tron-bch-lock-mint.mjs diff --git a/Parameters/ASM DEX/build-B_usdt-tron-bch_scope.md b/Parameters/ASM DEX/build-B_usdt-tron-bch_scope.md index c0e8f5f..1290354 100644 --- a/Parameters/ASM DEX/build-B_usdt-tron-bch_scope.md +++ b/Parameters/ASM DEX/build-B_usdt-tron-bch_scope.md @@ -47,9 +47,9 @@ USDT **stays** dollar-denominated as `τUSDt` on BCH; trust-**minimized**. |---|---|---|---|---| | **B1** | TRC20 HTLC on Nile — claim / refund / wrong-secret | Nile | the Tron HTLC primitive (sha256) | ✅ **validated 2026-06-27** | | **B2** | trustless swap USDT(Nile) ↔ BCH(chipnet) — one sha256 over both legs | Nile + chipnet | **Flavor 1** end-to-end (T4-equivalent for Tron) | ✅ **validated 2026-06-28** | -| **B3** | Tron vault lock → oracle attest → BCH mints `τUSDt` | Nile + chipnet | **Flavor 2** happy path (lock-and-mint) | planned | -| **B4** | burn `τUSDt` on BCH → vault releases USDT to user | Nile + chipnet | redeem (reverse posting) | planned | -| **B5** | reconciliation + **over-mint rejection** (mint > lock fails) | chipnet | the verifiability invariant | planned | +| **B3** | Tron vault lock → oracle attest → BCH mints `τUSDt` | Nile + chipnet | **Flavor 2** happy path (lock-and-mint) | ✅ **validated 2026-06-28** | +| **B4** | return `τUSDt` on BCH → vault releases USDT to user | Nile + chipnet | redeem (reverse posting) | ✅ **validated 2026-06-28** | +| **B5** | reconciliation + **over-mint rejection** (replay attestation fails) | chipnet | the verifiability invariant | ✅ **validated 2026-06-28** | | **B6** | bonded M-of-N vault variant (slashable equivocation) | Nile + chipnet | the trust floor (tBTC-v2-style) | later | ## B1 result (validated 2026-06-27) @@ -87,6 +87,24 @@ This applies to USDT on every chain (ETH/BSC/Solana are the same Tether family), lock-and-mint vault (B3) must use the same balance-delta pattern. Diagnosed with `tron-usdt-probe.mjs` + `tron-sim-transfer.mjs` (constant-call surfaced the `false` return). +## B3/B4/B5 result (validated 2026-06-28) + +`contracts/tron/TronUSDTVault.sol` + `scripts/tron-bch-lock-mint.mjs` — the real +lock-and-mint cycle (Flavor 2), upgrading `chipnet-btc-iou-demo.mjs` from a *simulated* +lock to a **real Tron lock with on-chain reconciliation across both chains**. All 7 +checks PASS: +- **B3** Alice locked 5 real USDT in `TronUSDTVault`; the bridge oracle attested it; the + reused `DeviantWrapMintIOU` minted exactly 5,000,000 `τUSDt` to Alice (covenant 1:1). +- **B5a** replaying the same oracle attestation was **rejected** (`DeviantWrapMintIOU.cash:48`, + outpoint-bound → one attestation = one mint). +- **B4** Alice returned the `τUSDt` to the bridge; the operator released the 5 USDT. +- **B5b** `Σ τUSDt circulating == USDT locked` held at both states (5:5 then 0:0). + +v1 witness = a single oracle/operator (trust-MINIMIZED, not trustless — the VM wall). +Token moves in the vault use the balance-delta pattern (the Tether-`false` gotcha). The +oracle message layout (69 bytes: recipientLockingBytecode‖amount‖reservoirOutpoint) and +Schnorr `checkDataSig` are reused verbatim from the BTC-IOU machinery. + ## Components & deps - **Installed:** `tronweb` (Nile RPC + signing), BCH stack (cashc, mainnet-js, diff --git a/contracts/tron/TronUSDTVault.sol b/contracts/tron/TronUSDTVault.sol new file mode 100644 index 0000000..adc723b --- /dev/null +++ b/contracts/tron/TronUSDTVault.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/** + * TronUSDTVault — the source-chain LOCK for Build B Flavor 2 (lock-and-mint). + * + * lock() : pulls USDT in, records it, emits Locked — the bridge oracle observes + * this and mints an equal τUSDt IOU on BCH (DeviantWrapMintIOU). + * release() : operator-gated payout for redeem — when a τUSDt IOU is returned/burned + * on BCH, the operator releases the matching USDT here. + * + * v1 trust = a single `operator` (the bridge). B6 replaces this with a bonded M-of-N + * signer set (BondedSignerSlash.cash). Reconciliation is public: lockedBalance() + * (real USDT held) must equal Σ τUSDt minted-and-circulating on BCH. + * + * Token moves use BALANCE DELTA, not the return value — Tether performs the transfer + * but returns false (see TronHTLC.sol / the Tether gotcha). + */ +interface ITRC20 { + function balanceOf(address account) external view returns (uint256); +} + +contract TronUSDTVault { + address public immutable token; // USDT (TRC20) + address public operator; // bridge release authority (v1) + uint256 public totalLocked; // accounting; == lockedBalance() absent fees + uint256 public lockCount; + + event Locked(uint256 indexed lockId, address indexed from, uint256 amount, bytes32 bchRecipient); + event Released(uint256 indexed redeemId, address indexed to, uint256 amount); + + constructor(address _token, address _operator) { token = _token; operator = _operator; } + + /** Lock USDT to be wrapped onto BCH. Caller must approve() first. */ + function lock(uint256 amount, bytes32 bchRecipient) external returns (uint256 lockId) { + require(amount > 0, "amount=0"); + uint256 pre = ITRC20(token).balanceOf(address(this)); + (bool ok, ) = token.call(abi.encodeWithSelector(0x23b872dd, msg.sender, address(this), amount)); + require(ok, "transferFrom reverted"); + require(ITRC20(token).balanceOf(address(this)) >= pre + amount, "transferFrom not credited"); + totalLocked += amount; + lockId = lockCount++; + emit Locked(lockId, msg.sender, amount, bchRecipient); + } + + /** Release locked USDT on redeem (operator-gated in v1). */ + function release(uint256 redeemId, address to, uint256 amount) external { + require(msg.sender == operator, "not operator"); + require(amount <= totalLocked, "exceeds locked"); + totalLocked -= amount; + uint256 pre = ITRC20(token).balanceOf(to); + (bool ok, ) = token.call(abi.encodeWithSelector(0xa9059cbb, to, amount)); + require(ok, "transfer reverted"); + require(ITRC20(token).balanceOf(to) >= pre + amount, "transfer not credited"); + emit Released(redeemId, to, amount); + } + + /** Real USDT held — the public, on-chain backing for reconciliation. */ + function lockedBalance() external view returns (uint256) { + return ITRC20(token).balanceOf(address(this)); + } +} diff --git a/scripts/tron-bch-lock-mint.mjs b/scripts/tron-bch-lock-mint.mjs new file mode 100644 index 0000000..5839ff6 --- /dev/null +++ b/scripts/tron-bch-lock-mint.mjs @@ -0,0 +1,219 @@ +#!/usr/bin/env node +/** + * tron-bch-lock-mint.mjs — Build B milestones B3/B4/B5: the lock-and-mint IOU + * (Flavor 2, the credit/debit model). USDT stays dollar-denominated as τUSDt on BCH. + * + * Unlike chipnet-btc-iou-demo.mjs (which SIMULATES the lock), here the lock is REAL on + * Tron Nile (TronUSDTVault), and reconciliation reads BOTH chains. + * + * B3 lock+mint : Alice locks real USDT in the Tron vault → bridge oracle attests it + * → DeviantWrapMintIOU mints exactly that many τUSDt to Alice on BCH. + * B5a over-mint: replaying the same oracle attestation is REJECTED (outpoint-bound). + * B4 redeem : Alice returns τUSDt to the bridge on BCH → operator releases the + * matching USDT from the Tron vault. + * B5b reconcile: Σ τUSDt circulating (BCH) == USDT locked (Tron), at both states. + * + * v1 witness = a single oracle/operator (the bridge) — trust-MINIMIZED, not trustless. + * B6 swaps it for bonded M-of-N. Run: node --import tsx scripts/tron-bch-lock-mint.mjs --yes + */ +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { TestNetWallet } from 'mainnet-js'; +import { Contract, ElectrumNetworkProvider, TransactionBuilder, SignatureTemplate } from 'cashscript'; +import { secp256k1, sha256 as laSha256 } from '@bitauth/libauth'; +import solc from 'solc'; +import { TronWeb } from 'tronweb'; + +const ROOT = join(import.meta.dirname, '..'); +const WRAP_ART = join(ROOT, 'contracts/dist/DeviantWrapMintIOU.json'); +const KEYS = { b: join(ROOT, '.chipnet-keys-b.json'), c: join(ROOT, '.chipnet-keys-c.json'), e: join(ROOT, '.chipnet-keys-e.json') }; +const NILE = 'https://nile.trongrid.io'; +const USDT = 'TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf'; +const LOCK = 5_000_000; // 5 USDT (6 dec) → 5_000_000 τUSDt units (1:1 atomic) +const SUPPLY = 100_000_000n; // τUSDt mint reservoir +const DUST = 1000n, MF = 2500n; +const CALL_FEE = 400_000_000, DEPLOY_FEE = 1_500_000_000; + +// ---- BCH helpers (from chipnet-btc-iou-demo.mjs) ---- +const hexToBin = (h) => { const o = new Uint8Array(h.length / 2); for (let i = 0; i < o.length; i++) o[i] = parseInt(h.slice(i * 2, i * 2 + 2), 16); return o; }; +const txidLE = (h) => { const d = hexToBin(h); const o = new Uint8Array(d.length); for (let i = 0; i < d.length; i++) o[i] = d[d.length - 1 - i]; return o; }; +const p2pkh = (pkh) => { const o = new Uint8Array(25); o[0] = 0x76; o[1] = 0xa9; o[2] = 0x14; o.set(pkh, 3); o[23] = 0x88; o[24] = 0xac; return o; }; +const u32le = (n) => { const b = new Uint8Array(4); new DataView(b.buffer).setUint32(0, Number(n), true); return b; }; +const u64le = (n) => { const b = new Uint8Array(8); new DataView(b.buffer).setBigUint64(0, BigInt(n), true); return b; }; +const oracleSign = (priv, msg) => { const s = secp256k1.signMessageHashSchnorr(priv, laSha256.hash(msg)); if (typeof s === 'string') throw new Error(s); return s; }; +const mintMessage = (lb, amt, resTxid, resVout) => { const m = new Uint8Array(69); m.set(lb, 0); m.set(u64le(amt), 25); m.set(txidLE(resTxid), 33); m.set(u32le(resVout), 65); return m; }; +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); +const divider = (c = '=') => console.log(c.repeat(80)); +const step = (t) => { console.log(''); divider(); console.log(t); divider(); }; +const txidOf = (s) => (typeof s === 'string' ? s : s?.txid ?? s?.txId); +const hexOf = (u) => Buffer.from(u).toString('hex'); +function pickBch(u, need) { const p = []; let t = 0n; for (const x of u) { p.push(x); t += BigInt(x.satoshis); if (t >= need) break; } return { p, t }; } + +// ---- Tron helpers ---- +let tronWeb; +function compileVault() { + const out = JSON.parse(solc.compile(JSON.stringify({ + language: 'Solidity', + sources: { 'TronUSDTVault.sol': { content: readFileSync(join(ROOT, 'contracts/tron/TronUSDTVault.sol'), 'utf8') } }, + settings: { evmVersion: 'istanbul', optimizer: { enabled: true, runs: 200 }, outputSelection: { '*': { '*': ['abi', 'evm.bytecode.object'] } } }, + }))); + const errs = (out.errors || []).filter((e) => e.severity === 'error'); + if (errs.length) { errs.forEach((e) => console.error(e.formattedMessage)); throw new Error('solc failed'); } + return { abi: out.contracts['TronUSDTVault.sol'].TronUSDTVault.abi, bytecode: out.contracts['TronUSDTVault.sol'].TronUSDTVault.evm.bytecode.object }; +} +async function tronSend(contract, method, args, label, feeLimit = CALL_FEE) { + const txid = await contract[method](...args).send({ feeLimit, callValue: 0 }); + let info; + for (let i = 0; i < 20; i++) { await sleep(3000); info = await tronWeb.trx.getTransactionInfo(txid); if (info && info.receipt) break; } + const result = info?.receipt?.result || (info?.blockNumber > 0 ? 'SUCCESS' : 'PENDING'); + console.log(` ${label}: ${result} ${txid.slice(0, 12)}…`); + if (result !== 'SUCCESS') throw new Error(`${label} -> ${result}`); + return txid; +} + +async function main() { + if (!process.argv.includes('--yes')) { console.log('Re-run with --yes to broadcast on Nile + chipnet.'); return; } + const art = JSON.parse(readFileSync(WRAP_ART, 'utf8')); + + // oracle key (create if missing — it only signs, needs no funds) + if (!existsSync(KEYS.e)) { const w = await TestNetWallet.newRandom(); writeFileSync(KEYS.e, JSON.stringify({ wif: w.privateKeyWif }, null, 2)); console.log('generated oracle key .chipnet-keys-e.json'); } + + // Tron: Alice locks; operator (= Alice's Tron key here, i.e. the bridge) releases + const tronKey = JSON.parse(readFileSync(join(ROOT, '.tron-nile-keys.json'), 'utf8')); + tronWeb = new TronWeb({ fullHost: NILE, privateKey: tronKey.privateKey }); + const ALICE_TRON = tronKey.address; + const usdtC = await tronWeb.contract().at(USDT); + const usdtBal = async (a) => Number(await usdtC.balanceOf(a).call()); + + // BCH: Alice=b (IOU user), C=c (operator/funder/bridge sink), E=e (oracle) + const ALICE = await TestNetWallet.fromWIF(JSON.parse(readFileSync(KEYS.b, 'utf8')).wif); + const C = await TestNetWallet.fromWIF(JSON.parse(readFileSync(KEYS.c, 'utf8')).wif); + const E = await TestNetWallet.fromWIF(JSON.parse(readFileSync(KEYS.e, 'utf8')).wif); + const provider = new ElectrumNetworkProvider('chipnet'); + const sigC = new SignatureTemplate(C.privateKey); + const sigA = new SignatureTemplate(ALICE.privateKey); + const wrap = (cat) => new Contract(art, [txidLE(cat), E.publicKey], { provider }); + const ftBal = async (w, cat) => (await provider.getUtxos(w.cashaddr)).filter((x) => x.token?.category === cat && !x.token?.nft).reduce((s, x) => s + BigInt(x.token.amount), 0n); + async function pick(w, need) { const u = (await provider.getUtxos(w.cashaddr)).filter((x) => !x.token).sort((a, b) => Number(b.satoshis) - Number(a.satoshis)); const { p, t } = pickBch(u, need); if (t < need) throw new Error(`low BCH ${t}<${need}`); return { p, t }; } + async function eligible(w) { + let u = await w.getUtxos(); let e = u.filter((x) => x.vout === 0 && !x.token); if (e.length) return e[0]; + const bad = u.filter((x) => !(x.vout === 0 && !x.token)); + await w.send([{ cashaddr: w.cashaddr, value: 2000n, unit: 'sat' }], bad.length ? { utxoIds: bad } : undefined); await sleep(12000); + u = await w.getUtxos(); e = u.filter((x) => x.vout === 0 && !x.token); if (!e.length) throw new Error('splitter failed'); return e[0]; + } + + console.log(`Alice TRON ${ALICE_TRON} | BCH ${ALICE.cashaddr}`); + console.log(`bridge BCH operator/sink ${C.cashaddr} · oracle ${E.cashaddr}`); + + // ---- deploy the Tron vault + genesis the τUSDt reservoir on BCH ---- + step('Setup — deploy TronUSDTVault on Nile + genesis τUSDt reservoir on chipnet'); + const { abi, bytecode } = compileVault(); + const vaultInst = await tronWeb.contract().new({ abi, bytecode, feeLimit: DEPLOY_FEE, callValue: 0, parameters: [USDT, ALICE_TRON] }); + const VAULT = tronWeb.address.fromHex(vaultInst.address); + const vault = await tronWeb.contract(abi, VAULT); + console.log(` vault ${VAULT} (token ${USDT}, operator ${ALICE_TRON})`); + + const u0 = await eligible(C); const cat = u0.txid; const w = wrap(cat); + { + const need = DUST + MF + 1000n, uSat = BigInt(u0.satoshis); + const all = (await provider.getUtxos(C.cashaddr)).filter((x) => !x.token && !(x.txid === u0.txid && x.vout === u0.vout)).sort((a, b) => Number(b.satoshis) - Number(a.satoshis)); + const { p, t } = pickBch(all, need > uSat ? need - uSat : 0n); const total = uSat + t; + const tb = new TransactionBuilder({ provider }); + tb.addInput(u0, sigC.unlockP2PKH()); for (const x of p) tb.addInput(x, sigC.unlockP2PKH()); + tb.addOutput({ to: w.tokenAddress, amount: DUST, token: { amount: SUPPLY, category: cat, nft: { capability: 'minting', commitment: '' } } }); + tb.addOutput({ to: C.cashaddr, amount: total - DUST - MF }); + console.log(` reservoir ${txidOf(await tb.send())} · category ${cat.slice(0, 16)}…`); await sleep(9000); + } + const reservoir = async () => (await provider.getUtxos(w.tokenAddress)).find((x) => x.token?.nft?.capability === 'minting' && x.token?.category === cat); + + // ============ B3 — real lock on Tron → mint τUSDt on BCH ============ + step(`B3 — Alice locks ${LOCK / 1e6} USDT in the Tron vault → mint ${LOCK} τUSDt on BCH`); + const aliceUsdtStart = await usdtBal(ALICE_TRON); + await tronSend(usdtC, 'approve', [VAULT, LOCK], 'approve USDT'); + const bchRecipient = '0x' + hexOf(ALICE.publicKeyHash) + '0'.repeat(24); // record only + await tronSend(vault, 'lock', [LOCK, bchRecipient], 'lock USDT'); + const vaultLocked1 = Number(await vault.lockedBalance().call()); + console.log(` vault holds ${vaultLocked1 / 1e6} USDT (real, on-chain)`); + + let oSig, msg; // kept for the B5a replay test + { + const res = await reservoir(); if (!res) throw new Error('reservoir missing'); + msg = mintMessage(p2pkh(ALICE.publicKeyHash), LOCK, res.txid, res.vout); + oSig = oracleSign(E.privateKey, msg); + const { p, t } = await pick(C, DUST + MF + 1000n); + const tb = new TransactionBuilder({ provider }); + tb.addInput(res, w.unlock.mint(oSig, msg)); + for (const x of p) tb.addInput(x, sigC.unlockP2PKH()); + tb.addOutput({ to: w.tokenAddress, amount: BigInt(res.satoshis), token: { amount: SUPPLY - BigInt(LOCK), category: cat, nft: { capability: 'minting', commitment: '' } } }); + tb.addOutput({ to: ALICE.tokenaddr, amount: DUST, token: { amount: BigInt(LOCK), category: cat } }); + tb.addOutput({ to: C.cashaddr, amount: t - DUST - MF }); + console.log(` minted ${txidOf(await tb.send())} — Alice holds ${LOCK} τUSDt`); await sleep(9000); + } + const aliceTusdt1 = await ftBal(ALICE, cat); + + // reconciliation point 1 (after mint) + const res1 = await reservoir(); + const mintedOut1 = SUPPLY - BigInt(res1.token.amount); + const bridgeHeld1 = await ftBal(C, cat); + const circulating1 = mintedOut1 - bridgeHeld1; + console.log(` reconcile #1: locked ${vaultLocked1} USDT-atomic == circulating ${circulating1} τUSDt → ${BigInt(vaultLocked1) === circulating1 ? 'OK' : 'MISMATCH'}`); + + // ============ B5a — replay the same attestation → must be REJECTED ============ + step('B5a — replay the SAME oracle attestation (double-mint) → expect REJECTED'); + let replayRejected = false, replayMsg = ''; + try { + const res = await reservoir(); // reservoir moved in B3; old msg is bound to the OLD outpoint + const { p, t } = await pick(C, DUST + MF + 1000n); + const tb = new TransactionBuilder({ provider }); + tb.addInput(res, w.unlock.mint(oSig, msg)); // reuse B3's sig+msg + for (const x of p) tb.addInput(x, sigC.unlockP2PKH()); + tb.addOutput({ to: w.tokenAddress, amount: BigInt(res.satoshis), token: { amount: SUPPLY - BigInt(LOCK) * 2n, category: cat, nft: { capability: 'minting', commitment: '' } } }); + tb.addOutput({ to: ALICE.tokenaddr, amount: DUST, token: { amount: BigInt(LOCK), category: cat } }); + tb.addOutput({ to: C.cashaddr, amount: t - DUST - MF }); + await tb.send(); + replayMsg = 'UNEXPECTEDLY ACCEPTED'; + } catch (e) { replayRejected = true; replayMsg = String(e.message).split('\n')[0].slice(0, 80); } + console.log(` replay: ${replayRejected ? 'REJECTED' : 'NOT rejected'} — ${replayMsg}`); + + // ============ B4 — redeem: return τUSDt on BCH → release USDT on Tron ============ + step(`B4 — Alice returns ${LOCK} τUSDt to the bridge on BCH → operator releases ${LOCK / 1e6} USDT on Tron`); + { + const ft = (await provider.getUtxos(ALICE.cashaddr)).find((x) => x.token?.category === cat && !x.token?.nft && BigInt(x.token.amount) >= BigInt(LOCK)); + const { p, t } = await pick(ALICE, DUST + MF); + const tb = new TransactionBuilder({ provider }); + tb.addInput(ft, sigA.unlockP2PKH()); for (const x of p) tb.addInput(x, sigA.unlockP2PKH()); + tb.addOutput({ to: C.tokenaddr, amount: DUST, token: { amount: BigInt(LOCK), category: cat } }); // return to bridge = burn from circulation + tb.addOutput({ to: ALICE.cashaddr, amount: t + BigInt(ft.satoshis) - DUST - MF }); + console.log(` τUSDt returned to bridge ${txidOf(await tb.send())}`); await sleep(9000); + } + await tronSend(vault, 'release', [1, ALICE_TRON, LOCK], 'release USDT to Alice'); + const vaultLocked2 = Number(await vault.lockedBalance().call()); + const aliceUsdtEnd = await usdtBal(ALICE_TRON); + + // reconciliation point 2 (after redeem) + const res2 = await reservoir(); + const mintedOut2 = SUPPLY - BigInt(res2.token.amount); + const bridgeHeld2 = await ftBal(C, cat); + const circulating2 = mintedOut2 - bridgeHeld2; + console.log(` reconcile #2: locked ${vaultLocked2} USDT-atomic == circulating ${circulating2} τUSDt → ${BigInt(vaultLocked2) === circulating2 ? 'OK' : 'MISMATCH'}`); + + // ---- result ---- + step('RESULT — lock-and-mint τUSDt (B3/B4/B5)'); + const checks = [ + [`B3 lock: vault holds ${LOCK} real USDT-atomic after lock`, vaultLocked1 === LOCK], + [`B3 mint: Alice holds ${LOCK} τUSDt (covenant-enforced 1:1)`, aliceTusdt1 === BigInt(LOCK)], + ['B5b reconcile #1: τUSDt circulating == USDT locked (5:5)', BigInt(vaultLocked1) === circulating1 && circulating1 === BigInt(LOCK)], + ['B5a over-mint: replaying the attestation was rejected', replayRejected], + ['B4 redeem: vault released the USDT (locked back to 0)', vaultLocked2 === 0], + ['B5b reconcile #2: τUSDt circulating == USDT locked (0:0)', BigInt(vaultLocked2) === circulating2 && circulating2 === 0n], + ['B4 round-trip: Alice net USDT ≈ start (only gas/fees spent)', Math.abs(aliceUsdtEnd - aliceUsdtStart) <= 0], + ]; + let ok = true; for (const [l, p] of checks) { console.log(` ${p ? 'PASS' : 'FAIL'} ${l}`); if (!p) ok = false; } + divider(); + console.log(ok + ? `B3/B4/B5 VALIDATED — real lock-and-mint τUSDt. Alice locked real USDT on Tron; the\nbridge oracle attested it; DeviantWrapMintIOU minted exactly that many τUSDt on BCH;\nreplaying the attestation was rejected (outpoint-bound); redeem returned the τUSDt and\nreleased the USDT; and Σ minted == Σ locked at every step (publicly reconcilable).\nv1 trust = the oracle/operator (trust-MINIMIZED). B6 = bonded M-of-N trust floor.` + : 'SOME CHECKS FAILED.'); + console.log(`\nvault @ ${VAULT} · τUSDt category ${cat.slice(0, 16)}… · USDT @ ${USDT}`); + if (!ok) process.exitCode = 1; +} +main().catch((e) => { console.error('FAILED:', e?.stack ?? e?.message ?? e); process.exitCode = 1; });