Snapshot of accumulated work-in-progress on top of the baseline commit. This session — retire Tier B from the wallet shell (single-tier model, glossary "Tiers & trust"): - SendFlow.tsx: BCH-only; removed the non-functional Tier-B send path, the D-4/D-5 raw-BCH co-display, and the ADR-0013 friction ladder. - ArtemisionScreen.tsx: Create wizard collapsed to a single 5-step on-chain genesis flow; tier-selection step + Tier-B branch (prepareTierBImport) deleted. - QuickActions.tsx / SettingsScreen.tsx: receive note reworded; "Send friction thresholds" + "Tier explainer" rows removed; registry-trust note reworded. - Verified: tsc clean, vitest 74/74, full preview walkthrough, no console errors. Pre-existing uncommitted work also folded in: - USDm -> FIATm family rename/reorg (Parameters/FIATm/, design records) - ~130 chipnet + sim-fiatm scripts (catalytic-converter, hyperinflation backtests) - snapshots 0153-0168; wiki updates (glossary, decisions, index); ADR-0044/0045 - mockups + additional shell screens (Agora, Apps, MainScreen, AssetDetail, ...) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
223 lines
9.3 KiB
JavaScript
223 lines
9.3 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* chipnet-t001-verify.mjs — victim-side defenses against the T-001 rug.
|
|
*
|
|
* Step (d) of the T-001 PoC (snapshot 0018 "Next" item 3). Runs the two
|
|
* wallet-side checks from snapshot 0018 against the on-chain state left
|
|
* by chipnet-t001-attack.mjs and confirms each one fires.
|
|
*
|
|
* D-1 — Canonical contract bytecode pinning.
|
|
* The wallet ships the canonical DeviantOpenMintFT template.
|
|
* For the contract anchoring the RUG category it reconstructs
|
|
* what the canonical contract address WOULD be for the published
|
|
* params, and compares to the contract address actually holding
|
|
* the reservoir. The backdoor contract has extra bytecode (the
|
|
* founderFreeMint path + founderPk param) → a different P2SH32
|
|
* address → the comparison fails → token flagged "non-standard,
|
|
* peg not guaranteed".
|
|
*
|
|
* D-2 — Per-UTXO backing check at receive time.
|
|
* For every incoming RUG FT UTXO the wallet checks
|
|
* utxo.value >= ft_amount * satsPerUnit. The underbacked UTXO
|
|
* from the rug carries ~675 sats against a multi-million-sat
|
|
* peg requirement → flagged UNDERBACKED, excluded from the
|
|
* redeemable peg balance.
|
|
*
|
|
* IMPORTANT — this is PoC-grade verification logic living in the test
|
|
* script on purpose. The production D-1/D-2 implementation belongs in a
|
|
* dedicated src/verify/ module folded into the UTXO classification
|
|
* engine (snapshot 0018 "Next" items 1-2); that is separate scoped work.
|
|
* This script proves the checks are sound and catch the live attack.
|
|
*
|
|
* Env vars:
|
|
* VICTIM=a|b|c whose wallet runs the checks (default c — must match
|
|
* the VICTIM used in chipnet-t001-attack.mjs)
|
|
*
|
|
* Exit code 0 = both defenses fired (T-001 neutralised).
|
|
* Exit code 1 = a defense failed to fire, or setup is incomplete.
|
|
*/
|
|
import { readFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { TestNetWallet } from 'mainnet-js';
|
|
import { Contract, ElectrumNetworkProvider } from 'cashscript';
|
|
|
|
const ROOT = join(import.meta.dirname, '..');
|
|
const T001_FILE = join(ROOT, 'ChipnetTests', '.chipnet-t001-token.json');
|
|
const CANONICAL_ARTIFACT_PATH = join(ROOT, 'contracts/dist/DeviantOpenMintFT.json');
|
|
const BACKDOOR_ARTIFACT_PATH = join(ROOT, 'contracts/dist/DeviantOpenMintFT_Backdoor.json');
|
|
|
|
const VICTIM = (process.env.VICTIM ?? 'c').toLowerCase();
|
|
const KEYS = {
|
|
a: join(ROOT, '.chipnet-keys.json'),
|
|
b: join(ROOT, '.chipnet-keys-b.json'),
|
|
c: join(ROOT, '.chipnet-keys-c.json'),
|
|
};
|
|
if (!KEYS[VICTIM]) throw new Error(`VICTIM must be a/b/c, got ${VICTIM}`);
|
|
|
|
function hexToBin(hex) {
|
|
if (hex.length % 2 !== 0) throw new Error(`odd hex length: ${hex.length}`);
|
|
const out = new Uint8Array(hex.length / 2);
|
|
for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
return out;
|
|
}
|
|
function txidToConsensusBytes(txidHex) {
|
|
const display = hexToBin(txidHex);
|
|
const out = new Uint8Array(display.length);
|
|
for (let i = 0; i < display.length; i++) out[i] = display[display.length - 1 - i];
|
|
return out;
|
|
}
|
|
function divider() { console.log('='.repeat(72)); }
|
|
|
|
async function main() {
|
|
const t001 = JSON.parse(readFileSync(T001_FILE, 'utf8'));
|
|
const canonicalArtifact = JSON.parse(readFileSync(CANONICAL_ARTIFACT_PATH, 'utf8'));
|
|
const backdoorArtifact = JSON.parse(readFileSync(BACKDOOR_ARTIFACT_PATH, 'utf8'));
|
|
const { wif } = JSON.parse(readFileSync(KEYS[VICTIM], 'utf8'));
|
|
|
|
const victim = await TestNetWallet.fromWIF(wif);
|
|
const provider = new ElectrumNetworkProvider('chipnet');
|
|
|
|
const rateSats = BigInt(t001.rateSats);
|
|
const tokenCategory = txidToConsensusBytes(t001.categoryId);
|
|
const founderLockingBytecode = hexToBin(t001.founderLockingBytecodeHex);
|
|
|
|
divider();
|
|
console.log(`T-001 victim-side defenses — Wallet ${VICTIM.toUpperCase()} (${victim.cashaddr})`);
|
|
console.log(` Token under test: ${t001.name} (category ${t001.categoryId.slice(0, 16)}...)`);
|
|
console.log(` Published rate: ${rateSats} sats/unit`);
|
|
divider();
|
|
|
|
let d1Fired = false;
|
|
let d2Fired = false;
|
|
|
|
// ---- D-1: canonical contract bytecode pinning -------------------------
|
|
console.log('');
|
|
console.log('D-1 — Canonical contract bytecode pinning');
|
|
console.log('-'.repeat(72));
|
|
|
|
// The wallet's embedded canonical template, instantiated with the
|
|
// token's PUBLISHED params. If the token were honest, this address
|
|
// would equal the contract that actually holds the reservoir.
|
|
const canonicalContract = new Contract(
|
|
canonicalArtifact,
|
|
[tokenCategory, rateSats, BigInt(t001.founderFeeBps), founderLockingBytecode],
|
|
{ provider },
|
|
);
|
|
|
|
// Confirm the contract address from metadata actually holds the
|
|
// minting-NFT reservoir for the claimed category (so we are checking
|
|
// the right contract, not a decoy).
|
|
const contractUtxos = await provider.getUtxos(t001.contractTokenAddress);
|
|
const reservoir = contractUtxos.find(
|
|
(u) => u.token?.nft?.capability === 'minting' && u.token?.category === t001.categoryId,
|
|
);
|
|
if (!reservoir) {
|
|
console.log(` FAIL: no minting-NFT reservoir for category at ${t001.contractTokenAddress}`);
|
|
console.log(' Has chipnet:t001-genesis run and confirmed?');
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(` On-chain contract (holds reservoir): ${t001.contractTokenAddress}`);
|
|
console.log(` Canonical template @ published params: ${canonicalContract.tokenAddress}`);
|
|
|
|
if (canonicalContract.tokenAddress === t001.contractTokenAddress) {
|
|
console.log(' D-1 result: contract IS canonical — D-1 did not flag it.');
|
|
} else {
|
|
d1Fired = true;
|
|
console.log(' D-1 result: MISMATCH — on-chain contract is NOT the canonical');
|
|
console.log(' DeviantOpenMintFT template. Token flagged');
|
|
console.log(' "non-standard CashToken — peg not guaranteed".');
|
|
console.log(' → no protocol-swap eligibility, no peg display.');
|
|
|
|
// Corroborate: show the on-chain address DOES match the backdoor
|
|
// template — i.e. we have positively identified the rug contract.
|
|
const backdoorContract = new Contract(
|
|
backdoorArtifact,
|
|
[
|
|
tokenCategory,
|
|
rateSats,
|
|
BigInt(t001.founderFeeBps),
|
|
founderLockingBytecode,
|
|
hexToBin(t001.founderPkHex),
|
|
],
|
|
{ provider },
|
|
);
|
|
const matchesBackdoor = backdoorContract.tokenAddress === t001.contractTokenAddress;
|
|
console.log(` Corroboration: on-chain address ${matchesBackdoor ? 'matches' : 'does NOT match'} ` +
|
|
`the known DeviantOpenMintFT_Backdoor template.`);
|
|
}
|
|
|
|
// ---- D-2: per-UTXO backing check at receive time ----------------------
|
|
console.log('');
|
|
console.log('D-2 — Per-UTXO backing check at receive time');
|
|
console.log('-'.repeat(72));
|
|
|
|
const victimUtxos = await provider.getUtxos(victim.cashaddr);
|
|
const rugFtUtxos = victimUtxos.filter(
|
|
(u) => u.token?.category === t001.categoryId && !u.token?.nft && u.token?.amount > 0n,
|
|
);
|
|
|
|
if (rugFtUtxos.length === 0) {
|
|
console.log(` No ${t001.name} FT UTXOs in Wallet ${VICTIM.toUpperCase()}.`);
|
|
console.log(' Has chipnet:t001-attack run and confirmed?');
|
|
process.exit(1);
|
|
}
|
|
|
|
let redeemableTotal = 0n;
|
|
let pegRequiredTotal = 0n;
|
|
let underbackedCount = 0;
|
|
for (const u of rugFtUtxos) {
|
|
const ftAmount = u.token.amount;
|
|
const value = BigInt(u.satoshis);
|
|
const expected = ftAmount * rateSats;
|
|
redeemableTotal += value;
|
|
pegRequiredTotal += expected;
|
|
const underbacked = value < expected;
|
|
if (underbacked) underbackedCount += 1;
|
|
console.log(
|
|
` UTXO ${u.txid.slice(0, 16)}:${u.vout} — ${ftAmount} ${t001.name}, ` +
|
|
`${value} sats`,
|
|
);
|
|
console.log(
|
|
` peg requires ${expected} sats → ${underbacked ? 'UNDERBACKED' : 'ok'}` +
|
|
(underbacked ? ` (carries ${(value * 10000n) / expected} bps of peg)` : ''),
|
|
);
|
|
}
|
|
|
|
if (underbackedCount > 0) {
|
|
d2Fired = true;
|
|
console.log('');
|
|
console.log(` D-2 result: ${underbackedCount} underbacked UTXO(s) flagged.`);
|
|
console.log(` Redeemable now: ${redeemableTotal} sats`);
|
|
console.log(` Peg implies: ${pegRequiredTotal} sats`);
|
|
console.log(` Shortfall: ${pegRequiredTotal - redeemableTotal} sats`);
|
|
console.log(' Underbacked UTXOs excluded from redeemable peg balance.');
|
|
} else {
|
|
console.log(' D-2 result: all UTXOs fully backed — D-2 did not flag anything.');
|
|
}
|
|
|
|
// ---- Verdict ----------------------------------------------------------
|
|
console.log('');
|
|
divider();
|
|
if (d1Fired && d2Fired) {
|
|
console.log('T-001 NEUTRALISED — both defenses fired.');
|
|
console.log('The wallet correctly refuses to treat RUG as Deviant-compliant:');
|
|
console.log(' D-1 caught the non-canonical contract;');
|
|
console.log(' D-2 caught the underbacked UTXO independently.');
|
|
divider();
|
|
process.exit(0);
|
|
} else {
|
|
console.log('T-001 NOT fully neutralised:');
|
|
console.log(` D-1 ${d1Fired ? 'fired' : 'did NOT fire'}`);
|
|
console.log(` D-2 ${d2Fired ? 'fired' : 'did NOT fire'}`);
|
|
console.log('Expected both to fire against the rug. Investigate before relying on these checks.');
|
|
divider();
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error('FAILED:', err?.message ?? err);
|
|
if (err?.stack) console.error(err.stack);
|
|
process.exit(1);
|
|
});
|