// Scenario 4: developer-registry abuse (design 4.5). Registering a payee for someone else's code, factory // inheritance edge cases (CREATE, CREATE2, same-tx override, unregistered factory, EOA override attempt), and // self-dealing: a developer that also mines its own blocks to collect its own tips. Criterion per design 4.5 // (base fees are burned, no positive-expectation loop); we record the maximum share a self-dealer recovers. import { encodeFunctionData, decodeFunctionResult, keccak256, getContractAddress, parseEther } from 'viem'; import { readFileSync } from 'node:fs'; import * as k from './lib/common.mjs'; const Worker = JSON.parse(readFileSync(new URL('./contracts/Worker.json', import.meta.url))); const Factory = JSON.parse(readFileSync(new URL('./contracts/Factory.json', import.meta.url))); const REG = '0x0000000000000000000000000000000000000210'; const REG_ABI = [ { type: 'function', name: 'register', inputs: [{ type: 'address' }, { type: 'address' }], outputs: [] }, { type: 'function', name: 'payeeOf', stateMutability: 'view', inputs: [{ type: 'address' }], outputs: [{ type: 'address' }] }, { type: 'function', name: 'creatorOf', stateMutability: 'view', inputs: [{ type: 'address' }], outputs: [{ type: 'address' }] }, ]; const results = { scenario: '4-registry-abuse', cases: [], selfDealing: null }; const checks = new k.Checks(); // The child address from a Factory call: the Created(address) event is the log emitted BY the factory (register's // Registered event is emitted by the registry, so we cannot rely on log order). function childFrom(receipt, factory) { const log = (receipt?.logs || []).find((l) => l.address.toLowerCase() === factory.toLowerCase()); return log ? ('0x' + log.data.slice(26)) : null; } const call = (to, data) => k.rpc(k.node1, 'eth_call', [{ to, data }, 'latest']); async function payeeOf(a) { return decodeFunctionResult({ abi: REG_ABI, functionName: 'payeeOf', data: await call(REG, encodeFunctionData({ abi: REG_ABI, functionName: 'payeeOf', args: [a] })) }); } async function creatorOf(a) { return decodeFunctionResult({ abi: REG_ABI, functionName: 'creatorOf', data: await call(REG, encodeFunctionData({ abi: REG_ABI, functionName: 'creatorOf', args: [a] })) }); } // Does an eth_call revert? Returns true if it throws (reverts). async function reverts(from, to, data) { try { await k.rpc(k.node1, 'eth_call', [{ from, to, data }, 'latest']); return false; } catch { return true; } } // Send a signed tx and wait for its receipt. async function sendWait(account, fields, timeout = 30_000) { const raw = await k.signTx(account, fields); const s = await k.send(k.node1, raw); const r = await k.waitReceipt(keccak256(raw), timeout); return { raw, hash: keccak256(raw), sent: s, receipt: r }; } async function deploy(account, artifact, gas = 600_000n) { const n = await k.nonceOf(account); const addr = getContractAddress({ from: account.address, nonce: BigInt(n) }); const r = await sendWait(account, { nonce: n, to: null, data: artifact.bytecode, gas }); return { addr: r.receipt?.contractAddress ?? addr, receipt: r.receipt }; } async function main() { await k.waitTip(2); await k.fund([k.A, k.B, k.C, k.D, k.E ?? k.C]); // ---- 1. Register a payee for someone else's code: must revert; payee unchanged. ---- const w1 = await deploy(k.A, Worker); checks.check(w1.receipt && w1.receipt.contractAddress, `deployed Worker from A (${w1.addr})`); const creatorW1 = await creatorOf(w1.addr); checks.check(creatorW1.toLowerCase() === k.A.address.toLowerCase(), `creatorOf(worker) == A (${creatorW1})`); // B (not the account, not the creator) tries to register a payee for A's contract. const badData = encodeFunctionData({ abi: REG_ABI, functionName: 'register', args: [w1.addr, k.B.address] }); const rev1 = await reverts(k.B.address, REG, badData); checks.check(rev1, 'register(worker, B) by B reverts (not the account or its creator)'); // B tries to register a payee for an unrelated EOA. const rev2 = await reverts(k.B.address, REG, encodeFunctionData({ abi: REG_ABI, functionName: 'register', args: [k.C.address, k.B.address] })); checks.check(rev2, 'register(C_eoa, B) by B reverts'); const payeeW1 = await payeeOf(w1.addr); checks.check(payeeW1 === '0x0000000000000000000000000000000000000000', `worker payee still unset after the failed registrations (${payeeW1})`); results.cases.push({ name: 'register-for-others', creatorW1, rev1, rev2, payeeW1 }); // ---- 2. Factory inheritance edge cases ---- const fac = await deploy(k.A, Factory, 900_000n); checks.check(fac.receipt && fac.addr, `deployed Factory from A (${fac.addr})`); // Factory registers itself with payee D. await sendWait(k.A, { nonce: await k.nonceOf(k.A), to: fac.addr, data: encodeFunctionData({ abi: Factory.abi, functionName: 'registerSelf', args: [k.D.address] }), gas: 200_000n }); const facPayee = await payeeOf(fac.addr); checks.check(facPayee.toLowerCase() === k.D.address.toLowerCase(), `factory self-registered payee = D (${facPayee})`); // CREATE child inherits the factory payee. const createData = encodeFunctionData({ abi: Factory.abi, functionName: 'createChild', args: [] }); const childPredict = getContractAddress({ from: fac.addr, nonce: 1n, opcode: 'CREATE' }); const cr = await sendWait(k.A, { nonce: await k.nonceOf(k.A), to: fac.addr, data: createData, gas: 500_000n }); const child1 = childFrom(cr.receipt, fac.addr) ?? childPredict; const child1Payee = await payeeOf(child1), child1Creator = await creatorOf(child1); checks.check(child1Creator.toLowerCase() === fac.addr.toLowerCase(), `CREATE child creator == factory (${child1Creator})`); checks.check(child1Payee.toLowerCase() === k.D.address.toLowerCase(), `CREATE child inherits factory payee D (${child1Payee})`); // CREATE2 child inherits too. const salt = '0x' + '11'.repeat(32); const c2 = await sendWait(k.A, { nonce: await k.nonceOf(k.A), to: fac.addr, data: encodeFunctionData({ abi: Factory.abi, functionName: 'createChild2', args: [salt] }), gas: 500_000n }); const child2 = childFrom(c2.receipt, fac.addr); const child2Payee = child2 ? await payeeOf(child2) : null; checks.check(child2Payee && child2Payee.toLowerCase() === k.D.address.toLowerCase(), `CREATE2 child inherits factory payee D (${child2Payee})`); // Same-tx override by the creator (factory) sets a different payee. const co = await sendWait(k.A, { nonce: await k.nonceOf(k.A), to: fac.addr, data: encodeFunctionData({ abi: Factory.abi, functionName: 'createAndOverride', args: [k.C.address] }), gas: 500_000n }); const child3 = childFrom(co.receipt, fac.addr); const child3Payee = child3 ? await payeeOf(child3) : null; checks.check(child3Payee && child3Payee.toLowerCase() === k.C.address.toLowerCase(), `same-tx creator override sets child payee to C (${child3Payee})`); // EOA cannot override a factory child's registration (not the account, not the creator). const eoaOverride = await reverts(k.A.address, REG, encodeFunctionData({ abi: REG_ABI, functionName: 'register', args: [child1, k.A.address] })); checks.check(eoaOverride, 'EOA (A) cannot override a factory child registration'); // Unregistered factory: its child inherits no payee (share will burn). const fac2 = await deploy(k.B, Factory, 900_000n); const u = await sendWait(k.B, { nonce: await k.nonceOf(k.B), to: fac2.addr, data: createData, gas: 500_000n }); const uchild = childFrom(u.receipt, fac2.addr); const uchildPayee = uchild ? await payeeOf(uchild) : null; checks.check(uchildPayee === '0x0000000000000000000000000000000000000000', `unregistered factory's child has no payee (${uchildPayee})`); results.cases.push({ name: 'factory-inheritance', facPayee, child1Payee, child1Creator, child2Payee, child3Payee, eoaOverrideReverts: eoaOverride, unregisteredChildPayee: uchildPayee }); // ---- 3. Self-dealing: sender == payee == block miner (node1 mines to the `miner` account). ---- const SD = k.miner; // node1's single miner pays this address const wsd = await deploy(SD, Worker); checks.check(wsd.receipt && wsd.addr, `self-dealer deployed Worker (${wsd.addr})`); // SD registers itself as the payee of its own contract (SD is the creator). await sendWait(SD, { nonce: await k.nonceOf(SD), to: REG, data: encodeFunctionData({ abi: REG_ABI, functionName: 'register', args: [wsd.addr, SD.address] }), gas: 120_000n }); const sdPayee = await payeeOf(wsd.addr); checks.check(sdPayee.toLowerCase() === SD.address.toLowerCase(), `self-dealer registered its own payee (${sdPayee})`); // SD calls its Worker with a healthy priority fee so a tip exists; the including block is mined by SD. const workData = encodeFunctionData({ abi: Worker.abi, functionName: 'work', args: [2000n] }); const sd = await sendWait(SD, { nonce: await k.nonceOf(SD), to: wsd.addr, data: workData, gas: 1_000_000n, maxFeePerGas: 5_000_000_000n, maxPriorityFeePerGas: 3_000_000_000n }); const ig = sd.receipt?.igneum; checks.check(!!ig, 'self-dealing call produced an igneum receipt with fee breakdown'); if (ig) { const minerTip = BigInt(ig.minerTip); const devToSD = (ig.developerShares || []).filter((s) => s.payee && s.payee.toLowerCase() === SD.address.toLowerCase()).reduce((a, s) => a + BigInt(s.wei), 0n); const devTotal = (ig.developerShares || []).reduce((a, s) => a + BigInt(s.wei), 0n); const burnedExec = BigInt(ig.burnedExecutionBaseFee); const burnedProving = BigInt(ig.burnedProvingFee); const totalPaid = minerTip + devTotal + burnedExec + burnedProving; const recovered = minerTip + devToSD; // SD is both the block miner and the payee const tip = minerTip + devTotal; const shareOfTotal = Number(recovered) / Number(totalPaid); const shareOfTip = Number(recovered) / Number(tip); checks.check(burnedExec > 0n && burnedProving > 0n, `both base fees are burned (exec ${burnedExec}, proving ${burnedProving})`); checks.check(recovered < totalPaid, `self-dealer recovers less than it pays (no positive-expectation loop): recovered ${recovered} < paid ${totalPaid}`); checks.check(shareOfTip > 0.99, `self-dealer recovers ~all of the tip (${(shareOfTip * 100).toFixed(1)}%)`); results.selfDealing = { minerTip: minerTip.toString(), devToSD: devToSD.toString(), devTotal: devTotal.toString(), burnedExec: burnedExec.toString(), burnedProving: burnedProving.toString(), totalPaid: totalPaid.toString(), recovered: recovered.toString(), maxShareOfTotalRecovered: +shareOfTotal.toFixed(4), shareOfTipRecovered: +shareOfTip.toFixed(4), }; } const s = checks.summary('scenario 4'); results.summary = s; const { writeFileSync } = await import('node:fs'); writeFileSync(new URL('./results/scenario4.json', import.meta.url), JSON.stringify(results, null, 2)); process.exit(s.ok ? 0 : 1); } main().catch((e) => { console.error(e); process.exit(2); });