Update docs
This commit is contained in:
5
scripts/generate/helpers/sanitize.js
Normal file
5
scripts/generate/helpers/sanitize.js
Normal 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)))`,
|
||||
};
|
||||
@ -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/');
|
||||
}
|
||||
|
||||
384
scripts/generate/templates/Arrays.js
Normal file
384
scripts/generate/templates/Arrays.js
Normal 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(),
|
||||
'}',
|
||||
);
|
||||
3
scripts/generate/templates/Arrays.opts.js
Normal file
3
scripts/generate/templates/Arrays.opts.js
Normal file
@ -0,0 +1,3 @@
|
||||
const TYPES = ['address', 'bytes32', 'uint256'];
|
||||
|
||||
module.exports = { TYPES };
|
||||
@ -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(),
|
||||
'}',
|
||||
);
|
||||
|
||||
@ -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()],
|
||||
'}',
|
||||
'',
|
||||
]),
|
||||
);
|
||||
|
||||
@ -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(),
|
||||
'}',
|
||||
);
|
||||
|
||||
19
scripts/generate/templates/EnumerableMap.opts.js
Normal file
19
scripts/generate/templates/EnumerableMap.opts.js
Normal 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,
|
||||
};
|
||||
@ -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(),
|
||||
'}',
|
||||
);
|
||||
|
||||
12
scripts/generate/templates/EnumerableSet.opts.js
Normal file
12
scripts/generate/templates/EnumerableSet.opts.js
Normal 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 };
|
||||
189
scripts/generate/templates/MerkleProof.js
Normal file
189
scripts/generate/templates/MerkleProof.js
Normal 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(),
|
||||
'}',
|
||||
);
|
||||
11
scripts/generate/templates/MerkleProof.opts.js
Normal file
11
scripts/generate/templates/MerkleProof.opts.js
Normal 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 };
|
||||
92
scripts/generate/templates/Packing.js
Normal file
92
scripts/generate/templates/Packing.js
Normal 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(),
|
||||
'}',
|
||||
);
|
||||
3
scripts/generate/templates/Packing.opts.js
Normal file
3
scripts/generate/templates/Packing.opts.js
Normal file
@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
SIZES: [1, 2, 4, 6, 8, 12, 16, 20, 24, 28, 32],
|
||||
};
|
||||
48
scripts/generate/templates/Packing.t.js
Normal file
48
scripts/generate/templates/Packing.t.js
Normal 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(),
|
||||
'}',
|
||||
);
|
||||
@ -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(),
|
||||
'}',
|
||||
);
|
||||
|
||||
15
scripts/generate/templates/Slot.opts.js
Normal file
15
scripts/generate/templates/Slot.opts.js
Normal 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 };
|
||||
119
scripts/generate/templates/SlotDerivation.js
Normal file
119
scripts/generate/templates/SlotDerivation.js
Normal 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(),
|
||||
'}',
|
||||
);
|
||||
127
scripts/generate/templates/SlotDerivation.t.js
Normal file
127
scripts/generate/templates/SlotDerivation.t.js
Normal 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(),
|
||||
'}',
|
||||
);
|
||||
@ -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(),
|
||||
'}',
|
||||
);
|
||||
|
||||
57
scripts/generate/templates/StorageSlotMock.js
Normal file
57
scripts/generate/templates/StorageSlotMock.js
Normal 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(),
|
||||
'}',
|
||||
);
|
||||
80
scripts/generate/templates/TransientSlot.js
Normal file
80
scripts/generate/templates/TransientSlot.js
Normal 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(),
|
||||
'}',
|
||||
);
|
||||
35
scripts/generate/templates/TransientSlotMock.js
Normal file
35
scripts/generate/templates/TransientSlotMock.js
Normal 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(),
|
||||
'}',
|
||||
);
|
||||
Reference in New Issue
Block a user