Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Commit

Permalink
Handle StarkWare proxy version 5 (#149)
Browse files Browse the repository at this point in the history
* Handle StarkWare proxy version 5

* Format and lint fixes

* Changeset
  • Loading branch information
mateuszradomski authored Mar 11, 2024
1 parent d0a563d commit 9fba046
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 52 deletions.
6 changes: 6 additions & 0 deletions packages/discovery/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# @l2beat/discovery

## 0.44.6

### Patch Changes

- Handle StarkWare proxy version 5

## 0.44.5

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/discovery/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
126 changes: 77 additions & 49 deletions packages/discovery/src/discovery/handlers/user/AccessControlHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>()
Expand All @@ -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)
}
Expand All @@ -57,68 +59,94 @@ export class AccessControlHandler implements ClassicHandler {
blockNumber: number,
): Promise<HandlerResult> {
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<EthereumAddress>
}
> = {}
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<Record<string, AccessControlType>> {
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<EthereumAddress>
} {
// 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<EthereumAddress>
} {
// 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):
Expand Down
31 changes: 29 additions & 2 deletions packages/discovery/src/discovery/proxies/auto/StarkWareProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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(
Expand All @@ -79,6 +80,26 @@ async function getFinalizedState(
return !BigNumber.from(stored.toString()).eq(0)
}

async function getProxyVersion(
provider: DiscoveryProvider,
address: EthereumAddress,
blockNumber: number,
): Promise<Semver | undefined> {
const versionString = await getCallResult<string>(
provider,
address,
'function PROXY_VERSION() view returns (string)',
[],
blockNumber,
)

if (!versionString) {
return undefined
}

return parseSemver(versionString)
}

export async function detectStarkWareProxy(
provider: DiscoveryProvider,
address: EthereumAddress,
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,47 @@ 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'

export async function getProxyGovernance(
provider: DiscoveryProvider,
address: EthereumAddress,
blockNumber: number,
proxyVersion: Semver,
): Promise<EthereumAddress[]> {
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<EthereumAddress[]> {
// 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<EthereumAddress[]> {
const deployer = await provider.getDeployer(address)
if (!deployer) {
Expand Down
34 changes: 34 additions & 0 deletions packages/discovery/src/utils/semver.test.ts
Original file line number Diff line number Diff line change
@@ -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()
})
})
24 changes: 24 additions & 0 deletions packages/discovery/src/utils/semver.ts
Original file line number Diff line number Diff line change
@@ -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 }
}

0 comments on commit 9fba046

Please sign in to comment.