---
description: Function selector collision
---

## Function selector collision

**Severity**: `High`
**ID**: `function-selector-collision`

Flags different proxy and implementation function signatures that produce the same four-byte selector and can route implementation calls to the proxy instead.

### What it does

Solidity already rejects selector collisions within one contract and its inheritance tree, so this lint focuses on the separate APIs that coexist at a proxy address. It identifies a proxy and implementation pair when a concrete contract's fallback directly calls the built-in `address.delegatecall` with the full calldata (`msg.data` or the parameterized fallback's `bytes calldata` input while it is still unmodified at that call site) on an address converted from a statically typed contract or interface value. It compares the proxy's effective external API with the target type's effective external API and reports different signatures with the same four-byte selector. Identical signatures are not reported because they are function shadowing rather than a hash collision.

The calldata check is path-sensitive and is performed for each delegatecall. Solidity evaluates the callee, call options, and arguments before making the call, so an assignment to the fallback input in any of those expressions makes the input modified for that call. An assignment after the call does not affect it. When different control-flow paths reach the same call, an unmodified path can still establish the proxy relationship even if another path changed the input.

A direct `msg.sig == Contract.function.selector` comparison limits that path to the selected implementation function, while `!=` excludes it. The comparison can appear with its operands reversed or under negation. The lint keeps selector constraints and calldata state correlated through branches, `try` clauses, loops, short-circuiting boolean expressions, and ternary expressions. It also accounts for exits, `break`, and `continue` before joining paths. Loop exploration is bounded; if it produces too many distinct states, the lint conservatively forgets selector constraints while continuing to distinguish modified from potentially unmodified calldata.

The explicit source-level target type keeps the check conservative. The lint does not compare unrelated contracts across the project, recover target types erased to `address`, follow calldata or selector aliases, recognize delegatecalls hidden behind helper functions, read EIP-1967 slots, or inspect inline-assembly delegatecalls. Inline assembly also makes a parameterized fallback input no longer provably unmodified. Selector filtering requires the direct `msg.sig` equality or inequality form; custom dispatch expressions are not used to narrow the implementation API. Receive functions are excluded because they only handle empty calldata. For these proxy forms, use `forge selectors collision <proxy> <implementation>` to designate the pair explicitly.

### Why is this bad?

A proxy dispatches its own external functions before its fallback. If a proxy function and an implementation function have the same selector, calls intended for the implementation execute the proxy function instead. The implementation function becomes unreachable through the proxy and may produce unexpected state changes or access-control behavior.

### Example

#### Bad

```solidity
interface IImplementation {
    function gsf() external;
}

contract Proxy {
    IImplementation internal implementation;

    // tgeo() and gsf() both have selector 0x67e43e43.
    function tgeo() external {}

    fallback() external payable {
        address(implementation).delegatecall(msg.data);
    }
}
```

#### Good

```solidity
interface IImplementation {
    function gsf() external;
}

contract Proxy {
    IImplementation internal implementation;

    function proxyAdminAction() external {}

    fallback() external payable {
        address(implementation).delegatecall(msg.data);
    }
}
```
