Skip to content

Commit 40f372d

Browse files
committed
add support for proxy contracts
add proxy support to `graph add` adjust proxy message make errors more descriptive handle ESC in proxy confirmation without crashing refactor proxy check code into helper function
1 parent 197c4d3 commit 40f372d

File tree

4 files changed

+150
-10
lines changed

4 files changed

+150
-10
lines changed

packages/cli/src/command-helpers/contracts.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,51 @@ export class ContractService {
206206
return null;
207207
}
208208

209+
async getProxyImplementation(networkId: string, address: string) {
210+
const urls = this.getRpcUrls(networkId);
211+
if (!urls.length) {
212+
throw new Error(`No JSON-RPC available for ${networkId} in the registry`);
213+
}
214+
215+
const EIP_1967_SLOT = '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc';
216+
const OPEN_ZEPPELIN_SLOT = '0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3';
217+
const getStorageAt = async (url: string, slot: string) => {
218+
const response = await fetch(url, {
219+
method: 'POST',
220+
headers: { 'Content-Type': 'application/json' },
221+
body: JSON.stringify({
222+
jsonrpc: '2.0',
223+
method: 'eth_getStorageAt',
224+
params: [address, slot, 'latest'],
225+
id: 1,
226+
}),
227+
});
228+
const json = await response.json();
229+
if (json?.result) {
230+
const impl = '0x' + json.result.slice(-40);
231+
if (impl !== '0x0000000000000000000000000000000000000000') {
232+
return impl;
233+
}
234+
}
235+
return null;
236+
};
237+
238+
for (const url of urls) {
239+
for (const slot of [EIP_1967_SLOT, OPEN_ZEPPELIN_SLOT]) {
240+
try {
241+
const impl = await getStorageAt(url, slot);
242+
if (impl) {
243+
return impl;
244+
}
245+
} catch (error) {
246+
logger(`Failed to fetch proxy implementation from ${url}: ${error}`);
247+
}
248+
}
249+
}
250+
251+
throw new Error(`No implementation address found`);
252+
}
253+
209254
private async fetchTransactionByHash(networkId: string, txHash: string) {
210255
const urls = this.getRpcUrls(networkId);
211256
if (!urls.length) {
@@ -234,6 +279,6 @@ export class ContractService {
234279
}
235280
}
236281

237-
throw new Error(`JSON-RPC is unreachable`);
282+
throw new Error(`Failed to fetch tx ${txHash}`);
238283
}
239284
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { prompt } from 'gluegun';
2+
import EthereumABI from '../protocols/ethereum/abi.js';
3+
import { ContractService } from './contracts.js';
4+
import { retryWithPrompt } from './retry.js';
5+
import { withSpinner } from './spinner.js';
6+
7+
export interface CheckForProxyResult {
8+
implementationAbi: EthereumABI | null;
9+
implementationAddress: string | null;
10+
}
11+
12+
export async function checkForProxy(
13+
contractService: ContractService,
14+
network: string,
15+
address: string,
16+
abi: EthereumABI,
17+
): Promise<CheckForProxyResult> {
18+
let implementationAddress = null;
19+
let implementationAbi = null;
20+
21+
const maybeProxy = abi.callFunctionSignatures()?.includes('upgradeTo(address)');
22+
if (maybeProxy) {
23+
const impl = await retryWithPrompt(() =>
24+
withSpinner(
25+
'Fetching proxy implementation address...',
26+
'Failed to fetch proxy implementation address',
27+
'Warning fetching proxy implementation address',
28+
() => contractService.getProxyImplementation(network, address),
29+
),
30+
);
31+
32+
if (impl) {
33+
const useImplementation = await prompt
34+
.confirm(`Proxy contract detected. Use implementation contract ABI at ${impl}?`, true)
35+
.catch(() => false);
36+
37+
if (useImplementation) {
38+
implementationAddress = impl;
39+
implementationAbi = await retryWithPrompt(() =>
40+
withSpinner(
41+
'Fetching implementation contract ABI...',
42+
'Failed to fetch implementation ABI',
43+
'Warning fetching implementation ABI',
44+
() => contractService.getABI(EthereumABI, network, implementationAddress!),
45+
),
46+
);
47+
}
48+
}
49+
}
50+
51+
return {
52+
implementationAbi,
53+
implementationAddress,
54+
};
55+
}

