Skip to content

Commit

Permalink
Add minimal proxy support (#195)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
anastasiarods authored Jan 9, 2025
1 parent d467b26 commit 607b8c8
Show file tree
Hide file tree
Showing 87 changed files with 1,136 additions and 3,542 deletions.
5 changes: 5 additions & 0 deletions .changeset/strange-roses-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@3loop/transaction-decoder': minor
---

Add minimal proxy support using whatsabi
7 changes: 4 additions & 3 deletions apps/docs/src/content/docs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@ import { Card, CardGrid, LinkCard } from '@astrojs/starlight/components'
Fully written in TypeScript, can be used on both the client side and server side in JS applications.
</Card>
<Card title="Highly customizable" icon="puzzle">
Provides a set of data loaders to simplify resolution of ABIs and other data required for decoding.
Leverage plug-and-play data loaders for ABI and metadata resolution, and connect your own storage for metadata
caching.
</Card>
<Card title="You only need an RPC" icon="rocket">
Optional API providers can be used to fetch contract metadata or you can connect just your storage.
The library uses standart JSON RPC methods to fetch transaction data.
</Card>
<Card title="Flexible Intepreters" icon="setting">
Define any custom interpretation of EVM transactions.
Define custom interpretations for EVM transactions or use the default ones.
</Card>
</CardGrid>

Expand Down
5 changes: 4 additions & 1 deletion packages/transaction-decoder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,5 +93,8 @@
"engines": {
"node": ">=18.16"
},
"sideEffects": false
"sideEffects": false,
"dependencies": {
"@shazow/whatsabi": "^0.18.0"
}
}
3 changes: 1 addition & 2 deletions packages/transaction-decoder/src/abi-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ import {
SchemaAST,
} from 'effect'
import { ContractABI, ContractAbiResolverStrategy, GetContractABIStrategy } from './abi-strategy/request-model.js'
import { Abi, getAddress } from 'viem'
import { getProxyImplementation } from './decoding/proxies.js'
import { Abi } from 'viem'

export interface AbiParams {
chainID: number
Expand Down
2 changes: 1 addition & 1 deletion packages/transaction-decoder/src/decoding/log-decode.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Address, type GetTransactionReturnType, type Log, decodeEventLog, getAbiItem, getAddress } from 'viem'
import { type GetTransactionReturnType, type Log, decodeEventLog, getAbiItem, getAddress } from 'viem'
import { Effect } from 'effect'
import type { DecodedLogEvent, Interaction, RawDecodedLog } from '../types.js'
import { getProxyImplementation } from './proxies.js'
Expand Down
161 changes: 138 additions & 23 deletions packages/transaction-decoder/src/decoding/proxies.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
import { Effect, PrimaryKey, Request, RequestResolver, Schedule, Schema, SchemaAST } from 'effect'

import { PublicClient, RPCCallError, RPCFetchError } from '../public-client.js'
import { Address, Hex } from 'viem'
import { Effect, Either, PrimaryKey, Request, RequestResolver, Schema, SchemaAST } from 'effect'
import { PublicClient, RPCFetchError, UnknownNetwork } from '../public-client.js'
import { Address, getAddress, Hex } from 'viem'
import { ProxyType } from '../types.js'
import { ZERO_ADDRESS } from './constants.js'
import { whatsabi } from '@shazow/whatsabi'

interface StorageSlot {
type: ProxyType
slot: Hex
}

