Explorar el Código

Add script to automatically minimize pragma (#5740)

Co-authored-by: Arr00 <13561405+arr00@users.noreply.github.com>
Hadrien Croubois hace 3 meses
padre
commit
387ce69e6b

+ 1 - 1
contracts/mocks/DummyImplementation.sol

@@ -1,6 +1,6 @@
 // SPDX-License-Identifier: MIT
 
-pragma solidity ^0.8.22;
+pragma solidity ^0.8.21;
 
 import {ERC1967Utils} from "../proxy/ERC1967/ERC1967Utils.sol";
 import {StorageSlot} from "../utils/StorageSlot.sol";

+ 1 - 1
contracts/mocks/account/AccountMock.sol

@@ -1,6 +1,6 @@
 // SPDX-License-Identifier: MIT
 
-pragma solidity ^0.8.27;
+pragma solidity ^0.8.26;
 
 import {Account} from "../../account/Account.sol";
 import {AccountERC7579} from "../../account/extensions/draft-AccountERC7579.sol";

+ 3 - 2
package.json

@@ -24,11 +24,12 @@
     "clean": "hardhat clean && rimraf build contracts/build",
     "prepack": "scripts/prepack.sh",
     "generate": "scripts/generate/run.js",
+    "pragma": "npm run compile && scripts/minimize-pragma.js artifacts/build-info/*",
     "version": "scripts/release/version.sh",
     "test": ". scripts/set-max-old-space-size.sh && hardhat test",
     "test:generation": "scripts/checks/generation.sh",
-    "test:inheritance": "scripts/checks/inheritance-ordering.js artifacts/build-info/*",
-    "test:pragma": "scripts/checks/pragma-validity.js artifacts/build-info/*",
+    "test:inheritance": "npm run compile && scripts/checks/inheritance-ordering.js artifacts/build-info/*",
+    "test:pragma": "npm run compile && scripts/checks/pragma-validity.js artifacts/build-info/*",
     "gas-report": "env ENABLE_GAS_REPORT=true npm run test",
     "slither": "npm run clean && slither ."
   },

+ 138 - 0
scripts/minimize-pragma.js

@@ -0,0 +1,138 @@
+#!/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));
+    }
+  }
+})();