feat(crosschain): SafeERC20 refund fix + robustness suite + naming model

Hardening pass on the USDt→τUSDt wrap (builds on 0170's end-to-end validation).

Security fix — found via robustness testing:
- UsdtLockNile used require(token.transfer(...)) assuming a bool return; real
  Tether-style USDt returns nothing → the refund path reverted, which would
  STRAND the locker's funds. Fixed with SafeERC20 handling (no-data or true) on
  both transfer/transferFrom; redeployed to Nile (TVahBUBqUDLqVjDn77Yav…Vnn2o).
- scripts/tron-wrap-robustness.cjs: R2 (too-early) and R3 (consumed) revert for
  the right reason (decoded on-chain revert). R1 (self-refund) validated against
  a clean standard token — contracts/tron/MockUsdt.sol + tron-refund-validate.cjs
  — since the Nile faucet USDT has a non-standard transfer that reverts for
  contract senders.

Naming model (revises the prefix-primary stance):
- identity = CashToken category; human name = USDt + a network badge (the "BIC");
  the ε/τ prefix is a text-only fallback. Applied to the mockup (ASM·Multichain:
  name+badge, prefix in button/stepper, synced selectors, new source-chain
  dropdown Ethereum/Tron→ε/τ) and to ASM-Multichain.md / README.md, plus the
  cross-chain-redemption non-goal (per-chain IOUs + AMM swap, not a universal IOU).

Design record:
- Parameters/ASM DEX/cross-chain-asm-concept-and-state.md — concept, the
  trustless-vs-authority analysis, oracle vs light-client vs zk, the VM wall, the
  reference hacks, the trust ladder, and the 48-hour build state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
walerikus 2026-06-23 23:05:48 +02:00
parent f32c28ed84
commit a71841fc29
9 changed files with 657 additions and 37 deletions

View file

@ -49,9 +49,11 @@ carries its claim back on the source chain and is redeemable. This is the
single-party, oracle-attested sibling of an atomic swap — a **lock-and-mint**
bridge.
Examples: lock 1 ETH on Ethereum → mint a transferable **εETH** CashToken on BCH;
lock 1 USDT on Tron → mint **τUSDT**. The wrapped token's prefix marks its *source
chain* (see [naming](#wrapped-asset-naming--one-marker-per-source-chain) below).
Examples: lock 1 USDt on Ethereum → mint a transferable **USDt** CashToken on BCH
(displayed `USDt · Ethereum`); lock 1 USDt on Tron → mint a *distinct* **USDt**
(`USDt · Tron`). Same human name, **different CashToken categories** — the chain
rides as a badge, with `εUSDt`/`τUSDt` as the bare-string fallback (see
[naming](#wrapped-asset-naming) below).
## Flow
@ -131,30 +133,40 @@ per chain, ADR-0035) stays intact.
| "Redeem anywhere" | native, but heavy & higher-trust | emergent from liquidity, automatable |
| Works for USDT on BCH? | no (you don't issue USDT; no CCTP on BCH) | yes |
## Wrapped-asset naming — one marker per source chain
## Wrapped-asset naming
Every VM-script chain can be a source, and all of them mint onto Bitcoin Cash as
CashTokens. To keep the *same* token wrapped from *different* chains distinct, the
IOU carries a **Greek prefix matching its source chain's initial**:
Distinguishing which chain an IOU belongs to is **three separate layers** — keep
them separate (identity → name+badge → text-only fallback):
| Source chain | Marker | Wrapped examples |
|---|---|---|
| Ethereum | **ε** | εETH, εUSDT |
| Tron | **τ** | τTRX, τUSDT |
1. **Identity = the CashToken category.** Every `(source chain, asset)` IOU is a
*distinct* category (the genesis txid, baked into a per-chain covenant).
Tron-USDt and Ethereum-USDt are **unforgeably different tokens** — they can
never merge or be confused at the protocol level, *whatever you call them*. The
category is the source of truth; every name below is just display over it.
2. **Human name = the asset + a network badge.** The displayed ticker stays the
familiar **`USDt`**; the chain rides as **BCMR metadata + a mandatory network
badge** — `USDt · Tron`, with a chain logo — exactly how exchanges
(`USDT (TRC-20)` vs `(ERC-20)`) and wallets render the same USDT across chains.
**Anywhere there's room for a badge, use the badge.**
3. **Greek prefix = text-only fallback.** For **bare-string surfaces** where no
badge can render — order-book rows, receipts, logs, compact Activity entries —
use a self-contained source-chain Greek prefix:
So `εUSDT` (USDT wrapped from Ethereum) and `τUSDT` (USDT wrapped from Tron) are
**different CashTokens** with different backing locks — never fungible with each
other. The prefix is part of the token's identity: the covenant that mints `εETH`
is a different contract, gated by an attestation of an *Ethereum* lock, from the
one that mints `τTRX`. New source chains extend the table with their own marker.
| Source chain | Prefix | Bare-string form |
|---|---|---|
| Ethereum | **ε** | εUSDt, εUSDC, εETH |
| Tron | **τ** | τUSDt, τTRX |
**Why a prefix, not a suffix.** The marker leads the ticker so it never collides
with the base symbol's own letters — a suffix would give `USDTτ` ("USDT" already
ends in *t*, doubling it) and `USDTε` reads as "USDTe". Leading the marker keeps
the base asset legible and groups the chain badge up front; in UI it renders as a
small `ε`/`τ` pill before the ticker. It also reads as a **namespace** — the chain
*owns* the asset (`ε ∷ USDT` = "Ethereum's USDT"), the ticker a leaf under it,
like a path — which is the natural way to say "this IOU belongs to chain X."
The prefix *leads* the ticker so it never collides with the base symbol's
letters (a suffix gives `USDtτ` — doubled *t* — and `USDtε` reads "USDte"), and
it reads as a namespace (`ε ∷ USDt` = "Ethereum's USDt").
**Rule of thumb:** name `USDt` + badge where there's room; `εUSDt` where there
isn't. You don't *need* the prefix to distinguish chains — the category + a
network tag already does — so the prefix is never the *only* thing telling two
IOUs apart; it's just the compact label when a badge can't be shown. (Mockup: the
ASM·Multichain mint row shows `USDt` + an `Ethereum` badge; the button/stepper use
the `εUSDt` fallback.)
## Trust model — honest boundary

View file

@ -72,9 +72,11 @@ Both move value across chains, but they fall on opposite sides of the SP/P2P
line:
- **ASM · Multichain** = single-party, **no counterparty**. You lock under a VM
condition; an oracle attests; a covenant mints a transferable IOU, marked by its
**source chain** (`εETH`, `τTRX` — see
[ASM-Multichain](ASM-Multichain.md#wrapped-asset-naming--one-marker-per-source-chain)).
condition; an oracle attests; a covenant mints a transferable IOU whose identity
is its **CashToken category** (one per source chain); displayed as the asset name
+ a network badge (`USDt · Tron`), with a `τ`/`ε` prefix as the text-only
fallback (see
[naming](ASM-Multichain.md#wrapped-asset-naming)).
Trust = the lock VM + the oracle (not a person). Requires the source chain to
support **VM scripts**.
- **DEX · Atomic swap** = two-party, **needs a counterparty**, but **trustless**

View file

@ -0,0 +1,267 @@
# Cross-chain ASM — concept, conclusions, and build state
**Date:** 2026-06-23 (covers the 2026-06-21 → 06-23 work + the design discussion)
**Scope:** the cross-chain ASM (lock → oracle → mint IOU on BCH), its place in the
ASM/DEX taxonomy, the trust analysis that grounds it, and what is built/validated.
This is the synthesis record. Per-cell detail lives in `ASM-Multichain.md`,
`README.md`, etc. This file captures the *concept*, the *conclusions*, and the
*state*, so the reasoning survives a context compact.
---
## 1. The concept (the goal)
A **permissionless, non-custodial onramp for stablecoin liquidity into Bitcoin
Cash** — and a market that prices the project's own money against it.
- Open the gates of crypto stablecoins into BCH **without friction, middlemen,
custody, CEXs, or permission.** Users interact directly, cross-chain, on-chain,
non-custodially, without asking anyone.
- **Zero authority, zero custody, zero friction, zero middleman, zero
counterparty** — and *trust-minimized* (a bounded, auditable oracle is the one
residual, on a path to trustless).
- The endpoint = the original goal: **external money arrives from any VM-script
chain → the AMM/DEX lets the market value the FIATm/USDm (MBA) assets.** Value
*discovered by a market*, not *asserted by an issuer*.
**The permission moat.** Every existing path to get stablecoins onto a chain is
gatekept: the **issuer** must decide to mint there (Circle/Tether haven't, won't
soon), a **CEX** must list it (fees, custody), **CCTP/IBC don't reach BCH.** So
BCH waits for a permission that may never come. The **trustless wrap is the one
gate nobody can refuse** — you don't ask, you lock-and-mint. That un-gatekeepability
is the moat.
---
## 2. Architecture — two halves of one machine
- **Build B — the gate.** Cross-chain wrap: lock an asset on a source VM-script
chain (Tron/Ethereum) → mint a transferable IOU on BCH. Counterparty-free,
non-custodial.
- **Build A — the market.** The first-party AMM/DEX (`SharePairAMM`) where the
arriving money *prices* USDm/FIATm.
- **BCH as the hub.** Money arrives from any chain → is market-valued against
USDm → leaves to any chain. Multi-chain routing is done by a **swap + unwrap on
BCH's DEX** (sell τUSDt, buy εUSDC, unwrap on Ethereum) — *not* by nested
bridge-of-a-bridge IOUs. Each token stays redeemable to its own home chain.
---
## 3. Naming model (user-approved 2026-06-22, revises the earlier prefix stance)
Three layers, kept separate:
1. **Identity = the CashToken category.** Tron-USDt and Ethereum-USDt are
unforgeably different tokens — different categories, can trade at different
prices. The category is the source of truth.
2. **Human name = the asset + a network badge.** Displayed ticker stays `USDt`;
the chain rides as BCMR metadata + a **mandatory network badge** (`USDt · Tron`).
3. **Greek prefix (ε/τ) = TEXT-ONLY fallback** for bare-string surfaces
(order-book rows, receipts, logs): Ethereum→ε, Tron→τ. Prefix leads the ticker
(a suffix doubles the *t* in `USDtτ`).
**Banking mapping** (the user's framework): currency = ticker (`USDt`);
**BIC/bank-name = the chain badge** (which ledger); **IBAN/account = the
destination wallet address**; **@username/email/phone = an alias** (ENS, paymail)
resolving to the address. Banks surface the *destination* (currency + BIC), hide
the *mechanism*, and never put the *trust model* on the screen — so should we.
We are **more honest than Circle**: Circle hides the per-chain distinction ("it's
all USDC") behind authority; we **surface** it (badge + category) because no one
guarantees `τUSDt = εUSDt` — they really are different claims.
---
## 4. The conceptual spine (what the design discussion established)
- **Lock-and-mint = debit/credit = correspondent banking, crypto-native.** A
cross-chain send is *debit here, credit there* — exactly a cross-bank wire
(nostro/vostro + SWIFT). The "wrap/IOU" words are back-office plumbing; the user
sees **Send.**
- **IOUs all the way down.** Bank deposits, USDt, USDc are all IOUs. "Native USDC"
is *Circle's* IOU; per-chain USDCs are *separate* IOUs unified only by Circle's
authority + CCTP. There is no pristine non-IOU thing.
- **Lock ≈ burn** (commutative — same result for the holder). The only reasons
they differ: (a) *eligibility* — burn needs issuer mint-authority (Circle can,
we can't burn Tether's USDt → we lock); (b) *backing verifiability*.
- **The only real axis is trustless vs authority** (not lock/burn, not
native/wrapped — those are implementation labels):
| Authority | Trust-minimized | Trustless |
|---|---|---|
| CCTP / USDC / WBTC / CEX | **our oracle lock-and-mint** | HTLC atomic swap / IBC / zk |
| trust an issuer/custodian, unbounded discretion (mint-at-will, freeze) | bounded auditable oracle + verifiable lock | no attestor — verify the source itself |
| = no on-chain check | = ASM·Multichain (today) | = DEX·Atomic (today) / future zk |
- **Verifiability beats promises.** Our advantage isn't "we promise honestly" —
it's *checkability*: `total τUSDt minted (BCH)` vs `total USDt locked (Tron)` is
publicly cross-checkable in real time. Over-minting is **detectable**. Circle's
reserves are off-chain, self-attested monthly — over-issuance leaves no trace.
- **USDm is the only stablecoin verifiable 1:1 to the bottom** (BCH-backed
on-chain, no bank, no authority). Wrapped `τUSDt` is hard-1:1 *at our bridge
layer*, but inherits Tether's unverifiable dollar-backing at the base — we add
**no new authority**, we just don't fix Tether's.
---
## 5. Oracle vs light client vs zk — the verification spectrum
The work always reduces to **two checks**: (1) **inclusion** — is the tx in a
block (Merkle proof); (2) **consensus** — is that block endorsed by the source's
validators/PoW. The *only* variable across architectures is **believe vs verify:**
- **Oracle** = a courier whose handoff the destination **can't verify**, so it
**believes** the oracle's signature. `checkDataSig` proves *a key signed*, not
*that the claim is true*. Attack surface: the **key** (stealable) + the **input**
(poisonable). Has a secret.
- **Light client** = the destination **re-verifies the source's consensus
on-chain**. No key, no secret, **nothing to steal.** Trust reduces to "the
source chain's own consensus" — the irreducible floor. A relayer just carries
re-checkable evidence; a lying relayer is rejected.
- **zk proof** = a prover produces a proof the 2 checks were done correctly;
**soundness makes a false proof impossible.** Trust = math.
> **An oracle is just a relayer the destination can't yet verify.** Give BCH the
> power to check the handoff (a zk verifier in a covenant), and the *same courier*
> drops from "trusted oracle" to "irrelevant relayer." The trust never lived in
> the messenger — it lives in the destination's inability to check.
> **"One `checkDataSig` away from IBC."**
Recipient-swap note: a corrupt **oracle** *can* redirect a lock's mint (recipient
is an attested field). A **light client cannot** — the recipient is bound to the
real, consensus-confirmed source lock. This is a strong argument for the verifier.
Boundary: this collapse holds for **cross-chain state** facts (independently
verifiable on the source). A pure **data oracle** (price/weather) has no
underlying proof → irreducibly trusted, never collapsible.
---
## 6. Why BCH must use an oracle today (the VM wall)
- BCH covenants can't run a Tron/Ethereum **light client**: no loops, tight
op/stack limits → can't verify ~27 Tron SR signatures + rotation per header;
**Ethereum impossible** (BLS aggregate sigs, no BLS opcode).
- **zk verification** needs **pairing opcodes BCH lacks** → a future **CHIP**.
**STARKs** (hash-based, no pairings) are the more plausible path but still
heavy; BCH's bigint + VM-limits upgrades move toward it but aren't there yet.
- **PoW chains** (BTC/LTC) are *more* verifiable (sha256 + difficulty), **but the
lock side is the wall**: Bitcoin has no covenants → can't release on a BCH event
→ BTC↔BCH trustless = **HTLC atomic swap only** (a swap, needs a counterparty),
not a peg/mint. BTC single-party locks = timelock (`CLTV`) + hashlock (`HTLC`);
no foreign-event-conditional release.
- **The wall is always on the chain with the weaker script.** Tron/ETH→BCH: the
wall is BCH (verify). BTC/LTC→BCH: the wall is Bitcoin (lock).
---
## 7. Security model + the reference hacks
Our threat is **not** direct theft of the locked funds (non-custody holds —
locked asset is only releasable to the locker via refund, or the rightful claimant
via attested unwrap; **never** the operator). The threat is **minting unbacked
`τUSDt`** (via a stolen oracle key OR a covenant bug) and draining its value on the
DEX. Locked funds stay safe; IOU *supply integrity* depends on the oracle + the
covenant's correctness.
| Hack | Vector | Assets / chains | Lesson for us |
|---|---|---|---|
| **Wormhole** $326M | **destination code bug** bypassed the guardian sig check → forged VAA → **over-minted 120k wETH** | wETH · Ethereum↔Solana | our covenant/`checkDataSig` correctness (why we robustness-test) |
| **Ronin** $625M | **stole 5-of-9 validator keys** → fraudulent withdrawals → drained reserve | ETH+USDC · Ethereum↔Ronin | oracle key security; M-of-N + bounded mint |
| **Multichain** ~$130M | **fake-MPC, CEO-held keys** (custodial) | many · many | structurally excluded (we have no custodian) |
None were caused by the underlying chains' consensus — **all bridge-layer
failures** (code bug / stolen keys / custody). M-of-N hardens the *key* vector,
not the *code-bug* vector (Wormhole had 19 guardians — irrelevant when the check
itself was broken).
**The trust ladder (our roadmap):**
`1 oracle (fragile) → M-of-N + independent sources per oracle + confirmation depth
(a staked/slashed, fee-earning network — Axelar/THORChain-class, buildable on BCH
today via multiple checkDataSig) → zk light-client (trustless, needs a pairing
CHIP)`. Staked-network ceiling: stake must scale with TVL; objective slashing for
a *false attestation* needs fraud-proofs.
---
## 8. Build state (2026-06-21 → 06-23)
**Build A — DONE, chipnet-validated.** `scripts/chipnet-amm-usdm-pusd.mjs` on
`SharePairAMM` (ADR-0030). USDm/PUSD **price discovery 1.000 → 1.440 PUSD/USDm**,
constant product held, LP withdraw. (v1 = stand-in FTs; v2 = real USDm category +
real Paryon PUSD.) Cauldron is *not* on chipnet and *not needed* — we have our own
AMM; `@cashlab/cauldron` is for mainnet compat later (ADR-0016).
**Build B — cross-chain wrap, end-to-end validated across Tron Nile + chipnet.**
- `contracts/DeviantWrapMintIOU.cash` (cashc 0.12.1) — mint-on-attestation IOU
covenant: `checkDataSig` oracle gate, **no BCH backing** (dust only — value is
the source lock), **replay-bound to the reservoir outpoint**. Validated:
no-backing, reservoir decrement, **replay rejected by the covenant** (line 48
outpoint binding — not the builder).
- `scripts/chipnet-wrapmint-iou.mjs` — BCH leg (genesis reservoir into the
covenant in one tx; oracle-attested mint).
- `contracts/tron/UsdtLockNile.sol` (+ `scripts/tron-deploy-lock.cjs`, evmVersion
istanbul) — Nile lock contract (lock/markConsumed/refund), **SafeERC20-fixed**,
deployed @ **`TVahBUBqUDLqVjDn77YavvqMvT2rtVnn2o`**.
- `scripts/tron-bch-wrap.mjs` — the **oracle watcher**: **real end-to-end wrap
validated** — locked **0.5 real USDt** on Nile → minted **500000 τUSDt** on
chipnet → `markConsumed`. Genuine `USDt(Tron) → τUSDt(BCH)`.
**Robustness — found + fixed a real bug, then validated the safety paths.**
- **Bug:** `require(token.transfer(...))` assumed a standard `bool` return; real
Tether-style USDt returns nothing → **the refund path reverted → would strand
funds.** Fixed with **SafeERC20** (accept no-data or `true`), redeployed.
- `scripts/tron-wrap-robustness.cjs` — **R2 (too-early) ✓, R3 (consumed) ✓** (real
decoded reverts). **R1 (self-refund)** blocked by a **Nile-USDT token quirk** —
its non-standard `transfer` reverts for *contract* senders (EOA transfer works);
isolated with a clean token: `contracts/tron/MockUsdt.sol` +
`scripts/tron-refund-validate.cjs` → **R1 PASS**.
- Meta-lesson: the first test "passed" for the wrong reason (a TronWeb client error
counted as a contract revert). A guard only counts if it fails for the *right*
reason (`receipt.result === 'REVERT'`).
**Deps:** `tronweb ^6.3.0`, `solc ^0.8.35`. Keys/state gitignored
(`.tron-*.json`, `.chipnet-*.json`, `ChipnetTests/`).
**Commits:** `f32c28e` = Build A + Build B + Tron lock + docs + snapshot 0170.
**Uncommitted batch:** the naming-model docs + mockup source-chain dropdown +
SafeERC20 contract fix + MockUsdt + robustness/refund-validate scripts (a snapshot
0171 + commit is pending).
**Mockup** (`design/mockups/bch-dex.html`, design-only, stays a demo): all four
cells; ASM·Multichain has the naming model (name + chain badge, prefix in
button/stepper), a **source-chain dropdown** (Ethereum/Tron → ε/τ), and
wrap/unwrap. Real swaps live in `shells/app/src/asm-core`.
---
## 9. Deferred / next
- **Unwrap (reverse leg):** burn `τUSDt` → oracle attests burn → `UsdtLockNile`
releases USDt on Tron. (Lock contract has the hooks; release path TBD. Keep the
non-custody invariant: release only to the rightful claimant, never the operator.)
- **Sepolia / εUSDC** (the second source chain).
- **M-of-N staked oracle network** (decentralize Build B; the real hardening).
- **zk / light-client verifier** via a future BCH CHIP → fully trustless; also
*binds recipient+amount to the verified source*, eliminating recipient-swap.
- **Real USDm + real Paryon PUSD** in the AMM (v2).
- **Send-abstraction UX:** one **Send** button that auto-routes cross-chain and
hides lock/mint/oracle entirely (bank-style: surface currency + ledger + fee +
time; never the mechanism or the trust model).
## 10. Non-goals / honest caveats
- **Universal redemption** (one IOU redeemable on any chain) = **NON-GOAL**: needs
pooled multi-chain reserves + rebalancing + higher trust, collapses the per-chain
markers, and **CCTP doesn't cover BCH**. The answer is **per-chain IOUs + the BCH
AMM swap** (`τUSDt → εUSDC` then unwrap) — emergent "redeem anywhere" given
liquidity, automatable.
- **Today = trust-minimized, not trustless.** The oracle is the residual trust —
bounded (mints exactly the attested amount/recipient) and auditable
(locked-vs-minted is public). The zk path removes it.
- We **add no new authority**: `τUSDt` is only as good as Tether's USDt + our
(non-custodial, verifiable) bridge. We don't become a second Circle.
---
See also: `[[wrapped-asset-source-chain-marker]]`,
`[[exchange-is-a-function-not-p2p-venue]]`, `[[chipnet-dex-and-cross-chain-wrap-plan]]`
(auto-memory), ADR-0016/0030/0035, snapshot 0170.

View file

@ -0,0 +1,48 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.6;
// MockUsdt — a minimal, well-behaved standard TRC-20 for testing the lock leg's
// refund path. Unlike the Nile faucet USDT (whose non-standard `transfer` reverts
// for contract senders), this token's transfer works from anyone — so it isolates
// the lock contract's own logic. Mints the full supply to the deployer.
contract MockUsdt {
string public name = "Mock USDT";
string public symbol = "mUSDT";
uint8 public decimals = 6;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
constructor(uint256 supply) {
balanceOf[msg.sender] = supply;
totalSupply = supply;
emit Transfer(address(0), msg.sender, supply);
}
function transfer(address to, uint256 v) external returns (bool) {
require(balanceOf[msg.sender] >= v, "balance");
balanceOf[msg.sender] -= v;
balanceOf[to] += v;
emit Transfer(msg.sender, to, v);
return true;
}
function approve(address s, uint256 v) external returns (bool) {
allowance[msg.sender][s] = v;
emit Approval(msg.sender, s, v);
return true;
}
function transferFrom(address f, address to, uint256 v) external returns (bool) {
require(balanceOf[f] >= v, "balance");
require(allowance[f][msg.sender] >= v, "allowance");
allowance[f][msg.sender] -= v;
balanceOf[f] -= v;
balanceOf[to] += v;
emit Transfer(f, to, v);
return true;
}
}

View file

@ -47,7 +47,7 @@ contract UsdtLockNile {
require(amount > 0, "amount");
require(bchRecipient.length == 25, "bchRecipient"); // P2PKH locking bytecode
require(deadline > block.timestamp, "deadline");
require(token.transferFrom(msg.sender, address(this), amount), "transferFrom");
_safeTransferFrom(msg.sender, address(this), amount);
id = nextId++;
locks[id] = Lock(msg.sender, amount, bchRecipient, deadline, false, false);
emit Locked(id, msg.sender, amount, bchRecipient, deadline);
@ -70,7 +70,20 @@ contract UsdtLockNile {
require(!L.settled && !L.consumed, "not refundable");
require(block.timestamp >= L.deadline, "too early");
L.settled = true;
require(token.transfer(L.locker, L.amount), "transfer");
_safeTransfer(L.locker, L.amount);
emit Refunded(id);
}
// SafeERC20: USDt (Tether-style) returns NO bool from transfer/transferFrom, so a
// strict `require(token.transfer(...), ...)` reverts and would strand funds. Accept
// success when the call doesn't revert AND (no return data OR it decodes to true).
function _safeTransfer(address to, uint256 amount) internal {
(bool ok, bytes memory data) = address(token).call(abi.encodeWithSelector(token.transfer.selector, to, amount));
require(ok && (data.length == 0 || abi.decode(data, (bool))), "transfer failed");
}
function _safeTransferFrom(address from, address to, uint256 amount) internal {
(bool ok, bytes memory data) = address(token).call(abi.encodeWithSelector(token.transferFrom.selector, from, to, amount));
require(ok && (data.length == 0 || abi.decode(data, (bool))), "transferFrom failed");
}
}

View file

@ -123,11 +123,13 @@ button:disabled{cursor:default}
<div style="font-weight:500"><i class="ti ti-cpu" aria-hidden="true"></i> Zero Counterparty Automated Swap Mechanism:</div>
<div style="opacity:.85;margin-top:2px">Contract-fixed rate, zero order-book, size-independent.</div>
</div>
<div style="font-size:13px;color:var(--color-text-secondary);margin-bottom:4px">Source chain</div>
<div class="dx-row" style="margin-bottom:10px"><select id="mc-chain-sel" style="width:100%" aria-label="Source chain"><option value="ethereum">Ethereum · ε</option><option value="tron">Tron · τ</option></select></div>
<div style="font-size:13px;color:var(--color-text-secondary);margin-bottom:2px" id="mc-toplabel">You lock (source chain)</div>
<div class="dx-row"><input id="mc-in" class="dx-amt" inputmode="decimal" value="1" aria-label="Amount"/><span style="text-align:right"><span id="mc-topbadge"></span><select id="mc-asset" style="width:auto;min-width:84px" aria-label="Asset"><option value="ETH">ETH</option><option value="USDT">USDT</option><option value="USDC">USDC</option></select><br/><span class="chain" id="mc-srcchain">Ethereum · native</span></span></div>
<div style="text-align:center;margin:-4px 0"><button id="mc-flip" style="border-radius:50%;width:34px;height:34px;padding:0;display:inline-flex;align-items:center;justify-content:center" aria-label="Flip direction — wrap / unwrap"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 4v16M3 8l4-4 4 4M17 20V4M21 16l-4 4-4-4"/></svg></button></div>
<div style="font-size:13px;color:var(--color-text-secondary);margin-bottom:2px" id="mc-botlabel">You mint (destination chain)</div>
<div class="dx-row"><span id="mc-out" class="dx-amt" style="opacity:.9">0</span><span style="text-align:right"><span style="font-weight:500" id="mc-outsym" title="ε = wrapped from Ethereum (the source chain), shown as a leading marker. Each origin chain has its own — e.g. Tron → τ (τUSDT)">εETH</span><br/><span class="chain" id="mc-dstchain">BCH · CashToken</span></span></div>
<div class="dx-row"><span id="mc-out" class="dx-amt" style="opacity:.9">0</span><span style="text-align:right"><select id="mc-outsym" style="width:auto;min-width:84px;font-weight:500" aria-label="Asset (synced)"><option value="ETH">ETH</option><option value="USDT">USDT</option><option value="USDC">USDC</option></select><span id="mc-outbadge"></span><br/><span class="chain" id="mc-dstchain">BCH · CashToken</span></span></div>
<div style="margin:10px 0 8px">
<div class="dx-det"><span>IOU</span><span class="dx-mono" id="mc-iou">transferable · claim on source</span></div>
<div class="dx-det"><span id="mc-feelabel">Mint fee (0.3%)</span><span id="mc-fee" class="dx-mono">—</span></div>
@ -395,9 +397,20 @@ button:disabled{cursor:default}
var BFEE=0.003,NEED=19,mcT=[],mcRunning=false;
var mcStep=stepper({pre:'',icons:{lock:'ti-lock',attest:'ti-signature',verify:'ti-coin',settle:'ti-arrows-exchange'}});
var MCA={ETH:{std:'Ethereum · native',d:4,def:1,mk:'ε'},USDT:{std:'Ethereum · ERC-20',d:2,def:1000,mk:'ε'},USDC:{std:'Ethereum · ERC-20',d:2,def:1000,mk:'ε'}};
function mcA(){var s=$('#mc-asset').value,m=MCA[s]||MCA.ETH;return {sym:s,mk:m.mk,ep:m.mk+s,std:m.std,d:m.d};}
function mcBadge(mk){return '<span style="display:inline-block;background:var(--color-background-info);color:var(--color-text-info);border-radius:4px;padding:0 5px;margin-right:2px;font-size:.82em;vertical-align:1px">'+mk+'</span>';}
var MCHAINS={
ethereum:{name:'Ethereum',mk:'ε',assets:{ETH:{std:'native',d:4,def:1},USDT:{std:'ERC-20',d:2,def:1000},USDC:{std:'ERC-20',d:2,def:1000}}},
tron:{name:'Tron',mk:'τ',assets:{TRX:{std:'native',d:4,def:100},USDT:{std:'TRC-20',d:2,def:1000},USDC:{std:'TRC-20',d:2,def:1000}}}
};
var mcChain='ethereum';
function mcCh(){return MCHAINS[mcChain];}
function mcA(){var ch=mcCh(),s=$('#mc-asset').value,m=ch.assets[s]||ch.assets[Object.keys(ch.assets)[0]];return {sym:s,mk:ch.mk,ep:ch.mk+s,std:ch.name+' · '+m.std,d:m.d,def:m.def,chain:ch.name};}
function mcDefAmt(){var m=mcCh().assets[$('#mc-asset').value];return m?m.def:1;}
function mcPopulate(keep){var ch=mcCh(),ks=Object.keys(ch.assets);if(ks.indexOf(keep)<0)keep=ks.indexOf('USDT')>=0?'USDT':ks[0];var opts=ks.map(function(k){return'<option value="'+k+'"'+(k===keep?' selected':'')+'>'+k+'</option>';}).join('');$('#mc-asset').innerHTML=opts;$('#mc-outsym').innerHTML=opts;}
function mcSetChain(c){mcChain=c;var s=$('#mc-chain-sel');if(s)s.value=c;mcPopulate($('#mc-asset').value);$('#mc-in').value=mcDefAmt();mcLabels();mcReset();mcQuote();}
// Network badge — the human name stays the asset (USDt); the chain rides as a
// badge, and the on-chain identity is the CashToken category. The Greek prefix
// (x.ep) is kept only as the text-only fallback (button/stepper/logs).
function mcChainBadge(chain,ep){return '<span title="On-chain identity = its CashToken category. Wrapped from '+chain+'. Text-only short form: '+ep+'." style="display:inline-block;background:var(--color-background-tertiary);color:var(--color-text-secondary);border:0.5px solid var(--color-border-tertiary);border-radius:4px;padding:0 6px;margin:0 3px;font-size:.72em;vertical-align:1px">'+chain+'</span>';}
var mcDir='wrap';
var MCDIR={
wrap:{top:'You lock (source chain)',bot:'You mint (destination chain)',flow:'Lock → mint flow',fee:'Mint fee (0.3%)',iou:'transferable · claim on source',trust:'lock VM + oracle',conf:'source confirmations',
@ -424,8 +437,9 @@ button:disabled{cursor:default}
function mcQuote(){var a=parseFloat($('#mc-in').value),x=mcA(),D=MCDIR[mcDir];
$('#mc-out').textContent=a>0?(a*(1-BFEE)).toLocaleString(undefined,{maximumFractionDigits:x.d}):'0';
$('#mc-fee').textContent=(a>0?(a*BFEE).toFixed(x.d):'0')+' '+x.sym;
if(mcDir==='wrap'){$('#mc-topbadge').innerHTML='';$('#mc-srcchain').textContent=x.std;$('#mc-outsym').innerHTML=mcBadge(x.mk)+x.sym;$('#mc-dstchain').textContent='BCH · CashToken';}
else{$('#mc-topbadge').innerHTML=mcBadge(x.mk);$('#mc-srcchain').textContent='BCH · CashToken';$('#mc-outsym').innerHTML=x.sym;$('#mc-dstchain').textContent=x.std;}
$('#mc-outsym').value=x.sym;
if(mcDir==='wrap'){$('#mc-topbadge').innerHTML='';$('#mc-srcchain').textContent=x.std;$('#mc-outbadge').innerHTML=mcChainBadge(x.chain,x.ep);$('#mc-dstchain').textContent='BCH · CashToken';}
else{$('#mc-topbadge').innerHTML=mcChainBadge(x.chain,x.ep);$('#mc-srcchain').textContent='BCH · CashToken';$('#mc-outbadge').innerHTML='';$('#mc-dstchain').textContent=x.std;}
if(!mcRunning)$('#mc-go').textContent=D.go(x);}
function mcLabels(){var D=MCDIR[mcDir];$('#mc-toplabel').textContent=D.top;$('#mc-botlabel').textContent=D.bot;$('#mc-flowtitle').textContent=D.flow;$('#mc-feelabel').textContent=D.fee;$('#mc-iou').textContent=D.iou;$('#mc-trust').textContent=D.trust;$('#mc-note').innerHTML='<i class="ti ti-alert-triangle" aria-hidden="true"></i> '+D.note;}
function mcReset(){mcRunning=false;mcT.forEach(clearTimeout);mcT=[];var x=mcA(),D=MCDIR[mcDir];$('#mc-go').disabled=false;$('#mc-go').textContent=D.go(x);
@ -443,11 +457,13 @@ button:disabled{cursor:default}
mcT.push(setTimeout(function(){mcStep('verify','done',D.mintD(x,got));mcT.push(setTimeout(settle,400));},900));}
function settle(){mcStep('settle','done',D.settleD(x,got,recip.slice(0,18)));mcRunning=false;$('#mc-go').disabled=false;$('#mc-go').textContent='Run again';}
}
function mcSetDir(d){mcDir=d;$('#mc-in').value=MCA[$('#mc-asset').value].def;mcLabels();mcReset();mcQuote();}
function mcSetDir(d){mcDir=d;$('#mc-in').value=mcDefAmt();mcLabels();mcReset();mcQuote();}
$('#mc-in').addEventListener('input',mcQuote);$('#mc-go').addEventListener('click',mcRun);$('#mc-reset').addEventListener('click',mcReset);
$('#mc-asset').addEventListener('change',function(){$('#mc-in').value=MCA[$('#mc-asset').value].def;mcReset();mcQuote();});
$('#mc-asset').addEventListener('change',function(){$('#mc-in').value=mcDefAmt();mcReset();mcQuote();});
$('#mc-outsym').addEventListener('change',function(){$('#mc-asset').value=$('#mc-outsym').value;$('#mc-in').value=mcDefAmt();mcReset();mcQuote();});
$('#mc-flip').addEventListener('click',function(){mcSetDir(mcDir==='wrap'?'unwrap':'wrap');});
mcLabels();mcQuote();mcReset();
$('#mc-chain-sel').addEventListener('change',function(){mcSetChain($('#mc-chain-sel').value);});
mcSetChain('ethereum');
var atT=[],atRunning=false;
var atStep=stepper({pre:'at-',icons:{agree:'ti-users',secret:'ti-key',locka:'ti-lock',lockb:'ti-lock',claimb:'ti-coin',claima:'ti-circle-check'}});

View file

@ -0,0 +1,87 @@
#!/usr/bin/env node
/**
* tron-refund-validate.cjs — validate R1 (self-refund after timeout) against a
* well-behaved standard TRC-20 (MockUsdt), isolating the lock contract's refund
* logic from the Nile faucet USDT's non-standard `transfer`.
*
* deploy MockUsdt → deploy UsdtLockNile(mock, me) → approve + lock 0.2 →
* wait past the deadline → refund → assert the locker's balance is restored.
*
* Run: node scripts/tron-refund-validate.cjs --yes
*/
const fs = require('fs');
const path = require('path');
const solc = require('solc');
const mod = require('tronweb');
const TronWeb = mod.TronWeb ?? mod.default ?? mod;
const ROOT = path.join(__dirname, '..');
const KEYS = JSON.parse(fs.readFileSync(path.join(ROOT, '.tron-nile-keys.json'), 'utf8'));
const FULL_HOST = 'https://nile.trongrid.io';
const BCH_RECIPIENT = '0x76a9149aa6baabb2bbb1d1940e4ec82b9640a7ab62b8dc88ac';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const big = (v) => BigInt((v?.toString?.() ?? v));
function compile(file, name) {
const input = {
language: 'Solidity',
sources: { [file]: { content: fs.readFileSync(path.join(ROOT, 'contracts/tron', file), 'utf8') } },
settings: { optimizer: { enabled: true, runs: 200 }, evmVersion: 'istanbul', outputSelection: { '*': { '*': ['abi', 'evm.bytecode.object'] } } },
};
const out = JSON.parse(solc.compile(JSON.stringify(input)));
const errs = (out.errors || []).filter((e) => e.severity === 'error');
if (errs.length) { errs.forEach((e) => console.error(e.formattedMessage)); throw new Error('solc'); }
const c = out.contracts[file][name];
return { abi: c.abi, bytecode: c.evm.bytecode.object };
}
async function main() {
if (!process.argv.includes('--yes')) { console.log('Re-run with --yes.'); return; }
const tw = new TronWeb({ fullHost: FULL_HOST, privateKey: KEYS.privateKey });
tw.setAddress(KEYS.address);
const me = KEYS.address;
const chainNow = async () => Number(big((await tw.trx.getCurrentBlock()).block_header.raw_data.timestamp) / 1000n);
const mockArt = compile('MockUsdt.sol', 'MockUsdt');
const lockArt = compile('UsdtLockNile.sol', 'UsdtLockNile');
console.log('deploy MockUsdt (1000 mUSDT)…');
const mock = await tw.contract().new({ abi: mockArt.abi, bytecode: mockArt.bytecode, feeLimit: 1500_000_000, parameters: ['1000000000'] });
const mockAddr = tw.address.fromHex(mock.address);
console.log(' MockUsdt @ ' + mockAddr);
console.log('deploy UsdtLockNile(mock, me)…');
const lockI = await tw.contract().new({ abi: lockArt.abi, bytecode: lockArt.bytecode, feeLimit: 1500_000_000, parameters: [mockAddr, me] });
const lockAddr = tw.address.fromHex(lockI.address);
console.log(' lock @ ' + lockAddr);
const usdt = tw.contract(mockArt.abi, mockAddr);
const lock = tw.contract(lockArt.abi, lockAddr);
const bal = async () => big(await usdt.balanceOf(me).call());
console.log('approve + lock 0.2 mUSDT…');
await usdt.approve(lockAddr, '10000000').send({ feeLimit: 200_000_000, shouldPollResponse: false }); await sleep(7000);
const id = big(await lock.nextId().call());
const before = await bal();
const dl = (await chainNow()) + 60;
await lock.lock('200000', BCH_RECIPIENT, dl).send({ feeLimit: 300_000_000, shouldPollResponse: false }); await sleep(8000);
const afterLock = await bal();
console.log(` locked id ${id}: ${before} → ${afterLock}; waiting past deadline…`);
while ((await chainNow()) < dl + 4) await sleep(8000);
console.log('refund…');
const txid = await lock.refund(id.toString()).send({ feeLimit: 500_000_000, shouldPollResponse: false }); await sleep(12000);
const tx = await tw.trx.getTransaction(txid);
const ret = tx?.ret?.[0]?.contractRet;
const afterRefund = await bal();
const ok = ret === 'SUCCESS' && afterRefund === before;
console.log(` refund ${ret}: ${afterLock} → ${afterRefund} (restored=${ok})`);
console.log('\n=== RESULT ===');
console.log(` ${ok ? 'PASS' : 'FAIL'} R1 self-refund after timeout returns the locked token to the locker`);
console.log(ok
? 'VALIDATED — the refund safety valve works with a standard TRC-20. Non-custody holds; the Nile USDT failure was a token quirk (non-standard transfer), now handled by SafeERC20.'
: 'FAIL.');
if (!ok) process.exitCode = 1;
}
main().catch((e) => { console.error('FAILED:', e?.message ?? e); process.exitCode = 1; });

View file

@ -0,0 +1,94 @@
#!/usr/bin/env node
/**
* tron-wrap-robustness.cjs — robustness tests for the cross-chain wrap's Tron
* lock leg (UsdtLockNile). Proves the non-custodial guarantees:
*
* R1 refund AFTER the deadline (unconsumed) → USDt returns to the locker
* R2 refund BEFORE the deadline → reverts "too early"
* R3 refund after markConsumed → reverts "not refundable"
*
* Verification is robust against TVM quirks: R1 is checked by the locker's USDt
* BALANCE actually being restored (not a receipt poll), and R2/R3 by the exact
* revert string from a raw constant call (TVM returns the revert data even when
* result.result===true, so we decode Error(string) from constant_result). Timing
* is driven off the chain clock. Run: node scripts/tron-wrap-robustness.cjs --yes
*/
const fs = require('fs');
const path = require('path');
const mod = require('tronweb');
const TronWeb = mod.TronWeb ?? mod.default ?? mod;
const ROOT = path.join(__dirname, '..');
const KEYS = JSON.parse(fs.readFileSync(path.join(ROOT, '.tron-nile-keys.json'), 'utf8'));
const LOCK = JSON.parse(fs.readFileSync(path.join(ROOT, '.tron-nile-lock.json'), 'utf8'));
const USDT = 'TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf';
const FULL_HOST = 'https://nile.trongrid.io';
const BCH_RECIPIENT = '0x76a9149aa6baabb2bbb1d1940e4ec82b9640a7ab62b8dc88ac';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const big = (v) => BigInt((v?.toString?.() ?? v));
function decodeRevert(hex) {
if (!hex) return null;
if (hex.startsWith('08c379a0')) { const len = parseInt(hex.slice(72, 136), 16); return Buffer.from(hex.slice(136, 136 + len * 2), 'hex').toString('utf8'); }
return '0x' + hex.slice(0, 40);
}
async function main() {
if (!process.argv.includes('--yes')) { console.log('Re-run with --yes.'); return; }
const tw = new TronWeb({ fullHost: FULL_HOST, privateKey: KEYS.privateKey });
tw.setAddress(KEYS.address);
const me = KEYS.address, meHex = tw.address.toHex(me), lockHex = tw.address.toHex(LOCK.address);
const usdt = await tw.contract().at(USDT);
const lock = tw.contract(LOCK.abi, LOCK.address);
const bal = async () => big(await usdt.balanceOf(me).call());
const chainNow = async () => Number(big((await tw.trx.getCurrentBlock()).block_header.raw_data.timestamp) / 1000n);
const send = (build, fl) => build().send({ feeLimit: fl || 200_000_000, shouldPollResponse: false });
// raw constant call → exact revert reason (TVM returns revert data in constant_result)
async function refundRevert(id) {
const r = await fetch(FULL_HOST + '/wallet/triggerconstantcontract', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ owner_address: meHex, contract_address: lockHex, function_selector: 'refund(uint256)', parameter: BigInt(id).toString(16).padStart(64, '0'), visible: false }),
}).then((x) => x.json());
const cr = r.constant_result && r.constant_result[0];
const reverted = !!(cr && cr.startsWith('08c379a0'));
return { reverted, reason: reverted ? decodeRevert(cr) : '(would succeed)' };
}
const results = [];
console.log(`lock ${LOCK.address} · USDt ${await bal()} · chain ${await chainNow()}`);
console.log('approve…'); await send(() => usdt.approve(LOCK.address, '10000000'), 100_000_000); await sleep(8000);
// R1 — self-refund after deadline (verified by balance restoration)
{
const id = big(await lock.nextId().call()), before = await bal(), dl = (await chainNow()) + 60;
await send(() => lock.lock('200000', BCH_RECIPIENT, dl), 300_000_000); await sleep(8000);
const afterLock = await bal();
console.log(`[R1] lock id ${id}: ${before} → ${afterLock}; waiting for deadline…`);
while ((await chainNow()) < dl + 4) await sleep(8000);
await send(() => lock.refund(id.toString()), 300_000_000); await sleep(10000);
const afterRefund = await bal(), ok = afterRefund === before;
console.log(`[R1] refund → ${afterLock} → ${afterRefund} (restored=${ok})`);
results.push(['R1 self-refund after timeout returns USDt to the locker', ok]);
}
// R2 + R3 — refund guards (revert reasons from constant call)
{
const id = big(await lock.nextId().call()), dl = (await chainNow()) + 300;
await send(() => lock.lock('100000', BCH_RECIPIENT, dl), 300_000_000); await sleep(8000);
const r2 = await refundRevert(id), r2ok = r2.reverted && /too early/i.test(r2.reason);
console.log(`[R2] refund before deadline → ${r2.reverted ? 'REVERT' : 'ok'} "${r2.reason}" ${r2ok ? '✓' : '✗'}`);
results.push(['R2 refund before the deadline reverts (too early)', r2ok]);
await send(() => lock.markConsumed(id.toString()), 100_000_000); await sleep(8000);
const r3 = await refundRevert(id), r3ok = r3.reverted && /not refundable/i.test(r3.reason);
console.log(`[R3] refund after consumed → ${r3.reverted ? 'REVERT' : 'ok'} "${r3.reason}" ${r3ok ? '✓' : '✗'}`);
results.push(['R3 consumed lock blocks refund (not refundable)', r3ok]);
}
console.log('\n=== RESULT ===');
let allok = true;
for (const [label, pass] of results) { console.log(` ${pass ? 'PASS' : 'FAIL'} ${label}`); if (!pass) allok = false; }
console.log(allok ? 'ALL ROBUSTNESS CHECKS PASSED — self-refund works, double-claim blocked. Non-custody holds.' : 'SOME CHECKS FAILED.');
if (!allok) process.exitCode = 1;
}
main().catch((e) => { console.error('FAILED:', e?.message ?? e); process.exitCode = 1; });

View file

@ -0,0 +1,81 @@
# Snapshot 0171 — cross-chain wrap hardening: SafeERC20 fix, robustness suite, naming model, source-chain selector
**Date:** 2026-06-23
**Track:** chipnet covenants + cross-chain (Build B hardening) · design records
**Status:** ✅ SafeERC20 fix found-and-validated on Nile · ✅ refund safety (R1) + anti-double-claim (R2/R3) validated · ✅ naming model applied to mockup + docs · ✅ cross-chain design record written
Builds on snapshot 0170 (USDm/PUSD AMM + the first end-to-end `USDt → τUSDt` wrap).
This snapshot is the **hardening + design-clarity** pass that came out of stress-testing
the wrap and a long trust-model discussion.
---
## A — Robustness testing found a real, fund-stranding bug (SafeERC20)
Stress-testing the wrap's *safety* paths (not the happy path) surfaced a genuine bug:
- `UsdtLockNile` used `require(token.transfer(...), "transfer")`, assuming a standard
`bool` return. **Real Tether-style USDt returns nothing** → the **refund path
reverted → the locker's funds would be stranded.** That's the exact non-custody
guarantee, broken by a non-standard token.
- **Fix:** SafeERC20-style handling (low-level call; accept success when there's no
return data OR it decodes to `true`) on both `transfer` and `transferFrom`.
`contracts/tron/UsdtLockNile.sol` updated and **redeployed to Nile @
`TVahBUBqUDLqVjDn77YavvqMvT2rtVnn2o`**.
**Validation (`scripts/tron-wrap-robustness.cjs`):**
- **R2** refund *before* deadline → reverts `"too early"` ✓ (real decoded revert)
- **R3** refund *after* `markConsumed` → reverts `"not refundable"` ✓ (no double-claim)
- **R1** self-refund *after* timeout was blocked by a **Nile-USDT token quirk** — its
non-standard `transfer` reverts for *contract* senders (EOA transfer succeeds).
Isolated with a clean token: **`contracts/tron/MockUsdt.sol`** +
**`scripts/tron-refund-validate.cjs`** → **R1 PASS** (balance restored). So the refund
*logic* is correct; the Nile faucet token is the anomaly.
**Meta-lesson:** the first robustness run "passed" R2/R3 for the *wrong reason* — a
TronWeb client error (`must use send()`) was caught and counted as a contract revert.
Reverts are now confirmed via the on-chain receipt (`result === 'REVERT'`), and timing
is driven off the chain clock. A guard only counts if it fails for the *right reason*.
## B — Naming model (revises the earlier prefix-primary stance)
Three layers, kept separate (applied to the mockup + docs):
1. **Identity = the CashToken category** (τUSDt ≠ εUSDt — different tokens).
2. **Human name = the asset + a network badge** (`USDt · Tron`) — the badge is the
transaction's "BIC" (which ledger), bank-style.
3. **Greek prefix (ε/τ) = TEXT-ONLY fallback** for bare-string surfaces (order book,
receipts, logs).
Applied:
- `design/mockups/bch-dex.html` (ASM·Multichain): mint side shows `USDt` + an
`Ethereum`/`Tron` badge; button/stepper keep the `εUSDt` fallback; both asset
selectors sync; **added a source-chain dropdown** (Ethereum/Tron → ε/τ) driving the
asset list, badge, marker, and standard label.
- `Parameters/ASM DEX/ASM-Multichain.md` + `README.md`: the three-layer "Wrapped-asset
naming" section + the cross-chain-redemption non-goal (universal IOU rejected;
per-chain IOUs + BCH AMM swap is the answer).
## C — Cross-chain ASM design record
New `Parameters/ASM DEX/cross-chain-asm-concept-and-state.md` — the synthesis of the
session's concept + conclusions + build state, so the reasoning survives a compact.
Covers: the permissionless-onramp concept; trustless-vs-authority as the only real
axis; oracle vs light-client vs zk (the 2 checks, believe-vs-verify, "an oracle is a
relayer you can't yet verify"); the VM wall (why BCH needs an oracle today; STARK path;
the BTC lock-side wall); the reference hacks (Wormhole code-bug over-mint, Ronin stolen
keys, Multichain custodial); the trust ladder to a staked M-of-N network → zk; and the
full 48-hour build state.
## D — Deferred / next
- **Unwrap (reverse leg):** burn `τUSDt` → attest burn → release USDt on Tron (keep the
non-custody invariant: release only to the rightful claimant, never the operator).
- **Sepolia / εUSDC** (second source chain).
- **M-of-N staked oracle network** (decentralize Build B; the real hardening — independent
sources per oracle + confirmation depth + stake/slash).
- **zk / light-client verifier** via a future BCH CHIP → fully trustless; also binds
recipient+amount to the verified source (eliminates recipient-swap).
- **Real USDm + real Paryon PUSD** in the AMM (v2).
- **Send-abstraction UX** — one Send button, auto-route cross-chain, hide the mechanism.
Deps unchanged since 0170 (`tronweb ^6.3.0`, `solc ^0.8.35`). Keys/state gitignored.