#!/usr/bin/env node const fs = require('fs'); const graphlib = require('graphlib'); const semver = require('semver'); const pLimit = require('p-limit').default; 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, concurrency, _: 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' }, concurrency: { alias: 'c', type: 'number', default: 8 }, }); // limit concurrency const limit = pLimit(concurrency); /******************************************************************************************************************** * 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 => limit(() => 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)); } } })();