interface ProxyResult {
type: ProxyType
interface ProxyResult extends StorageSlot {
address: Address
}

const storageSlots: StorageSlot[] = [
const knownStorageSlots: StorageSlot[] = [
{ type: 'eip1167', slot: '0x' }, //EIP1167 minimal proxy
{ type: 'eip1967', slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' }, //EIP1967
{ type: 'zeppelin', slot: '0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3' }, //zeppelin
{ type: 'safe', slot: '0xa619486e00000000000000000000000000000000000000000000000000000000' }, // gnosis Safe Proxy Factor 1.1.1
]

const zeroSlot = '0x0000000000000000000000000000000000000000000000000000000000000000'

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

class ProxyLoader extends Schema.TaggedRequest<ProxyLoader>()('ProxyLoader', {
failure: Schema.instanceOf(RPCFetchError),
failure: Schema.Union(Schema.instanceOf(RPCFetchError), Schema.instanceOf(UnknownNetwork)),
success: Schema.NullOr(SchemaProxy),
payload: {
address: SchemaAddress,
Expand All @@ -56,7 +56,9 @@ const getStorageSlot = (request: ProxyLoader, slot: StorageSlot) =>
address: request.address,
slot: slot.slot,
}),
catch: () => new RPCFetchError('Get storage'),
catch: (e) => {
return new RPCFetchError(`Get storage error: ${(e as { details?: string }).details ?? ''}`)
},
})
})

Expand All @@ -72,19 +74,121 @@ const ethCall = (request: ProxyLoader, slot: StorageSlot) =>
data: slot.slot,
})
)?.data,
catch: () => new RPCCallError('Eth call'),
catch: (e) => new RPCFetchError(`Eth call error: ${(e as { details?: string }).details ?? ''}`),
})
})

const ethGetCode = (request: ProxyLoader) =>
Effect.gen(function* () {
const service = yield* PublicClient
const { client: publicClient } = yield* service.getPublicClient(request.chainID)
return yield* Effect.tryPromise({
try: () => publicClient.getCode({ address: request.address }),
catch: (e) => new RPCFetchError(`Eth get code error: ${(e as { details?: string }).details ?? ''}`),
})
})

const getProxyTypeFromBytecode = (request: ProxyLoader, code: Hex) =>
Effect.gen(function* () {
const service = yield* PublicClient
const { client: publicClient } = yield* service.getPublicClient(request.chainID)

//use whatsabi to only resolve proxies with a known bytecode
const cachedCodeProvider = yield* Effect.try({
try: () =>
whatsabi.providers.WithCachedCode(publicClient, {
[request.address]: code,
}),
catch: () => new RPCFetchError(`Get proxy type from bytecode error`),
})

const result = yield* Effect.tryPromise({
try: () =>
whatsabi.autoload(request.address, {
provider: cachedCodeProvider,
abiLoader: false, // Skip ABI loaders
signatureLookup: false, // Skip looking up selector signatures
}),
catch: () => new RPCFetchError('Get proxy type from bytecode'),
})

//if there are soeme proxies, return the list of them but with udpdated types
if (result && result.proxies.length > 0) {
const proxies: (ProxyResult | StorageSlot)[] = result.proxies
.map((proxy) => {
if (proxy.name === 'EIP1967Proxy') {
return knownStorageSlots.find((slot) => slot.type === 'eip1967')
}

if (proxy.name === 'GnosisSafeProxy') {
return knownStorageSlots.find((slot) => slot.type === 'safe')
}

if (proxy.name === 'ZeppelinOSProxy') {
return knownStorageSlots.find((slot) => slot.type === 'zeppelin')
}

if (proxy.name === 'FixedProxy') {
const implementation = (proxy as any as { resolvedAddress: Address }).resolvedAddress

if (!implementation) return undefined

return {
type: 'eip1167',
address: getAddress(implementation),
slot: '0x',
} as ProxyResult
}

return undefined
})
.filter(Boolean)
.filter((proxy, index, self) => self.findIndex((p) => p?.type === proxy.type) === index)

return proxies
}

return undefined
})

