Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages/legacy/verified/** -text
3 changes: 3 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,6 @@ deployments/hardhat/
**/chain-*/
**/*-localhost/
**/*-hardhat/

# Verified mainnet source, must stay byte-identical
packages/legacy/verified/
1 change: 1 addition & 0 deletions .solhintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages/legacy/verified/
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@
"scripts/lint-staged-run.sh 'prettier -w --cache --log-level warn'"
],
"*.sol": [
"solhint --fix --noPrompt --noPoster",
"prettier -w --cache --log-level warn"
"scripts/lint-staged-run.sh 'solhint --fix --noPrompt --noPoster'",
"scripts/lint-staged-run.sh 'prettier -w --cache --log-level warn'"
],
"*.md": [
"markdownlint --fix",
Expand Down
39 changes: 39 additions & 0 deletions packages/legacy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Legacy mainnet implementations

A frozen archive of the source of seven implementations running on Ethereum mainnet, as verified on [Sourcify](https://sourcify.dev). Each contract has its own bundle under `verified/<Contract>/`, holding the verified sources and the compiler version and settings.

This package is private and is not published. It holds no Solidity imports from the workspace and no tests of contract behaviour.

| Contract | Implementation | Proxy | solc |
| ------------------- | -------------------------------------------- | -------------------------------------------- | ----- |
| GraphToken | `0xc944E90C64B2c07662A292be6244BDf05Cda44a7` | none | 0.7.4 |
| L1GraphTokenGateway | `0xD41ca6A1d034D178c196DFa916f22f7D1a1B8222` | `0x01cDC91B0A9bA741903aA3699BF4CE31d6C5cC06` | 0.7.6 |
| BridgeEscrow | `0xBcD54513aa593646d72aEA31406c633C235Ad6EA` | `0x36aFF7001294daE4C2ED4fDEfC478a00De77F090` | 0.7.6 |
| L1GNS | `0x83A5B6C542aB20436087Fc79724bdF06ea80f836` | `0xaDcA0dd4729c8BA3aCf3E99F3A9f471EF37b6825` | 0.7.6 |
| Curation | `0xDeb46851907fd85DD475780CcE2eE0D67c969825` | `0x8FE00a685Bcb3B2cc296ff6FfEaB10acA4CE1538` | 0.7.6 |
| L1Staking | `0x20a14D78848BC8B3F3d4B77239F2adC3C0259A10` | `0xF55041E37E12cD407ad00CE2910B8269B01263b9` | 0.7.6 |
| StakingExtension | `0xA479c00cDa8C07bce458D7a826C7b091672EB92C` | none | 0.7.6 |

When this archive was created, the workspace source reproduced only Curation.

GraphToken and L1Staking are Sourcify partial matches (`sourcifyMatch: "match"`), so their sources may differ from the deployed ones outside the code.

## Checks

`pnpm test:self` needs no network access. It compares every source file with `sourceHashes` in `compiler.json`, compiles every bundle with its exact solc-js version, and compares the result with the hash of the on-chain code in `mainnet.json`. The comparison ignores metadata and immutable values.

## Sources must stay unformatted

Never edit or format anything under `verified/`. Formatting doesn't change bytecode, so only the source hashes catch it. `.prettierignore`, `.solhintignore`, `scripts/lint-staged-run.sh` and `.gitattributes` keep tools and git from rewriting these files, and `GraphToken.sol` keeps its CRLF line endings. Don't add a `.solhint.json` to this package.

## Re-running the scripts

```bash
# Rewrite verified/ from Sourcify, using the addresses in targets.json
node scripts/fetch-verified.mjs

# Check the addresses on-chain at a finalized block and rewrite mainnet.json
MAINNET_RPC_URL=<url> node scripts/record-mainnet.mjs
```

`record-mainnet` exits 1 if a proxy, or L1Staking's `extensionImpl`, no longer points at the listed implementation. Pass the RPC URL only as an inline env var, and never commit it.
40 changes: 40 additions & 0 deletions packages/legacy/mainnet.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"block": 26306549,
"contracts": {
"GraphToken": {
"implementation": "0xc944E90C64B2c07662A292be6244BDf05Cda44a7",
"proxy": null,
"runtimeHash": "43e4307034b17a35b4411b390f76d0b7278c95408743f4e7104d7f11327012ee"
},
"L1GraphTokenGateway": {
"implementation": "0xD41ca6A1d034D178c196DFa916f22f7D1a1B8222",
"proxy": "0x01cDC91B0A9bA741903aA3699BF4CE31d6C5cC06",
"runtimeHash": "40b384d3427296452c81f952e3434c2394c24661ea4d42f7e8e3a2a589001eaf"
},
"BridgeEscrow": {
"implementation": "0xBcD54513aa593646d72aEA31406c633C235Ad6EA",
"proxy": "0x36aFF7001294daE4C2ED4fDEfC478a00De77F090",
"runtimeHash": "26e16ae208b7353a0f44e768f4f40e6467758b7004394a8a640a8a4ea29bce9e"
},
"L1GNS": {
"implementation": "0x83A5B6C542aB20436087Fc79724bdF06ea80f836",
"proxy": "0xaDcA0dd4729c8BA3aCf3E99F3A9f471EF37b6825",
"runtimeHash": "f88f9f204bd4f121019d8eb9164a1952f0ae0e79ed602088206e2059d3a51515"
},
"Curation": {
"implementation": "0xDeb46851907fd85DD475780CcE2eE0D67c969825",
"proxy": "0x8FE00a685Bcb3B2cc296ff6FfEaB10acA4CE1538",
"runtimeHash": "c8b0bdcab52a661ff3a8615f5bf29336615832f4f0b4b3cf4e2506c6b22a87b6"
},
"L1Staking": {
"implementation": "0x20a14D78848BC8B3F3d4B77239F2adC3C0259A10",
"proxy": "0xF55041E37E12cD407ad00CE2910B8269B01263b9",
"runtimeHash": "692f896bc98b60d256e121d90e2111108dfcfd14ccc1aa935f8be645ffb7f8d6"
},
"StakingExtension": {
"implementation": "0xA479c00cDa8C07bce458D7a826C7b091672EB92C",
"proxy": null,
"runtimeHash": "3f13acd82e366c4a32dd279d795ad0fd3a60b0b5b3e672ca82ca20e3371edce2"
}
}
}
14 changes: 14 additions & 0 deletions packages/legacy/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "@graphprotocol/legacy",
"version": "0.0.0",
"private": true,
"description": "Archive of the Sourcify-verified source of The Graph's mainnet implementations",
"license": "GPL-2.0-or-later",
"scripts": {
"test:self": "node --test 'scripts/*.test.mjs' && node scripts/verify.mjs"
},
"devDependencies": {
"solc-0.7.4": "npm:solc@0.7.4",
"solc-0.7.6": "npm:solc@0.7.6"
}
}
51 changes: 51 additions & 0 deletions packages/legacy/scripts/fetch-verified.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env node
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'

