deviant/scripts/tron-refund-validate.cjs
walerikus a71841fc29 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>
2026-06-23 23:05:48 +02:00

87 lines
4.3 KiB
JavaScript

#!/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; });