Files
openzeppelin-contracts/scripts/minimize-pragma.js
2025-06-25 09:56:44 -04:00

139 lines
5.4 KiB
JavaScript
Executable File

#!/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<string[]>} 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<string>} 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<string>} 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));
}
}
})();