diff --git a/.changeset/heavy-baboons-give.md b/.changeset/heavy-baboons-give.md new file mode 100644 index 000000000..ebdde906f --- /dev/null +++ b/.changeset/heavy-baboons-give.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Packing`: Added a new utility for packing and unpacking multiple values into a single bytes32. Includes initial support for packing two `uint128` in an `Uint128x2` type. diff --git a/contracts/mocks/Stateless.sol b/contracts/mocks/Stateless.sol index 56f5b4c66..c8ec7184f 100644 --- a/contracts/mocks/Stateless.sol +++ b/contracts/mocks/Stateless.sol @@ -24,6 +24,7 @@ import {ERC721Holder} from "../token/ERC721/utils/ERC721Holder.sol"; import {Math} from "../utils/math/Math.sol"; import {MerkleProof} from "../utils/cryptography/MerkleProof.sol"; import {MessageHashUtils} from "../utils/cryptography/MessageHashUtils.sol"; +import {Packing} from "../utils/Packing.sol"; import {SafeCast} from "../utils/math/SafeCast.sol"; import {SafeERC20} from "../token/ERC20/utils/SafeERC20.sol"; import {ShortStrings} from "../utils/ShortStrings.sol"; diff --git a/contracts/utils/Packing.sol b/contracts/utils/Packing.sol new file mode 100644 index 000000000..1929f9b59 --- /dev/null +++ b/contracts/utils/Packing.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +/** + * @dev Helper library packing and unpacking multiple values into bytes32 + */ +library Packing { + type Uint128x2 is bytes32; + + /// @dev Cast a bytes32 into a Uint128x2 + function asUint128x2(bytes32 self) internal pure returns (Uint128x2) { + return Uint128x2.wrap(self); + } + + /// @dev Cast a Uint128x2 into a bytes32 + function asBytes32(Uint128x2 self) internal pure returns (bytes32) { + return Uint128x2.unwrap(self); + } + + /// @dev Pack two uint128 into a Uint128x2 + function pack(uint128 first128, uint128 second128) internal pure returns (Uint128x2) { + return Uint128x2.wrap(bytes32(bytes16(first128)) | bytes32(uint256(second128))); + } + + /// @dev Split a Uint128x2 into two uint128 + function split(Uint128x2 self) internal pure returns (uint128, uint128) { + return (first(self), second(self)); + } + + /// @dev Get the first element of a Uint128x2 counting from higher to lower bytes + function first(Uint128x2 self) internal pure returns (uint128) { + return uint128(bytes16(Uint128x2.unwrap(self))); + } + + /// @dev Get the second element of a Uint128x2 counting from higher to lower bytes + function second(Uint128x2 self) internal pure returns (uint128) { + return uint128(uint256(Uint128x2.unwrap(self))); + } +} diff --git a/contracts/utils/README.adoc b/contracts/utils/README.adoc index 5e8f183a9..81db37e60 100644 --- a/contracts/utils/README.adoc +++ b/contracts/utils/README.adoc @@ -33,6 +33,7 @@ Miscellaneous contracts and libraries containing utility functions you can use t * {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. + * {Packing}: A library for packing and unpacking multiple values into bytes32 * {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]. [NOTE] @@ -120,4 +121,6 @@ Ethereum contracts have no native concept of an interface, so applications must {{Context}} +{{Packing}} + {{Panic}} diff --git a/test/utils/Packing.t.sol b/test/utils/Packing.t.sol new file mode 100644 index 000000000..95961f391 --- /dev/null +++ b/test/utils/Packing.t.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {Packing} from "@openzeppelin/contracts/utils/Packing.sol"; + +contract PackingTest is Test { + using Packing for *; + + // Pack a pair of arbitrary uint128, and check that split recovers the correct values + function testUint128x2(uint128 first, uint128 second) external { + Packing.Uint128x2 packed = Packing.pack(first, second); + assertEq(packed.first(), first); + assertEq(packed.second(), second); + + (uint128 recoveredFirst, uint128 recoveredSecond) = packed.split(); + assertEq(recoveredFirst, first); + assertEq(recoveredSecond, second); + } + + // split an arbitrary bytes32 into a pair of uint128, and check that repack matches the input + function testUint128x2(bytes32 input) external { + (uint128 first, uint128 second) = input.asUint128x2().split(); + assertEq(Packing.pack(first, second).asBytes32(), input); + } +} diff --git a/test/utils/Packing.test.js b/test/utils/Packing.test.js new file mode 100644 index 000000000..8fb909545 --- /dev/null +++ b/test/utils/Packing.test.js @@ -0,0 +1,27 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { generators } = require('../helpers/random'); + +async function fixture() { + return { mock: await ethers.deployContract('$Packing') }; +} + +describe('Packing', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + it('Uint128x2', async function () { + const first = generators.uint256() % 2n ** 128n; + const second = generators.uint256() % 2n ** 128n; + const packed = ethers.hexlify(ethers.toBeArray((first << 128n) | second)); + + expect(await this.mock.$asUint128x2(packed)).to.equal(packed); + expect(await this.mock.$asBytes32(packed)).to.equal(packed); + expect(await this.mock.$pack(first, second)).to.equal(packed); + expect(await this.mock.$split(packed)).to.deep.equal([first, second]); + expect(await this.mock.$first(packed)).to.equal(first); + expect(await this.mock.$second(packed)).to.equal(second); + }); +});