packages/cli/src/commands/add.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Args, Command, Errors, Flags } from '@oclif/core';
44
import { ContractService } from '../command-helpers/contracts.js';
55
import * as DataSourcesExtractor from '../command-helpers/data-sources.js';
66
import { updateNetworksFile } from '../command-helpers/network.js';
7+
import { checkForProxy } from '../command-helpers/proxy.js';
78
import { loadRegistry } from '../command-helpers/registry.js';
89
import { retryWithPrompt } from '../command-helpers/retry.js';
910
import {
@@ -88,7 +89,9 @@ export default class AddCommand extends Command {
8889

8990
let startBlock = startBlockFlag ? parseInt(startBlockFlag).toString() : startBlockFlag;
9091
let contractName = contractNameFlag || DEFAULT_CONTRACT_NAME;
91-
let ethabi = null;
92+
93+
let ethabi: EthereumABI | null = null;
94+
let implAddress = null;
9295

9396
if (sourcifyContractInfo) {
9497
startBlock ??= sourcifyContractInfo.startBlock;
@@ -112,6 +115,18 @@ export default class AddCommand extends Command {
112115
),
113116
);
114117
if (!ethabi) throw Error;
118+
119+
const { implementationAbi, implementationAddress } = await checkForProxy(
120+
contractService,
121+
network,
122+
address,
123+
ethabi,
124+
);
125+
if (implementationAddress) {
126+
implAddress = implementationAddress;
127+
ethabi = implementationAbi!;
128+
}
129+
if (!ethabi) throw Error;
115130
} catch (error) {
116131
// we cannot ask user to do prompt in test environment
117132
if (process.env.NODE_ENV !== 'test') {
@@ -136,10 +151,15 @@ export default class AddCommand extends Command {
136151
}
137152
}
138153
}
154+
if (!ethabi) {
155+
this.error('Failed to load ABI', { exit: 1 });
156+
}
139157

140158
try {
141159
if (isLocalHost) throw Error; // Triggers user prompting without waiting for Etherscan lookup to fail
142-
startBlock ||= Number(await contractService.getStartBlock(network, address)).toString();
160+
startBlock ||= Number(
161+
await contractService.getStartBlock(network, implAddress ?? address),
162+
).toString();
143163
} catch (error) {
144164
// we cannot ask user to do prompt in test environment
145165
if (process.env.NODE_ENV !== 'test') {
@@ -166,7 +186,8 @@ export default class AddCommand extends Command {
166186
if (isLocalHost) throw Error; // Triggers user prompting without waiting for Etherscan lookup to fail
167187
if (contractName === DEFAULT_CONTRACT_NAME) {
168188
contractName =
169-
(await contractService.getContractName(network, address)) ?? DEFAULT_CONTRACT_NAME;
189+
(await contractService.getContractName(network, implAddress ?? address)) ??
190+
DEFAULT_CONTRACT_NAME;
170191
}
171192
} catch (error) {
172193
// not asking user to do prompt in test environment
@@ -266,8 +287,6 @@ export default class AddCommand extends Command {
266287
'Warning during codegen',
267288
async () => await system.run(yarn ? 'yarn codegen' : 'npm run codegen'),
268289
);
269-
270-
this.exit(0);
271290
}
272291
}
273292

packages/cli/src/commands/init.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { DEFAULT_IPFS_URL } from '../command-helpers/ipfs.js';
1111
import { initNetworksConfig } from '../command-helpers/network.js';
1212
import { chooseNodeUrl } from '../command-helpers/node.js';
1313
import { PromptManager } from '../command-helpers/prompt-manager.js';
14+
import { checkForProxy } from '../command-helpers/proxy.js';
1415
import { loadRegistry } from '../command-helpers/registry.js';
1516
import { retryWithPrompt } from '../command-helpers/retry.js';
1617
import { generateScaffold, writeScaffold } from '../command-helpers/scaffold.js';
@@ -635,6 +636,7 @@ async function processInitForm(
635636
}
636637

637638
// If ABI is not provided, try to fetch it from Etherscan API
639+
let implAddress: string | undefined = undefined;
638640
if (protocolInstance.hasABIs() && !initAbi) {
639641
abiFromApi = await retryWithPrompt(() =>
640642
withSpinner(
@@ -644,18 +646,37 @@ async function processInitForm(
644646
() => contractService.getABI(protocolInstance.getABI(), network.id, address),
645647
),
646648
);
647-
initDebugger.extend('processInitForm')("abiFromEtherscan len: '%s'", abiFromApi?.name);
649+
initDebugger.extend('processInitForm')("ABI: '%s'", abiFromApi?.name);
648650
} else {
649651
abiFromApi = initAbi;
650652
}
653+
654+
if (abiFromApi) {
655+
const { implementationAbi, implementationAddress } = await checkForProxy(
656+
contractService,
657+
network.id,
658+
address,
659+
abiFromApi,
660+
);
661+
if (implementationAddress) {
662+
implAddress = implementationAddress;
663+
abiFromApi = implementationAbi!;
664+
initDebugger.extend('processInitForm')(
665+
"Impl ABI: '%s', Impl Address: '%s'",
666+
abiFromApi?.name,
667+
implAddress,
668+
);
669+
}
670+
}
671+
651672
// If startBlock is not provided, try to fetch it from Etherscan API
652673
if (!initStartBlock) {
653674
startBlock = await retryWithPrompt(() =>
654675
withSpinner(
655676
'Fetching start block from contract API...',
656677
'Failed to fetch start block',
657678
'Warning fetching start block',
658-
() => contractService.getStartBlock(network.id, address),
679+
() => contractService.getStartBlock(network.id, implAddress ?? address),
659680
),
660681
);
661682
initDebugger.extend('processInitForm')("startBlockFromEtherscan: '%s'", startBlock);
@@ -668,7 +689,7 @@ async function processInitForm(
668689
'Fetching contract name from contract API...',
669690
'Failed to fetch contract name',
670691
'Warning fetching contract name',
671-
() => contractService.getContractName(network.id, address),
692+
() => contractService.getContractName(network.id, implAddress ?? address),
672693
),
673694
);
674695
initDebugger.extend('processInitForm')("contractNameFromEtherscan: '%s'", contractName);
@@ -1302,7 +1323,7 @@ async function addAnotherContract(
13021323
name: 'contract',
13031324
initial: ProtocolContract.identifierName(),
13041325
required: true,
1305-
message: () => `\nContract ${ProtocolContract.identifierName()}`,
1326+
message: () => `Contract ${ProtocolContract.identifierName()}`,
13061327
validate: value => {
13071328
const { valid, error } = validateContract(value, ProtocolContract);
13081329
return valid ? true : error;

0 commit comments

Comments
 (0)