GOMTU Crypto
guidePart 32 of 41 in this guide

Solidity abi.encode vs abi.encodePacked: Differences and Collision Risks

Compare Solidity abi.encode and abi.encodePacked, see why packed dynamic values can collide, and choose safer encoding for hashes and contract calls.

GOMTU
GOMTU
Crypto Research · September 2, 2026 · 7 min read
Share𝕏in
Solidity abi.encode vs abi.encodePacked: Differences and Collision Risks

Two Solidity expressions can hash different-looking inputs to the same value even when Keccak-256 has not been broken. The surprise often comes from the bytes fed into the hash, not the hash function itself. If a contract uses those bytes for authorization, allowlists, or replay protection, an encoding shortcut can become a security boundary.

This guide connects that problem to blockchain basics and compares abi.encode, abi.encodePacked, abi.encodeCall, and explicit concatenation. It is technical education, not financial advice, and code handling assets should receive independent review and testing.

What Are abi.encode and abi.encodePacked?

Advertisement

Both functions turn Solidity values into bytes, but they preserve different information.

Think of shipping two items. Standard ABI encoding puts each item in a labeled, fixed-grid container and records where variable-size cargo begins. Packed encoding removes much of that spacing and metadata to make one compact stream. The compact parcel can be useful, but without separators you may not know where one flexible item ends and the next begins.

abi.encode(...) uses the standard ABI format. Static values occupy 32-byte words. Dynamic values such as string, bytes, and dynamic arrays use offsets and length information, followed by their data. Because the decoder also knows the expected types, abi.decode can reverse a valid standard encoding.

abi.encodePacked(...) uses Solidity's non-standard packed mode. Direct arguments shorter than 32 bytes are generally concatenated without normal padding, while dynamic values are placed in-line without their length. Structs and nested arrays are unsupported, and Solidity provides no matching packed decoder because boundaries can be ambiguous.

The Difference at a Glance

Questionabi.encodeabi.encodePacked
Standard ABI layout?YesNo, non-standard packed mode
Keeps dynamic lengths and offsets?YesNo
Reversible with abi.decode?Yes, with matching typesNo general decoder
Suitable for ordinary call arguments?YesNo
Compact for selected byte construction?Less compactOften compact
Safe with multiple dynamic values in an authentication hash?Preserves boundariesAmbiguous; avoid

Compact does not automatically mean cheaper overall. Gas depends on where encoding runs, whether the result is copied, hashed, stored, or sent as calldata, and what the optimizer can remove. Measure the complete operation instead of treating packed encoding as a universal optimization.

Why Packed Dynamic Values Can Collide

The Solidity ABI specification gives the essential warning: packed encoding becomes ambiguous when two dynamically sized elements appear because their lengths are omitted.

bytes memory x = abi.encodePacked("a", "bc");
bytes memory y = abi.encodePacked("ab", "c");
 
assert(keccak256(x) == keccak256(y));

Both byte streams are simply abc. This is an encoding collision, not a cryptographic collision: the inputs to Keccak-256 are already identical. Moving bytes across the invisible boundary creates another logical tuple with the same packed representation.

The risk becomes concrete when a contract treats this hash as proof of permission:

function digest(string memory role, string memory action)
    external
    pure
    returns (bytes32)
{
    return keccak256(abi.encodePacked(role, action)); // unsafe boundary
}

If an attacker can influence both dynamic fields, the contract may authenticate a tuple different from the one a signer or administrator intended. Adding a visible delimiter is fragile unless the contract strictly forbids that delimiter inside every field and applies one unambiguous escaping rule. Standard encoding is usually the clearer fix:

return keccak256(abi.encode(role, action));

The official documentation advises that when packed data protects signatures, authentication, or integrity, developers should keep the types fixed and allow at most one dynamic argument. Unless there is a compelling reason, it recommends abi.encode.

Static Types Need Care Too

Packed encoding retains less type-shaped structure even without strings. For direct arguments, values such as uint16 use their short representation rather than a full 32-byte word. Explicit casts can therefore change the byte stream:

abi.encodePacked(uint16(0x12)) // 0x0012
abi.encodePacked(uint256(0x12)) // 32-byte representation

