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
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) {
bytes memory collision1 = abi.encodePacked("0x123", "456");
bytes memory collision2 = abi.encodePacked("0x1", "23456");
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)));
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();
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.