-
-
Notifications
You must be signed in to change notification settings - Fork 79
/
Copy pathproxies.ts
340 lines (286 loc) · 13.7 KB
/
proxies.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
/**
* @module proxies
* This module contains code to resolve a variety of types of proxies.
*
* The resolvers are detected and configured by whatsabi.autoload(...).
*
* If you already know which proxy it is and how it's configured, then the resolvers could be used manually too.
*
* @example
* Using WhatsABI to only resolve proxies with a known bytecode:
* ```ts
* const address = "0x...";
*
* // Skip this and use the regular `provider` if you don't already have the bytecode or don't care about saving an RPC call. :)
* const bytecode = "0x..."; // Already loaded from somewhere
* const cachedCodeProvider = whatsabi.providers.WithCachedCode(provider, {
* [address]: bytecode,
* });
*
* const result = whatsabi.autoload(cachedCodeProvider, address, {
* abiLoader: false, // Skip ABI loaders
* signatureLookup: false, // Skip looking up selector signatures
* })
*
* if (result.address !== address) console.log(`Resolved proxy: ${address} -> ${result.address}`);
* if (result.proxies.length > 0) console.log("Proxies detected:", result.proxies);
* // Note that some proxies can only be resolved relative to a selector, like DiamondProxy. These will need to be resolved manually via result.proxies.
* ```
* @example
* Resolve a DiamondProxy:
* ```ts
* // Let's say we have a result with a DiamondProxy in it, from the above example
* const resolver = result.proxies[0] as whatsabi.proxies.DiamondProxyResolver;
*
* // DiamondProxies have different contracts mapped relative to the selector,
* // so we must resolve them against a selector.
* const selector = "0x6e9960c3"; // function getAdmin() returns (address)
*
* const implementationAddress = await resolver.resolve(provider, address, selector);
* ```
*
*/
import type { StorageProvider, CallProvider } from "./providers.js";
import { addSlotOffset, readArray, joinSlot } from "./slots.js";
import { addressWithChecksum } from "./utils.js";
export interface ProxyResolver {
readonly name: string;
resolve(provider: StorageProvider|CallProvider, address: string, selector?: string): Promise<string>
toString(): string,
}
// Some helpers:
const _zeroAddress = "0x0000000000000000000000000000000000000000";
// Convert 32 byte hex to a 20 byte hex address
function addressFromPadded(data:string): string {
return "0x" + data.slice(data.length - 40);
}
// Resolvers:
export class BaseProxyResolver {
name: string;
constructor(name?: string) {
this.name = name || this.constructor.name;
}
toString(): string {
return this.name;
}
}
export class GnosisSafeProxyResolver extends BaseProxyResolver implements ProxyResolver {
async resolve(provider: StorageProvider, address: string): Promise<string> {
const slotPosition = 0; // masterCopy() is always first slot
return addressFromPadded(await provider.getStorageAt(address, slotPosition));
}
}
// 2016-era upgradeable proxy by Nick Johnson
// https://gist.github.com/Arachnid/4ca9da48d51e23e5cfe0f0e14dd6318f
export class LegacyUpgradeableProxyResolver extends BaseProxyResolver implements ProxyResolver {
async resolve(provider: StorageProvider, address: string): Promise<string> {
const slotPosition = 1; // // _dist is in the second slot
return addressFromPadded(await provider.getStorageAt(address, slotPosition));
}
}
const EIP1967FallbackSelectors = [
"0x5c60da1b", // implementation()
"0xda525716", // childImplementation()
"0xa619486e", // masterCopy()
"0xbb82aa5e", // comptrollerImplementation()
];
export class EIP1967ProxyResolver extends BaseProxyResolver implements ProxyResolver {
override name = "EIP1967Proxy";
async resolve(provider: StorageProvider & CallProvider, address: string): Promise<string> {
// Is there an implementation defined?
const implAddr = addressFromPadded(await provider.getStorageAt(address, slots.EIP1967_IMPL));
if (implAddr !== _zeroAddress) {
return implAddr;
}
// Gotta find the fallback...
const fallbackAddr = addressFromPadded(await provider.getStorageAt(address, slots.EIP1967_BEACON));
if (fallbackAddr === _zeroAddress) {
return _zeroAddress;
}
// Possible optimizations for the future:
// 1. We could getCode and finding the correct selector using disasm, but maybe not worth it with small number of calls.
// 2. We could use multicall3 (if available)
for (const selector of EIP1967FallbackSelectors) {
try {
const addr = addressFromPadded(await provider.call({
to: fallbackAddr,
data: selector,
}));
if (addr !== _zeroAddress) return addr;
} catch (e: any) {
if (e.toString().includes("revert")) continue;
throw e;
}
}
return _zeroAddress;
}
}
const diamondSelectors = [
"0xcdffacc6", // Diamond Loupe uses selector "0xcdffacc6": facetAddress(bytes4 _functionSelector)
"0x0d741577", // Some implementations (OpenZeppelin) use selector "0x0d741577": implementation(bytes4 func)
];
// ERC2535 - Diamond/Facet Proxy
export class DiamondProxyResolver extends BaseProxyResolver implements ProxyResolver {
override name = "DiamondProxy";
async resolve(provider: StorageProvider & CallProvider, address: string, selector: string): Promise<string> {
if (!selector) {
throw "DiamondProxy requires a selector to resolve to a specific facet";
} else if (selector.startsWith("0x")) {
selector = selector.slice(2);
}
// Selectors are considered "strings and byte arrays" so they're "unpadded data" (ie. end-padded) as opposed to start-padded like addresses etc.
//
// ethers.utils.defaultAbiCoder.encode(["bytes4", "bytes32"], ["0x" + selector, slots.DIAMOND_STORAGE])
// keccak256("0x" + selector.padEnd(64, "0") + slots.DIAMOND_STORAGE.slice(2));
const facetMappingSlot = joinSlot([selector.padEnd(64, "0"), slots.DIAMOND_STORAGE]);
const facet = await provider.getStorageAt(address, facetMappingSlot);
// It's a struct with a few fields, take the right 20 bytes
const storageAddr = "0x" + facet.slice(facet.length - 40);
if (storageAddr !== _zeroAddress) {
return storageAddr;
}
// Try the selectors are a fallback
for (const facetSelector of diamondSelectors) {
try {
const addr = addressFromPadded(await provider.call({
to: address,
data: facetSelector + selector,
}));
if (addr !== _zeroAddress) return addr;
} catch (e: any) {
if (e.toString().includes("revert")) continue;
throw e;
}
}
return _zeroAddress;
}
// Return the facet-to-selectors mapping
// Note that this does not respect frozen facet state.
async facets(provider: StorageProvider, address: string): Promise<Record<string, string[]>> {
// Would be cool if we could read the private facets storage and return known selectors... let's do it!
//
// Shoutout to @banteg for sharing the rest of the owl:
// - https://twitter.com/shazow/status/1693636008179343598
// - https://gist.github.com/banteg/0cee21909f7c1baedfa6c3d96ffe94f2
// TODO: Respect frozen facets?
// let isFrozen = false;
// if (config && !config.ignoreFrozen) {
// const isFrozenOffset = addSlotOffset(storageStart, 3); // isFrozen
// const isFrozenWord = await provider.getStorageAt(address, isFrozenOffset);
// isFrozen = isFrozenWord.slice(-1) === "1"
// }
// ... the rest of the owl :3
// 1. Read the DiamondStorage.facets array
//
// struct DiamondStorage {
// mapping(bytes4 => SelectorToFacet) selectorToFacet;
// mapping(address => FacetToSelectors) facetToSelectors;
// address[] facets;
// bool isFrozen;
// }
const storageStart = slots.DIAMOND_STORAGE;
const facetsOffset = addSlotOffset(storageStart, 2); // Facets live in the 3rd slot (0-indexed)
const addressWidth = 20; // Addresses are 20 bytes
const facets = await readArray(provider, address, facetsOffset, addressWidth);
// 2. Read FacetToSelectors.selectors[] via facetToSelectors[address].selectors[]
//
// struct FacetToSelectors {
// bytes4[] selectors;
// uint16 facetPosition;
// }
const selectorWidth = 4;
const facetSelectors : Record<string, string[]> = {};
const slot = addSlotOffset(storageStart, 1); // facetToSelector in 2nd slot
for (const f of facets) {
const facet = addressFromPadded(f);
const facetSelectorsSlot = joinSlot([facet, slot]);
const selectors = await readArray(provider, address, facetSelectorsSlot, selectorWidth);
facetSelectors[addressWithChecksum(facet)] = selectors.map(s => "0x" + s);
}
return facetSelectors;
}
// Return all of the valid selectors that work on this DiamondProxy.
// Note that this does not respect frozen facet state.
async selectors(provider: StorageProvider, address: string): Promise<string[]> {
// Get values from the mapping
const f = await this.facets(provider, address);
return Object.values(f).flat();
}
}
export class ZeppelinOSProxyResolver extends BaseProxyResolver implements ProxyResolver {
override name = "ZeppelinOSProxy";
async resolve(provider: StorageProvider, address: string): Promise<string> {
return addressFromPadded(await provider.getStorageAt(address, slots.ZEPPELINOS_IMPL));
}
}
export class PROXIABLEProxyResolver extends BaseProxyResolver implements ProxyResolver {
override name = "PROXIABLEProxy";
async resolve(provider: StorageProvider, address: string): Promise<string> {
return addressFromPadded(await provider.getStorageAt(address, slots.PROXIABLE));
}
}
// https://github.com/0xsequence/wallet-contracts/blob/master/contracts/Wallet.sol
// Implementation pointer is stored in slot keyed on the deployed address.
export class SequenceWalletProxyResolver extends BaseProxyResolver implements ProxyResolver {
override name = "SequenceWalletProxy";
async resolve(provider: StorageProvider, address: string): Promise<string> {
return addressFromPadded(await provider.getStorageAt(address, address.toLowerCase().slice(2)));
}
}
// FixedProxyResolver is used when we already know the resolved address
// No additional resolving required
// Example: EIP-1167
export class FixedProxyResolver extends BaseProxyResolver implements ProxyResolver {
override name = "FixedProxy";
readonly resolvedAddress : string;
constructor(name: string, resolvedAddress: string) {
super(name);
this.resolvedAddress = resolvedAddress;
}
async resolve(provider: StorageProvider, address: string): Promise<string> {
return this.resolvedAddress;
}
};
// Lookups:
// BYTE32's representing references to known proxy storage slots.
export const slots : Record<string, string> = {
// EIP-1967: Proxy Storage Slots
// bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)
EIP1967_IMPL: "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc",
// EIP-1967
// Beacon slot is a fallback if implementation is not set.
// bytes32(uint256(keccak256('eip1967.proxy.beacon')) - 1)).
// Beacon fallback has selectors:
// - implementation()
// - childImplementation()
// - masterCopy() in Gnosis Safe
// - comptrollerImplementation() in Compound
EIP1967_BEACON: "0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50",
// https://github.com/OpenZeppelin/openzeppelin-labs/blob/54ad91472fdd0ac4c34aa97d3a3da45c28245510/initializer_with_sol_editing/contracts/UpgradeabilityProxy.sol
// bytes32(uint256(keccak256("org.zeppelinos.proxy.implementation")))
ZEPPELINOS_IMPL: "0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3",
// ERC-1822: Universal Upgradeable Proxy Standard (UUPS)
// bytes32(uint256(keccak256("PROXIABLE")))
PROXIABLE: "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7",
// Gnosis Safe Proxy Factor 1.1.1
// Not actually a slot, but there's a PUSH32 to the masterCopy() selector
// masterCopy value lives in the 0th slot on the contract
GNOSIS_SAFE_SELECTOR: "0xa619486e00000000000000000000000000000000000000000000000000000000",
// Diamond Proxy, as used by ZkSync Era contract
// https://etherscan.io/address/0x32400084c286cf3e17e7b677ea9583e60a000324#code
// keccak256("diamond.standard.diamond.storage") - 1;
DIAMOND_STORAGE: "0xc8fcad8db84d3cc18b4c41d551ea0ee66dd599cde068d998e57d5e09332c131b",
// EIP-1167 minimal proxy standard
// Parsed in disasm
}
export const slotResolvers : Record<string, ProxyResolver> = {
[slots.EIP1967_IMPL]: new EIP1967ProxyResolver("EIP1967Proxy"),
[slots.EIP1967_BEACON]: new EIP1967ProxyResolver("EIP1967Proxy"),
[slots.ZEPPELINOS_IMPL]: new ZeppelinOSProxyResolver("ZeppelinOSProxy"),
[slots.PROXIABLE]: new PROXIABLEProxyResolver("PROXIABLE"),
[slots.GNOSIS_SAFE_SELECTOR]: new GnosisSafeProxyResolver("GnosisSafeProxy"),
[slots.DIAMOND_STORAGE]: new DiamondProxyResolver("DiamondProxy"),
// Not sure why, there's a compiler optimization that adds 2 to the normal slot?
// Would love to understand this, if people have ideas
["0xc8fcad8db84d3cc18b4c41d551ea0ee66dd599cde068d998e57d5e09332c131d"]: new DiamondProxyResolver("DiamondProxy"),
};