Pieces Protocol

First Flight #32
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Severity: high
Invalid

Improper Use of abi.encodePacked() Leading to Hash Collisions in TokenDivider.sol

Summary

The TokenDivider contract improperly uses abi.encodePacked() with dynamic types when concatenating strings. This results in potential hash collisions that could be exploited to disrupt the system’s integrity. The issue is demonstrated in the createTokens function.

Vulnerability Details

The improper use of abi.encodePacked() appears in the following code snippet:

Found in src/TokenDivider.sol [Line: 116]()
```solidity
string(abi.encodePacked(ERC721(nftAddress).name(), "Fraccion")),
```
- Found in src/TokenDivider.sol [Line: 117]()
```solidity
string(abi.encodePacked("F", ERC721(nftAddress).symbol())));
```

Dynamic types concatenated with abi.encodePacked() may produce identical byte sequences for different inputs. This could lead to unexpected behavior if the result is hashed or used in critical operations.

Proof of Concept

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "forge-std/console.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
contract TokenDivider {
function createTokens(address nftAddress) external view returns (string memory, string memory) {
string memory name = string(abi.encodePacked(ERC721(nftAddress).name(), "Fraccion"));
string memory symbol = string(abi.encodePacked("F", ERC721(nftAddress).symbol()));
return (name, symbol);
}
}
contract TokenDividerExploit {
function demonstrateCollision() external pure returns (bool) {
// Simulate hash collisions with dynamic types
bytes memory collision1 = abi.encodePacked("0x123", "456");
bytes memory collision2 = abi.encodePacked("0x1", "23456");
// Log the collisions
console.log("Collision 1: %s", string(collision1));
console.log("Collision 2: %s", string(collision2));
console.log("Hash 1: %s", toHexString(keccak256(collision1)));
console.log("Hash 2: %s", toHexString(keccak256(collision2)));
// Check for identical keccak256 hashes
return keccak256(collision1) == keccak256(collision2);
}
function toHexString(bytes32 data) internal pure returns (string memory) {
bytes memory hexChars = "0123456789abcdef";
bytes memory str = new bytes(64);
for (uint256 i = 0; i < 32; i++) {
str[i * 2] = hexChars[uint8(data[i] >> 4)];
str[1 + i * 2] = hexChars[uint8(data[i] & 0x0f)];
}
return string(str);
}
}
contract TokenDividerExploitTest is Test {
TokenDividerExploit exploit;
function setUp() public {
exploit = new TokenDividerExploit();
}
function testHashCollision() public {
bool result = exploit.demonstrateCollision();
// Assert that a collision occurred
assertTrue(result, "Hash collision did not occur as expected");
}
}

Results:

forge test --match-test testHashCollision -vvvv
[⠰] Compiling...
[⠃] Compiling 1 files with Solc 0.8.28
[⠒] Solc 0.8.28 finished in 382.58ms
Compiler run successful with warnings:
Warning (2018): Function state mutability can be restricted to view
--> test/TokenDividerExploitTest.sol:50:5:
|
50 | function testHashCollision() public {
| ^ (Relevant source part starts here and spans across multiple lines).
Ran 1 test for test/TokenDividerExploitTest.sol:TokenDividerExploitTest
[PASS] testHashCollision() (gas: 60794)
Logs:
Collision 1: 0x123456
Collision 2: 0x123456
Hash 1: 5462d984a8e2b55d8deb1f69505cec3a1118749768d005cc3792f6f32dfd78ee
Hash 2: 5462d984a8e2b55d8deb1f69505cec3a1118749768d005cc3792f6f32dfd78ee
Traces:
[60794] TokenDividerExploitTest::testHashCollision()
├─ [52364] TokenDividerExploit::demonstrateCollision() [staticcall]
│ ├─ [0] console::log("Collision 1: %s", "0x123456") [staticcall]
│ │ └─ ← [Stop]
│ ├─ [0] console::log("Collision 2: %s", "0x123456") [staticcall]
│ │ └─ ← [Stop]
│ ├─ [0] console::log("Hash 1: %s", "5462d984a8e2b55d8deb1f69505cec3a1118749768d005cc3792f6f32dfd78ee") [staticcall]
│ │ └─ ← [Stop]
│ ├─ [0] console::log("Hash 2: %s", "5462d984a8e2b55d8deb1f69505cec3a1118749768d005cc3792f6f32dfd78ee") [staticcall]
│ │ └─ ← [Stop]
│ └─ ← [Return] true
├─ [0] VM::assertTrue(true, "Hash collision did not occur as expected") [staticcall]
│ └─ ← [Return]
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 313.99µs (154.20µs CPU time)
Ran 1 test suite in 3.08ms (313.99µs CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

This demonstrates that different inputs can result in identical outputs, validating the hash collision vulnerability.

Impact

Potential disruption of system functionality.

Vulnerability in any hash-based operations, such as uniqueness checks or access control.

Risk of exploitation by attackers to bypass security measures or manipulate protocol behavior.

Tools Used

Manual code review

aderyn --output report.md

Recommendations

To prevent hash collisions, use abi.encode instead of abi.encodePacked when concatenating dynamic types. This ensures proper padding and prevents overlapping byte sequences.

Updates

Lead Judging Commences

juan_pedro_ventu Lead Judge 4 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Hash collision

Appeal created

0xalexsr Auditor
4 months ago
riceee Auditor
4 months ago
juan_pedro_ventu Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Other

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.