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 <noreply@anthropic.com>
This commit is contained in:
walerikus 2026-06-28 12:25:34 +02:00
parent 34c672b927
commit 613e9218ff
3 changed files with 302 additions and 3 deletions

View file

@ -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,

View file

@ -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));
}
}

View file

@ -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; });