@1001-digital/proxies 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +276 -0
- package/dist/index.d.ts +353 -0
- package/dist/index.js +442 -0
- package/package.json +56 -0
- package/src/abi.ts +72 -0
- package/src/constants.ts +42 -0
- package/src/decode.ts +103 -0
- package/src/detect.ts +60 -0
- package/src/enrich.ts +36 -0
- package/src/errors.ts +33 -0
- package/src/index.ts +122 -0
- package/src/patterns/diamond.ts +77 -0
- package/src/patterns/eip1167.ts +45 -0
- package/src/patterns/eip1822.ts +34 -0
- package/src/patterns/eip1967-beacon.ts +43 -0
- package/src/patterns/eip1967.ts +44 -0
- package/src/patterns/eip897.ts +35 -0
- package/src/patterns/safe.ts +37 -0
- package/src/rpc.ts +93 -0
- package/src/selector.ts +37 -0
- package/src/types.ts +131 -0
package/src/decode.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { ProxiesDecodeError } from './errors'
|
|
2
|
+
|
|
3
|
+
// Sanity limits to reject malformed data that accidentally decodes.
|
|
4
|
+
// Real diamonds have <100 facets and <200 selectors/facet; bound malformed-input cost.
|
|
5
|
+
const MAX_FACETS = 200
|
|
6
|
+
const MAX_SELECTORS_PER_FACET = 1000
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Literal on-chain facet tuple (`(address, bytes4[])`) — the shape returned by
|
|
10
|
+
* the ERC-2535 loupe `facets()` call. Use {@link decodeFacets} to parse it.
|
|
11
|
+
*/
|
|
12
|
+
export interface DecodedFacet {
|
|
13
|
+
facetAddress: string
|
|
14
|
+
functionSelectors: string[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Decode the return value of `facets()` — type `(address, bytes4[])[]`.
|
|
19
|
+
* Throws {@link ProxiesDecodeError} on malformed input.
|
|
20
|
+
*/
|
|
21
|
+
export function decodeFacets(hex: string): DecodedFacet[] {
|
|
22
|
+
const h = hex.startsWith('0x') ? hex.slice(2) : hex
|
|
23
|
+
const W = 64 // one 32-byte word in hex chars
|
|
24
|
+
|
|
25
|
+
const readWord = (pos: number): string => {
|
|
26
|
+
if (pos < 0 || pos + W > h.length) {
|
|
27
|
+
throw new ProxiesDecodeError('malformed facets() return: out of bounds')
|
|
28
|
+
}
|
|
29
|
+
return h.slice(pos, pos + W)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const readUint = (pos: number): number => {
|
|
33
|
+
const word = readWord(pos)
|
|
34
|
+
// Upper 24 bytes must be zero for the value to fit safely in a JS number
|
|
35
|
+
if (!/^0{48}/.test(word)) {
|
|
36
|
+
throw new ProxiesDecodeError('malformed facets() return: value too large')
|
|
37
|
+
}
|
|
38
|
+
return parseInt(word, 16)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const outerOff = readUint(0) * 2
|
|
42
|
+
const n = readUint(outerOff)
|
|
43
|
+
if (n > MAX_FACETS) {
|
|
44
|
+
throw new ProxiesDecodeError(`malformed facets() return: ${n} facets exceeds limit`)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const head = outerOff + W
|
|
48
|
+
const facets: DecodedFacet[] = []
|
|
49
|
+
|
|
50
|
+
for (let i = 0; i < n; i++) {
|
|
51
|
+
const tupleOff = readUint(head + i * W) * 2
|
|
52
|
+
const tx = head + tupleOff
|
|
53
|
+
|
|
54
|
+
const addrWord = readWord(tx)
|
|
55
|
+
if (!/^0{24}/.test(addrWord)) {
|
|
56
|
+
throw new ProxiesDecodeError('malformed facets() return: invalid address')
|
|
57
|
+
}
|
|
58
|
+
const facetAddress = '0x' + addrWord.slice(24).toLowerCase()
|
|
59
|
+
|
|
60
|
+
const selOff = readUint(tx + W) * 2
|
|
61
|
+
const selStart = tx + selOff
|
|
62
|
+
|
|
63
|
+
const m = readUint(selStart)
|
|
64
|
+
if (m > MAX_SELECTORS_PER_FACET) {
|
|
65
|
+
throw new ProxiesDecodeError(
|
|
66
|
+
`malformed facets() return: ${m} selectors exceeds limit`,
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const selectors: string[] = []
|
|
71
|
+
for (let j = 0; j < m; j++) {
|
|
72
|
+
const slot = readWord(selStart + W + j * W)
|
|
73
|
+
selectors.push('0x' + slot.slice(0, 8).toLowerCase())
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
facets.push({ facetAddress, functionSelectors: selectors })
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return facets
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Parse a 32-byte ABI-encoded bool. Returns `null` if the response is not a
|
|
84
|
+
* well-formed bool (wrong length, or non-zero high bits).
|
|
85
|
+
*/
|
|
86
|
+
export function parseBool(hex: string): boolean | null {
|
|
87
|
+
if (hex.length !== 66) return null
|
|
88
|
+
const body = hex.slice(2).toLowerCase()
|
|
89
|
+
if (!/^0{63}[01]$/.test(body)) return null
|
|
90
|
+
return body.slice(-1) === '1'
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Parse a 32-byte ABI-encoded address (right-padded). Returns the lowercase
|
|
95
|
+
* `0x` address, or `null` if the payload is not a well-formed address (wrong
|
|
96
|
+
* length, or non-zero high bytes).
|
|
97
|
+
*/
|
|
98
|
+
export function parseAddress(hex: string): string | null {
|
|
99
|
+
if (hex.length !== 66) return null
|
|
100
|
+
const body = hex.slice(2).toLowerCase()
|
|
101
|
+
if (!/^0{24}[0-9a-f]{40}$/.test(body)) return null
|
|
102
|
+
return '0x' + body.slice(24)
|
|
103
|
+
}
|
package/src/detect.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { detectDiamond } from './patterns/diamond'
|
|
2
|
+
import { detectEip1167 } from './patterns/eip1167'
|
|
3
|
+
import { detectEip1822 } from './patterns/eip1822'
|
|
4
|
+
import { detectEip1967 } from './patterns/eip1967'
|
|
5
|
+
import { detectEip1967Beacon } from './patterns/eip1967-beacon'
|
|
6
|
+
import { detectEip897 } from './patterns/eip897'
|
|
7
|
+
import { detectGnosisSafe } from './patterns/safe'
|
|
8
|
+
import type { RawProxy } from './types'
|
|
9
|
+
|
|
10
|
+
type Detector = (
|
|
11
|
+
rpc: string,
|
|
12
|
+
address: string,
|
|
13
|
+
fetchFn: typeof globalThis.fetch,
|
|
14
|
+
) => Promise<RawProxy | null>
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Priority order used by {@link detectProxy}. First detector that returns
|
|
18
|
+
* non-null wins. Diamonds come first because the loupe probe is a specific,
|
|
19
|
+
* low-false-positive signal; standard storage slots follow; Safe's slot-0
|
|
20
|
+
* convention and EIP-897's `implementation()` view function come last.
|
|
21
|
+
*/
|
|
22
|
+
const DETECTORS: Detector[] = [
|
|
23
|
+
detectDiamond,
|
|
24
|
+
detectEip1967,
|
|
25
|
+
detectEip1967Beacon,
|
|
26
|
+
detectEip1822,
|
|
27
|
+
detectEip1167,
|
|
28
|
+
detectGnosisSafe,
|
|
29
|
+
detectEip897,
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Detect any supported proxy pattern at `address`.
|
|
34
|
+
*
|
|
35
|
+
* Tries the built-in detectors in priority order (`eip-2535-diamond → eip-1967
|
|
36
|
+
* → eip-1967-beacon → eip-1822 → eip-1167 → gnosis-safe → eip-897`). Returns
|
|
37
|
+
* the first match, or `null` when no pattern matches.
|
|
38
|
+
*
|
|
39
|
+
* Each detector runs with error isolation — an RPC failure in one pattern does
|
|
40
|
+
* not poison the probe for subsequent ones.
|
|
41
|
+
*
|
|
42
|
+
* Single-hop only: if the resolved implementation is itself a proxy, the
|
|
43
|
+
* result describes the direct pattern only. Re-run detection on the resolved
|
|
44
|
+
* implementation to chain manually.
|
|
45
|
+
*/
|
|
46
|
+
export async function detectProxy(
|
|
47
|
+
rpc: string,
|
|
48
|
+
address: string,
|
|
49
|
+
fetchFn: typeof globalThis.fetch,
|
|
50
|
+
): Promise<RawProxy | null> {
|
|
51
|
+
for (const detect of DETECTORS) {
|
|
52
|
+
try {
|
|
53
|
+
const match = await detect(rpc, address, fetchFn)
|
|
54
|
+
if (match) return match
|
|
55
|
+
} catch {
|
|
56
|
+
// Detector-level errors shouldn't block later detectors
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return null
|
|
60
|
+
}
|
package/src/enrich.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { filterAbiBySelectors } from './abi'
|
|
2
|
+
import type { EnrichedTarget, ResolvedTarget, TargetEnricher } from './types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Apply an enricher to each resolved target, producing an `EnrichedTarget[]`.
|
|
6
|
+
*
|
|
7
|
+
* Enricher errors are swallowed per-target — one bad fetch does not fail the
|
|
8
|
+
* whole resolution. Pass `null` to skip enrichment entirely (targets will
|
|
9
|
+
* carry only `address` + `selectors`).
|
|
10
|
+
*
|
|
11
|
+
* When a target has `selectors` defined (diamond facets), the returned ABI is
|
|
12
|
+
* filtered to those selectors. When `selectors` is undefined (any single-impl
|
|
13
|
+
* proxy pattern), the full implementation ABI is passed through untouched.
|
|
14
|
+
*/
|
|
15
|
+
export async function enrichTargets(
|
|
16
|
+
targets: ResolvedTarget[],
|
|
17
|
+
enrich: TargetEnricher | null,
|
|
18
|
+
): Promise<EnrichedTarget[]> {
|
|
19
|
+
const enrichments = enrich
|
|
20
|
+
? await Promise.all(
|
|
21
|
+
targets.map(t => enrich(t.address).catch(() => null)),
|
|
22
|
+
)
|
|
23
|
+
: targets.map(() => null)
|
|
24
|
+
|
|
25
|
+
return targets.map((t, i) => {
|
|
26
|
+
const src = enrichments[i]
|
|
27
|
+
const info: EnrichedTarget = { address: t.address }
|
|
28
|
+
if (t.selectors !== undefined) info.selectors = t.selectors
|
|
29
|
+
if (src?.abi) {
|
|
30
|
+
info.abi = t.selectors !== undefined
|
|
31
|
+
? filterAbiBySelectors(src.abi, t.selectors)
|
|
32
|
+
: src.abi
|
|
33
|
+
}
|
|
34
|
+
return info
|
|
35
|
+
})
|
|
36
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export function errorMessage(error: unknown): string {
|
|
2
|
+
return error instanceof Error ? error.message : String(error)
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export class ProxiesError extends Error {
|
|
6
|
+
constructor(message: string, options?: ErrorOptions) {
|
|
7
|
+
super(message, options)
|
|
8
|
+
this.name = 'ProxiesError'
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Raised when the on-chain `facets()` return value cannot be decoded. */
|
|
13
|
+
export class ProxiesDecodeError extends ProxiesError {
|
|
14
|
+
constructor(message: string, options?: ErrorOptions) {
|
|
15
|
+
super(message, options)
|
|
16
|
+
this.name = 'ProxiesDecodeError'
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Raised when a JSON-RPC request fails at the transport level. */
|
|
21
|
+
export class ProxiesFetchError extends ProxiesError {
|
|
22
|
+
public readonly status: number
|
|
23
|
+
|
|
24
|
+
constructor(
|
|
25
|
+
message: string,
|
|
26
|
+
details: { status: number },
|
|
27
|
+
options?: ErrorOptions,
|
|
28
|
+
) {
|
|
29
|
+
super(message, options)
|
|
30
|
+
this.name = 'ProxiesFetchError'
|
|
31
|
+
this.status = details.status
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { buildCompositeAbi } from './abi'
|
|
2
|
+
import { detectProxy } from './detect'
|
|
3
|
+
import { enrichTargets } from './enrich'
|
|
4
|
+
import type {
|
|
5
|
+
FetchProxyOptions,
|
|
6
|
+
Proxy,
|
|
7
|
+
ProxiesClient,
|
|
8
|
+
ProxiesConfig,
|
|
9
|
+
TargetEnricher,
|
|
10
|
+
} from './types'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create a proxy inspection client.
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* const proxies = createProxies()
|
|
17
|
+
* const result = await proxies.fetch('https://rpc…', '0x…')
|
|
18
|
+
* if (result) console.log(result.pattern, result.targets)
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* Pass `config.enrich` to populate each target's ABI from any source
|
|
22
|
+
* (Sourcify, Etherscan, a local cache). Omit it to get raw targets with
|
|
23
|
+
* address + (optional) selectors only.
|
|
24
|
+
*/
|
|
25
|
+
export function createProxies(config: ProxiesConfig = {}): ProxiesClient {
|
|
26
|
+
const fetchFn = config.fetch ?? globalThis.fetch
|
|
27
|
+
const defaultEnrich = config.enrich ?? null
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
detect(rpc, address) {
|
|
31
|
+
return detectProxy(rpc, address, fetchFn)
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
async fetch(rpc, address, options): Promise<Proxy | null> {
|
|
35
|
+
const raw = await detectProxy(rpc, address, fetchFn)
|
|
36
|
+
if (!raw) return null
|
|
37
|
+
|
|
38
|
+
const enricher: TargetEnricher | null = options?.enrich === false
|
|
39
|
+
? null
|
|
40
|
+
: (options?.enrich ?? defaultEnrich)
|
|
41
|
+
|
|
42
|
+
const targets = await enrichTargets(raw.targets, enricher)
|
|
43
|
+
|
|
44
|
+
const abiLayers = targets
|
|
45
|
+
.map(t => t.abi)
|
|
46
|
+
.filter((a): a is unknown[] => a !== undefined)
|
|
47
|
+
const compositeAbi = abiLayers.length > 0
|
|
48
|
+
? buildCompositeAbi(abiLayers)
|
|
49
|
+
: undefined
|
|
50
|
+
|
|
51
|
+
const proxy: Proxy = { pattern: raw.pattern, targets }
|
|
52
|
+
if (raw.beacon) proxy.beacon = raw.beacon
|
|
53
|
+
if (raw.admin) proxy.admin = raw.admin
|
|
54
|
+
if (compositeAbi) proxy.compositeAbi = compositeAbi
|
|
55
|
+
return proxy
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── Detection ──
|
|
61
|
+
|
|
62
|
+
export { detectProxy } from './detect'
|
|
63
|
+
export { detectDiamond } from './patterns/diamond'
|
|
64
|
+
export { detectEip1967 } from './patterns/eip1967'
|
|
65
|
+
export { detectEip1967Beacon } from './patterns/eip1967-beacon'
|
|
66
|
+
export { detectEip1822 } from './patterns/eip1822'
|
|
67
|
+
export { detectEip1167 } from './patterns/eip1167'
|
|
68
|
+
export { detectGnosisSafe } from './patterns/safe'
|
|
69
|
+
export { detectEip897 } from './patterns/eip897'
|
|
70
|
+
|
|
71
|
+
// ── Composition ──
|
|
72
|
+
|
|
73
|
+
export { enrichTargets } from './enrich'
|
|
74
|
+
export { filterAbiBySelectors, buildCompositeAbi } from './abi'
|
|
75
|
+
|
|
76
|
+
// ── Utilities ──
|
|
77
|
+
|
|
78
|
+
export { decodeFacets, parseAddress } from './decode'
|
|
79
|
+
export { computeSelector, canonicalSignature } from './selector'
|
|
80
|
+
|
|
81
|
+
// ── RPC ──
|
|
82
|
+
|
|
83
|
+
export { ethCall, ethGetStorageAt, ethGetCode } from './rpc'
|
|
84
|
+
|
|
85
|
+
// ── Constants ──
|
|
86
|
+
|
|
87
|
+
export {
|
|
88
|
+
SUPPORTS_INTERFACE_SELECTOR,
|
|
89
|
+
DIAMOND_LOUPE_INTERFACE_ID,
|
|
90
|
+
FACETS_SELECTOR,
|
|
91
|
+
IMPLEMENTATION_SELECTOR,
|
|
92
|
+
EIP1967_IMPL_SLOT,
|
|
93
|
+
EIP1967_BEACON_SLOT,
|
|
94
|
+
EIP1967_ADMIN_SLOT,
|
|
95
|
+
EIP1822_PROXIABLE_SLOT,
|
|
96
|
+
EIP1167_BYTECODE_PREFIX,
|
|
97
|
+
EIP1167_BYTECODE_SUFFIX,
|
|
98
|
+
ZERO_ADDRESS,
|
|
99
|
+
} from './constants'
|
|
100
|
+
|
|
101
|
+
// ── Errors ──
|
|
102
|
+
|
|
103
|
+
export { ProxiesError, ProxiesDecodeError, ProxiesFetchError } from './errors'
|
|
104
|
+
|
|
105
|
+
// ── Types ──
|
|
106
|
+
|
|
107
|
+
export type {
|
|
108
|
+
ProxiesConfig,
|
|
109
|
+
ProxiesClient,
|
|
110
|
+
FetchProxyOptions,
|
|
111
|
+
ProxyPattern,
|
|
112
|
+
ResolvedTarget,
|
|
113
|
+
RawProxy,
|
|
114
|
+
EnrichedTarget,
|
|
115
|
+
Proxy,
|
|
116
|
+
TargetEnrichment,
|
|
117
|
+
TargetEnricher,
|
|
118
|
+
AbiParam,
|
|
119
|
+
AbiFunctionLike,
|
|
120
|
+
} from './types'
|
|
121
|
+
|
|
122
|
+
export type { DecodedFacet } from './decode'
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DIAMOND_LOUPE_INTERFACE_ID,
|
|
3
|
+
FACETS_SELECTOR,
|
|
4
|
+
SUPPORTS_INTERFACE_SELECTOR,
|
|
5
|
+
ZERO_ADDRESS,
|
|
6
|
+
} from '../constants'
|
|
7
|
+
import { decodeFacets, parseBool } from '../decode'
|
|
8
|
+
import { ethCall } from '../rpc'
|
|
9
|
+
import type { DecodedFacet } from '../decode'
|
|
10
|
+
import type { RawProxy, ResolvedTarget } from '../types'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Detect whether a contract implements ERC-2535 (Diamond) and return its
|
|
14
|
+
* facets as a {@link RawProxy}. Returns `null` if not a diamond.
|
|
15
|
+
*
|
|
16
|
+
* Strategy:
|
|
17
|
+
* 1. Try ERC-165 `supportsInterface(0x48e2b093)`.
|
|
18
|
+
* - Valid bool `true` → fetch and return facets
|
|
19
|
+
* - Valid bool `false` → definitively not a diamond (null)
|
|
20
|
+
* - Malformed / error → fall through to step 2
|
|
21
|
+
* 2. Probe `facets()` directly. If it returns a non-empty decoded array,
|
|
22
|
+
* treat the contract as a diamond.
|
|
23
|
+
*
|
|
24
|
+
* Zero-address facets (deleted selectors) are filtered out of the result.
|
|
25
|
+
*/
|
|
26
|
+
export async function detectDiamond(
|
|
27
|
+
rpc: string,
|
|
28
|
+
address: string,
|
|
29
|
+
fetchFn: typeof globalThis.fetch,
|
|
30
|
+
): Promise<RawProxy | null> {
|
|
31
|
+
const calldata = SUPPORTS_INTERFACE_SELECTOR
|
|
32
|
+
+ DIAMOND_LOUPE_INTERFACE_ID.slice(2).padEnd(64, '0')
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const res = await ethCall(rpc, address, calldata, fetchFn)
|
|
36
|
+
const bool = parseBool(res)
|
|
37
|
+
if (bool === true) return tryFacets(rpc, address, fetchFn)
|
|
38
|
+
if (bool === false) return null
|
|
39
|
+
// Malformed response — fall through to facets() probe
|
|
40
|
+
} catch {
|
|
41
|
+
// RPC/revert error — fall through to facets() probe
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return tryFacets(rpc, address, fetchFn)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Minimum valid `facets()` payload: outer offset word + length word = 2 × 32
|
|
48
|
+
// bytes → 128 hex chars + '0x' prefix.
|
|
49
|
+
const MIN_FACETS_PAYLOAD_LEN = 130
|
|
50
|
+
|
|
51
|
+
async function tryFacets(
|
|
52
|
+
rpc: string,
|
|
53
|
+
address: string,
|
|
54
|
+
fetchFn: typeof globalThis.fetch,
|
|
55
|
+
): Promise<RawProxy | null> {
|
|
56
|
+
let res: string
|
|
57
|
+
try {
|
|
58
|
+
res = await ethCall(rpc, address, FACETS_SELECTOR, fetchFn)
|
|
59
|
+
} catch {
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (res === '0x' || res.length < MIN_FACETS_PAYLOAD_LEN) return null
|
|
64
|
+
|
|
65
|
+
let facets: DecodedFacet[]
|
|
66
|
+
try {
|
|
67
|
+
facets = decodeFacets(res)
|
|
68
|
+
} catch {
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const targets: ResolvedTarget[] = facets
|
|
73
|
+
.filter(f => f.facetAddress !== ZERO_ADDRESS)
|
|
74
|
+
.map(f => ({ address: f.facetAddress, selectors: f.functionSelectors }))
|
|
75
|
+
|
|
76
|
+
return targets.length > 0 ? { pattern: 'eip-2535-diamond', targets } : null
|
|
77
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EIP1167_BYTECODE_PREFIX,
|
|
3
|
+
EIP1167_BYTECODE_SUFFIX,
|
|
4
|
+
ZERO_ADDRESS,
|
|
5
|
+
} from '../constants'
|
|
6
|
+
import { ethGetCode } from '../rpc'
|
|
7
|
+
import type { RawProxy } from '../types'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Detect an EIP-1167 minimal proxy (clone) from runtime bytecode.
|
|
11
|
+
*
|
|
12
|
+
* Matches the canonical 45-byte runtime:
|
|
13
|
+
* `363d3d373d3d3d363d73<20-byte impl>5af43d82803e903d91602b57fd5bf3`
|
|
14
|
+
*
|
|
15
|
+
* Does NOT match the "optimized" variants (e.g. push-based relays) that some
|
|
16
|
+
* tooling emits — this is the standards-compliant shape per EIP-1167.
|
|
17
|
+
*
|
|
18
|
+
* Returns `null` when the bytecode does not match.
|
|
19
|
+
*/
|
|
20
|
+
export async function detectEip1167(
|
|
21
|
+
rpc: string,
|
|
22
|
+
address: string,
|
|
23
|
+
fetchFn: typeof globalThis.fetch,
|
|
24
|
+
): Promise<RawProxy | null> {
|
|
25
|
+
let code: string
|
|
26
|
+
try {
|
|
27
|
+
code = await ethGetCode(rpc, address, fetchFn)
|
|
28
|
+
} catch {
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const body = code.toLowerCase().replace(/^0x/, '')
|
|
33
|
+
// 20 prefix + 40 address + 30 suffix = 90 hex chars
|
|
34
|
+
if (body.length !== 90) return null
|
|
35
|
+
if (!body.startsWith(EIP1167_BYTECODE_PREFIX)) return null
|
|
36
|
+
if (!body.endsWith(EIP1167_BYTECODE_SUFFIX)) return null
|
|
37
|
+
|
|
38
|
+
const impl = '0x' + body.slice(EIP1167_BYTECODE_PREFIX.length, EIP1167_BYTECODE_PREFIX.length + 40)
|
|
39
|
+
if (impl === ZERO_ADDRESS) return null
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
pattern: 'eip-1167',
|
|
43
|
+
targets: [{ address: impl }],
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { EIP1822_PROXIABLE_SLOT, ZERO_ADDRESS } from '../constants'
|
|
2
|
+
import { parseAddress } from '../decode'
|
|
3
|
+
import { ethGetStorageAt } from '../rpc'
|
|
4
|
+
import type { RawProxy } from '../types'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Detect an EIP-1822 UUPS proxy.
|
|
8
|
+
*
|
|
9
|
+
* Reads the PROXIABLE slot (`keccak256('PROXIABLE')`). Returns `null` when the
|
|
10
|
+
* slot is empty or malformed.
|
|
11
|
+
*
|
|
12
|
+
* Note: most modern UUPS proxies use the EIP-1967 slot instead — this detector
|
|
13
|
+
* only catches the older EIP-1822 convention.
|
|
14
|
+
*/
|
|
15
|
+
export async function detectEip1822(
|
|
16
|
+
rpc: string,
|
|
17
|
+
address: string,
|
|
18
|
+
fetchFn: typeof globalThis.fetch,
|
|
19
|
+
): Promise<RawProxy | null> {
|
|
20
|
+
let raw: string
|
|
21
|
+
try {
|
|
22
|
+
raw = await ethGetStorageAt(rpc, address, EIP1822_PROXIABLE_SLOT, fetchFn)
|
|
23
|
+
} catch {
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const impl = parseAddress(raw)
|
|
28
|
+
if (!impl || impl === ZERO_ADDRESS) return null
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
pattern: 'eip-1822',
|
|
32
|
+
targets: [{ address: impl }],
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { EIP1967_BEACON_SLOT, IMPLEMENTATION_SELECTOR, ZERO_ADDRESS } from '../constants'
|
|
2
|
+
import { parseAddress } from '../decode'
|
|
3
|
+
import { ethCall, ethGetStorageAt } from '../rpc'
|
|
4
|
+
import type { RawProxy } from '../types'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Detect an EIP-1967 beacon proxy.
|
|
8
|
+
*
|
|
9
|
+
* Reads the beacon slot (`0xa3f0…750d`); if non-zero, calls `implementation()`
|
|
10
|
+
* on the beacon contract. Returns `null` when the slot is empty or the beacon
|
|
11
|
+
* call fails.
|
|
12
|
+
*/
|
|
13
|
+
export async function detectEip1967Beacon(
|
|
14
|
+
rpc: string,
|
|
15
|
+
address: string,
|
|
16
|
+
fetchFn: typeof globalThis.fetch,
|
|
17
|
+
): Promise<RawProxy | null> {
|
|
18
|
+
let beaconRaw: string
|
|
19
|
+
try {
|
|
20
|
+
beaconRaw = await ethGetStorageAt(rpc, address, EIP1967_BEACON_SLOT, fetchFn)
|
|
21
|
+
} catch {
|
|
22
|
+
return null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const beacon = parseAddress(beaconRaw)
|
|
26
|
+
if (!beacon || beacon === ZERO_ADDRESS) return null
|
|
27
|
+
|
|
28
|
+
let implRaw: string
|
|
29
|
+
try {
|
|
30
|
+
implRaw = await ethCall(rpc, beacon, IMPLEMENTATION_SELECTOR, fetchFn)
|
|
31
|
+
} catch {
|
|
32
|
+
return null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const impl = parseAddress(implRaw)
|
|
36
|
+
if (!impl || impl === ZERO_ADDRESS) return null
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
pattern: 'eip-1967-beacon',
|
|
40
|
+
targets: [{ address: impl }],
|
|
41
|
+
beacon,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { EIP1967_ADMIN_SLOT, EIP1967_IMPL_SLOT } from '../constants'
|
|
2
|
+
import { parseAddress } from '../decode'
|
|
3
|
+
import { ethGetStorageAt } from '../rpc'
|
|
4
|
+
import type { RawProxy } from '../types'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Detect an EIP-1967 transparent or UUPS proxy.
|
|
8
|
+
*
|
|
9
|
+
* Reads the implementation slot (`0x3608…3bc3`). If non-zero, reads the admin
|
|
10
|
+
* slot in parallel and attaches it when present.
|
|
11
|
+
*
|
|
12
|
+
* Returns `null` when the implementation slot is empty or malformed.
|
|
13
|
+
*/
|
|
14
|
+
export async function detectEip1967(
|
|
15
|
+
rpc: string,
|
|
16
|
+
address: string,
|
|
17
|
+
fetchFn: typeof globalThis.fetch,
|
|
18
|
+
): Promise<RawProxy | null> {
|
|
19
|
+
let implRaw: string
|
|
20
|
+
let adminRaw: string
|
|
21
|
+
try {
|
|
22
|
+
[implRaw, adminRaw] = await Promise.all([
|
|
23
|
+
ethGetStorageAt(rpc, address, EIP1967_IMPL_SLOT, fetchFn),
|
|
24
|
+
ethGetStorageAt(rpc, address, EIP1967_ADMIN_SLOT, fetchFn).catch(() => '0x'),
|
|
25
|
+
])
|
|
26
|
+
} catch {
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const impl = parseAddress(implRaw)
|
|
31
|
+
if (!impl || impl === '0x0000000000000000000000000000000000000000') return null
|
|
32
|
+
|
|
33
|
+
const proxy: RawProxy = {
|
|
34
|
+
pattern: 'eip-1967',
|
|
35
|
+
targets: [{ address: impl }],
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const admin = parseAddress(adminRaw)
|
|
39
|
+
if (admin && admin !== '0x0000000000000000000000000000000000000000') {
|
|
40
|
+
proxy.admin = admin
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return proxy
|
|
44
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { IMPLEMENTATION_SELECTOR, ZERO_ADDRESS } from '../constants'
|
|
2
|
+
import { parseAddress } from '../decode'
|
|
3
|
+
import { ethCall } from '../rpc'
|
|
4
|
+
import type { RawProxy } from '../types'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Detect an EIP-897 delegate proxy by calling `implementation()` as a view
|
|
8
|
+
* function.
|
|
9
|
+
*
|
|
10
|
+
* Last-resort detector — any contract with a public `implementation()` view
|
|
11
|
+
* returning a non-zero address will match. Only reached when all other
|
|
12
|
+
* pattern detectors have returned `null`.
|
|
13
|
+
*
|
|
14
|
+
* Returns `null` when the call reverts or returns zero.
|
|
15
|
+
*/
|
|
16
|
+
export async function detectEip897(
|
|
17
|
+
rpc: string,
|
|
18
|
+
address: string,
|
|
19
|
+
fetchFn: typeof globalThis.fetch,
|
|
20
|
+
): Promise<RawProxy | null> {
|
|
21
|
+
let raw: string
|
|
22
|
+
try {
|
|
23
|
+
raw = await ethCall(rpc, address, IMPLEMENTATION_SELECTOR, fetchFn)
|
|
24
|
+
} catch {
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const impl = parseAddress(raw)
|
|
29
|
+
if (!impl || impl === ZERO_ADDRESS) return null
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
pattern: 'eip-897',
|
|
33
|
+
targets: [{ address: impl }],
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ZERO_ADDRESS } from '../constants'
|
|
2
|
+
import { parseAddress } from '../decode'
|
|
3
|
+
import { ethGetStorageAt } from '../rpc'
|
|
4
|
+
import type { RawProxy } from '../types'
|
|
5
|
+
|
|
6
|
+
const SAFE_SINGLETON_SLOT = '0x0000000000000000000000000000000000000000000000000000000000000000'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Detect a Gnosis Safe proxy — the singleton (implementation) address is
|
|
10
|
+
* stored at storage slot 0.
|
|
11
|
+
*
|
|
12
|
+
* Returns `null` when slot 0 is empty or not address-shaped.
|
|
13
|
+
*
|
|
14
|
+
* Cheap, but has a higher false-positive surface than the standard slots
|
|
15
|
+
* because any contract may use storage slot 0. Detector priority puts this
|
|
16
|
+
* after all EIP-standard patterns to keep the match rate clean.
|
|
17
|
+
*/
|
|
18
|
+
export async function detectGnosisSafe(
|
|
19
|
+
rpc: string,
|
|
20
|
+
address: string,
|
|
21
|
+
fetchFn: typeof globalThis.fetch,
|
|
22
|
+
): Promise<RawProxy | null> {
|
|
23
|
+
let raw: string
|
|
24
|
+
try {
|
|
25
|
+
raw = await ethGetStorageAt(rpc, address, SAFE_SINGLETON_SLOT, fetchFn)
|
|
26
|
+
} catch {
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const impl = parseAddress(raw)
|
|
31
|
+
if (!impl || impl === ZERO_ADDRESS) return null
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
pattern: 'gnosis-safe',
|
|
35
|
+
targets: [{ address: impl }],
|
|
36
|
+
}
|
|
37
|
+
}
|