From c7e959e81e77227a7bcb2566c75aaa5bd6810266 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Sun, 13 Aug 2023 21:12:18 +1000 Subject: [PATCH] feat: multi file diff feat: cross chain diff chore: validation of address arguments and options --- README.md | 20 ++-- lib/diffContracts.d.ts | 35 ++++++ lib/diffContracts.js | 135 +++++++++++++++++++++ lib/parserEtherscan.d.ts | 2 +- lib/sol2uml.js | 76 ++++++------ lib/utils/diff.d.ts | 7 ++ lib/utils/diff.js | 187 +++++++++++++++++++++++++++++ lib/utils/regEx.d.ts | 2 + lib/utils/regEx.js | 5 +- lib/utils/validators.d.ts | 3 + lib/utils/validators.js | 41 +++++++ src/ts/__tests__/diff.test.ts | 2 +- src/ts/diffContracts.ts | 213 ++++++++++++++++++++++++++++++++++ src/ts/parserEtherscan.ts | 2 +- src/ts/sol2uml.ts | 162 ++++++++++++++++---------- src/ts/{ => utils}/diff.ts | 2 +- src/ts/utils/regEx.ts | 4 + src/ts/utils/validators.ts | 41 +++++++ tests/tests.sh | 51 ++++++++ 19 files changed, 886 insertions(+), 104 deletions(-) create mode 100644 lib/diffContracts.d.ts create mode 100644 lib/diffContracts.js create mode 100644 lib/utils/validators.d.ts create mode 100644 lib/utils/validators.js create mode 100644 src/ts/diffContracts.ts rename src/ts/{ => utils}/diff.ts (99%) create mode 100644 src/ts/utils/validators.ts create mode 100644 tests/tests.sh diff --git a/README.md b/README.md index 6a44ebc1..473ff9d7 100644 --- a/README.md +++ b/README.md @@ -189,15 +189,21 @@ The red sections are removals from contract A that are not in contract B. The line numbers are from contract B. There are no line numbers for the red sections as they are not in contract B. Arguments: - addressA Contract address in hexadecimal format with a 0x prefix of the first contract. - addressB Contract address in hexadecimal format with a 0x prefix of the second contract. + addressA Contract address in hexadecimal format with a 0x prefix of the first contract + addressB Contract address in hexadecimal format with a 0x prefix of the second contract Options: - -l, --lineBuffer Minimum number of lines before and after changes (default: "4") - --aFile Contract A source code filename without the .sol extension. (default: compares all source files) - --bFile Contract B source code filename without the .sol extension. (default: aFile if specified) - -s, --saveFiles Save the flattened contract code to the filesystem. The file names will be the contract address with a .sol extension. (default: false) - -h, --help display help for command + -l, --lineBuffer Minimum number of lines before and after changes (default: 4) + -af --aFile Contract A source code filename without the .sol extension (default: compares all source files) + -bf --bFile Contract B source code filename without the .sol extension (default: aFile if specified) + -bn, --bNetwork Ethereum network which maps to a blockchain explorer for contract B if on a different blockchain to contract A. Contract A uses the `network` option (default: value of `network` option) (choices: "mainnet", "goerli", "sepolia", "polygon", + "arbitrum", "avalanche", "bsc", "crono", "fantom", "moonbeam", "optimism", "gnosis", "celo") + -be, --bExplorerUrl Override the `bNetwork` option with custom blockchain explorer API URL for contract B if on a different blockchain to contract A. Contract A uses the `explorerUrl` (default: value of `explorerUrl` option) + -bk, --bApiKey Blockchain explorer API key for contract B if on a different blockchain to contract A. Contract A uses the `apiKey` option (default: value of `apiKey` option) + -s, --summary Only show a summary of the file differences. (default: false) + --flatten Flatten into a single file before comparing (default: false) + --saveFiles Save the flattened contract code to the filesystem when using the `flatten` option. The file names will be the contract address with a .sol extension (default: false) + -h, --help display help for command ``` ## UML Class diagram examples diff --git a/lib/diffContracts.d.ts b/lib/diffContracts.d.ts new file mode 100644 index 00000000..7c98b026 --- /dev/null +++ b/lib/diffContracts.d.ts @@ -0,0 +1,35 @@ +import { EtherscanParser } from './parserEtherscan'; +interface DiffOptions { + network: string; + lineBuffer: number; +} +interface FlattenAndDiffOptions extends DiffOptions { + aFile?: string; + bFile?: string; + saveFiles?: boolean; +} +interface DiffFiles { + filename?: string; + aCode?: string; + bCode?: string; + result: 'added' | 'removed' | 'match' | 'changed'; +} +interface CompareContracts { + files: DiffFiles[]; + contractNameA: string; + contractNameB: string; +} +export declare const compareContracts: (addressA: string, addressB: string, etherscanParserA: EtherscanParser, etherscanParserB: EtherscanParser, options: DiffOptions) => Promise; +export declare const displayContractNames: (addressA: string, addressB: string, contractNameA: string, contractNameB: string, options: { + network: string; + bNetwork?: string; +}) => void; +export declare const displayFileDiffSummary: (fileDiffs: DiffFiles[]) => void; +export declare const displayFileDiffs: (fileDiffs: DiffFiles[], options?: { + lineBuffer?: number; +}) => void; +export declare const flattenAndDiff: (addressA: string, addressB: string, aEtherscanParser: EtherscanParser, bEtherscanParser: EtherscanParser, options: FlattenAndDiffOptions) => Promise<{ + contractNameA: string; + contractNameB: string; +}>; +export {}; diff --git a/lib/diffContracts.js b/lib/diffContracts.js new file mode 100644 index 00000000..63a5c2bc --- /dev/null +++ b/lib/diffContracts.js @@ -0,0 +1,135 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.flattenAndDiff = exports.displayFileDiffs = exports.displayFileDiffSummary = exports.displayContractNames = exports.compareContracts = void 0; +const regEx_1 = require("./utils/regEx"); +const clc = require('cli-color'); +const writerFiles_1 = require("./writerFiles"); +const diff_1 = require("./utils/diff"); +const compareContracts = async (addressA, addressB, etherscanParserA, etherscanParserB, options) => { + const files = []; + const { files: aFiles, contractName: contractNameA } = await etherscanParserA.getSourceCode(addressA); + const { files: bFiles, contractName: contractNameB } = await etherscanParserB.getSourceCode(addressB); + if (aFiles.length === 1 && bFiles.length === 1) { + if ((0, regEx_1.isAddress)(aFiles[0].filename)) + files.push({ + filename: `${aFiles[0].filename} to ${bFiles[0].filename}`, + aCode: aFiles[0].code, + bCode: bFiles[0].code, + result: aFiles[0].code === bFiles[0].code ? 'match' : 'changed', + }); + return { + files, + contractNameA, + contractNameB, + }; + } + // For each file in the A contract + for (const aFile of aFiles) { + // Look for A contract filename in B contract + const bFile = bFiles.find((bFile) => bFile.filename === aFile.filename); + if (bFile) { + // The A contract filename exists in the B contract + if (aFile.code !== bFile.code) { + // console.log(`${aFile.filename} ${clc.red('different')}:`) + files.push({ + filename: aFile.filename, + aCode: aFile.code, + bCode: bFile.code, + result: 'changed', + }); + } + else { + files.push({ + filename: aFile.filename, + aCode: aFile.code, + bCode: bFile.code, + result: 'match', + }); + } + } + else { + // The A contract filename does not exist in the B contract + files.push({ + filename: aFile.filename, + aCode: aFile.code, + result: 'removed', + }); + } + } + // For each file in the B contract + for (const bFile of bFiles) { + // Look for B contract filename in A contract + const aFile = aFiles.find((aFile) => aFile.filename === bFile.filename); + if (!aFile) { + // The B contract filename does not exist in the A contract + files.push({ + filename: bFile.filename, + bCode: bFile.code, + result: 'added', + }); + } + } + // Sort by filename + return { + files: files.sort((a, b) => a.filename.localeCompare(b.filename)), + contractNameA, + contractNameB, + }; +}; +exports.compareContracts = compareContracts; +const displayContractNames = (addressA, addressB, contractNameA, contractNameB, options) => { + console.log(`Contract A: ${addressA} ${contractNameA} on ${options.network}`); + console.log(`Contract B: ${addressB} ${contractNameB} on ${options.bNetwork || options.network}\n`); +}; +exports.displayContractNames = displayContractNames; +const displayFileDiffSummary = (fileDiffs) => { + for (const file of fileDiffs) { + switch (file.result) { + case 'match': + console.log(`${file.result.padEnd(7)} ${file.filename}`); + break; + case 'added': + console.log(`${clc.green(file.result.padEnd(7))} ${file.filename}`); + break; + case 'changed': + case 'removed': + console.log(`${clc.red(file.result)} ${file.filename}`); + break; + } + } +}; +exports.displayFileDiffSummary = displayFileDiffSummary; +const displayFileDiffs = (fileDiffs, options = {}) => { + for (const file of fileDiffs) { + switch (file.result) { + case 'added': + console.log(`Added ${file.filename}`); + console.log(clc.green(file.bCode)); + break; + case 'changed': + console.log(`Changed ${file.filename}`); + (0, diff_1.diffCode)(file.aCode, file.bCode, options.lineBuffer); + break; + case 'removed': + console.log(`Removed ${file.filename}`); + console.log(clc.red(file.aCode)); + break; + } + } +}; +exports.displayFileDiffs = displayFileDiffs; +const flattenAndDiff = async (addressA, addressB, aEtherscanParser, bEtherscanParser, options) => { + // Get verified Solidity code from Etherscan and flatten + const { solidityCode: codeA, contractName: contractNameA } = await aEtherscanParser.getSolidityCode(addressA, options.aFile); + const { solidityCode: codeB, contractName: contractNameB } = await bEtherscanParser.getSolidityCode(addressB, options.bFile || options.aFile); + (0, exports.displayContractNames)(addressA, addressB, contractNameA, contractNameB, options); + (0, diff_1.diffCode)(codeA, codeB, options.lineBuffer); + if (options.saveFiles) { + await (0, writerFiles_1.writeSolidity)(codeA, addressA); + await (0, writerFiles_1.writeSolidity)(codeB, addressB); + } + (0, exports.displayContractNames)(addressA, addressB, contractNameA, contractNameB, options); + return { contractNameA, contractNameB }; +}; +exports.flattenAndDiff = flattenAndDiff; +//# sourceMappingURL=diffContracts.js.map \ No newline at end of file diff --git a/lib/parserEtherscan.d.ts b/lib/parserEtherscan.d.ts index 1e0796ad..e4062568 100644 --- a/lib/parserEtherscan.d.ts +++ b/lib/parserEtherscan.d.ts @@ -42,7 +42,7 @@ export declare class EtherscanParser { * @oaram filename optional, case-sensitive name of the source file without the .sol */ getSourceCode(contractAddress: string, filename?: string): Promise<{ - files: readonly { + files: { code: string; filename: string; }[]; diff --git a/lib/sol2uml.js b/lib/sol2uml.js index 754bf910..92bef3bf 100755 --- a/lib/sol2uml.js +++ b/lib/sol2uml.js @@ -1,20 +1,21 @@ #! /usr/bin/env node "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const converterClasses2Dot_1 = require("./converterClasses2Dot"); -const parserGeneral_1 = require("./parserGeneral"); -const parserEtherscan_1 = require("./parserEtherscan"); -const filterClasses_1 = require("./filterClasses"); const commander_1 = require("commander"); +const ethers_1 = require("ethers"); +const path_1 = require("path"); +const converterClasses2Dot_1 = require("./converterClasses2Dot"); const converterClasses2Storage_1 = require("./converterClasses2Storage"); const converterStorage2Dot_1 = require("./converterStorage2Dot"); -const regEx_1 = require("./utils/regEx"); -const writerFiles_1 = require("./writerFiles"); -const path_1 = require("path"); +const diffContracts_1 = require("./diffContracts"); +const filterClasses_1 = require("./filterClasses"); +const parserEtherscan_1 = require("./parserEtherscan"); +const parserGeneral_1 = require("./parserGeneral"); const squashClasses_1 = require("./squashClasses"); -const diff_1 = require("./diff"); const slotValues_1 = require("./slotValues"); -const ethers_1 = require("ethers"); +const regEx_1 = require("./utils/regEx"); +const validators_1 = require("./utils/validators"); +const writerFiles_1 = require("./writerFiles"); const clc = require('cli-color'); const program = new commander_1.Command(); const debugControl = require('debug'); @@ -33,7 +34,7 @@ Can also flatten or compare verified source files on Etherscan-like explorers.`) .choices(parserEtherscan_1.networks) .default('mainnet') .env('ETH_NETWORK')) - .addOption(new commander_1.Option('-e, --explorerUrl ', 'Override network with custom blockchain explorer API URL. eg Polygon Mumbai testnet https://api-testnet.polygonscan.com/api').env('EXPLORER_URL')) + .addOption(new commander_1.Option('-e, --explorerUrl ', 'Override the `network` option with a custom blockchain explorer API URL. eg Polygon Mumbai testnet https://api-testnet.polygonscan.com/api').env('EXPLORER_URL')) .addOption(new commander_1.Option('-k, --apiKey ', 'Blockchain explorer API key. eg Etherscan, Arbiscan, Optimism, BscScan, CronoScan, FTMScan, PolygonScan or SnowTrace API key').env('SCAN_API_KEY')) .option('-bc, --backColor ', 'Canvas background color. "none" will use a transparent canvas.', 'white') .option('-sc, --shapeColor ', 'Basic drawing color for graphics, not text', 'black') @@ -125,7 +126,7 @@ WARNING: sol2uml does not use the Solidity compiler so may differ with solc. A k .option('-c, --contract ', 'Contract name in the local Solidity files. Not needed when using an address as the first argument as the contract name can be derived from Etherscan.') .option('-cf, --contractFile ', 'Filename the contract is located in. This can include the relative path to the desired file.') .option('-d, --data', 'Gets the values in the storage slots from an Ethereum node.', false) - .option('-s, --storage
', 'The address of the contract with the storage values. This will be different from the contract with the code if a proxy contract is used. This is not needed if `fileFolderAddress` is an address and the contract is not proxied.') + .option('-s, --storage
', 'The address of the contract with the storage values. This will be different from the contract with the code if a proxy contract is used. This is not needed if `fileFolderAddress` is an address and the contract is not proxied.', validators_1.validateAddress) .addOption(new commander_1.Option('-u, --url ', 'URL of the Ethereum node to get storage values if the `data` option is used.') .env('NODE_URL') .default('http://localhost:8545')) @@ -195,7 +196,7 @@ In order for the merged code to compile, the following is done: 3. File imports are commented out. 4. "SPDX-License-Identifier" is renamed to "SPDX--License-Identifier". 5. Contract dependencies are analysed so the files are merged in an order that will compile.\n`) - .argument('', 'Contract address in hexadecimal format with a 0x prefix.') + .argument('', 'Contract address in hexadecimal format with a 0x prefix.', validators_1.validateAddress) .action(async (contractAddress, options, command) => { try { debug(`About to flatten ${contractAddress}`); @@ -223,12 +224,17 @@ The results show the comparison of contract A to B. The ${clc.green('green')} sections are additions to contract B that are not in contract A. The ${clc.red('red')} sections are removals from contract A that are not in contract B. The line numbers are from contract B. There are no line numbers for the red sections as they are not in contract B.\n`) - .argument('', 'Contract address in hexadecimal format with a 0x prefix of the first contract.') - .argument('', 'Contract address in hexadecimal format with a 0x prefix of the second contract.') - .addOption(new commander_1.Option('-l, --lineBuffer ', 'Minimum number of lines before and after changes').default('4')) - .addOption(new commander_1.Option('-af --aFile ', 'Contract A source code filename without the .sol extension. (default: compares all source files)')) - .addOption(new commander_1.Option('-bf --bFile ', 'Contract B source code filename without the .sol extension. (default: aFile if specified)')) - .option('-s, --saveFiles', 'Save the flattened contract code to the filesystem. The file names will be the contract address with a .sol extension.', false) + .argument('', 'Contract address in hexadecimal format with a 0x prefix of the first contract', validators_1.validateAddress) + .argument('', 'Contract address in hexadecimal format with a 0x prefix of the second contract', validators_1.validateAddress) + .option('-l, --lineBuffer ', 'Minimum number of lines before and after changes (default: 4)', validators_1.validateLineBuffer) + .option('-af --aFile ', 'Contract A source code filename without the .sol extension (default: compares all source files)') + .option('-bf --bFile ', 'Contract B source code filename without the .sol extension (default: aFile if specified)') + .addOption(new commander_1.Option('-bn, --bNetwork ', 'Ethereum network which maps to a blockchain explorer for contract B if on a different blockchain to contract A. Contract A uses the `network` option (default: value of `network` option)').choices(parserEtherscan_1.networks)) + .option('-be, --bExplorerUrl ', 'Override the `bNetwork` option with custom blockchain explorer API URL for contract B if on a different blockchain to contract A. Contract A uses the `explorerUrl` (default: value of `explorerUrl` option)') + .option('-bk, --bApiKey ', 'Blockchain explorer API key for contract B if on a different blockchain to contract A. Contract A uses the `apiKey` option (default: value of `apiKey` option)') + .option('-s, --summary', 'Only show a summary of the file differences.', false) + .option('--flatten', 'Flatten into a single file before comparing', false) + .option('--saveFiles', 'Save the flattened contract code to the filesystem when using the `flatten` option. The file names will be the contract address with a .sol extension', false) .action(async (addressA, addressB, options, command) => { try { debug(`About to diff ${addressA} and ${addressB}`); @@ -236,21 +242,25 @@ The line numbers are from contract B. There are no line numbers for the red sect ...command.parent._optionValues, ...options, }; - // Diff solidity code - const lineBuffer = parseInt(options.lineBuffer); - if (isNaN(lineBuffer)) - throw Error(`Invalid line buffer "${options.lineBuffer}". Must be a number`); - const etherscanParser = new parserEtherscan_1.EtherscanParser(combinedOptions.apiKey, combinedOptions.network, combinedOptions.explorerUrl); - // Get verified Solidity code from Etherscan and flatten - const { solidityCode: codeA, contractName: contractNameA } = await etherscanParser.getSolidityCode(addressA, combinedOptions.aFile); - const { solidityCode: codeB, contractName: contractNameB } = await etherscanParser.getSolidityCode(addressB, combinedOptions.bFile || combinedOptions.aFile); - console.log(`Difference between`); - console.log(`A. ${addressA} ${contractNameA} on ${combinedOptions.network}`); - console.log(`B. ${addressB} ${contractNameB} on ${combinedOptions.network}\n`); - (0, diff_1.diffCode)(codeA, codeB, lineBuffer); - if (options.saveFiles) { - await (0, writerFiles_1.writeSolidity)(codeA, addressA); - await (0, writerFiles_1.writeSolidity)(codeB, addressB); + const aEtherscanParser = new parserEtherscan_1.EtherscanParser(combinedOptions.apiKey, combinedOptions.network, combinedOptions.explorerUrl); + const bEtherscanParser = new parserEtherscan_1.EtherscanParser(combinedOptions.bApiKey || combinedOptions.apiKey, combinedOptions.bNetwork || combinedOptions.network, combinedOptions.bExplorerUrl || combinedOptions.explorerUrl); + if (options.flatten || options.aFile) { + await (0, diffContracts_1.flattenAndDiff)(addressA, addressB, aEtherscanParser, bEtherscanParser, combinedOptions); + } + else { + const { contractNameA, contractNameB, files } = await (0, diffContracts_1.compareContracts)(addressA, addressB, aEtherscanParser, bEtherscanParser, combinedOptions); + (0, diffContracts_1.displayContractNames)(addressA, addressB, contractNameA, contractNameB, combinedOptions); + (0, diffContracts_1.displayFileDiffSummary)(files); + if (!options.summary) { + // Just show the summary if all the files are the same + const diffFiles = files.filter((f) => f.result !== 'match'); + if (diffFiles.length === 0) + return; + console.log(); + (0, diffContracts_1.displayFileDiffs)(files, combinedOptions); + (0, diffContracts_1.displayContractNames)(addressA, addressB, contractNameA, contractNameB, combinedOptions); + (0, diffContracts_1.displayFileDiffSummary)(files); + } } } catch (err) { diff --git a/lib/utils/diff.d.ts b/lib/utils/diff.d.ts index e69de29b..077c9eb9 100644 --- a/lib/utils/diff.d.ts +++ b/lib/utils/diff.d.ts @@ -0,0 +1,7 @@ +/** + * Compares code using Google's diff_match_patch and displays the results in the console. + * @param codeA + * @param codeB + * @param lineBuff the number of lines to display before and after each change. + */ +export declare const diffCode: (codeA: string, codeB: string, lineBuff: number) => void; diff --git a/lib/utils/diff.js b/lib/utils/diff.js index 877d01dc..5e7c76bf 100644 --- a/lib/utils/diff.js +++ b/lib/utils/diff.js @@ -1 +1,188 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.diffCode = void 0; +const diff_match_patch_1 = __importStar(require("diff-match-patch")); +const clc = require('cli-color'); +const SkippedLinesMarker = `\n---`; +/** + * Compares code using Google's diff_match_patch and displays the results in the console. + * @param codeA + * @param codeB + * @param lineBuff the number of lines to display before and after each change. + */ +const diffCode = (codeA, codeB, lineBuff) => { + // @ts-ignore + const dmp = new diff_match_patch_1.default(); + const diff = dmp.diff_main(codeA, codeB); + dmp.diff_cleanupSemantic(diff); + const linesB = countLines(codeB) + 1; + diff_pretty(diff, linesB, lineBuff); +}; +exports.diffCode = diffCode; +/** + * Convert a diff array into human-readable for the console + * @param {!Array.} diffs Array of diff tuples. + * @param lines number of a lines in the second contract B + * @param lineBuff number of a lines to output before and after the change + */ +const diff_pretty = (diffs, lines, lineBuff = 2) => { + const linePad = lines.toString().length; + let output = ''; + let diffIndex = 0; + let lineCount = 1; + const firstLineNumber = '1'.padStart(linePad) + ' '; + for (const diff of diffs) { + diffIndex++; + const initialLineNumber = diffIndex <= 1 ? firstLineNumber : ''; + const op = diff[0]; // Operation (insert, delete, equal) + const text = diff[1]; // Text of change. + switch (op) { + case diff_match_patch_1.DIFF_INSERT: + // If first diff then we need to add the first line number + const linesInserted = addLineNumbers(text, lineCount, linePad); + output += initialLineNumber + clc.green(linesInserted); + lineCount += countLines(text); + break; + case diff_match_patch_1.DIFF_DELETE: + // zero start line means blank line numbers are used + const linesDeleted = addLineNumbers(text, 0, linePad); + output += initialLineNumber + clc.red(linesDeleted); + break; + case diff_match_patch_1.DIFF_EQUAL: + const eolPositions = findEOLPositions(text); + // If no changes yet + if (diffIndex <= 1) { + output += lastLines(text, eolPositions, lineBuff, linePad); + } + // if no more changes + else if (diffIndex === diffs.length) { + output += firstLines(text, eolPositions, lineBuff, lineCount, linePad); + } + else { + // else the first n lines and last n lines + output += firstAndLastLines(text, eolPositions, lineBuff, lineCount, linePad); + } + lineCount += eolPositions.length; + break; + } + } + output += '\n'; + console.log(output); +}; +/** + * Used when there is no more changes left + */ +const firstLines = (text, eolPositions, lineBuff, lineStart, linePad) => { + const lines = text.slice(0, eolPositions[lineBuff]); + return addLineNumbers(lines, lineStart, linePad); +}; +/** + * Used before the first change + */ +const lastLines = (text, eolPositions, lineBuff, linePad) => { + const eolFrom = eolPositions.length - (lineBuff + 1); + let lines = text; + let lineCount = 1; + if (eolFrom >= 0) { + lines = eolFrom >= 0 ? text.slice(eolPositions[eolFrom] + 1) : text; + lineCount = eolFrom + 2; + } + const firstLineNumber = lineCount.toString().padStart(linePad) + ' '; + return firstLineNumber + addLineNumbers(lines, lineCount, linePad); +}; +/** + * Used between changes to show the lines after the last change and before the next change. + * @param text + * @param eolPositions + * @param lineBuff + * @param lineStart + * @param linePad + */ +const firstAndLastLines = (text, eolPositions, lineBuff, lineStart, linePad) => { + if (eolPositions.length <= 2 * lineBuff) { + return addLineNumbers(text, lineStart, linePad); + } + const endFirstLines = eolPositions[lineBuff]; + const eolFrom = eolPositions.length - (lineBuff + 1); + const startLastLines = eolPositions[eolFrom]; + if (startLastLines <= endFirstLines) { + return addLineNumbers(text, lineStart, linePad); + } + // Lines after the previous change + let lines = text.slice(0, endFirstLines); + let output = addLineNumbers(lines, lineStart, linePad); + output += SkippedLinesMarker; + // Lines before the next change + lines = text.slice(startLastLines); + const lineCount = lineStart + eolFrom; + output += addLineNumbers(lines, lineCount, linePad); + return output; +}; +/** + * Gets the positions of the end of lines in the string + * @param text + */ +const findEOLPositions = (text) => { + const eolPositions = []; + text.split('').forEach((c, i) => { + if (c === '\n') { + eolPositions.push(i); + } + }); + return eolPositions; +}; +/** + * Counts the number of carriage returns in a string + * @param text + */ +const countLines = (text) => (text.match(/\n/g) || '').length; +/** + * Adds left padded line numbers to each line. + * @param text with the lines of code + * @param lineStart the line number of the first line in the text. If zero, then no lines numbers are added. + * @param linePad the width of the largest number which may not be in the text + */ +const addLineNumbers = (text, lineStart, linePad) => { + let lineCount = lineStart; + let textWithLineNumbers = ''; + text.split('').forEach((c, i) => { + if (c === '\n') { + if (lineStart > 0) { + textWithLineNumbers += `\n${(++lineCount) + .toString() + .padStart(linePad)} `; + } + else { + textWithLineNumbers += `\n${' '.repeat(linePad)} `; + } + } + else { + textWithLineNumbers += c; + } + }); + return textWithLineNumbers; +}; //# sourceMappingURL=diff.js.map \ No newline at end of file diff --git a/lib/utils/regEx.d.ts b/lib/utils/regEx.d.ts index 53bab013..71e10255 100644 --- a/lib/utils/regEx.d.ts +++ b/lib/utils/regEx.d.ts @@ -1,2 +1,4 @@ +export declare const ethereumAddress: RegExp; +export declare const ethereumAddresses: RegExp; export declare const isAddress: (input: string) => boolean; export declare const parseSolidityVersion: (compilerVersion: string) => string; diff --git a/lib/utils/regEx.js b/lib/utils/regEx.js index 85cc438e..13fd3a64 100644 --- a/lib/utils/regEx.js +++ b/lib/utils/regEx.js @@ -1,6 +1,9 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.parseSolidityVersion = exports.isAddress = void 0; +exports.parseSolidityVersion = exports.isAddress = exports.ethereumAddresses = exports.ethereumAddress = void 0; +exports.ethereumAddress = /^0x([A-Fa-f0-9]{40})$/; +// comma-separated list of addresses with no whitespace +exports.ethereumAddresses = /^(0x[A-Fa-f0-9]{40},?)+$/; const isAddress = (input) => { return input.match(/^0x([A-Fa-f0-9]{40})$/) !== null; }; diff --git a/lib/utils/validators.d.ts b/lib/utils/validators.d.ts new file mode 100644 index 00000000..3825d363 --- /dev/null +++ b/lib/utils/validators.d.ts @@ -0,0 +1,3 @@ +export declare const validateAddress: (address: string) => string; +export declare const validateAddresses: (addresses: string) => string[]; +export declare const validateLineBuffer: (lineBufferParam: string) => number; diff --git a/lib/utils/validators.js b/lib/utils/validators.js new file mode 100644 index 00000000..67c098f4 --- /dev/null +++ b/lib/utils/validators.js @@ -0,0 +1,41 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.validateLineBuffer = exports.validateAddresses = exports.validateAddress = void 0; +const regEx_1 = require("./regEx"); +const commander_1 = require("commander"); +const utils_1 = require("ethers/lib/utils"); +const validateAddress = (address) => { + try { + if (typeof address === 'string' && address?.match(regEx_1.ethereumAddress)) + return (0, utils_1.getAddress)(address); + } + catch (err) { } + throw new commander_1.InvalidArgumentError(`Address must be in hexadecimal format with a 0x prefix.`); +}; +exports.validateAddress = validateAddress; +const validateAddresses = (addresses) => { + try { + const addressArray = convertAddresses(addresses); + if (addressArray) + return addressArray; + } + catch (err) { } + throw new commander_1.InvalidArgumentError(`Must be an address or an array of addresses in hexadecimal format with a 0x prefix. +If running for multiple addresses, the comma-separated list of addresses must not have white spaces.`); +}; +exports.validateAddresses = validateAddresses; +const convertAddresses = (addresses) => { + if (typeof addresses === 'string' && addresses?.match(regEx_1.ethereumAddress)) + return [(0, utils_1.getAddress)(addresses).toLowerCase()]; + if (typeof addresses === 'string' && addresses?.match(regEx_1.ethereumAddresses)) + return addresses.split(',').map((a) => (0, utils_1.getAddress)(a).toLowerCase()); + return undefined; +}; +const validateLineBuffer = (lineBufferParam) => { + const lineBuffer = parseInt(lineBufferParam); + if (isNaN(lineBuffer)) + throw Error(`Invalid line buffer "${lineBuffer}". Must be a number`); + return lineBuffer; +}; +exports.validateLineBuffer = validateLineBuffer; +//# sourceMappingURL=validators.js.map \ No newline at end of file diff --git a/src/ts/__tests__/diff.test.ts b/src/ts/__tests__/diff.test.ts index 3b847e76..802f56fa 100644 --- a/src/ts/__tests__/diff.test.ts +++ b/src/ts/__tests__/diff.test.ts @@ -1,4 +1,4 @@ -import { diffCode } from '../diff' +import { diffCode } from '../utils/diff' describe('Diff', () => { describe('2 line buffer', () => { diff --git a/src/ts/diffContracts.ts b/src/ts/diffContracts.ts new file mode 100644 index 00000000..b5164e03 --- /dev/null +++ b/src/ts/diffContracts.ts @@ -0,0 +1,213 @@ +import { isAddress } from './utils/regEx' + +const clc = require('cli-color') + +import { writeSolidity } from './writerFiles' +import { diffCode } from './utils/diff' +import { EtherscanParser } from './parserEtherscan' + +interface DiffOptions { + network: string + lineBuffer: number +} + +interface FlattenAndDiffOptions extends DiffOptions { + aFile?: string + bFile?: string + saveFiles?: boolean +} + +interface DiffFiles { + filename?: string + aCode?: string + bCode?: string + result: 'added' | 'removed' | 'match' | 'changed' +} +interface CompareContracts { + files: DiffFiles[] + contractNameA: string + contractNameB: string +} +export const compareContracts = async ( + addressA: string, + addressB: string, + etherscanParserA: EtherscanParser, + etherscanParserB: EtherscanParser, + options: DiffOptions, +): Promise => { + const files: DiffFiles[] = [] + const { files: aFiles, contractName: contractNameA } = + await etherscanParserA.getSourceCode(addressA) + + const { files: bFiles, contractName: contractNameB } = + await etherscanParserB.getSourceCode(addressB) + + if (aFiles.length === 1 && bFiles.length === 1) { + if (isAddress(aFiles[0].filename)) + files.push({ + filename: `${aFiles[0].filename} to ${bFiles[0].filename}`, + aCode: aFiles[0].code, + bCode: bFiles[0].code, + result: aFiles[0].code === bFiles[0].code ? 'match' : 'changed', + }) + return { + files, + contractNameA, + contractNameB, + } + } + + // For each file in the A contract + for (const aFile of aFiles) { + // Look for A contract filename in B contract + const bFile = bFiles.find((bFile) => bFile.filename === aFile.filename) + + if (bFile) { + // The A contract filename exists in the B contract + if (aFile.code !== bFile.code) { + // console.log(`${aFile.filename} ${clc.red('different')}:`) + files.push({ + filename: aFile.filename, + aCode: aFile.code, + bCode: bFile.code, + result: 'changed', + }) + } else { + files.push({ + filename: aFile.filename, + aCode: aFile.code, + bCode: bFile.code, + result: 'match', + }) + } + } else { + // The A contract filename does not exist in the B contract + files.push({ + filename: aFile.filename, + aCode: aFile.code, + result: 'removed', + }) + } + } + + // For each file in the B contract + for (const bFile of bFiles) { + // Look for B contract filename in A contract + const aFile = aFiles.find((aFile) => aFile.filename === bFile.filename) + if (!aFile) { + // The B contract filename does not exist in the A contract + files.push({ + filename: bFile.filename, + bCode: bFile.code, + result: 'added', + }) + } + } + + // Sort by filename + return { + files: files.sort((a, b) => a.filename.localeCompare(b.filename)), + contractNameA, + contractNameB, + } +} + +export const displayContractNames = ( + addressA: string, + addressB: string, + contractNameA: string, + contractNameB: string, + options: { network: string; bNetwork?: string }, +) => { + console.log( + `Contract A: ${addressA} ${contractNameA} on ${options.network}`, + ) + console.log( + `Contract B: ${addressB} ${contractNameB} on ${ + options.bNetwork || options.network + }\n`, + ) +} + +export const displayFileDiffSummary = (fileDiffs: DiffFiles[]) => { + for (const file of fileDiffs) { + switch (file.result) { + case 'match': + console.log(`${file.result.padEnd(7)} ${file.filename}`) + break + case 'added': + console.log( + `${clc.green(file.result.padEnd(7))} ${file.filename}`, + ) + break + case 'changed': + case 'removed': + console.log(`${clc.red(file.result)} ${file.filename}`) + break + } + } +} + +export const displayFileDiffs = ( + fileDiffs: DiffFiles[], + options: { lineBuffer?: number } = {}, +) => { + for (const file of fileDiffs) { + switch (file.result) { + case 'added': + console.log(`Added ${file.filename}`) + console.log(clc.green(file.bCode)) + break + case 'changed': + console.log(`Changed ${file.filename}`) + diffCode(file.aCode, file.bCode, options.lineBuffer) + break + case 'removed': + console.log(`Removed ${file.filename}`) + console.log(clc.red(file.aCode)) + break + } + } +} + +export const flattenAndDiff = async ( + addressA: string, + addressB: string, + aEtherscanParser: EtherscanParser, + bEtherscanParser: EtherscanParser, + options: FlattenAndDiffOptions, +): Promise<{ contractNameA: string; contractNameB: string }> => { + // Get verified Solidity code from Etherscan and flatten + const { solidityCode: codeA, contractName: contractNameA } = + await aEtherscanParser.getSolidityCode(addressA, options.aFile) + const { solidityCode: codeB, contractName: contractNameB } = + await bEtherscanParser.getSolidityCode( + addressB, + options.bFile || options.aFile, + ) + + displayContractNames( + addressA, + addressB, + contractNameA, + contractNameB, + options, + ) + + diffCode(codeA, codeB, options.lineBuffer) + + if (options.saveFiles) { + await writeSolidity(codeA, addressA) + await writeSolidity(codeB, addressB) + } + + displayContractNames( + addressA, + addressB, + contractNameA, + contractNameB, + options, + ) + + return { contractNameA, contractNameB } +} diff --git a/src/ts/parserEtherscan.ts b/src/ts/parserEtherscan.ts index 85a812d8..5be78ff7 100644 --- a/src/ts/parserEtherscan.ts +++ b/src/ts/parserEtherscan.ts @@ -227,7 +227,7 @@ export class EtherscanParser { contractAddress: string, filename?: string, ): Promise<{ - files: readonly { code: string; filename: string }[] + files: { code: string; filename: string }[] contractName: string compilerVersion: string remappings: Remapping[] diff --git a/src/ts/sol2uml.ts b/src/ts/sol2uml.ts index 42e7d6d4..6249fd74 100644 --- a/src/ts/sol2uml.ts +++ b/src/ts/sol2uml.ts @@ -1,25 +1,33 @@ #! /usr/bin/env node -import { convertUmlClasses2Dot } from './converterClasses2Dot' -import { parserUmlClasses } from './parserGeneral' -import { EtherscanParser, networks } from './parserEtherscan' -import { - classesConnectedToBaseContracts, - filterHiddenClasses, -} from './filterClasses' import { Command, Option } from 'commander' +import { ethers } from 'ethers' +import { basename } from 'path' + +import { convertUmlClasses2Dot } from './converterClasses2Dot' import { addDynamicVariables, convertClasses2StorageSections, } from './converterClasses2Storage' import { convertStorages2Dot } from './converterStorage2Dot' -import { isAddress } from './utils/regEx' -import { writeOutputFiles, writeSolidity } from './writerFiles' -import { basename } from 'path' +import { + compareContracts, + displayContractNames, + displayFileDiffs, + displayFileDiffSummary, + flattenAndDiff, +} from './diffContracts' +import { + classesConnectedToBaseContracts, + filterHiddenClasses, +} from './filterClasses' +import { EtherscanParser, networks } from './parserEtherscan' +import { parserUmlClasses } from './parserGeneral' import { squashUmlClasses } from './squashClasses' -import { diffCode } from './diff' import { addSlotValues } from './slotValues' -import { ethers } from 'ethers' +import { isAddress } from './utils/regEx' +import { validateAddress, validateLineBuffer } from './utils/validators' +import { writeOutputFiles, writeSolidity } from './writerFiles' const clc = require('cli-color') const program = new Command() @@ -61,7 +69,7 @@ Can also flatten or compare verified source files on Etherscan-like explorers.`, .addOption( new Option( '-e, --explorerUrl ', - 'Override network with custom blockchain explorer API URL. eg Polygon Mumbai testnet https://api-testnet.polygonscan.com/api', + 'Override the `network` option with a custom blockchain explorer API URL. eg Polygon Mumbai testnet https://api-testnet.polygonscan.com/api', ).env('EXPLORER_URL'), ) .addOption( @@ -262,6 +270,7 @@ WARNING: sol2uml does not use the Solidity compiler so may differ with solc. A k .option( '-s, --storage
', 'The address of the contract with the storage values. This will be different from the contract with the code if a proxy contract is used. This is not needed if `fileFolderAddress` is an address and the contract is not proxied.', + validateAddress, ) .addOption( new Option( @@ -397,6 +406,7 @@ In order for the merged code to compile, the following is done: .argument( '', 'Contract address in hexadecimal format with a 0x prefix.', + validateAddress, ) .action(async (contractAddress, options, command) => { try { @@ -443,33 +453,50 @@ The line numbers are from contract B. There are no line numbers for the red sect ) .argument( '', - 'Contract address in hexadecimal format with a 0x prefix of the first contract.', + 'Contract address in hexadecimal format with a 0x prefix of the first contract', + validateAddress, ) .argument( '', - 'Contract address in hexadecimal format with a 0x prefix of the second contract.', + 'Contract address in hexadecimal format with a 0x prefix of the second contract', + validateAddress, ) - .addOption( - new Option( - '-l, --lineBuffer ', - 'Minimum number of lines before and after changes', - ).default('4'), + .option( + '-l, --lineBuffer ', + 'Minimum number of lines before and after changes (default: 4)', + validateLineBuffer, ) - .addOption( - new Option( - '-af --aFile ', - 'Contract A source code filename without the .sol extension. (default: compares all source files)', - ), + .option( + '-af --aFile ', + 'Contract A source code filename without the .sol extension (default: compares all source files)', + ) + .option( + '-bf --bFile ', + 'Contract B source code filename without the .sol extension (default: aFile if specified)', ) .addOption( new Option( - '-bf --bFile ', - 'Contract B source code filename without the .sol extension. (default: aFile if specified)', - ), + '-bn, --bNetwork ', + 'Ethereum network which maps to a blockchain explorer for contract B if on a different blockchain to contract A. Contract A uses the `network` option (default: value of `network` option)', + ).choices(networks), + ) + .option( + '-be, --bExplorerUrl ', + 'Override the `bNetwork` option with custom blockchain explorer API URL for contract B if on a different blockchain to contract A. Contract A uses the `explorerUrl` (default: value of `explorerUrl` option)', + ) + .option( + '-bk, --bApiKey ', + 'Blockchain explorer API key for contract B if on a different blockchain to contract A. Contract A uses the `apiKey` option (default: value of `apiKey` option)', + ) + .option( + '-s, --summary', + 'Only show a summary of the file differences.', + false, ) + .option('--flatten', 'Flatten into a single file before comparing', false) .option( - '-s, --saveFiles', - 'Save the flattened contract code to the filesystem. The file names will be the contract address with a .sol extension.', + '--saveFiles', + 'Save the flattened contract code to the filesystem when using the `flatten` option. The file names will be the contract address with a .sol extension', false, ) .action(async (addressA, addressB, options, command) => { @@ -481,44 +508,61 @@ The line numbers are from contract B. There are no line numbers for the red sect ...options, } - // Diff solidity code - const lineBuffer = parseInt(options.lineBuffer) - if (isNaN(lineBuffer)) - throw Error( - `Invalid line buffer "${options.lineBuffer}". Must be a number`, - ) - - const etherscanParser = new EtherscanParser( + const aEtherscanParser = new EtherscanParser( combinedOptions.apiKey, combinedOptions.network, combinedOptions.explorerUrl, ) + const bEtherscanParser = new EtherscanParser( + combinedOptions.bApiKey || combinedOptions.apiKey, + combinedOptions.bNetwork || combinedOptions.network, + combinedOptions.bExplorerUrl || combinedOptions.explorerUrl, + ) - // Get verified Solidity code from Etherscan and flatten - const { solidityCode: codeA, contractName: contractNameA } = - await etherscanParser.getSolidityCode( + if (options.flatten || options.aFile) { + await flattenAndDiff( addressA, - combinedOptions.aFile, - ) - const { solidityCode: codeB, contractName: contractNameB } = - await etherscanParser.getSolidityCode( addressB, - combinedOptions.bFile || combinedOptions.aFile, + aEtherscanParser, + bEtherscanParser, + combinedOptions, ) + } else { + const { contractNameA, contractNameB, files } = + await compareContracts( + addressA, + addressB, + aEtherscanParser, + bEtherscanParser, + combinedOptions, + ) - console.log(`Difference between`) - console.log( - `A. ${addressA} ${contractNameA} on ${combinedOptions.network}`, - ) - console.log( - `B. ${addressB} ${contractNameB} on ${combinedOptions.network}\n`, - ) - - diffCode(codeA, codeB, lineBuffer) - - if (options.saveFiles) { - await writeSolidity(codeA, addressA) - await writeSolidity(codeB, addressB) + displayContractNames( + addressA, + addressB, + contractNameA, + contractNameB, + combinedOptions, + ) + displayFileDiffSummary(files) + + if (!options.summary) { + // Just show the summary if all the files are the same + const diffFiles = files.filter((f) => f.result !== 'match') + if (diffFiles.length === 0) return + + console.log() + displayFileDiffs(files, combinedOptions) + + displayContractNames( + addressA, + addressB, + contractNameA, + contractNameB, + combinedOptions, + ) + displayFileDiffSummary(files) + } } } catch (err) { console.error(err) diff --git a/src/ts/diff.ts b/src/ts/utils/diff.ts similarity index 99% rename from src/ts/diff.ts rename to src/ts/utils/diff.ts index 0d71fcc7..aa37d17f 100644 --- a/src/ts/diff.ts +++ b/src/ts/utils/diff.ts @@ -25,7 +25,7 @@ export const diffCode = (codeA: string, codeB: string, lineBuff: number) => { } /** - * Convert a diff array into human readable for the console + * Convert a diff array into human-readable for the console * @param {!Array.} diffs Array of diff tuples. * @param lines number of a lines in the second contract B * @param lineBuff number of a lines to output before and after the change diff --git a/src/ts/utils/regEx.ts b/src/ts/utils/regEx.ts index f58ea894..4aa2e2d2 100644 --- a/src/ts/utils/regEx.ts +++ b/src/ts/utils/regEx.ts @@ -1,3 +1,7 @@ +export const ethereumAddress = /^0x([A-Fa-f0-9]{40})$/ +// comma-separated list of addresses with no whitespace +export const ethereumAddresses = /^(0x[A-Fa-f0-9]{40},?)+$/ + export const isAddress = (input: string): boolean => { return input.match(/^0x([A-Fa-f0-9]{40})$/) !== null } diff --git a/src/ts/utils/validators.ts b/src/ts/utils/validators.ts new file mode 100644 index 00000000..b9fa3278 --- /dev/null +++ b/src/ts/utils/validators.ts @@ -0,0 +1,41 @@ +import { ethereumAddress, ethereumAddresses } from './regEx' +import { InvalidArgumentError } from 'commander' +import { getAddress } from 'ethers/lib/utils' + +export const validateAddress = (address: string): string => { + try { + if (typeof address === 'string' && address?.match(ethereumAddress)) + return getAddress(address) + } catch (err) {} + + throw new InvalidArgumentError( + `Address must be in hexadecimal format with a 0x prefix.`, + ) +} + +export const validateAddresses = (addresses: string): string[] => { + try { + const addressArray = convertAddresses(addresses) + if (addressArray) return addressArray + } catch (err) {} + + throw new InvalidArgumentError( + `Must be an address or an array of addresses in hexadecimal format with a 0x prefix. +If running for multiple addresses, the comma-separated list of addresses must not have white spaces.`, + ) +} + +const convertAddresses = (addresses: string): string[] => { + if (typeof addresses === 'string' && addresses?.match(ethereumAddress)) + return [getAddress(addresses).toLowerCase()] + if (typeof addresses === 'string' && addresses?.match(ethereumAddresses)) + return addresses.split(',').map((a) => getAddress(a).toLowerCase()) + return undefined +} + +export const validateLineBuffer = (lineBufferParam: string): number => { + const lineBuffer = parseInt(lineBufferParam) + if (isNaN(lineBuffer)) + throw Error(`Invalid line buffer "${lineBuffer}". Must be a number`) + return lineBuffer +} diff --git a/tests/tests.sh b/tests/tests.sh new file mode 100644 index 00000000..321adf31 --- /dev/null +++ b/tests/tests.sh @@ -0,0 +1,51 @@ + +export ARCHIVE_NODE_URL=https:// + +# Flatten + +### Aave V3 Pool mainnet +sol2uml flatten 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2 + +# Diff + +## Origin contracts +### OETH VaultCore +sol2uml diff 0x1091588Cc431275F99DC5Df311fd8E1Ab81c89F3 0xEA24e9Bac006DE9635Ac7fA4D767fFb64FB5645c -v +sol2uml diff 0x1091588Cc431275F99DC5Df311fd8E1Ab81c89F3 0xEA24e9Bac006DE9635Ac7fA4D767fFb64FB5645c -s +sol2uml diff 0x1091588Cc431275F99DC5Df311fd8E1Ab81c89F3 0xEA24e9Bac006DE9635Ac7fA4D767fFb64FB5645c --aFile VaultCore +sol2uml diff 0x1091588Cc431275F99DC5Df311fd8E1Ab81c89F3 0xEA24e9Bac006DE9635Ac7fA4D767fFb64FB5645c --aFile OETHVaultCore --bFile VaultCore -v +sol2uml diff 0x1091588Cc431275F99DC5Df311fd8E1Ab81c89F3 0xEA24e9Bac006DE9635Ac7fA4D767fFb64FB5645c --aFile VaultStorage + +### OETH Frax Strategy +### Has added and changed contracts +sol2uml diff 0x167747bf5b3b6bf2f7f7c4cce32c463e9598d425 0x5061cde874f75d119de3b07e191644097343ab9e -v + +### OUSD Token +## Old contract was flattened, new contract has multiple files +sol2uml diff 0xB248c975DaeAc47c4960EcBD10a79E486eBD1cA8 0x33db8d52d65F75E4cdDA1b02463760c9561A2aa1 -v +sol2uml diff 0xB248c975DaeAc47c4960EcBD10a79E486eBD1cA8 0x33db8d52d65F75E4cdDA1b02463760c9561A2aa1 --flatten -v +### OUSD Vault +sol2uml diff 0x997c35A0bf8E21404aE4379841E0603C957138c3 0x48Cf14DeA2f5dD31c57218877195913412D3278A -v + +## Curve +### stETH and frxETH Metapool +### Vyper contracts +sol2uml diff 0xdc24316b9ae028f1497c275eb9192a3ea0f67022 0xa1f8a6807c402e4a15ef4eba36528a3fed24e577 -v +### FRAX/USDC (crvFRAX) mainnet v arbitrum +sol2uml diff 0xdcef968d416a41cdac0ed8702fac8128a64241a2 0xc9b8a3fdecb9d5b218d02555a8baf332e5b740d5 --bNetwork arbitrum +sol2uml diff 0xdcef968d416a41cdac0ed8702fac8128a64241a2 0xc9b8a3fdecb9d5b218d02555a8baf332e5b740d5 --bn arbitrum + +## Aave +### Aave V3 Pool mainnet v Optimism +sol2uml diff 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2 0x794a61358d6845594f94dc1db02a252b5b4814ad -bn optimism +### Aave V3 Pool mainnet v Optimism flattened +sol2uml diff 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2 0x794a61358d6845594f94dc1db02a252b5b4814ad -bn optimism --flatten +### Aave V3 Pool Polygon v Optimism +### all files are the same +sol2uml diff 0x794a61358D6845594F94dc1DB02A252b5b4814aD 0x794a61358d6845594f94dc1db02a252b5b4814ad -n polygon -bn optimism + +## USDC +## flattened source files with different contract names +sol2uml diff 0xb7277a6e95992041568d9391d09d0122023778a2 0xa2327a938febf5fec13bacfb16ae10ecbc4cbdcf +# Compare DAI to USDC +sol2uml diff 0x6b175474e89094c44da98b954eedeac495271d0f 0xa2327a938febf5fec13bacfb16ae10ecbc4cbdcf