Add slot derivation library (#4975)

pull/4983/head
Hadrien Croubois 11 months ago committed by GitHub
parent 5e3ba29b08
commit cb2aaaa04a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      .changeset/gorgeous-badgers-vanish.md
  2. 8
      contracts/mocks/StorageSlotMock.sol
  3. 26
      contracts/utils/Arrays.sol
  4. 3
      contracts/utils/README.adoc
  5. 161
      contracts/utils/SlotDerivation.sol
  6. 17
      contracts/utils/StorageSlot.sol
  7. 72
      docs/modules/ROOT/pages/utilities.adoc
  8. 2
      scripts/generate/run.js
  9. 18
      scripts/generate/templates/Arrays.js
  10. 13
      scripts/generate/templates/Slot.opts.js
  11. 116
      scripts/generate/templates/SlotDerivation.js
  12. 73
      scripts/generate/templates/SlotDerivation.t.js
  13. 35
      scripts/generate/templates/StorageSlot.js
  14. 2
      test/helpers/random.js
  15. 20
      test/helpers/storage.js
  16. 203
      test/utils/SlotDerivation.t.sol
  17. 58
      test/utils/SlotDerivation.test.js
  18. 8
      test/utils/StorageSlot.test.js

@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---
`SlotDerivation`: Add a library of methods for derivating common storage slots.

@ -23,6 +23,10 @@ contract StorageSlotMock {
slot.getUint256Slot().value = value;
}
function setInt256Slot(bytes32 slot, int256 value) public {
slot.getInt256Slot().value = value;
}
function getBooleanSlot(bytes32 slot) public view returns (bool) {
return slot.getBooleanSlot().value;
}
@ -39,6 +43,10 @@ contract StorageSlotMock {
return slot.getUint256Slot().value;
}
function getInt256Slot(bytes32 slot) public view returns (int256) {
return slot.getInt256Slot().value;
}
mapping(uint256 key => string) public stringMap;
function setStringSlot(bytes32 slot, string calldata value) public {

@ -4,6 +4,7 @@
pragma solidity ^0.8.20;
import {SlotDerivation} from "./SlotDerivation.sol";
import {StorageSlot} from "./StorageSlot.sol";
import {Math} from "./math/Math.sol";
@ -11,6 +12,7 @@ import {Math} from "./math/Math.sol";
* @dev Collection of functions related to array types.
*/
library Arrays {
using SlotDerivation for bytes32;
using StorageSlot for bytes32;
/**
@ -379,15 +381,11 @@ library Arrays {
*/
function unsafeAccess(address[] storage arr, uint256 pos) internal pure returns (StorageSlot.AddressSlot storage) {
bytes32 slot;
// We use assembly to calculate the storage slot of the element at index `pos` of the dynamic array `arr`
// following https://docs.soliditylang.org/en/v0.8.20/internals/layout_in_storage.html#mappings-and-dynamic-arrays.
/// @solidity memory-safe-assembly
assembly {
mstore(0, arr.slot)
slot := add(keccak256(0, 0x20), pos)
slot := arr.slot
}
return slot.getAddressSlot();
return slot.deriveArray().offset(pos).getAddressSlot();
}
/**
@ -397,15 +395,11 @@ library Arrays {
*/
function unsafeAccess(bytes32[] storage arr, uint256 pos) internal pure returns (StorageSlot.Bytes32Slot storage) {
bytes32 slot;
// We use assembly to calculate the storage slot of the element at index `pos` of the dynamic array `arr`
// following https://docs.soliditylang.org/en/v0.8.20/internals/layout_in_storage.html#mappings-and-dynamic-arrays.
/// @solidity memory-safe-assembly
assembly {
mstore(0, arr.slot)
slot := add(keccak256(0, 0x20), pos)
slot := arr.slot
}
return slot.getBytes32Slot();
return slot.deriveArray().offset(pos).getBytes32Slot();
}
/**
@ -415,15 +409,11 @@ library Arrays {
*/
function unsafeAccess(uint256[] storage arr, uint256 pos) internal pure returns (StorageSlot.Uint256Slot storage) {
bytes32 slot;
// We use assembly to calculate the storage slot of the element at index `pos` of the dynamic array `arr`
// following https://docs.soliditylang.org/en/v0.8.20/internals/layout_in_storage.html#mappings-and-dynamic-arrays.
/// @solidity memory-safe-assembly
assembly {
mstore(0, arr.slot)
slot := add(keccak256(0, 0x20), pos)
slot := arr.slot
}
return slot.getUint256Slot();
return slot.deriveArray().offset(pos).getUint256Slot();
}
/**

@ -28,6 +28,7 @@ Miscellaneous contracts and libraries containing utility functions you can use t
* {Base64}: On-chain base64 and base64URL encoding according to https://datatracker.ietf.org/doc/html/rfc4648[RFC-4648].
* {Strings}: Common operations for strings formatting.
* {ShortString}: Library to encode (and decode) short strings into (or from) a single bytes32 slot for optimizing costs. Short strings are limited to 31 characters.
* {SlotDerivation}: Methods for deriving storage slot from ERC-7201 namespaces as well as from constructions such as mapping and arrays.
* {StorageSlot}: Methods for accessing specific storage slots formatted as common primitive types.
* {Multicall}: Abstract contract with an utility to allow batching together multiple calls in a single transaction. Useful for allowing EOAs to perform multiple operations at once.
* {Context}: An utility for abstracting the sender and calldata in the current execution context.
@ -108,6 +109,8 @@ Ethereum contracts have no native concept of an interface, so applications must
{{ShortStrings}}
{{SlotDerivation}}
{{StorageSlot}}
{{Multicall}}

@ -0,0 +1,161 @@
// SPDX-License-Identifier: MIT
// This file was procedurally generated from scripts/generate/templates/SlotDerivation.js.
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.
*/
library SlotDerivation {
/**
* @dev Derive an ERC-7201 slot from a string (namespace).
*/
function erc7201Slot(string memory namespace) internal pure returns (bytes32 slot) {
/// @solidity memory-safe-assembly
assembly {
mstore(0x00, sub(keccak256(add(namespace, 0x20), mload(namespace)), 1))
slot := and(keccak256(0x00, 0x20), not(0xff))
}
}
/**
* @dev Add an offset to a slot to get the n-th element of a structure or an array.
*/
function offset(bytes32 slot, uint256 pos) internal pure returns (bytes32 result) {
unchecked {
return bytes32(uint256(slot) + pos);
}
}
/**
* @dev Derive the location of the first element in an array from the slot where the length is stored.
*/
function deriveArray(bytes32 slot) internal pure returns (bytes32 result) {
/// @solidity memory-safe-assembly
assembly {
mstore(0x00, slot)
result := keccak256(0x00, 0x20)
}
}
/**
* @dev Derive the location of a mapping element from the key.
*/
function deriveMapping(bytes32 slot, address key) internal pure returns (bytes32 result) {
/// @solidity memory-safe-assembly
assembly {
mstore(0x00, key)
mstore(0x20, slot)
result := keccak256(0x00, 0x40)
}
}
/**
* @dev Derive the location of a mapping element from the key.
*/
function deriveMapping(bytes32 slot, bool key) internal pure returns (bytes32 result) {
/// @solidity memory-safe-assembly
assembly {
mstore(0x00, key)
mstore(0x20, slot)
result := keccak256(0x00, 0x40)
}
}
/**
* @dev Derive the location of a mapping element from the key.
*/
function deriveMapping(bytes32 slot, bytes32 key) internal pure returns (bytes32 result) {
/// @solidity memory-safe-assembly
assembly {
mstore(0x00, key)
mstore(0x20, slot)
result := keccak256(0x00, 0x40)
}
}
/**
* @dev Derive the location of a mapping element from the key.
*/
function deriveMapping(bytes32 slot, uint256 key) internal pure returns (bytes32 result) {
/// @solidity memory-safe-assembly
assembly {
mstore(0x00, key)
mstore(0x20, slot)
result := keccak256(0x00, 0x40)
}
}
/**
* @dev Derive the location of a mapping element from the key.
*/
function deriveMapping(bytes32 slot, int256 key) internal pure returns (bytes32 result) {
/// @solidity memory-safe-assembly
assembly {
mstore(0x00, key)
mstore(0x20, slot)
result := keccak256(0x00, 0x40)
}
}
/**
* @dev Derive the location of a mapping element from the key.
*/
function deriveMapping(bytes32 slot, string memory key) internal pure returns (bytes32 result) {
/// @solidity memory-safe-assembly
assembly {
let length := mload(key)
let begin := add(key, 0x20)
let end := add(begin, length)
let cache := mload(end)
mstore(end, slot)
result := keccak256(begin, add(length, 0x20))
mstore(end, cache)
}
}
/**
* @dev Derive the location of a mapping element from the key.
*/
function deriveMapping(bytes32 slot, bytes memory key) internal pure returns (bytes32 result) {
/// @solidity memory-safe-assembly
assembly {
let length := mload(key)
let begin := add(key, 0x20)
let end := add(begin, length)
let cache := mload(end)
mstore(end, slot)
result := keccak256(begin, add(length, 0x20))
mstore(end, cache)
}
}
}

@ -15,6 +15,7 @@ pragma solidity ^0.8.20;
* 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) {
@ -27,6 +28,8 @@ pragma solidity ^0.8.20;
* }
* }
* ```
*
* TIP: Consider using this library along with {SlotDerivation}.
*/
library StorageSlot {
struct AddressSlot {
@ -45,6 +48,10 @@ library StorageSlot {
uint256 value;
}
struct Int256Slot {
int256 value;
}
struct StringSlot {
string value;
}
@ -93,6 +100,16 @@ library StorageSlot {
}
}
/**
* @dev Returns an `Int256Slot` with member `value` located at `slot`.
*/
function getInt256Slot(bytes32 slot) internal pure returns (Int256Slot storage r) {
/// @solidity memory-safe-assembly
assembly {
r.slot := slot
}
}
/**
* @dev Returns an `StringSlot` with member `value` located at `slot`.
*/

@ -1,6 +1,7 @@
= Utilities
The OpenZeppelin Contracts provide a ton of useful utilities that you can use in your project. Here are some of the more popular ones.
The OpenZeppelin Contracts provide a ton of useful utilities that you can use in your project. For a complete list, check out the xref:api:utils.adoc[API Reference].
Here are some of the more popular ones.
[[cryptography]]
== Cryptography
@ -9,7 +10,7 @@ The OpenZeppelin Contracts provide a ton of useful utilities that you can use in
xref:api:utils.adoc#ECDSA[`ECDSA`] provides functions for recovering and managing Ethereum account ECDSA signatures. These are often generated via https://web3js.readthedocs.io/en/v1.7.3/web3-eth.html#sign[`web3.eth.sign`], and are a 65 byte array (of type `bytes` in Solidity) arranged the following way: `[[v (1)], [r (32)], [s (32)]]`.
The data signer can be recovered with xref:api:utils.adoc#ECDSA-recover-bytes32-bytes-[`ECDSA.recover`], and its address compared to verify the signature. Most wallets will hash the data to sign and add the prefix '\x19Ethereum Signed Message:\n', so when attempting to recover the signer of an Ethereum signed message hash, you'll want to use xref:api:utils.adoc#MessageHashUtils-toEthSignedMessageHash-bytes32-[`toEthSignedMessageHash`].
The data signer can be recovered with xref:api:utils.adoc#ECDSA-recover-bytes32-bytes-[`ECDSA.recover`], and its address compared to verify the signature. Most wallets will hash the data to sign and add the prefix `\x19Ethereum Signed Message:\n`, so when attempting to recover the signer of an Ethereum signed message hash, you'll want to use xref:api:utils.adoc#MessageHashUtils-toEthSignedMessageHash-bytes32-[`toEthSignedMessageHash`].
[source,solidity]
----
@ -27,12 +28,18 @@ WARNING: Getting signature verification right is not trivial: make sure you full
=== Verifying Merkle Proofs
Developers can build a Merkle Tree off-chain, which allows for verifying that an element (leaf) is part of a set by using a Merkle Proof. This technique is widely used for creating whitelists (e.g. for airdrops) and other advanced use cases.
TIP: OpenZeppelin Contracts provides a https://github.com/OpenZeppelin/merkle-tree[JavaScript library] for building trees off-chain and generating proofs.
xref:api:utils.adoc#MerkleProof[`MerkleProof`] provides:
* xref:api:utils.adoc#MerkleProof-verify-bytes32---bytes32-bytes32-[`verify`] - can prove that some value is part of a https://en.wikipedia.org/wiki/Merkle_tree[Merkle tree].
* xref:api:utils.adoc#MerkleProof-multiProofVerify-bytes32-bytes32---bytes32---bool---[`multiProofVerify`] - can prove multiple values are part of a Merkle tree.
For an on-chain Merkle Tree, see the xref:api:utils.adoc#MerkleTree[`MerkleTree`] library.
[[introspection]]
== Introspection
@ -98,6 +105,8 @@ contract MyContract {
Easy!
TIP: While working with different data types that might require casting, you can use xref:api:utils.adoc#SafeCast[`SafeCast`] for type casting with added overflow checks.
[[structures]]
== Structures
@ -108,12 +117,71 @@ Some use cases require more powerful data structures than arrays and mappings of
- xref:api:utils.adoc#DoubleEndedQueue[`DoubleEndedQueue`]: Store items in a queue with `pop()` and `queue()` constant time operations.
- xref:api:utils.adoc#EnumerableSet[`EnumerableSet`]: A https://en.wikipedia.org/wiki/Set_(abstract_data_type)[set] with enumeration capabilities.
- xref:api:utils.adoc#EnumerableMap[`EnumerableMap`]: A `mapping` variant with enumeration capabilities.
- xref:api:utils.adoc#MerkleTree[`MerkleTree`]: An on-chain https://wikipedia.org/wiki/Merkle_Tree[Merkle Tree] with helper functions.
The `Enumerable*` structures are similar to mappings in that they store and remove elements in constant time and don't allow for repeated entries, but they also support _enumeration_, which means you can easily query all stored entries both on and off-chain.
=== Building a Merkle Tree
Building an on-chain Merkle Tree allow developers to keep track of the history of roots in a decentralized manner. For these cases, the xref:api:utils.adoc#MerkleTree[`MerkleTree`] includes a predefined structure with functions to manipulate the tree (e.g. pushing values or resetting the tree).
The Merkle Tree does not keep track of the roots purposely, so that developers can choose their tracking mechanism. Setting up and using an Merkle Tree in Solidity is as simple as follows:
[source,solidity]
----
// NOTE: Functions are exposed without access control for demonstration purposes
using MerkleTree for MerkleTree.Bytes32PushTree;
MerkleTree.Bytes32PushTree private _tree;
function setup(uint8 _depth, bytes32 _zero) public /* onlyOwner */ {
root = _tree.setup(_depth, _zero);
}
function push(bytes32 leaf) public /* onlyOwner */ {
(uint256 leafIndex, bytes32 currentRoot) = _tree.push(leaf);
// Store the new root.
}
----
[[misc]]
== Misc
=== Storage Slots
Solidity allocates a storage pointer for each variable declared in a contract. However, there are cases when it's required to access storage pointers that can't be derived by using regular Solidity.
For those cases, the xref:api:utils.adoc#StorageSlot[`StorageSlot`] library allows for manipulating storage slots directly.
[source,solidity]
----
bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
function _getImplementation() internal view returns (address) {
return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value;
}
function _setImplementation(address newImplementation) internal {
require(newImplementation.code.length > 0);
StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation;
}
----
WARNING: Manipulating storage slots directly is an advanced practice. Developers MUST make sure that the storage pointer is not colliding with other variables.
One of the most common use cases for writing directly to storage slots is ERC-7201 for namespaced storage, which is guaranteed to not collide with other storage slots derived by Solidity.
Users can leverage this standard using the xref:api:utils.adoc#SlotDerivation[`SlotDerivation`] library.
[source,solidity]
----
using SlotDerivation for bytes32;
string private constant _NAMESPACE = "<namespace>" // eg. example.main
function erc7201Pointer() internal view returns (bytes32) {
return _NAMESPACE.erc7201Slot();
}
----
=== Base64
xref:api:utils.adoc#Base64[`Base64`] util allows you to transform `bytes32` data into its Base64 `string` representation.

@ -36,6 +36,7 @@ for (const [file, template] of Object.entries({
'utils/structs/EnumerableSet.sol': './templates/EnumerableSet.js',
'utils/structs/EnumerableMap.sol': './templates/EnumerableMap.js',
'utils/structs/Checkpoints.sol': './templates/Checkpoints.js',
'utils/SlotDerivation.sol': './templates/SlotDerivation.js',
'utils/StorageSlot.sol': './templates/StorageSlot.js',
'utils/Arrays.sol': './templates/Arrays.js',
})) {
@ -45,6 +46,7 @@ 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/SlotDerivation.t.sol': './templates/SlotDerivation.t.js',
})) {
generateFromTemplate(file, template, './test/');
}

@ -5,6 +5,7 @@ const { TYPES } = require('./Arrays.opts');
const header = `\
pragma solidity ^0.8.20;
import {SlotDerivation} from "./SlotDerivation.sol";
import {StorageSlot} from "./StorageSlot.sol";
import {Math} from "./math/Math.sol";
@ -327,16 +328,12 @@ const unsafeAccessStorage = type => `
function unsafeAccess(${type}[] storage arr, uint256 pos) internal pure returns (StorageSlot.${capitalize(
type,
)}Slot storage) {
bytes32 slot;
// We use assembly to calculate the storage slot of the element at index \`pos\` of the dynamic array \`arr\`
// following https://docs.soliditylang.org/en/v0.8.20/internals/layout_in_storage.html#mappings-and-dynamic-arrays.
/// @solidity memory-safe-assembly
assembly {
mstore(0, arr.slot)
slot := add(keccak256(0, 0x20), pos)
}
return slot.get${capitalize(type)}Slot();
bytes32 slot;
/// @solidity memory-safe-assembly
assembly {
slot := arr.slot
}
return slot.deriveArray().offset(pos).get${capitalize(type)}Slot();
}`;
const unsafeAccessMemory = type => `
@ -368,6 +365,7 @@ function unsafeSetLength(${type}[] storage array, uint256 len) internal {
module.exports = format(
header.trimEnd(),
'library Arrays {',
'using SlotDerivation for bytes32;',
'using StorageSlot for bytes32;',
// sorting, comparator, helpers and internal
sort('bytes32'),

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

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

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

@ -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;
@ -24,6 +15,7 @@ pragma solidity ^0.8.20;
* 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,20 +28,22 @@ 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 an \`${name}Slot\` with member \`value\` located at \`slot\`.
*/
function get${type.struct}(bytes32 slot) internal pure returns (${type.struct} storage r) {
function get${name}Slot(bytes32 slot) internal pure returns (${name}Slot storage r) {
/// @solidity memory-safe-assembly
assembly {
r.slot := slot
@ -57,11 +51,11 @@ function get${type.struct}(bytes32 slot) internal pure returns (${type.struct} s
}
`;
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) {
function get${name}Slot(${type} storage store) internal pure returns (${name}Slot storage r) {
/// @solidity memory-safe-assembly
assembly {
r.slot := store.slot
@ -73,6 +67,7 @@ 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)])],
TYPES.map(type => struct(type)),
TYPES.flatMap(type => [get(type), type.isValueType ? '' : getStorage(type)]),
'}',
);

