Update docs

This commit is contained in:
github-actions
2024-10-21 14:27:36 +00:00
parent 63bb51f17d
commit edf6031131
435 changed files with 42062 additions and 23945 deletions

View File

@ -0,0 +1,5 @@
module.exports = {
address: expr => `and(${expr}, shr(96, not(0)))`,
bool: expr => `iszero(iszero(${expr}))`,
bytes: (expr, size) => `and(${expr}, shl(${256 - 8 * size}, not(0)))`,
};

View File

@ -1,6 +1,6 @@
#!/usr/bin/env node
const cp = require('child_process');
// const cp = require('child_process');
const fs = require('fs');
const path = require('path');
const format = require('./format-lines');
@ -23,20 +23,27 @@ function generateFromTemplate(file, template, outputPrefix = '') {
...(version ? [version + ` (${file})`] : []),
`// This file was procedurally generated from ${input}.`,
'',
require(template),
require(template).trimEnd(),
);
fs.writeFileSync(output, content);
cp.execFileSync('prettier', ['--write', output]);
// cp.execFileSync('prettier', ['--write', output]);
}
// Contracts
for (const [file, template] of Object.entries({
'utils/cryptography/MerkleProof.sol': './templates/MerkleProof.js',
'utils/math/SafeCast.sol': './templates/SafeCast.js',
'utils/structs/Checkpoints.sol': './templates/Checkpoints.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/TransientSlot.sol': './templates/TransientSlot.js',
'utils/Arrays.sol': './templates/Arrays.js',
'utils/Packing.sol': './templates/Packing.js',
'mocks/StorageSlotMock.sol': './templates/StorageSlotMock.js',
'mocks/TransientSlotMock.sol': './templates/TransientSlotMock.js',
})) {
generateFromTemplate(file, template, './contracts/');
}
@ -44,6 +51,8 @@ for (const [file, template] of Object.entries({
// Tests
for (const [file, template] of Object.entries({
'utils/structs/Checkpoints.t.sol': './templates/Checkpoints.t.js',
'utils/Packing.t.sol': './templates/Packing.t.js',
'utils/SlotDerivation.t.sol': './templates/SlotDerivation.t.js',
})) {
generateFromTemplate(file, template, './test/');
}

View File

@ -0,0 +1,384 @@
const format = require('../format-lines');
const { capitalize } = require('../../helpers');
const { TYPES } = require('./Arrays.opts');
const header = `\
pragma solidity ^0.8.20;
import {Comparators} from "./Comparators.sol";
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.
*
* IMPORTANT: Consider memory side-effects when using custom comparator functions that access memory in an unsafe way.
*/
function sort(
${type}[] memory array,
function(${type}, ${type}) pure returns (bool) comp
) internal pure returns (${type}[] memory) {
${
type === 'uint256'
? '_quickSort(_begin(array), _end(array), comp);'
: 'sort(_castToUint256Array(array), _castToUint256Comp(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 === 'uint256' ? 'sort(array, Comparators.lt);' : 'sort(_castToUint256Array(array), Comparators.lt);'}
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(uint256, uint256) pure returns (bool) comp) private pure {
unchecked {
if (end - begin < 0x40) return;
// Use first element as pivot
uint256 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(uint256[] memory array) private pure returns (uint256 ptr) {
assembly ("memory-safe") {
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(uint256[] memory array) private pure returns (uint256 ptr) {
unchecked {
return _begin(array) + array.length * 0x20;
}
}
/**
* @dev Load memory word (as a uint256) at location \`ptr\`.
*/
function _mload(uint256 ptr) private pure returns (uint256 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 castArray = type => `\
/// @dev Helper: low level cast ${type} memory array to uint256 memory array
function _castToUint256Array(${type}[] memory input) private pure returns (uint256[] memory output) {
assembly {
output := input
}
}
`;
const castComparator = type => `\
/// @dev Helper: low level cast ${type} comp function to uint256 comp function
function _castToUint256Comp(
function(${type}, ${type}) pure returns (bool) input
) private pure returns (function(uint256, uint256) 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;
assembly ("memory-safe") {
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 {
assembly ("memory-safe") {
sstore(array.slot, len)
}
}
`;
// GENERATE
module.exports = format(
header.trimEnd(),
'library Arrays {',
format(
[].concat(
'using SlotDerivation for bytes32;',
'using StorageSlot for bytes32;',
'',
// sorting, comparator, helpers and internal
sort('uint256'),
TYPES.filter(type => type !== 'uint256').map(sort),
quickSort,
TYPES.filter(type => type !== 'uint256').map(castArray),
TYPES.filter(type => type !== 'uint256').map(castComparator),
// lookup
search,
// unsafe (direct) storage and memory access
TYPES.map(unsafeAccessStorage),
TYPES.map(unsafeAccessMemory),
TYPES.map(unsafeSetLength),
),
).trimEnd(),
'}',
);

View File

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

View File

@ -1,5 +1,5 @@
const format = require('../format-lines');
const { OPTS } = require('./Checkpoints.opts.js');
const { OPTS } = require('./Checkpoints.opts');
// TEMPLATE
const header = `\
@ -17,10 +17,10 @@ import {Math} from "../math/Math.sol";
`;
const errors = `\
/**
* @dev A value was attempted to be inserted on a past checkpoint.
*/
error CheckpointUnorderedInsertion();
/**
* @dev A value was attempted to be inserted on a past checkpoint.
*/
error CheckpointUnorderedInsertion();
`;
const template = opts => `\
@ -37,7 +37,7 @@ struct ${opts.checkpointTypeName} {
* @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.
*/
@ -45,7 +45,7 @@ function push(
${opts.historyTypeName} storage self,
${opts.keyTypeName} key,
${opts.valueTypeName} value
) internal returns (${opts.valueTypeName}, ${opts.valueTypeName}) {
) internal returns (${opts.valueTypeName} oldValue, ${opts.valueTypeName} newValue) {
return _insert(self.${opts.checkpointFieldName}, key, value);
}
@ -108,20 +108,12 @@ function latest(${opts.historyTypeName} storage self) internal view returns (${o
* @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}
)
{
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);
${opts.checkpointTypeName} storage ckpt = _unsafeAccess(self.${opts.checkpointFieldName}, pos - 1);
return (true, ckpt.${opts.keyFieldName}, ckpt.${opts.valueFieldName});
}
}
@ -148,25 +140,26 @@ function _insert(
${opts.checkpointTypeName}[] storage self,
${opts.keyTypeName} key,
${opts.valueTypeName} value
) private returns (${opts.valueTypeName}, ${opts.valueTypeName}) {
) private returns (${opts.valueTypeName} oldValue, ${opts.valueTypeName} newValue) {
uint256 pos = self.length;
if (pos > 0) {
// Copying to memory is important here.
${opts.checkpointTypeName} memory last = _unsafeAccess(self, pos - 1);
${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(last.${opts.keyFieldName} > key) {
if (lastKey > key) {
revert CheckpointUnorderedInsertion();
}
// Update or push new checkpoint
if (last.${opts.keyFieldName} == key) {
_unsafeAccess(self, pos - 1).${opts.valueFieldName} = value;
if (lastKey == key) {
last.${opts.valueFieldName} = value;
} else {
self.push(${opts.checkpointTypeName}({${opts.keyFieldName}: key, ${opts.valueFieldName}: value}));
}
return (last.${opts.valueFieldName}, value);
return (lastValue, value);
} else {
self.push(${opts.checkpointTypeName}({${opts.keyFieldName}: key, ${opts.valueFieldName}: value}));
return (0, value);
@ -174,7 +167,7 @@ function _insert(
}
/**
* @dev Return the index of the last (most recent) checkpoint with key lower or equal than the search key, or \`high\`
* @dev Return the index of the first (oldest) checkpoint with key strictly bigger 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\`.
*
@ -198,9 +191,9 @@ function _upperBinaryLookup(
}
/**
* @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\`.
* @dev Return the index of the first (oldest) checkpoint with key 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.
*/
@ -224,11 +217,10 @@ function _lowerBinaryLookup(
/**
* @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)
{
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)
@ -241,7 +233,11 @@ function _unsafeAccess(${opts.checkpointTypeName}[] storage self, uint256 pos)
module.exports = format(
header.trimEnd(),
'library Checkpoints {',
errors,
OPTS.flatMap(opts => template(opts)),
format(
[].concat(
errors,
OPTS.map(opts => template(opts)),
),
).trimEnd(),
'}',
);

View File

@ -7,8 +7,8 @@ const header = `\
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {SafeCast} from "../../../contracts/utils/math/SafeCast.sol";
import {Checkpoints} from "../../../contracts/utils/structs/Checkpoints.sol";
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";
import {Checkpoints} from "@openzeppelin/contracts/utils/structs/Checkpoints.sol";
`;
/* eslint-disable max-len */
@ -22,18 +22,13 @@ 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}) {
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 view {
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);
@ -42,11 +37,7 @@ function _prepareKeys(
}
}
function _assertLatestCheckpoint(
bool exist,
${opts.keyTypeName} key,
${opts.valueTypeName} value
) internal {
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);
@ -54,11 +45,9 @@ function _assertLatestCheckpoint(
}
// tests
function testPush(
${opts.keyTypeName}[] memory keys,
${opts.valueTypeName}[] memory values,
${opts.keyTypeName} pastKey
) public {
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);
@ -71,7 +60,7 @@ function testPush(
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;
if (i > 0 && key == keys[i - 1]) ++duplicates;
// push
_ckpts.push(key, value);
@ -95,14 +84,12 @@ function testPush(
// used to test reverts
function push(${opts.keyTypeName} key, ${opts.valueTypeName} value) external {
_ckpts.push(key, value);
_ckpts.push(key, value);
}
function testLookup(
${opts.keyTypeName}[] memory keys,
${opts.valueTypeName}[] memory values,
${opts.keyTypeName} lookup
) public {
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);
@ -124,7 +111,7 @@ function testLookup(
upper = value;
}
// find the first key that is not smaller than the lookup key
if (key >= lookup && (i == 0 || keys[i-1] < lookup)) {
if (key >= lookup && (i == 0 || keys[i - 1] < lookup)) {
lowerKey = key;
}
if (key == lowerKey) {
@ -142,5 +129,10 @@ function testLookup(
// GENERATE
module.exports = format(
header,
...OPTS.flatMap(opts => [`contract Checkpoints${opts.historyTypeName}Test is Test {`, [template(opts)], '}']),
...OPTS.flatMap(opts => [
`contract Checkpoints${opts.historyTypeName}Test is Test {`,
[template(opts).trimEnd()],
'}',
'',
]),
);

View File

@ -1,12 +1,6 @@
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' },
];
const { TYPES } = require('./EnumerableMap.opts');
/* eslint-disable max-len */
const header = `\
@ -42,6 +36,10 @@ import {EnumerableSet} from "./EnumerableSet.sol";
* - \`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]
* ====
@ -56,7 +54,7 @@ import {EnumerableSet} from "./EnumerableSet.sol";
`;
/* eslint-enable max-len */
const defaultMap = () => `\
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.
@ -80,11 +78,7 @@ struct Bytes32ToBytes32Map {
* 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) {
function set(Bytes32ToBytes32Map storage map, bytes32 key, bytes32 value) internal returns (bool) {
map._values[key] = value;
return map._keys.add(key);
}
@ -123,21 +117,21 @@ function length(Bytes32ToBytes32Map storage map) internal view returns (uint256)
*
* - \`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]);
function at(Bytes32ToBytes32Map storage map, uint256 index) internal view returns (bytes32 key, bytes32 value) {
bytes32 atKey = map._keys.at(index);
return (atKey, map._values[atKey]);
}
/**
* @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)) {
function tryGet(Bytes32ToBytes32Map storage map, bytes32 key) internal view returns (bool exists, bytes32 value) {
bytes32 val = map._values[key];
if (val == bytes32(0)) {
return (contains(map, key), bytes32(0));
} else {
return (true, value);
return (true, val);
}
}
@ -150,7 +144,7 @@ function tryGet(Bytes32ToBytes32Map storage map, bytes32 key) internal view retu
*/
function get(Bytes32ToBytes32Map storage map, bytes32 key) internal view returns (bytes32) {
bytes32 value = map._values[key];
if(value == 0 && !contains(map, key)) {
if (value == 0 && !contains(map, key)) {
revert EnumerableMapNonexistentKey(key);
}
return value;
@ -183,11 +177,7 @@ struct ${name} {
* 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) {
function set(${name} storage map, ${keyType} key, ${valueType} value) internal returns (bool) {
return set(map._inner, ${toBytes32(keyType, 'key')}, ${toBytes32(valueType, 'value')});
}
@ -223,18 +213,18 @@ function length(${name} storage map) internal view returns (uint256) {
*
* - \`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')});
function at(${name} storage map, uint256 index) internal view returns (${keyType} key, ${valueType} value) {
(bytes32 atKey, bytes32 val) = at(map._inner, index);
return (${fromBytes32(keyType, 'atKey')}, ${fromBytes32(valueType, 'val')});
}
/**
* @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')});
function tryGet(${name} storage map, ${keyType} key) internal view returns (bool exists, ${valueType} value) {
(bool success, bytes32 val) = tryGet(map._inner, ${toBytes32(keyType, 'key')});
return (success, ${fromBytes32(valueType, 'val')});
}
/**
@ -260,8 +250,7 @@ function keys(${name} storage map) internal view returns (${keyType}[] memory) {
bytes32[] memory store = keys(map._inner);
${keyType}[] memory result;
/// @solidity memory-safe-assembly
assembly {
assembly ("memory-safe") {
result := store
}
@ -273,11 +262,13 @@ function keys(${name} storage map) internal view returns (${keyType}[] memory) {
module.exports = format(
header.trimEnd(),
'library EnumerableMap {',
[
'using EnumerableSet for EnumerableSet.Bytes32Set;',
'',
defaultMap(),
TYPES.map(details => customMap(details).trimEnd()).join('\n\n'),
],
format(
[].concat(
'using EnumerableSet for EnumerableSet.Bytes32Set;',
'',
defaultMap,
TYPES.map(details => customMap(details)),
),
).trimEnd(),
'}',
);

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

@ -1,11 +1,6 @@
const format = require('../format-lines');
const { fromBytes32, toBytes32 } = require('./conversion');
const TYPES = [
{ name: 'Bytes32Set', type: 'bytes32' },
{ name: 'AddressSet', type: 'address' },
{ name: 'UintSet', type: 'uint256' },
];
const { TYPES } = require('./EnumerableSet.opts');
/* eslint-disable max-len */
const header = `\
@ -48,7 +43,7 @@ pragma solidity ^0.8.20;
`;
/* eslint-enable max-len */
const defaultSet = () => `\
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.
@ -232,8 +227,7 @@ function values(${name} storage set) internal view returns (${type}[] memory) {
bytes32[] memory store = _values(set._inner);
${type}[] memory result;
/// @solidity memory-safe-assembly
assembly {
assembly ("memory-safe") {
result := store
}
@ -245,6 +239,11 @@ function values(${name} storage set) internal view returns (${type}[] memory) {
module.exports = format(
header.trimEnd(),
'library EnumerableSet {',
[defaultSet(), TYPES.map(details => customSet(details).trimEnd()).join('\n\n')],
format(
[].concat(
defaultSet,
TYPES.map(details => customSet(details)),
),
).trimEnd(),
'}',
);

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,189 @@
const format = require('../format-lines');
const { OPTS } = require('./MerkleProof.opts');
const DEFAULT_HASH = 'Hashes.commutativeKeccak256';
const formatArgsSingleLine = (...args) => args.filter(Boolean).join(', ');
const formatArgsMultiline = (...args) => '\n' + format(args.filter(Boolean).join(',\0').split('\0'));
// TEMPLATE
const header = `\
pragma solidity ^0.8.20;
import {Hashes} from "./Hashes.sol";
/**
* @dev These functions deal with verification of Merkle Tree proofs.
*
* The tree and the proofs can be generated using our
* https://github.com/OpenZeppelin/merkle-tree[JavaScript library].
* You will find a quickstart guide in the readme.
*
* WARNING: You should avoid using leaf values that are 64 bytes long prior to
* hashing, or use a hash function other than keccak256 for hashing leaves.
* This is because the concatenation of a sorted pair of internal nodes in
* the Merkle tree could be reinterpreted as a leaf value.
* OpenZeppelin's JavaScript library generates Merkle trees that are safe
* against this attack out of the box.
*
* IMPORTANT: Consider memory side-effects when using custom hashing functions
* that access memory in an unsafe way.
*
* NOTE: This library supports proof verification for merkle trees built using
* custom _commutative_ hashing functions (i.e. \`H(a, b) == H(b, a)\`). Proving
* leaf inclusion in trees built using non-commutative hashing functions requires
* additional logic that is not supported by this library.
*/
`;
const errors = `\
/**
*@dev The multiproof provided is not valid.
*/
error MerkleProofInvalidMultiproof();
`;
/* eslint-disable max-len */
const templateProof = ({ suffix, location, visibility, hash }) => `\
/**
* @dev Returns true if a \`leaf\` can be proved to be a part of a Merkle tree
* defined by \`root\`. For this, a \`proof\` must be provided, containing
* sibling hashes on the branch from the leaf to the root of the tree. Each
* pair of leaves and each pair of pre-images are assumed to be sorted.
*
* This version handles proofs in ${location} with ${hash ? 'a custom' : 'the default'} hashing function.
*/
function verify${suffix}(${(hash ? formatArgsMultiline : formatArgsSingleLine)(
`bytes32[] ${location} proof`,
'bytes32 root',
'bytes32 leaf',
hash && `function(bytes32, bytes32) view returns (bytes32) ${hash}`,
)}) internal ${visibility} returns (bool) {
return processProof${suffix}(proof, leaf${hash ? `, ${hash}` : ''}) == root;
}
/**
* @dev Returns the rebuilt hash obtained by traversing a Merkle tree up
* from \`leaf\` using \`proof\`. A \`proof\` is valid if and only if the rebuilt
* hash matches the root of the tree. When processing the proof, the pairs
* of leaves & pre-images are assumed to be sorted.
*
* This version handles proofs in ${location} with ${hash ? 'a custom' : 'the default'} hashing function.
*/
function processProof${suffix}(${(hash ? formatArgsMultiline : formatArgsSingleLine)(
`bytes32[] ${location} proof`,
'bytes32 leaf',
hash && `function(bytes32, bytes32) view returns (bytes32) ${hash}`,
)}) internal ${visibility} returns (bytes32) {
bytes32 computedHash = leaf;
for (uint256 i = 0; i < proof.length; i++) {
computedHash = ${hash ?? DEFAULT_HASH}(computedHash, proof[i]);
}
return computedHash;
}
`;
const templateMultiProof = ({ suffix, location, visibility, hash }) => `\
/**
* @dev Returns true if the \`leaves\` can be simultaneously proven to be a part of a Merkle tree defined by
* \`root\`, according to \`proof\` and \`proofFlags\` as described in {processMultiProof}.
*
* This version handles multiproofs in ${location} with ${hash ? 'a custom' : 'the default'} hashing function.
*
* CAUTION: Not all Merkle trees admit multiproofs. See {processMultiProof} for details.
*
* NOTE: Consider the case where \`root == proof[0] && leaves.length == 0\` as it will return \`true\`.
* The \`leaves\` must be validated independently. See {processMultiProof${suffix}}.
*/
function multiProofVerify${suffix}(${formatArgsMultiline(
`bytes32[] ${location} proof`,
`bool[] ${location} proofFlags`,
'bytes32 root',
`bytes32[] memory leaves`,
hash && `function(bytes32, bytes32) view returns (bytes32) ${hash}`,
)}) internal ${visibility} returns (bool) {
return processMultiProof${suffix}(proof, proofFlags, leaves${hash ? `, ${hash}` : ''}) == root;
}
/**
* @dev Returns the root of a tree reconstructed from \`leaves\` and sibling nodes in \`proof\`. The reconstruction
* proceeds by incrementally reconstructing all inner nodes by combining a leaf/inner node with either another
* leaf/inner node or a proof sibling node, depending on whether each \`proofFlags\` item is true or false
* respectively.
*
* This version handles multiproofs in ${location} with ${hash ? 'a custom' : 'the default'} hashing function.
*
* CAUTION: Not all Merkle trees admit multiproofs. To use multiproofs, it is sufficient to ensure that: 1) the tree
* is complete (but not necessarily perfect), 2) the leaves to be proven are in the opposite order they are in the
* tree (i.e., as seen from right to left starting at the deepest layer and continuing at the next layer).
*
* NOTE: The _empty set_ (i.e. the case where \`proof.length == 1 && leaves.length == 0\`) is considered a no-op,
* and therefore a valid multiproof (i.e. it returns \`proof[0]\`). Consider disallowing this case if you're not
* validating the leaves elsewhere.
*/
function processMultiProof${suffix}(${formatArgsMultiline(
`bytes32[] ${location} proof`,
`bool[] ${location} proofFlags`,
`bytes32[] memory leaves`,
hash && `function(bytes32, bytes32) view returns (bytes32) ${hash}`,
)}) internal ${visibility} returns (bytes32 merkleRoot) {
// This function rebuilds the root hash by traversing the tree up from the leaves. The root is rebuilt by
// consuming and producing values on a queue. The queue starts with the \`leaves\` array, then goes onto the
// \`hashes\` array. At the end of the process, the last hash in the \`hashes\` array should contain the root of
// the Merkle tree.
uint256 leavesLen = leaves.length;
uint256 proofFlagsLen = proofFlags.length;
// Check proof validity.
if (leavesLen + proof.length != proofFlagsLen + 1) {
revert MerkleProofInvalidMultiproof();
}
// The xxxPos values are "pointers" to the next value to consume in each array. All accesses are done using
// \`xxx[xxxPos++]\`, which return the current value and increment the pointer, thus mimicking a queue's "pop".
bytes32[] memory hashes = new bytes32[](proofFlagsLen);
uint256 leafPos = 0;
uint256 hashPos = 0;
uint256 proofPos = 0;
// At each step, we compute the next hash using two values:
// - a value from the "main queue". If not all leaves have been consumed, we get the next leaf, otherwise we
// get the next hash.
// - depending on the flag, either another value from the "main queue" (merging branches) or an element from the
// \`proof\` array.
for (uint256 i = 0; i < proofFlagsLen; i++) {
bytes32 a = leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++];
bytes32 b = proofFlags[i]
? (leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++])
: proof[proofPos++];
hashes[i] = ${hash ?? DEFAULT_HASH}(a, b);
}
if (proofFlagsLen > 0) {
if (proofPos != proof.length) {
revert MerkleProofInvalidMultiproof();
}
unchecked {
return hashes[proofFlagsLen - 1];
}
} else if (leavesLen > 0) {
return leaves[0];
} else {
return proof[0];
}
}
`;
/* eslint-enable max-len */
// GENERATE
module.exports = format(
header.trimEnd(),
'library MerkleProof {',
format(
[].concat(
errors,
OPTS.flatMap(opts => templateProof(opts)),
OPTS.flatMap(opts => templateMultiProof(opts)),
),
).trimEnd(),
'}',
);

View File

@ -0,0 +1,11 @@
const { product } = require('../../helpers');
const OPTS = product(
[
{ suffix: '', location: 'memory' },
{ suffix: 'Calldata', location: 'calldata' },
],
[{ visibility: 'pure' }, { visibility: 'view', hash: 'hasher' }],
).map(objs => Object.assign({}, ...objs));
module.exports = { OPTS };

View File

@ -0,0 +1,92 @@
const format = require('../format-lines');
const sanitize = require('../helpers/sanitize');
const { product } = require('../../helpers');
const { SIZES } = require('./Packing.opts');
// TEMPLATE
const header = `\
pragma solidity ^0.8.20;
/**
* @dev Helper library packing and unpacking multiple values into bytesXX.
*
* Example usage:
*
* \`\`\`solidity
* library MyPacker {
* type MyType is bytes32;
*
* function _pack(address account, bytes4 selector, uint64 period) external pure returns (MyType) {
* bytes12 subpack = Packing.pack_4_8(selector, bytes8(period));
* bytes32 pack = Packing.pack_20_12(bytes20(account), subpack);
* return MyType.wrap(pack);
* }
*
* function _unpack(MyType self) external pure returns (address, bytes4, uint64) {
* bytes32 pack = MyType.unwrap(self);
* return (
* address(Packing.extract_32_20(pack, 0)),
* Packing.extract_32_4(pack, 20),
* uint64(Packing.extract_32_8(pack, 24))
* );
* }
* }
* \`\`\`
*
* _Available since v5.1._
*/
// solhint-disable func-name-mixedcase
`;
const errors = `\
error OutOfRangeAccess();
`;
const pack = (left, right) => `\
function pack_${left}_${right}(bytes${left} left, bytes${right} right) internal pure returns (bytes${
left + right
} result) {
assembly ("memory-safe") {
left := ${sanitize.bytes('left', left)}
right := ${sanitize.bytes('right', right)}
result := or(left, shr(${8 * left}, right))
}
}
`;
const extract = (outer, inner) => `\
function extract_${outer}_${inner}(bytes${outer} self, uint8 offset) internal pure returns (bytes${inner} result) {
if (offset > ${outer - inner}) revert OutOfRangeAccess();
assembly ("memory-safe") {
result := ${sanitize.bytes('shl(mul(8, offset), self)', inner)}
}
}
`;
const replace = (outer, inner) => `\
function replace_${outer}_${inner}(bytes${outer} self, bytes${inner} value, uint8 offset) internal pure returns (bytes${outer} result) {
bytes${inner} oldValue = extract_${outer}_${inner}(self, offset);
assembly ("memory-safe") {
value := ${sanitize.bytes('value', inner)}
result := xor(self, shr(mul(8, offset), xor(oldValue, value)))
}
}
`;
// GENERATE
module.exports = format(
header.trimEnd(),
'library Packing {',
format(
[].concat(
errors,
product(SIZES, SIZES)
.filter(([left, right]) => SIZES.includes(left + right))
.map(([left, right]) => pack(left, right)),
product(SIZES, SIZES)
.filter(([outer, inner]) => outer > inner)
.flatMap(([outer, inner]) => [extract(outer, inner), replace(outer, inner)]),
),
).trimEnd(),
'}',
);

View File

@ -0,0 +1,3 @@
module.exports = {
SIZES: [1, 2, 4, 6, 8, 12, 16, 20, 24, 28, 32],
};

View File

@ -0,0 +1,48 @@
const format = require('../format-lines');
const { product } = require('../../helpers');
const { SIZES } = require('./Packing.opts');
// TEMPLATE
const header = `\
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {Packing} from "@openzeppelin/contracts/utils/Packing.sol";
`;
const testPack = (left, right) => `\
function testPack(bytes${left} left, bytes${right} right) external {
assertEq(left, Packing.pack_${left}_${right}(left, right).extract_${left + right}_${left}(0));
assertEq(right, Packing.pack_${left}_${right}(left, right).extract_${left + right}_${right}(${left}));
}
`;
const testReplace = (outer, inner) => `\
function testReplace(bytes${outer} container, bytes${inner} newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, ${outer - inner}));
bytes${inner} oldValue = container.extract_${outer}_${inner}(offset);
assertEq(newValue, container.replace_${outer}_${inner}(newValue, offset).extract_${outer}_${inner}(offset));
assertEq(container, container.replace_${outer}_${inner}(newValue, offset).replace_${outer}_${inner}(oldValue, offset));
}
`;
// GENERATE
module.exports = format(
header,
'contract PackingTest is Test {',
format(
[].concat(
'using Packing for *;',
'',
product(SIZES, SIZES)
.filter(([left, right]) => SIZES.includes(left + right))
.map(([left, right]) => testPack(left, right)),
product(SIZES, SIZES)
.filter(([outer, inner]) => outer > inner)
.map(([outer, inner]) => testReplace(outer, inner)),
),
).trimEnd(),
'}',
);

View File

@ -7,7 +7,7 @@ const header = `\
pragma solidity ^0.8.20;
/**
* @dev Wrappers over Solidity's uintXX/intXX casting operators with added overflow
* @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
@ -21,25 +21,25 @@ pragma solidity ^0.8.20;
`;
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);
/**
* @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 => `\
@ -55,7 +55,7 @@ const toUintDownCast = length => `\
*/
function toUint${length}(uint256 value) internal pure returns (uint${length}) {
if (value > type(uint${length}).max) {
revert SafeCastOverflowedUintDowncast(${length}, value);
revert SafeCastOverflowedUintDowncast(${length}, value);
}
return uint${length}(value);
}
@ -77,7 +77,7 @@ const toIntDownCast = length => `\
function toInt${length}(int256 value) internal pure returns (int${length} downcasted) {
downcasted = int${length}(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(${length}, value);
revert SafeCastOverflowedIntDowncast(${length}, value);
}
}
`;
@ -94,7 +94,7 @@ const toInt = 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);
revert SafeCastOverflowedUintToInt(value);
}
return int${length}(value);
}
@ -110,17 +110,29 @@ const toUint = length => `\
*/
function toUint${length}(int${length} value) internal pure returns (uint${length}) {
if (value < 0) {
revert SafeCastOverflowedIntToUint(value);
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) {
assembly ("memory-safe") {
u := iszero(iszero(b))
}
}
`;
// GENERATE
module.exports = format(
header.trimEnd(),
'library SafeCast {',
errors,
[...LENGTHS.map(toUintDownCast), toUint(256), ...LENGTHS.map(toIntDownCast), toInt(256)],
format(
[].concat(errors, LENGTHS.map(toUintDownCast), toUint(256), LENGTHS.map(toIntDownCast), toInt(256), boolToUint),
).trimEnd(),
'}',
);

View File

@ -0,0 +1,15 @@
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) }));
Object.assign(TYPES, Object.fromEntries(TYPES.map(entry => [entry.type, entry])));
module.exports = { TYPES };

View File

@ -0,0 +1,119 @@
const format = require('../format-lines');
const sanitize = require('../helpers/sanitize');
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.
*
* _Available since v5.1._
*/
`;
const namespace = `\
/**
* @dev Derive an ERC-7201 slot from a string (namespace).
*/
function erc7201Slot(string memory namespace) internal pure returns (bytes32 slot) {
assembly ("memory-safe") {
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) {
assembly ("memory-safe") {
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) {
assembly ("memory-safe") {
mstore(0x00, ${(sanitize[type] ?? (x => x))('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) {
assembly ("memory-safe") {
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 {',
format(
[].concat(
namespace,
array,
TYPES.map(type => (type.isValueType ? mapping(type) : mapping2(type))),
),
).trimEnd(),
'}',
);

View File

@ -0,0 +1,127 @@
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 {SymTest} from "halmos-cheatcodes/SymTest.sol";
import {SlotDerivation} from "@openzeppelin/contracts/utils/SlotDerivation.sol";
`;
const array = `\
bytes[] private _array;
function symbolicDeriveArray(uint256 length, uint256 offset) public {
vm.assume(length > 0);
vm.assume(offset < length);
_assertDeriveArray(length, offset);
}
function testDeriveArray(uint256 length, uint256 offset) public {
length = bound(length, 1, type(uint256).max);
offset = bound(offset, 0, length - 1);
_assertDeriveArray(length, offset);
}
function _assertDeriveArray(uint256 length, uint256 offset) public {
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 }) => `\
mapping(${type} => bytes) private _${type}Mapping;
function testSymbolicDeriveMapping${name}(${type} 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);
}
`;
const mappingDirty = ({ type, name }) => `\
function testSymbolicDeriveMapping${name}Dirty(bytes32 dirtyKey) public {
${type} key;
assembly {
key := dirtyKey
}
// run the "normal" test using a potentially dirty value
testSymbolicDeriveMapping${name}(key);
}
`;
const boundedMapping = ({ type, name }) => `\
mapping(${type} => bytes) private _${type}Mapping;
function testDeriveMapping${name}(${type} memory key) public {
_assertDeriveMapping${name}(key);
}
function symbolicDeriveMapping${name}() public {
_assertDeriveMapping${name}(svm.create${name}(256, "DeriveMapping${name}Input"));
}
function _assertDeriveMapping${name}(${type} memory key) internal {
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,
'contract SlotDerivationTest is Test, SymTest {',
format(
[].concat(
'using SlotDerivation for bytes32;',
'',
array,
TYPES.flatMap(type =>
[].concat(
type,
(type.variants ?? []).map(variant => ({
type: variant,
name: capitalize(variant),
isValueType: type.isValueType,
})),
),
).map(type => (type.isValueType ? mapping(type) : boundedMapping(type))),
mappingDirty(TYPES.bool),
mappingDirty(TYPES.address),
),
).trimEnd(),
'}',
);

View File

@ -1,14 +1,5 @@
const format = require('../format-lines');
const { capitalize } = require('../../helpers');
const TYPES = [
{ type: 'address', isValueType: true },
{ type: 'bool', isValueType: true, name: 'Boolean' },
{ type: 'bytes32', isValueType: true },
{ type: 'uint256', isValueType: true },
{ type: 'string', isValueType: false },
{ type: 'bytes', isValueType: false },
].map(type => Object.assign(type, { struct: (type.name ?? capitalize(type.type)) + 'Slot' }));
const { TYPES } = require('./Slot.opts');
const header = `\
pragma solidity ^0.8.20;
@ -21,9 +12,10 @@ pragma solidity ^0.8.20;
*
* 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:
* 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) {
@ -36,36 +28,38 @@ pragma solidity ^0.8.20;
* }
* }
* \`\`\`
*
* TIP: Consider using this library along with {SlotDerivation}.
*/
`;
const struct = type => `\
struct ${type.struct} {
${type.type} value;
const struct = ({ type, name }) => `\
struct ${name}Slot {
${type} value;
}
`;
const get = type => `\
const get = ({ name }) => `\
/**
* @dev Returns an \`${type.struct}\` with member \`value\` located at \`slot\`.
* @dev Returns ${
name.toLowerCase().startsWith('a') ? 'an' : 'a'
} \`${name}Slot\` 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
}
function get${name}Slot(bytes32 slot) internal pure returns (${name}Slot storage r) {
assembly ("memory-safe") {
r.slot := slot
}
}
`;
const getStorage = type => `\
const getStorage = ({ type, name }) => `\
/**
* @dev Returns an \`${type.struct}\` representation of the ${type.type} storage pointer \`store\`.
* @dev Returns an \`${name}Slot\` representation of the ${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
}
function get${name}Slot(${type} storage store) internal pure returns (${name}Slot storage r) {
assembly ("memory-safe") {
r.slot := store.slot
}
}
`;
@ -73,6 +67,11 @@ function get${type.struct}(${type.type} storage store) internal pure returns (${
module.exports = format(
header.trimEnd(),
'library StorageSlot {',
[...TYPES.map(struct), ...TYPES.flatMap(type => [get(type), type.isValueType ? '' : getStorage(type)])],
format(
[].concat(
TYPES.map(type => struct(type)),
TYPES.flatMap(type => [get(type), !type.isValueType && getStorage(type)].filter(Boolean)),
),
).trimEnd(),
'}',
);

View File

@ -0,0 +1,57 @@
const format = require('../format-lines');
const { TYPES } = require('./Slot.opts');
const header = `\
pragma solidity ^0.8.20;
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;
}
`;
// GENERATE
module.exports = format(
header,
'contract StorageSlotMock is Multicall {',
format(
[].concat(
'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)),
),
).trimEnd(),
'}',
);

View File

@ -0,0 +1,80 @@
const format = require('../format-lines');
const { TYPES } = require('./Slot.opts');
const header = `\
pragma solidity ^0.8.24;
/**
* @dev Library for reading and writing value-types to specific transient storage slots.
*
* Transient slots are often used to store temporary values that are removed after the current transaction.
* This library helps with reading and writing to such slots without the need for inline assembly.
*
* * Example reading and writing values using transient storage:
* \`\`\`solidity
* contract Lock {
* using TransientSlot for *;
*
* // 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 udvt = ({ type, name }) => `\
/**
* @dev UDVT that represent a slot holding a ${type}.
*/
type ${name}Slot is bytes32;
/**
* @dev Cast an arbitrary slot to a ${name}Slot.
*/
function as${name}(bytes32 slot) internal pure returns (${name}Slot) {
return ${name}Slot.wrap(slot);
}
`;
const transient = ({ type, name }) => `\
/**
* @dev Load the value held at location \`slot\` in transient storage.
*/
function tload(${name}Slot slot) internal view returns (${type} value) {
assembly ("memory-safe") {
value := tload(slot)
}
}
/**
* @dev Store \`value\` at location \`slot\` in transient storage.
*/
function tstore(${name}Slot slot, ${type} value) internal {
assembly ("memory-safe") {
tstore(slot, value)
}
}
`;
// GENERATE
module.exports = format(
header.trimEnd(),
'library TransientSlot {',
format(
[].concat(
TYPES.filter(type => type.isValueType).map(type => udvt(type)),
TYPES.filter(type => type.isValueType).map(type => transient(type)),
),
).trimEnd(),
'}',
);

View File

@ -0,0 +1,35 @@
const format = require('../format-lines');
const { TYPES } = require('./Slot.opts');
const header = `\
pragma solidity ^0.8.24;
import {Multicall} from "../utils/Multicall.sol";
import {TransientSlot} from "../utils/TransientSlot.sol";
`;
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,
'contract TransientSlotMock is Multicall {',
format(
[].concat(
'using TransientSlot for *;',
'',
TYPES.filter(type => type.isValueType).map(type => transient(type)),
),
).trimEnd(),
'}',
);