HIGH severity: Potentially dangerous out-of-bounds memory access in `BytesParsing::sliceUnchecked`. **Description:** [`BytesParsing::sliceUnchecked`](https://github.com/wormhole-foundation/wormhole-circle-integration/blob/f7df33b159a71b163b8b5c7e7381c0d8f193da99/evm/src/libraries/BytesParsing.sol#L16-L57) currently[ bails...

Vulnerable code:
function sliceUnchecked(bytes memory encoded, uint256 offset, uint256 length)
    internal
    pure
    returns (bytes memory ret, uint256 nextOffset)
{
    //bail early for degenerate case
    if (length == 0) {
        return (new bytes(0), offset);
    }

    assembly ("memory-safe") {
        nextOffset := add(offset, length)
        ret := mload(freeMemoryPtr)

        /* snip: inline dev comments */

        let shift := and(length, 31) //equivalent to `mod(length, 32)` but 2 gas cheaper
        if iszero(shift) { shift := wordSize }

        let dest := add(ret, shift)
        let end := add(dest, length)
        for { let src := add(add(encoded, shift), offset) } lt(dest, end) {
            src := add(src, wordSize)
            dest := add(dest, wordSize)
        } { mstore(dest, mload(src)) }

        mstore(ret, length)
        //When compiling with --via-ir then normally allocated memory (i.e. via new) will have 32 byte
        //  memory alignment and so we enforce the same memory alignment here.
        mstore(freeMemoryPtr, and(add(dest, 31), not(31)))
    }
}

function slice(bytes memory encoded, uint256 offset, uint256 length)
    internal
    pure
    returns (bytes memory ret, uint256 nextOffset)
{
    (ret, nextOffset) = sliceUnchecked(encoded, offset, length);
    checkBound(nextOffset, encoded.length);
}

diff --git a/evm/.gitignore b/evm/.gitignore
--- a/evm/.gitignore
+++ b/evm/.gitignore
@@ -7,3 +7,4 @@ lib
 node_modules
 out
 ts/src/ethers-contracts
+venv/
diff --git a/evm/forge/tests/differential/BytesParsing.t.sol b/evm/forge/tests/differential/BytesParsing.t.sol
new file mode 100644
--- /dev/null
+++ b/evm/forge/tests/differential/BytesParsing.t.sol
@@ -0,0 +1,72 @@
+// SPDX-License-Identifier: Apache 2
+pragma solidity ^0.8.19;
+
+import "forge-std/Test.sol";
+import "forge-std/console.sol";
+
+import {BytesParsing} from "src/libraries/BytesParsing.sol";
+
+contract BytesParsingTest is Test {
+    using BytesParsing for bytes;
+
+    function setUp() public {}
+
+    function test_sliceUncheckedFuzz(bytes memory encoded, uint256 offset, uint256 length) public {
+        bound(offset, 0, type(uint8).max);
+        bound(length, 0, type(uint8).max);
+        if (offset > encoded.length || length > encoded.length || offset + length > encoded.length) {
+            return;
+        }
+
+        sliceUncheckedBase(encoded, offset, length);
+    }
+
+    function test_sliceUncheckedConcreteReadOOB() public {
+        bytes memory encoded = bytes("");
+        bytes32 dirty = 0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef;
+        assembly {
+            mstore(add(encoded, 0x20), dirty)
+        }
+        uint256 offset = 0;
+        uint256 length = 32;
+
+        sliceUncheckedBase(encoded, offset, length);
+    }
+
+    function sliceUncheckedBase(bytes memory encoded, uint256 offset, uint256 length)
+        internal
+        returns (
+            bytes memory soliditySlice,
+            uint256 solidityNextOffset,
+            bytes memory pythonSlice,
+            uint256 pythonNextOffset
+        )
+    {
+        (soliditySlice, solidityNextOffset) = encoded.sliceUnchecked(offset, length);
+        assertEq(soliditySlice.length, length, "wrong length");
+
+        string[] memory inputs = new string[](9);
+        inputs[0] = "python";
+        inputs[1] = "forge/tests/differential/python/bytes_parsing.py";
+        inputs[2] = "slice_unchecked";
+        inputs[3] = "--encoded";
+        inputs[4] = vm.toString(encoded);
+        inputs[5] = "--offset";
+        inputs[6] = vm.toString(offset);
+        inputs[7] = "--length";
+        inputs[8] = vm.toString(length);
+
+        (pythonSlice, pythonNextOffset) = abi.decode(vm.ffi(inputs), (bytes, uint256));
+
+        emit log_named_uint("soliditySlice.length", soliditySlice.length);
+        emit log_named_uint("pythonSlice.length", pythonSlice.length);
+
+        emit log_named_bytes("soliditySlice", soliditySlice);
+        emit log_named_bytes("pythonSlice", pythonSlice);
+        emit log_named_uint("solidityNextOffset", solidityNextOffset);
+        emit log_named_uint("pythonNextOffset", pythonNextOffset);
+
+        assertEq(soliditySlice, pythonSlice, "wrong slice");
+        assertEq(solidityNextOffset, pythonNextOffset, "wrong next offset");
+    }
+}
diff --git a/evm/forge/tests/differential/python/bytes_parsing.py b/evm/forge/tests/differential/python/bytes_parsing.py
new file mode 100644
--- /dev/null
+++ b/evm/forge/tests/differential/python/bytes_parsing.py
@@ -0,0 +1,42 @@
+from eth_abi import encode
+import argparse
+
+
+def main(args):
+    if args.function == "slice_unchecked":
+        slice, next_offset = slice_unchecked(args)
+        encode_and_print(slice, next_offset)
+
+
+def slice_unchecked(args):
+    if args.length == 0:
+        return (b"", args.offset)
+
+    next_offset = args.offset + args.length
+
+    encoded_bytes = (
+        bytes.fromhex(args.encoded[2:])
+        if args.encoded.startswith("0x")
+        else bytes.fromhex(args.encoded)
+    )
+    return (encoded_bytes[args.offset : next_offset], next_offset)
+
+
+def encode_and_print(slice, next_offset):
+    encoded_output = encode(["bytes", "uint256"], (slice, next_offset))
+    ## append 0x for FFI parsing
+    print("0x" + encoded_output.hex())
+
+
+def parse_args():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("function", choices=["slice_unchecked"])
+    parser.add_argument("--encoded", type=str)
+    parser.add_argument("--offset", type=int)
+    parser.add_argument("--length", type=int)
+    return parser.parse_args()
+
+
+if __name__ == "__main__":
+    args = parse_args()
+    main(args)
diff --git a/evm/forge/tests/differential/python/requirements.txt b/evm/forge/tests/differential/python/requirements.txt
new file mode 100644
--- /dev/null
+++ b/evm/forge/tests/differential/python/requirements.txt
@@ -0,0 +1 @@
+eth_abi==5.0.0
\ No newline at end of file
diff --git a/evm/foundry.toml b/evm/foundry.toml
--- a/evm/foundry.toml
+++ b/evm/foundry.toml
@@ -31,4 +31,7 @@ gas_reports = ["*"]

 gas_limit = "18446744073709551615"

+[profile.ffi]
+ffi = true
+