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.
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 dataSection 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.
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.
// 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.
// 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
// 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.