import { sha256 } from './lib.mjs'

const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
const SOURCIFY = 'https://sourcify.dev/server/v2/contract/1'

async function fetchBundle(implementation) {
const response = await fetch(`${SOURCIFY}/${implementation}?fields=stdJsonInput,compilation`)
if (!response.ok) throw new Error(`sourcify ${implementation}: HTTP ${response.status}`)
return response.json()
}

function writeBundle(name, { match, stdJsonInput, compilation }) {
if (!match) throw new Error(`${name}: not verified on Sourcify`)
const dir = path.join(root, 'verified', name)
const sourcesDir = path.join(dir, 'sources')
fs.rmSync(dir, { recursive: true, force: true })

const sourceHashes = {}
for (const [file, { content }] of Object.entries(stdJsonInput.sources)) {
const dest = path.resolve(sourcesDir, file)
if (!dest.startsWith(sourcesDir + path.sep)) throw new Error(`${name}: unsafe source path ${file}`)
fs.mkdirSync(path.dirname(dest), { recursive: true })
fs.writeFileSync(dest, content)
sourceHashes[file] = sha256(fs.readFileSync(dest))
}

const { outputSelection: _, ...settings } = stdJsonInput.settings
const split = compilation.fullyQualifiedName.lastIndexOf(':')
const compiler = {
version: compilation.compilerVersion,
settings,
target: {
source: compilation.fullyQualifiedName.slice(0, split),
name: compilation.fullyQualifiedName.slice(split + 1),
},
sourcifyMatch: match,
sourceHashes,
}
fs.writeFileSync(path.join(dir, 'compiler.json'), JSON.stringify(compiler, null, 2) + '\n')
console.log(`${name}: ${Object.keys(sourceHashes).length} files, ${match}, ${compiler.version}`)
}

