dexorder
This commit is contained in:
20
lib_openzeppelin_contracts/scripts/checks/compare-layout.js
Normal file
20
lib_openzeppelin_contracts/scripts/checks/compare-layout.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const fs = require('fs');
|
||||
const { getStorageUpgradeReport } = require('@openzeppelin/upgrades-core/dist/storage');
|
||||
|
||||
const { ref, head } = require('yargs').argv;
|
||||
|
||||
const oldLayout = JSON.parse(fs.readFileSync(ref));
|
||||
const newLayout = JSON.parse(fs.readFileSync(head));
|
||||
|
||||
for (const name in oldLayout) {
|
||||
if (name in newLayout) {
|
||||
const report = getStorageUpgradeReport(oldLayout[name], newLayout[name], {});
|
||||
if (!report.ok) {
|
||||
console.log(`Storage layout incompatilibity found in ${name}:`);
|
||||
console.log(report.explain());
|
||||
process.exitCode = 1;
|
||||
}
|
||||
} else {
|
||||
console.log(`WARNING: ${name} is missing from the current branch`);
|
||||
}
|
||||
}
|
||||
247
lib_openzeppelin_contracts/scripts/checks/compareGasReports.js
Executable file
247
lib_openzeppelin_contracts/scripts/checks/compareGasReports.js
Executable file
@@ -0,0 +1,247 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const chalk = require('chalk');
|
||||
const { argv } = require('yargs')
|
||||
.env()
|
||||
.options({
|
||||
style: {
|
||||
type: 'string',
|
||||
choices: ['shell', 'markdown'],
|
||||
default: 'shell',
|
||||
},
|
||||
hideEqual: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
strictTesting: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Deduce base tx cost from the percentage denominator
|
||||
const BASE_TX_COST = 21000;
|
||||
|
||||
// Utilities
|
||||
function sum(...args) {
|
||||
return args.reduce((a, b) => a + b, 0);
|
||||
}
|
||||
|
||||
function average(...args) {
|
||||
return sum(...args) / args.length;
|
||||
}
|
||||
|
||||
function variation(current, previous, offset = 0) {
|
||||
return {
|
||||
value: current,
|
||||
delta: current - previous,
|
||||
prcnt: (100 * (current - previous)) / (previous - offset),
|
||||
};
|
||||
}
|
||||
|
||||
// Report class
|
||||
class Report {
|
||||
// Read report file
|
||||
static load(filepath) {
|
||||
return JSON.parse(fs.readFileSync(filepath, 'utf8'));
|
||||
}
|
||||
|
||||
// Compare two reports
|
||||
static compare(update, ref, opts = { hideEqual: true, strictTesting: false }) {
|
||||
if (JSON.stringify(update.options?.solcInfo) !== JSON.stringify(ref.options?.solcInfo)) {
|
||||
console.warn('WARNING: Reports produced with non matching metadata');
|
||||
}
|
||||
|
||||
// gasReporter 1.0.0 uses ".info", but 2.0.0 uses ".data"
|
||||
const updateInfo = update.info ?? update.data;
|
||||
const refInfo = ref.info ?? ref.data;
|
||||
|
||||
const deployments = updateInfo.deployments
|
||||
.map(contract =>
|
||||
Object.assign(contract, { previousVersion: refInfo.deployments.find(({ name }) => name === contract.name) }),
|
||||
)
|
||||
.filter(contract => contract.gasData?.length && contract.previousVersion?.gasData?.length)
|
||||
.flatMap(contract => [
|
||||
{
|
||||
contract: contract.name,
|
||||
method: '[bytecode length]',
|
||||
avg: variation(contract.bytecode.length / 2 - 1, contract.previousVersion.bytecode.length / 2 - 1),
|
||||
},
|
||||
{
|
||||
contract: contract.name,
|
||||
method: '[construction cost]',
|
||||
avg: variation(
|
||||
...[contract.gasData, contract.previousVersion.gasData].map(x => Math.round(average(...x))),
|
||||
BASE_TX_COST,
|
||||
),
|
||||
},
|
||||
])
|
||||
.sort((a, b) => `${a.contract}:${a.method}`.localeCompare(`${b.contract}:${b.method}`));
|
||||
|
||||
const methods = Object.keys(updateInfo.methods)
|
||||
.filter(key => refInfo.methods[key])
|
||||
.filter(key => updateInfo.methods[key].numberOfCalls > 0)
|
||||
.filter(
|
||||
key => !opts.strictTesting || updateInfo.methods[key].numberOfCalls === refInfo.methods[key].numberOfCalls,
|
||||
)
|
||||
.map(key => ({
|
||||
contract: refInfo.methods[key].contract,
|
||||
method: refInfo.methods[key].fnSig,
|
||||
min: variation(...[updateInfo, refInfo].map(x => Math.min(...x.methods[key].gasData)), BASE_TX_COST),
|
||||
max: variation(...[updateInfo, refInfo].map(x => Math.max(...x.methods[key].gasData)), BASE_TX_COST),
|
||||
avg: variation(...[updateInfo, refInfo].map(x => Math.round(average(...x.methods[key].gasData))), BASE_TX_COST),
|
||||
}))
|
||||
.sort((a, b) => `${a.contract}:${a.method}`.localeCompare(`${b.contract}:${b.method}`));
|
||||
|
||||
return []
|
||||
.concat(deployments, methods)
|
||||
.filter(row => !opts.hideEqual || row.min?.delta || row.max?.delta || row.avg?.delta);
|
||||
}
|
||||
}
|
||||
|
||||
// Display
|
||||
function center(text, length) {
|
||||
return text.padStart((text.length + length) / 2).padEnd(length);
|
||||
}
|
||||
|
||||
function plusSign(num) {
|
||||
return num > 0 ? '+' : '';
|
||||
}
|
||||
|
||||
function formatCellShell(cell) {
|
||||
const format = chalk[cell?.delta > 0 ? 'red' : cell?.delta < 0 ? 'green' : 'reset'];
|
||||
return [
|
||||
format((!isFinite(cell?.value) ? '-' : cell.value.toString()).padStart(8)),
|
||||
format((!isFinite(cell?.delta) ? '-' : plusSign(cell.delta) + cell.delta.toString()).padStart(8)),
|
||||
format((!isFinite(cell?.prcnt) ? '-' : plusSign(cell.prcnt) + cell.prcnt.toFixed(2) + '%').padStart(8)),
|
||||
];
|
||||
}
|
||||
|
||||
function formatCmpShell(rows) {
|
||||
const contractLength = Math.max(8, ...rows.map(({ contract }) => contract.length));
|
||||
const methodLength = Math.max(7, ...rows.map(({ method }) => method.length));
|
||||
|
||||
const COLS = [
|
||||
{ txt: '', length: 0 },
|
||||
{ txt: 'Contract', length: contractLength },
|
||||
{ txt: 'Method', length: methodLength },
|
||||
{ txt: 'Min', length: 30 },
|
||||
{ txt: 'Max', length: 30 },
|
||||
{ txt: 'Avg', length: 30 },
|
||||
{ txt: '', length: 0 },
|
||||
];
|
||||
const HEADER = COLS.map(entry => chalk.bold(center(entry.txt, entry.length || 0)))
|
||||
.join(' | ')
|
||||
.trim();
|
||||
const SEPARATOR = COLS.map(({ length }) => (length > 0 ? '-'.repeat(length + 2) : ''))
|
||||
.join('|')
|
||||
.trim();
|
||||
|
||||
return [
|
||||
'',
|
||||
HEADER,
|
||||
...rows.map(entry =>
|
||||
[
|
||||
'',
|
||||
chalk.grey(entry.contract.padEnd(contractLength)),
|
||||
entry.method.padEnd(methodLength),
|
||||
...formatCellShell(entry.min),
|
||||
...formatCellShell(entry.max),
|
||||
...formatCellShell(entry.avg),
|
||||
'',
|
||||
]
|
||||
.join(' | ')
|
||||
.trim(),
|
||||
),
|
||||
'',
|
||||
]
|
||||
.join(`\n${SEPARATOR}\n`)
|
||||
.trim();
|
||||
}
|
||||
|
||||
function alignPattern(align) {
|
||||
switch (align) {
|
||||
case 'left':
|
||||
case undefined:
|
||||
return ':-';
|
||||
case 'right':
|
||||
return '-:';
|
||||
case 'center':
|
||||
return ':-:';
|
||||
}
|
||||
}
|
||||
|
||||
function trend(value) {
|
||||
return value > 0 ? ':x:' : value < 0 ? ':heavy_check_mark:' : ':heavy_minus_sign:';
|
||||
}
|
||||
|
||||
function formatCellMarkdown(cell) {
|
||||
return [
|
||||
!isFinite(cell?.value) ? '-' : cell.value.toString(),
|
||||
!isFinite(cell?.delta) ? '-' : plusSign(cell.delta) + cell.delta.toString(),
|
||||
!isFinite(cell?.prcnt) ? '-' : plusSign(cell.prcnt) + cell.prcnt.toFixed(2) + '% ' + trend(cell.delta),
|
||||
];
|
||||
}
|
||||
|
||||
function formatCmpMarkdown(rows) {
|
||||
const COLS = [
|
||||
{ txt: '' },
|
||||
{ txt: 'Contract', align: 'left' },
|
||||
{ txt: 'Method', align: 'left' },
|
||||
{ txt: 'Min', align: 'right' },
|
||||
{ txt: '(+/-)', align: 'right' },
|
||||
{ txt: '%', align: 'right' },
|
||||
{ txt: 'Max', align: 'right' },
|
||||
{ txt: '(+/-)', align: 'right' },
|
||||
{ txt: '%', align: 'right' },
|
||||
{ txt: 'Avg', align: 'right' },
|
||||
{ txt: '(+/-)', align: 'right' },
|
||||
{ txt: '%', align: 'right' },
|
||||
{ txt: '' },
|
||||
];
|
||||
const HEADER = COLS.map(entry => entry.txt)
|
||||
.join(' | ')
|
||||
.trim();
|
||||
const SEPARATOR = COLS.map(entry => (entry.txt ? alignPattern(entry.align) : ''))
|
||||
.join('|')
|
||||
.trim();
|
||||
|
||||
return [
|
||||
'# Changes to gas costs',
|
||||
'',
|
||||
HEADER,
|
||||
SEPARATOR,
|
||||
rows
|
||||
.map(entry =>
|
||||
[
|
||||
'',
|
||||
entry.contract,
|
||||
entry.method,
|
||||
...formatCellMarkdown(entry.min),
|
||||
...formatCellMarkdown(entry.max),
|
||||
...formatCellMarkdown(entry.avg),
|
||||
'',
|
||||
]
|
||||
.join(' | ')
|
||||
.trim(),
|
||||
)
|
||||
.join('\n'),
|
||||
'',
|
||||
]
|
||||
.join('\n')
|
||||
.trim();
|
||||
}
|
||||
|
||||
// MAIN
|
||||
const report = Report.compare(Report.load(argv._[0]), Report.load(argv._[1]), argv);
|
||||
|
||||
switch (argv.style) {
|
||||
case 'markdown':
|
||||
console.log(formatCmpMarkdown(report));
|
||||
break;
|
||||
case 'shell':
|
||||
default:
|
||||
console.log(formatCmpShell(report));
|
||||
break;
|
||||
}
|
||||
38
lib_openzeppelin_contracts/scripts/checks/extract-layout.js
Normal file
38
lib_openzeppelin_contracts/scripts/checks/extract-layout.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const fs = require('fs');
|
||||
const { findAll, astDereferencer, srcDecoder } = require('solidity-ast/utils');
|
||||
const { extractStorageLayout } = require('@openzeppelin/upgrades-core/dist/storage/extract');
|
||||
|
||||
const { _ } = require('yargs').argv;
|
||||
|
||||
const skipPath = ['contracts/mocks/', 'contracts-exposed/'];
|
||||
const skipKind = ['interface', 'library'];
|
||||
|
||||
function extractLayouts(path) {
|
||||
const layout = {};
|
||||
const { input, output } = JSON.parse(fs.readFileSync(path));
|
||||
|
||||
const decoder = srcDecoder(input, output);
|
||||
const deref = astDereferencer(output);
|
||||
|
||||
for (const src in output.contracts) {
|
||||
if (skipPath.some(prefix => src.startsWith(prefix))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const contractDef of findAll('ContractDefinition', output.sources[src].ast)) {
|
||||
if (skipKind.includes(contractDef.contractKind)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
layout[contractDef.name] = extractStorageLayout(
|
||||
contractDef,
|
||||
decoder,
|
||||
deref,
|
||||
output.contracts[src][contractDef.name].storageLayout,
|
||||
);
|
||||
}
|
||||
}
|
||||
return layout;
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(Object.assign(..._.map(extractLayouts))));
|
||||
6
lib_openzeppelin_contracts/scripts/checks/generation.sh
Executable file
6
lib_openzeppelin_contracts/scripts/checks/generation.sh
Executable file
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
npm run generate
|
||||
git diff -R --exit-code
|
||||
54
lib_openzeppelin_contracts/scripts/checks/inheritance-ordering.js
Executable file
54
lib_openzeppelin_contracts/scripts/checks/inheritance-ordering.js
Executable file
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const path = require('path');
|
||||
const graphlib = require('graphlib');
|
||||
const { findAll } = require('solidity-ast/utils');
|
||||
const { _: artifacts } = require('yargs').argv;
|
||||
|
||||
for (const artifact of artifacts) {
|
||||
const { output: solcOutput } = require(path.resolve(__dirname, '../..', artifact));
|
||||
|
||||
const graph = new graphlib.Graph({ directed: true });
|
||||
const names = {};
|
||||
const linearized = [];
|
||||
|
||||
for (const source in solcOutput.contracts) {
|
||||
if (['contracts-exposed/', 'contracts/mocks/'].some(pattern => source.startsWith(pattern))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const contractDef of findAll('ContractDefinition', solcOutput.sources[source].ast)) {
|
||||
names[contractDef.id] = contractDef.name;
|
||||
linearized.push(contractDef.linearizedBaseContracts);
|
||||
|
||||
contractDef.linearizedBaseContracts.forEach((c1, i, contracts) =>
|
||||
contracts.slice(i + 1).forEach(c2 => {
|
||||
graph.setEdge(c1, c2);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// graphlib.alg.findCycles will not find minimal cycles.
|
||||
/// We are only interested int cycles of lengths 2 (needs proof)
|
||||
graph.nodes().forEach((x, i, nodes) =>
|
||||
nodes
|
||||
.slice(i + 1)
|
||||
.filter(y => graph.hasEdge(x, y) && graph.hasEdge(y, x))
|
||||
.forEach(y => {
|
||||
console.log(`Conflict between ${names[x]} and ${names[y]} detected in the following dependency chains:`);
|
||||
linearized
|
||||
.filter(chain => chain.includes(parseInt(x)) && chain.includes(parseInt(y)))
|
||||
.forEach(chain => {
|
||||
const comp = chain.indexOf(parseInt(x)) < chain.indexOf(parseInt(y)) ? '>' : '<';
|
||||
console.log(`- ${names[x]} ${comp} ${names[y]} in ${names[chain.find(Boolean)]}`);
|
||||
// console.log(`- ${names[x]} ${comp} ${names[y]}: ${chain.reverse().map(id => names[id]).join(', ')}`);
|
||||
});
|
||||
process.exitCode = 1;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!process.exitCode) {
|
||||
console.log('Contract ordering is consistent.');
|
||||
}
|
||||
Reference in New Issue
Block a user