Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/heavy-baboons-give.md
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions contracts/mocks/Stateless.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
40 changes: 40 additions & 0 deletions contracts/utils/Packing.sol
Original file line number Diff line number Diff line change
@@ -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;
Copy link
Collaborator Author

@Amxx Amxx Apr 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now we only have (uint128, uint128) <> bytes32, but we can imagine having other types, such as uint64x4

Everything here is designed to scale to other types.


/// @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 changes: 3 additions & 0 deletions contracts/utils/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -117,4 +118,6 @@ Ethereum contracts have no native concept of an interface, so applications must

{{Context}}

{{Packing}}

{{Panic}}
27 changes: 27 additions & 0 deletions test/utils/Packing.t.sol
Original file line number Diff line number Diff line change
@@ -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 changes: 27 additions & 0 deletions test/utils/Packing.test.js
Original file line number Diff line number Diff line change
@@ -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);
});
});