Fix .gitignore: stop tracking ignored files

This commit is contained in:
5t4l1n
2025-07-27 10:39:02 +05:30
parent b42747e9a3
commit 3a87ef0576
625 changed files with 88566 additions and 63 deletions
@@ -0,0 +1,19 @@
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(report.explain());
process.exitCode = 1;
}
} else {
console.log(`WARNING: ${name} is missing from the current branch`);
}
}
@@ -0,0 +1,243 @@
#!/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.config.metadata) !== JSON.stringify(ref.config.metadata)) {
throw new Error('Reports produced with non matching metadata');
}
const deployments = update.info.deployments
.map(contract =>
Object.assign(contract, { previousVersion: ref.info.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(update.info.methods)
.filter(key => ref.info.methods[key])
.filter(key => update.info.methods[key].numberOfCalls > 0)
.filter(
key => !opts.strictTesting || update.info.methods[key].numberOfCalls === ref.info.methods[key].numberOfCalls,
)
.map(key => ({
contract: ref.info.methods[key].contract,
method: ref.info.methods[key].fnSig,
min: variation(...[update, ref].map(x => Math.min(...x.info.methods[key].gasData)), BASE_TX_COST),
max: variation(...[update, ref].map(x => Math.max(...x.info.methods[key].gasData)), BASE_TX_COST),
avg: variation(...[update, ref].map(x => Math.round(average(...x.info.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;
}
@@ -0,0 +1,40 @@
const fs = require('fs');
const { findAll } = require('solidity-ast/utils');
const { astDereferencer } = require('@openzeppelin/upgrades-core/dist/ast-dereferencer');
const { solcInputOutputDecoder } = require('@openzeppelin/upgrades-core/dist/src-decoder');
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 = solcInputOutputDecoder(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))));
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
npm run generate
git diff -R --exit-code
@@ -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 (source.includes('/mocks/')) {
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.');
}
@@ -0,0 +1,41 @@
#!/usr/bin/env node
const path = require('path');
const glob = require('glob');
const startCase = require('lodash.startcase');
const baseDir = process.argv[2];
const files = glob.sync(baseDir + '/**/*.adoc').map(f => path.relative(baseDir, f));
console.log('.API');
function getPageTitle(directory) {
switch (directory) {
case 'metatx':
return 'Meta Transactions';
case 'common':
return 'Common (Tokens)';
default:
return startCase(directory);
}
}
const links = files.map(file => {
const doc = file.replace(baseDir, '');
const title = path.parse(file).name;
return {
xref: `* xref:${doc}[${getPageTitle(title)}]`,
title,
};
});
// Case-insensitive sort based on titles (so 'token/ERC20' gets sorted as 'erc20')
const sortedLinks = links.sort(function (a, b) {
return a.title.toLowerCase().localeCompare(b.title.toLowerCase(), undefined, { numeric: true });
});
for (const link of sortedLinks) {
console.log(link.xref);
}
@@ -0,0 +1,16 @@
function formatLines(...lines) {
return [...indentEach(0, lines)].join('\n') + '\n';
}
function* indentEach(indent, lines) {
for (const line of lines) {
if (Array.isArray(line)) {
yield* indentEach(indent + 1, line);
} else {
const padding = ' '.repeat(indent);
yield* line.split('\n').map(subline => (subline === '' ? '' : padding + subline));
}
}
}
module.exports = formatLines;
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env node
const cp = require('child_process');
const fs = require('fs');
const path = require('path');
const format = require('./format-lines');
function getVersion(path) {
try {
return fs.readFileSync(path, 'utf8').match(/\/\/ OpenZeppelin Contracts \(last updated v[^)]+\)/)[0];
} catch (err) {
return null;
}
}
function generateFromTemplate(file, template, outputPrefix = '') {
const script = path.relative(path.join(__dirname, '../..'), __filename);
const input = path.join(path.dirname(script), template);
const output = path.join(outputPrefix, file);
const version = getVersion(output);
const content = format(
'// SPDX-License-Identifier: MIT',
...(version ? [version + ` (${file})`] : []),
`// This file was procedurally generated from ${input}.`,
'',
require(template),
);
fs.writeFileSync(output, content);
cp.execFileSync('prettier', ['--write', output]);
}
// Contracts
for (const [file, template] of Object.entries({
'utils/math/SafeCast.sol': './templates/SafeCast.js',
'utils/structs/EnumerableSet.sol': './templates/EnumerableSet.js',
'utils/structs/EnumerableMap.sol': './templates/EnumerableMap.js',
'utils/Checkpoints.sol': './templates/Checkpoints.js',
'utils/StorageSlot.sol': './templates/StorageSlot.js',
})) {
generateFromTemplate(file, template, './contracts/');
}
// Tests
for (const [file, template] of Object.entries({
'utils/Checkpoints.t.sol': './templates/Checkpoints.t.js',
})) {
generateFromTemplate(file, template, './test/');
}
@@ -0,0 +1,304 @@
const format = require('../format-lines');
const { OPTS, LEGACY_OPTS } = require('./Checkpoints.opts.js');
// TEMPLATE
const header = `\
pragma solidity ^0.8.0;
import "./math/Math.sol";
import "./math/SafeCast.sol";
/**
* @dev This library defines the \`History\` struct, for checkpointing values as they change at different points in
* time, and later looking up past values by block number. See {Votes} as an example.
*
* To create a history of checkpoints define a variable type \`Checkpoints.History\` in your contract, and store a new
* checkpoint for the current transaction block using the {push} function.
*
* _Available since v4.5._
*/
`;
const types = opts => `\
struct ${opts.historyTypeName} {
${opts.checkpointTypeName}[] ${opts.checkpointFieldName};
}
struct ${opts.checkpointTypeName} {
${opts.keyTypeName} ${opts.keyFieldName};
${opts.valueTypeName} ${opts.valueFieldName};
}
`;
/* eslint-disable max-len */
const operations = opts => `\
/**
* @dev Pushes a (\`key\`, \`value\`) pair into a ${opts.historyTypeName} so that it is stored as the checkpoint.
*
* Returns previous value and new value.
*/
function push(
${opts.historyTypeName} storage self,
${opts.keyTypeName} key,
${opts.valueTypeName} value
) internal returns (${opts.valueTypeName}, ${opts.valueTypeName}) {
return _insert(self.${opts.checkpointFieldName}, key, value);
}
/**
* @dev Returns the value in the first (oldest) checkpoint with key greater or equal than the search key, or zero if there is none.
*/
function lowerLookup(${opts.historyTypeName} storage self, ${opts.keyTypeName} key) internal view returns (${opts.valueTypeName}) {
uint256 len = self.${opts.checkpointFieldName}.length;
uint256 pos = _lowerBinaryLookup(self.${opts.checkpointFieldName}, key, 0, len);
return pos == len ? 0 : _unsafeAccess(self.${opts.checkpointFieldName}, pos).${opts.valueFieldName};
}
/**
* @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key, or zero if there is none.
*/
function upperLookup(${opts.historyTypeName} storage self, ${opts.keyTypeName} key) internal view returns (${opts.valueTypeName}) {
uint256 len = self.${opts.checkpointFieldName}.length;
uint256 pos = _upperBinaryLookup(self.${opts.checkpointFieldName}, key, 0, len);
return pos == 0 ? 0 : _unsafeAccess(self.${opts.checkpointFieldName}, pos - 1).${opts.valueFieldName};
}
/**
* @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key, or zero if there is none.
*
* NOTE: This is a variant of {upperLookup} that is optimised to find "recent" checkpoint (checkpoints with high keys).
*/
function upperLookupRecent(${opts.historyTypeName} storage self, ${opts.keyTypeName} key) internal view returns (${opts.valueTypeName}) {
uint256 len = self.${opts.checkpointFieldName}.length;
uint256 low = 0;
uint256 high = len;
if (len > 5) {
uint256 mid = len - Math.sqrt(len);
if (key < _unsafeAccess(self.${opts.checkpointFieldName}, mid)._key) {
high = mid;
} else {
low = mid + 1;
}
}
uint256 pos = _upperBinaryLookup(self.${opts.checkpointFieldName}, key, low, high);
return pos == 0 ? 0 : _unsafeAccess(self.${opts.checkpointFieldName}, pos - 1).${opts.valueFieldName};
}
`;
const legacyOperations = opts => `\
/**
* @dev Returns the value at a given block number. If a checkpoint is not available at that block, the closest one
* before it is returned, or zero otherwise. Because the number returned corresponds to that at the end of the
* block, the requested block number must be in the past, excluding the current block.
*/
function getAtBlock(${opts.historyTypeName} storage self, uint256 blockNumber) internal view returns (uint256) {
require(blockNumber < block.number, "Checkpoints: block not yet mined");
uint32 key = SafeCast.toUint32(blockNumber);
uint256 len = self.${opts.checkpointFieldName}.length;
uint256 pos = _upperBinaryLookup(self.${opts.checkpointFieldName}, key, 0, len);
return pos == 0 ? 0 : _unsafeAccess(self.${opts.checkpointFieldName}, pos - 1).${opts.valueFieldName};
}
/**
* @dev Returns the value at a given block number. If a checkpoint is not available at that block, the closest one
* before it is returned, or zero otherwise. Similar to {upperLookup} but optimized for the case when the searched
* checkpoint is probably "recent", defined as being among the last sqrt(N) checkpoints where N is the number of
* checkpoints.
*/
function getAtProbablyRecentBlock(${opts.historyTypeName} storage self, uint256 blockNumber) internal view returns (uint256) {
require(blockNumber < block.number, "Checkpoints: block not yet mined");
uint32 key = SafeCast.toUint32(blockNumber);
uint256 len = self.${opts.checkpointFieldName}.length;
uint256 low = 0;
uint256 high = len;
if (len > 5) {
uint256 mid = len - Math.sqrt(len);
if (key < _unsafeAccess(self.${opts.checkpointFieldName}, mid)._blockNumber) {
high = mid;
} else {
low = mid + 1;
}
}
uint256 pos = _upperBinaryLookup(self.${opts.checkpointFieldName}, key, low, high);
return pos == 0 ? 0 : _unsafeAccess(self.${opts.checkpointFieldName}, pos - 1).${opts.valueFieldName};
}
/**
* @dev Pushes a value onto a History so that it is stored as the checkpoint for the current block.
*
* Returns previous value and new value.
*/
function push(${opts.historyTypeName} storage self, uint256 value) internal returns (uint256, uint256) {
return _insert(self.${opts.checkpointFieldName}, SafeCast.toUint32(block.number), SafeCast.toUint224(value));
}
/**
* @dev Pushes a value onto a History, by updating the latest value using binary operation \`op\`. The new value will
* be set to \`op(latest, delta)\`.
*
* Returns previous value and new value.
*/
function push(
${opts.historyTypeName} storage self,
function(uint256, uint256) view returns (uint256) op,
uint256 delta
) internal returns (uint256, uint256) {
return push(self, op(latest(self), delta));
}
`;
const common = opts => `\
/**
* @dev Returns the value in the most recent checkpoint, or zero if there are no checkpoints.
*/
function latest(${opts.historyTypeName} storage self) internal view returns (${opts.valueTypeName}) {
uint256 pos = self.${opts.checkpointFieldName}.length;
return pos == 0 ? 0 : _unsafeAccess(self.${opts.checkpointFieldName}, pos - 1).${opts.valueFieldName};
}
/**
* @dev Returns whether there is a checkpoint in the structure (i.e. it is not empty), and if so the key and value
* in the most recent checkpoint.
*/
function latestCheckpoint(${opts.historyTypeName} storage self)
internal
view
returns (
bool exists,
${opts.keyTypeName} ${opts.keyFieldName},
${opts.valueTypeName} ${opts.valueFieldName}
)
{
uint256 pos = self.${opts.checkpointFieldName}.length;
if (pos == 0) {
return (false, 0, 0);
} else {
${opts.checkpointTypeName} memory ckpt = _unsafeAccess(self.${opts.checkpointFieldName}, pos - 1);
return (true, ckpt.${opts.keyFieldName}, ckpt.${opts.valueFieldName});
}
}
/**
* @dev Returns the number of checkpoint.
*/
function length(${opts.historyTypeName} storage self) internal view returns (uint256) {
return self.${opts.checkpointFieldName}.length;
}
/**
* @dev Pushes a (\`key\`, \`value\`) pair into an ordered list of checkpoints, either by inserting a new checkpoint,
* or by updating the last one.
*/
function _insert(
${opts.checkpointTypeName}[] storage self,
${opts.keyTypeName} key,
${opts.valueTypeName} value
) private returns (${opts.valueTypeName}, ${opts.valueTypeName}) {
uint256 pos = self.length;
if (pos > 0) {
// Copying to memory is important here.
${opts.checkpointTypeName} memory last = _unsafeAccess(self, pos - 1);
// Checkpoint keys must be non-decreasing.
require(last.${opts.keyFieldName} <= key, "Checkpoint: decreasing keys");
// Update or push new checkpoint
if (last.${opts.keyFieldName} == key) {
_unsafeAccess(self, pos - 1).${opts.valueFieldName} = value;
} else {
self.push(${opts.checkpointTypeName}({${opts.keyFieldName}: key, ${opts.valueFieldName}: value}));
}
return (last.${opts.valueFieldName}, value);
} else {
self.push(${opts.checkpointTypeName}({${opts.keyFieldName}: key, ${opts.valueFieldName}: value}));
return (0, value);
}
}
/**
* @dev Return the index of the last (most recent) checkpoint with key lower or equal than the search key, or \`high\` if there is none.
* \`low\` and \`high\` define a section where to do the search, with inclusive \`low\` and exclusive \`high\`.
*
* WARNING: \`high\` should not be greater than the array's length.
*/
function _upperBinaryLookup(
${opts.checkpointTypeName}[] storage self,
${opts.keyTypeName} key,
uint256 low,
uint256 high
) private view returns (uint256) {
while (low < high) {
uint256 mid = Math.average(low, high);
if (_unsafeAccess(self, mid).${opts.keyFieldName} > key) {
high = mid;
} else {
low = mid + 1;
}
}
return high;
}
/**
* @dev Return the index of the first (oldest) checkpoint with key is greater or equal than the search key, or \`high\` if there is none.
* \`low\` and \`high\` define a section where to do the search, with inclusive \`low\` and exclusive \`high\`.
*
* WARNING: \`high\` should not be greater than the array's length.
*/
function _lowerBinaryLookup(
${opts.checkpointTypeName}[] storage self,
${opts.keyTypeName} key,
uint256 low,
uint256 high
) private view returns (uint256) {
while (low < high) {
uint256 mid = Math.average(low, high);
if (_unsafeAccess(self, mid).${opts.keyFieldName} < key) {
low = mid + 1;
} else {
high = mid;
}
}
return high;
}
/**
* @dev Access an element of the array without performing bounds check. The position is assumed to be within bounds.
*/
function _unsafeAccess(${opts.checkpointTypeName}[] storage self, uint256 pos)
private
pure
returns (${opts.checkpointTypeName} storage result)
{
assembly {
mstore(0, self.slot)
result.slot := add(keccak256(0, 0x20), pos)
}
}
`;
/* eslint-enable max-len */
// GENERATE
module.exports = format(
header.trimEnd(),
'library Checkpoints {',
[
// Legacy types & functions
types(LEGACY_OPTS),
legacyOperations(LEGACY_OPTS),
common(LEGACY_OPTS),
// New flavors
...OPTS.flatMap(opts => [types(opts), operations(opts), common(opts)]),
],
'}',
);
@@ -0,0 +1,22 @@
// OPTIONS
const VALUE_SIZES = [224, 160];
const defaultOpts = size => ({
historyTypeName: `Trace${size}`,
checkpointTypeName: `Checkpoint${size}`,
checkpointFieldName: '_checkpoints',
keyTypeName: `uint${256 - size}`,
keyFieldName: '_key',
valueTypeName: `uint${size}`,
valueFieldName: '_value',
});
module.exports = {
OPTS: VALUE_SIZES.map(size => defaultOpts(size)),
LEGACY_OPTS: {
...defaultOpts(224),
historyTypeName: 'History',
checkpointTypeName: 'Checkpoint',
keyFieldName: '_blockNumber',
},
};
@@ -0,0 +1,256 @@
const format = require('../format-lines');
const { capitalize } = require('../../helpers');
const { OPTS, LEGACY_OPTS } = require('./Checkpoints.opts.js');
// TEMPLATE
const header = `\
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "../../contracts/utils/Checkpoints.sol";
import "../../contracts/utils/math/SafeCast.sol";
`;
/* eslint-disable max-len */
const common = opts => `\
using Checkpoints for Checkpoints.${opts.historyTypeName};
// Maximum gap between keys used during the fuzzing tests: the \`_prepareKeys\` function with make sure that
// key#n+1 is in the [key#n, key#n + _KEY_MAX_GAP] range.
uint8 internal constant _KEY_MAX_GAP = 64;
Checkpoints.${opts.historyTypeName} internal _ckpts;
// helpers
function _bound${capitalize(opts.keyTypeName)}(
${opts.keyTypeName} x,
${opts.keyTypeName} min,
${opts.keyTypeName} max
) internal view returns (${opts.keyTypeName}) {
return SafeCast.to${capitalize(opts.keyTypeName)}(bound(uint256(x), uint256(min), uint256(max)));
}
function _prepareKeys(
${opts.keyTypeName}[] memory keys,
${opts.keyTypeName} maxSpread
) internal view {
${opts.keyTypeName} lastKey = 0;
for (uint256 i = 0; i < keys.length; ++i) {
${opts.keyTypeName} key = _bound${capitalize(opts.keyTypeName)}(keys[i], lastKey, lastKey + maxSpread);
keys[i] = key;
lastKey = key;
}
}
function _assertLatestCheckpoint(
bool exist,
${opts.keyTypeName} key,
${opts.valueTypeName} value
) internal {
(bool _exist, ${opts.keyTypeName} _key, ${opts.valueTypeName} _value) = _ckpts.latestCheckpoint();
assertEq(_exist, exist);
assertEq(_key, key);
assertEq(_value, value);
}
`;
const testTrace = opts => `\
// tests
function testPush(
${opts.keyTypeName}[] memory keys,
${opts.valueTypeName}[] memory values,
${opts.keyTypeName} pastKey
) public {
vm.assume(values.length > 0 && values.length <= keys.length);
_prepareKeys(keys, _KEY_MAX_GAP);
// initial state
assertEq(_ckpts.length(), 0);
assertEq(_ckpts.latest(), 0);
_assertLatestCheckpoint(false, 0, 0);
uint256 duplicates = 0;
for (uint256 i = 0; i < keys.length; ++i) {
${opts.keyTypeName} key = keys[i];
${opts.valueTypeName} value = values[i % values.length];
if (i > 0 && key == keys[i-1]) ++duplicates;
// push
_ckpts.push(key, value);
// check length & latest
assertEq(_ckpts.length(), i + 1 - duplicates);
assertEq(_ckpts.latest(), value);
_assertLatestCheckpoint(true, key, value);
}
if (keys.length > 0) {
${opts.keyTypeName} lastKey = keys[keys.length - 1];
if (lastKey > 0) {
pastKey = _bound${capitalize(opts.keyTypeName)}(pastKey, 0, lastKey - 1);
vm.expectRevert();
this.push(pastKey, values[keys.length % values.length]);
}
}
}
// used to test reverts
function push(${opts.keyTypeName} key, ${opts.valueTypeName} value) external {
_ckpts.push(key, value);
}
function testLookup(
${opts.keyTypeName}[] memory keys,
${opts.valueTypeName}[] memory values,
${opts.keyTypeName} lookup
) public {
vm.assume(values.length > 0 && values.length <= keys.length);
_prepareKeys(keys, _KEY_MAX_GAP);
${opts.keyTypeName} lastKey = keys.length == 0 ? 0 : keys[keys.length - 1];
lookup = _bound${capitalize(opts.keyTypeName)}(lookup, 0, lastKey + _KEY_MAX_GAP);
${opts.valueTypeName} upper = 0;
${opts.valueTypeName} lower = 0;
${opts.keyTypeName} lowerKey = type(${opts.keyTypeName}).max;
for (uint256 i = 0; i < keys.length; ++i) {
${opts.keyTypeName} key = keys[i];
${opts.valueTypeName} value = values[i % values.length];
// push
_ckpts.push(key, value);
// track expected result of lookups
if (key <= lookup) {
upper = value;
}
// find the first key that is not smaller than the lookup key
if (key >= lookup && (i == 0 || keys[i-1] < lookup)) {
lowerKey = key;
}
if (key == lowerKey) {
lower = value;
}
}
// check lookup
assertEq(_ckpts.lowerLookup(lookup), lower);
assertEq(_ckpts.upperLookup(lookup), upper);
assertEq(_ckpts.upperLookupRecent(lookup), upper);
}
`;
const testHistory = opts => `\
// tests
function testPush(
${opts.keyTypeName}[] memory keys,
${opts.valueTypeName}[] memory values,
${opts.keyTypeName} pastKey
) public {
vm.assume(values.length > 0 && values.length <= keys.length);
_prepareKeys(keys, _KEY_MAX_GAP);
// initial state
assertEq(_ckpts.length(), 0);
assertEq(_ckpts.latest(), 0);
_assertLatestCheckpoint(false, 0, 0);
uint256 duplicates = 0;
for (uint256 i = 0; i < keys.length; ++i) {
${opts.keyTypeName} key = keys[i];
${opts.valueTypeName} value = values[i % values.length];
if (i > 0 && key == keys[i - 1]) ++duplicates;
// push
vm.roll(key);
_ckpts.push(value);
// check length & latest
assertEq(_ckpts.length(), i + 1 - duplicates);
assertEq(_ckpts.latest(), value);
_assertLatestCheckpoint(true, key, value);
}
// Can't push any key in the past
if (keys.length > 0) {
${opts.keyTypeName} lastKey = keys[keys.length - 1];
if (lastKey > 0) {
pastKey = _bound${capitalize(opts.keyTypeName)}(pastKey, 0, lastKey - 1);
vm.roll(pastKey);
vm.expectRevert();
this.push(values[keys.length % values.length]);
}
}
}
// used to test reverts
function push(${opts.valueTypeName} value) external {
_ckpts.push(value);
}
function testLookup(
${opts.keyTypeName}[] memory keys,
${opts.valueTypeName}[] memory values,
${opts.keyTypeName} lookup
) public {
vm.assume(keys.length > 0);
vm.assume(values.length > 0 && values.length <= keys.length);
_prepareKeys(keys, _KEY_MAX_GAP);
${opts.keyTypeName} lastKey = keys[keys.length - 1];
vm.assume(lastKey > 0);
lookup = _bound${capitalize(opts.keyTypeName)}(lookup, 0, lastKey - 1);
${opts.valueTypeName} upper = 0;
for (uint256 i = 0; i < keys.length; ++i) {
${opts.keyTypeName} key = keys[i];
${opts.valueTypeName} value = values[i % values.length];
// push
vm.roll(key);
_ckpts.push(value);
// track expected result of lookups
if (key <= lookup) {
upper = value;
}
}
// check lookup
assertEq(_ckpts.getAtBlock(lookup), upper);
assertEq(_ckpts.getAtProbablyRecentBlock(lookup), upper);
vm.expectRevert(); this.getAtBlock(lastKey);
vm.expectRevert(); this.getAtBlock(lastKey + 1);
vm.expectRevert(); this.getAtProbablyRecentBlock(lastKey);
vm.expectRevert(); this.getAtProbablyRecentBlock(lastKey + 1);
}
// used to test reverts
function getAtBlock(${opts.keyTypeName} key) external view {
_ckpts.getAtBlock(key);
}
// used to test reverts
function getAtProbablyRecentBlock(${opts.keyTypeName} key) external view {
_ckpts.getAtProbablyRecentBlock(key);
}
`;
/* eslint-enable max-len */
// GENERATE
module.exports = format(
header,
// HISTORY
`contract Checkpoints${LEGACY_OPTS.historyTypeName}Test is Test {`,
[common(LEGACY_OPTS), testHistory(LEGACY_OPTS)],
'}',
// TRACEXXX
...OPTS.flatMap(opts => [
`contract Checkpoints${opts.historyTypeName}Test is Test {`,
[common(opts), testTrace(opts)],
'}',
]),
);
@@ -0,0 +1,310 @@
const format = require('../format-lines');
const { fromBytes32, toBytes32 } = require('./conversion');
const TYPES = [
{ name: 'UintToUintMap', keyType: 'uint256', valueType: 'uint256' },
{ name: 'UintToAddressMap', keyType: 'uint256', valueType: 'address' },
{ name: 'AddressToUintMap', keyType: 'address', valueType: 'uint256' },
{ name: 'Bytes32ToUintMap', keyType: 'bytes32', valueType: 'uint256' },
];
/* eslint-disable max-len */
const header = `\
pragma solidity ^0.8.0;
import "./EnumerableSet.sol";
/**
* @dev Library for managing an enumerable variant of Solidity's
* https://solidity.readthedocs.io/en/latest/types.html#mapping-types[\`mapping\`]
* type.
*
* Maps have the following properties:
*
* - Entries are added, removed, and checked for existence in constant time
* (O(1)).
* - Entries are enumerated in O(n). No guarantees are made on the ordering.
*
* \`\`\`solidity
* contract Example {
* // Add the library methods
* using EnumerableMap for EnumerableMap.UintToAddressMap;
*
* // Declare a set state variable
* EnumerableMap.UintToAddressMap private myMap;
* }
* \`\`\`
*
* The following map types are supported:
*
* - \`uint256 -> address\` (\`UintToAddressMap\`) since v3.0.0
* - \`address -> uint256\` (\`AddressToUintMap\`) since v4.6.0
* - \`bytes32 -> bytes32\` (\`Bytes32ToBytes32Map\`) since v4.6.0
* - \`uint256 -> uint256\` (\`UintToUintMap\`) since v4.7.0
* - \`bytes32 -> uint256\` (\`Bytes32ToUintMap\`) since v4.7.0
*
* [WARNING]
* ====
* Trying to delete such a structure from storage will likely result in data corruption, rendering the structure
* unusable.
* See https://github.com/ethereum/solidity/pull/11843[ethereum/solidity#11843] for more info.
*
* In order to clean an EnumerableMap, you can either remove all elements one by one or create a fresh instance using an
* array of EnumerableMap.
* ====
*/
`;
/* eslint-enable max-len */
const defaultMap = () => `\
// To implement this library for multiple types with as little code
// repetition as possible, we write it in terms of a generic Map type with
// bytes32 keys and values.
// The Map implementation uses private functions, and user-facing
// implementations (such as Uint256ToAddressMap) are just wrappers around
// the underlying Map.
// This means that we can only create new EnumerableMaps for types that fit
// in bytes32.
struct Bytes32ToBytes32Map {
// Storage of keys
EnumerableSet.Bytes32Set _keys;
mapping(bytes32 => bytes32) _values;
}
/**
* @dev Adds a key-value pair to a map, or updates the value for an existing
* key. O(1).
*
* Returns true if the key was added to the map, that is if it was not
* already present.
*/
function set(
Bytes32ToBytes32Map storage map,
bytes32 key,
bytes32 value
) internal returns (bool) {
map._values[key] = value;
return map._keys.add(key);
}
/**
* @dev Removes a key-value pair from a map. O(1).
*
* Returns true if the key was removed from the map, that is if it was present.
*/
function remove(Bytes32ToBytes32Map storage map, bytes32 key) internal returns (bool) {
delete map._values[key];
return map._keys.remove(key);
}
/**
* @dev Returns true if the key is in the map. O(1).
*/
function contains(Bytes32ToBytes32Map storage map, bytes32 key) internal view returns (bool) {
return map._keys.contains(key);
}
/**
* @dev Returns the number of key-value pairs in the map. O(1).
*/
function length(Bytes32ToBytes32Map storage map) internal view returns (uint256) {
return map._keys.length();
}
/**
* @dev Returns the key-value pair stored at position \`index\` in the map. O(1).
*
* Note that there are no guarantees on the ordering of entries inside the
* array, and it may change when more entries are added or removed.
*
* Requirements:
*
* - \`index\` must be strictly less than {length}.
*/
function at(Bytes32ToBytes32Map storage map, uint256 index) internal view returns (bytes32, bytes32) {
bytes32 key = map._keys.at(index);
return (key, map._values[key]);
}
/**
* @dev Tries to returns the value associated with \`key\`. O(1).
* Does not revert if \`key\` is not in the map.
*/
function tryGet(Bytes32ToBytes32Map storage map, bytes32 key) internal view returns (bool, bytes32) {
bytes32 value = map._values[key];
if (value == bytes32(0)) {
return (contains(map, key), bytes32(0));
} else {
return (true, value);
}
}
/**
* @dev Returns the value associated with \`key\`. O(1).
*
* Requirements:
*
* - \`key\` must be in the map.
*/
function get(Bytes32ToBytes32Map storage map, bytes32 key) internal view returns (bytes32) {
bytes32 value = map._values[key];
require(value != 0 || contains(map, key), "EnumerableMap: nonexistent key");
return value;
}
/**
* @dev Same as {get}, with a custom error message when \`key\` is not in the map.
*
* CAUTION: This function is deprecated because it requires allocating memory for the error
* message unnecessarily. For custom revert reasons use {tryGet}.
*/
function get(
Bytes32ToBytes32Map storage map,
bytes32 key,
string memory errorMessage
) internal view returns (bytes32) {
bytes32 value = map._values[key];
require(value != 0 || contains(map, key), errorMessage);
return value;
}
/**
* @dev Return the an array containing all the keys
*
* WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
* to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
* this function has an unbounded cost, and using it as part of a state-changing function may render the function
* uncallable if the map grows to a point where copying to memory consumes too much gas to fit in a block.
*/
function keys(Bytes32ToBytes32Map storage map) internal view returns (bytes32[] memory) {
return map._keys.values();
}
`;
const customMap = ({ name, keyType, valueType }) => `\
// ${name}
struct ${name} {
Bytes32ToBytes32Map _inner;
}
/**
* @dev Adds a key-value pair to a map, or updates the value for an existing
* key. O(1).
*
* Returns true if the key was added to the map, that is if it was not
* already present.
*/
function set(
${name} storage map,
${keyType} key,
${valueType} value
) internal returns (bool) {
return set(map._inner, ${toBytes32(keyType, 'key')}, ${toBytes32(valueType, 'value')});
}
/**
* @dev Removes a value from a map. O(1).
*
* Returns true if the key was removed from the map, that is if it was present.
*/
function remove(${name} storage map, ${keyType} key) internal returns (bool) {
return remove(map._inner, ${toBytes32(keyType, 'key')});
}
/**
* @dev Returns true if the key is in the map. O(1).
*/
function contains(${name} storage map, ${keyType} key) internal view returns (bool) {
return contains(map._inner, ${toBytes32(keyType, 'key')});
}
/**
* @dev Returns the number of elements in the map. O(1).
*/
function length(${name} storage map) internal view returns (uint256) {
return length(map._inner);
}
/**
* @dev Returns the element stored at position \`index\` in the map. O(1).
* Note that there are no guarantees on the ordering of values inside the
* array, and it may change when more values are added or removed.
*
* Requirements:
*
* - \`index\` must be strictly less than {length}.
*/
function at(${name} storage map, uint256 index) internal view returns (${keyType}, ${valueType}) {
(bytes32 key, bytes32 value) = at(map._inner, index);
return (${fromBytes32(keyType, 'key')}, ${fromBytes32(valueType, 'value')});
}
/**
* @dev Tries to returns the value associated with \`key\`. O(1).
* Does not revert if \`key\` is not in the map.
*/
function tryGet(${name} storage map, ${keyType} key) internal view returns (bool, ${valueType}) {
(bool success, bytes32 value) = tryGet(map._inner, ${toBytes32(keyType, 'key')});
return (success, ${fromBytes32(valueType, 'value')});
}
/**
* @dev Returns the value associated with \`key\`. O(1).
*
* Requirements:
*
* - \`key\` must be in the map.
*/
function get(${name} storage map, ${keyType} key) internal view returns (${valueType}) {
return ${fromBytes32(valueType, `get(map._inner, ${toBytes32(keyType, 'key')})`)};
}
/**
* @dev Same as {get}, with a custom error message when \`key\` is not in the map.
*
* CAUTION: This function is deprecated because it requires allocating memory for the error
* message unnecessarily. For custom revert reasons use {tryGet}.
*/
function get(
${name} storage map,
${keyType} key,
string memory errorMessage
) internal view returns (${valueType}) {
return ${fromBytes32(valueType, `get(map._inner, ${toBytes32(keyType, 'key')}, errorMessage)`)};
}
/**
* @dev Return the an array containing all the keys
*
* WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
* to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
* this function has an unbounded cost, and using it as part of a state-changing function may render the function
* uncallable if the map grows to a point where copying to memory consumes too much gas to fit in a block.
*/
function keys(${name} storage map) internal view returns (${keyType}[] memory) {
bytes32[] memory store = keys(map._inner);
${keyType}[] memory result;
/// @solidity memory-safe-assembly
assembly {
result := store
}
return result;
}
`;
// GENERATE
module.exports = format(
header.trimEnd(),
'library EnumerableMap {',
[
'using EnumerableSet for EnumerableSet.Bytes32Set;',
'',
defaultMap(),
TYPES.map(details => customMap(details).trimEnd()).join('\n\n'),
],
'}',
);
@@ -0,0 +1,250 @@
const format = require('../format-lines');
const { fromBytes32, toBytes32 } = require('./conversion');
const TYPES = [
{ name: 'Bytes32Set', type: 'bytes32' },
{ name: 'AddressSet', type: 'address' },
{ name: 'UintSet', type: 'uint256' },
];
/* eslint-disable max-len */
const header = `\
pragma solidity ^0.8.0;
/**
* @dev Library for managing
* https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive
* types.
*
* Sets have the following properties:
*
* - Elements are added, removed, and checked for existence in constant time
* (O(1)).
* - Elements are enumerated in O(n). No guarantees are made on the ordering.
*
* \`\`\`solidity
* contract Example {
* // Add the library methods
* using EnumerableSet for EnumerableSet.AddressSet;
*
* // Declare a set state variable
* EnumerableSet.AddressSet private mySet;
* }
* \`\`\`
*
* As of v3.3.0, sets of type \`bytes32\` (\`Bytes32Set\`), \`address\` (\`AddressSet\`)
* and \`uint256\` (\`UintSet\`) are supported.
*
* [WARNING]
* ====
* Trying to delete such a structure from storage will likely result in data corruption, rendering the structure
* unusable.
* See https://github.com/ethereum/solidity/pull/11843[ethereum/solidity#11843] for more info.
*
* In order to clean an EnumerableSet, you can either remove all elements one by one or create a fresh instance using an
* array of EnumerableSet.
* ====
*/
`;
/* eslint-enable max-len */
const defaultSet = () => `\
// To implement this library for multiple types with as little code
// repetition as possible, we write it in terms of a generic Set type with
// bytes32 values.
// The Set implementation uses private functions, and user-facing
// implementations (such as AddressSet) are just wrappers around the
// underlying Set.
// This means that we can only create new EnumerableSets for types that fit
// in bytes32.
struct Set {
// Storage of set values
bytes32[] _values;
// Position of the value in the \`values\` array, plus 1 because index 0
// means a value is not in the set.
mapping(bytes32 => uint256) _indexes;
}
/**
* @dev Add a value to a set. O(1).
*
* Returns true if the value was added to the set, that is if it was not
* already present.
*/
function _add(Set storage set, bytes32 value) private returns (bool) {
if (!_contains(set, value)) {
set._values.push(value);
// The value is stored at length-1, but we add 1 to all indexes
// and use 0 as a sentinel value
set._indexes[value] = set._values.length;
return true;
} else {
return false;
}
}
/**
* @dev Removes a value from a set. O(1).
*
* Returns true if the value was removed from the set, that is if it was
* present.
*/
function _remove(Set storage set, bytes32 value) private returns (bool) {
// We read and store the value's index to prevent multiple reads from the same storage slot
uint256 valueIndex = set._indexes[value];
if (valueIndex != 0) {
// Equivalent to contains(set, value)
// To delete an element from the _values array in O(1), we swap the element to delete with the last one in
// the array, and then remove the last element (sometimes called as 'swap and pop').
// This modifies the order of the array, as noted in {at}.
uint256 toDeleteIndex = valueIndex - 1;
uint256 lastIndex = set._values.length - 1;
if (lastIndex != toDeleteIndex) {
bytes32 lastValue = set._values[lastIndex];
// Move the last value to the index where the value to delete is
set._values[toDeleteIndex] = lastValue;
// Update the index for the moved value
set._indexes[lastValue] = valueIndex; // Replace lastValue's index to valueIndex
}
// Delete the slot where the moved value was stored
set._values.pop();
// Delete the index for the deleted slot
delete set._indexes[value];
return true;
} else {
return false;
}
}
/**
* @dev Returns true if the value is in the set. O(1).
*/
function _contains(Set storage set, bytes32 value) private view returns (bool) {
return set._indexes[value] != 0;
}
/**
* @dev Returns the number of values on the set. O(1).
*/
function _length(Set storage set) private view returns (uint256) {
return set._values.length;
}
/**
* @dev Returns the value stored at position \`index\` in the set. O(1).
*
* Note that there are no guarantees on the ordering of values inside the
* array, and it may change when more values are added or removed.
*
* Requirements:
*
* - \`index\` must be strictly less than {length}.
*/
function _at(Set storage set, uint256 index) private view returns (bytes32) {
return set._values[index];
}
/**
* @dev Return the entire set in an array
*
* WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
* to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
* this function has an unbounded cost, and using it as part of a state-changing function may render the function
* uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
*/
function _values(Set storage set) private view returns (bytes32[] memory) {
return set._values;
}
`;
const customSet = ({ name, type }) => `\
// ${name}
struct ${name} {
Set _inner;
}
/**
* @dev Add a value to a set. O(1).
*
* Returns true if the value was added to the set, that is if it was not
* already present.
*/
function add(${name} storage set, ${type} value) internal returns (bool) {
return _add(set._inner, ${toBytes32(type, 'value')});
}
/**
* @dev Removes a value from a set. O(1).
*
* Returns true if the value was removed from the set, that is if it was
* present.
*/
function remove(${name} storage set, ${type} value) internal returns (bool) {
return _remove(set._inner, ${toBytes32(type, 'value')});
}
/**
* @dev Returns true if the value is in the set. O(1).
*/
function contains(${name} storage set, ${type} value) internal view returns (bool) {
return _contains(set._inner, ${toBytes32(type, 'value')});
}
/**
* @dev Returns the number of values in the set. O(1).
*/
function length(${name} storage set) internal view returns (uint256) {
return _length(set._inner);
}
/**
* @dev Returns the value stored at position \`index\` in the set. O(1).
*
* Note that there are no guarantees on the ordering of values inside the
* array, and it may change when more values are added or removed.
*
* Requirements:
*
* - \`index\` must be strictly less than {length}.
*/
function at(${name} storage set, uint256 index) internal view returns (${type}) {
return ${fromBytes32(type, '_at(set._inner, index)')};
}
/**
* @dev Return the entire set in an array
*
* WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
* to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
* this function has an unbounded cost, and using it as part of a state-changing function may render the function
* uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
*/
function values(${name} storage set) internal view returns (${type}[] memory) {
bytes32[] memory store = _values(set._inner);
${type}[] memory result;
/// @solidity memory-safe-assembly
assembly {
result := store
}
return result;
}
`;
// GENERATE
module.exports = format(
header.trimEnd(),
'library EnumerableSet {',
[defaultSet(), TYPES.map(details => customSet(details).trimEnd()).join('\n\n')],
'}',
);
@@ -0,0 +1,163 @@
const assert = require('assert');
const format = require('../format-lines');
const { range } = require('../../helpers');
const LENGTHS = range(8, 256, 8).reverse(); // 248 → 8 (in steps of 8)
// Returns the version of OpenZeppelin Contracts in which a particular function was introduced.
// This is used in the docs for each function.
const version = (selector, length) => {
switch (selector) {
case 'toUint(uint)': {
switch (length) {
case 8:
case 16:
case 32:
case 64:
case 128:
return '2.5';
case 96:
case 224:
return '4.2';
default:
assert(LENGTHS.includes(length));
return '4.7';
}
}
case 'toInt(int)': {
switch (length) {
case 8:
case 16:
case 32:
case 64:
case 128:
return '3.1';
default:
assert(LENGTHS.includes(length));
return '4.7';
}
}
case 'toUint(int)': {
switch (length) {
case 256:
return '3.0';
default:
assert(false);
return;
}
}
case 'toInt(uint)': {
switch (length) {
case 256:
return '3.0';
default:
assert(false);
return;
}
}
default:
assert(false);
}
};
const header = `\
pragma solidity ^0.8.0;
/**
* @dev Wrappers over Solidity's uintXX/intXX casting operators with added overflow
* checks.
*
* Downcasting from uint256/int256 in Solidity does not revert on overflow. This can
* easily result in undesired exploitation or bugs, since developers usually
* assume that overflows raise errors. \`SafeCast\` restores this intuition by
* reverting the transaction when such an operation overflows.
*
* Using this library instead of the unchecked operations eliminates an entire
* class of bugs, so it's recommended to use it always.
*
* Can be combined with {SafeMath} and {SignedSafeMath} to extend it to smaller types, by performing
* all math on \`uint256\` and \`int256\` and then downcasting.
*/
`;
const toUintDownCast = length => `\
/**
* @dev Returns the downcasted uint${length} from uint256, reverting on
* overflow (when the input is greater than largest uint${length}).
*
* Counterpart to Solidity's \`uint${length}\` operator.
*
* Requirements:
*
* - input must fit into ${length} bits
*
* _Available since v${version('toUint(uint)', length)}._
*/
function toUint${length}(uint256 value) internal pure returns (uint${length}) {
require(value <= type(uint${length}).max, "SafeCast: value doesn't fit in ${length} bits");
return uint${length}(value);
}
`;
/* eslint-disable max-len */
const toIntDownCast = length => `\
/**
* @dev Returns the downcasted int${length} from int256, reverting on
* overflow (when the input is less than smallest int${length} or
* greater than largest int${length}).
*
* Counterpart to Solidity's \`int${length}\` operator.
*
* Requirements:
*
* - input must fit into ${length} bits
*
* _Available since v${version('toInt(int)', length)}._
*/
function toInt${length}(int256 value) internal pure returns (int${length} downcasted) {
downcasted = int${length}(value);
require(downcasted == value, "SafeCast: value doesn't fit in ${length} bits");
}
`;
/* eslint-enable max-len */
const toInt = length => `\
/**
* @dev Converts an unsigned uint${length} into a signed int${length}.
*
* Requirements:
*
* - input must be less than or equal to maxInt${length}.
*
* _Available since v${version('toInt(uint)', length)}._
*/
function toInt${length}(uint${length} value) internal pure returns (int${length}) {
// Note: Unsafe cast below is okay because \`type(int${length}).max\` is guaranteed to be positive
require(value <= uint${length}(type(int${length}).max), "SafeCast: value doesn't fit in an int${length}");
return int${length}(value);
}
`;
const toUint = length => `\
/**
* @dev Converts a signed int${length} into an unsigned uint${length}.
*
* Requirements:
*
* - input must be greater than or equal to 0.
*
* _Available since v${version('toUint(int)', length)}._
*/
function toUint${length}(int${length} value) internal pure returns (uint${length}) {
require(value >= 0, "SafeCast: value must be positive");
return uint${length}(value);
}
`;
// GENERATE
module.exports = format(
header.trimEnd(),
'library SafeCast {',
[...LENGTHS.map(toUintDownCast), toUint(256), ...LENGTHS.map(toIntDownCast), toInt(256)],
'}',
);
@@ -0,0 +1,87 @@
const format = require('../format-lines');
const { capitalize, unique } = require('../../helpers');
const TYPES = [
{ type: 'address', isValueType: true, version: '4.1' },
{ type: 'bool', isValueType: true, name: 'Boolean', version: '4.1' },
{ type: 'bytes32', isValueType: true, version: '4.1' },
{ type: 'uint256', isValueType: true, version: '4.1' },
{ type: 'string', isValueType: false, version: '4.9' },
{ type: 'bytes', isValueType: false, version: '4.9' },
].map(type => Object.assign(type, { struct: (type.name ?? capitalize(type.type)) + 'Slot' }));
const VERSIONS = unique(TYPES.map(t => t.version)).map(
version =>
`_Available since v${version} for ${TYPES.filter(t => t.version == version)
.map(t => `\`${t.type}\``)
.join(', ')}._`,
);
const header = `\
pragma solidity ^0.8.0;
/**
* @dev Library for reading and writing primitive types to specific storage slots.
*
* Storage slots are often used to avoid storage conflict when dealing with upgradeable contracts.
* This library helps with reading and writing to such slots without the need for inline assembly.
*
* The functions in this library return Slot structs that contain a \`value\` member that can be used to read or write.
*
* Example usage to set ERC1967 implementation slot:
* \`\`\`solidity
* contract ERC1967 {
* bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
*
* function _getImplementation() internal view returns (address) {
* return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value;
* }
*
* function _setImplementation(address newImplementation) internal {
* require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract");
* StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation;
* }
* }
* \`\`\`
*
${VERSIONS.map(s => ` * ${s}`).join('\n')}
*/
`;
const struct = type => `\
struct ${type.struct} {
${type.type} value;
}
`;
const get = type => `\
/**
* @dev Returns an \`${type.struct}\` with member \`value\` located at \`slot\`.
*/
function get${type.struct}(bytes32 slot) internal pure returns (${type.struct} storage r) {
/// @solidity memory-safe-assembly
assembly {
r.slot := slot
}
}
`;
const getStorage = type => `\
/**
* @dev Returns an \`${type.struct}\` representation of the ${type.type} storage pointer \`store\`.
*/
function get${type.struct}(${type.type} storage store) internal pure returns (${type.struct} storage r) {
/// @solidity memory-safe-assembly
assembly {
r.slot := store.slot
}
}
`;
// GENERATE
module.exports = format(
header.trimEnd(),
'library StorageSlot {',
[...TYPES.map(struct), ...TYPES.flatMap(type => [get(type), type.isValueType ? '' : getStorage(type)])],
'}',
);
@@ -0,0 +1,30 @@
function toBytes32(type, value) {
switch (type) {
case 'bytes32':
return value;
case 'uint256':
return `bytes32(${value})`;
case 'address':
return `bytes32(uint256(uint160(${value})))`;
default:
throw new Error(`Conversion from ${type} to bytes32 not supported`);
}
}
function fromBytes32(type, value) {
switch (type) {
case 'bytes32':
return value;
case 'uint256':
return `uint256(${value})`;
case 'address':
return `address(uint160(uint256(${value})))`;
default:
throw new Error(`Conversion from bytes32 to ${type} not supported`);
}
}
module.exports = {
toBytes32,
fromBytes32,
};
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail -x
git config user.name 'github-actions'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
@@ -0,0 +1,37 @@
function chunk(array, size = 1) {
return Array.range(Math.ceil(array.length / size)).map(i => array.slice(i * size, i * size + size));
}
function range(start, stop = undefined, step = 1) {
if (!stop) {
stop = start;
start = 0;
}
return start < stop
? Array(Math.ceil((stop - start) / step))
.fill()
.map((_, i) => start + i * step)
: [];
}
function unique(array, op = x => x) {
return array.filter((obj, i) => array.findIndex(entry => op(obj) === op(entry)) === i);
}
function zip(...args) {
return Array(Math.max(...args.map(arg => arg.length)))
.fill(null)
.map((_, i) => args.map(arg => arg[i]));
}
function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
module.exports = {
chunk,
range,
unique,
zip,
capitalize,
};
@@ -0,0 +1,180 @@
#!/usr/bin/env node
const { promises: fs } = require('fs');
const path = require('path');
const pathUpdates = {
// 'access/AccessControl.sol': undefined,
// 'access/Ownable.sol': undefined,
'access/TimelockController.sol': 'governance/TimelockController.sol',
'cryptography/ECDSA.sol': 'utils/cryptography/ECDSA.sol',
'cryptography/MerkleProof.sol': 'utils/cryptography/MerkleProof.sol',
'drafts/EIP712.sol': 'utils/cryptography/EIP712.sol',
'drafts/ERC20Permit.sol': 'token/ERC20/extensions/ERC20Permit.sol',
'drafts/IERC20Permit.sol': 'token/ERC20/extensions/IERC20Permit.sol',
'GSN/Context.sol': 'utils/Context.sol',
// 'GSN/GSNRecipientERC20Fee.sol': undefined,
// 'GSN/GSNRecipientSignature.sol': undefined,
// 'GSN/GSNRecipient.sol': undefined,
// 'GSN/IRelayHub.sol': undefined,
// 'GSN/IRelayRecipient.sol': undefined,
'introspection/ERC165Checker.sol': 'utils/introspection/ERC165Checker.sol',
'introspection/ERC165.sol': 'utils/introspection/ERC165.sol',
'introspection/ERC1820Implementer.sol': 'utils/introspection/ERC1820Implementer.sol',
'introspection/IERC165.sol': 'utils/introspection/IERC165.sol',
'introspection/IERC1820Implementer.sol': 'utils/introspection/IERC1820Implementer.sol',
'introspection/IERC1820Registry.sol': 'utils/introspection/IERC1820Registry.sol',
'math/Math.sol': 'utils/math/Math.sol',
'math/SafeMath.sol': 'utils/math/SafeMath.sol',
'math/SignedSafeMath.sol': 'utils/math/SignedSafeMath.sol',
'payment/escrow/ConditionalEscrow.sol': 'utils/escrow/ConditionalEscrow.sol',
'payment/escrow/Escrow.sol': 'utils/escrow/Escrow.sol',
'payment/escrow/RefundEscrow.sol': 'utils/escrow/RefundEscrow.sol',
'payment/PaymentSplitter.sol': 'finance/PaymentSplitter.sol',
'utils/PaymentSplitter.sol': 'finance/PaymentSplitter.sol',
'payment/PullPayment.sol': 'security/PullPayment.sol',
'presets/ERC1155PresetMinterPauser.sol': 'token/ERC1155/presets/ERC1155PresetMinterPauser.sol',
'presets/ERC20PresetFixedSupply.sol': 'token/ERC20/presets/ERC20PresetFixedSupply.sol',
'presets/ERC20PresetMinterPauser.sol': 'token/ERC20/presets/ERC20PresetMinterPauser.sol',
'presets/ERC721PresetMinterPauserAutoId.sol': 'token/ERC721/presets/ERC721PresetMinterPauserAutoId.sol',
'presets/ERC777PresetFixedSupply.sol': 'token/ERC777/presets/ERC777PresetFixedSupply.sol',
'proxy/BeaconProxy.sol': 'proxy/beacon/BeaconProxy.sol',
// 'proxy/Clones.sol': undefined,
'proxy/IBeacon.sol': 'proxy/beacon/IBeacon.sol',
'proxy/Initializable.sol': 'proxy/utils/Initializable.sol',
'utils/Initializable.sol': 'proxy/utils/Initializable.sol',
'proxy/ProxyAdmin.sol': 'proxy/transparent/ProxyAdmin.sol',
// 'proxy/Proxy.sol': undefined,
'proxy/TransparentUpgradeableProxy.sol': 'proxy/transparent/TransparentUpgradeableProxy.sol',
'proxy/UpgradeableBeacon.sol': 'proxy/beacon/UpgradeableBeacon.sol',
'proxy/UpgradeableProxy.sol': 'proxy/ERC1967/ERC1967Proxy.sol',
'token/ERC1155/ERC1155Burnable.sol': 'token/ERC1155/extensions/ERC1155Burnable.sol',
'token/ERC1155/ERC1155Holder.sol': 'token/ERC1155/utils/ERC1155Holder.sol',
'token/ERC1155/ERC1155Pausable.sol': 'token/ERC1155/extensions/ERC1155Pausable.sol',
'token/ERC1155/ERC1155Receiver.sol': 'token/ERC1155/utils/ERC1155Receiver.sol',
// 'token/ERC1155/ERC1155.sol': undefined,
'token/ERC1155/IERC1155MetadataURI.sol': 'token/ERC1155/extensions/IERC1155MetadataURI.sol',
// 'token/ERC1155/IERC1155Receiver.sol': undefined,
// 'token/ERC1155/IERC1155.sol': undefined,
'token/ERC20/ERC20Burnable.sol': 'token/ERC20/extensions/ERC20Burnable.sol',
'token/ERC20/ERC20Capped.sol': 'token/ERC20/extensions/ERC20Capped.sol',
'token/ERC20/ERC20Pausable.sol': 'token/ERC20/extensions/ERC20Pausable.sol',
'token/ERC20/ERC20Snapshot.sol': 'token/ERC20/extensions/ERC20Snapshot.sol',
// 'token/ERC20/ERC20.sol': undefined,
// 'token/ERC20/IERC20.sol': undefined,
'token/ERC20/SafeERC20.sol': 'token/ERC20/utils/SafeERC20.sol',
'token/ERC20/TokenTimelock.sol': 'token/ERC20/utils/TokenTimelock.sol',
'token/ERC721/ERC721Burnable.sol': 'token/ERC721/extensions/ERC721Burnable.sol',
'token/ERC721/ERC721Holder.sol': 'token/ERC721/utils/ERC721Holder.sol',
'token/ERC721/ERC721Pausable.sol': 'token/ERC721/extensions/ERC721Pausable.sol',
// 'token/ERC721/ERC721.sol': undefined,
'token/ERC721/IERC721Enumerable.sol': 'token/ERC721/extensions/IERC721Enumerable.sol',
'token/ERC721/IERC721Metadata.sol': 'token/ERC721/extensions/IERC721Metadata.sol',
// 'token/ERC721/IERC721Receiver.sol': undefined,
// 'token/ERC721/IERC721.sol': undefined,
// 'token/ERC777/ERC777.sol': undefined,
// 'token/ERC777/IERC777Recipient.sol': undefined,
// 'token/ERC777/IERC777Sender.sol': undefined,
// 'token/ERC777/IERC777.sol': undefined,
// 'utils/Address.sol': undefined,
// 'utils/Arrays.sol': undefined,
// 'utils/Context.sol': undefined,
// 'utils/Counters.sol': undefined,
// 'utils/Create2.sol': undefined,
'utils/EnumerableMap.sol': 'utils/structs/EnumerableMap.sol',
'utils/EnumerableSet.sol': 'utils/structs/EnumerableSet.sol',
'utils/Pausable.sol': 'security/Pausable.sol',
'utils/ReentrancyGuard.sol': 'security/ReentrancyGuard.sol',
'utils/SafeCast.sol': 'utils/math/SafeCast.sol',
// 'utils/Strings.sol': undefined,
'utils/cryptography/draft-EIP712.sol': 'utils/cryptography/EIP712.sol',
'token/ERC20/extensions/draft-ERC20Permit.sol': 'token/ERC20/extensions/ERC20Permit.sol',
'token/ERC20/extensions/draft-IERC20Permit.sol': 'token/ERC20/extensions/IERC20Permit.sol',
};
async function main(paths = ['contracts']) {
const files = await listFilesRecursively(paths, /\.sol$/);
const updatedFiles = [];
for (const file of files) {
if (await updateFile(file, updateImportPaths)) {
updatedFiles.push(file);
}
}
if (updatedFiles.length > 0) {
console.log(`${updatedFiles.length} file(s) were updated`);
for (const c of updatedFiles) {
console.log('-', c);
}
} else {
console.log('No files were updated');
}
}
async function listFilesRecursively(paths, filter) {
const queue = paths;
const files = [];
while (queue.length > 0) {
const top = queue.shift();
const stat = await fs.stat(top);
if (stat.isFile()) {
if (top.match(filter)) {
files.push(top);
}
} else if (stat.isDirectory()) {
for (const name of await fs.readdir(top)) {
queue.push(path.join(top, name));
}
}
}
return files;
}
async function updateFile(file, update) {
const content = await fs.readFile(file, 'utf8');
const updatedContent = update(content);
if (updatedContent !== content) {
await fs.writeFile(file, updatedContent);
return true;
} else {
return false;
}
}
function updateImportPaths(source) {
for (const [oldPath, newPath] of Object.entries(pathUpdates)) {
source = source.replace(
path.join('@openzeppelin/contracts', oldPath),
path.join('@openzeppelin/contracts', newPath),
);
source = source.replace(
path.join('@openzeppelin/contracts-upgradeable', getUpgradeablePath(oldPath)),
path.join('@openzeppelin/contracts-upgradeable', getUpgradeablePath(newPath)),
);
}
return source;
}
function getUpgradeablePath(file) {
const { dir, name, ext } = path.parse(file);
const upgradeableName = name + 'Upgradeable';
return path.format({ dir, ext, name: upgradeableName });
}
module.exports = {
pathUpdates,
updateImportPaths,
getUpgradeablePath,
};
if (require.main === module) {
const args = process.argv.length > 2 ? process.argv.slice(2) : undefined;
main(args).catch(e => {
console.error(e);
process.exit(1);
});
}
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -euo pipefail
shopt -s globstar
# cross platform `mkdir -p`
node -e 'fs.mkdirSync("build/contracts", { recursive: true })'
cp artifacts/contracts/**/*.json build/contracts
rm build/contracts/*.dbg.json
node scripts/remove-ignored-artifacts.js
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
# cd to the root of the repo
cd "$(git rev-parse --show-toplevel)"
# avoids re-compilation during publishing of both packages
if [[ ! -v ALREADY_COMPILED ]]; then
npm run clean
npm run prepare
npm run prepack
fi
cp README.md contracts/
mkdir contracts/build contracts/build/contracts
cp -r build/contracts/*.json contracts/build/contracts
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -euo pipefail
OUTDIR="$(node -p 'require("./docs/config.js").outputDir')"
if [ ! -d node_modules ]; then
npm ci
fi
rm -rf "$OUTDIR"
hardhat docgen
# copy examples and adjust imports
examples_dir="docs/modules/api/examples"
mkdir -p "$examples_dir"
for f in contracts/mocks/docs/*.sol; do
name="$(basename "$f")"
sed -e '/^import/s|\.\./\.\./|@openzeppelin/contracts/|' "$f" > "docs/modules/api/examples/$name"
done
node scripts/gen-nav.js "$OUTDIR" > "$OUTDIR/../nav.adoc"
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -euo pipefail
if [ "${SKIP_COMPILE:-}" == true ]; then
exit
fi
npm run clean
env COMPILE_MODE=production npm run compile
@@ -0,0 +1,33 @@
#!/usr/bin/env node
// Adjusts the format of the changelog that changesets generates.
// This is run automatically when npm version is run.
const fs = require('fs');
const changelog = fs.readFileSync('CHANGELOG.md', 'utf8');
// Groups:
// - 1: Pull Request Number and URL
// - 2: Changeset entry
const RELEASE_LINE_REGEX = /^- (\[#.*?\]\(.*?\))?.*?! - (.*)$/gm;
// Captures vX.Y.Z or vX.Y.Z-rc.W
const VERSION_TITLE_REGEX = /^## (\d+\.\d+\.\d+(-rc\.\d+)?)$/gm;
const isPrerelease = process.env.PRERELEASE === 'true';
const formatted = changelog
// Remove titles
.replace(/^### Major Changes\n\n/gm, '')
.replace(/^### Minor Changes\n\n/gm, '')
.replace(/^### Patch Changes\n\n/gm, '')
// Remove extra whitespace between items
.replace(/^(- \[.*\n)\n(?=-)/gm, '$1')
// Format each release line
.replace(RELEASE_LINE_REGEX, (_, pr, entry) => (pr ? `- ${entry} (${pr})` : `- ${entry}`))
// Add date to new version
.replace(VERSION_TITLE_REGEX, `\n## $1 (${new Date().toISOString().split('T')[0]})`)
// Conditionally allow vX.Y.Z.rc-.W sections only in prerelease
.replace(/^## \d\.\d\.\d-rc\S+[^]+?(?=^#)/gm, section => (isPrerelease ? section : ''));
fs.writeFileSync('CHANGELOG.md', formatted);
@@ -0,0 +1,15 @@
#!/usr/bin/env node
// Synchronizes the version in contracts/package.json with the one in package.json.
// This is run automatically when npm version is run.
const fs = require('fs');
setVersion('package.json', 'contracts/package.json');
function setVersion(from, to) {
const fromJson = JSON.parse(fs.readFileSync(from));
const toJson = JSON.parse(fs.readFileSync(to));
toJson.version = fromJson.version;
fs.writeFileSync(to, JSON.stringify(toJson, null, 2) + '\n');
}
@@ -0,0 +1,34 @@
#!/usr/bin/env node
const fs = require('fs');
const proc = require('child_process');
const semver = require('semver');
const run = (cmd, ...args) => proc.execFileSync(cmd, args, { encoding: 'utf8' }).trim();
const gitStatus = run('git', 'status', '--porcelain', '-uno', 'contracts/**/*.sol');
if (gitStatus.length > 0) {
console.error('Contracts directory is not clean');
process.exit(1);
}
const { version } = require('../../package.json');
// Get latest tag according to semver.
const [tag] = run('git', 'tag')
.split(/\r?\n/)
.filter(semver.coerce) // check version can be processed
.filter(v => semver.satisfies(v, `< ${version}`)) // ignores prereleases unless currently a prerelease
.sort(semver.rcompare);
// Ordering tag → HEAD is important here.
const files = run('git', 'diff', tag, 'HEAD', '--name-only', 'contracts/**/*.sol')
.split(/\r?\n/)
.filter(file => file && !file.match(/mock/i) && fs.existsSync(file));
for (const file of files) {
const current = fs.readFileSync(file, 'utf8');
const updated = current.replace(
/(\/\/ SPDX-License-Identifier:.*)$(\n\/\/ OpenZeppelin Contracts .*$)?/m,
`$1\n// OpenZeppelin Contracts (last updated v${version}) (${file.replace('contracts/', '')})`,
);
fs.writeFileSync(file, updated);
}
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
changeset version
scripts/release/format-changelog.js
scripts/release/synchronize-versions.js
scripts/release/update-comment.js
oz-docs update-version
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
npx changeset pre exit rc
git add .
git commit -m "Exit release candidate"
git push origin
@@ -0,0 +1,47 @@
const { readFileSync } = require('fs');
const { join } = require('path');
const { version } = require(join(__dirname, '../../../package.json'));
module.exports = async ({ github, context }) => {
const changelog = readFileSync('CHANGELOG.md', 'utf8');
await github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: `v${version}`,
body: extractSection(changelog, version),
prerelease: process.env.PRERELEASE === 'true',
});
};
// From https://github.com/frangio/extract-changelog/blob/master/src/utils/word-regexp.ts
function makeWordRegExp(word) {
const start = word.length > 0 && /\b/.test(word[0]) ? '\\b' : '';
const end = word.length > 0 && /\b/.test(word[word.length - 1]) ? '\\b' : '';
return new RegExp(start + [...word].map(c => (/[a-z0-9]/i.test(c) ? c : '\\' + c)).join('') + end);
}
// From https://github.com/frangio/extract-changelog/blob/master/src/core.ts
function extractSection(document, wantedHeading) {
// ATX Headings as defined in GitHub Flavored Markdown (https://github.github.com/gfm/#atx-headings)
const heading = /^ {0,3}(?<lead>#{1,6})(?: [ \t\v\f]*(?<text>.*?)[ \t\v\f]*)?(?:[\n\r]+|$)/gm;
const wantedHeadingRe = makeWordRegExp(wantedHeading);
let start, end;
for (const m of document.matchAll(heading)) {
if (!start) {
if (m.groups.text.search(wantedHeadingRe) === 0) {
start = m;
}
} else if (m.groups.lead.length <= start.groups.lead.length) {
end = m;
break;
}
}
if (start) {
return document.slice(start.index + start[0].length, end?.index);
}
}
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail
CHECKSUMS="$RUNNER_TEMP/checksums.txt"
# Extract tarball content into a tmp directory
tar xf "$TARBALL" -C "$RUNNER_TEMP"
# Move to extracted directory
cd "$RUNNER_TEMP/package"
# Checksum all Solidity files
find . -type f -name "*.sol" | xargs shasum > "$CHECKSUMS"
# Back to directory with git contents
cd "$GITHUB_WORKSPACE/contracts"
# Check against tarball contents
shasum -c "$CHECKSUMS"
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail
dist_tag() {
PACKAGE_JSON_NAME="$(jq -r .name ./package.json)"
LATEST_NPM_VERSION="$(npm info "$PACKAGE_JSON_NAME" version)"
PACKAGE_JSON_VERSION="$(jq -r .version ./package.json)"
if [ "$PRERELEASE" = "true" ]; then
echo "next"
elif npx semver -r ">$LATEST_NPM_VERSION" "$PACKAGE_JSON_VERSION" > /dev/null; then
echo "latest"
else
# This is a patch for an older version
# npm can't publish without a tag
echo "tmp"
fi
}
cd contracts
TARBALL="$(npm pack | tee /dev/stderr | tail -1)"
echo "tarball_name=$TARBALL" >> $GITHUB_OUTPUT
echo "tarball=$(pwd)/$TARBALL" >> $GITHUB_OUTPUT
echo "tag=$(dist_tag)" >> $GITHUB_OUTPUT
cd ..
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
set -euo pipefail
# Define merge branch name
MERGE_BRANCH=merge/$GITHUB_REF_NAME
# Create the branch and force to start from ref
git checkout -B "$MERGE_BRANCH" "$GITHUB_REF_NAME"
# Get deleted changesets in this branch that might conflict with master
readarray -t DELETED_CHANGESETS < <(git diff origin/master --name-only -- '.changeset/*.md')
# Merge master, which will take those files cherry-picked. Auto-resolve conflicts favoring master.
git merge origin/master -m "Merge master to $GITHUB_REF_NAME" -X theirs
# Remove the originally deleted changesets to correctly sync with master
rm -f "${DELETED_CHANGESETS[@]}"
git add .changeset/
# Allow empty here since there may be no changes if `rm -f` failed for all changesets
git commit --allow-empty -m "Sync changesets with master"
git push -f origin "$MERGE_BRANCH"
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail
# Intentionally escape $ to avoid interpolation and writing the token to disk
echo "//registry.npmjs.org/:_authToken=\${NPM_TOKEN}" > .npmrc
# Actual publish
npm publish "$TARBALL" --tag "$TAG"
delete_tag() {
PACKAGE_JSON_NAME="$(tar xfO "$TARBALL" package/package.json | jq -r .name)"
npm dist-tag rm "$PACKAGE_JSON_NAME" "$1"
}
if [ "$TAG" = tmp ]; then
delete_tag "$TAG"
elif [ "$TAG" = latest ]; then
delete_tag next
fi
@@ -0,0 +1,7 @@
module.exports = ({ github, context }) =>
github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'release-cycle.yml',
ref: process.env.REF || process.env.GITHUB_REF_NAME,
});
@@ -0,0 +1,17 @@
const { coerce, inc, rsort } = require('semver');
const { join } = require('path');
const { version } = require(join(__dirname, '../../../package.json'));
module.exports = async ({ core }) => {
// Variables not in the context
const refName = process.env.GITHUB_REF_NAME;
// Compare package.json version's next patch vs. first version patch
// A recently opened branch will give the next patch for the previous minor
// So, we get the max against the patch 0 of the release branch's version
const branchPatch0 = coerce(refName.replace('release-v', '')).version;
const packageJsonNextPatch = inc(version, 'patch');
const [nextVersion] = rsort([branchPatch0, packageJsonNextPatch], false);
core.exportVariable('TITLE', `Release v${nextVersion}`);
};
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
set -euo pipefail
# Set changeset status location
# This is needed because `changeset status --output` only works with relative routes
CHANGESETS_STATUS_JSON="$(realpath --relative-to=. "$RUNNER_TEMP/status.json")"
# Save changeset status to temp file
npx changeset status --output="$CHANGESETS_STATUS_JSON"
# Defensive assertion. SHOULD NOT BE REACHED
if [ "$(jq '.releases | length' "$CHANGESETS_STATUS_JSON")" != 1 ]; then
echo "::error file=$CHANGESETS_STATUS_JSON::The status doesn't contain only 1 release"
exit 1;
fi;
# Create branch
BRANCH_SUFFIX="$(jq -r '.releases[0].newVersion | gsub("\\.\\d+$"; "")' $CHANGESETS_STATUS_JSON)"
RELEASE_BRANCH="release-v$BRANCH_SUFFIX"
git checkout -b "$RELEASE_BRANCH"
# Output branch
echo "branch=$RELEASE_BRANCH" >> $GITHUB_OUTPUT
# Enter in prerelease state
npx changeset pre enter rc
git add .
git commit -m "Start release candidate"
# Push branch
if ! git push origin "$RELEASE_BRANCH"; then
echo "::error file=scripts/release/start.sh::Can't push $RELEASE_BRANCH. Did you forget to run this workflow from $RELEASE_BRANCH?"
exit 1
fi
@@ -0,0 +1,112 @@
const { readPreState } = require('@changesets/pre');
const { default: readChangesets } = require('@changesets/read');
const { join } = require('path');
const { fetch } = require('undici');
const { version, name: packageName } = require(join(__dirname, '../../../contracts/package.json'));
module.exports = async ({ github, context, core }) => {
const state = await getState({ github, context, core });
function setOutput(key, value) {
core.info(`State ${key} = ${value}`);
core.setOutput(key, value);
}
// Jobs to trigger
setOutput('start', shouldRunStart(state));
setOutput('promote', shouldRunPromote(state));
setOutput('changesets', shouldRunChangesets(state));
setOutput('publish', shouldRunPublish(state));
setOutput('merge', shouldRunMerge(state));
// Global Variables
setOutput('is_prerelease', state.prerelease);
};
function shouldRunStart({ isMaster, isWorkflowDispatch, botRun }) {
return isMaster && isWorkflowDispatch && !botRun;
}
function shouldRunPromote({ isReleaseBranch, isWorkflowDispatch, botRun }) {
return isReleaseBranch && isWorkflowDispatch && !botRun;
}
function shouldRunChangesets({ isReleaseBranch, isPush, isWorkflowDispatch, botRun }) {
return (isReleaseBranch && isPush) || (isReleaseBranch && isWorkflowDispatch && botRun);
}
function shouldRunPublish({ isReleaseBranch, isPush, hasPendingChangesets, isPublishedOnNpm }) {
return isReleaseBranch && isPush && !hasPendingChangesets && !isPublishedOnNpm;
}
function shouldRunMerge({
isReleaseBranch,
isPush,
prerelease,
isCurrentFinalVersion,
hasPendingChangesets,
prBackExists,
}) {
return isReleaseBranch && isPush && !prerelease && isCurrentFinalVersion && !hasPendingChangesets && prBackExists;
}
async function getState({ github, context, core }) {
// Variables not in the context
const refName = process.env.GITHUB_REF_NAME;
const botRun = process.env.TRIGGERING_ACTOR === 'github-actions[bot]';
const { changesets, preState } = await readChangesetState();
// Static vars
const state = {
refName,
hasPendingChangesets: changesets.length > 0,
prerelease: preState?.mode === 'pre',
isMaster: refName === 'master',
isReleaseBranch: refName.startsWith('release-v'),
isWorkflowDispatch: context.eventName === 'workflow_dispatch',
isPush: context.eventName === 'push',
isCurrentFinalVersion: !version.includes('-rc.'),
botRun,
};
// Async vars
const { data: prs } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
head: `${context.repo.owner}:merge/${state.refName}`,
base: 'master',
state: 'open',
});
state.prBackExists = prs.length === 0;
state.isPublishedOnNpm = await isPublishedOnNpm(packageName, version);
// Log every state value in debug mode
if (core.isDebug()) for (const [key, value] of Object.entries(state)) core.debug(`${key}: ${value}`);
return state;
}
// From https://github.com/changesets/action/blob/v1.4.1/src/readChangesetState.ts
async function readChangesetState(cwd = process.cwd()) {
const preState = await readPreState(cwd);
const isInPreMode = preState !== undefined && preState.mode === 'pre';
let changesets = await readChangesets(cwd);
if (isInPreMode) {
changesets = changesets.filter(x => !preState.changesets.includes(x.id));
}
return {
preState: isInPreMode ? preState : undefined,
changesets,
};
}
async function isPublishedOnNpm(package, version) {
const res = await fetch(`https://registry.npmjs.com/${package}/${version}`);
return res.ok;
}
@@ -0,0 +1,45 @@
#!/usr/bin/env node
// This script removes the build artifacts of ignored contracts.
const fs = require('fs');
const path = require('path');
const match = require('micromatch');
function readJSON(path) {
return JSON.parse(fs.readFileSync(path));
}
const pkgFiles = readJSON('package.json').files;
// Get only negated patterns.
const ignorePatterns = pkgFiles
.filter(pat => pat.startsWith('!'))
// Remove the negation part. Makes micromatch usage more intuitive.
.map(pat => pat.slice(1));
const ignorePatternsSubtrees = ignorePatterns
// Add **/* to ignore all files contained in the directories.
.concat(ignorePatterns.map(pat => path.join(pat, '**/*')))
.map(p => p.replace(/^\//, ''));
const artifactsDir = 'build/contracts';
const buildinfo = 'artifacts/build-info';
const filenames = fs.readdirSync(buildinfo);
let n = 0;
for (const filename of filenames) {
const solcOutput = readJSON(path.join(buildinfo, filename)).output;
for (const sourcePath in solcOutput.contracts) {
const ignore = match.any(sourcePath, ignorePatternsSubtrees);
if (ignore) {
for (const contract in solcOutput.contracts[sourcePath]) {
fs.unlinkSync(path.join(artifactsDir, contract + '.json'));
n += 1;
}
}
}
}
console.error(`Removed ${n} mock artifacts`);
@@ -0,0 +1,63 @@
const proc = require('child_process');
const read = cmd => proc.execSync(cmd, { encoding: 'utf8' }).trim();
const run = cmd => {
proc.execSync(cmd, { stdio: 'inherit' });
};
const tryRead = cmd => {
try {
return read(cmd);
} catch (e) {
return undefined;
}
};
const releaseBranchRegex = /^release-v(?<version>(?<major>\d+)\.(?<minor>\d+)(?:\.(?<patch>\d+))?)$/;
const currentBranch = read('git rev-parse --abbrev-ref HEAD');
const match = currentBranch.match(releaseBranchRegex);
if (!match) {
console.error('Not currently on a release branch');
process.exit(1);
}
if (/-.*$/.test(require('../package.json').version)) {
console.error('Refusing to update docs: prerelease detected');
process.exit(0);
}
const current = match.groups;
const docsBranch = `docs-v${current.major}.x`;
// Fetch remotes and find the docs branch if it exists
run('git fetch --all --no-tags');
const matchingDocsBranches = tryRead(`git rev-parse --glob='*/${docsBranch}'`);
if (!matchingDocsBranches) {
// Create the branch
run(`git checkout --orphan ${docsBranch}`);
} else {
const [publishedRef, ...others] = new Set(matchingDocsBranches.split('\n'));
if (others.length > 0) {
console.error(
`Found conflicting ${docsBranch} branches.\n` +
'Either local branch is outdated or there are multiple matching remote branches.',
);
process.exit(1);
}
const publishedVersion = JSON.parse(read(`git show ${publishedRef}:package.json`)).version;
const publishedMinor = publishedVersion.match(/\d+\.(?<minor>\d+)\.\d+/).groups.minor;
if (current.minor < publishedMinor) {
console.error('Refusing to update docs: newer version is published');
process.exit(0);
}
run('git checkout --quiet --detach');
run(`git reset --soft ${publishedRef}`);
run(`git checkout ${docsBranch}`);
}
run('npm run prepare-docs');
run('git add -f docs'); // --force needed because generated docs files are gitignored
run('git commit -m "Update docs"');
run(`git checkout ${currentBranch}`);
@@ -0,0 +1,21 @@
The upgradeable variant of OpenZeppelin Contracts is automatically generated from the original Solidity code. We call this process "transpilation" and it is implemented by our [Upgradeability Transpiler](https://github.com/OpenZeppelin/openzeppelin-transpiler/).
When the `master` branch or `release-v*` branches are updated, the code is transpiled and pushed to [OpenZeppelin/openzeppelin-contracts-upgradeable](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable) by the `upgradeable.yml` workflow.
## `transpile.sh`
Applies patches and invokes the transpiler with the command line flags we need for our requirements (for example, excluding certain files).
## `transpile-onto.sh`
```
bash scripts/upgradeable/transpile-onto.sh <target> [<base>]
```
Transpiles the contents of the current git branch and commits the result as a new commit on branch `<target>`. If branch `<target>` doesn't exist, it will copy the commit history of `[<base>]` (this is used in GitHub Actions, but is usually not necessary locally).
## `patch-apply.sh` & `patch-save.sh`
Some of the upgradeable contract variants require ad-hoc changes that are not implemented by the transpiler. These changes are implemented by patches stored in `upgradeable.patch` in this directory. `patch-apply.sh` applies these patches.
If the patches fail to apply due to changes in the repo, the conflicts have to be resolved manually. Once fixed, `patch-save.sh` will take the changes staged in Git and update `upgradeable.patch` to match.
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -euo pipefail
DIRNAME="$(dirname -- "${BASH_SOURCE[0]}")"
PATCH="$DIRNAME/upgradeable.patch"
error() {
echo Error: "$*" >&2
exit 1
}
if ! git diff-files --quiet ":!$PATCH" || ! git diff-index --quiet HEAD ":!$PATCH"; then
error "Repository must have no staged or unstaged changes"
fi
if ! git apply -3 "$PATCH"; then
error "Fix conflicts and run $DIRNAME/patch-save.sh"
fi
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -euo pipefail
DIRNAME="$(dirname -- "${BASH_SOURCE[0]}")"
PATCH="$DIRNAME/upgradeable.patch"
error() {
echo Error: "$*" >&2
exit 1
}
if ! git diff-files --quiet ":!$PATCH"; then
error "Unstaged changes. Stage to include in patch or temporarily stash."
fi
git diff-index --cached --patch --output="$PATCH" HEAD
git restore --staged --worktree ":!$PATCH"
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
set -euo pipefail
if [ $# -lt 1 ]; then
echo "usage: bash $0 <target> [<base>]" >&2
exit 1
fi
set -x
target="$1"
base="${2-}"
bash scripts/upgradeable/transpile.sh
commit="$(git rev-parse --short HEAD)"
branch="$(git rev-parse --abbrev-ref HEAD)"
git add contracts
# detach from the current branch to avoid making changes to it
git checkout --quiet --detach
# switch to the target branch, creating it if necessary
if git rev-parse -q --verify "$target"; then
# if the branch exists, make it the current HEAD without checking out its contents
git reset --soft "$target"
git checkout "$target"
else
# if the branch doesn't exist, create it as an orphan and check it out
git checkout --orphan "$target"
if [ -n "$base" ] && git rev-parse -q --verify "$base"; then
# if base was specified and it exists, set it as the branch history
git reset --soft "$base"
fi
fi
# commit if there are changes to commit
if ! git diff --quiet --cached; then
git commit -m "Transpile $commit"
fi
git checkout "$branch"
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
set -euo pipefail -x
DIRNAME="$(dirname -- "${BASH_SOURCE[0]}")"
bash "$DIRNAME/patch-apply.sh"
npm run clean
npm run compile
build_info=($(jq -r '.input.sources | keys | if any(test("^contracts/mocks/.*\\bunreachable\\b")) then empty else input_filename end' artifacts/build-info/*))
build_info_num=${#build_info[@]}
if [ $build_info_num -ne 1 ]; then
echo "found $build_info_num relevant build info files but expected just 1"
exit 1
fi
# -D: delete original and excluded files
# -b: use this build info file
# -i: use included Initializable
# -x: exclude proxy-related contracts with a few exceptions
# -p: emit public initializer
npx @openzeppelin/upgrade-safe-transpiler@latest -D \
-b "$build_info" \
-i contracts/proxy/utils/Initializable.sol \
-x 'contracts-exposed/**/*' \
-x 'contracts/proxy/**/*' \
-x '!contracts/proxy/Clones.sol' \
-x '!contracts/proxy/ERC1967/ERC1967Storage.sol' \
-x '!contracts/proxy/ERC1967/ERC1967Upgrade.sol' \
-x '!contracts/proxy/utils/UUPSUpgradeable.sol' \
-x '!contracts/proxy/beacon/IBeacon.sol' \
-p 'contracts/**/presets/**/*'
@@ -0,0 +1,481 @@
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
deleted file mode 100644
index 2797a088..00000000
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ /dev/null
@@ -1,21 +0,0 @@
----
-name: Bug report
-about: Report a bug in OpenZeppelin Contracts
-
----
-
-<!-- Briefly describe the issue you're experiencing. Tell us what you were trying to do and what happened instead. -->
-
-<!-- Remember, this is not a place to ask for help debugging code. For that, we welcome you in the OpenZeppelin Community Forum: https://forum.openzeppelin.com/. -->
-
-**💻 Environment**
-
-<!-- Tell us what version of OpenZeppelin Contracts you're using, and how you're using it: Truffle, Remix, etc. -->
-
-**📝 Details**
-
-<!-- Describe the problem you have been experiencing in more detail. Include as much information as you think is relevant. Keep in mind that transactions can fail for many reasons; context is key here. -->
-
-**🔢 Code to reproduce bug**
-
-<!-- We will be able to better help if you provide a minimal example that triggers the bug. -->
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 4018cef2..d343a53d 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,4 +1,8 @@
+blank_issues_enabled: false
contact_links:
+ - name: Bug Reports & Feature Requests
+ url: https://github.com/OpenZeppelin/openzeppelin-contracts/issues/new/choose
+ about: Visit the OpenZeppelin Contracts repository
- name: Questions & Support Requests
url: https://forum.openzeppelin.com/c/support/contracts/18
about: Ask in the OpenZeppelin Forum
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
deleted file mode 100644
index ff596b0c..00000000
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ /dev/null
@@ -1,14 +0,0 @@
----
-name: Feature request
-about: Suggest an idea for OpenZeppelin Contracts
-
----
-
-**🧐 Motivation**
-<!-- Is your feature request related to a specific problem? Is it just a crazy idea? Tell us about it! -->
-
-**📝 Details**
-<!-- Please describe your feature request in detail. -->
-
-<!-- Make sure that you have reviewed the OpenZeppelin Contracts Contributor Guidelines. -->
-<!-- https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/CONTRIBUTING.md -->
diff --git a/README.md b/README.md
index 9fc95518..53130e3c 100644
--- a/README.md
+++ b/README.md
@@ -16,17 +16,20 @@
:building_construction: **Want to scale your decentralized application?** Check out [OpenZeppelin Defender](https://openzeppelin.com/defender) — a secure platform for automating and monitoring your operations.
+> **Note**
+> You are looking at the upgradeable variant of OpenZeppelin Contracts. Be sure to review the documentation on [Using OpenZeppelin Contracts with Upgrades](https://docs.openzeppelin.com/contracts/4.x/upgradeable).
+
## Overview
### Installation
```
-$ npm install @openzeppelin/contracts
+$ npm install @openzeppelin/contracts-upgradeable
```
OpenZeppelin Contracts features a [stable API](https://docs.openzeppelin.com/contracts/releases-stability#api-stability), which means that your contracts won't break unexpectedly when upgrading to a newer minor version.
-An alternative to npm is to use the GitHub repository (`openzeppelin/openzeppelin-contracts`) to retrieve the contracts. When doing this, make sure to specify the tag for a release such as `v4.5.0`, instead of using the `master` branch.
+An alternative to npm is to use the GitHub repository (`openzeppelin/openzeppelin-contracts-upgradeable`) to retrieve the contracts. When doing this, make sure to specify the tag for a release such as `v4.5.0`, instead of using the `master` branch.
### Usage
@@ -35,10 +38,11 @@ Once installed, you can use the contracts in the library by importing them:
```solidity
pragma solidity ^0.8.0;
-import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
+import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
-contract MyCollectible is ERC721 {
- constructor() ERC721("MyCollectible", "MCO") {
+contract MyCollectible is ERC721Upgradeable {
+ function initialize() initializer public {
+ __ERC721_init("MyCollectible", "MCO");
}
}
```
diff --git a/contracts/finance/VestingWallet.sol b/contracts/finance/VestingWallet.sol
index fe67eb54..d26ea4e1 100644
--- a/contracts/finance/VestingWallet.sol
+++ b/contracts/finance/VestingWallet.sol
@@ -15,6 +15,8 @@ import "../utils/Context.sol";
* Any token transferred to this contract will follow the vesting schedule as if they were locked from the beginning.
* Consequently, if the vesting has already started, any amount of tokens sent to this contract will (at least partly)
* be immediately releasable.
+ *
+ * @custom:storage-size 52
*/
contract VestingWallet is Context {
event EtherReleased(uint256 amount);
diff --git a/contracts/governance/TimelockControllerWith46Migration.sol b/contracts/governance/TimelockControllerWith46Migration.sol
new file mode 100644
index 00000000..3315e7bd
--- /dev/null
+++ b/contracts/governance/TimelockControllerWith46Migration.sol
@@ -0,0 +1,39 @@
+// SPDX-License-Identifier: MIT
+// OpenZeppelin Contracts v4.6.0 (governance/TimelockControllerWith46Migration.sol)
+
+pragma solidity ^0.8.0;
+
+import "./TimelockController.sol";
+
+/**
+ * @dev Extension of the TimelockController that includes an additional
+ * function to migrate from OpenZeppelin Upgradeable Contracts <4.6 to >=4.6.
+ *
+ * This migration is necessary to setup administration rights over the new
+ * `CANCELLER_ROLE`.
+ *
+ * The migration is trustless and can be performed by anyone.
+ *
+ * _Available since v4.6._
+ */
+contract TimelockControllerWith46Migration is TimelockController {
+ constructor(
+ uint256 minDelay,
+ address[] memory proposers,
+ address[] memory executors,
+ address admin
+ ) TimelockController(minDelay, proposers, executors, admin) {}
+
+ /**
+ * @dev Migration function. Running it is necessary for upgradeable
+ * instances that were initially setup with code <4.6 and that upgraded
+ * to >=4.6.
+ */
+ function migrateTo46() public virtual {
+ require(
+ getRoleAdmin(PROPOSER_ROLE) == TIMELOCK_ADMIN_ROLE && getRoleAdmin(CANCELLER_ROLE) == DEFAULT_ADMIN_ROLE,
+ "TimelockController: already migrated"
+ );
+ _setRoleAdmin(CANCELLER_ROLE, TIMELOCK_ADMIN_ROLE);
+ }
+}
diff --git a/contracts/governance/extensions/GovernorVotes.sol b/contracts/governance/extensions/GovernorVotes.sol
index 64431711..885f0e42 100644
--- a/contracts/governance/extensions/GovernorVotes.sol
+++ b/contracts/governance/extensions/GovernorVotes.sol
@@ -10,6 +10,8 @@ import "../../interfaces/IERC5805.sol";
* @dev Extension of {Governor} for voting weight extraction from an {ERC20Votes} token, or since v4.5 an {ERC721Votes} token.
*
* _Available since v4.3._
+ *
+ * @custom:storage-size 51
*/
abstract contract GovernorVotes is Governor {
IERC5805 public immutable token;
diff --git a/contracts/governance/extensions/GovernorVotesComp.sol b/contracts/governance/extensions/GovernorVotesComp.sol
index 17250ad7..1d26b72e 100644
--- a/contracts/governance/extensions/GovernorVotesComp.sol
+++ b/contracts/governance/extensions/GovernorVotesComp.sol
@@ -10,6 +10,8 @@ import "../../token/ERC20/extensions/ERC20VotesComp.sol";
* @dev Extension of {Governor} for voting weight extraction from a Comp token.
*
* _Available since v4.3._
+ *
+ * @custom:storage-size 51
*/
abstract contract GovernorVotesComp is Governor {
ERC20VotesComp public immutable token;
diff --git a/contracts/package.json b/contracts/package.json
index 55e70b17..ceefb984 100644
--- a/contracts/package.json
+++ b/contracts/package.json
@@ -1,5 +1,5 @@
{
- "name": "@openzeppelin/contracts",
+ "name": "@openzeppelin/contracts-upgradeable",
"description": "Secure Smart Contract library for Solidity",
"version": "4.8.2",
"files": [
@@ -13,7 +13,7 @@
},
"repository": {
"type": "git",
- "url": "https://github.com/OpenZeppelin/openzeppelin-contracts.git"
+ "url": "https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable.git"
},
"keywords": [
"solidity",
diff --git a/contracts/security/PullPayment.sol b/contracts/security/PullPayment.sol
index 65b4980f..f336592e 100644
--- a/contracts/security/PullPayment.sol
+++ b/contracts/security/PullPayment.sol
@@ -22,6 +22,8 @@ import "../utils/escrow/Escrow.sol";
* To use, derive from the `PullPayment` contract, and use {_asyncTransfer}
* instead of Solidity's `transfer` function. Payees can query their due
* payments with {payments}, and retrieve them with {withdrawPayments}.
+ *
+ * @custom:storage-size 51
*/
abstract contract PullPayment {
Escrow private immutable _escrow;
diff --git a/contracts/token/ERC20/extensions/ERC20Capped.sol b/contracts/token/ERC20/extensions/ERC20Capped.sol
index 16f830d1..9ef98148 100644
--- a/contracts/token/ERC20/extensions/ERC20Capped.sol
+++ b/contracts/token/ERC20/extensions/ERC20Capped.sol
@@ -7,6 +7,8 @@ import "../ERC20.sol";
/**
* @dev Extension of {ERC20} that adds a cap to the supply of tokens.
+ *
+ * @custom:storage-size 51
*/
abstract contract ERC20Capped is ERC20 {
uint256 private immutable _cap;
diff --git a/contracts/token/ERC20/extensions/ERC20Permit.sol b/contracts/token/ERC20/extensions/ERC20Permit.sol
index a357199b..9dc8e894 100644
--- a/contracts/token/ERC20/extensions/ERC20Permit.sol
+++ b/contracts/token/ERC20/extensions/ERC20Permit.sol
@@ -18,6 +18,8 @@ import "../../../utils/Counters.sol";
* need to send a transaction, and thus is not required to hold Ether at all.
*
* _Available since v3.4._
+ *
+ * @custom:storage-size 51
*/
abstract contract ERC20Permit is ERC20, IERC20Permit, EIP712 {
using Counters for Counters.Counter;
diff --git a/contracts/token/ERC20/extensions/ERC20Wrapper.sol b/contracts/token/ERC20/extensions/ERC20Wrapper.sol
index bfe782e4..7264fe32 100644
--- a/contracts/token/ERC20/extensions/ERC20Wrapper.sol
+++ b/contracts/token/ERC20/extensions/ERC20Wrapper.sol
@@ -14,6 +14,8 @@ import "../utils/SafeERC20.sol";
* wrapping of an existing "basic" ERC20 into a governance token.
*
* _Available since v4.2._
+ *
+ * @custom:storage-size 51
*/
abstract contract ERC20Wrapper is ERC20 {
IERC20 private immutable _underlying;
diff --git a/contracts/token/ERC20/utils/TokenTimelock.sol b/contracts/token/ERC20/utils/TokenTimelock.sol
index ed855b7b..3d30f59d 100644
--- a/contracts/token/ERC20/utils/TokenTimelock.sol
+++ b/contracts/token/ERC20/utils/TokenTimelock.sol
@@ -11,6 +11,8 @@ import "./SafeERC20.sol";
*
* Useful for simple vesting schedules like "advisors get all of their tokens
* after 1 year".
+ *
+ * @custom:storage-size 53
*/
contract TokenTimelock {
using SafeERC20 for IERC20;
diff --git a/contracts/utils/cryptography/EIP712.sol b/contracts/utils/cryptography/EIP712.sol
index 6a4e1cad..55d8eced 100644
--- a/contracts/utils/cryptography/EIP712.sol
+++ b/contracts/utils/cryptography/EIP712.sol
@@ -4,7 +4,6 @@
pragma solidity ^0.8.8;
import "./ECDSA.sol";
-import "../ShortStrings.sol";
import "../../interfaces/IERC5267.sol";
/**
@@ -30,27 +29,19 @@ import "../../interfaces/IERC5267.sol";
*
* _Available since v3.4._
*
- * @custom:oz-upgrades-unsafe-allow state-variable-immutable state-variable-assignment
+ * @custom:storage-size 52
*/
abstract contract EIP712 is IERC5267 {
- using ShortStrings for *;
-
bytes32 private constant _TYPE_HASH =
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
- // Cache the domain separator as an immutable value, but also store the chain id that it corresponds to, in order to
- // invalidate the cached domain separator if the chain id changes.
- bytes32 private immutable _cachedDomainSeparator;
- uint256 private immutable _cachedChainId;
- address private immutable _cachedThis;
-
+ /// @custom:oz-renamed-from _HASHED_NAME
bytes32 private immutable _hashedName;
+ /// @custom:oz-renamed-from _HASHED_VERSION
bytes32 private immutable _hashedVersion;
- ShortString private immutable _name;
- ShortString private immutable _version;
- string private _nameFallback;
- string private _versionFallback;
+ string private _name;
+ string private _version;
/**
* @dev Initializes the domain separator and parameter caches.
@@ -65,29 +56,23 @@ abstract contract EIP712 is IERC5267 {
* contract upgrade].
*/
constructor(string memory name, string memory version) {
- _name = name.toShortStringWithFallback(_nameFallback);
- _version = version.toShortStringWithFallback(_versionFallback);
- _hashedName = keccak256(bytes(name));
- _hashedVersion = keccak256(bytes(version));
-
- _cachedChainId = block.chainid;
- _cachedDomainSeparator = _buildDomainSeparator();
- _cachedThis = address(this);
+ _name = name;
+ _version = version;
+
+ // Reset prior values in storage if upgrading
+ _hashedName = 0;
+ _hashedVersion = 0;
}
/**
* @dev Returns the domain separator for the current chain.
*/
function _domainSeparatorV4() internal view returns (bytes32) {
- if (address(this) == _cachedThis && block.chainid == _cachedChainId) {
- return _cachedDomainSeparator;
- } else {
- return _buildDomainSeparator();
- }
+ return _buildDomainSeparator();
}
function _buildDomainSeparator() private view returns (bytes32) {
- return keccak256(abi.encode(_TYPE_HASH, _hashedName, _hashedVersion, block.chainid, address(this)));
+ return keccak256(abi.encode(_TYPE_HASH, _EIP712NameHash(), _EIP712VersionHash(), block.chainid, address(this)));
}
/**
@@ -129,14 +114,80 @@ abstract contract EIP712 is IERC5267 {
uint256[] memory extensions
)
{
+ // If the hashed name and version in storage are non-zero, the contract hasn't been properly initialized
+ // and the EIP712 domain is not reliable, as it will be missing name and version.
+ require(_hashedName == 0 && _hashedVersion == 0, "EIP712: Uninitialized");
+
return (
hex"0f", // 01111
- _name.toStringWithFallback(_nameFallback),
- _version.toStringWithFallback(_versionFallback),
+ _EIP712Name(),
+ _EIP712Version(),
block.chainid,
address(this),
bytes32(0),
new uint256[](0)
);
}
+
+ /**
+ * @dev The name parameter for the EIP712 domain.
+ *
+ * NOTE: This function reads from storage by default, but can be redefined to return a constant value if gas costs
+ * are a concern.
+ */
+ function _EIP712Name() internal virtual view returns (string memory) {
+ return _name;
+ }
+
+ /**
+ * @dev The version parameter for the EIP712 domain.
+ *
+ * NOTE: This function reads from storage by default, but can be redefined to return a constant value if gas costs
+ * are a concern.
+ */
+ function _EIP712Version() internal virtual view returns (string memory) {
+ return _version;
+ }
+
+ /**
+ * @dev The hash of the name parameter for the EIP712 domain.
+ *
+ * NOTE: In previous versions this function was virtual. In this version you should override `_EIP712Name` instead.
+ */
+ function _EIP712NameHash() internal view returns (bytes32) {
+ string memory name = _EIP712Name();
+ if (bytes(name).length > 0) {
+ return keccak256(bytes(name));
+ } else {
+ // If the name is empty, the contract may have been upgraded without initializing the new storage.
+ // We return the name hash in storage if non-zero, otherwise we assume the name is empty by design.
+ bytes32 hashedName = _hashedName;
+ if (hashedName != 0) {
+ return hashedName;
+ } else {
+ return keccak256("");
+ }
+ }
+ }
+
+ /**
+ * @dev The hash of the version parameter for the EIP712 domain.
+ *
+ * NOTE: In previous versions this function was virtual. In this version you should override `_EIP712Version` instead.
+ */
+ function _EIP712VersionHash() internal view returns (bytes32) {
+ string memory version = _EIP712Version();
+ if (bytes(version).length > 0) {
+ return keccak256(bytes(version));
+ } else {
+ // If the version is empty, the contract may have been upgraded without initializing the new storage.
+ // We return the version hash in storage if non-zero, otherwise we assume the version is empty by design.
+ bytes32 hashedVersion = _hashedVersion;
+ if (hashedVersion != 0) {
+ return hashedVersion;
+ } else {
+ return keccak256("");
+ }
+ }
+ }
}
diff --git a/package.json b/package.json
index 8458dd61..b4672240 100644
--- a/package.json
+++ b/package.json
@@ -36,7 +36,7 @@
},
"repository": {
"type": "git",
- "url": "https://github.com/OpenZeppelin/openzeppelin-contracts.git"
+ "url": "https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable.git"
},
"keywords": [
"solidity",
diff --git a/test/utils/cryptography/EIP712.test.js b/test/utils/cryptography/EIP712.test.js
index 54a4e772..ba4602ed 100644
--- a/test/utils/cryptography/EIP712.test.js
+++ b/test/utils/cryptography/EIP712.test.js
@@ -47,26 +47,6 @@ contract('EIP712', function (accounts) {
const rebuildDomain = await getDomain(this.eip712);
expect(mapValues(rebuildDomain, String)).to.be.deep.equal(mapValues(this.domain, String));
});
-
- if (shortOrLong === 'short') {
- // Long strings are in storage, and the proxy will not be properly initialized unless
- // the upgradeable contract variant is used and the initializer is invoked.
-
- it('adjusts when behind proxy', async function () {
- const factory = await Clones.new();
- const cloneReceipt = await factory.$clone(this.eip712.address);
- const cloneAddress = cloneReceipt.logs.find(({ event }) => event === 'return$clone').args.instance;
- const clone = new EIP712Verifier(cloneAddress);
-
- const cloneDomain = { ...this.domain, verifyingContract: clone.address };
-
- const reportedDomain = await getDomain(clone);
- expect(mapValues(reportedDomain, String)).to.be.deep.equal(mapValues(cloneDomain, String));
-
- const expectedSeparator = await domainSeparator(cloneDomain);
- expect(await clone.$_domainSeparatorV4()).to.equal(expectedSeparator);
- });
- }
});
it('hash digest', async function () {