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
130 changes: 130 additions & 0 deletions src/libs/BytesParser.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.20 <0.9.0;

/// @title BytesParser
/// @author Wormhole Labs
/// @notice Parses tightly packed data.
/// @dev Modified from
/// https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/main/src/libraries/BytesParsing.sol
library BytesParser {
error LengthMismatch(uint256 encodedLength, uint256 expectedLength);
error InvalidBool(uint8 value);

/// @notice Reverts if the encoded byte array length does not match the expected length.
/// @param encoded The byte array to check.
/// @param expected The expected length.
function checkLength(bytes memory encoded, uint256 expected) internal pure {
if (encoded.length != expected) revert LengthMismatch(encoded.length, expected);
}

/// @notice Reads a uint8 from `encoded` at the given byte `offset` without bounds checking.
/// @param encoded The byte array to read from.
/// @param offset The byte offset to start reading from.
/// @return value The decoded uint8 value.
/// @return nextOffset The offset immediately after the read bytes.
function asUint8Unchecked(
bytes memory encoded,
uint256 offset
) internal pure returns (uint8 value, uint256 nextOffset) {
assembly ("memory-safe") {
nextOffset := add(offset, 1)
value := mload(add(encoded, nextOffset))
}
}

/// @notice Reads a bool from `encoded` at the given byte `offset` without bounds checking.
/// @dev Reverts with `InvalidBool` if the underlying uint8 is not 0 or 1.
/// @param encoded The byte array to read from.
/// @param offset The byte offset to start reading from.
/// @return value The decoded bool value.
/// @return nextOffset The offset immediately after the read bytes.
function asBoolUnchecked(
bytes memory encoded,
uint256 offset
) internal pure returns (bool value, uint256 nextOffset) {
uint8 uint8Value;
(uint8Value, nextOffset) = asUint8Unchecked(encoded, offset);

if (uint8Value & 0xfe != 0) revert InvalidBool(uint8Value);

uint256 cleanedValue = uint256(uint8Value);
// skip 2x iszero opcode
assembly ("memory-safe") {
value := cleanedValue
}
}

/// @notice Reads a uint256 from `encoded` at the given byte `offset` without bounds checking.
/// @param encoded The byte array to read from.
/// @param offset The byte offset to start reading from.
/// @return value The decoded uint256 value.
/// @return nextOffset The offset immediately after the read bytes.
function asUint256Unchecked(
bytes memory encoded,
uint256 offset
) internal pure returns (uint256 value, uint256 nextOffset) {
assembly ("memory-safe") {
nextOffset := add(offset, 32)
value := mload(add(encoded, nextOffset))
}
}

/// @notice Reads a uint128 from `encoded` at the given byte `offset` without bounds checking.
/// @param encoded The byte array to read from.
/// @param offset The byte offset to start reading from.
/// @return value The decoded uint128 value.
/// @return nextOffset The offset immediately after the read bytes.
function asUint128Unchecked(
bytes memory encoded,
uint256 offset
) internal pure returns (uint128 value, uint256 nextOffset) {
assembly ("memory-safe") {
nextOffset := add(offset, 16)
value := mload(add(encoded, nextOffset))
}
}

/// @notice Reads a uint32 from `encoded` at the given byte `offset` without bounds checking.
/// @param encoded The byte array to read from.
/// @param offset The byte offset to start reading from.
/// @return value The decoded uint32 value.
/// @return nextOffset The offset immediately after the read bytes.
function asUint32Unchecked(
bytes memory encoded,
uint256 offset
) internal pure returns (uint32 value, uint256 nextOffset) {
assembly ("memory-safe") {
nextOffset := add(offset, 4)
value := mload(add(encoded, nextOffset))
}
}

/// @notice Reads a bytes32 from `encoded` at the given byte `offset` without bounds checking.
/// @param encoded The byte array to read from.
/// @param offset The byte offset to start reading from.
/// @return value The decoded bytes32 value.
/// @return nextOffset The offset immediately after the read bytes.
function asBytes32Unchecked(
bytes memory encoded,
uint256 offset
) internal pure returns (bytes32 value, uint256 nextOffset) {
uint256 uint256Value;
(uint256Value, nextOffset) = asUint256Unchecked(encoded, offset);
value = bytes32(uint256Value);
}

/// @notice Reads an address from `encoded` at the given byte `offset` without bounds checking.
/// @param encoded The byte array to read from.
/// @param offset The byte offset to start reading from.
/// @return value The decoded address value.
/// @return nextOffset The offset immediately after the read bytes.
function asAddressUnchecked(
bytes memory encoded,
uint256 offset
) internal pure returns (address value, uint256 nextOffset) {
assembly ("memory-safe") {
nextOffset := add(offset, 20)
value := mload(add(encoded, nextOffset))
}
}
}
23 changes: 23 additions & 0 deletions src/libs/TypeConverter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,37 @@ pragma solidity >=0.8.20 <0.9.0;
/// @author M0 Labs
/// @notice Utilities for converting between different data types.
library TypeConverter {
/// @notice Thrown when a uint256 value exceeds the max uint16 value.
error Uint16Overflow();

/// @notice Thrown when a uint256 value exceeds the max uint32 value.
error Uint32Overflow();

/// @notice Thrown when a uint256 value exceeds the max uint64 value.
error Uint64Overflow();

/// @notice Thrown when a uint256 value exceeds the max uint128 value.
error Uint128Overflow();

/// @notice Thrown when a bytes32 value doesn't represent a valid Ethereum address.
error InvalidAddress(bytes32 value);

/// @notice Converts a uint256 to uint16, reverting if the value overflows.
/// @param value The uint256 value to convert.
/// @return The uint16 representation of the value.
function toUint16(uint256 value) internal pure returns (uint16) {
if (value > type(uint16).max) revert Uint16Overflow();
return uint16(value);
}

/// @notice Converts a uint256 to uint32, reverting if the value overflows.
/// @param value The uint256 value to convert.
/// @return The uint32 representation of the value.
function toUint32(uint256 value) internal pure returns (uint32) {
if (value > type(uint32).max) revert Uint32Overflow();
return uint32(value);
}

/// @notice Converts a uint256 to uint64, reverting if the value overflows.
/// @param value The uint256 value to convert.
/// @return The uint64 representation of the value.
Expand Down
129 changes: 129 additions & 0 deletions test/BytesParserTest.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;

import { Test } from "forge-std/Test.sol";

import { BytesParser } from "../src/libs/BytesParser.sol";

contract BytesParserTest is Test {
using BytesParser for bytes;

function test_asUint8Unchecked() external pure {
bytes memory data = hex"0203";

(uint8 value, uint256 nextOffset) = data.asUint8Unchecked(0);
assertEq(value, 2);
assertEq(nextOffset, 1);

(value, nextOffset) = data.asUint8Unchecked(nextOffset);
assertEq(value, 3);
assertEq(nextOffset, 2);
}

function testFuzz_asUint8Unchecked(uint8 inputValue) external pure {
bytes memory data = abi.encodePacked(uint8(inputValue));

(uint8 value, uint256 nextOffset) = data.asUint8Unchecked(0);
assertEq(value, inputValue);
assertEq(nextOffset, 1);
}

function test_asBoolUnchecked() external pure {
bytes memory trueData = abi.encodePacked(true);
bytes memory falseData = abi.encodePacked(false);

(bool trueValue,) = trueData.asBoolUnchecked(0);
(bool falseValue,) = falseData.asBoolUnchecked(0);

assertTrue(trueValue);
assertFalse(falseValue);
}

/// forge-config: default.allow_internal_expect_revert = true
function test_asBoolUnchecked_invalidValue() external {
bytes memory invalidData = abi.encodePacked(uint8(2));

vm.expectRevert(abi.encodeWithSelector(BytesParser.InvalidBool.selector, 0x02));
invalidData.asBoolUnchecked(0);
}

function test_asUint256Unchecked() external pure {
bytes memory data = abi.encodePacked(uint256(1));

(uint256 value, uint256 nextOffset) = data.asUint256Unchecked(0);
assertEq(value, 1);
assertEq(nextOffset, 32);
}

function testFuzz_asUint256Unchecked(uint256 inputValue) external pure {
bytes memory data = abi.encodePacked(inputValue);

(uint256 value, uint256 nextOffset) = data.asUint256Unchecked(0);
assertEq(value, inputValue);
assertEq(nextOffset, 32);
}

function test_asUint128Unchecked() external pure {
bytes memory data = abi.encodePacked(uint128(1));

(uint128 value, uint256 nextOffset) = data.asUint128Unchecked(0);
assertEq(value, 1);
assertEq(nextOffset, 16);
}

function testFuzz_asUint128Unchecked(uint128 inputValue) external pure {
bytes memory data = abi.encodePacked(inputValue);

(uint128 value, uint256 nextOffset) = data.asUint128Unchecked(0);
assertEq(value, inputValue);
assertEq(nextOffset, 16);
}

function test_asUint32Unchecked() external pure {
bytes memory data = abi.encodePacked(uint32(1));

(uint32 value, uint256 nextOffset) = data.asUint32Unchecked(0);
assertEq(value, 1);
assertEq(nextOffset, 4);
}

function testFuzz_asUint32Unchecked(uint32 inputValue) external pure {
bytes memory data = abi.encodePacked(inputValue);

(uint32 value, uint256 nextOffset) = data.asUint32Unchecked(0);
assertEq(value, inputValue);
assertEq(nextOffset, 4);
}

function test_asBytes32Unchecked() external pure {
bytes memory data = abi.encodePacked(bytes32(uint256(1)));

(bytes32 value, uint256 nextOffset) = data.asBytes32Unchecked(0);
assertEq(value, bytes32(uint256(1)));
assertEq(nextOffset, 32);
}

function testFuzz_asBytes32Unchecked(bytes32 inputValue) external pure {
bytes memory data = abi.encodePacked(inputValue);

(bytes32 value, uint256 nextOffset) = data.asBytes32Unchecked(0);
assertEq(value, inputValue);
assertEq(nextOffset, 32);
}

function test_asAddressUnchecked() external pure {
bytes memory data = abi.encodePacked(address(1));

(address value, uint256 nextOffset) = data.asAddressUnchecked(0);
assertEq(value, address(1));
assertEq(nextOffset, 20);
}

function testFuzz_asAddressUnchecked(address inputValue) external pure {
bytes memory data = abi.encodePacked(inputValue);

(address value, uint256 nextOffset) = data.asAddressUnchecked(0);
assertEq(value, inputValue);
assertEq(nextOffset, 20);
}
}
84 changes: 84 additions & 0 deletions test/TypeConverter.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,90 @@ import { Test } from "../lib/forge-std/src/Test.sol";
import { TypeConverter } from "../src/libs/TypeConverter.sol";

contract TypeConverterTest is Test {
///////////////////////////////////////////////////////////////////////////
// toUint16 //
///////////////////////////////////////////////////////////////////////////

function test_toUint16_basic() external pure {
uint256 value = 100;
uint16 result = TypeConverter.toUint16(value);
assertEq(result, uint16(100));
}

function test_toUint16_maxUint16() external pure {
uint256 value = type(uint16).max;
uint16 result = TypeConverter.toUint16(value);
assertEq(result, type(uint16).max);
}

function test_toUint16_zero() external pure {
uint256 value = 0;
uint16 result = TypeConverter.toUint16(value);
assertEq(result, 0);
}

function testFuzz_toUint16(uint16 value) external pure {
uint256 uint256Value = uint256(value);
uint16 result = TypeConverter.toUint16(uint256Value);
assertEq(result, value);
}

/// forge-config: default.allow_internal_expect_revert = true
function test_toUint16_overflow() external {
uint256 value = uint256(type(uint16).max) + 1;
vm.expectRevert(TypeConverter.Uint16Overflow.selector);
TypeConverter.toUint16(value);
}

/// forge-config: default.allow_internal_expect_revert = true
function testFuzz_toUint16_overflow(uint256 value) external {
vm.assume(value > type(uint16).max);
vm.expectRevert(TypeConverter.Uint16Overflow.selector);
TypeConverter.toUint16(value);
}

///////////////////////////////////////////////////////////////////////////
// toUint32 //
///////////////////////////////////////////////////////////////////////////

function test_toUint32_basic() external pure {
uint256 value = 100;
uint32 result = TypeConverter.toUint32(value);
assertEq(result, uint32(100));
}

function test_toUint32_maxUint32() external pure {
uint256 value = type(uint32).max;
uint32 result = TypeConverter.toUint32(value);
assertEq(result, type(uint32).max);
}

function test_toUint32_zero() external pure {
uint256 value = 0;
uint32 result = TypeConverter.toUint32(value);
assertEq(result, 0);
}

function testFuzz_toUint32(uint32 value) external pure {
uint256 uint256Value = uint256(value);
uint32 result = TypeConverter.toUint32(uint256Value);
assertEq(result, value);
}

/// forge-config: default.allow_internal_expect_revert = true
function test_toUint32_overflow() external {
uint256 value = uint256(type(uint32).max) + 1;
vm.expectRevert(TypeConverter.Uint32Overflow.selector);
TypeConverter.toUint32(value);
}

/// forge-config: default.allow_internal_expect_revert = true
function testFuzz_toUint32_overflow(uint256 value) external {
vm.assume(value > type(uint32).max);
vm.expectRevert(TypeConverter.Uint32Overflow.selector);
TypeConverter.toUint32(value);
}

///////////////////////////////////////////////////////////////////////////
// toUint64 //
///////////////////////////////////////////////////////////////////////////
Expand Down
Loading