Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
63 changes: 63 additions & 0 deletions contracts/utils/AbiDecode.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.24;

import {Calldata} from "./Calldata.sol";
import {Memory} from "./Memory.sol";

/// @dev Utilities to decode ABI-encoded data without reverting.
library AbiDecode {
using Memory for *;

/**
* @dev Attempts to decode a `bytes` value from a bytes input (in memory). Returns a boolean indicating success
* and a slice pointing to the decoded buffer. If decoding fails, returns an empty slice.
*/
function tryDecodeBytes(bytes memory input) internal pure returns (bool success, Memory.Slice output) {
return tryDecodeBytes(input.asSlice());
}

/**
* @dev Attempts to decode a `bytes` value from a Memory.Slice input. Returns a boolean indicating success and a
* slice pointing to the decoded buffer. If decoding fails, returns an empty slice.
*/
function tryDecodeBytes(Memory.Slice input) internal pure returns (bool success, Memory.Slice output) {
unchecked {
uint256 inputLength = input.length();
if (inputLength < 0x20) {
return (false, Memory.emptySlice());
}
uint256 offset = uint256(input.load(0));
if (inputLength - 0x20 < offset) {
return (false, Memory.emptySlice());
}
uint256 length = uint256(input.load(offset));
if (inputLength - 0x20 - offset < length) {
return (false, Memory.emptySlice());
}
return (true, input.slice(0x20 + offset, length));
}
}

/**
* @dev Attempts to decode a `bytes` value from a bytes input (in calldata). Returns a boolean indicating success
* and a slice pointing to the decoded buffer. If decoding fails, returns an empty slice.
*/
function tryDecodeBytesCalldata(bytes calldata input) internal pure returns (bool success, bytes calldata output) {
unchecked {
uint256 inputLength = input.length;
if (inputLength < 0x20) {
return (false, Calldata.emptyBytes());
}
uint256 offset = uint256(bytes32(input[0x00:0x20]));
if (inputLength - 0x20 < offset) {
return (false, Calldata.emptyBytes());
}
uint256 length = uint256(bytes32(input[offset:offset + 0x20]));
if (inputLength - 0x20 - offset < length) {
return (false, Calldata.emptyBytes());
}
return (true, input[0x20 + offset:0x20 + offset + length]);
}
}
}
5 changes: 5 additions & 0 deletions contracts/utils/Memory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ library Memory {

type Slice is bytes32;

/// @dev Empty slice
function emptySlice() internal pure returns (Slice) {
return Slice.wrap(0);
}

/// @dev Get a slice representation of a bytes object in memory
function asSlice(bytes memory self) internal pure returns (Slice result) {
assembly ("memory-safe") {
Expand Down
48 changes: 48 additions & 0 deletions test/utils/AbiDecode.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.24;

import {Test} from "forge-std/Test.sol";
import {AbiDecode} from "@openzeppelin/contracts/utils/AbiDecode.sol";
import {Memory} from "@openzeppelin/contracts/utils/Memory.sol";

contract AbiDecodeTest is Test {
using AbiDecode for *;
using Memory for *;

function testDecode(bytes memory buffer) public pure {
(bool success, Memory.Slice output) = abi.encode(buffer).tryDecodeBytes();
assertTrue(success);
assertEq(output.toBytes(), buffer);
}

function testDecodeNoRevert(bytes memory buffer) public pure {
(bool success, Memory.Slice output) = buffer.tryDecodeBytes();
if (success) {
assertEq(output.toBytes(), abi.decode(buffer, (bytes)));
} else {
assertEq(output.toBytes(), new bytes(0));
}
}

function testDecodeCalldata(bytes memory buffer) public view {
(bool success, bytes memory output) = this.__tryDecodeBytesCalldata(abi.encode(buffer));
assertTrue(success);
assertEq(output, buffer);
}

function testDecodeCalldataNoRevert(bytes calldata buffer) public pure {
(bool success, bytes calldata output) = buffer.tryDecodeBytesCalldata();
if (success) {
assertEq(output, new bytes(0));
} else {
assertEq(output, new bytes(0));
}
Comment on lines +34 to +40
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix the success-branch assertion in testDecodeCalldataNoRevert.

Line 37 currently asserts empty bytes even when success == true, so the success path is not actually verifying decoded content.

✅ Suggested test fix
 function testDecodeCalldataNoRevert(bytes calldata buffer) public pure {
     (bool success, bytes calldata output) = buffer.tryDecodeBytesCalldata();
     if (success) {
-        assertEq(output, new bytes(0));
+        assertEq(output, abi.decode(buffer, (bytes)));
     } else {
         assertEq(output, new bytes(0));
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/utils/AbiDecode.t.sol` around lines 34 - 40, The success branch in
testDecodeCalldataNoRevert is asserting empty bytes incorrectly; when
tryDecodeBytesCalldata returns success == true you should verify the decoded
output matches the original input buffer. Update the success branch assertion in
testDecodeCalldataNoRevert to compare output to the original buffer (e.g.,
assertEq(output, bytes(buffer)) or equivalently abi.decode(buffer, (bytes))) so
that tryDecodeBytesCalldata, success, and output are validated correctly.

}

function __tryDecodeBytesCalldata(
bytes calldata buffer
) external pure returns (bool success, bytes calldata output) {
return buffer.tryDecodeBytesCalldata();
}
}
Loading