pqc-migration-toolkit/lib/abi.js
Aere Network de9a79298c Aere PQC migration toolkit: measure an address quantum exposure from its bytecode
Zero dependencies, no account, no API. Every command takes --rpc, so it runs
against a node you operate and never needs ours. That is the point: an exposure
measurement you cannot re-run is an opinion with a logo on it.

Ships with two self-checks meant to be run by you, not just by us. The second,
proba-vocabular.mjs, exists because of a real defect found on 2026-08-16: the
scanner emits RED / YELLOW / GREEN, two downstream files each wrote their own
copy of that list, and both wrote AMBER, so a hybrid contract, exactly a client
who has already started migrating, was reported as unmeasurable. Both sides were
self-consistent, so nothing we had could see it. The vocabulary now has one
exported source, and the test walks the whole path with a negative control that
makes it able to fail.

The same scanner returns RED about our own contracts. See VERIFY-US.md.
2026-08-17 10:31:31 +03:00

108 lines
3.7 KiB
JavaScript

// abi.js, a small, zero-dependency ABI encoder covering exactly the type set the
// migration SDK needs to build AereAccountMigrator and ERC-20 calldata:
//
// uint256, address, bool, bytes, address[], uint256[]
//
// It implements the standard Solidity head/tail layout (dynamic parameters get a
// 32-byte offset in the head and their bytes in the tail). It is validated
// byte-for-byte against ethers v6 in selftest.js, so the calldata this toolkit
// hands a user to sign is provably identical to what a full ABI library emits.
//
// This file ENCODES calldata only. It never signs, deploys, or broadcasts.
import { keccak256 } from './keccak.js';
function stripHex(h) {
return h.startsWith('0x') || h.startsWith('0X') ? h.slice(2) : h;
}
function uint256Hex(value) {
const v = BigInt(value);
if (v < 0n) throw new Error('uint256 cannot be negative');
const hex = v.toString(16);
if (hex.length > 64) throw new Error('uint256 overflow');
return hex.padStart(64, '0');
}
function addressHex(addr) {
const a = stripHex(addr).toLowerCase();
if (a.length !== 40 || /[^0-9a-f]/.test(a)) throw new Error(`invalid address: ${addr}`);
return a.padStart(64, '0');
}
function boolHex(b) {
return (b ? 1n : 0n).toString(16).padStart(64, '0');
}
function isDynamic(type) {
return type === 'bytes' || type.endsWith('[]');
}
// Encode a single dynamic value to hex (no 0x), including its own length word.
function encodeDynamic(type, value) {
if (type === 'bytes') {
const h = stripHex(value).toLowerCase();
if (h.length % 2 !== 0) throw new Error('bytes must be an even-length hex string');
const byteLen = h.length / 2;
const padded = h.padEnd(Math.ceil(h.length / 64) * 64, '0');
return uint256Hex(byteLen) + padded;
}
if (type === 'address[]') {
if (!Array.isArray(value)) throw new Error('address[] expects an array');
return uint256Hex(value.length) + value.map(addressHex).join('');
}
if (type === 'uint256[]') {
if (!Array.isArray(value)) throw new Error('uint256[] expects an array');
return uint256Hex(value.length) + value.map(uint256Hex).join('');
}
throw new Error(`unsupported dynamic type: ${type}`);
}
function encodeStatic(type, value) {
if (type === 'uint256') return uint256Hex(value);
if (type === 'address') return addressHex(value);
if (type === 'bool') return boolHex(value);
throw new Error(`unsupported static type: ${type}`);
}
/**
* ABI-encode a parameter tuple. Returns 0x-prefixed hex.
* @param {string[]} types
* @param {any[]} values
*/
export function encodeParameters(types, values) {
if (types.length !== values.length) throw new Error('types/values length mismatch');
const head = [];
const tail = [];
let tailOffset = types.length * 32; // bytes; every head slot is 32 bytes
for (let i = 0; i < types.length; i++) {
const t = types[i];
if (isDynamic(t)) {
head.push(uint256Hex(tailOffset));
const enc = encodeDynamic(t, values[i]);
tail.push(enc);
tailOffset += enc.length / 2;
} else {
head.push(encodeStatic(t, values[i]));
}
}
return '0x' + head.join('') + tail.join('');
}
/** 4-byte selector (0x + 8 hex chars) for a canonical function signature. */
export function functionSelector(signature) {
const h = keccak256(new TextEncoder().encode(signature));
let s = '0x';
for (let i = 0; i < 4; i++) s += h[i].toString(16).padStart(2, '0');
return s;
}
/**
* Build full calldata: selector(signature) ++ encodeParameters(types, values).
* `signature` must be the canonical form, e.g. "migrate(address,address[],uint256[],bool)".
* @returns {string} 0x-prefixed calldata
*/
export function encodeFunctionData(signature, types, values) {
return functionSelector(signature) + encodeParameters(types, values).slice(2);
}