const targets = JSON.parse(fs.readFileSync(path.join(root, 'targets.json'), 'utf8'))
for (const [name, { implementation }] of Object.entries(targets)) {
writeBundle(name, await fetchBundle(implementation))
}
97 changes: 97 additions & 0 deletions packages/legacy/scripts/lib.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { createHash } from 'node:crypto'
import fs from 'node:fs'
import { createRequire } from 'node:module'
import path from 'node:path'

const require = createRequire(import.meta.url)

export const sha256 = (data) => createHash('sha256').update(data).digest('hex')

export function normalizeRuntime(hex, ranges) {
const code = hex.toLowerCase().replace(/^0x/, '')
const metadataLength = parseInt(code.slice(-4), 16)
const stripped = code.slice(0, code.length - 2 * (metadataLength + 2))
return ranges.reduce((acc, { start, length }) => {
const [from, to] = [2 * start, 2 * (start + length)]
return acc.slice(0, from) + '0'.repeat(to - from) + acc.slice(to)
}, stripped)
}

export function runtimeRanges(deployedBytecode) {
const { immutableReferences = {}, linkReferences = {} } = deployedBytecode
return [
...Object.values(immutableReferences).flat(),
...Object.values(linkReferences)
.flatMap((libs) => Object.values(libs))
.flat(),
].map(({ start, length }) => ({ start, length }))
}

export const runtimeHash = (normalized) => sha256(normalized)

const readCompiler = (dir) => JSON.parse(fs.readFileSync(path.join(dir, 'compiler.json'), 'utf8'))

function listSources(dir) {
const root = path.join(dir, 'sources')
return fs
.readdirSync(root, { recursive: true, withFileTypes: true })
.filter((entry) => entry.isFile())
.map((entry) => path.relative(root, path.join(entry.parentPath, entry.name)).split(path.sep).join('/'))
.sort()
}

export function compileBundle(dir) {
const { version, settings, target } = readCompiler(dir)
const solc = require(`solc-${version.split('+')[0]}`)
if (!solc.version().startsWith(version)) throw new Error(`expected solc ${version}, loaded ${solc.version()}`)

const sources = Object.fromEntries(
listSources(dir).map((file) => [file, { content: fs.readFileSync(path.join(dir, 'sources', file), 'utf8') }]),
)
const outputSelection = {
'*': {
'*': [
'evm.deployedBytecode.object',
'evm.deployedBytecode.immutableReferences',
'evm.deployedBytecode.linkReferences',
'storageLayout',
],
},
}
const input = { language: 'Solidity', sources, settings: { ...settings, outputSelection } }
const output = JSON.parse(solc.compile(JSON.stringify(input)))

const errors = (output.errors ?? []).filter((e) => e.severity === 'error')
if (errors.length) throw new Error(`${dir}:\n${errors.map((e) => e.formattedMessage).join('\n')}`)

const contract = output.contracts?.[target.source]?.[target.name]
if (!contract) throw new Error(`${dir}: no output for ${target.source}:${target.name}`)
return {
object: contract.evm.deployedBytecode.object,
ranges: runtimeRanges(contract.evm.deployedBytecode),
storageLayout: contract.storageLayout,
}
}

export function compareAddresses(targets, contracts) {
return Object.keys(targets)
.filter((name) => name in contracts)
.flatMap((name) =>
['implementation', 'proxy']
.filter((key) => String(targets[name][key]).toLowerCase() !== String(contracts[name][key]).toLowerCase())
.map(
(key) => `${name}: ${key} is ${targets[name][key]} in targets.json, ${contracts[name][key]} in mainnet.json`,
),
)
}

export function checkSources(dir) {
const { sourceHashes } = readCompiler(dir)
const found = new Set(listSources(dir))
const changed = [...found].filter(
(file) =>
!(file in sourceHashes) || sha256(fs.readFileSync(path.join(dir, 'sources', file))) !== sourceHashes[file],
)
const missing = Object.keys(sourceHashes).filter((file) => !found.has(file))
return [...changed, ...missing].sort()
}
105 changes: 105 additions & 0 deletions packages/legacy/scripts/lib.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { test } from 'node:test'
import { fileURLToPath } from 'node:url'

