Add slot derivation library (#4975)
parent
5e3ba29b08
commit
cb2aaaa04a
@ -0,0 +1,5 @@ |
||||
--- |
||||
'openzeppelin-solidity': minor |
||||
--- |
||||
|
||||
`SlotDerivation`: Add a library of methods for derivating common storage slots. |
@ -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) |
||||
} |
||||
} |
||||
} |
@ -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)), |
||||
'}', |
||||
); |
@ -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); |
||||
}); |
||||
} |
||||
}); |
||||
}); |
||||
}); |
Loading…
Reference in new issue