mirror of
https://github.com/th30d4y/OpenLearnX.git
synced 2026-05-26 19:26:33 +00:00
Fix .gitignore: stop tracking ignored files
This commit is contained in:
@@ -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;
|
||||
@@ -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
@@ -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
|
||||
@@ -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
@@ -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,
|
||||
});
|
||||
+17
@@ -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 () {
|
||||
Reference in New Issue
Block a user