Bladeren bron

Add Packing library (#4992)

Co-authored-by: ernestognw <ernestognw@gmail.com>
Hadrien Croubois 1 jaar geleden
bovenliggende
commit
f8c2e1035e

+ 5 - 0
.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.

+ 1 - 0
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";

+ 40 - 0
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)));
+    }
+}

+ 3 - 0
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}}

+ 27 - 0
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);
+    }
+}

+ 27 - 0
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);
+  });
+});