This commit is contained in:
dexorder
2024-10-17 02:42:28 -04:00
commit 25def69c66
878 changed files with 112489 additions and 0 deletions

View File

@@ -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;

View File

@@ -0,0 +1,53 @@
#!/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/structs/Checkpoints.sol': './templates/Checkpoints.js',
'utils/SlotDerivation.sol': './templates/SlotDerivation.js',
'utils/StorageSlot.sol': './templates/StorageSlot.js',
'utils/Arrays.sol': './templates/Arrays.js',
'mocks/StorageSlotMock.sol': './templates/StorageSlotMock.js',
})) {
generateFromTemplate(file, template, './contracts/');
}
// Tests
for (const [file, template] of Object.entries({
'utils/structs/Checkpoints.t.sol': './templates/Checkpoints.t.js',
'utils/SlotDerivation.t.sol': './templates/SlotDerivation.t.js',
})) {
generateFromTemplate(file, template, './test/');
}

View File

@@ -0,0 +1,385 @@
const format = require('../format-lines');
const { capitalize } = require('../../helpers');
const { TYPES } = require('./Arrays.opts');
const header = `\
pragma solidity ^0.8.20;
import {SlotDerivation} from "./SlotDerivation.sol";
import {StorageSlot} from "./StorageSlot.sol";
import {Math} from "./math/Math.sol";
/**
* @dev Collection of functions related to array types.
*/
`;
const sort = type => `\
/**
* @dev Sort an array of ${type} (in memory) following the provided comparator function.
*
* This function does the sorting "in place", meaning that it overrides the input. The object is returned for
* convenience, but that returned value can be discarded safely if the caller has a memory pointer to the array.
*
* NOTE: this function's cost is \`O(n · log(n))\` in average and \`O(n²)\` in the worst case, with n the length of the
* array. Using it in view functions that are executed through \`eth_call\` is safe, but one should be very careful
* when executing this as part of a transaction. If the array being sorted is too large, the sort operation may
* consume more gas than is available in a block, leading to potential DoS.
*/
function sort(
${type}[] memory array,
function(${type}, ${type}) pure returns (bool) comp
) internal pure returns (${type}[] memory) {
${
type === 'bytes32'
? '_quickSort(_begin(array), _end(array), comp);'
: 'sort(_castToBytes32Array(array), _castToBytes32Comp(comp));'
}
return array;
}
/**
* @dev Variant of {sort} that sorts an array of ${type} in increasing order.
*/
function sort(${type}[] memory array) internal pure returns (${type}[] memory) {
${type === 'bytes32' ? 'sort(array, _defaultComp);' : 'sort(_castToBytes32Array(array), _defaultComp);'}
return array;
}
`;
const quickSort = `
/**
* @dev Performs a quick sort of a segment of memory. The segment sorted starts at \`begin\` (inclusive), and stops
* at end (exclusive). Sorting follows the \`comp\` comparator.
*
* Invariant: \`begin <= end\`. This is the case when initially called by {sort} and is preserved in subcalls.
*
* IMPORTANT: Memory locations between \`begin\` and \`end\` are not validated/zeroed. This function should
* be used only if the limits are within a memory array.
*/
function _quickSort(uint256 begin, uint256 end, function(bytes32, bytes32) pure returns (bool) comp) private pure {
unchecked {
if (end - begin < 0x40) return;
// Use first element as pivot
bytes32 pivot = _mload(begin);
// Position where the pivot should be at the end of the loop
uint256 pos = begin;
for (uint256 it = begin + 0x20; it < end; it += 0x20) {
if (comp(_mload(it), pivot)) {
// If the value stored at the iterator's position comes before the pivot, we increment the
// position of the pivot and move the value there.
pos += 0x20;
_swap(pos, it);
}
}
_swap(begin, pos); // Swap pivot into place
_quickSort(begin, pos, comp); // Sort the left side of the pivot
_quickSort(pos + 0x20, end, comp); // Sort the right side of the pivot
}
}
/**
* @dev Pointer to the memory location of the first element of \`array\`.
*/
function _begin(bytes32[] memory array) private pure returns (uint256 ptr) {
/// @solidity memory-safe-assembly
assembly {
ptr := add(array, 0x20)
}
}
/**
* @dev Pointer to the memory location of the first memory word (32bytes) after \`array\`. This is the memory word
* that comes just after the last element of the array.
*/
function _end(bytes32[] memory array) private pure returns (uint256 ptr) {
unchecked {
return _begin(array) + array.length * 0x20;
}
}
/**
* @dev Load memory word (as a bytes32) at location \`ptr\`.
*/
function _mload(uint256 ptr) private pure returns (bytes32 value) {
assembly {
value := mload(ptr)
}
}
/**
* @dev Swaps the elements memory location \`ptr1\` and \`ptr2\`.
*/
function _swap(uint256 ptr1, uint256 ptr2) private pure {
assembly {
let value1 := mload(ptr1)
let value2 := mload(ptr2)
mstore(ptr1, value2)
mstore(ptr2, value1)
}
}
`;
const defaultComparator = `
/// @dev Comparator for sorting arrays in increasing order.
function _defaultComp(bytes32 a, bytes32 b) private pure returns (bool) {
return a < b;
}
`;
const castArray = type => `\
/// @dev Helper: low level cast ${type} memory array to uint256 memory array
function _castToBytes32Array(${type}[] memory input) private pure returns (bytes32[] memory output) {
assembly {
output := input
}
}
`;
const castComparator = type => `\
/// @dev Helper: low level cast ${type} comp function to bytes32 comp function
function _castToBytes32Comp(
function(${type}, ${type}) pure returns (bool) input
) private pure returns (function(bytes32, bytes32) pure returns (bool) output) {
assembly {
output := input
}
}
`;
const search = `
/**
* @dev Searches a sorted \`array\` and returns the first index that contains
* a value greater or equal to \`element\`. If no such index exists (i.e. all
* values in the array are strictly less than \`element\`), the array length is
* returned. Time complexity O(log n).
*
* NOTE: The \`array\` is expected to be sorted in ascending order, and to
* contain no repeated elements.
*
* IMPORTANT: Deprecated. This implementation behaves as {lowerBound} but lacks
* support for repeated elements in the array. The {lowerBound} function should
* be used instead.
*/
function findUpperBound(uint256[] storage array, uint256 element) internal view returns (uint256) {
uint256 low = 0;
uint256 high = array.length;
if (high == 0) {
return 0;
}
while (low < high) {
uint256 mid = Math.average(low, high);
// Note that mid will always be strictly less than high (i.e. it will be a valid array index)
// because Math.average rounds towards zero (it does integer division with truncation).
if (unsafeAccess(array, mid).value > element) {
high = mid;
} else {
low = mid + 1;
}
}
// At this point \`low\` is the exclusive upper bound. We will return the inclusive upper bound.
if (low > 0 && unsafeAccess(array, low - 1).value == element) {
return low - 1;
} else {
return low;
}
}
/**
* @dev Searches an \`array\` sorted in ascending order and returns the first
* index that contains a value greater or equal than \`element\`. If no such index
* exists (i.e. all values in the array are strictly less than \`element\`), the array
* length is returned. Time complexity O(log n).
*
* See C++'s https://en.cppreference.com/w/cpp/algorithm/lower_bound[lower_bound].
*/
function lowerBound(uint256[] storage array, uint256 element) internal view returns (uint256) {
uint256 low = 0;
uint256 high = array.length;
if (high == 0) {
return 0;
}
while (low < high) {
uint256 mid = Math.average(low, high);
// Note that mid will always be strictly less than high (i.e. it will be a valid array index)
// because Math.average rounds towards zero (it does integer division with truncation).
if (unsafeAccess(array, mid).value < element) {
// this cannot overflow because mid < high
unchecked {
low = mid + 1;
}
} else {
high = mid;
}
}
return low;
}
/**
* @dev Searches an \`array\` sorted in ascending order and returns the first
* index that contains a value strictly greater than \`element\`. If no such index
* exists (i.e. all values in the array are strictly less than \`element\`), the array
* length is returned. Time complexity O(log n).
*
* See C++'s https://en.cppreference.com/w/cpp/algorithm/upper_bound[upper_bound].
*/
function upperBound(uint256[] storage array, uint256 element) internal view returns (uint256) {
uint256 low = 0;
uint256 high = array.length;
if (high == 0) {
return 0;
}
while (low < high) {
uint256 mid = Math.average(low, high);
// Note that mid will always be strictly less than high (i.e. it will be a valid array index)
// because Math.average rounds towards zero (it does integer division with truncation).
if (unsafeAccess(array, mid).value > element) {
high = mid;
} else {
// this cannot overflow because mid < high
unchecked {
low = mid + 1;
}
}
}
return low;
}
/**
* @dev Same as {lowerBound}, but with an array in memory.
*/
function lowerBoundMemory(uint256[] memory array, uint256 element) internal pure returns (uint256) {
uint256 low = 0;
uint256 high = array.length;
if (high == 0) {
return 0;
}
while (low < high) {
uint256 mid = Math.average(low, high);
// Note that mid will always be strictly less than high (i.e. it will be a valid array index)
// because Math.average rounds towards zero (it does integer division with truncation).
if (unsafeMemoryAccess(array, mid) < element) {
// this cannot overflow because mid < high
unchecked {
low = mid + 1;
}
} else {
high = mid;
}
}
return low;
}
/**
* @dev Same as {upperBound}, but with an array in memory.
*/
function upperBoundMemory(uint256[] memory array, uint256 element) internal pure returns (uint256) {
uint256 low = 0;
uint256 high = array.length;
if (high == 0) {
return 0;
}
while (low < high) {
uint256 mid = Math.average(low, high);
// Note that mid will always be strictly less than high (i.e. it will be a valid array index)
// because Math.average rounds towards zero (it does integer division with truncation).
if (unsafeMemoryAccess(array, mid) > element) {
high = mid;
} else {
// this cannot overflow because mid < high
unchecked {
low = mid + 1;
}
}
}
return low;
}
`;
const unsafeAccessStorage = type => `
/**
* @dev Access an array in an "unsafe" way. Skips solidity "index-out-of-range" check.
*
* WARNING: Only use if you are certain \`pos\` is lower than the array length.
*/
function unsafeAccess(${type}[] storage arr, uint256 pos) internal pure returns (StorageSlot.${capitalize(
type,
)}Slot storage) {
bytes32 slot;
/// @solidity memory-safe-assembly
assembly {
slot := arr.slot
}
return slot.deriveArray().offset(pos).get${capitalize(type)}Slot();
}`;
const unsafeAccessMemory = type => `
/**
* @dev Access an array in an "unsafe" way. Skips solidity "index-out-of-range" check.
*
* WARNING: Only use if you are certain \`pos\` is lower than the array length.
*/
function unsafeMemoryAccess(${type}[] memory arr, uint256 pos) internal pure returns (${type} res) {
assembly {
res := mload(add(add(arr, 0x20), mul(pos, 0x20)))
}
}
`;
const unsafeSetLength = type => `
/**
* @dev Helper to set the length of an dynamic array. Directly writing to \`.length\` is forbidden.
*
* WARNING: this does not clear elements if length is reduced, of initialize elements if length is increased.
*/
function unsafeSetLength(${type}[] storage array, uint256 len) internal {
/// @solidity memory-safe-assembly
assembly {
sstore(array.slot, len)
}
}`;
// GENERATE
module.exports = format(
header.trimEnd(),
'library Arrays {',
'using SlotDerivation for bytes32;',
'using StorageSlot for bytes32;',
// sorting, comparator, helpers and internal
sort('bytes32'),
TYPES.filter(type => type !== 'bytes32').map(sort),
quickSort,
defaultComparator,
TYPES.filter(type => type !== 'bytes32').map(castArray),
TYPES.filter(type => type !== 'bytes32').map(castComparator),
// lookup
search,
// unsafe (direct) storage and memory access
TYPES.map(unsafeAccessStorage),
TYPES.map(unsafeAccessMemory),
TYPES.map(unsafeSetLength),
'}',
);

