Function Selector Collision

Vulnerability where proxy and implementation contracts have identical 4-byte function signatures, causing unpredictable behavior.

Function selector collision occurs when a proxy contract and its implementation contract have functions with identical 4-byte signatures (selectors), causing ambiguity in which function should be executed. This can lead to unintended behavior, bypass of security controls, or complete system compromise.

Understanding Function Selectors

Ethereum uses the first 4 bytes of a function signature's keccak256 hash as the selector:

1// Function: transfer(address,uint256)
2// Selector: bytes4(keccak256("transfer(address,uint256)")) = 0xa9059cbb
3
4contract Example {
5 function transfer(address to, uint256 amount) external {
6 // Function body
7 }
8}

When a contract receives a call, it matches the first 4 bytes of calldata against its function selectors to determine which function to execute.

How Collisions Occur in Proxies

Proxy Management Functions

Proxy contracts typically have administrative functions like upgrade(), admin(), or implementation(). If the implementation accidentally includes functions with the same selectors, collisions occur:

1// Proxy contract
2contract SimpleProxy {
3 address public implementation;
4 address public admin;
5
6 function upgrade(address newImpl) external {
7 require(msg.sender == admin, "Not admin");
8 implementation = newImpl;
9 }
10
11 fallback() external payable {
12 _delegate(implementation);
13 }
14}
15
16// Implementation contract - COLLISION!
17contract BadImplementation {
18 mapping(address => uint256) private _balances;
19
20 // DANGEROUS: Same selector as proxy's upgrade function!
21 function upgrade(address recipient) external {
22 // This was meant to be "upgrade user status"
23 // but collides with proxy upgrade function!
24 _balances[recipient] = 1000000;
25 }
26}

In this example:

  • Proxy's upgrade(address) manages contract upgrades
  • Implementation's upgrade(address) manages user upgrades
  • Both have the same selector: bytes4(keccak256("upgrade(address)"))

The Collision Problem

When a user calls upgrade(address):

  1. The proxy receives the call
  2. It matches the selector to its own upgrade function
  3. Instead of delegating to implementation, it executes proxy logic
  4. This bypasses the implementation's intended functionality

Types of Selector Collisions

1. Administrative Function Collisions

Most dangerous - attacker can call proxy admin functions through implementation interface:

1// Proxy admin function
2function changeAdmin(address newAdmin) external;
3
4// Implementation function (different purpose, same selector)
5function changeAdmin(address userAddr) external {
6 // Intended to change user admin status
7 // But actually changes proxy admin!
8}

2. Hidden Function Collisions

Functions with different names can have the same selector:

1// These functions have the same selector!
2console.log(web3.utils.keccak256("func1(bytes)").slice(0,10)); // 0x47c0e3ab
3console.log(web3.utils.keccak256("func2(bytes)").slice(0,10)); // 0x47c0e3ab
4
5// Different function names, same selector!
6function func1(bytes memory data) external {}
7function func2(bytes memory info) external {}

3. Accidental Overloading Collisions

When implementation tries to override proxy functions:

1contract VulnerableImpl {
2 // Trying to "override" proxy's admin function
3 // But this creates a collision instead of override!
4 function admin() external view returns (address) {
5 return address(0x1234); // Hardcoded admin
6 }
7}

Real-World Collision Examples

Example 1: Parity Wallet Hack

The Parity multi-sig wallet had a collision between:

  • Library's initWallet() function
  • Implementation's initialization function
  • Attacker called initWallet() on the library, becoming owner
  • Then called kill(), destroying the library and bricking 587 wallets

Example 2: Function Name Brute Force

Attackers can generate functions with specific selectors to create collisions:

1import itertools
2from Crypto.Hash import keccak
3
4target_selector = "0xa9059cbb" # transfer(address,uint256)
5
6# Brute force function names with target selector
7for length in range(1, 10):
8 for chars in itertools.product('abcdefghijklmnopqrstuvwxyz', repeat=length):
9 func_name = ''.join(chars)
10 signature = f"{func_name}(address,uint256)"
11 hash = keccak.new(digest_bits=256).update(signature.encode()).digest()
12 selector = "0x" + hash[:4].hex()
13
14 if selector == target_selector:
15 print(f"Found collision: {signature}")
16 break

Transparent Proxy Solution

OpenZeppelin's Transparent Proxy pattern solves this by separating admin and user calls:

