diff --git a/scripts/minimize-pragma.js b/scripts/minimize-pragma.js new file mode 100644 index 00000000000..f750a7d1b0e --- /dev/null +++ b/scripts/minimize-pragma.js @@ -0,0 +1,129 @@ +const fs = require('fs'); +const graphlib = require('graphlib'); +const semver = require('semver'); +const { hideBin } = require('yargs/helpers'); +const yargs = require('yargs/yargs'); + +const getContractsMetadata = require('./get-contracts-metadata'); +const { versions: allSolcVersions, compile } = require('./solc-versions'); + +const { + argv: { pattern, skipPatterns, minVersionForContracts, minVersionForInterfaces, _: artifacts }, +} = yargs(hideBin(process.argv)) + .env('') + .options({ + pattern: { alias: 'p', type: 'string', default: 'contracts/**/*.sol' }, + skipPatterns: { alias: 's', type: 'string', default: 'contracts/mocks/**/*.sol' }, + minVersionForContracts: { type: 'string', default: '0.8.20' }, + minVersionForInterfaces: { type: 'string', default: '0.0.0' }, + }); + +/******************************************************************************************************************** + * HELPERS * + ********************************************************************************************************************/ + +/** + * Updates the pragma in the given file to the newPragma version. + * @param {*} file Absolute path to the file to update. + * @param {*} pragma New pragma version to set. (ex: '>=0.8.4') + */ +const updatePragma = (file, pragma) => + fs.writeFileSync( + file, + fs.readFileSync(file, 'utf8').replace(/pragma solidity [><=^]*[0-9]+.[0-9]+.[0-9]+;/, `pragma solidity ${pragma};`), + 'utf8', + ); + +/** + * Get the applicable pragmas for a given file by compiling it with all solc versions. + * @param {*} file Absolute path to the file to compile. + * @param {*} candidates List of solc version to test. (ex: ['0.8.4','0.8.5']) + * @returns {Promise} List of applicable pragmas. + */ +const getApplicablePragmas = (file, candidates = allSolcVersions) => + Promise.all( + candidates.map(version => + compile(file, version).then( + () => version, + () => null, + ), + ), + ).then(versions => versions.filter(Boolean)); + +/** + * Get the minimum applicable pragmas for a given file. + * @param {*} file Absolute path to the file to compile. + * @param {*} candidates List of solc version to test. (ex: ['0.8.4','0.8.5']) + * @returns {Promise} Smallest applicable pragma out of the list. + */ +const getMinimalApplicablePragma = (file, candidates = allSolcVersions) => + getApplicablePragmas(file, candidates).then(valid => { + if (valid.length == 0) { + throw new Error(`No valid pragma found for ${file}`); + } else { + return valid.sort(semver.compare).at(0); + } + }); + +/** + * Get the minimum applicable pragmas for a given file, and update the file to use it. + * @param {*} file Absolute path to the file to compile. + * @param {*} candidates List of solc version to test. (ex: ['0.8.4','0.8.5']) + * @param {*} prefix Prefix to use when building the pragma (ex: '^') + * @returns {Promise} Version that was used and set in the file + */ +const setMinimalApplicablePragma = (file, candidates = allSolcVersions, prefix = '>=') => + getMinimalApplicablePragma(file, candidates) + .then(version => `${prefix}${version}`) + .then(pragma => { + updatePragma(file, pragma); + return pragma; + }); + +/******************************************************************************************************************** + * MAIN * + ********************************************************************************************************************/ + +// Build metadata from artifact files (hardhat compilation) +const metadata = getContractsMetadata(pattern, skipPatterns, artifacts); + +// Build dependency graph +const graph = new graphlib.Graph({ directed: true }); +Object.keys(metadata).forEach(file => { + graph.setNode(file); + metadata[file].sources.forEach(dep => graph.setEdge(dep, file)); +}); + +// Weaken all pragma to allow exploration +Object.keys(metadata).forEach(file => updatePragma(file, '>=0.0.0')); + +// Do a topological traversal of the dependency graph, minimizing pragma for each file we encounter +(async () => { + const queue = graph.sources(); + const pragmas = {}; + while (queue.length) { + const file = queue.shift(); + if (!Object.hasOwn(pragmas, file)) { + if (Object.hasOwn(metadata, file)) { + const minVersion = metadata[file].interface ? minVersionForInterfaces : minVersionForContracts; + const parentsPragmas = graph + .predecessors(file) + .map(file => pragmas[file]) + .filter(Boolean); + const candidates = allSolcVersions.filter( + v => semver.gte(v, minVersion) && parentsPragmas.every(p => semver.satisfies(v, p)), + ); + const pragmaPrefix = metadata[file].interface ? '>=' : '^'; + + process.stdout.write( + `[${Object.keys(pragmas).length + 1}/${Object.keys(metadata).length}] Searching minimal version for ${file} ... `, + ); + const pragma = await setMinimalApplicablePragma(file, candidates, pragmaPrefix); + console.log(pragma); + + pragmas[file] = pragma; + } + queue.push(...graph.successors(file)); + } + } +})();