import { checkSources, compareAddresses, compileBundle, normalizeRuntime, runtimeHash, runtimeRanges } from './lib.mjs'

const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
const BRIDGE_ESCROW_DIR = path.join(root, 'verified/BridgeEscrow')

const readMainnet = () => JSON.parse(fs.readFileSync(path.join(root, 'mainnet.json'), 'utf8')).contracts
const targetSource = (dir) => JSON.parse(fs.readFileSync(path.join(dir, 'compiler.json'), 'utf8')).target.source

function copyBundle(t) {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'legacy-test-'))
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }))
const dir = path.join(tmp, 'BridgeEscrow')
fs.cpSync(BRIDGE_ESCROW_DIR, dir, { recursive: true })
return dir
}

function editTarget(dir, edit) {
const file = path.join(dir, 'sources', targetSource(dir))
const before = fs.readFileSync(file, 'utf8')
const after = edit(before)
assert.notEqual(after, before)
fs.writeFileSync(file, after)
}

test('normalizeRuntime zeroes ranges and removes metadata', () => {
// 4 code bytes, then a 2-byte CBOR blob and its 2-byte length
assert.equal(normalizeRuntime('0xAABBCCDD' + 'eeff' + '0002', [{ start: 1, length: 2 }]), 'aa0000dd')
})

test('runtimeRanges reads immutable and link references', () => {
const ranges = runtimeRanges({
immutableReferences: { 1: [{ start: 3, length: 32 }] },
linkReferences: { 'a.sol': { Lib: [{ start: 40, length: 20 }] } },
})
assert.deepEqual(ranges, [
{ start: 3, length: 32 },
{ start: 40, length: 20 },
])
})

test('verified BridgeEscrow matches mainnet.json', () => {
const { object, ranges } = compileBundle(BRIDGE_ESCROW_DIR)
assert.equal(runtimeHash(normalizeRuntime(object, ranges)), readMainnet().BridgeEscrow.runtimeHash)
})

test('a code change no longer matches mainnet.json', (t) => {
const dir = copyBundle(t)
editTarget(dir, (src) =>
src.replace(
/contract BridgeEscrow\b[^{]*\{/,
(m) => `${m} function inserted() external pure returns (uint256) { return 1; }`,
),
)
const { object, ranges } = compileBundle(dir)
assert.notEqual(runtimeHash(normalizeRuntime(object, ranges)), readMainnet().BridgeEscrow.runtimeHash)
})

test('a comment-only change fails the source check', (t) => {
const dir = copyBundle(t)
assert.deepEqual(checkSources(dir), [])
editTarget(dir, (src) => `${src}// comment\n`)
assert.deepEqual(checkSources(dir), [targetSource(dir)])
})

test('checkSources reports missing and extra files', (t) => {
const dir = copyBundle(t)
fs.rmSync(path.join(dir, 'sources', targetSource(dir)))
fs.writeFileSync(path.join(dir, 'sources/extra.sol'), '')
assert.deepEqual(checkSources(dir).sort(), ['extra.sol', targetSource(dir)].sort())
})

test('compareAddresses reports mismatched addresses', () => {
const targets = {
A: { implementation: '0xAbC', proxy: '0xDeF' },
B: { implementation: '0x123', proxy: null },
}
assert.deepEqual(
compareAddresses(targets, {
A: { implementation: '0xabc', proxy: '0xdef', runtimeHash: 'x' },
B: { implementation: '0x123', proxy: null, runtimeHash: 'y' },
}),
[],
)
assert.deepEqual(
compareAddresses(targets, {
A: { implementation: '0xbad', proxy: '0xDeF' },
B: { implementation: '0x123', proxy: '0xbad' },
}),
[
'A: implementation is 0xAbC in targets.json, 0xbad in mainnet.json',
'B: proxy is null in targets.json, 0xbad in mainnet.json',
],
)
})

test('compareAddresses skips contracts missing from either file', () => {
assert.deepEqual(compareAddresses({ A: { implementation: '0x1', proxy: null } }, {}), [])
})
Loading
Loading