View File

@@ -0,0 +1,3 @@
const TYPES = ['address', 'bytes32', 'uint256'];
module.exports = { TYPES };

View File

@@ -0,0 +1,248 @@
const format = require('../format-lines');
const { OPTS } = require('./Checkpoints.opts');
// TEMPLATE
const header = `\
pragma solidity ^0.8.20;
import {Math} from "../math/Math.sol";
/**
* @dev This library defines the \`Trace*\` 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.Trace*\` in your contract, and store a new
* checkpoint for the current transaction block using the {push} function.
*/
`;
const errors = `\
/**
* @dev A value was attempted to be inserted on a past checkpoint.
*/
error CheckpointUnorderedInsertion();
`;
const template = opts => `\
struct ${opts.historyTypeName} {
${opts.checkpointTypeName}[] ${opts.checkpointFieldName};
}
struct ${opts.checkpointTypeName} {
${opts.keyTypeName} ${opts.keyFieldName};
${opts.valueTypeName} ${opts.valueFieldName};
}
/**
* @dev Pushes a (\`key\`, \`value\`) pair into a ${opts.historyTypeName} so that it is stored as the checkpoint.
*
* Returns previous value and new value.
*
* IMPORTANT: Never accept \`key\` as a user input, since an arbitrary \`type(${opts.keyTypeName}).max\` key set will disable the
* library.
*/
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};
}
/**
* @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} storage 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 Returns checkpoint at given position.
*/
function at(${opts.historyTypeName} storage self, uint32 pos) internal view returns (${opts.checkpointTypeName} memory) {
return self.${opts.checkpointFieldName}[pos];
}
/**
* @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) {
${opts.checkpointTypeName} storage last = _unsafeAccess(self, pos - 1);
${opts.keyTypeName} lastKey = last.${opts.keyFieldName};
${opts.valueTypeName} lastValue = last.${opts.valueFieldName};
// Checkpoint keys must be non-decreasing.
if (lastKey > key) {
revert CheckpointUnorderedInsertion();
}
// Update or push new checkpoint
if (lastKey == key) {
_unsafeAccess(self, pos - 1).${opts.valueFieldName} = value;
} else {
self.push(${opts.checkpointTypeName}({${opts.keyFieldName}: key, ${opts.valueFieldName}: value}));
}
return (lastValue, 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 {',
errors,
OPTS.flatMap(opts => template(opts)),
'}',
);

View File

@@ -0,0 +1,17 @@
// OPTIONS
const VALUE_SIZES = [224, 208, 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 = {
VALUE_SIZES,
OPTS: VALUE_SIZES.map(size => defaultOpts(size)),
};

View File

@@ -0,0 +1,146 @@
const format = require('../format-lines');
const { capitalize } = require('../../helpers');
const { OPTS } = require('./Checkpoints.opts.js');
// TEMPLATE
const header = `\
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";
import {Checkpoints} from "@openzeppelin/contracts/utils/structs/Checkpoints.sol";
`;
/* eslint-disable max-len */
const template = 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 pure 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 pure {
${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);
}
// 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);
}
`;
// GENERATE
module.exports = format(
header,
...OPTS.flatMap(opts => [`contract Checkpoints${opts.historyTypeName}Test is Test {`, [template(opts)], '}']),
);

