BlockchainSolidity9 min readUpdated

The bytes Type in Solidity: bytes32, Dynamic bytes, and ABI Encoding

By Mudassir Khan — Agentic AI Consultant & AI Systems Architect, Islamabad, Pakistan

Cover illustration for: The bytes Type in Solidity: bytes32, Dynamic bytes, and ABI Encoding

Section 01 · Definition

What is bytes in Solidity?

bytes is the type for raw binary data. It comes in two shapes: fixed-size (bytes1 ... bytes32) and dynamic (bytes).

Quick answer

What is bytes? bytes is Solidity's binary data type. The fixed-size family bytes1 through bytes32 stores exactly that many bytes inline (one storage slot for bytes32). The dynamic bytes type stores a variable number of bytes laid out the same way as string. Both types support indexing — bytes[0] returns a bytes1 — unlike string.

solidity
bytes32 public root;            // Merkle root, ECDSA hash, etc.
bytes4  public selector;        // first 4 bytes of keccak256("transfer(address,uint256)")
bytes1  public flag;

bytes   public payload;         // dynamic length raw data

Section 02 · Three close cousins

bytes32 vs bytes vs string

Picking between fixed bytes, dynamic bytes, and string is one of the small decisions that compounds across a contract's gas profile.

Three card comparison of bytes32, dynamic bytes, and string with use cases and gas notes.
Use the smallest, most fixed type that fits your data.

The decision tree is simple. Is the data exactly N bytes, with N at most 32? Use bytesN. Is it variable length raw binary? Use dynamic bytes. Is it variable length, intended as human-readable text, and going to be displayed in a wallet? Use string.

Section 03 · Hashing

bytes32 is what keccak256 returns

The most common bytes32 in any contract is a hash. Solidity's three hash functions all return bytes32 and that fact shapes the rest of the type system.

solidity
// keccak256 — the workhorse hash on Ethereum
bytes32 hashOfPayload = keccak256(abi.encode(amount, recipient, deadline));

// Hashed event topics
bytes32 constant TRANSFER_EVENT_HASH =
    keccak256("Transfer(address,address,uint256)");

// Function selector — first 4 bytes of the hash
bytes4 transferSelector = bytes4(keccak256("transfer(address,uint256)"));

Every signature verification, Merkle proof, commit-reveal scheme, and ERC712 typed message starts with a bytes32 hash. Storing one is one storage slot. Comparing two costs almost nothing. That predictability is why bytes32 is the most common state shape after uint256 and address.

Section 04 · ABI encoding

abi.encode and abi.encodePacked

Dynamic bytes is the type that abi encoding produces. You will use it whenever a contract talks to another contract through a low-level call.

solidity
// Encode arguments to send through a low-level call
bytes memory data = abi.encodeWithSelector(
    IERC20.transfer.selector,
    recipient,
    amount
);

(bool ok, bytes memory result) = address(token).call(data);
require(ok, "call failed");

// abi.encodePacked is shorter but unsafe for hashing across types
bytes memory packed = abi.encodePacked(uint256(1), uint256(2));

// abi.encode is safe — fixed 32-byte alignment per argument
bytes memory padded = abi.encode(uint256(1), uint256(2));

Use abi.encode for hashes, never encodePacked

abi.encodePacked omits padding, which means encodePacked('a', 'bc') and encodePacked('ab', 'c') produce the same bytes. If you hash that, two different inputs give the same hash — a collision. For any keccak256 input that needs to uniquely identify a tuple, use abi.encode.

Section 05 · Real-world uses

Where bytes shows up in production code

solidity
// 1. Merkle tree airdrop verification
function claim(bytes32[] calldata proof, uint256 amount) external {
    bytes32 leaf = keccak256(abi.encode(msg.sender, amount));
    require(MerkleProof.verify(proof, root, leaf), "bad proof");
}

// 2. ECDSA signature recovery
function isSignedByOwner(bytes32 digest, bytes calldata sig) external view returns (bool) {
    address signer = ECDSA.recover(digest, sig);
    return signer == owner;
}

// 3. Commit-reveal randomness
mapping(address => bytes32) public commitments;

function commit(bytes32 hashedSecret) external { commitments[msg.sender] = hashedSecret; }
function reveal(uint256 secret, bytes32 nonce) external {
    require(keccak256(abi.encode(secret, nonce)) == commitments[msg.sender], "bad reveal");
}

// 4. Calldata forwarding (proxy pattern)
fallback() external payable {
    (bool ok, bytes memory result) = implementation.delegatecall(msg.data);
    // ...
}

Section 07 · FAQ

Frequently asked questions

When should I use bytes32 instead of string?

When the data is binary (a hash, a selector, a key) or when the text is short, fixed length, and never displayed to a human directly. bytes32 fits in one storage slot and reads with one SLOAD; a string of the same length spreads across multiple slots and costs significantly more.

What is the difference between abi.encode and abi.encodePacked?

abi.encode pads each argument to 32 bytes, producing the same encoding the EVM ABI uses for function calls. abi.encodePacked drops padding, producing a more compact byte string. encodePacked is unsafe for hashing because two different argument tuples can produce identical packed bytes.

Can I cast bytes32 to a string?

Not directly. You go through bytes: string(abi.encodePacked(myBytes32)). The result is only valid UTF-8 if the original 32 bytes happened to be valid UTF-8 — for a hash, they usually are not, and most wallets will display garbled text.

How do function selectors work?

A function selector is the first 4 bytes of keccak256("functionName(argTypes)"). When you call f(x, y), the EVM looks up the function by matching the first 4 bytes of calldata against the selectors of every public function. You can compute one with bytes4(keccak256("transfer(address,uint256)")).

Is bytes cheaper than string?

They share storage layout, so storage cost is the same. bytes is slightly easier to work with for raw binary because you can index it (bytes[i] is bytes1) and pass it to abi functions without conversion. Reach for string only when the data is text and the wallet will display it.

Written by Mudassir Khan

Agentic AI consultant and AI systems architect based in Islamabad, Pakistan. CEO of Cube A Cloud. 38+ agentic AI launches delivered for global founders and CTOs.

View agentic AI consulting serviceSee ChainTrust case study

Related service

Agentic AI Consulting

See scope & pricing →

Related case study

ChainTrust Compliance Engine

Read case study →

More on this topic

Need an AI systems architect?

Book a 30-minute architecture call. I will sketch the high-level design for your use case and give you an honest view of the trade-offs.

Book a strategy call →