Skip to content

Commit 607b8c8

Browse files
Add minimal proxy support (#195)
* Add eip1167 proxy support and get proxies list from bytecode using whatsabi * Add new test mock for addresses bytecode * Generate test mock files inside json rpc mock * Test updates * Add changeset
1 parent d467b26 commit 607b8c8

File tree

87 files changed

+1136
-3542
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

87 files changed

+1136
-3542
lines changed

.changeset/strange-roses-check.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@3loop/transaction-decoder': minor
3+
---
4+
5+
Add minimal proxy support using whatsabi

apps/docs/src/content/docs/index.mdx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,14 @@ import { Card, CardGrid, LinkCard } from '@astrojs/starlight/components'
2323
Fully written in TypeScript, can be used on both the client side and server side in JS applications.
2424
</Card>
2525
<Card title="Highly customizable" icon="puzzle">
26-
Provides a set of data loaders to simplify resolution of ABIs and other data required for decoding.
26+
Leverage plug-and-play data loaders for ABI and metadata resolution, and connect your own storage for metadata
27+
caching.
2728
</Card>
2829
<Card title="You only need an RPC" icon="rocket">
29-
Optional API providers can be used to fetch contract metadata or you can connect just your storage.
30+
The library uses standart JSON RPC methods to fetch transaction data.
3031
</Card>
3132
<Card title="Flexible Intepreters" icon="setting">
32-
Define any custom interpretation of EVM transactions.
33+
Define custom interpretations for EVM transactions or use the default ones.
3334
</Card>
3435
</CardGrid>
3536

packages/transaction-decoder/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,5 +93,8 @@
9393
"engines": {
9494
"node": ">=18.16"
9595
},
96-
"sideEffects": false
96+
"sideEffects": false,
97+
"dependencies": {
98+
"@shazow/whatsabi": "^0.18.0"
99+
}
97100
}

packages/transaction-decoder/src/abi-loader.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@ import {
1212
SchemaAST,
1313
} from 'effect'
1414
import { ContractABI, ContractAbiResolverStrategy, GetContractABIStrategy } from './abi-strategy/request-model.js'
15-
import { Abi, getAddress } from 'viem'
16-
import { getProxyImplementation } from './decoding/proxies.js'
15+
import { Abi } from 'viem'
1716

1817
export interface AbiParams {
1918
chainID: number

packages/transaction-decoder/src/decoding/log-decode.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Address, type GetTransactionReturnType, type Log, decodeEventLog, getAbiItem, getAddress } from 'viem'
1+
import { type GetTransactionReturnType, type Log, decodeEventLog, getAbiItem, getAddress } from 'viem'
22
import { Effect } from 'effect'
33
import type { DecodedLogEvent, Interaction, RawDecodedLog } from '../types.js'
44
import { getProxyImplementation } from './proxies.js'

packages/transaction-decoder/src/decoding/proxies.ts

Lines changed: 138 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,29 @@
1-
import { Effect, PrimaryKey, Request, RequestResolver, Schedule, Schema, SchemaAST } from 'effect'
2-
3-
import { PublicClient, RPCCallError, RPCFetchError } from '../public-client.js'
4-
import { Address, Hex } from 'viem'
1+
import { Effect, Either, PrimaryKey, Request, RequestResolver, Schema, SchemaAST } from 'effect'
2+
import { PublicClient, RPCFetchError, UnknownNetwork } from '../public-client.js'
3+
import { Address, getAddress, Hex } from 'viem'
54
import { ProxyType } from '../types.js'
65
import { ZERO_ADDRESS } from './constants.js'
6+
import { whatsabi } from '@shazow/whatsabi'
77

88
interface StorageSlot {
99
type: ProxyType
1010
slot: Hex
1111
}
1212