View File

@@ -0,0 +1,281 @@
const format = require('../format-lines');
const { fromBytes32, toBytes32 } = require('./conversion');
const { TYPES } = require('./EnumerableMap.opts');
/* eslint-disable max-len */
const header = `\
pragma solidity ^0.8.20;
import {EnumerableSet} from "./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
* - \`uint256 -> bytes32\` (\`UintToBytes32Map\`) since v5.1.0
* - \`address -> address\` (\`AddressToAddressMap\`) since v5.1.0
* - \`address -> bytes32\` (\`AddressToBytes32Map\`) since v5.1.0
* - \`bytes32 -> address\` (\`Bytes32ToAddressMap\`) since v5.1.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 \`UintToAddressMap\` are just wrappers around the underlying Map.
// This means that we can only create new EnumerableMaps for types that fit in bytes32.
/**
* @dev Query for a nonexistent map key.
*/
error EnumerableMapNonexistentKey(bytes32 key);
struct Bytes32ToBytes32Map {
// Storage of keys
EnumerableSet.Bytes32Set _keys;
mapping(bytes32 key => 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];
if(value == 0 && !contains(map, key)) {
revert EnumerableMapNonexistentKey(key);
}
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 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'),
],
'}',
);

View File

@@ -0,0 +1,19 @@
const { capitalize } = require('../../helpers');
const mapType = str => (str == 'uint256' ? 'Uint' : capitalize(str));
const formatType = (keyType, valueType) => ({
name: `${mapType(keyType)}To${mapType(valueType)}Map`,
keyType,
valueType,
});
const TYPES = ['uint256', 'address', 'bytes32']
.flatMap((key, _, array) => array.map(value => [key, value]))
.slice(0, -1) // remove bytes32 → byte32 (last one) that is already defined
.map(args => formatType(...args));
module.exports = {
TYPES,
formatType,
};

View File

@@ -0,0 +1,245 @@
const format = require('../format-lines');
const { fromBytes32, toBytes32 } = require('./conversion');
const { TYPES } = require('./EnumerableSet.opts');
/* eslint-disable max-len */
const header = `\
pragma solidity ^0.8.20;
/**
* @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 is the index of the value in the \`values\` array plus 1.
// Position 0 is used to mean a value is not in the set.
mapping(bytes32 value => uint256) _positions;
}
/**
* @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._positions[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 cache the value's position to prevent multiple reads from the same storage slot
uint256 position = set._positions[value];
if (position != 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 valueIndex = position - 1;
uint256 lastIndex = set._values.length - 1;
if (valueIndex != lastIndex) {
bytes32 lastValue = set._values[lastIndex];
// Move the lastValue to the index where the value to delete is
set._values[valueIndex] = lastValue;
// Update the tracked position of the lastValue (that was just moved)
set._positions[lastValue] = position;
}
// Delete the slot where the moved value was stored
set._values.pop();
// Delete the tracked position for the deleted slot
delete set._positions[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._positions[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')],
'}',
);

View File

@@ -0,0 +1,12 @@
const { capitalize } = require('../../helpers');
const mapType = str => (str == 'uint256' ? 'Uint' : capitalize(str));
const formatType = type => ({
name: `${mapType(type)}Set`,
type,
});
const TYPES = ['bytes32', 'address', 'uint256'].map(formatType);
module.exports = { TYPES, formatType };

View File

@@ -0,0 +1,138 @@
const format = require('../format-lines');
const { range } = require('../../helpers');
const LENGTHS = range(8, 256, 8).reverse(); // 248 → 8 (in steps of 8)
const header = `\
pragma solidity ^0.8.20;
/**
* @dev Wrappers over Solidity's uintXX/intXX/bool 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.
*/
`;
const errors = `\
/**
* @dev Value doesn't fit in an uint of \`bits\` size.
*/
error SafeCastOverflowedUintDowncast(uint8 bits, uint256 value);
/**
* @dev An int value doesn't fit in an uint of \`bits\` size.
*/
error SafeCastOverflowedIntToUint(int256 value);
/**
* @dev Value doesn't fit in an int of \`bits\` size.
*/
error SafeCastOverflowedIntDowncast(uint8 bits, int256 value);
/**
* @dev An uint value doesn't fit in an int of \`bits\` size.
*/
error SafeCastOverflowedUintToInt(uint256 value);
`;
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
*/
function toUint${length}(uint256 value) internal pure returns (uint${length}) {
if (value > type(uint${length}).max) {
revert SafeCastOverflowedUintDowncast(${length}, value);
}
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
*/
function toInt${length}(int256 value) internal pure returns (int${length} downcasted) {
downcasted = int${length}(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(${length}, value);
}
}
`;
/* 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}.
*/
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
if (value > uint${length}(type(int${length}).max)) {
revert SafeCastOverflowedUintToInt(value);
}
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.
*/
function toUint${length}(int${length} value) internal pure returns (uint${length}) {
if (value < 0) {
revert SafeCastOverflowedIntToUint(value);
}
return uint${length}(value);
}
`;
const boolToUint = `
/**
* @dev Cast a boolean (false or true) to a uint256 (0 or 1) with no jump.
*/
function toUint(bool b) internal pure returns (uint256 u) {
/// @solidity memory-safe-assembly
assembly {
u := iszero(iszero(b))
}
}
`;
// GENERATE
module.exports = format(
header.trimEnd(),
'library SafeCast {',
errors,
[...LENGTHS.map(toUintDownCast), toUint(256), ...LENGTHS.map(toIntDownCast), toInt(256), boolToUint],
'}',
);

View File

@@ -0,0 +1,13 @@
const { capitalize } = require('../../helpers');
const TYPES = [
{ type: 'address', isValueType: true },
{ type: 'bool', isValueType: true, name: 'Boolean' },
{ type: 'bytes32', isValueType: true, variants: ['bytes4'] },
{ type: 'uint256', isValueType: true, variants: ['uint32'] },
{ type: 'int256', isValueType: true, variants: ['int32'] },
{ type: 'string', isValueType: false },
{ type: 'bytes', isValueType: false },
].map(type => Object.assign(type, { name: type.name ?? capitalize(type.type) }));
module.exports = { TYPES };

View File

@@ -0,0 +1,116 @@
const format = require('../format-lines');
const { TYPES } = require('./Slot.opts');
const header = `\
pragma solidity ^0.8.20;
/**
* @dev Library for computing storage (and transient storage) locations from namespaces and deriving slots
* corresponding to standard patterns. The derivation method for array and mapping matches the storage layout used by
* the solidity language / compiler.
*
* See https://docs.soliditylang.org/en/v0.8.20/internals/layout_in_storage.html#mappings-and-dynamic-arrays[Solidity docs for mappings and dynamic arrays.].
*
* Example usage:
* \`\`\`solidity
* contract Example {
* // Add the library methods
* using StorageSlot for bytes32;
* using SlotDerivation for bytes32;
*
* // Declare a namespace
* string private constant _NAMESPACE = "<namespace>" // eg. OpenZeppelin.Slot
*
* function setValueInNamespace(uint256 key, address newValue) internal {
* _NAMESPACE.erc7201Slot().deriveMapping(key).getAddressSlot().value = newValue;
* }
*
* function getValueInNamespace(uint256 key) internal view returns (address) {
* return _NAMESPACE.erc7201Slot().deriveMapping(key).getAddressSlot().value;
* }
* }
* \`\`\`
*
* TIP: Consider using this library along with {StorageSlot}.
*
* NOTE: This library provides a way to manipulate storage locations in a non-standard way. Tooling for checking
* upgrade safety will ignore the slots accessed through this library.
*/
`;
const namespace = `\
/**
* @dev Derive an ERC-7201 slot from a string (namespace).
*/
function erc7201Slot(string memory namespace) internal pure returns (bytes32 slot) {
/// @solidity memory-safe-assembly
assembly {
mstore(0x00, sub(keccak256(add(namespace, 0x20), mload(namespace)), 1))
slot := and(keccak256(0x00, 0x20), not(0xff))
}
}
`;
const array = `\
/**
* @dev Add an offset to a slot to get the n-th element of a structure or an array.
*/
function offset(bytes32 slot, uint256 pos) internal pure returns (bytes32 result) {
unchecked {
return bytes32(uint256(slot) + pos);
}
}
/**
* @dev Derive the location of the first element in an array from the slot where the length is stored.
*/
function deriveArray(bytes32 slot) internal pure returns (bytes32 result) {
/// @solidity memory-safe-assembly
assembly {
mstore(0x00, slot)
result := keccak256(0x00, 0x20)
}
}
`;
const mapping = ({ type }) => `\
/**
* @dev Derive the location of a mapping element from the key.
*/
function deriveMapping(bytes32 slot, ${type} key) internal pure returns (bytes32 result) {
/// @solidity memory-safe-assembly
assembly {
mstore(0x00, key)
mstore(0x20, slot)
result := keccak256(0x00, 0x40)
}
}
`;
const mapping2 = ({ type }) => `\
/**
* @dev Derive the location of a mapping element from the key.
*/
function deriveMapping(bytes32 slot, ${type} memory key) internal pure returns (bytes32 result) {
/// @solidity memory-safe-assembly
assembly {
let length := mload(key)
let begin := add(key, 0x20)
let end := add(begin, length)
let cache := mload(end)
mstore(end, slot)
result := keccak256(begin, add(length, 0x20))
mstore(end, cache)
}
}
`;
// GENERATE
module.exports = format(
header.trimEnd(),
'library SlotDerivation {',
namespace,
array,
TYPES.map(type => (type.isValueType ? mapping(type) : mapping2(type))),
'}',
);

View File

@@ -0,0 +1,73 @@
const format = require('../format-lines');
const { capitalize } = require('../../helpers');
const { TYPES } = require('./Slot.opts');
const header = `\
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {SlotDerivation} from "@openzeppelin/contracts/utils/SlotDerivation.sol";
`;
const array = `\
bytes[] private _array;
function testDeriveArray(uint256 length, uint256 offset) public {
length = bound(length, 1, type(uint256).max);
offset = bound(offset, 0, length - 1);
bytes32 baseSlot;
assembly {
baseSlot := _array.slot
sstore(baseSlot, length) // store length so solidity access does not revert
}
bytes storage derived = _array[offset];
bytes32 derivedSlot;
assembly {
derivedSlot := derived.slot
}
assertEq(baseSlot.deriveArray().offset(offset), derivedSlot);
}
`;
const mapping = ({ type, name, isValueType }) => `\
mapping(${type} => bytes) private _${type}Mapping;
function testDeriveMapping${name}(${type} ${isValueType ? '' : 'memory'} key) public {
bytes32 baseSlot;
assembly {
baseSlot := _${type}Mapping.slot
}
bytes storage derived = _${type}Mapping[key];
bytes32 derivedSlot;
assembly {
derivedSlot := derived.slot
}
assertEq(baseSlot.deriveMapping(key), derivedSlot);
}
`;
// GENERATE
module.exports = format(
header.trimEnd(),
'contract SlotDerivationTest is Test {',
'using SlotDerivation for bytes32;',
'',
array,
TYPES.flatMap(type =>
[].concat(
type,
(type.variants ?? []).map(variant => ({
type: variant,
name: capitalize(variant),
isValueType: type.isValueType,
})),
),
).map(type => mapping(type)),
'}',
);

View File

@@ -0,0 +1,127 @@
const format = require('../format-lines');
const { TYPES } = require('./Slot.opts');
const header = `\
pragma solidity ^0.8.24;
/**
* @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 ERC-1967 implementation slot:
* \`\`\`solidity
* contract ERC1967 {
* // Define the slot. Alternatively, use the SlotDerivation library to derive the slot.
* bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
*
* function _getImplementation() internal view returns (address) {
* return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value;
* }
*
* function _setImplementation(address newImplementation) internal {
* require(newImplementation.code.length > 0);
* StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation;
* }
* }
* \`\`\`
*
* Since version 5.1, this library also support writing and reading value types to and from transient storage.
*
* * Example using transient storage:
* \`\`\`solidity
* contract Lock {
* // Define the slot. Alternatively, use the SlotDerivation library to derive the slot.
* bytes32 internal constant _LOCK_SLOT = 0xf4678858b2b588224636b8522b729e7722d32fc491da849ed75b3fdf3c84f542;
*
* modifier locked() {
* require(!_LOCK_SLOT.asBoolean().tload());
*
* _LOCK_SLOT.asBoolean().tstore(true);
* _;
* _LOCK_SLOT.asBoolean().tstore(false);
* }
* }
* \`\`\`
*
* TIP: Consider using this library along with {SlotDerivation}.
*/
`;
const struct = ({ type, name }) => `\
struct ${name}Slot {
${type} value;
}
`;
const get = ({ name }) => `\
/**
* @dev Returns an \`${name}Slot\` with member \`value\` located at \`slot\`.
*/
function get${name}Slot(bytes32 slot) internal pure returns (${name}Slot storage r) {
/// @solidity memory-safe-assembly
assembly {
r.slot := slot
}
}
`;
const getStorage = ({ type, name }) => `\
/**
* @dev Returns an \`${name}Slot\` representation of the ${type} storage pointer \`store\`.
*/
function get${name}Slot(${type} storage store) internal pure returns (${name}Slot storage r) {
/// @solidity memory-safe-assembly
assembly {
r.slot := store.slot
}
}
`;
const udvt = ({ type, name }) => `\
/**
* @dev UDVT that represent a slot holding a ${type}.
*/
type ${name}SlotType is bytes32;
/**
* @dev Cast an arbitrary slot to a ${name}SlotType.
*/
function as${name}(bytes32 slot) internal pure returns (${name}SlotType) {
return ${name}SlotType.wrap(slot);
}
`;
const transient = ({ type, name }) => `\
/**
* @dev Load the value held at location \`slot\` in transient storage.
*/
function tload(${name}SlotType slot) internal view returns (${type} value) {
/// @solidity memory-safe-assembly
assembly {
value := tload(slot)
}
}
/**
* @dev Store \`value\` at location \`slot\` in transient storage.
*/
function tstore(${name}SlotType slot, ${type} value) internal {
/// @solidity memory-safe-assembly
assembly {
tstore(slot, value)
}
}
`;
// GENERATE
module.exports = format(
header.trimEnd(),
'library StorageSlot {',
TYPES.map(type => struct(type)),
TYPES.flatMap(type => [get(type), type.isValueType ? '' : getStorage(type)]),
TYPES.filter(type => type.isValueType).map(type => udvt(type)),
TYPES.filter(type => type.isValueType).map(type => transient(type)),
'}',
);

View File

@@ -0,0 +1,65 @@
const format = require('../format-lines');
const { TYPES } = require('./Slot.opts');
const header = `\
pragma solidity ^0.8.24;
import {Multicall} from "../utils/Multicall.sol";
import {StorageSlot} from "../utils/StorageSlot.sol";
`;
const storageSetValueType = ({ type, name }) => `\
function set${name}Slot(bytes32 slot, ${type} value) public {
slot.get${name}Slot().value = value;
}
`;
const storageGetValueType = ({ type, name }) => `\
function get${name}Slot(bytes32 slot) public view returns (${type}) {
return slot.get${name}Slot().value;
}
`;
const storageSetNonValueType = ({ type, name }) => `\
mapping(uint256 key => ${type}) public ${type}Map;
function set${name}Slot(bytes32 slot, ${type} calldata value) public {
slot.get${name}Slot().value = value;
}
function set${name}Storage(uint256 key, ${type} calldata value) public {
${type}Map[key].get${name}Slot().value = value;
}
function get${name}Slot(bytes32 slot) public view returns (${type} memory) {
return slot.get${name}Slot().value;
}
function get${name}Storage(uint256 key) public view returns (${type} memory) {
return ${type}Map[key].get${name}Slot().value;
}
`;
const transient = ({ type, name }) => `\
event ${name}Value(bytes32 slot, ${type} value);
function tload${name}(bytes32 slot) public {
emit ${name}Value(slot, slot.as${name}().tload());
}
function tstore(bytes32 slot, ${type} value) public {
slot.as${name}().tstore(value);
}
`;
// GENERATE
module.exports = format(
header.trimEnd(),
'contract StorageSlotMock is Multicall {',
'using StorageSlot for *;',
TYPES.filter(type => type.isValueType).map(type => storageSetValueType(type)),
TYPES.filter(type => type.isValueType).map(type => storageGetValueType(type)),
TYPES.filter(type => !type.isValueType).map(type => storageSetNonValueType(type)),
TYPES.filter(type => type.isValueType).map(type => transient(type)),
'}',
);

View File

@@ -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,
};