All guides

Function Selectors Explained

How Solidity turns a function signature into a 4-byte selector, why selectors matter for diamonds, and how selector collisions actually happen.

8 min read

Every external call to an Ethereum contract begins with four bytes that say which function you want. Those four bytes are the function selector. For most contracts this is an implementation detail you never think about. For diamonds it is the central organising concept — a diamond is essentially a routing table keyed by selector.

How a selector is computed

Take the function's canonical signature, hash it with keccak-256, and keep the first four bytes:

// The canonical signature is the name plus parameter types.
// No spaces, no parameter names, no return types.
transfer(address,uint256)

// keccak256("transfer(address,uint256)")
// = 0xa9059cbb2ab09eb219583f4a59a5d0623ade346d962bcd4e46b11da047c9049b
//    ^^^^^^^^
// The selector is the first 4 bytes: 0xa9059cbb

When you call a contract, the EVM does not receive a function name. It receives calldata whose first four bytes are this selector, followed by ABI-encoded arguments. Solidity's dispatcher compares those bytes against the selectors it knows and jumps to the matching code. A diamond does the same thing, except the lookup table lives in storage and can be modified after deployment.

What "canonical" means

The canonical signature includes the function name and the parameter types only. Parameter names, the calldata/memory location, visibility, mutability and return types are all excluded:

// These all produce the SAME selector, because only types matter:
function transfer(address to, uint256 amount) external;
function transfer(address recipient, uint256 value) external;
function transfer(address, uint256) external returns (bool);

// These produce DIFFERENT selectors:
function transfer(address,uint256)   // 0xa9059cbb
function transfer(address,uint128)   // different type
function transfer(address[],uint256) // different type

There are a few normalisation rules that trip people up when computing selectors by hand, mostly around type aliases and composite types:

// Aliases that must be expanded before hashing:
uint    -> uint256
int     -> int256
ufixed  -> ufixed128x18
byte    -> bytes1

// Structs become tuples:
struct Order { address maker; uint256 amount; }
function fill(Order calldata o) external;
// hashes as: fill((address,uint256))

// Enums become their underlying uint8:
function setAction(FacetCutAction a) external;
// hashes as: setAction(uint8)

This is why a hand-written selector list is a bad idea. Let the compiler tell you: in Solidity, this.myFunction.selector or type(IFoo).interfaceId produce the right values, and forge inspect <Contract> methods prints the whole table.

Why selectors matter so much in a diamond

In a conventional contract, the compiler guarantees you cannot register the same selector twice — it simply will not compile. A diamond has no such guarantee, because facets are combined at runtime by diamondCut. The standard therefore requires the diamond to enforce these rules itself:

  • Add must revert if the selector is already registered to some facet. Two facets can never both own the same selector.
  • Replace must revert if it would point a selector at its current facet.
  • Remove must revert if the selector is not currently registered.

The consequence is that adding an innocuous helper to a facet can make an upgrade revert, because a completely unrelated facet already claimed that selector. Louper shows you the full selector table for a deployed diamond, which is the fastest way to diagnose this class of failure.

Selector collisions are real

Four bytes gives roughly 4.3 billion possible values. That sounds like plenty, but by the birthday bound you only need on the order of 80,000 random signatures before a collision becomes likely — and an attacker searching deliberately can find one for a chosen target in seconds on a laptop.

// Real, famous example — both hash to 0x42966c68:
function burn(uint256) external;

// And this contrived-but-valid signature:
function collate_propagate_storage(bytes16) external;
// also 0x42966c68

The burn(uint256) collision above is a well known curiosity. In practice, accidental collisions between two sensible-looking function names are rare, but they are not impossible, and deliberate collisions are trivially cheap to construct. In a diamond the risk is not that a collision goes unnoticed — the diamondCut checks catch it — but that an attacker who can influence which facets get added could route a legitimate-looking selector to hostile code.

Unknown selectors

When a facet's source is not verified, all that can be recovered from the chain is the selector list. The names are gone. Louper falls back to selector databases to reverse the mapping, and when no match exists it displays the raw selector as unknown_0x12345678.

An unverified facet is worth pausing on. You can see that a function exists and that it can be called, but not what it does. For a protocol handling real value, unverified facets should be treated as an open question during review — see the security checklist.

Practical tips

  • Never hard-code selector constants that you computed by hand; derive them in code.
  • Before an upgrade, diff the deployed selector set against the set your new facets export. Louper's ABI export makes this a one-line comparison.
  • Keep facets narrow and single-purpose. Small facets collide less often and are easier to reason about.
  • Remember that supportsInterface from ERC-165 is itself a selector (0x01ffc9a7) that must be registered like any other.