Protocol code must define exact types and ordering. Do not let an off-chain service guess whether a number was intended as uint16, uint256, or raw bytes. Cross-language tests should compare the final hexadecimal bytes, not merely the human-readable values.

Which Encoding Function Should You Use?

Hashing structured values

Prefer keccak256(abi.encode(...)) when tuple boundaries matter. For signed structured messages, use an established scheme such as EIP-712 typed-data signing, which includes type hashes and domain separation. EIP-712 itself notes that it does not provide replay protection automatically, so nonce, deadline, chain, contract, and application context still need deliberate design.

Building external contract calls

Prefer abi.encodeCall(functionPointer, (...)) when you have a typed function pointer. Solidity documents that it performs full type checking and produces the selector plus standard-encoded arguments. abi.encodeWithSelector and abi.encodeWithSignature can also construct calls, but they provide less compile-time assurance; a misspelled signature string can silently produce the wrong selector.

Our Ethereum calldata guide explains how the four-byte selector and standard ABI arguments fit inside transaction input.

Concatenating bytes or strings

Use bytes.concat(...) or string.concat(...) when concatenation is the actual intent. Those APIs communicate the goal more clearly than borrowing an encoding function. Still define separators or framing if the result will later be split into multiple fields.

Compact protocol fields

Packed bytes can be appropriate when a protocol specifies an exact fixed-width layout or only one dynamic tail. Document each byte range, validate lengths, and create test vectors for every supported type. Never assume another library reproduces Solidity's packed rules without checking its documentation and output.

Security Checklist

  1. List every encoded field in order, including its exact Solidity type.
  2. Mark each string, bytes, and dynamic array as dynamic.
  3. Reject abi.encodePacked when two attacker-controlled dynamic values share an authentication or integrity hash.
  4. Include domain context—such as chain ID, verifying contract, action, nonce, and deadline—when the threat model requires it.
  5. Prefer abi.encodeCall for typed call construction.
  6. Compare Solidity and off-chain byte vectors in tests.
  7. Add adversarial cases that move bytes across field boundaries.
  8. Have authorization and asset-moving code reviewed independently.

Risks and Common Mistakes

  • Confusing encoding collisions with hash collisions: identical packed bytes naturally produce identical hashes; Keccak-256 need not fail.
  • Hashing without domain separation: an unambiguous tuple can still be replayed in another contract, chain, or action context if the design omits scope.
  • Treating a selector as a complete call: contract calldata needs a four-byte selector plus standard ABI-encoded arguments.
  • Assuming packed bytes are decodable: dynamic boundaries and lengths may be gone.
  • Optimizing before measuring: smaller intermediate bytes do not prove lower end-to-end gas.
  • Trusting one happy-path test: boundary manipulation, empty strings, maximum values, arrays, and cross-language output need coverage.
  • Using encoding correctness as an audit substitute: access control, signature recovery, nonce consumption, expiry, upgradeability, and external calls remain separate risks.

Smart-contract errors can cause irreversible loss. Test on a local network or testnet, use maintained tooling, limit exposed value, and do your own research (DYOR).

FAQ

Is abi.encodePacked deprecated?

The current Solidity documentation still defines it. There has been public discussion about removing it in a future breaking release, but an open discussion is not a released language change. Check the documentation for the exact compiler version you use.

Can I safely pack two addresses?

Two fixed-width address values have identifiable widths when the protocol fixes their order. That avoids the specific missing-length ambiguity of two dynamic strings. You still need domain separation, fixed types, and agreement with off-chain encoders if the hash authorizes anything.

Does adding a separator make two dynamic strings safe?

Only if the framing is provably unambiguous, including escaping and validation. In security-sensitive code, standard ABI encoding or a reviewed typed-data standard is easier to reason about.

Why is there no abi.decodePacked?

Packed encoding may omit padding, offsets, and dynamic lengths. Multiple original tuples can produce the same bytes, so a general inverse cannot know the intended boundaries.

Primary Sources

Sources accessed September 2, 2026:

Choose the representation by the boundary you must preserve. Standard ABI encoding favors structure and reversibility; packed encoding favors compact byte construction under strict constraints. This article is educational and not financial advice (NFA).

Advertisement

Keep learning

Explore related topics

More from GOMTU