Skip to content

Commit

Permalink
Merge pull request #1 from gnosis/feature/library-agnostic-refactor
Browse files Browse the repository at this point in the history
Refactor: Make library `web3/ethers` agnostic and require a EIP1193-compilant `request` function
  • Loading branch information
jfschwarz authored Jul 14, 2022
2 parents 4ce8ece + b892ec4 commit ca9786e
Show file tree
Hide file tree
Showing 7 changed files with 2,001 additions and 2,134 deletions.
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

0 comments on commit ca9786e

Please sign in to comment.