export const GetProxyResolver = RequestResolver.fromEffect(
(request: ProxyLoader): Effect.Effect<ProxyResult | undefined, RPCFetchError, PublicClient> =>
(request: ProxyLoader): Effect.Effect<ProxyResult | undefined, RPCFetchError | UnknownNetwork, PublicClient> =>
Effect.gen(function* () {
// NOTE: Should we make this recursive when we have a Proxy of a Proxy?

const effects = storageSlots.map((slot) =>
Effect.gen(function* () {
const res: ProxyResult | undefined = { type: slot.type, address: '0x' }
//Getting the bytecode of the address first
const codeResult = yield* ethGetCode(request).pipe(Effect.either)

if (Either.isLeft(codeResult)) {
yield* Effect.logError(`ProxyResolver error: ${JSON.stringify(codeResult.left)}`)
return undefined
}

const code = codeResult.right

//If code is empty and it is EOA, return empty result
if (!code) return undefined

let proxySlots: StorageSlot[] | undefined

//Getting the proxies list from the bytecode
const proxies = yield* getProxyTypeFromBytecode(request, code)
if (proxies && proxies.length > 0) {
//If it is EIP1167 proxy, return it becasue it is alredy resolved from the bytecode
if (proxies.some((proxy) => proxy.type === 'eip1167')) {
return proxies.find((proxy) => proxy.type === 'eip1167') as ProxyResult
}

proxySlots = proxies as StorageSlot[]
}

if (!proxySlots) {
return undefined
}

//get the implementation address by requesting the storage slot value of possible proxies
const effects = (proxySlots ?? knownStorageSlots).map((slot) =>
Effect.gen(function* () {
let address: Hex | undefined
switch (slot.type) {
case 'eip1967':
Expand All @@ -100,21 +204,32 @@ export const GetProxyResolver = RequestResolver.fromEffect(

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

res.address = ('0x' + address.slice(address.length - 40)) as Address
return res
return {
type: slot.type,
address: ('0x' + address.slice(address.length - 40)) as Address,
slot: slot.slot,
}
}),
)

const policy = Schedule.addDelay(
Schedule.recurs(2), // Retry for a maximum of 2 times
() => '100 millis', // Add a delay of 100 milliseconds between retries
)
const res = yield* Effect.all(effects, {
concurrency: 'inherit',
batching: 'inherit',
}).pipe(Effect.retryOrElse(policy, () => Effect.succeed(undefined)))
mode: 'either',
})

const resRight = res
.filter(Either.isRight)
.map((r) => r.right)
.find((x) => x != null)

const resLeft = res.filter(Either.isLeft).map((r) => r.left)

if (resLeft.length > 0) {
yield* Effect.logError(`ProxyResolver error: ${resLeft.map((e) => JSON.stringify(e)).join(', ')}`)
}

return res?.find((x) => x != null)
return resRight
}),
).pipe(RequestResolver.contextFromEffect)

Expand Down
5 changes: 0 additions & 5 deletions packages/transaction-decoder/src/public-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,6 @@ export class RPCFetchError {
constructor(readonly reason: unknown) {}
}

export class RPCCallError {
readonly _tag = 'EPCCallError'
constructor(readonly reason: unknown) {}
}

export interface PublicClientConfig {
readonly traceAPI?: 'parity' | 'geth' | 'none'
}
Expand Down
2 changes: 1 addition & 1 deletion packages/transaction-decoder/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,4 +162,4 @@ export interface Asset {
tokenId?: string
}

export type ProxyType = 'eip1967' | 'zeppelin' | 'safe'
export type ProxyType = 'eip1967' | 'zeppelin' | 'safe' | 'eip1167'
9 changes: 9 additions & 0 deletions packages/transaction-decoder/test/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import { Hex } from 'viem'

export const RPC = 'https://rpc.ankr.com/eth'
export const ZERO_SLOT = '0x0000000000000000000000000000000000000000000000000000000000000000'
export const PROXY_SLOTS = [
'0x747b7a908f10c8c0afdd3ea97976f30ac0c0d54304254ab3089ae5d161fc727a',
'0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc',
'0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3',
'0xa619486e00000000000000000000000000000000000000000000000000000000',
] as const

type TXS = readonly {
hash: Hex
chainID: number
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +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"}]
Loading

0 comments on commit 607b8c8

Please sign in to comment.