Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor: Make library web3/ethers agnostic and require a EIP1193-compilant request function #1

Merged
merged 2 commits into from
Jul 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ build
node_modules
coverage
.env
*.log
*.log
.idea
33 changes: 25 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ethers-proxies

Detect proxy contracts and their target addresses using ethers
Detect proxy contracts and their target addresses using an [EIP-1193](https://eips.ethereum.org/EIPS/eip-1193) compatible `request` function

This package offers a utility function for checking if a smart contract at a given address implements one of the known proxy patterns.
It detects the following kinds of proxies:
Expand Down Expand Up @@ -28,16 +28,33 @@ yarn add ethers-proxies

## How to use

The function needs an ethers provider it uses to run a set of checks against the given address.
The function requires an [EIP-1193](https://eips.ethereum.org/EIPS/eip-1193) compatible `request` function that it uses to make JSON-RPC requests to run a set of checks against the given address.
It returns a promise that resolves to the proxy target address, i.e., the address of the contract implementing the logic.
The promise resolves to `null` if no proxy can be detected.


### Ethers with an adapter function
```ts
import { InfuraProvider } from '@ethersproject/providers'
import detectProxyTarget from 'ethers-proxies'
import { detectProxyTarget, EIP1193ProviderRequestFunc } from 'ethers-proxies'

const infuraProvider = new InfuraProvider(1, process.env.INFURA_API_KEY)
const requestFunc: EIP1193ProviderRequestFunc = ({ method, params }) => infuraProvider.send(method, params)

const target = await detectProxyTarget('0xA7AeFeaD2F25972D80516628417ac46b3F2604Af', requestFunc)
console.log(target) // logs "0x4bd844F72A8edD323056130A86FC624D0dbcF5b0"
```

### Web3 with an EIP1193 provider
Web3.js doesn't have a way to export an EIP1193 provider, so you need to ensure that the underlying provider you use is EIP1193 compatible. Most Ethereum-supported browsers like MetaMask and TrustWallet have an [EIP-1193](https://eips.ethereum.org/EIPS/eip-1193) compliant provider.
Otherwise, you can use providers like [eip1193-provider](https://www.npmjs.com/package/eip1193-provider).

```ts
import Web3 from 'web3'

const web3 = new Web3(Web3.givenProvider || "ws://localhost:8545");

const provider = new InfuraProvider(1, process.env.INFURA_API_KEY)
const target = await detectProxyTarget('0xA7AeFeaD2F25972D80516628417ac46b3F2604Af', provider)
const target = await detectProxyTarget('0xA7AeFeaD2F25972D80516628417ac46b3F2604Af', web3.currentProvider.request)
console.log(target) // logs "0x4bd844F72A8edD323056130A86FC624D0dbcF5b0"
```

Expand All @@ -50,7 +67,7 @@ detectProxyTarget(address: string, provider: Provider, blockTag?: BlockTag): Pro

**Arguments**
- `address` (string): The address of the proxy contract
- `provider` (Provider): An ethers Provider instance
- `blockTag` (optional: BlockTag): Any ethers [BlockTag](https://docs.ethers.io/v5/api/providers/types/#providers-BlockTag), default is `"latest"`
- `jsonRpcRequest` (EIP1193ProviderRequestFunc): A JSON-RPC request function, compatible with [EIP-1193](https://eips.ethereum.org/EIPS/eip-1193) (`(method: string, params: any[]) => Promise<any>`)
- `blockTag` (optional: BlockTag): `"earliest"`, `"latest"`, `"pending"` or hex block number, default is `"latest"`

The function returns a promise that will generally resolve to either the detected target contract address or `null` if it couldn't detect one.
The function returns a promise that will generally resolve to either the detected target contract address (non-checksummed) or `null` if it couldn't detect one.
38 changes: 16 additions & 22 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,32 +26,26 @@
"fix": "yarn fix:prettier && yarn fix:lint",
"fix:prettier": "prettier \"src/**/*.ts\" --write",
"fix:lint": "eslint src --ext .ts --fix",
"test": "jest"
"test": "env-cmd jest"
},
"devDependencies": {
"@ethersproject/providers": "^5.5.1",
"@types/jest": "^27.0.3",
"@typescript-eslint/eslint-plugin": "^4.31.2",
"@typescript-eslint/parser": "^4.31.2",
"cspell": "^5.10.1",
"@ethersproject/providers": "^5.6.8",
"@types/jest": "^28.1.5",
"@types/node": "^18.0.4",
"@typescript-eslint/eslint-plugin": "^5.30.6",
"@typescript-eslint/parser": "^5.30.6",
"cspell": "^6.2.3",
"env-cmd": "^10.1.0",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint": "^8.19.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-import": "^2.24.2",
"jest": "^27.4.5",
"prettier": "^2.4.1",
"eslint-plugin-import": "^2.26.0",
"jest": "^28.1.2",
"prettier": "^2.7.1",
"rimraf": "^3.0.2",
"ts-jest": "^27.1.2",
"ts-node": "^10.2.1",
"typescript": "^4.4.3"
"ts-jest": "^28.0.6",
"ts-node": "^10.9.1",
"typescript": "^4.7.4"
},
"dependencies": {
"@ethersproject/abi": "^5.0.0",
"@ethersproject/abstract-provider": "^5.0.0",
"@ethersproject/address": "^5.0.0",
"@ethersproject/bignumber": "^5.0.0",
"@ethersproject/bytes": "^5.5.0",
"@ethersproject/contracts": "^5.0.0"
}
"dependencies": {}
}
186 changes: 122 additions & 64 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { Interface } from '@ethersproject/abi'
import { BlockTag, Provider } from '@ethersproject/abstract-provider'
import { getAddress } from '@ethersproject/address'
import { BigNumber } from '@ethersproject/bignumber'
import { hexZeroPad } from '@ethersproject/bytes'
import { Contract } from '@ethersproject/contracts'
import {
BlockTag,
EIP1193ProviderRequestFunc,
} from './types'

// obtained as bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)
const EIP_1967_LOGIC_SLOT =
Expand All @@ -21,108 +19,168 @@ const OPEN_ZEPPELIN_IMPLEMENTATION_SLOT =
const EIP_1822_LOGIC_SLOT =
'0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7'

const EIP_1167_BEACON_INTERFACE = new Interface([
'function implementation() view returns (address)',

const EIP_1167_BEACON_METHODS = [
// bytes4(keccak256("implementation()")) padded to 32 bytes
'0x5c60da1b00000000000000000000000000000000000000000000000000000000',
// bytes4(keccak256("childImplementation()")) padded to 32 bytes
// some implementations use this over the standard method name so that the beacon contract is not detected as an EIP-897 proxy itself
'function childImplementation() view returns (address)',
])
'0xda52571600000000000000000000000000000000000000000000000000000000',
]

const EIP_897_INTERFACE = new Interface([
'function implementation() view returns (address)',
])
const EIP_897_INTERFACE = [
// bytes4(keccak256("implementation()")) padded to 32 bytes
'0x5c60da1b00000000000000000000000000000000000000000000000000000000',
]

const GNOSIS_SAFE_PROXY_INTERFACE = new Interface([
'function masterCopy() view returns (address)',
])
const GNOSIS_SAFE_PROXY_INTERFACE = [
// bytes4(keccak256("masterCopy()")) padded to 32 bytes
'0xa619486e00000000000000000000000000000000000000000000000000000000',
]

const detectProxyTarget = (
proxyAddress: string,
provider: Provider,
blockTag?: BlockTag | Promise<BlockTag>
jsonRpcRequest: EIP1193ProviderRequestFunc,
blockTag?: BlockTag,
): Promise<string | null> =>
Promise.any([
// EIP-1167 Minimal Proxy Contract
jsonRpcRequest({
method: 'eth_getCode',
params: [proxyAddress, blockTag],
})
.then(parse1167Bytecode)
.then(readAddress),

// EIP-1967 direct proxy
provider
.getStorageAt(proxyAddress, EIP_1967_LOGIC_SLOT, blockTag)
jsonRpcRequest({
method: 'eth_getStorageAt',
params: [proxyAddress, EIP_1967_LOGIC_SLOT, blockTag],
})
.then(readAddress),

// EIP-1967 beacon proxy
provider
.getStorageAt(proxyAddress, EIP_1967_BEACON_SLOT, blockTag)
jsonRpcRequest({
method: 'eth_getStorageAt',
params: [proxyAddress, EIP_1967_BEACON_SLOT, blockTag],
})
.then(readAddress)
.then((beaconAddress) => {
const contract = new Contract(
beaconAddress,
EIP_1167_BEACON_INTERFACE,
provider
)
return contract
.implementation({ blockTag })
.catch(() => contract.childImplementation({ blockTag }))
})
.then((beaconAddress) =>
jsonRpcRequest({
method: 'eth_call',
params: [
{
to: beaconAddress,
data: EIP_1167_BEACON_METHODS[0],
},
blockTag,
],
})
.catch(() =>
jsonRpcRequest({
method: 'eth_call',
params: [
{
to: beaconAddress,
data: EIP_1167_BEACON_METHODS[1],
},
blockTag,
],
}),
),
)
.then(readAddress),

// OpenZeppelin proxy pattern
provider
.getStorageAt(proxyAddress, OPEN_ZEPPELIN_IMPLEMENTATION_SLOT, blockTag)
jsonRpcRequest({
method: 'eth_getStorageAt',
params: [proxyAddress, OPEN_ZEPPELIN_IMPLEMENTATION_SLOT, blockTag],
})
.then(readAddress),

// EIP-1822 Universal Upgradeable Proxy Standard
provider
.getStorageAt(proxyAddress, EIP_1822_LOGIC_SLOT, blockTag)
.then(readAddress),

// EIP-1167 Minimal Proxy Contract
provider
.getCode(proxyAddress, blockTag)
.then(parse1167Bytecode)
jsonRpcRequest({
method: 'eth_getStorageAt',
params: [proxyAddress, EIP_1822_LOGIC_SLOT, blockTag],
})
.then(readAddress),

// EIP-897 DelegateProxy pattern
new Contract(proxyAddress, EIP_897_INTERFACE, provider)
.implementation({ blockTag })
.then(readAddress),
jsonRpcRequest({
method: 'eth_call',
params: [
{
to: proxyAddress,
data: EIP_897_INTERFACE[0],
},
blockTag,
],
}).then(readAddress),

// GnosisSafeProxy contract
new Contract(proxyAddress, GNOSIS_SAFE_PROXY_INTERFACE, provider)
.masterCopy({ blockTag })
.then(readAddress),
jsonRpcRequest({
method: 'eth_call',
params: [
{
to: proxyAddress,
data: GNOSIS_SAFE_PROXY_INTERFACE[0],
},
blockTag,
],
}).then(readAddress),
]).catch(() => null)

const readAddress = (value: string) => {
const number = BigNumber.from(value)
if (number.isZero()) {
throw new Error('empty slot')
const readAddress = (value: unknown): string => {
if (typeof value !== 'string' || value === '0x') {
throw new Error(`Invalid address value: ${value}`)
}

if (value.length === 66) {
const EMPTY_SLOT = `0x${'0'.repeat(64)}`
if (value === EMPTY_SLOT) {
throw new Error('Empty slot')
}

return '0x' + value.slice(-40)
}
return getAddress(hexZeroPad(number.toHexString(), 20))

const zeroAddress = `0x{'0'.repeat(40)}`
if (value === zeroAddress) {
throw new Error('Empty address')
}

return value
}

const EIP_1167_BYTECODE_PREFIX = '363d3d373d3d3d363d'
const EIP_1167_BYTECODE_PREFIX = '0x363d3d373d3d3d363d'
const EIP_1167_BYTECODE_SUFFIX = '57fd5bf3'
const parse1167Bytecode = (bytecode: string) => {
const prefix = `0x${EIP_1167_BYTECODE_PREFIX}`
const parse1167Bytecode = (bytecode: unknown): string => {
if (
!bytecode.startsWith(prefix) ||
typeof bytecode !== 'string' ||
!bytecode.startsWith(EIP_1167_BYTECODE_PREFIX) ||
!bytecode.endsWith(EIP_1167_BYTECODE_SUFFIX)
) {
throw new Error('Not an EIP-1167 bytecode')
}

// detect length of address (20 bytes non-optimized, 0 < N < 20 bytes for vanity addresses)
const pushNHex = bytecode.substring(prefix.length, prefix.length + 2)
const pushNHex = bytecode.substring(
EIP_1167_BYTECODE_PREFIX.length,
EIP_1167_BYTECODE_PREFIX.length + 2,
)
// push1 ... push20 use opcodes 0x60 ... 0x73
const addressLength = parseInt(pushNHex, 16) - 0x5f

if (addressLength < 1 || addressLength > 20) {
throw new Error('Not an EIP-1167 bytecode')
}

// extract address
return `0x${bytecode.substring(
prefix.length + 2,
prefix.length + 2 + addressLength * 2 // address length is in bytes, 2 hex chars make up 1 byte
)}`
const addressFromBytecode = bytecode.substring(
EIP_1167_BYTECODE_PREFIX.length + 2,
EIP_1167_BYTECODE_PREFIX.length + 2 + addressLength * 2, // address length is in bytes, 2 hex chars make up 1 byte
)

// padStart is needed for vanity addresses
return `0x${addressFromBytecode.padStart(40, '0')}`
}

export default detectProxyTarget
9 changes: 9 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export type BlockTag = number | 'earliest' | 'latest' | 'pending';

export interface RequestArguments {
method: string;
params: unknown[];
}

export type EIP1193ProviderRequestFunc = (args: RequestArguments) => Promise<unknown>

Loading