@ -4,12 +4,14 @@ const generators = {
address: () => ethers.Wallet.createRandom().address,
bytes32: () => ethers.hexlify(ethers.randomBytes(32)),
uint256: () => ethers.toBigInt(ethers.randomBytes(32)),
int256: () => ethers.toBigInt(ethers.randomBytes(32)) + ethers.MinInt256,
hexBytes: length => ethers.hexlify(ethers.randomBytes(length)),
};
generators.address.zero = ethers.ZeroAddress;
generators.bytes32.zero = ethers.ZeroHash;
generators.uint256.zero = 0n;
generators.int256.zero = 0n;
generators.hexBytes.zero = '0x';
module.exports = {

@ -5,18 +5,18 @@ const ImplementationLabel = 'eip1967.proxy.implementation';
const AdminLabel = 'eip1967.proxy.admin';
const BeaconLabel = 'eip1967.proxy.beacon';
const erc1967slot = label => ethers.toBeHex(ethers.toBigInt(ethers.id(label)) - 1n);
const erc7201slot = label => ethers.toBeHex(ethers.toBigInt(ethers.keccak256(erc1967slot(label))) & ~0xffn);
const erc1967Slot = label => ethers.toBeHex(ethers.toBigInt(ethers.id(label)) - 1n);
const erc7201Slot = label => ethers.toBeHex(ethers.toBigInt(ethers.keccak256(erc1967Slot(label))) & ~0xffn);
const erc7201format = contractName => `openzeppelin.storage.${contractName}`;
const getSlot = (address, slot) =>
ethers.provider.getStorage(address, ethers.isBytesLike(slot) ? slot : erc1967slot(slot));
ethers.provider.getStorage(address, ethers.isBytesLike(slot) ? slot : erc1967Slot(slot));
const setSlot = (address, slot, value) =>
Promise.all([
ethers.isAddressable(address) ? address.getAddress() : Promise.resolve(address),
ethers.isAddressable(value) ? value.getAddress() : Promise.resolve(value),
]).then(([address, value]) => setStorageAt(address, ethers.isBytesLike(slot) ? slot : erc1967slot(slot), value));
]).then(([address, value]) => setStorageAt(address, ethers.isBytesLike(slot) ? slot : erc1967Slot(slot), value));
const getAddressInSlot = (address, slot) =>
getSlot(address, slot).then(slotValue => ethers.AbiCoder.defaultAbiCoder().decode(['address'], slotValue)[0]);
@ -25,7 +25,7 @@ const upgradeableSlot = (contractName, offset) => {
try {
// Try to get the artifact paths, will throw if it doesn't exist
artifacts._getArtifactPathSync(`${contractName}Upgradeable`);
return offset + ethers.toBigInt(erc7201slot(erc7201format(contractName)));
return offset + ethers.toBigInt(erc7201Slot(erc7201format(contractName)));
} catch (_) {
return offset;
}
@ -35,11 +35,11 @@ module.exports = {
ImplementationLabel,
AdminLabel,
BeaconLabel,
ImplementationSlot: erc1967slot(ImplementationLabel),
AdminSlot: erc1967slot(AdminLabel),
BeaconSlot: erc1967slot(BeaconLabel),
erc1967slot,
erc7201slot,
ImplementationSlot: erc1967Slot(ImplementationLabel),
AdminSlot: erc1967Slot(AdminLabel),
BeaconSlot: erc1967Slot(BeaconLabel),
erc1967Slot,
erc7201Slot,
erc7201format,
setSlot,
getSlot,

@ -0,0 +1,203 @@
// SPDX-License-Identifier: MIT
// This file was procedurally generated from scripts/generate/templates/SlotDerivation.t.js.
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {SlotDerivation} from "@openzeppelin/contracts/utils/SlotDerivation.sol";
contract SlotDerivationTest is Test {
using SlotDerivation for bytes32;
bytes[] private _array;
function testDeriveArray(uint256 length, uint256 offset) public {
length = bound(length, 1, type(uint256).max);
offset = bound(offset, 0, length - 1);
bytes32 baseSlot;
assembly {
baseSlot := _array.slot
sstore(baseSlot, length) // store length so solidity access does not revert
}
bytes storage derived = _array[offset];
bytes32 derivedSlot;
assembly {
derivedSlot := derived.slot
}
assertEq(baseSlot.deriveArray().offset(offset), derivedSlot);
}
mapping(address => bytes) private _addressMapping;
function testDeriveMappingAddress(address key) public {
bytes32 baseSlot;
assembly {
baseSlot := _addressMapping.slot
}
bytes storage derived = _addressMapping[key];
bytes32 derivedSlot;
assembly {
derivedSlot := derived.slot
}
assertEq(baseSlot.deriveMapping(key), derivedSlot);
}
mapping(bool => bytes) private _boolMapping;
function testDeriveMappingBoolean(bool key) public {
bytes32 baseSlot;
assembly {
baseSlot := _boolMapping.slot
}
bytes storage derived = _boolMapping[key];
bytes32 derivedSlot;
assembly {
derivedSlot := derived.slot
}
assertEq(baseSlot.deriveMapping(key), derivedSlot);
}
mapping(bytes32 => bytes) private _bytes32Mapping;
function testDeriveMappingBytes32(bytes32 key) public {
bytes32 baseSlot;
assembly {
baseSlot := _bytes32Mapping.slot
}
bytes storage derived = _bytes32Mapping[key];
bytes32 derivedSlot;
assembly {
derivedSlot := derived.slot
}
assertEq(baseSlot.deriveMapping(key), derivedSlot);
}
mapping(bytes4 => bytes) private _bytes4Mapping;
function testDeriveMappingBytes4(bytes4 key) public {
bytes32 baseSlot;
assembly {
baseSlot := _bytes4Mapping.slot
}
bytes storage derived = _bytes4Mapping[key];
bytes32 derivedSlot;
assembly {
derivedSlot := derived.slot
}
assertEq(baseSlot.deriveMapping(key), derivedSlot);
}
mapping(uint256 => bytes) private _uint256Mapping;
function testDeriveMappingUint256(uint256 key) public {
bytes32 baseSlot;
assembly {
baseSlot := _uint256Mapping.slot
}
bytes storage derived = _uint256Mapping[key];
bytes32 derivedSlot;
assembly {
derivedSlot := derived.slot
}
assertEq(baseSlot.deriveMapping(key), derivedSlot);
}
mapping(uint32 => bytes) private _uint32Mapping;
function testDeriveMappingUint32(uint32 key) public {
bytes32 baseSlot;
assembly {
baseSlot := _uint32Mapping.slot
}
bytes storage derived = _uint32Mapping[key];
bytes32 derivedSlot;
assembly {
derivedSlot := derived.slot
}
assertEq(baseSlot.deriveMapping(key), derivedSlot);
}
mapping(int256 => bytes) private _int256Mapping;
function testDeriveMappingInt256(int256 key) public {
bytes32 baseSlot;
assembly {
baseSlot := _int256Mapping.slot
}
bytes storage derived = _int256Mapping[key];
bytes32 derivedSlot;
assembly {
derivedSlot := derived.slot
}
assertEq(baseSlot.deriveMapping(key), derivedSlot);
}
mapping(int32 => bytes) private _int32Mapping;
function testDeriveMappingInt32(int32 key) public {
bytes32 baseSlot;
assembly {
baseSlot := _int32Mapping.slot
}
bytes storage derived = _int32Mapping[key];
bytes32 derivedSlot;
assembly {
derivedSlot := derived.slot
}
assertEq(baseSlot.deriveMapping(key), derivedSlot);
}
mapping(string => bytes) private _stringMapping;
function testDeriveMappingString(string memory key) public {
bytes32 baseSlot;
assembly {
baseSlot := _stringMapping.slot
}
bytes storage derived = _stringMapping[key];
bytes32 derivedSlot;
assembly {
derivedSlot := derived.slot
}
assertEq(baseSlot.deriveMapping(key), derivedSlot);
}
mapping(bytes => bytes) private _bytesMapping;
function testDeriveMappingBytes(bytes memory key) public {
bytes32 baseSlot;
assembly {
baseSlot := _bytesMapping.slot
}
bytes storage derived = _bytesMapping[key];
bytes32 derivedSlot;
assembly {
derivedSlot := derived.slot
}
assertEq(baseSlot.deriveMapping(key), derivedSlot);
}
}

@ -0,0 +1,58 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { erc7201Slot } = require('../helpers/storage');
const { generators } = require('../helpers/random');
async function fixture() {
const [account] = await ethers.getSigners();
const mock = await ethers.deployContract('$SlotDerivation');
return { mock, account };
}
describe('SlotDerivation', function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
describe('namespaces', function () {
const namespace = 'example.main';
it('erc-7201', async function () {
expect(await this.mock.$erc7201Slot(namespace)).to.equal(erc7201Slot(namespace));
});
});
describe('derivation', function () {
it('offset', async function () {
const base = generators.bytes32();
const offset = generators.uint256();
expect(await this.mock.$offset(base, offset)).to.equal((ethers.toBigInt(base) + offset) & ethers.MaxUint256);
});
it('array', async function () {
const base = generators.bytes32();
expect(await this.mock.$deriveArray(base)).to.equal(ethers.keccak256(base));
});
describe('mapping', function () {
for (const { type, key, isValueType } of [
{ type: 'bool', key: true, isValueType: true },
{ type: 'address', key: generators.address(), isValueType: true },
{ type: 'bytes32', key: generators.bytes32(), isValueType: true },
{ type: 'uint256', key: generators.uint256(), isValueType: true },
{ type: 'int256', key: generators.int256(), isValueType: true },
{ type: 'bytes', key: generators.hexBytes(128), isValueType: false },
{ type: 'string', key: 'lorem ipsum', isValueType: false },
]) {
it(type, async function () {
const base = generators.bytes32();
const expected = isValueType
? ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode([type, 'bytes32'], [key, base]))
: ethers.solidityPackedKeccak256([type, 'bytes32'], [key, base]);
expect(await this.mock[`$deriveMapping(bytes32,${type})`](base, key)).to.equal(expected);
});
}
});
});
});

@ -19,10 +19,12 @@ describe('StorageSlot', function () {
for (const { type, value, zero } of [
{ type: 'Boolean', value: true, zero: false },
{ type: 'Address', value: generators.address(), zero: ethers.ZeroAddress },
{ type: 'Bytes32', value: generators.bytes32(), zero: ethers.ZeroHash },
{ type: 'Address', value: generators.address(), zero: generators.address.zero },
{ type: 'Bytes32', value: generators.bytes32(), zero: generators.bytes32.zero },
{ type: 'Uint256', value: generators.uint256(), zero: generators.uint256.zero },
{ type: 'Int256', value: generators.int256(), zero: generators.int256.zero },
{ type: 'Bytes', value: generators.hexBytes(128), zero: generators.hexBytes.zero },
{ type: 'String', value: 'lorem ipsum', zero: '' },
{ type: 'Bytes', value: generators.hexBytes(128), zero: '0x' },
]) {
describe(`${type} storage slot`, function () {
it('set', async function () {

Loading…
Cancel
Save