Add transient storage slot support in StorageSlot.sol (#4980)

Co-authored-by: ernestognw <ernestognw@gmail.com>
pull/4986/head
Hadrien Croubois 10 months ago committed by GitHub
parent 2d259ac346
commit d6ad9db0a0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      .changeset/kind-planets-cough.md
  2. 2
      .github/workflows/checks.yml
  3. 72
      contracts/mocks/StorageSlotMock.sol
  4. 2
      contracts/utils/README.adoc
  5. 180
      contracts/utils/StorageSlot.sol
  6. 15
      docs/modules/ROOT/pages/utilities.adoc
  7. 2
      foundry.toml
  8. 10
      hardhat.config.js
  9. 2629
      package-lock.json
  10. 8
      package.json
  11. 1
      scripts/generate/run.js
  12. 58
      scripts/generate/templates/StorageSlot.js
  13. 65
      scripts/generate/templates/StorageSlotMock.js
  14. 7
      scripts/helpers.js
  15. 2
      test/governance/extensions/GovernorTimelockAccess.test.js
  16. 5
      test/helpers/strings.js
  17. 82
      test/utils/StorageSlot.test.js

@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---
`StorageSlot`: Add primitives for operating on the transient storage space using a typed-slot representation.

@ -14,7 +14,7 @@ concurrency:
cancel-in-progress: true
env:
NODE_OPTIONS: --max_old_space_size=5120
NODE_OPTIONS: --max_old_space_size=8192
jobs:
lint:

@ -1,20 +1,22 @@
// SPDX-License-Identifier: MIT
// This file was procedurally generated from scripts/generate/templates/StorageSlotMock.js.
pragma solidity ^0.8.20;
pragma solidity ^0.8.24;
import {Multicall} from "../utils/Multicall.sol";
import {StorageSlot} from "../utils/StorageSlot.sol";
contract StorageSlotMock {
contract StorageSlotMock is Multicall {
using StorageSlot for *;
function setBooleanSlot(bytes32 slot, bool value) public {
slot.getBooleanSlot().value = value;
}
function setAddressSlot(bytes32 slot, address value) public {
slot.getAddressSlot().value = value;
}
function setBooleanSlot(bytes32 slot, bool value) public {
slot.getBooleanSlot().value = value;
}
function setBytes32Slot(bytes32 slot, bytes32 value) public {
slot.getBytes32Slot().value = value;
}
@ -27,14 +29,14 @@ contract StorageSlotMock {
slot.getInt256Slot().value = value;
}
function getBooleanSlot(bytes32 slot) public view returns (bool) {
return slot.getBooleanSlot().value;
}
function getAddressSlot(bytes32 slot) public view returns (address) {
return slot.getAddressSlot().value;
}
function getBooleanSlot(bytes32 slot) public view returns (bool) {
return slot.getBooleanSlot().value;
}
function getBytes32Slot(bytes32 slot) public view returns (bytes32) {
return slot.getBytes32Slot().value;
}
@ -82,4 +84,54 @@ contract StorageSlotMock {
function getBytesStorage(uint256 key) public view returns (bytes memory) {
return bytesMap[key].getBytesSlot().value;
}
event AddressValue(bytes32 slot, address value);
function tloadAddress(bytes32 slot) public {
emit AddressValue(slot, slot.asAddress().tload());
}
function tstore(bytes32 slot, address value) public {
slot.asAddress().tstore(value);
}
event BooleanValue(bytes32 slot, bool value);
function tloadBoolean(bytes32 slot) public {
emit BooleanValue(slot, slot.asBoolean().tload());
}
function tstore(bytes32 slot, bool value) public {
slot.asBoolean().tstore(value);
}
event Bytes32Value(bytes32 slot, bytes32 value);
function tloadBytes32(bytes32 slot) public {
emit Bytes32Value(slot, slot.asBytes32().tload());
}
function tstore(bytes32 slot, bytes32 value) public {
slot.asBytes32().tstore(value);
}
event Uint256Value(bytes32 slot, uint256 value);
function tloadUint256(bytes32 slot) public {
emit Uint256Value(slot, slot.asUint256().tload());
}
function tstore(bytes32 slot, uint256 value) public {
slot.asUint256().tstore(value);
}
event Int256Value(bytes32 slot, int256 value);
function tloadInt256(bytes32 slot) public {
emit Int256Value(slot, slot.asInt256().tload());
}
function tstore(bytes32 slot, int256 value) public {
slot.asInt256().tstore(value);
}
}

@ -29,7 +29,7 @@ Miscellaneous contracts and libraries containing utility functions you can use t
* {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.
* {StorageSlot}: Methods for accessing specific storage slots formatted as common primitive types. Also include primitives for reading from and writing to transient storage (only value types are currently supported).
* {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.
* {Panic}: A library to revert with https://docs.soliditylang.org/en/v0.8.20/control-structures.html#panic-via-assert-and-error-via-require[Solidity panic codes].

@ -2,7 +2,7 @@
// OpenZeppelin Contracts (last updated v5.0.0) (utils/StorageSlot.sol)
// This file was procedurally generated from scripts/generate/templates/StorageSlot.js.
pragma solidity ^0.8.20;
pragma solidity ^0.8.24;
/**
* @dev Library for reading and writing primitive types to specific storage slots.
@ -29,6 +29,24 @@ pragma solidity ^0.8.20;
* }
* ```
*
* Since version 5.1, this library also support writing and reading value types to and from transient storage.
*
* * Example using transient storage:
* ```solidity
* contract Lock {
* // Define the slot. Alternatively, use the SlotDerivation library to derive the slot.
* bytes32 internal constant _LOCK_SLOT = 0xf4678858b2b588224636b8522b729e7722d32fc491da849ed75b3fdf3c84f542;
*
* modifier locked() {
* require(!_LOCK_SLOT.asBoolean().tload());
*
* _LOCK_SLOT.asBoolean().tstore(true);
* _;
* _LOCK_SLOT.asBoolean().tstore(false);
* }
* }
* ```
*
* TIP: Consider using this library along with {SlotDerivation}.
*/
library StorageSlot {
@ -149,4 +167,164 @@ library StorageSlot {
r.slot := store.slot
}
}
/**
* @dev UDVT that represent a slot holding a address.
*/
type AddressSlotType is bytes32;
/**
* @dev Cast an arbitrary slot to a AddressSlotType.
*/
function asAddress(bytes32 slot) internal pure returns (AddressSlotType) {
return AddressSlotType.wrap(slot);
}
/**
* @dev UDVT that represent a slot holding a bool.
*/
type BooleanSlotType is bytes32;
/**
* @dev Cast an arbitrary slot to a BooleanSlotType.
*/
function asBoolean(bytes32 slot) internal pure returns (BooleanSlotType) {
return BooleanSlotType.wrap(slot);
}
/**
* @dev UDVT that represent a slot holding a bytes32.
*/
type Bytes32SlotType is bytes32;
/**
* @dev Cast an arbitrary slot to a Bytes32SlotType.
*/
function asBytes32(bytes32 slot) internal pure returns (Bytes32SlotType) {
return Bytes32SlotType.wrap(slot);
}
/**
* @dev UDVT that represent a slot holding a uint256.
*/
type Uint256SlotType is bytes32;
/**
* @dev Cast an arbitrary slot to a Uint256SlotType.
*/
function asUint256(bytes32 slot) internal pure returns (Uint256SlotType) {
return Uint256SlotType.wrap(slot);
}
/**
* @dev UDVT that represent a slot holding a int256.
*/
type Int256SlotType is bytes32;
/**
* @dev Cast an arbitrary slot to a Int256SlotType.
*/
function asInt256(bytes32 slot) internal pure returns (Int256SlotType) {
return Int256SlotType.wrap(slot);
}
/**
* @dev Load the value held at location `slot` in transient storage.
*/
function tload(AddressSlotType slot) internal view returns (address value) {
/// @solidity memory-safe-assembly
assembly {
value := tload(slot)
}
}
/**
* @dev Store `value` at location `slot` in transient storage.
*/
function tstore(AddressSlotType slot, address value) internal {
/// @solidity memory-safe-assembly
assembly {
tstore(slot, value)
}
}
/**
* @dev Load the value held at location `slot` in transient storage.
*/
function tload(BooleanSlotType slot) internal view returns (bool value) {
/// @solidity memory-safe-assembly
assembly {
value := tload(slot)
}
}
/**
* @dev Store `value` at location `slot` in transient storage.
*/
function tstore(BooleanSlotType slot, bool value) internal {
/// @solidity memory-safe-assembly
assembly {
tstore(slot, value)
}
}
/**
* @dev Load the value held at location `slot` in transient storage.
*/
function tload(Bytes32SlotType slot) internal view returns (bytes32 value) {
/// @solidity memory-safe-assembly
assembly {
value := tload(slot)
}
}
/**
* @dev Store `value` at location `slot` in transient storage.
*/
function tstore(Bytes32SlotType slot, bytes32 value) internal {
/// @solidity memory-safe-assembly
assembly {
tstore(slot, value)
}
}
/**
* @dev Load the value held at location `slot` in transient storage.
*/
function tload(Uint256SlotType slot) internal view returns (uint256 value) {
/// @solidity memory-safe-assembly
assembly {
value := tload(slot)
}
}
/**
* @dev Store `value` at location `slot` in transient storage.
*/
function tstore(Uint256SlotType slot, uint256 value) internal {
/// @solidity memory-safe-assembly
assembly {
tstore(slot, value)
}
}
/**
* @dev Load the value held at location `slot` in transient storage.
*/
function tload(Int256SlotType slot) internal view returns (int256 value) {
/// @solidity memory-safe-assembly
assembly {
value := tload(slot)
}
}
/**
* @dev Store `value` at location `slot` in transient storage.
*/
function tstore(Int256SlotType slot, int256 value) internal {
/// @solidity memory-safe-assembly
assembly {
tstore(slot, value)
}
}
}

@ -166,6 +166,21 @@ function _setImplementation(address newImplementation) internal {
}
----
The xref:api:utils.adoc#StorageSlot[`StorageSlot`] library also supports transient storage through user defined value types (UDVTs[https://docs.soliditylang.org/en/latest/types.html#user-defined-value-types]), which enables the same value types as in Solidity.
[source,solidity]
----
bytes32 internal constant _LOCK_SLOT = 0xf4678858b2b588224636b8522b729e7722d32fc491da849ed75b3fdf3c84f542;
function _getTransientLock() internal view returns (bool) {
return _LOCK_SLOT.asBoolean().tload();
}
function _setTransientLock(bool lock) internal {
_LOCK_SLOT.asBoolean().tstore(lock);
}
----
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.

@ -1,4 +1,6 @@
[profile.default]
solc_version = '0.8.24'
evm_version = 'cancun'
src = 'contracts'
out = 'out'
libs = ['node_modules', 'lib']

@ -18,7 +18,7 @@ const { argv } = require('yargs/yargs')()
compiler: {
alias: 'compileVersion',
type: 'string',
default: '0.8.20',
default: '0.8.24',
},
src: {
alias: 'source',
@ -36,6 +36,11 @@ const { argv } = require('yargs/yargs')()
type: 'boolean',
default: false,
},
evm: {
alias: 'evmVersion',
type: 'string',
default: 'cancun',
},
// Extra modules
coverage: {
type: 'boolean',
@ -78,6 +83,7 @@ module.exports = {
enabled: withOptimizations,
runs: 200,
},
evmVersion: argv.evm,
viaIR: withOptimizations && argv.ir,
outputSelection: { '*': { '*': ['storageLayout'] } },
},
@ -90,11 +96,13 @@ module.exports = {
'*': {
'code-size': withOptimizations,
'unused-param': !argv.coverage, // coverage causes unused-param warnings
'transient-storage': false,
default: 'error',
},
},
networks: {
hardhat: {
hardfork: argv.evm,
allowUnlimitedContractSize,
initialBaseFeePerGas: argv.coverage ? 0 : undefined,
},

2629
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -67,10 +67,10 @@
"ethers": "^6.7.1",
"glob": "^10.3.5",
"graphlib": "^2.1.8",
"hardhat": "^2.17.4",
"hardhat-exposed": "^0.3.14-0",
"hardhat-gas-reporter": "^2.0.0",
"hardhat-ignore-warnings": "^0.2.0",
"hardhat": "^2.22.2",
"hardhat-exposed": "^0.3.15",
"hardhat-gas-reporter": "^1.0.9",
"hardhat-ignore-warnings": "^0.2.11",
"lodash.startcase": "^4.4.0",
"micromatch": "^4.0.2",
"p-limit": "^3.1.0",

@ -39,6 +39,7 @@ for (const [file, template] of Object.entries({
'utils/SlotDerivation.sol': './templates/SlotDerivation.js',
'utils/StorageSlot.sol': './templates/StorageSlot.js',
'utils/Arrays.sol': './templates/Arrays.js',
'mocks/StorageSlotMock.sol': './templates/StorageSlotMock.js',
})) {
generateFromTemplate(file, template, './contracts/');
}

@ -2,7 +2,7 @@ const format = require('../format-lines');
const { TYPES } = require('./Slot.opts');
const header = `\
pragma solidity ^0.8.20;
pragma solidity ^0.8.24;
/**
* @dev Library for reading and writing primitive types to specific storage slots.
@ -28,7 +28,25 @@ pragma solidity ^0.8.20;
* }
* }
* \`\`\`
*
*
* Since version 5.1, this library also support writing and reading value types to and from transient storage.
*
* * Example using transient storage:
* \`\`\`solidity
* contract Lock {
* // Define the slot. Alternatively, use the SlotDerivation library to derive the slot.
* bytes32 internal constant _LOCK_SLOT = 0xf4678858b2b588224636b8522b729e7722d32fc491da849ed75b3fdf3c84f542;
*
* modifier locked() {
* require(!_LOCK_SLOT.asBoolean().tload());
*
* _LOCK_SLOT.asBoolean().tstore(true);
* _;
* _LOCK_SLOT.asBoolean().tstore(false);
* }
* }
* \`\`\`
*
* TIP: Consider using this library along with {SlotDerivation}.
*/
`;
@ -63,11 +81,47 @@ function get${name}Slot(${type} storage store) internal pure returns (${name}Slo
}
`;
const udvt = ({ type, name }) => `\
/**
* @dev UDVT that represent a slot holding a ${type}.
*/
type ${name}SlotType is bytes32;
/**
* @dev Cast an arbitrary slot to a ${name}SlotType.
*/
function as${name}(bytes32 slot) internal pure returns (${name}SlotType) {
return ${name}SlotType.wrap(slot);
}
`;
const transient = ({ type, name }) => `\
/**
* @dev Load the value held at location \`slot\` in transient storage.
*/
function tload(${name}SlotType slot) internal view returns (${type} value) {
/// @solidity memory-safe-assembly
assembly {
value := tload(slot)
}
}
/**
* @dev Store \`value\` at location \`slot\` in transient storage.
*/
function tstore(${name}SlotType slot, ${type} value) internal {
/// @solidity memory-safe-assembly
assembly {
tstore(slot, value)
}
}
`;
// GENERATE
module.exports = format(
header.trimEnd(),
'library StorageSlot {',
TYPES.map(type => struct(type)),
TYPES.flatMap(type => [get(type), type.isValueType ? '' : getStorage(type)]),
TYPES.filter(type => type.isValueType).map(type => udvt(type)),
TYPES.filter(type => type.isValueType).map(type => transient(type)),
'}',
);

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

@ -1,10 +1,7 @@
const iterate = require('../test/helpers/iterate');
const strings = require('../test/helpers/strings');
module.exports = {
// Capitalize the first char of a string
// Example: capitalize('uint256') → 'Uint256'
capitalize: str => str.charAt(0).toUpperCase() + str.slice(1),
// Iterate tools for the test helpers
...iterate,
...strings,
};

@ -375,7 +375,7 @@ describe('GovernorTimelockAccess', function () {
if (delay > 0) {
await this.helper.waitForEta();
}
expect(await this.helper.execute())
await expect(this.helper.execute())
.to.emit(this.mock, 'ProposalExecuted')
.withArgs(this.proposal.id)
.to.emit(this.receiver, 'CalledUnrestricted');

@ -0,0 +1,5 @@
module.exports = {
// Capitalize the first char of a string
// Example: capitalize('uint256') → 'Uint256'
capitalize: str => str.charAt(0).toUpperCase() + str.slice(1),
};

@ -6,10 +6,18 @@ const { generators } = require('../helpers/random');
const slot = ethers.id('some.storage.slot');
const otherSlot = ethers.id('some.other.storage.slot');
const TYPES = [
{ name: 'Boolean', type: 'bool', value: true, isValueType: true, zero: false },
{ name: 'Address', type: 'address', value: generators.address(), isValueType: true, zero: generators.address.zero },
{ name: 'Bytes32', type: 'bytes32', value: generators.bytes32(), isValueType: true, zero: generators.bytes32.zero },
{ name: 'Uint256', type: 'uint256', value: generators.uint256(), isValueType: true, zero: generators.uint256.zero },
{ name: 'Int256', type: 'int256', value: generators.int256(), isValueType: true, zero: generators.int256.zero },
{ name: 'Bytes', type: 'bytes', value: generators.hexBytes(128), isValueType: false, zero: generators.hexBytes.zero },
{ name: 'String', type: 'string', value: 'lorem ipsum', isValueType: false, zero: '' },
];
async function fixture() {
const [account] = await ethers.getSigners();
const mock = await ethers.deployContract('StorageSlotMock');
return { mock, account };
return { mock: await ethers.deployContract('StorageSlotMock') };
}
describe('StorageSlot', function () {
@ -17,60 +25,82 @@ describe('StorageSlot', function () {
Object.assign(this, await loadFixture(fixture));
});
for (const { type, value, zero } of [
{ type: 'Boolean', value: true, zero: false },
{ 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: '' },
]) {
for (const { name, type, value, zero } of TYPES) {
describe(`${type} storage slot`, function () {
it('set', async function () {
await this.mock.getFunction(`set${type}Slot`)(slot, value);
await this.mock.getFunction(`set${name}Slot`)(slot, value);
});
describe('get', function () {
beforeEach(async function () {
await this.mock.getFunction(`set${type}Slot`)(slot, value);
await this.mock.getFunction(`set${name}Slot`)(slot, value);
});
it('from right slot', async function () {
expect(await this.mock.getFunction(`get${type}Slot`)(slot)).to.equal(value);
expect(await this.mock.getFunction(`get${name}Slot`)(slot)).to.equal(value);
});
it('from other slot', async function () {
expect(await this.mock.getFunction(`get${type}Slot`)(otherSlot)).to.equal(zero);
expect(await this.mock.getFunction(`get${name}Slot`)(otherSlot)).to.equal(zero);
});
});
});
}
for (const { type, value, zero } of [
{ type: 'String', value: 'lorem ipsum', zero: '' },
{ type: 'Bytes', value: generators.hexBytes(128), zero: '0x' },
]) {
for (const { name, type, value, zero } of TYPES.filter(type => !type.isValueType)) {
describe(`${type} storage pointer`, function () {
it('set', async function () {
await this.mock.getFunction(`set${type}Storage`)(slot, value);
await this.mock.getFunction(`set${name}Storage`)(slot, value);
});
describe('get', function () {
beforeEach(async function () {
await this.mock.getFunction(`set${type}Storage`)(slot, value);
await this.mock.getFunction(`set${name}Storage`)(slot, value);
});
it('from right slot', async function () {
expect(await this.mock.getFunction(`${type.toLowerCase()}Map`)(slot)).to.equal(value);
expect(await this.mock.getFunction(`get${type}Storage`)(slot)).to.equal(value);
expect(await this.mock.getFunction(`${type}Map`)(slot)).to.equal(value);
expect(await this.mock.getFunction(`get${name}Storage`)(slot)).to.equal(value);
});
it('from other slot', async function () {
expect(await this.mock.getFunction(`${type.toLowerCase()}Map`)(otherSlot)).to.equal(zero);
expect(await this.mock.getFunction(`get${type}Storage`)(otherSlot)).to.equal(zero);
expect(await this.mock.getFunction(`${type}Map`)(otherSlot)).to.equal(zero);
expect(await this.mock.getFunction(`get${name}Storage`)(otherSlot)).to.equal(zero);
});
});
});
}
for (const { name, type, value, zero } of TYPES.filter(type => type.isValueType)) {
describe(`${type} transient slot`, function () {
const load = `tload${name}(bytes32)`;
const store = `tstore(bytes32,${type})`;
const event = `${name}Value`;
it('load', async function () {
await expect(this.mock[load](slot)).to.emit(this.mock, event).withArgs(slot, zero);
});
it('store and load (2 txs)', async function () {
await this.mock[store](slot, value);
await expect(this.mock[load](slot)).to.emit(this.mock, event).withArgs(slot, zero);
});
it('store and load (batched)', async function () {
await expect(
this.mock.multicall([
this.mock.interface.encodeFunctionData(store, [slot, value]),
this.mock.interface.encodeFunctionData(load, [slot]),
this.mock.interface.encodeFunctionData(load, [otherSlot]),
]),
)
.to.emit(this.mock, event)
.withArgs(slot, value)
.to.emit(this.mock, event)
.withArgs(otherSlot, zero);
await expect(this.mock[load](slot)).to.emit(this.mock, event).withArgs(slot, zero);
});
});
}
});

Loading…
Cancel
Save