13-
interface ProxyResult {
14-
type: ProxyType
13+
interface ProxyResult extends StorageSlot {
1514
address: Address
1615
}
1716

18-
const storageSlots: StorageSlot[] = [
17+
const knownStorageSlots: StorageSlot[] = [
18+
{ type: 'eip1167', slot: '0x' }, //EIP1167 minimal proxy
1919
{ type: 'eip1967', slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' }, //EIP1967
2020
{ type: 'zeppelin', slot: '0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3' }, //zeppelin
2121
{ type: 'safe', slot: '0xa619486e00000000000000000000000000000000000000000000000000000000' }, // gnosis Safe Proxy Factor 1.1.1
2222
]
2323

2424
const zeroSlot = '0x0000000000000000000000000000000000000000000000000000000000000000'
2525

26-
export interface GetProxy extends Request.Request<ProxyResult | undefined, RPCFetchError> {
26+
export interface GetProxy extends Request.Request<ProxyResult | undefined, RPCFetchError | UnknownNetwork> {
2727
readonly _tag: 'GetProxy'
2828
readonly address: Address
2929
readonly chainID: number
@@ -34,7 +34,7 @@ class SchemaAddress extends Schema.make<Address>(SchemaAST.stringKeyword) {}
3434
class SchemaProxy extends Schema.make<ProxyResult | undefined>(SchemaAST.objectKeyword) {}
3535

3636
class ProxyLoader extends Schema.TaggedRequest<ProxyLoader>()('ProxyLoader', {
37-
failure: Schema.instanceOf(RPCFetchError),
37+
failure: Schema.Union(Schema.instanceOf(RPCFetchError), Schema.instanceOf(UnknownNetwork)),
3838
success: Schema.NullOr(SchemaProxy),
3939
payload: {
4040
address: SchemaAddress,
@@ -56,7 +56,9 @@ const getStorageSlot = (request: ProxyLoader, slot: StorageSlot) =>
5656
address: request.address,
5757
slot: slot.slot,
5858
}),
59-
catch: () => new RPCFetchError('Get storage'),
59+
catch: (e) => {
60+
return new RPCFetchError(`Get storage error: ${(e as { details?: string }).details ?? ''}`)
61+
},
6062
})
6163
})
6264

@@ -72,19 +74,121 @@ const ethCall = (request: ProxyLoader, slot: StorageSlot) =>
7274
data: slot.slot,
7375
})
7476
)?.data,
75-
catch: () => new RPCCallError('Eth call'),
77+
catch: (e) => new RPCFetchError(`Eth call error: ${(e as { details?: string }).details ?? ''}`),
7678
})
7779
})
7880