1contract TransparentUpgradeableProxy {
2 bytes32 private constant _ADMIN_SLOT =
3 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;
4
5 modifier ifAdmin() {
6 if (msg.sender == _getAdmin()) {
7 _;
8 } else {
9 _fallback();
10 }
11 }
12
13 function upgrade(address newImplementation) external ifAdmin {
14 _upgradeTo(newImplementation);
15 }
16
17 function admin() external ifAdmin returns (address) {
18 return _getAdmin();
19 }
20
21 function implementation() external ifAdmin returns (address) {
22 return _getImplementation();
23 }
24
25 fallback() external payable {
26 _fallback();
27 }
28
29 function _fallback() internal {
30 // Only delegate to implementation if not admin
31 require(msg.sender != _getAdmin(), "Admin cannot call fallback");
32 _delegate(_getImplementation());
33 }
34}

How it works:

  • If caller is admin → execute proxy functions
  • If caller is NOT admin → delegate to implementation
  • Admin can never call implementation functions
  • Users can never call proxy admin functions

Detection and Prevention

1. Automated Collision Detection

Tools to check for selector collisions:

1// Check for collisions between proxy and implementation
2function checkCollisions(proxyABI, implABI) {
3 const proxySelectors = new Set();
4 const implSelectors = new Set();
5
6 // Extract selectors from ABIs
7 proxyABI.forEach(func => {
8 if (func.type === 'function') {
9 const selector = web3.utils.keccak256(func.signature).slice(0, 10);
10 proxySelectors.add(selector);
11 }
12 });
13
14 implABI.forEach(func => {
15 if (func.type === 'function') {
16 const selector = web3.utils.keccak256(func.signature).slice(0, 10);
17 if (proxySelectors.has(selector)) {
18 console.log(`COLLISION DETECTED: ${func.signature}`);
19 }
20 }
21 });
22}

2. Safe Function Naming

Use prefixes or unique patterns for implementation functions:

1contract SafeImplementation {
2 // Use prefixes to avoid collisions
3 function impl_upgrade(address user) external {
4 // Implementation-specific upgrade logic
5 }
6
7 function impl_admin() external view returns (address) {
8 // Implementation-specific admin logic
9 }
10}

3. UUPS Pattern (User-Controlled Upgrades)

UUPS moves upgrade logic to implementation, avoiding proxy admin functions:

1contract UUPSImplementation is UUPSUpgradeable {
2 function _authorizeUpgrade(address) internal override onlyOwner {
3 // Upgrade authorization logic in implementation
4 }
5
6 // No collision with proxy admin functions since there are none!
7 function admin() external view returns (address) {
8 return owner();
9 }
10}

Testing for Collisions

1describe("Function Selector Collisions", function() {
2 it("should detect collisions between proxy and implementation", async function() {
3 const proxyInterface = await proxy.interface;
4 const implInterface = await implementation.interface;
5
6 const proxySelectors = Object.keys(proxyInterface.functions);
7 const implSelectors = Object.keys(implInterface.functions);
8
9 const collisions = proxySelectors.filter(
10 selector => implSelectors.includes(selector)
11 );
12
13 expect(collisions).to.have.length(0,
14 `Found collisions: ${collisions.join(', ')}`);
15 });
16
17 it("should route admin calls correctly", async function() {
18 // Admin calling upgrade should work
19 await proxy.connect(admin).upgrade(newImpl.address);
20
21 // User calling upgrade should delegate to implementation
22 await proxy.connect(user).upgrade(user.address);
23 // This should call implementation's upgrade function, not proxy's
24 });
25});

Mitigation Strategies

1. Use Established Proxy Patterns

  • OpenZeppelin's Transparent Proxy
  • UUPS (EIP-1822) pattern
  • Diamond Standard (EIP-2535)

2. Function Signature Analysis

Always check for collisions during development:

1# Use tools like slither to detect collisions
2slither . --detect function-selector-collisions

3. Clear Interface Separation

Keep proxy admin functions and implementation business logic completely separate:

1// Proxy admin interface
2interface IProxyAdmin {
3 function upgrade(address) external;
4 function admin() external view returns (address);
5}
6
7// Implementation business interface
8interface IToken {
9 function transfer(address, uint256) external returns (bool);
10 function balanceOf(address) external view returns (uint256);
11}

Function selector collision is a subtle but critical vulnerability in proxy patterns. Proper detection, established patterns like Transparent Proxy, and careful interface design are essential to prevent these potentially devastating attacks.

Need expert guidance on Function Selector Collision?

Our team at Zealynx has deep expertise in blockchain security and DeFi protocols. Whether you need an audit or consultation, we're here to help.

Get a Quote

oog
zealynx

Subscribe to Our Newsletter

Stay updated with our latest security insights and blog posts

© 2024 Zealynx