From 9fba046db24deea016183c57c32a30f96e0bd542 Mon Sep 17 00:00:00 2001 From: Mateusz Radomski <33978857+mateuszradomski@users.noreply.github.com> Date: Mon, 11 Mar 2024 11:18:29 +0100 Subject: [PATCH] Handle StarkWare proxy version 5 (#149) * Handle StarkWare proxy version 5 * Format and lint fixes * Changeset --- packages/discovery/CHANGELOG.md | 6 + packages/discovery/package.json | 2 +- .../handlers/user/AccessControlHandler.ts | 126 +++++++++++------- .../discovery/proxies/auto/StarkWareProxy.ts | 31 ++++- .../proxies/auto/StarkWareProxyGovernance.ts | 34 +++++ packages/discovery/src/utils/semver.test.ts | 34 +++++ packages/discovery/src/utils/semver.ts | 24 ++++ 7 files changed, 205 insertions(+), 52 deletions(-) create mode 100644 packages/discovery/src/utils/semver.test.ts create mode 100644 packages/discovery/src/utils/semver.ts diff --git a/packages/discovery/CHANGELOG.md b/packages/discovery/CHANGELOG.md index 3b6a78dc..789f8d35 100644 --- a/packages/discovery/CHANGELOG.md +++ b/packages/discovery/CHANGELOG.md @@ -1,5 +1,11 @@ # @l2beat/discovery +## 0.44.6 + +### Patch Changes + +- Handle StarkWare proxy version 5 + ## 0.44.5 ### Patch Changes diff --git a/packages/discovery/package.json b/packages/discovery/package.json index 68e3f69b..8f6bce3e 100644 --- a/packages/discovery/package.json +++ b/packages/discovery/package.json @@ -1,7 +1,7 @@ { "name": "@l2beat/discovery", "description": "L2Beat discovery - engine & tooling utilized for keeping an eye on L2s", - "version": "0.44.5", + "version": "0.44.6", "main": "dist/index.js", "types": "dist/index.d.ts", "bin": { diff --git a/packages/discovery/src/discovery/handlers/user/AccessControlHandler.ts b/packages/discovery/src/discovery/handlers/user/AccessControlHandler.ts index 15758d8d..a2894ae8 100644 --- a/packages/discovery/src/discovery/handlers/user/AccessControlHandler.ts +++ b/packages/discovery/src/discovery/handlers/user/AccessControlHandler.ts @@ -23,6 +23,8 @@ const abi = new utils.Interface([ 'event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole)', ]) +const DEFAULT_ADMIN_ROLE_BYTES = '0x' + '0'.repeat(64) + export class AccessControlHandler implements ClassicHandler { readonly dependencies: string[] = [] private readonly knownNames = new Map() @@ -33,7 +35,7 @@ export class AccessControlHandler implements ClassicHandler { abi: string[], readonly logger: DiscoveryLogger, ) { - this.knownNames.set('0x' + '0'.repeat(64), 'DEFAULT_ADMIN_ROLE') + this.knownNames.set(DEFAULT_ADMIN_ROLE_BYTES, 'DEFAULT_ADMIN_ROLE') for (const [hash, name] of Object.entries(definition.roleNames ?? {})) { this.knownNames.set(hash, name) } @@ -57,68 +59,94 @@ export class AccessControlHandler implements ClassicHandler { blockNumber: number, ): Promise { this.logger.logExecution(this.field, ['Checking AccessControl']) - const logs = await provider.getLogs( + const unnamedRoles = await fetchAccessControl( + provider, address, - [ - [ - abi.getEventTopic('RoleGranted'), - abi.getEventTopic('RoleRevoked'), - abi.getEventTopic('RoleAdminChanged'), - ], - ], - 0, blockNumber, ) - const roles: Record< - string, - { - adminRole: string - members: Set - } - > = {} + return { + field: this.field, + value: Object.fromEntries( + Object.entries(unnamedRoles).map(([role, { adminRole, members }]) => { + return [ + this.getRoleName(role), + { adminRole: this.getRoleName(adminRole), members }, + ] + }), + ), + ignoreRelative: this.definition.ignoreRelative, + } + } +} - getRole('DEFAULT_ADMIN_ROLE') +export interface AccessControlType { + readonly adminRole: string + readonly members: string[] +} - function getRole(role: string): { +export async function fetchAccessControl( + provider: DiscoveryProvider, + address: EthereumAddress, + blockNumber: number, +): Promise> { + const logs = await provider.getLogs( + address, + [ + [ + abi.getEventTopic('RoleGranted'), + abi.getEventTopic('RoleRevoked'), + abi.getEventTopic('RoleAdminChanged'), + ], + ], + 0, + blockNumber, + ) + + const roles: Record< + string, + { adminRole: string members: Set - } { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - const value = roles[role] ?? { - adminRole: 'DEFAULT_ADMIN_ROLE', - members: new Set(), - } - roles[role] = value - return value } + > = {} - for (const log of logs) { - const parsed = parseRoleLog(log) - const role = getRole(this.getRoleName(parsed.role)) - if (parsed.type === 'RoleAdminChanged') { - role.adminRole = this.getRoleName(parsed.adminRole) - } else if (parsed.type === 'RoleGranted') { - role.members.add(parsed.account) - } else { - role.members.delete(parsed.account) - } + getRole(DEFAULT_ADMIN_ROLE_BYTES) + + function getRole(role: string): { + adminRole: string + members: Set + } { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + const value = roles[role] ?? { + adminRole: DEFAULT_ADMIN_ROLE_BYTES, + members: new Set(), } + roles[role] = value + return value + } - return { - field: this.field, - value: Object.fromEntries( - Object.entries(roles).map(([role, config]) => [ - role, - { - adminRole: config.adminRole, - members: [...config.members].map((x) => x.toString()), - }, - ]), - ), - ignoreRelative: this.definition.ignoreRelative, + for (const log of logs) { + const parsed = parseRoleLog(log) + const role = getRole(parsed.role) + if (parsed.type === 'RoleAdminChanged') { + role.adminRole = parsed.adminRole + } else if (parsed.type === 'RoleGranted') { + role.members.add(parsed.account) + } else { + role.members.delete(parsed.account) } } + + return Object.fromEntries( + Object.entries(roles).map(([role, config]) => [ + role, + { + adminRole: config.adminRole, + members: [...config.members].map((x) => x.toString()), + }, + ]), + ) } function parseRoleLog(log: providers.Log): diff --git a/packages/discovery/src/discovery/proxies/auto/StarkWareProxy.ts b/packages/discovery/src/discovery/proxies/auto/StarkWareProxy.ts index 4d00a922..287d9851 100644 --- a/packages/discovery/src/discovery/proxies/auto/StarkWareProxy.ts +++ b/packages/discovery/src/discovery/proxies/auto/StarkWareProxy.ts @@ -4,6 +4,7 @@ import { BigNumber, utils } from 'ethers' import { Bytes } from '../../../utils/Bytes' import { EthereumAddress } from '../../../utils/EthereumAddress' import { Hash256 } from '../../../utils/Hash256' +import { parseSemver, Semver } from '../../../utils/semver' import { DiscoveryProvider } from '../../provider/DiscoveryProvider' import { bytes32ToAddress } from '../../utils/address' import { getCallResult } from '../../utils/getCallResult' @@ -63,7 +64,7 @@ async function getUpgradeDelay( // Web3.solidityKeccak(['string'], ["StarkWare2019.finalization-flag-slot"]). const FINALIZED_STATE_SLOT = Bytes.fromHex( - '0x7184681641399eb4ad2fdb92114857ee6ff239f94ad635a1779978947b8843be', + '0x7d433c6f837e8f93009937c466c82efbb5ba621fae36886d0cac433c5d0aa7d2', ) async function getFinalizedState( @@ -79,6 +80,26 @@ async function getFinalizedState( return !BigNumber.from(stored.toString()).eq(0) } +async function getProxyVersion( + provider: DiscoveryProvider, + address: EthereumAddress, + blockNumber: number, +): Promise { + const versionString = await getCallResult( + provider, + address, + 'function PROXY_VERSION() view returns (string)', + [], + blockNumber, + ) + + if (!versionString) { + return undefined + } + + return parseSemver(versionString) +} + export async function detectStarkWareProxy( provider: DiscoveryProvider, address: EthereumAddress, @@ -88,12 +109,18 @@ export async function detectStarkWareProxy( if (implementation === EthereumAddress.ZERO) { return } + + const proxyVersion = await getProxyVersion(provider, address, blockNumber) + if (!proxyVersion) { + return undefined + } + const [callImplementation, upgradeDelay, isFinal, proxyGovernance] = await Promise.all([ getCallImplementation(provider, address, blockNumber), getUpgradeDelay(provider, address, blockNumber), getFinalizedState(provider, address, blockNumber), - getProxyGovernance(provider, address, blockNumber), + getProxyGovernance(provider, address, blockNumber, proxyVersion), ]) const diamond = await getStarkWareDiamond( diff --git a/packages/discovery/src/discovery/proxies/auto/StarkWareProxyGovernance.ts b/packages/discovery/src/discovery/proxies/auto/StarkWareProxyGovernance.ts index 7f513b4c..fb48c19e 100644 --- a/packages/discovery/src/discovery/proxies/auto/StarkWareProxyGovernance.ts +++ b/packages/discovery/src/discovery/proxies/auto/StarkWareProxyGovernance.ts @@ -2,6 +2,8 @@ import { assert } from '@l2beat/backend-tools' import { utils } from 'ethers' import { EthereumAddress } from '../../../utils/EthereumAddress' +import { Semver } from '../../../utils/semver' +import { fetchAccessControl } from '../../handlers/user/AccessControlHandler' import { DiscoveryProvider } from '../../provider/DiscoveryProvider' import { getCallResult } from '../../utils/getCallResult' @@ -9,6 +11,38 @@ export async function getProxyGovernance( provider: DiscoveryProvider, address: EthereumAddress, blockNumber: number, + proxyVersion: Semver, +): Promise { + if (proxyVersion.major === 5) { + return getProxyGovernanceV5(provider, address, blockNumber) + } else if (proxyVersion.major <= 4) { + return getProxyGovernanceV4Down(provider, address, blockNumber) + } else { + throw new Error('Unsupported proxy version') + } +} + +async function getProxyGovernanceV5( + provider: DiscoveryProvider, + address: EthereumAddress, + blockNumber: number, +): Promise { + // int.from_bytes(Web3.keccak(text="ROLE_UPGRADE_GOVERNOR"), "big") & MASK_250 . + const UPGRADE_GOVERNOR_HASH = + '0x0251e864ca2a080f55bce5da2452e8cfcafdbc951a3e7fff5023d558452ec228' + const unnamedRoles = await fetchAccessControl(provider, address, blockNumber) + + return ( + unnamedRoles[UPGRADE_GOVERNOR_HASH]?.members.map((address) => + EthereumAddress(address), + ) ?? [] + ) +} + +async function getProxyGovernanceV4Down( + provider: DiscoveryProvider, + address: EthereumAddress, + blockNumber: number, ): Promise { const deployer = await provider.getDeployer(address) if (!deployer) { diff --git a/packages/discovery/src/utils/semver.test.ts b/packages/discovery/src/utils/semver.test.ts new file mode 100644 index 00000000..791f10e2 --- /dev/null +++ b/packages/discovery/src/utils/semver.test.ts @@ -0,0 +1,34 @@ +import { expect } from 'earl' + +import { parseSemver } from './semver' + +describe(parseSemver.name, () => { + it('should parse a version', () => { + expect(parseSemver('1.2.3')).toEqual({ major: 1, minor: 2, patch: 3 }) + expect(parseSemver('0.0.0')).toEqual({ major: 0, minor: 0, patch: 0 }) + expect(parseSemver('1.0.0')).toEqual({ major: 1, minor: 0, patch: 0 }) + expect(parseSemver('0.1.0')).toEqual({ major: 0, minor: 1, patch: 0 }) + expect(parseSemver('0.0.1')).toEqual({ major: 0, minor: 0, patch: 1 }) + expect(parseSemver('999.999.999')).toEqual({ + major: 999, + minor: 999, + patch: 999, + }) + }) + + it('should throw on invalid characters in the version string', () => { + expect(() => parseSemver('1.2.3!')).toThrow() + }) + + it('should handle leading zeros in the version string', () => { + expect(parseSemver('1.02.03')).toEqual({ major: 1, minor: 2, patch: 3 }) + expect(parseSemver('01.02.03')).toEqual({ major: 1, minor: 2, patch: 3 }) + }) + + it('throws on invalid semantic version string', () => { + expect(() => parseSemver('1.2')).toThrow() + expect(() => parseSemver('1.a.3')).toThrow() + expect(() => parseSemver('1.2.3.4')).toThrow() + expect(() => parseSemver('')).toThrow() + }) +}) diff --git a/packages/discovery/src/utils/semver.ts b/packages/discovery/src/utils/semver.ts new file mode 100644 index 00000000..7d7cbe05 --- /dev/null +++ b/packages/discovery/src/utils/semver.ts @@ -0,0 +1,24 @@ +import { assert } from '@l2beat/backend-tools' + +export interface Semver { + major: number + minor: number + patch: number +} + +export function parseSemver(version: string): Semver { + const numbers = version.split('.').map(Number) + + assert(numbers.length === 3, 'Invalid semantic version string') + assert( + numbers.every((n) => !isNaN(n)), + 'Invalid semantic version string', + ) + + const [major, minor, patch] = numbers + assert(major !== undefined, 'Failed to parse major version') + assert(minor !== undefined, 'Failed to parse minor version') + assert(patch !== undefined, 'Failed to parse patch version') + + return { major, minor, patch } +}