Skip to content

Commit ab1340c

Browse files
hasparuskrzkaczor
andauthored
Stop looping on self-referencing contracts (#26)
* Change null to undefined * Stop looping on self-referencing contracts * Update package.json Co-authored-by: Kris Kaczor <[email protected]> Co-authored-by: Kris Kaczor <[email protected]>
1 parent c9b65e1 commit ab1340c

File tree

6 files changed

+128
-12
lines changed

6 files changed

+128
-12
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
"lint": "eslint --ext .ts ./packages/*/src/**/*.ts",
1010
"lint:fix": "pnpm lint -- --fix",
1111
"prepare-deploy": "cp ./vercel.json ./packages/vscode-host/dist",
12-
"test": "mocha"
12+
"test": "mocha",
13+
"check": "pnpm test && pnpm lint"
1314
},
1415
"devDependencies": {
1516
"@types/jsdom": "^16.2.14",
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
export const addresses = {
22
L1ChugSplashProxy: "0x99c9fc46f92e8a1c0dec1b1747d010903e884be1",
33
DAI: "0x6b175474e89094c44da98b954eedeac495271d0f",
4+
NonfungiblePositionManager: "0xc36442b4a4522e871399cd717abdd847ab11fe88",
5+
UniswapV3Router: "0xe592427a0aece92de3edee1f18e0157c05861564",
46
};

packages/ethereum-viewer/src/explorer/fetchFiles.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,22 @@ import { prettyStringify } from "../util/stringify";
66
import * as types from "./api-types";
77
import { ApiName, explorerApiKeys, explorerApiUrls } from "./networks";
88

9+
interface FetchFilesOptions {
10+
/**
11+
* For unit testing.
12+
* @internal
13+
*/
14+
fetch?: typeof _fetch;
15+
/**
16+
* If more than 0, we fetch implementation contract and merge its files.
17+
*/
18+
proxyDepth?: number;
19+
}
20+
921
export async function fetchFiles(
1022
apiName: ApiName,
1123
contractAddress: string,
12-
fetch: typeof _fetch = _fetch
24+
{ fetch = _fetch, proxyDepth = 3 }: FetchFilesOptions = {}
1325
): Promise<FetchFilesResult> {
1426
const apiUrl = explorerApiUrls[apiName];
1527
const url =
@@ -71,8 +83,16 @@ export async function fetchFiles(
7183
files[info.ContractName + ".sol"] = sourceCode;
7284
}
7385

74-
if (implementationAddr) {
75-
const implementation = await fetchFiles(apiName, implementationAddr);
86+
if (
87+
implementationAddr &&
88+
proxyDepth > 0 &&
89+
implementationAddr !== contractAddress
90+
) {
91+
const implementation = await fetchFiles(apiName, implementationAddr, {
92+
fetch,
93+
proxyDepth: proxyDepth - 1,
94+
});
95+
7696
Object.assign(
7797
files,
7898
prefixFiles(implementation.files, implementation.info.ContractName)

packages/ethereum-viewer/src/extension.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as vscode from "vscode";
2+
import { addresses } from "./addresses";
23

34
import { registerContributedCommands } from "./contributedCommands";
45
import { executeHostCommand } from "./executeHostCommand";
@@ -34,7 +35,7 @@ async function main(context: vscode.ExtensionContext) {
3435
name: network,
3536
});
3637

37-
let address: string | null = null;
38+
let address: string | undefined;
3839

3940
if (IN_DETH_HOST) address = await executeHostCommand("getContractAddress");
4041

@@ -49,7 +50,7 @@ async function detectExplorerApiName(): Promise<explorer.ApiName> {
4950
if (IN_DETH_HOST) {
5051
const detectedName = await executeHostCommand("getApiName");
5152

52-
if (detectedName === null) return "etherscan";
53+
if (!detectedName) return "etherscan";
5354

5455
if (!(detectedName in explorerApiUrls)) {
5556
await vscode.window.showErrorMessage(

packages/ethereum-viewer/src/test/explorer.test.ts renamed to packages/ethereum-viewer/src/test/fetchFiles.test.ts

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ describe(fetchFiles.name, () => {
3333
};
3434
};
3535

36-
await fetchFiles("optimistic.etherscan", "0x0", f);
36+
await fetchFiles("optimistic.etherscan", "0x0", { fetch: f });
3737

3838
const expected =
3939
"https://api-optimistic.etherscan.io/api?module=contract&action=getsourcecode&address=0x0&apikey=862Y3WJ4JB4B34PZQRFEV3IK6SZ8GNR9N5";
@@ -69,7 +69,7 @@ describe(fetchFiles.name, () => {
6969
};
7070
};
7171

72-
const { files } = await fetchFiles("etherscan", "0x0", f);
72+
const { files } = await fetchFiles("etherscan", "0x0", { fetch: f });
7373

7474
const expected =
7575
"Oops! It seems this contract source code is not verified on https://etherscan.io.";
@@ -78,4 +78,94 @@ describe(fetchFiles.name, () => {
7878
`Expected ${files["error.md"]} to contain ${expected}`
7979
);
8080
});
81+
82+
it("does not fall into infinite recursion when contract has itself as implementation", async () => {
83+
const addr = "0xc36442b4a4522e871399cd717abdd847ab11fe88";
84+
let callsCounter = 0;
85+
const f = async (): Promise<ContractSourceResponse> => {
86+
callsCounter++;
87+
return {
88+
status: "1",
89+
message: "OK",
90+
result: [
91+
{
92+
SourceCode: "// woop",
93+
ABI: "[]",
94+
ContractName: "ImplementsItself",
95+
CompilerVersion: "v0.7.6+commit.7338295f",
96+
OptimizationUsed: "1",
97+
Runs: "2000",
98+
ConstructorArguments: "",
99+
EVMVersion: "Default",
100+
Library: "",
101+
LicenseType: "",
102+
Proxy: "1",
103+
Implementation: addr,
104+
SwarmSource: "",
105+
},
106+
],
107+
};
108+
};
109+
110+
await fetchFiles("etherscan", addr, { fetch: f });
111+
112+
assert(
113+
callsCounter === 1,
114+
`fetchFiles should have been called once, but was called ${callsCounter} times`
115+
);
116+
});
117+
118+
it("does not follow .Implementation without end", async () => {
119+
const makeContract = ({
120+
Implementation,
121+
}: {
122+
Implementation: string;
123+
}): ContractSourceResponse => ({
124+
status: "1",
125+
message: "OK",
126+
result: [
127+
{
128+
SourceCode: "//",
129+
ABI: "[]",
130+
ContractName: "ImplementsItself",
131+
CompilerVersion: "v0.7.6+commit.7338295f",
132+
OptimizationUsed: "1",
133+
Runs: "2000",
134+
ConstructorArguments: "",
135+
EVMVersion: "Default",
136+
Library: "",
137+
LicenseType: "",
138+
Proxy: "1",
139+
Implementation: Implementation,
140+
SwarmSource: "",
141+
},
142+
],
143+
});
144+
145+
const contracts = {
146+
"0x00": makeContract({ Implementation: "0x01" }),
147+
"0x01": makeContract({ Implementation: "0x02" }),
148+
"0x02": makeContract({ Implementation: "0x03" }),
149+
"0x03": makeContract({ Implementation: "0x04" }),
150+
"0x04": makeContract({ Implementation: "0x05" }),
151+
};
152+
153+
const fetchedAddresses: string[] = [];
154+
const f = async (url: string): Promise<ContractSourceResponse> => {
155+
const address = new URL(url).searchParams.get("address")!;
156+
157+
fetchedAddresses.push(address);
158+
159+
return contracts[address as keyof typeof contracts];
160+
};
161+
162+
await fetchFiles("etherscan", "0x00", { fetch: f });
163+
164+
const actual = JSON.stringify(fetchedAddresses);
165+
const expected = JSON.stringify(["0x00", "0x01", "0x02", "0x03"]);
166+
assert(
167+
actual === expected,
168+
`Actual: ${actual}\n` + `Expected: ${expected}`
169+
);
170+
});
81171
});

packages/vscode-host/src/deth/commands/ethViewerCommands.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
export const ethViewerCommands = {
22
getBrowserUrl: () => window.location.href,
33
replaceBrowserUrl: (url: string) => window.location.replace(url),
4-
getContractAddress: (): string | null => {
4+
getContractAddress: (): string | undefined => {
55
const url = new URL(window.location.href);
66

77
// surge.sh doesn't seem to support rewrites, so we also read from search params.
@@ -14,14 +14,16 @@ export const ethViewerCommands = {
1414
if (path.startsWith("token/")) path = path.slice(6);
1515
if (path.endsWith("/")) path = path.slice(0, -1);
1616

17-
return path.startsWith("0x") ? path : null;
17+
return path.startsWith("0x") ? path : undefined;
1818
},
19-
getApiName: (): string | null => {
19+
getApiName: (): string | undefined => {
2020
const { hostname } = window.location;
2121

2222
if (hostname.endsWith(".deth.net")) return hostname.slice(0, -9);
2323

24-
return new URLSearchParams(window.location.search).get("explorer");
24+
return (
25+
new URLSearchParams(window.location.search).get("explorer") || undefined
26+
);
2527
},
2628
openRepoOnGithub: () => {
2729
window.open("https://github.com/dethcrypto/ethereum-code-viewer", "_blank");

0 commit comments

Comments
 (0)