Skip to content

Commit 3408398

Browse files
committed
chore: add alternative verification scripts
Signed-off-by: Tomás Migone <[email protected]>
1 parent d35f3ea commit 3408398

File tree

8 files changed

+456
-14
lines changed

8 files changed

+456
-14
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Usage: ./flatten-verify contracts/payments/GraphPayments.sol
5+
6+
SRC="$1"
7+
OUT_DIR="build/flattened"
8+
OUT_BASE="$(basename "$SRC" .sol)"
9+
OUT_FLAT="${OUT_DIR}/${OUT_BASE}.flattened.sol"
10+
OUT_CLEAN="${OUT_DIR}/${OUT_BASE}.for-verify.sol"
11+
12+
mkdir -p "$OUT_DIR"
13+
14+
echo "🔹 Flattening $SRC$OUT_FLAT"
15+
npx hardhat flatten "$SRC" > "$OUT_FLAT"
16+
17+
echo "🔹 Cleaning SPDX & pragma duplicates → $OUT_CLEAN"
18+
awk 'BEGIN{spdx=0;pragma=0}
19+
/^\/\/ SPDX-License-Identifier:/ { if (spdx++) next }
20+
/^pragma solidity/ { if (pragma++) next }
21+
{ print }' "$OUT_FLAT" > "$OUT_CLEAN"
22+
23+
echo "✅ Done."
24+
echo "Flattened file: $OUT_FLAT"
25+
echo "Cleaned verify file: $OUT_CLEAN"
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// scripts/compare-bytecode-immutables-aware.ts
2+
import fs from 'node:fs'
3+
4+
import { ethers } from 'hardhat'
5+
6+
function stripCborTrailer(hex: string) {
7+
if (!hex || hex.length < 6) return hex
8+
const lenHex = hex.slice(-4)
9+
const len = parseInt(lenHex, 16)
10+
const trailerLen = len * 2 + 4 // hex chars
11+
if (trailerLen > 0 && trailerLen < hex.length) return hex.slice(0, hex.length - trailerLen)
12+
return hex
13+
}
14+
15+
function maskRanges(hex: string, ranges: { start: number; length: number }[]) {
16+
// hex is 0x...; convert to array of bytes (2 hex chars per byte), mask with '??'
17+
const body = hex.startsWith('0x') ? hex.slice(2) : hex
18+
const bytes = body.match(/.{1,2}/g) ?? []
19+
for (const { start, length } of ranges) {
20+
for (let i = 0; i < length; i++) {
21+
const idx = start + i
22+
if (idx >= 0 && idx < bytes.length) bytes[idx] = '??'
23+
}
24+
}
25+
return '0x' + bytes.join('')
26+
}
27+
28+
async function main() {
29+
const address = process.env.ADDRESS!
30+
const fqn = process.env.FQN // e.g. contracts/payments/GraphPayments.sol:GraphPayments
31+
32+
if (!fqn || !address) {
33+
console.error('❌ FQN or ADDRESS is not set')
34+
process.exit(1)
35+
}
36+
37+
const artifactPath = `build/contracts/${fqn.replace(/:/g, '/')}.json`
38+
const artifact = JSON.parse(fs.readFileSync(artifactPath, 'utf8'))
39+
40+
// on-chain runtime, strip CBOR trailer
41+
const onchain = await ethers.provider.getCode(address)
42+
const onchainStripped = stripCborTrailer(onchain.toLowerCase())
43+
44+
// compiled runtime from artifact (still has immutable placeholders)
45+
const compiled = (artifact.deployedBytecode as string).toLowerCase()
46+
const immRefs = artifact.immutableReferences ?? {} // { <hex slot>: [{ start, length }] }
47+
48+
// Build ranges: artifact.immutableReferences lists byte offsets (not hex chars)
49+
const ranges: { start: number; length: number }[] = []
50+
for (const _ of Object.keys(immRefs)) {
51+
for (const ref of immRefs[_]) {
52+
ranges.push({ start: ref.start, length: ref.length })
53+
}
54+
}
55+
56+
// mask CBOR trailer and immutable regions on BOTH sides
57+
const compiledStripped = stripCborTrailer(compiled)
58+
const maskedOnchain = maskRanges(onchainStripped, ranges)
59+
const maskedCompiled = maskRanges(compiledStripped, ranges)
60+
61+
console.log('Masked on-chain prefix:', maskedOnchain.slice(0, 20), '…', maskedOnchain.length)
62+
console.log('Masked compiled pref:', maskedCompiled.slice(0, 20), '…', maskedCompiled.length)
63+
64+
if (maskedOnchain === maskedCompiled) {
65+
console.log('✅ Runtime matches when ignoring immutables & metadata.')
66+
} else {
67+
console.error('❌ Runtime still differs after masking. Re-check solc settings / viaIR / optimizer.')
68+
process.exit(1)
69+
}
70+
}
71+
72+
main().catch((e) => {
73+
console.error(e)
74+
process.exit(1)
75+
})
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// scripts/compare-bytecode.ts
2+
import fs from 'node:fs'
3+
4+
import { ethers } from 'hardhat'
5+
6+
function stripCborRuntime(code: string) {
7+
// Deployed runtime bytecode ends with a CBOR-encoded metadata whose byte length is stored in the final 2 bytes.
8+
// Remove that trailer so metadataHash differences don't confuse us.
9+
if (!code || code.length < 4) return code
10+
const lenHex = code.slice(-4)
11+
const len = parseInt(lenHex, 16)
12+
const trailerLen = len * 2 + 4 // hex chars
13+
if (trailerLen > 0 && trailerLen < code.length) return code.slice(0, code.length - trailerLen)
14+
return code
15+
}
16+
17+
async function main() {
18+
const address = process.env.ADDRESS! // target deployed address
19+
const fqn = process.env.FQN // "contracts/payments/GraphPayments.sol:GraphPayments";
20+
21+
if (!fqn || !address) {
22+
console.error('❌ FQN or ADDRESS is not set')
23+
process.exit(1)
24+
}
25+
26+
const artifactPath = `build/contracts/${fqn.replace(/:/g, '/')}.json`
27+
const artifact = JSON.parse(fs.readFileSync(artifactPath, 'utf8'))
28+
29+
const onchain = await ethers.provider.getCode(address)
30+
const onchainStripped = stripCborRuntime(onchain.toLowerCase())
31+
32+
// Hardhat artifact has both creation bytecode (bytecode) and deployed runtime (deployedBytecode)
33+
const compiled = artifact.deployedBytecode.toLowerCase()
34+
35+
// If there are link placeholders like __$abcd$__, the bytecode is not linked:
36+
if (compiled.includes('__$')) {
37+
console.error('⚠️ Artifact has unlinked libraries. Link addresses must match deployment.')
38+
process.exit(1)
39+
}
40+
41+
const compiledStripped = stripCborRuntime(compiled)
42+
43+
console.log('On-chain (stripped) prefix:', onchainStripped.slice(0, 20), '…', onchainStripped.length)
44+
console.log('Compiled (stripped) pref:', compiledStripped.slice(0, 20), '…', compiledStripped.length)
45+
46+
if (onchainStripped === compiledStripped) {
47+
console.log('✅ Runtime matches (ignoring metadata). Problem likely in constructor args / creation code.')
48+
} else if (onchainStripped.startsWith(compiledStripped) || compiledStripped.startsWith(onchainStripped)) {
49+
console.log('✅ Prefix matches. Differences only in metadata. Check metadata.bytecodeHash and solc version.')
50+
} else {
51+
console.error('❌ Runtime mismatch. Check solc settings, viaIR, optimizer, libraries, or immutables.')
52+
process.exit(1)
53+
}
54+
}
55+
main().catch((e) => {
56+
console.error(e)
57+
process.exit(1)
58+
})
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// scripts/compare-creation.ts
2+
import fs from 'node:fs'
3+
4+
import { Interface } from 'ethers'
5+
import { ethers } from 'hardhat'
6+
7+
async function main() {
8+
const fqn = process.env.FQN // "contracts/payments/GraphPayments.sol:GraphPayments";
9+
const txHash = process.env.TX! // deployment tx hash
10+
const args = process.env.ARGS ? JSON.parse(process.env.ARGS) : [] // '[arg1,arg2,...]'
11+
12+
if (!fqn || !txHash) {
13+
console.error('❌ FQN or TX is not set')
14+
process.exit(1)
15+
}
16+
17+
const artifactPath = `build/contracts/${fqn.replace(/:/g, '/')}.json`
18+
const artifact = JSON.parse(fs.readFileSync(artifactPath, 'utf8'))
19+
20+
// Creation (init) code is artifact.bytecode with libraries linked + encoded ctor args appended.
21+
if (artifact.bytecode.includes('__$')) {
22+
console.error('Unlinked libraries in creation bytecode.')
23+
process.exit(1)
24+
}
25+
26+
const iface = new Interface(artifact.abi)
27+
const encodedArgs = iface.encodeDeploy(args)
28+
const compiledInit = (artifact.bytecode + encodedArgs.slice(2)).toLowerCase()
29+
30+
const tx = await ethers.provider.getTransaction(txHash)
31+
if (!tx) {
32+
console.error('❌ Transaction not found')
33+
process.exit(1)
34+
}
35+
const deployedInit = (tx.data || '').toLowerCase()
36+
37+
console.log('compiled init len:', compiledInit.length, ' deployed init len:', deployedInit.length)
38+
console.log(
39+
compiledInit === deployedInit
40+
? '✅ Creation bytecode matches.'
41+
: '❌ Creation bytecode mismatch (ctor args / linking / settings).',
42+
)
43+
}
44+
main().catch((e) => {
45+
console.error(e)
46+
process.exit(1)
47+
})
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// scripts/decode-creation-args.ts
2+
import fs from 'node:fs'
3+
4+
import { ethers } from 'hardhat'
5+
6+
function stripCborTrailer(hex: string) {
7+
// Remove trailing CBOR metadata based on the last 2 bytes length
8+
if (!hex || hex.length < 6) return hex
9+
const lenHex = hex.slice(-4)
10+
const len = parseInt(lenHex, 16)
11+
const trailerLen = len * 2 + 4 // hex chars
12+
if (trailerLen > 0 && trailerLen < hex.length) return hex.slice(0, hex.length - trailerLen)
13+
return hex
14+
}
15+
16+
async function main() {
17+
const FQN = process.env.FQN // e.g. "contracts/payments/GraphPayments.sol:GraphPayments"
18+
const TX = process.env.TX // deployment tx hash
19+
20+
if (!FQN || !TX) {
21+
console.error('❌ FQN or TX is not set')
22+
process.exit(1)
23+
}
24+
25+
const artifactPath = `build/contracts/${FQN.replace(/:/g, '/')}.json`
26+
const artifact = JSON.parse(fs.readFileSync(artifactPath, 'utf8'))
27+
28+
const tx = await ethers.provider.getTransaction(TX)
29+
if (!tx?.data) throw new Error('Cannot fetch tx.data; check TX hash / network')
30+
31+
const txData = tx.data.toLowerCase()
32+
let creation = (artifact.bytecode as string).toLowerCase()
33+
if (creation.startsWith('0x') === false) creation = '0x' + creation
34+
35+
// Try exact prefix match first
36+
let prefix = creation
37+
if (!txData.startsWith(prefix)) {
38+
// Try stripping CBOR trailers from both sides just in case
39+
prefix = stripCborTrailer(prefix)
40+
const txNoCbor = stripCborTrailer(txData)
41+
if (txNoCbor.startsWith(prefix)) {
42+
// use tx with its trailer stripped as well
43+
const argsData = '0x' + txNoCbor.slice(prefix.length)
44+
await decode(argsData, artifact)
45+
return
46+
}
47+
// Fallback: find the longest prefix that matches (robust against tiny metadata diffs)
48+
let i = Math.min(prefix.length, txData.length)
49+
while (i > 2 && !txData.startsWith(prefix.slice(0, i))) i -= 2
50+
if (i <= 2) {
51+
throw new Error('Creation bytecode prefix not found in tx.data. Check solc settings.')
52+
}
53+
const argsData = '0x' + txData.slice(i)
54+
await decode(argsData, artifact)
55+
return
56+
}
57+
58+
const argsData = '0x' + txData.slice(prefix.length)
59+
await decode(argsData, artifact)
60+
}
61+
62+
async function decode(argsData: string, artifact: any) {
63+
const ctor = (artifact.abi as any[]).find((x) => x.type === 'constructor')
64+
const types = ctor?.inputs?.map((i: any) => i.type) ?? []
65+
if (types.length === 0) {
66+
if (argsData === '0x' || argsData.length <= 2) {
67+
console.log('Constructor has no params. Args: []')
68+
return
69+
} else {
70+
console.warn('No constructor inputs in ABI, but args data present. Types unknown.')
71+
console.log('Raw argsData:', argsData)
72+
return
73+
}
74+
}
75+
76+
const coder = ethers.AbiCoder.defaultAbiCoder()
77+
const decoded = coder.decode(types, argsData)
78+
// Pretty-print as JSON array so you can reuse directly
79+
const printable = decoded.map((v: any) => (typeof v === 'bigint' ? v.toString() : v))
80+
console.log('Constructor types:', types)
81+
console.log('Decoded args JSON:', JSON.stringify(printable))
82+
}
83+
84+
main().catch((e) => {
85+
console.error(e)
86+
process.exit(1)
87+
})
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import fs from 'node:fs'
2+
3+
import { ethers } from 'hardhat'
4+
5+
function stripCbor(hex: string) {
6+
if (!hex || hex.length < 6) return hex
7+
const len = parseInt(hex.slice(-4), 16)
8+
const trailer = len * 2 + 4
9+
return trailer > 0 && trailer < hex.length ? hex.slice(0, hex.length - trailer) : hex
10+
}
11+
12+
function hexToBytes(hex: string) {
13+
const h = hex.startsWith('0x') ? hex.slice(2) : hex
14+
return h.match(/.{1,2}/g)?.map((b) => parseInt(b, 16)) ?? []
15+
}
16+
function bytesToHex(a: number[]) {
17+
return '0x' + a.map((b) => b.toString(16).padStart(2, '0')).join('')
18+
}
19+
20+
function mask(bytes: number[], ranges: { start: number; length: number }[]) {
21+
const out = bytes.slice()
22+
for (const { start, length } of ranges) {
23+
for (let i = 0; i < length; i++) {
24+
const idx = start + i
25+
if (idx >= 0 && idx < out.length) out[idx] = 0xff // sentinel
26+
}
27+
}
28+
return out
29+
}
30+
31+
function firstDiff(a: number[], b: number[]) {
32+
const n = Math.min(a.length, b.length)
33+
for (let i = 0; i < n; i++) if (a[i] !== b[i]) return i
34+
if (a.length !== b.length) return n
35+
return -1
36+
}
37+
38+
async function main() {
39+
const ADDRESS = process.env.ADDRESS
40+
const FQN = process.env.FQN
41+
if (!FQN || !ADDRESS) {
42+
console.error('❌ FQN or ADDRESS is not set')
43+
process.exit(1)
44+
}
45+
46+
const artifactPath = `build/contracts/${FQN.replace(/:/g, '/')}.json`
47+
const art = JSON.parse(fs.readFileSync(artifactPath, 'utf8'))
48+
49+
const on = stripCbor((await ethers.provider.getCode(ADDRESS)).toLowerCase())
50+
const comp = stripCbor((art.deployedBytecode as string).toLowerCase())
51+
52+
// Gather immutable ranges from artifact
53+
const immRefs = art.immutableReferences ?? {}
54+
const ranges: { start: number; length: number }[] = []
55+
for (const k of Object.keys(immRefs)) for (const r of immRefs[k]) ranges.push({ start: r.start, length: r.length })
56+
57+
console.log('immutable ranges:', ranges)
58+
59+
const onBytes = hexToBytes(on)
60+
const compBytes = hexToBytes(comp)
61+
62+
// Mask immutables on both sides
63+
const onMasked = mask(onBytes, ranges)
64+
const compMasked = mask(compBytes, ranges)
65+
66+
const i = firstDiff(onMasked, compMasked)
67+
if (i === -1) {
68+
console.log('✅ Runtime matches after masking immutables & stripping metadata.')
69+
return
70+
}
71+
72+
const lo = Math.max(0, i - 32),
73+
hi = Math.min(onMasked.length, i + 32)
74+
console.log('❌ First diff at byte offset:', i)
75+
console.log('on-chain :', bytesToHex(onBytes.slice(lo, hi)))
76+
console.log('compiled :', bytesToHex(compBytes.slice(lo, hi)))
77+
console.log('on-masked :', bytesToHex(onMasked.slice(lo, hi)))
78+
console.log('comp-masked:', bytesToHex(compMasked.slice(lo, hi)))
79+
}
80+
81+
main().catch((e) => {
82+
console.error(e)
83+
process.exit(1)
84+
})

0 commit comments

Comments
 (0)