81+
const ethGetCode = (request: ProxyLoader) =>
82+
Effect.gen(function* () {
83+
const service = yield* PublicClient
84+
const { client: publicClient } = yield* service.getPublicClient(request.chainID)
85+
return yield* Effect.tryPromise({
86+
try: () => publicClient.getCode({ address: request.address }),
87+
catch: (e) => new RPCFetchError(`Eth get code error: ${(e as { details?: string }).details ?? ''}`),
88+
})
89+
})
90+
91+
const getProxyTypeFromBytecode = (request: ProxyLoader, code: Hex) =>
92+
Effect.gen(function* () {
93+
const service = yield* PublicClient
94+
const { client: publicClient } = yield* service.getPublicClient(request.chainID)
95+
96+
//use whatsabi to only resolve proxies with a known bytecode
97+
const cachedCodeProvider = yield* Effect.try({
98+
try: () =>
99+
whatsabi.providers.WithCachedCode(publicClient, {
100+
[request.address]: code,
101+
}),
102+
catch: () => new RPCFetchError(`Get proxy type from bytecode error`),
103+
})
104+
105+
const result = yield* Effect.tryPromise({
106+
try: () =>
107+
whatsabi.autoload(request.address, {
108+
provider: cachedCodeProvider,
109+
abiLoader: false, // Skip ABI loaders
110+
signatureLookup: false, // Skip looking up selector signatures
111+
}),
112+
catch: () => new RPCFetchError('Get proxy type from bytecode'),
113+
})
114+
115+
//if there are soeme proxies, return the list of them but with udpdated types
116+
if (result && result.proxies.length > 0) {
117+
const proxies: (ProxyResult | StorageSlot)[] = result.proxies
118+
.map((proxy) => {
119+
if (proxy.name === 'EIP1967Proxy') {
120+
return knownStorageSlots.find((slot) => slot.type === 'eip1967')
121+
}
122+
123+
if (proxy.name === 'GnosisSafeProxy') {
124+
return knownStorageSlots.find((slot) => slot.type === 'safe')
125+
}
126+
127+
if (proxy.name === 'ZeppelinOSProxy') {
128+
return knownStorageSlots.find((slot) => slot.type === 'zeppelin')
129+
}
130+
131+
if (proxy.name === 'FixedProxy') {
132+
const implementation = (proxy as any as { resolvedAddress: Address }).resolvedAddress
133+
134+
if (!implementation) return undefined
135+
136+
return {
137+
type: 'eip1167',
138+
address: getAddress(implementation),
139+
slot: '0x',
140+
} as ProxyResult
141+
}
142+
143+
return undefined
144+
})
145+
.filter(Boolean)
146+
.filter((proxy, index, self) => self.findIndex((p) => p?.type === proxy.type) === index)
147+
148+
return proxies
149+
}
150+
151+
return undefined
152+
})
153+
79154
export const GetProxyResolver = RequestResolver.fromEffect(
80-
(request: ProxyLoader): Effect.Effect<ProxyResult | undefined, RPCFetchError, PublicClient> =>
155+
(request: ProxyLoader): Effect.Effect<ProxyResult | undefined, RPCFetchError | UnknownNetwork, PublicClient> =>
81156
Effect.gen(function* () {
82157
// NOTE: Should we make this recursive when we have a Proxy of a Proxy?
83158

84-
const effects = storageSlots.map((slot) =>
85-
Effect.gen(function* () {
86-
const res: ProxyResult | undefined = { type: slot.type, address: '0x' }
159+
//Getting the bytecode of the address first
160+
const codeResult = yield* ethGetCode(request).pipe(Effect.either)
161+
162+
if (Either.isLeft(codeResult)) {
163+
yield* Effect.logError(`ProxyResolver error: ${JSON.stringify(codeResult.left)}`)
164+
return undefined
165+
}
87166

167+
const code = codeResult.right
168+
169+
//If code is empty and it is EOA, return empty result
170+
if (!code) return undefined
171+
172+
let proxySlots: StorageSlot[] | undefined
173+
174+
//Getting the proxies list from the bytecode
175+
const proxies = yield* getProxyTypeFromBytecode(request, code)
176+
if (proxies && proxies.length > 0) {
177+
//If it is EIP1167 proxy, return it becasue it is alredy resolved from the bytecode
178+
if (proxies.some((proxy) => proxy.type === 'eip1167')) {
179+
return proxies.find((proxy) => proxy.type === 'eip1167') as ProxyResult
180+
}
181+
182+
proxySlots = proxies as StorageSlot[]
183+
}
184+
185+
if (!proxySlots) {
186+
return undefined
187+
}
188+
189+
//get the implementation address by requesting the storage slot value of possible proxies
190+
const effects = (proxySlots ?? knownStorageSlots).map((slot) =>
191+
Effect.gen(function* () {
88192
let address: Hex | undefined
89193
switch (slot.type) {
90194
case 'eip1967':
@@ -100,21 +204,32 @@ export const GetProxyResolver = RequestResolver.fromEffect(
100204

101205
if (!address || address === zeroSlot) return undefined
102206

103-
res.address = ('0x' + address.slice(address.length - 40)) as Address
104-
return res
207+
return {
208+
type: slot.type,
209+
address: ('0x' + address.slice(address.length - 40)) as Address,
210+
slot: slot.slot,
211+
}
105212
}),
106213
)
107214

108-
const policy = Schedule.addDelay(
109-
Schedule.recurs(2), // Retry for a maximum of 2 times
110-
() => '100 millis', // Add a delay of 100 milliseconds between retries
111-
)
112215
const res = yield* Effect.all(effects, {
113216
concurrency: 'inherit',
114217
batching: 'inherit',
115-
}).pipe(Effect.retryOrElse(policy, () => Effect.succeed(undefined)))
218+
mode: 'either',
219+
})
220+
221+
const resRight = res
222+
.filter(Either.isRight)
223+
.map((r) => r.right)
224+
.find((x) => x != null)
225+
226+
const resLeft = res.filter(Either.isLeft).map((r) => r.left)
227+
228+
if (resLeft.length > 0) {
229+
yield* Effect.logError(`ProxyResolver error: ${resLeft.map((e) => JSON.stringify(e)).join(', ')}`)
230+
}
116231

117-
return res?.find((x) => x != null)
232+
return resRight
118233
}),
119234
).pipe(RequestResolver.contextFromEffect)
120235

packages/transaction-decoder/src/public-client.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,6 @@ export class RPCFetchError {
1111
constructor(readonly reason: unknown) {}
1212
}
1313

14-
export class RPCCallError {
15-
readonly _tag = 'EPCCallError'
16-
constructor(readonly reason: unknown) {}
17-
}
18-
1914
export interface PublicClientConfig {
2015
readonly traceAPI?: 'parity' | 'geth' | 'none'
2116
}

packages/transaction-decoder/src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,4 +162,4 @@ export interface Asset {
162162
tokenId?: string
163163
}
164164

165-
export type ProxyType = 'eip1967' | 'zeppelin' | 'safe'
165+
export type ProxyType = 'eip1967' | 'zeppelin' | 'safe' | 'eip1167'

packages/transaction-decoder/test/constants.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
import { Hex } from 'viem'
22

3+
export const RPC = 'https://rpc.ankr.com/eth'
4+
export const ZERO_SLOT = '0x0000000000000000000000000000000000000000000000000000000000000000'
5+
export const PROXY_SLOTS = [
6+
'0x747b7a908f10c8c0afdd3ea97976f30ac0c0d54304254ab3089ae5d161fc727a',
7+
'0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc',
8+
'0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3',
9+
'0xa619486e00000000000000000000000000000000000000000000000000000000',
10+
] as const
11+
312
type TXS = readonly {
413
hash: Hex
514
chainID: number
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[{"inputs":[{"internalType":"address","name":"admin","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"implementation","type":"address"}],"name":"Upgraded","type":"event"},{"stateMutability":"payable","type":"fallback"},{"inputs":[],"name":"admin","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"implementation","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_logic","type":"address"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"initialize","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"newImplementation","type":"address"}],"name":"upgradeTo","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newImplementation","type":"address"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"upgradeToAndCall","outputs":[],"stateMutability":"payable","type":"function"}]

0 commit comments

Comments
 (0)