From 222f03a751314f3a172bbd0a30abc576ae154e3e Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 4 Apr 2024 20:52:04 +0200 Subject: [PATCH 1/5] add Packing library --- .changeset/heavy-baboons-give.md | 5 ++++ contracts/utils/Packing.sol | 40 ++++++++++++++++++++++++++++++++ contracts/utils/README.adoc | 3 +++ test/utils/Packing.t.sol | 27 +++++++++++++++++++++ test/utils/Packing.test.js | 27 +++++++++++++++++++++ 5 files changed, 102 insertions(+) create mode 100644 .changeset/heavy-baboons-give.md create mode 100644 contracts/utils/Packing.sol create mode 100644 test/utils/Packing.t.sol create mode 100644 test/utils/Packing.test.js diff --git a/.changeset/heavy-baboons-give.md b/.changeset/heavy-baboons-give.md new file mode 100644 index 00000000000..fc08afbe3f7 --- /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. diff --git a/contracts/utils/Packing.sol b/contracts/utils/Packing.sol new file mode 100644 index 00000000000..3b80494090a --- /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 high128, uint128 low128) internal pure returns (Uint128x2) { + return Uint128x2.wrap(bytes32(bytes16(high128)) | bytes32(uint256(low128))); + } + + /// @dev Split a Uint128x2 into two uint128 + function split(Uint128x2 self) internal pure returns (uint128, uint128) { + return (high(self), low(self)); + } + + /// @dev Get the first element of a Uint128x2 (high part) + function high(Uint128x2 self) internal pure returns (uint128) { + return uint128(bytes16(Uint128x2.unwrap(self))); + } + + /// @dev Get the second element of a Uint128x2 (low part) + function low(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 98747b41f78..684c80e225f 100644 --- a/contracts/utils/README.adoc +++ b/contracts/utils/README.adoc @@ -32,6 +32,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] @@ -117,4 +118,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 00000000000..525e4c398ef --- /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 Base64Test is Test { + using Packing for *; + + // Pack a pair of arbitrary uint128, and check that split recovers the correct values + function testUint128x2(uint128 high, uint128 low) external { + Packing.Uint128x2 packed = Packing.pack(high, low); + assertEq(packed.high(), high); + assertEq(packed.low(), low); + + (uint128 recoveredHigh, uint128 recoveredLow) = packed.split(); + assertEq(recoveredHigh, high); + assertEq(recoveredLow, low); + } + + // split an arbitrary bytes32 into a pair of uint128, and check that repack matches the input + function testUint128x2(bytes32 input) external { + (uint128 high, uint128 low) = input.asUint128x2().split(); + assertEq(Packing.pack(high, low).asBytes32(), input); + } +} diff --git a/test/utils/Packing.test.js b/test/utils/Packing.test.js new file mode 100644 index 00000000000..f226ba66ca4 --- /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 high = generators.uint256() % 2n ** 128n; + const low = generators.uint256() % 2n ** 128n; + const packed = ethers.hexlify(ethers.toBeArray((high << 128n) | low)); + + expect(await this.mock.$asUint128x2(packed)).to.equal(packed); + expect(await this.mock.$asBytes32(packed)).to.equal(packed); + expect(await this.mock.$pack(high, low)).to.equal(packed); + expect(await this.mock.$split(packed)).to.deep.equal([high, low]); + expect(await this.mock.$high(packed)).to.equal(high); + expect(await this.mock.$low(packed)).to.equal(low); + }); +}); From 395e76defb982cf44302349c604704a582034bc2 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 4 Apr 2024 20:56:58 +0200 Subject: [PATCH 2/5] Update test/utils/Packing.t.sol --- test/utils/Packing.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/utils/Packing.t.sol b/test/utils/Packing.t.sol index 525e4c398ef..d9b6c154171 100644 --- a/test/utils/Packing.t.sol +++ b/test/utils/Packing.t.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; import {Packing} from "@openzeppelin/contracts/utils/Packing.sol"; -contract Base64Test is Test { + contract PackingTest is Test { using Packing for *; // Pack a pair of arbitrary uint128, and check that split recovers the correct values From 9bb36d75687749ea5f3e7cf19b07923351af4fd2 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 4 Apr 2024 21:06:35 +0200 Subject: [PATCH 3/5] fix --- contracts/mocks/Stateless.sol | 1 + test/utils/Packing.t.sol | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/mocks/Stateless.sol b/contracts/mocks/Stateless.sol index 56f5b4c6610..c8ec7184fec 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/test/utils/Packing.t.sol b/test/utils/Packing.t.sol index d9b6c154171..829bdaad758 100644 --- a/test/utils/Packing.t.sol +++ b/test/utils/Packing.t.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; import {Packing} from "@openzeppelin/contracts/utils/Packing.sol"; - contract PackingTest is Test { +contract PackingTest is Test { using Packing for *; // Pack a pair of arbitrary uint128, and check that split recovers the correct values From 50961c8496585e666c7b5c2dd3b41bbbf58baf1d Mon Sep 17 00:00:00 2001 From: ernestognw Date: Thu, 4 Apr 2024 14:39:53 -0600 Subject: [PATCH 4/5] high/low -> first/second --- contracts/utils/Packing.sol | 14 +++++++------- test/utils/Packing.t.sol | 18 +++++++++--------- test/utils/Packing.test.js | 14 +++++++------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/contracts/utils/Packing.sol b/contracts/utils/Packing.sol index 3b80494090a..1929f9b59bf 100644 --- a/contracts/utils/Packing.sol +++ b/contracts/utils/Packing.sol @@ -19,22 +19,22 @@ library Packing { } /// @dev Pack two uint128 into a Uint128x2 - function pack(uint128 high128, uint128 low128) internal pure returns (Uint128x2) { - return Uint128x2.wrap(bytes32(bytes16(high128)) | bytes32(uint256(low128))); + 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 (high(self), low(self)); + return (first(self), second(self)); } - /// @dev Get the first element of a Uint128x2 (high part) - function high(Uint128x2 self) internal pure returns (uint128) { + /// @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 (low part) - function low(Uint128x2 self) internal pure returns (uint128) { + /// @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/test/utils/Packing.t.sol b/test/utils/Packing.t.sol index 829bdaad758..95961f391dc 100644 --- a/test/utils/Packing.t.sol +++ b/test/utils/Packing.t.sol @@ -9,19 +9,19 @@ contract PackingTest is Test { using Packing for *; // Pack a pair of arbitrary uint128, and check that split recovers the correct values - function testUint128x2(uint128 high, uint128 low) external { - Packing.Uint128x2 packed = Packing.pack(high, low); - assertEq(packed.high(), high); - assertEq(packed.low(), low); + function testUint128x2(uint128 first, uint128 second) external { + Packing.Uint128x2 packed = Packing.pack(first, second); + assertEq(packed.first(), first); + assertEq(packed.second(), second); - (uint128 recoveredHigh, uint128 recoveredLow) = packed.split(); - assertEq(recoveredHigh, high); - assertEq(recoveredLow, low); + (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 high, uint128 low) = input.asUint128x2().split(); - assertEq(Packing.pack(high, low).asBytes32(), input); + (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 index f226ba66ca4..8fb90954599 100644 --- a/test/utils/Packing.test.js +++ b/test/utils/Packing.test.js @@ -13,15 +13,15 @@ describe('Packing', function () { }); it('Uint128x2', async function () { - const high = generators.uint256() % 2n ** 128n; - const low = generators.uint256() % 2n ** 128n; - const packed = ethers.hexlify(ethers.toBeArray((high << 128n) | low)); + 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(high, low)).to.equal(packed); - expect(await this.mock.$split(packed)).to.deep.equal([high, low]); - expect(await this.mock.$high(packed)).to.equal(high); - expect(await this.mock.$low(packed)).to.equal(low); + 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); }); }); From 39add0f7feadebae4860d95cd44dc3238e68a660 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Thu, 4 Apr 2024 14:43:56 -0600 Subject: [PATCH 5/5] Update .changeset/heavy-baboons-give.md --- .changeset/heavy-baboons-give.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/heavy-baboons-give.md b/.changeset/heavy-baboons-give.md index fc08afbe3f7..ebdde906f7a 100644 --- a/.changeset/heavy-baboons-give.md +++ b/.changeset/heavy-baboons-give.md @@ -2,4 +2,4 @@ 'openzeppelin-solidity': minor --- -`Packing`: Added a new utility for packing and unpacking multiple values into a single bytes32. +`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.