├── .cspell.json ├── .editorconfig ├── .env.template ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── .vscode ├── extensions.json └── settings.json ├── .yarn └── sdks │ ├── eslint │ ├── bin │ │ └── eslint.js │ ├── lib │ │ ├── api.js │ │ └── unsupported-api.js │ └── package.json │ ├── integrations.yml │ ├── prettier │ ├── bin-prettier.js │ ├── index.js │ └── package.json │ └── typescript │ ├── bin │ ├── tsc │ └── tsserver │ ├── lib │ ├── tsc.js │ ├── tsserver.js │ ├── tsserverlibrary.js │ └── typescript.js │ └── package.json ├── .yarnrc.yml ├── LICENSE ├── README.md ├── jest.config.js ├── package.json ├── src ├── eip1167.ts ├── index.ts ├── readString.ts └── types.ts ├── test ├── detectProxy.spec.ts ├── parseEip1167Bytecode.spec.ts └── readString.ts ├── tsconfig.cjs.json ├── tsconfig.esm.json ├── tsconfig.json └── yarn.lock /.cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2", 3 | "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/master/cspell.schema.json", 4 | "language": "en", 5 | "words": [ 6 | "bignumber", 7 | "checksummed", 8 | "ethersproject", 9 | "keccak", 10 | "PROXIABLE", 11 | "viem", 12 | "zeppelinos" 13 | ], 14 | "flagWords": [], 15 | "ignorePaths": [ 16 | "package.json", 17 | "yarn.lock", 18 | "tsconfig.json", 19 | "node_modules/**", 20 | "build/**" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | max_line_length = 80 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | max_line_length = 0 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | INFURA_API_KEY= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | coverage 4 | .env 5 | *.log 6 | .idea 7 | 8 | .pnp.* 9 | .yarn/* 10 | !.yarn/patches 11 | !.yarn/plugins 12 | !.yarn/releases 13 | !.yarn/sdks 14 | !.yarn/versions -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # package.json is formatted by package managers, so we ignore it here 2 | package.json -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "trailingComma": "es5" 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "arcanis.vscode-zipfs", 4 | "dbaeumer.vscode-eslint", 5 | "esbenp.prettier-vscode" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { 3 | "**/.yarn": true, 4 | "**/.pnp.*": true 5 | }, 6 | "eslint.nodePath": ".yarn/sdks", 7 | "prettier.prettierPath": ".yarn/sdks/prettier/index.js", 8 | "typescript.tsdk": ".yarn/sdks/typescript/lib", 9 | "typescript.enablePromptUseWorkspaceTsdk": true 10 | } 11 | -------------------------------------------------------------------------------- /.yarn/sdks/eslint/bin/eslint.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = createRequire(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require eslint/bin/eslint.js 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real eslint/bin/eslint.js your application uses 20 | module.exports = absRequire(`eslint/bin/eslint.js`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/eslint/lib/api.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = createRequire(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require eslint 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real eslint your application uses 20 | module.exports = absRequire(`eslint`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/eslint/lib/unsupported-api.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = createRequire(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require eslint/use-at-your-own-risk 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real eslint/use-at-your-own-risk your application uses 20 | module.exports = absRequire(`eslint/use-at-your-own-risk`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/eslint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint", 3 | "version": "8.19.0-sdk", 4 | "main": "./lib/api.js", 5 | "type": "commonjs", 6 | "bin": { 7 | "eslint": "./bin/eslint.js" 8 | }, 9 | "exports": { 10 | "./package.json": "./package.json", 11 | ".": "./lib/api.js", 12 | "./use-at-your-own-risk": "./lib/unsupported-api.js" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.yarn/sdks/integrations.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by @yarnpkg/sdks. 2 | # Manual changes might be lost! 3 | 4 | integrations: 5 | - vscode 6 | -------------------------------------------------------------------------------- /.yarn/sdks/prettier/bin-prettier.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = createRequire(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require prettier/bin-prettier.js 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real prettier/bin-prettier.js your application uses 20 | module.exports = absRequire(`prettier/bin-prettier.js`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/prettier/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = createRequire(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require prettier 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real prettier your application uses 20 | module.exports = absRequire(`prettier`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/prettier/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prettier", 3 | "version": "2.7.1-sdk", 4 | "main": "./index.js", 5 | "type": "commonjs", 6 | "bin": "./bin-prettier.js" 7 | } 8 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/bin/tsc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = createRequire(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require typescript/bin/tsc 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real typescript/bin/tsc your application uses 20 | module.exports = absRequire(`typescript/bin/tsc`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/bin/tsserver: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = createRequire(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require typescript/bin/tsserver 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real typescript/bin/tsserver your application uses 20 | module.exports = absRequire(`typescript/bin/tsserver`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/lib/tsc.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = createRequire(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require typescript/lib/tsc.js 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real typescript/lib/tsc.js your application uses 20 | module.exports = absRequire(`typescript/lib/tsc.js`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/lib/tsserver.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = createRequire(absPnpApiPath); 11 | 12 | const moduleWrapper = tsserver => { 13 | if (!process.versions.pnp) { 14 | return tsserver; 15 | } 16 | 17 | const {isAbsolute} = require(`path`); 18 | const pnpApi = require(`pnpapi`); 19 | 20 | const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//); 21 | const isPortal = str => str.startsWith("portal:/"); 22 | const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`); 23 | 24 | const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => { 25 | return `${locator.name}@${locator.reference}`; 26 | })); 27 | 28 | // VSCode sends the zip paths to TS using the "zip://" prefix, that TS 29 | // doesn't understand. This layer makes sure to remove the protocol 30 | // before forwarding it to TS, and to add it back on all returned paths. 31 | 32 | function toEditorPath(str) { 33 | // We add the `zip:` prefix to both `.zip/` paths and virtual paths 34 | if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) { 35 | // We also take the opportunity to turn virtual paths into physical ones; 36 | // this makes it much easier to work with workspaces that list peer 37 | // dependencies, since otherwise Ctrl+Click would bring us to the virtual 38 | // file instances instead of the real ones. 39 | // 40 | // We only do this to modules owned by the the dependency tree roots. 41 | // This avoids breaking the resolution when jumping inside a vendor 42 | // with peer dep (otherwise jumping into react-dom would show resolution 43 | // errors on react). 44 | // 45 | const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str; 46 | if (resolved) { 47 | const locator = pnpApi.findPackageLocator(resolved); 48 | if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) { 49 | str = resolved; 50 | } 51 | } 52 | 53 | str = normalize(str); 54 | 55 | if (str.match(/\.zip\//)) { 56 | switch (hostInfo) { 57 | // Absolute VSCode `Uri.fsPath`s need to start with a slash. 58 | // VSCode only adds it automatically for supported schemes, 59 | // so we have to do it manually for the `zip` scheme. 60 | // The path needs to start with a caret otherwise VSCode doesn't handle the protocol 61 | // 62 | // Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910 63 | // 64 | // 2021-10-08: VSCode changed the format in 1.61. 65 | // Before | ^zip:/c:/foo/bar.zip/package.json 66 | // After | ^/zip//c:/foo/bar.zip/package.json 67 | // 68 | // 2022-04-06: VSCode changed the format in 1.66. 69 | // Before | ^/zip//c:/foo/bar.zip/package.json 70 | // After | ^/zip/c:/foo/bar.zip/package.json 71 | // 72 | // 2022-05-06: VSCode changed the format in 1.68 73 | // Before | ^/zip/c:/foo/bar.zip/package.json 74 | // After | ^/zip//c:/foo/bar.zip/package.json 75 | // 76 | case `vscode <1.61`: { 77 | str = `^zip:${str}`; 78 | } break; 79 | 80 | case `vscode <1.66`: { 81 | str = `^/zip/${str}`; 82 | } break; 83 | 84 | case `vscode <1.68`: { 85 | str = `^/zip${str}`; 86 | } break; 87 | 88 | case `vscode`: { 89 | str = `^/zip/${str}`; 90 | } break; 91 | 92 | // To make "go to definition" work, 93 | // We have to resolve the actual file system path from virtual path 94 | // and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip) 95 | case `coc-nvim`: { 96 | str = normalize(resolved).replace(/\.zip\//, `.zip::`); 97 | str = resolve(`zipfile:${str}`); 98 | } break; 99 | 100 | // Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server) 101 | // We have to resolve the actual file system path from virtual path, 102 | // everything else is up to neovim 103 | case `neovim`: { 104 | str = normalize(resolved).replace(/\.zip\//, `.zip::`); 105 | str = `zipfile://${str}`; 106 | } break; 107 | 108 | default: { 109 | str = `zip:${str}`; 110 | } break; 111 | } 112 | } else { 113 | str = str.replace(/^\/?/, process.platform === `win32` ? `` : `/`); 114 | } 115 | } 116 | 117 | return str; 118 | } 119 | 120 | function fromEditorPath(str) { 121 | switch (hostInfo) { 122 | case `coc-nvim`: { 123 | str = str.replace(/\.zip::/, `.zip/`); 124 | // The path for coc-nvim is in format of //zipfile://.yarn/... 125 | // So in order to convert it back, we use .* to match all the thing 126 | // before `zipfile:` 127 | return process.platform === `win32` 128 | ? str.replace(/^.*zipfile:\//, ``) 129 | : str.replace(/^.*zipfile:/, ``); 130 | } break; 131 | 132 | case `neovim`: { 133 | str = str.replace(/\.zip::/, `.zip/`); 134 | // The path for neovim is in format of zipfile:////.yarn/... 135 | return str.replace(/^zipfile:\/\//, ``); 136 | } break; 137 | 138 | case `vscode`: 139 | default: { 140 | return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`) 141 | } break; 142 | } 143 | } 144 | 145 | // Force enable 'allowLocalPluginLoads' 146 | // TypeScript tries to resolve plugins using a path relative to itself 147 | // which doesn't work when using the global cache 148 | // https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238 149 | // VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but 150 | // TypeScript already does local loads and if this code is running the user trusts the workspace 151 | // https://github.com/microsoft/vscode/issues/45856 152 | const ConfiguredProject = tsserver.server.ConfiguredProject; 153 | const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype; 154 | ConfiguredProject.prototype.enablePluginsWithOptions = function() { 155 | this.projectService.allowLocalPluginLoads = true; 156 | return originalEnablePluginsWithOptions.apply(this, arguments); 157 | }; 158 | 159 | // And here is the point where we hijack the VSCode <-> TS communications 160 | // by adding ourselves in the middle. We locate everything that looks 161 | // like an absolute path of ours and normalize it. 162 | 163 | const Session = tsserver.server.Session; 164 | const {onMessage: originalOnMessage, send: originalSend} = Session.prototype; 165 | let hostInfo = `unknown`; 166 | 167 | Object.assign(Session.prototype, { 168 | onMessage(/** @type {string | object} */ message) { 169 | const isStringMessage = typeof message === 'string'; 170 | const parsedMessage = isStringMessage ? JSON.parse(message) : message; 171 | 172 | if ( 173 | parsedMessage != null && 174 | typeof parsedMessage === `object` && 175 | parsedMessage.arguments && 176 | typeof parsedMessage.arguments.hostInfo === `string` 177 | ) { 178 | hostInfo = parsedMessage.arguments.hostInfo; 179 | if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) { 180 | const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match( 181 | // The RegExp from https://semver.org/ but without the caret at the start 182 | /(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ 183 | ) ?? []).map(Number) 184 | 185 | if (major === 1) { 186 | if (minor < 61) { 187 | hostInfo += ` <1.61`; 188 | } else if (minor < 66) { 189 | hostInfo += ` <1.66`; 190 | } else if (minor < 68) { 191 | hostInfo += ` <1.68`; 192 | } 193 | } 194 | } 195 | } 196 | 197 | const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => { 198 | return typeof value === 'string' ? fromEditorPath(value) : value; 199 | }); 200 | 201 | return originalOnMessage.call( 202 | this, 203 | isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON) 204 | ); 205 | }, 206 | 207 | send(/** @type {any} */ msg) { 208 | return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => { 209 | return typeof value === `string` ? toEditorPath(value) : value; 210 | }))); 211 | } 212 | }); 213 | 214 | return tsserver; 215 | }; 216 | 217 | if (existsSync(absPnpApiPath)) { 218 | if (!process.versions.pnp) { 219 | // Setup the environment to be able to require typescript/lib/tsserver.js 220 | require(absPnpApiPath).setup(); 221 | } 222 | } 223 | 224 | // Defer to the real typescript/lib/tsserver.js your application uses 225 | module.exports = moduleWrapper(absRequire(`typescript/lib/tsserver.js`)); 226 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/lib/tsserverlibrary.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = createRequire(absPnpApiPath); 11 | 12 | const moduleWrapper = tsserver => { 13 | if (!process.versions.pnp) { 14 | return tsserver; 15 | } 16 | 17 | const {isAbsolute} = require(`path`); 18 | const pnpApi = require(`pnpapi`); 19 | 20 | const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//); 21 | const isPortal = str => str.startsWith("portal:/"); 22 | const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`); 23 | 24 | const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => { 25 | return `${locator.name}@${locator.reference}`; 26 | })); 27 | 28 | // VSCode sends the zip paths to TS using the "zip://" prefix, that TS 29 | // doesn't understand. This layer makes sure to remove the protocol 30 | // before forwarding it to TS, and to add it back on all returned paths. 31 | 32 | function toEditorPath(str) { 33 | // We add the `zip:` prefix to both `.zip/` paths and virtual paths 34 | if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) { 35 | // We also take the opportunity to turn virtual paths into physical ones; 36 | // this makes it much easier to work with workspaces that list peer 37 | // dependencies, since otherwise Ctrl+Click would bring us to the virtual 38 | // file instances instead of the real ones. 39 | // 40 | // We only do this to modules owned by the the dependency tree roots. 41 | // This avoids breaking the resolution when jumping inside a vendor 42 | // with peer dep (otherwise jumping into react-dom would show resolution 43 | // errors on react). 44 | // 45 | const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str; 46 | if (resolved) { 47 | const locator = pnpApi.findPackageLocator(resolved); 48 | if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) { 49 | str = resolved; 50 | } 51 | } 52 | 53 | str = normalize(str); 54 | 55 | if (str.match(/\.zip\//)) { 56 | switch (hostInfo) { 57 | // Absolute VSCode `Uri.fsPath`s need to start with a slash. 58 | // VSCode only adds it automatically for supported schemes, 59 | // so we have to do it manually for the `zip` scheme. 60 | // The path needs to start with a caret otherwise VSCode doesn't handle the protocol 61 | // 62 | // Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910 63 | // 64 | // 2021-10-08: VSCode changed the format in 1.61. 65 | // Before | ^zip:/c:/foo/bar.zip/package.json 66 | // After | ^/zip//c:/foo/bar.zip/package.json 67 | // 68 | // 2022-04-06: VSCode changed the format in 1.66. 69 | // Before | ^/zip//c:/foo/bar.zip/package.json 70 | // After | ^/zip/c:/foo/bar.zip/package.json 71 | // 72 | // 2022-05-06: VSCode changed the format in 1.68 73 | // Before | ^/zip/c:/foo/bar.zip/package.json 74 | // After | ^/zip//c:/foo/bar.zip/package.json 75 | // 76 | case `vscode <1.61`: { 77 | str = `^zip:${str}`; 78 | } break; 79 | 80 | case `vscode <1.66`: { 81 | str = `^/zip/${str}`; 82 | } break; 83 | 84 | case `vscode <1.68`: { 85 | str = `^/zip${str}`; 86 | } break; 87 | 88 | case `vscode`: { 89 | str = `^/zip/${str}`; 90 | } break; 91 | 92 | // To make "go to definition" work, 93 | // We have to resolve the actual file system path from virtual path 94 | // and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip) 95 | case `coc-nvim`: { 96 | str = normalize(resolved).replace(/\.zip\//, `.zip::`); 97 | str = resolve(`zipfile:${str}`); 98 | } break; 99 | 100 | // Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server) 101 | // We have to resolve the actual file system path from virtual path, 102 | // everything else is up to neovim 103 | case `neovim`: { 104 | str = normalize(resolved).replace(/\.zip\//, `.zip::`); 105 | str = `zipfile://${str}`; 106 | } break; 107 | 108 | default: { 109 | str = `zip:${str}`; 110 | } break; 111 | } 112 | } else { 113 | str = str.replace(/^\/?/, process.platform === `win32` ? `` : `/`); 114 | } 115 | } 116 | 117 | return str; 118 | } 119 | 120 | function fromEditorPath(str) { 121 | switch (hostInfo) { 122 | case `coc-nvim`: { 123 | str = str.replace(/\.zip::/, `.zip/`); 124 | // The path for coc-nvim is in format of //zipfile://.yarn/... 125 | // So in order to convert it back, we use .* to match all the thing 126 | // before `zipfile:` 127 | return process.platform === `win32` 128 | ? str.replace(/^.*zipfile:\//, ``) 129 | : str.replace(/^.*zipfile:/, ``); 130 | } break; 131 | 132 | case `neovim`: { 133 | str = str.replace(/\.zip::/, `.zip/`); 134 | // The path for neovim is in format of zipfile:////.yarn/... 135 | return str.replace(/^zipfile:\/\//, ``); 136 | } break; 137 | 138 | case `vscode`: 139 | default: { 140 | return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`) 141 | } break; 142 | } 143 | } 144 | 145 | // Force enable 'allowLocalPluginLoads' 146 | // TypeScript tries to resolve plugins using a path relative to itself 147 | // which doesn't work when using the global cache 148 | // https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238 149 | // VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but 150 | // TypeScript already does local loads and if this code is running the user trusts the workspace 151 | // https://github.com/microsoft/vscode/issues/45856 152 | const ConfiguredProject = tsserver.server.ConfiguredProject; 153 | const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype; 154 | ConfiguredProject.prototype.enablePluginsWithOptions = function() { 155 | this.projectService.allowLocalPluginLoads = true; 156 | return originalEnablePluginsWithOptions.apply(this, arguments); 157 | }; 158 | 159 | // And here is the point where we hijack the VSCode <-> TS communications 160 | // by adding ourselves in the middle. We locate everything that looks 161 | // like an absolute path of ours and normalize it. 162 | 163 | const Session = tsserver.server.Session; 164 | const {onMessage: originalOnMessage, send: originalSend} = Session.prototype; 165 | let hostInfo = `unknown`; 166 | 167 | Object.assign(Session.prototype, { 168 | onMessage(/** @type {string | object} */ message) { 169 | const isStringMessage = typeof message === 'string'; 170 | const parsedMessage = isStringMessage ? JSON.parse(message) : message; 171 | 172 | if ( 173 | parsedMessage != null && 174 | typeof parsedMessage === `object` && 175 | parsedMessage.arguments && 176 | typeof parsedMessage.arguments.hostInfo === `string` 177 | ) { 178 | hostInfo = parsedMessage.arguments.hostInfo; 179 | if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) { 180 | const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match( 181 | // The RegExp from https://semver.org/ but without the caret at the start 182 | /(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ 183 | ) ?? []).map(Number) 184 | 185 | if (major === 1) { 186 | if (minor < 61) { 187 | hostInfo += ` <1.61`; 188 | } else if (minor < 66) { 189 | hostInfo += ` <1.66`; 190 | } else if (minor < 68) { 191 | hostInfo += ` <1.68`; 192 | } 193 | } 194 | } 195 | } 196 | 197 | const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => { 198 | return typeof value === 'string' ? fromEditorPath(value) : value; 199 | }); 200 | 201 | return originalOnMessage.call( 202 | this, 203 | isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON) 204 | ); 205 | }, 206 | 207 | send(/** @type {any} */ msg) { 208 | return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => { 209 | return typeof value === `string` ? toEditorPath(value) : value; 210 | }))); 211 | } 212 | }); 213 | 214 | return tsserver; 215 | }; 216 | 217 | if (existsSync(absPnpApiPath)) { 218 | if (!process.versions.pnp) { 219 | // Setup the environment to be able to require typescript/lib/tsserverlibrary.js 220 | require(absPnpApiPath).setup(); 221 | } 222 | } 223 | 224 | // Defer to the real typescript/lib/tsserverlibrary.js your application uses 225 | module.exports = moduleWrapper(absRequire(`typescript/lib/tsserverlibrary.js`)); 226 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/lib/typescript.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = createRequire(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require typescript 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real typescript your application uses 20 | module.exports = absRequire(`typescript`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript", 3 | "version": "4.7.4-sdk", 4 | "main": "./lib/typescript.js", 5 | "type": "commonjs", 6 | "bin": { 7 | "tsc": "./bin/tsc", 8 | "tsserver": "./bin/tsserver" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abipub/evm-proxy-detection/7acfdfe8657296d43f0b1f6325443cf527846302/.yarnrc.yml -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Gnosis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # evm-proxy-detection 2 | 3 | A zero dependencies module to detect proxy contracts and their target addresses using an [EIP-1193](https://eips.ethereum.org/EIPS/eip-1193) compatible JSON-RPC `request` function. 4 | 5 | It detects the following kinds of proxies: 6 | 7 | - [EIP-1167](https://eips.ethereum.org/EIPS/eip-1167) Minimal Proxy Contract 8 | - [EIP-1967](https://eips.ethereum.org/EIPS/eip-1967) Transparent Proxy Pattern 9 | - [EIP-897](https://eips.ethereum.org/EIPS/eip-897) Delegate Proxy Pattern 10 | - [EIP-1822](https://eips.ethereum.org/EIPS/eip-1822) Universal Upgradeable Proxy Standard 11 | - OpenZeppelin Proxy Pattern 12 | - Safe Proxy Contract 13 | - Additional custom proxies: Compound's Comptroller, Balancer's BatchRelayer 14 | 15 | ## Installation 16 | 17 | This module is distributed via npm. For adding it to your project, run: 18 | 19 | ``` 20 | npm install --save evm-proxy-detection 21 | ``` 22 | 23 | To install it using yarn, run: 24 | 25 | ``` 26 | yarn add evm-proxy-detection 27 | ``` 28 | 29 | ## How to use 30 | 31 | 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. 32 | It returns a promise that resolves to result object with the proxy target address, i.e., the address of the contract implementing the logic, and information about the detected proxy type. 33 | The promise resolves to `null` if no proxy can be detected. 34 | 35 | ### Viem 36 | 37 | ```ts 38 | import { createPublicClient, http } from 'viem' 39 | 40 | const client = createPublicClient({ 41 | chain, 42 | // enable json-rpc batching to reduce the number of http requests 43 | transport: http(undefined, { batch: true }), 44 | }) 45 | 46 | const result = await detectProxy(address, client.request) 47 | // logs: { target: "0x4bd844F72A8edD323056130A86FC624D0dbcF5b0", type: 'Eip1967', immutable: false } 48 | ``` 49 | 50 | ### Ethers with an adapter function 51 | 52 | ```ts 53 | import { InfuraProvider } from '@ethersproject/providers' 54 | import detectProxy from 'evm-proxy-detection' 55 | 56 | const infuraProvider = new InfuraProvider(1, process.env.INFURA_API_KEY) 57 | const requestFunc = ({ method, params }) => infuraProvider.send(method, params) 58 | 59 | const target = await detectProxy( 60 | '0xA7AeFeaD2F25972D80516628417ac46b3F2604Af', 61 | requestFunc 62 | ) 63 | console.log(target) 64 | // logs: { target: "0x4bd844F72A8edD323056130A86FC624D0dbcF5b0", type: 'Eip1967', immutable: false } 65 | ``` 66 | 67 | ### Web3 with an EIP1193 provider 68 | 69 | 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. 70 | Otherwise, you can use providers like [eip1193-provider](https://www.npmjs.com/package/eip1193-provider). 71 | 72 | ```ts 73 | import Web3 from 'web3' 74 | import detectProxy from 'evm-proxy-detection' 75 | 76 | const web3 = new Web3(Web3.givenProvider || 'ws://localhost:8545') 77 | 78 | const result = await detectProxy( 79 | '0xA7AeFeaD2F25972D80516628417ac46b3F2604Af', 80 | web3.currentProvider.request 81 | ) 82 | console.log(result) 83 | // logs: { target: "0x4bd844F72A8edD323056130A86FC624D0dbcF5b0", type: 'Eip1967', immutable: false } 84 | ``` 85 | 86 | ## API 87 | 88 | ```ts 89 | detectProxy(address: `0x${string}`, jsonRpcRequest: EIP1193ProviderRequestFunc, blockTag?: BlockTag): Promise 90 | ``` 91 | 92 | **Arguments** 93 | 94 | - `address`: The address of the proxy contract 95 | - `jsonRpcRequest`: A JSON-RPC request function, compatible with [EIP-1193](https://eips.ethereum.org/EIPS/eip-1193) (`(method: string, params: any[]) => Promise`) 96 | - `blockTag` (optional: BlockTag): `"earliest"`, `"latest"`, `"pending"` or hex block number, default is `"latest"` 97 | 98 | **Return value** 99 | 100 | The function returns a promise that will generally resolve to either a `Result` object describing the detected proxy or `null` if it couldn't detect one. 101 | 102 | ```ts 103 | interface Result { 104 | target: `0x${string}` 105 | immutable: boolean 106 | type: ProxyType 107 | } 108 | ``` 109 | 110 | - `target`: The address (non-checksummed) of the proxy target 111 | - `immutable`: Indicates if the proxy is immutable, meaning that the target address will never change 112 | - `type`: Identifies the detected proxy type (possible values shown below) 113 | 114 | ```ts 115 | enum ProxyType { 116 | Eip1167 = 'Eip1167', 117 | Eip1967Direct = 'Eip1967Direct', 118 | Eip1967Beacon = 'Eip1967Beacon', 119 | Eip1822 = 'Eip1822', 120 | Eip897 = 'Eip897', 121 | OpenZeppelin = 'OpenZeppelin', 122 | Safe = 'Safe', 123 | Comptroller = 'Comptroller', 124 | BatchRelayer = 'BatchRelayer', 125 | } 126 | ``` 127 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | testTimeout: 20000, 6 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "evm-proxy-detection", 3 | "version": "2.1.0", 4 | "description": "Detect proxy contracts and their target addresses using an EIP-1193 compatible JSON-RPC request function", 5 | "repository": "https://github.com/abipub/evm-proxy-detection.git", 6 | "author": "Jan-Felix ", 7 | "license": "MIT", 8 | "main": "build/cjs/index.js", 9 | "typings": "build/cjs/index.d.ts", 10 | "module": "build/esm/index.js", 11 | "exports": { 12 | ".": { 13 | "types": "./build/cjs/index.d.ts", 14 | "import": "./build/esm/index.js", 15 | "require": "./build/cjs/index.js" 16 | } 17 | }, 18 | "files": [ 19 | "build", 20 | "LICENSE", 21 | "README.md" 22 | ], 23 | "scripts": { 24 | "prepack": "yarn build", 25 | "build": "rimraf build && yarn build:cjs && yarn build:esm", 26 | "build:cjs": "tsc -p tsconfig.cjs.json", 27 | "build:esm": "tsc -p tsconfig.esm.json", 28 | "check": "yarn check:prettier && yarn check:lint && yarn check:spelling", 29 | "check:lint": "eslint src test --ext .ts", 30 | "check:prettier": "prettier \"src/**/*.ts\" --list-different", 31 | "check:spelling": "cspell \"**\"", 32 | "fix": "yarn fix:prettier && yarn fix:lint", 33 | "fix:prettier": "prettier \"src/**/*.ts\" --write", 34 | "fix:lint": "eslint src --ext .ts --fix", 35 | "test": "env-cmd jest" 36 | }, 37 | "devDependencies": { 38 | "@ethersproject/providers": "^5.6.8", 39 | "@types/jest": "^29.5.12", 40 | "@types/node": "^18.0.4", 41 | "@typescript-eslint/eslint-plugin": "^5.30.6", 42 | "@typescript-eslint/parser": "^5.30.6", 43 | "cspell": "^6.2.3", 44 | "env-cmd": "^10.1.0", 45 | "eslint": "^8.19.0", 46 | "eslint-config-prettier": "^8.5.0", 47 | "eslint-plugin-eslint-comments": "^3.2.0", 48 | "eslint-plugin-import": "^2.26.0", 49 | "jest": "^29.7.0", 50 | "prettier": "^2.7.1", 51 | "rimraf": "^3.0.2", 52 | "ts-jest": "^29.1.2", 53 | "ts-node": "^10.9.1", 54 | "typescript": "^4.7.4" 55 | }, 56 | "packageManager": "yarn@4.1.1" 57 | } 58 | -------------------------------------------------------------------------------- /src/eip1167.ts: -------------------------------------------------------------------------------- 1 | const EIP_1167_BYTECODE_PREFIX = '0x363d3d373d3d3d363d' 2 | const EIP_1167_BYTECODE_SUFFIX = '57fd5bf3' 3 | 4 | export const parse1167Bytecode = (bytecode: unknown): `0x${string}` => { 5 | if ( 6 | typeof bytecode !== 'string' || 7 | !bytecode.startsWith(EIP_1167_BYTECODE_PREFIX) 8 | ) { 9 | throw new Error('Not an EIP-1167 bytecode') 10 | } 11 | 12 | // detect length of address (20 bytes non-optimized, 0 < N < 20 bytes for vanity addresses) 13 | const pushNHex = bytecode.substring( 14 | EIP_1167_BYTECODE_PREFIX.length, 15 | EIP_1167_BYTECODE_PREFIX.length + 2 16 | ) 17 | // push1 ... push20 use opcodes 0x60 ... 0x73 18 | const addressLength = parseInt(pushNHex, 16) - 0x5f 19 | 20 | if (addressLength < 1 || addressLength > 20) { 21 | throw new Error('Not an EIP-1167 bytecode') 22 | } 23 | 24 | const addressFromBytecode = bytecode.substring( 25 | EIP_1167_BYTECODE_PREFIX.length + 2, 26 | EIP_1167_BYTECODE_PREFIX.length + 2 + addressLength * 2 // address length is in bytes, 2 hex chars make up 1 byte 27 | ) 28 | 29 | const SUFFIX_OFFSET_FROM_ADDRESS_END = 22 30 | if ( 31 | !bytecode 32 | .substring( 33 | EIP_1167_BYTECODE_PREFIX.length + 34 | 2 + 35 | addressLength * 2 + 36 | SUFFIX_OFFSET_FROM_ADDRESS_END 37 | ) 38 | .startsWith(EIP_1167_BYTECODE_SUFFIX) 39 | ) { 40 | throw new Error('Not an EIP-1167 bytecode') 41 | } 42 | 43 | // padStart is needed for vanity addresses 44 | return `0x${addressFromBytecode.padStart(40, '0')}` 45 | } 46 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { parse1167Bytecode } from './eip1167' 2 | import { readString } from './readString' 3 | import { 4 | BlockTag, 5 | EIP1193ProviderRequestFunc, 6 | ProxyType, 7 | Result, 8 | } from './types' 9 | 10 | // obtained as bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1) 11 | const EIP_1967_LOGIC_SLOT = 12 | '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' 13 | 14 | // obtained as keccak256("org.zeppelinos.proxy.implementation") 15 | const OPEN_ZEPPELIN_IMPLEMENTATION_SLOT = 16 | '0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3' 17 | 18 | // obtained as keccak256("PROXIABLE") 19 | const EIP_1822_LOGIC_SLOT = 20 | '0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7' 21 | 22 | // obtained as bytes32(uint256(keccak256('eip1967.proxy.beacon')) - 1) 23 | const EIP_1967_BEACON_SLOT = 24 | '0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50' 25 | 26 | const EIP_897_INTERFACE = [ 27 | // bytes4(keccak256("implementation()")) padded to 32 bytes 28 | '0x5c60da1b00000000000000000000000000000000000000000000000000000000', 29 | 30 | // bytes4(keccak256("proxyType()")) padded to 32 bytes 31 | '0x4555d5c900000000000000000000000000000000000000000000000000000000', 32 | ] 33 | 34 | const EIP_1967_BEACON_METHODS = [ 35 | // bytes4(keccak256("implementation()")) padded to 32 bytes 36 | '0x5c60da1b00000000000000000000000000000000000000000000000000000000', 37 | // bytes4(keccak256("childImplementation()")) padded to 32 bytes 38 | // some implementations use this over the standard method name so that the beacon contract is not detected as an EIP-897 proxy itself 39 | '0xda52571600000000000000000000000000000000000000000000000000000000', 40 | ] 41 | 42 | const SAFE_PROXY_INTERFACE = [ 43 | // bytes4(keccak256("masterCopy()")) padded to 32 bytes 44 | '0xa619486e00000000000000000000000000000000000000000000000000000000', 45 | ] 46 | 47 | const COMPTROLLER_PROXY_INTERFACE = [ 48 | // bytes4(keccak256("comptrollerImplementation()")) padded to 32 bytes 49 | '0xbb82aa5e00000000000000000000000000000000000000000000000000000000', 50 | ] 51 | 52 | const BATCH_RELAYER_INTERFACE = [ 53 | // bytes4(keccak256("version()")) padded to 32 bytes 54 | '0x54fd4d5000000000000000000000000000000000000000000000000000000000', 55 | // bytes4(keccak256("getLibrary()")) padded to 32 bytes 56 | '0x7678922e00000000000000000000000000000000000000000000000000000000', 57 | ] 58 | 59 | const detectProxy = ( 60 | proxyAddress: `0x${string}`, 61 | jsonRpcRequest: EIP1193ProviderRequestFunc, 62 | blockTag: BlockTag = 'latest' 63 | ): Promise => 64 | Promise.any([ 65 | // EIP-1167 Minimal Proxy Contract 66 | jsonRpcRequest({ 67 | method: 'eth_getCode', 68 | params: [proxyAddress, blockTag], 69 | }) 70 | .then(parse1167Bytecode) 71 | .then(readAddress) 72 | .then((target) => ({ 73 | target, 74 | type: ProxyType.Eip1167, 75 | immutable: true, 76 | })), 77 | 78 | // EIP-1967 direct proxy 79 | jsonRpcRequest({ 80 | method: 'eth_getStorageAt', 81 | params: [proxyAddress, EIP_1967_LOGIC_SLOT, blockTag], 82 | }) 83 | .then(readAddress) 84 | .then((target) => ({ 85 | target, 86 | type: ProxyType.Eip1967Direct, 87 | immutable: false, 88 | })), 89 | 90 | // EIP-1967 beacon proxy 91 | jsonRpcRequest({ 92 | method: 'eth_getStorageAt', 93 | params: [proxyAddress, EIP_1967_BEACON_SLOT, blockTag], 94 | }) 95 | .then(readAddress) 96 | .then((beaconAddress) => 97 | jsonRpcRequest({ 98 | method: 'eth_call', 99 | params: [ 100 | { 101 | to: beaconAddress, 102 | data: EIP_1967_BEACON_METHODS[0], 103 | }, 104 | blockTag, 105 | ], 106 | }).catch(() => 107 | jsonRpcRequest({ 108 | method: 'eth_call', 109 | params: [ 110 | { 111 | to: beaconAddress, 112 | data: EIP_1967_BEACON_METHODS[1], 113 | }, 114 | blockTag, 115 | ], 116 | }) 117 | ) 118 | ) 119 | .then(readAddress) 120 | .then((target) => ({ 121 | target, 122 | type: ProxyType.Eip1967Beacon, 123 | immutable: false, 124 | })), 125 | 126 | // OpenZeppelin proxy pattern 127 | jsonRpcRequest({ 128 | method: 'eth_getStorageAt', 129 | params: [proxyAddress, OPEN_ZEPPELIN_IMPLEMENTATION_SLOT, blockTag], 130 | }) 131 | .then(readAddress) 132 | .then((target) => ({ 133 | target, 134 | type: ProxyType.OpenZeppelin, 135 | immutable: false, 136 | })), 137 | 138 | // EIP-1822 Universal Upgradeable Proxy Standard 139 | jsonRpcRequest({ 140 | method: 'eth_getStorageAt', 141 | params: [proxyAddress, EIP_1822_LOGIC_SLOT, blockTag], 142 | }) 143 | .then(readAddress) 144 | .then((target) => ({ 145 | target, 146 | type: ProxyType.Eip1822, 147 | immutable: false, 148 | })), 149 | 150 | // EIP-897 DelegateProxy pattern 151 | jsonRpcRequest({ 152 | method: 'eth_call', 153 | params: [ 154 | { 155 | to: proxyAddress, 156 | data: EIP_897_INTERFACE[0], 157 | }, 158 | blockTag, 159 | ], 160 | }) 161 | .then(readAddress) 162 | .then(async (target) => ({ 163 | target, 164 | type: ProxyType.Eip897, 165 | // proxyType === 1 means that the proxy is immutable 166 | immutable: 167 | (await jsonRpcRequest({ 168 | method: 'eth_call', 169 | params: [ 170 | { 171 | to: proxyAddress, 172 | data: EIP_897_INTERFACE[1], 173 | }, 174 | blockTag, 175 | ], 176 | })) === 177 | '0x0000000000000000000000000000000000000000000000000000000000000001', 178 | })), 179 | 180 | // SafeProxy contract 181 | jsonRpcRequest({ 182 | method: 'eth_call', 183 | params: [ 184 | { 185 | to: proxyAddress, 186 | data: SAFE_PROXY_INTERFACE[0], 187 | }, 188 | blockTag, 189 | ], 190 | }) 191 | .then(readAddress) 192 | .then((target) => ({ 193 | target, 194 | type: ProxyType.Safe, 195 | immutable: false, 196 | })), 197 | 198 | // Comptroller proxy 199 | jsonRpcRequest({ 200 | method: 'eth_call', 201 | params: [ 202 | { 203 | to: proxyAddress, 204 | data: COMPTROLLER_PROXY_INTERFACE[0], 205 | }, 206 | blockTag, 207 | ], 208 | }) 209 | .then(readAddress) 210 | .then((target) => ({ 211 | target, 212 | type: ProxyType.Comptroller, 213 | immutable: false, 214 | })), 215 | 216 | /// Balancer BatchRelayer 217 | jsonRpcRequest({ 218 | method: 'eth_call', 219 | params: [ 220 | { to: proxyAddress, data: BATCH_RELAYER_INTERFACE[0] }, 221 | blockTag, 222 | ], 223 | }) 224 | .then(readJsonString) 225 | .then((json) => { 226 | if (json.name === 'BatchRelayer') { 227 | return jsonRpcRequest({ 228 | method: 'eth_call', 229 | params: [ 230 | { to: proxyAddress, data: BATCH_RELAYER_INTERFACE[1] }, 231 | blockTag, 232 | ], 233 | }) 234 | } 235 | throw new Error('Not a BatchRelayer') 236 | }) 237 | .then(readAddress) 238 | .then((target) => ({ 239 | target, 240 | type: ProxyType.BatchRelayer, 241 | immutable: true, 242 | })), 243 | ]).catch(() => null) 244 | 245 | const zeroAddress = '0x' + '0'.repeat(40) 246 | const readAddress = (value: unknown) => { 247 | if (typeof value !== 'string' || value === '0x') { 248 | throw new Error(`Invalid address value: ${value}`) 249 | } 250 | 251 | let address = value 252 | if (address.length === 66) { 253 | address = '0x' + address.slice(-40) 254 | } 255 | 256 | if (address === zeroAddress) { 257 | throw new Error('Empty address') 258 | } 259 | 260 | return address as `0x${string}` 261 | } 262 | 263 | const readJsonString = (value: unknown) => { 264 | if (typeof value !== 'string') { 265 | throw new Error(`Invalid hex string value: ${value}`) 266 | } 267 | return JSON.parse(readString(value as string)) 268 | } 269 | 270 | export default detectProxy 271 | 272 | export { parse1167Bytecode } 273 | -------------------------------------------------------------------------------- /src/readString.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts an ABI-encoded hex string from a JSON-RPC response to a UTF-8 string. 3 | * @param hex - The ABI-encoded hex string from JSON-RPC response (must include '0x' prefix) 4 | * @returns The decoded UTF-8 string 5 | */ 6 | export function readString(hex: string): string { 7 | if (typeof hex !== 'string') { 8 | throw new Error('Input must be a string') 9 | } 10 | 11 | if (!hex.startsWith('0x')) { 12 | throw new Error('Hex string must start with 0x') 13 | } 14 | 15 | // Remove '0x' prefix 16 | const cleanHex = hex.slice(2) 17 | 18 | // Handle empty response 19 | if (cleanHex === '') { 20 | return '' 21 | } 22 | 23 | // Ensure the hex string has an even length 24 | if (cleanHex.length % 2 !== 0) { 25 | throw new Error('Invalid hex string length') 26 | } 27 | 28 | // First 32 bytes (64 hex chars) contain the offset to the string data 29 | const offsetHex = cleanHex.slice(0, 64) 30 | const offset = parseInt(offsetHex, 16) 31 | 32 | if (isNaN(offset) || offset !== 32) { 33 | throw new Error('Invalid string offset') 34 | } 35 | 36 | // Next 32 bytes (64 hex chars) contain the length of the string in bytes 37 | const lengthHex = cleanHex.slice(64, 128) 38 | const length = parseInt(lengthHex, 16) 39 | 40 | if (isNaN(length)) { 41 | throw new Error('Invalid string length') 42 | } 43 | 44 | // Get the actual string data (padded to multiple of 32 bytes) 45 | const stringHex = cleanHex.slice(128, 128 + length * 2) 46 | 47 | // Convert hex string to bytes 48 | const bytes = new Uint8Array(length) 49 | for (let i = 0; i < stringHex.length; i += 2) { 50 | const byte = parseInt(stringHex.slice(i, i + 2), 16) 51 | if (isNaN(byte)) { 52 | throw new Error('Invalid hex string') 53 | } 54 | bytes[i / 2] = byte 55 | } 56 | 57 | // Use TextDecoder to convert bytes to string 58 | // @ts-ignore It's available in Node.js and browser environments 59 | return new TextDecoder('utf-8').decode(bytes) 60 | } 61 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export enum ProxyType { 2 | Eip1167 = 'Eip1167', 3 | Eip1967Direct = 'Eip1967Direct', 4 | Eip1967Beacon = 'Eip1967Beacon', 5 | Eip1822 = 'Eip1822', 6 | Eip897 = 'Eip897', 7 | OpenZeppelin = 'OpenZeppelin', 8 | Safe = 'Safe', 9 | Comptroller = 'Comptroller', 10 | BatchRelayer = 'BatchRelayer', 11 | } 12 | 13 | export interface Result { 14 | target: `0x${string}` 15 | type: ProxyType 16 | immutable: boolean 17 | } 18 | 19 | export type BlockTag = number | 'earliest' | 'latest' | 'pending' 20 | 21 | export interface RequestArguments { 22 | method: string 23 | params: unknown[] 24 | } 25 | 26 | export type EIP1193ProviderRequestFunc = ( 27 | args: RequestArguments 28 | ) => Promise 29 | -------------------------------------------------------------------------------- /test/detectProxy.spec.ts: -------------------------------------------------------------------------------- 1 | import { InfuraProvider } from '@ethersproject/providers' 2 | import { EIP1193ProviderRequestFunc } from '../src/types' 3 | import detectProxy from '../src' 4 | 5 | describe('detectProxy', () => { 6 | const infuraProvider = new InfuraProvider(1, process.env.INFURA_API_KEY) 7 | const requestFunc: EIP1193ProviderRequestFunc = ({ method, params }) => 8 | infuraProvider.send(method, params) 9 | 10 | // TODO fix to a block number to keep test stable for eternity (requires Infura archive access) 11 | const BLOCK_TAG = 'latest' // 19741734 12 | 13 | it('detects EIP-1967 direct proxies', async () => { 14 | expect( 15 | await detectProxy( 16 | '0xA7AeFeaD2F25972D80516628417ac46b3F2604Af', 17 | requestFunc, 18 | BLOCK_TAG 19 | ) 20 | ).toEqual({ 21 | target: '0x4bd844f72a8edd323056130a86fc624d0dbcf5b0', 22 | immutable: false, 23 | type: 'Eip1967Direct', 24 | }) 25 | }) 26 | 27 | it('detects EIP-1967 beacon proxies', async () => { 28 | expect( 29 | await detectProxy( 30 | '0xDd4e2eb37268B047f55fC5cAf22837F9EC08A881', 31 | requestFunc, 32 | BLOCK_TAG 33 | ) 34 | ).toEqual({ 35 | target: '0xe5c048792dcf2e4a56000c8b6a47f21df22752d1', 36 | immutable: false, 37 | type: 'Eip1967Beacon', 38 | }) 39 | }) 40 | 41 | it('detects EIP-1967 beacon variant proxies', async () => { 42 | expect( 43 | await detectProxy( 44 | '0x114f1388fAB456c4bA31B1850b244Eedcd024136', 45 | requestFunc, 46 | BLOCK_TAG 47 | ) 48 | ).toEqual({ 49 | target: '0x0fa0fd98727c443dd5275774c44d27cff9d279ed', 50 | immutable: false, 51 | type: 'Eip1967Beacon', 52 | }) 53 | }) 54 | 55 | it('detects OpenZeppelin proxies', async () => { 56 | expect( 57 | await detectProxy( 58 | '0xC986c2d326c84752aF4cC842E033B9ae5D54ebbB', 59 | requestFunc, 60 | BLOCK_TAG 61 | ) 62 | ).toEqual({ 63 | target: '0x0656368c4934e56071056da375d4a691d22161f8', 64 | immutable: false, 65 | type: 'OpenZeppelin', 66 | }) 67 | }) 68 | 69 | it('detects EIP-897 delegate proxies', async () => { 70 | expect( 71 | await detectProxy( 72 | '0x8260b9eC6d472a34AD081297794d7Cc00181360a', 73 | requestFunc, 74 | BLOCK_TAG 75 | ) 76 | ).toEqual({ 77 | target: '0xe4e4003afe3765aca8149a82fc064c0b125b9e5a', 78 | immutable: false, 79 | type: 'Eip1967Direct', 80 | }) 81 | }) 82 | 83 | it('detects EIP-1167 minimal proxies', async () => { 84 | expect( 85 | await detectProxy( 86 | '0x6d5d9b6ec51c15f45bfa4c460502403351d5b999', 87 | requestFunc, 88 | BLOCK_TAG 89 | ) 90 | ).toEqual({ 91 | target: '0x210ff9ced719e9bf2444dbc3670bac99342126fa', 92 | immutable: true, 93 | type: 'Eip1167', 94 | }) 95 | }) 96 | 97 | it('detects EIP-1167 minimal proxies with vanity addresses', async () => { 98 | expect( 99 | await detectProxy( 100 | '0xa81043fd06D57D140f6ad8C2913DbE87fdecDd5F', 101 | requestFunc, 102 | BLOCK_TAG 103 | ) 104 | ).toEqual({ 105 | target: '0x0000000010fd301be3200e67978e3cc67c962f48', 106 | immutable: true, 107 | type: 'Eip1167', 108 | }) 109 | }) 110 | 111 | it('detects Safe proxies', async () => { 112 | expect( 113 | await detectProxy( 114 | '0x0DA0C3e52C977Ed3cBc641fF02DD271c3ED55aFe', 115 | requestFunc, 116 | BLOCK_TAG 117 | ) 118 | ).toEqual({ 119 | target: '0xd9db270c1b5e3bd161e8c8503c55ceabee709552', 120 | immutable: false, 121 | type: 'Safe', 122 | }) 123 | }) 124 | 125 | it("detects Compound's custom proxy", async () => { 126 | expect( 127 | await detectProxy( 128 | '0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B', 129 | requestFunc, 130 | BLOCK_TAG 131 | ) 132 | ).toEqual({ 133 | target: '0xbafe01ff935c7305907c33bf824352ee5979b526', 134 | immutable: false, 135 | type: 'Comptroller', 136 | }) 137 | }) 138 | 139 | it("detects Balancer's BatchRelayer", async () => { 140 | expect( 141 | await detectProxy( 142 | '0x35cea9e57a393ac66aaa7e25c391d52c74b5648f', 143 | requestFunc, 144 | BLOCK_TAG 145 | ) 146 | ).toEqual({ 147 | target: '0xea66501df1a00261e3bb79d1e90444fc6a186b62', 148 | immutable: true, 149 | type: 'BatchRelayer', 150 | }) 151 | }) 152 | 153 | it('resolves to null if no proxy target is detected', async () => { 154 | expect( 155 | await detectProxy( 156 | '0x5864c777697Bf9881220328BF2f16908c9aFCD7e', 157 | requestFunc, 158 | BLOCK_TAG 159 | ) 160 | ).toBe(null) 161 | }) 162 | }) 163 | -------------------------------------------------------------------------------- /test/parseEip1167Bytecode.spec.ts: -------------------------------------------------------------------------------- 1 | import { parse1167Bytecode } from '../src' 2 | 3 | describe('parse1167Bytecode', () => { 4 | it('parses EIP-1167 bytecode with extra footer', () => { 5 | const bytecode1167WithFooter = 6 | '0x363d3d373d3d3d363d73f62849f9a0b5bf2913b396098f7c7019b51a820a5af43d82803e903d91602b57fd5bf3000000000000000000000000000000000000000000000000000000000000007a6900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' 7 | 8 | expect(parse1167Bytecode(bytecode1167WithFooter)).toEqual( 9 | '0xf62849f9a0b5bf2913b396098f7c7019b51a820a' 10 | ) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /test/readString.ts: -------------------------------------------------------------------------------- 1 | import { readString } from '../src/readString' 2 | 3 | describe('readString', () => { 4 | it('should convert simple hex string to utf-8 string', () => { 5 | const hex = '0x48656c6c6f20576f726c64' 6 | expect(readString(hex)).toEqual('Hello World') 7 | }) 8 | 9 | it('should convert ABI-encoded string to utf-8 string', () => { 10 | const hex = 11 | '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000b48656c6c6f20576f726c6400000000000000000000000000000000000000000000' 12 | expect(readString(hex)).toEqual('Hello World') 13 | }) 14 | 15 | it('should handle empty string', () => { 16 | const hex = 17 | '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' 18 | expect(readString(hex)).toEqual('') 19 | }) 20 | 21 | it('should throw error for invalid hex string', () => { 22 | const hex = '0x48656c6c6f20576f726c6' // odd length 23 | expect(() => readString(hex)).toThrow('Invalid hex string length') 24 | }) 25 | 26 | it('should throw error for invalid offset', () => { 27 | const hex = 28 | '0x0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000b48656c6c6f20576f726c6400000000000000000000000000000000000000000000' 29 | expect(() => readString(hex)).toThrow('Invalid string offset') 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "target": "es2017", 5 | "outDir": "build/cjs", 6 | "module": "commonjs" 7 | }, 8 | "include": ["src/**/*.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "outDir": "build/esm", 6 | "module": "esnext" 7 | }, 8 | "include": ["src/**/*.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "target": "es2017", 5 | "outDir": "build/test", 6 | "moduleResolution": "node", 7 | "module": "commonjs", 8 | "declaration": true, 9 | "inlineSourceMap": true, 10 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 11 | "resolveJsonModule": true /* Include modules imported with .json extension. */, 12 | 13 | "strict": true /* Enable all strict type-checking options. */, 14 | 15 | /* Strict Type-Checking Options */ 16 | "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, 17 | // "strictNullChecks": true /* Enable strict null checks. */, 18 | // "strictFunctionTypes": true /* Enable strict checking of function types. */, 19 | // "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */, 20 | // "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */, 21 | // "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */, 22 | 23 | /* Additional Checks */ 24 | "noUnusedParameters": true /* Report errors on unused parameters. */, 25 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, 26 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, 27 | 28 | /* Debugging Options */ 29 | "traceResolution": false /* Report module resolution log messages. */, 30 | "listEmittedFiles": false /* Print names of generated files part of the compilation. */, 31 | "listFiles": false /* Print names of files part of the compilation. */, 32 | "pretty": true /* Stylize errors and messages using color and context. */, 33 | 34 | "types": ["jest", "node"], 35 | "lib": ["es2021"] 36 | }, 37 | "include": ["src/**/*.ts", "test/**/*.ts"], 38 | "compileOnSave": false 39 | } 40 | --------------------------------------------------------------------------------