The proxy's fallback receives a call to upgradeTo(address). The selector is 0x3659cfe6. The transparent proxy checks: is this caller the admin? No — forward to implementation. The implementation happens to have a different function with the same 4-byte prefix. That function is not upgradeTo. It transfers tokens. The caller wanted an upgrade. They got a token transfer instead.
That is a function selector collision. It is not theoretical. Two functions with entirely different signatures can hash to the same first 4 bytes of keccak256. In a normal contract, this is a compile-time error. In a proxy, it is a silent runtime redirect.
This post covers every major way selector collisions become exploitable vulnerabilities: shadowed admin functions, transparent proxy routing bugs, Diamond facet overwrites, and cross-contract ABI confusion.
What Function Selectors Are
When you call a Solidity function, the EVM does not know function names. It knows 4-byte selectors: the first 4 bytes of the keccak256 hash of the function's canonical signature.
// These are equivalent
bytes4 selector = bytes4(keccak256("transfer(address,uint256)"));
// selector == 0xa9059cbb
The ABI encoding prepends this selector to calldata. When a contract receives a call, its dispatcher reads the first 4 bytes and routes to the matching function. If nothing matches, execution falls through to fallback().
Why collisions happen: 4 bytes is 2^32 possibilities — about 4.3 billion. That sounds large until you consider that the Ethereum function signature space has tens of millions of plausible human-readable signatures, and birthday-paradox probabilities make collisions findable in hours with a basic script. For any target selector, you can likely find a human-readable signature that matches it.
Why proxies make this dangerous: A standard contract's ABI is defined at compile time. The compiler catches selector duplicates. A proxy routes calls at runtime across two separate codebases — the proxy contract itself and the implementation contract. The compiler cannot see across that boundary. Collisions between proxy functions and implementation functions are invisible until execution.
Vulnerability 1: Admin Function Shadowed by Implementation
This is the core selector clash threat in proxy architectures. The proxy defines upgradeTo(address) (selector 0x3659cfe6). The implementation happens to have a function — perhaps a legacy function, perhaps a coincidental collision — whose signature also hashes to 0x3659cfe6.
// Proxy (simplified)
contract SimpleProxy {
address implementation;
address admin;
function upgradeTo(address newImpl) external {
require(msg.sender == admin, "not admin");
implementation = newImpl;
}
fallback() external payable {
address impl = implementation;
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
}
// Implementation — attacker controls deployment
contract MaliciousImpl {
// Selector 0x3659cfe6 — same as upgradeTo(address)
// Found via brute force: "collide(bytes28)" hashes to the same prefix
function collide(bytes28 data) external {
// Executes instead of proxy's upgradeTo when non-admin calls 0x3659cfe6
// Can be used to drain funds, corrupt state, or trigger unintended logic
}
}
When the proxy receives 0x3659cfe6 from the admin, it routes to upgradeTo. From any other caller, the fallback triggers delegatecall to the implementation — which hits collide(bytes28) instead. An attacker who can influence which implementation gets deployed can insert a selector-matched function to intercept calls that were intended for admin-only proxy functions.
The UUPS vs. Transparent Proxy tradeoff exists precisely because of this problem. The Transparent Proxy pattern solves it by routing based on caller identity: admin calls always stay in the proxy, user calls always go to the implementation. This prevents any implementation function from shadowing proxy admin functions for the admin. UUPS solves it differently — by moving upgradeTo into the implementation itself, eliminating the proxy-side selector entirely. Both approaches are valid responses to the same threat.
// Transparent Proxy pattern — caller-based routing
function _fallback() internal {
if (msg.sender == _admin()) {
// Admin: only proxy admin functions are accessible
// Implementation functions are NOT callable by admin
require(msg.sig == UPGRADE_SELECTOR || msg.sig == ADMIN_SELECTOR, "admin: use proxy interface");
} else {
// User: always delegatecall to implementation
_delegate(_implementation());
}
}
Vulnerability 2: Transparent Proxy Admin Routing Bug
The transparent proxy design prevents the admin from accidentally calling implementation functions — but it introduces a different failure mode. If the admin needs to interact with the implementation as a user (to call a business-logic function), they cannot do it directly. They must go through a separate address.
The bug surfaces when teams modify the transparent proxy pattern to relax this restriction:
// MODIFIED (vulnerable) transparent proxy — tries to be "flexible"
function _fallback() internal {
if (msg.sender == _admin() && _isAdminFunction(msg.sig)) {
// Admin calling an admin function — handle in proxy
_dispatchAdminFunction();
} else {
// Everyone else — delegatecall to implementation
_delegate(_implementation());
}
}
function _isAdminFunction(bytes4 sig) internal pure returns (bool) {
return sig == 0x3659cfe6 || sig == 0x8f283970; // upgradeTo, changeAdmin
// If an implementation function has 0x3659cfe6 or 0x8f283970 as its selector...
// ...and the admin calls it, it routes to the proxy instead of implementation
}
The problem: _isAdminFunction must be a complete and correct list of all admin selectors. Add a new admin function and forget to add its selector to the check — or have an implementation function whose selector matches one in the list — and the routing fails silently.
OpenZeppelin's fix for this is ProxyAdmin: a separate contract that the admin uses to interact with the proxy. The proxy's admin is always ProxyAdmin, never an EOA or a multisig directly. The admin can interact with the implementation as a user from their own address, bypassing the transparent proxy restriction. This is not just a convenience — it is a security property.
Vulnerability 3: Diamond Proxy (EIP-2535) Selector Collision
The Diamond pattern (EIP-2535) takes the proxy concept further: instead of one implementation, you have many — called facets. Each facet handles a set of function selectors. A central mapping routes incoming selectors to the correct facet.
// Diamond routing simplified
mapping(bytes4 => address) selectorToFacet;
fallback() external payable {
address facet = selectorToFacet[msg.sig];
require(facet != address(0), "function not found");
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
When you install a facet via diamondCut, you register its selectors. If FacetB declares a selector that FacetA already registered, one of two things happens depending on the implementation: the registration reverts (safe, explicit), or FacetB's selector silently overwrites FacetA's (dangerous).
// DiamondCut operation
IDiamond.FacetCut[] memory cuts = new IDiamond.FacetCut[](1);
cuts[0] = IDiamond.FacetCut({
facetAddress: address(facetB),
action: IDiamond.FacetCutAction.Add,
functionSelectors: facetBSelectors
});
diamond.diamondCut(cuts, address(0), "");
// If facetBSelectors contains a selector already in facetA:
// - Reference implementation: REVERTS with "already added"
// - Custom implementations: may silently overwrite
// - Result: facetA's function is now UNREACHABLE
The silent drop case is the dangerous one. No event fires for the lost function. No transaction reverts. FacetA.withdraw() is simply no longer reachable because its selector now points to FacetB.emergencyPause(). Users calling withdraw() will hit emergencyPause() — or vice versa.
The EIP-2535 reference implementation includes duplicate selector detection in _addFunctions. Custom Diamond implementations that skip this check, or that implement Replace actions without validating the incoming selector set, can introduce the silent-drop failure.
Before any diamondCut, enumerate all registered selectors and check for overlap with the incoming facet:
// Off-chain check before diamondCut
function checkSelectorConflicts(
IDiamond diamond,
bytes4[] memory newSelectors
) external view returns (bytes4[] memory conflicts) {
IDiamondLoupe.Facet[] memory facets = IDiamondLoupe(address(diamond)).facets();
// Build existing selector set, check newSelectors against it
}
Vulnerability 4: Cross-Contract ABI Confusion
Selector collisions do not require proxies. Any contract-to-contract call that relies on a hardcoded selector is a potential target.
// Contract A calls Contract B, expecting transfer(address,uint256)
// Selector: 0xa9059cbb
contract CallerA {
function sendTokens(address token, address to, uint256 amount) external {
(bool ok,) = token.call(
abi.encodeWithSelector(0xa9059cbb, to, amount)
);
require(ok);
}
}
If token is not an ERC-20 but a contract that happens to have a different function with selector 0xa9059cbb, the call succeeds — returning ok = true — but executed the wrong function. This is not hypothetical: the Ethereum signature database documents hundreds of known collisions across deployed contracts.
The more common real-world version: contract A integrates with contract B via an interface, contract B is upgraded or replaced with a version that changed a function signature slightly, and the selector no longer matches the expected behavior. The call still lands somewhere — just not where intended.
// Safe pattern: use named interfaces, not raw selectors
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
}
contract SafeCaller {
function sendTokens(IERC20 token, address to, uint256 amount) external {
// Compiler derives selector from interface — type-checked
bool ok = token.transfer(to, amount);
require(ok);
}
}
Always call external contracts through typed interfaces. Hardcoded selectors (abi.encodeWithSelector(0xabcd1234, ...)) bypass compile-time ABI validation.
Finding Collisions: A Python Brute-Force Tool
Selector collision detection is a solved problem. Given a target selector, you can find colliding signatures in minutes:
from web3 import Web3
import itertools
import string
TARGET = bytes.fromhex("42966c68") # known collision target
def get_selector(sig: str) -> bytes:
return Web3.keccak(text=sig)[:4]
# Brute-force approach: test known signatures from a corpus
def find_collision_in_corpus(target: bytes, corpus: list[str]) -> list[str]:
return [sig for sig in corpus if get_selector(sig) == target]
# Example: check your contract's selectors against a known collision database
def check_contract_selectors(signatures: list[str]) -> dict:
seen = {}
collisions = {}
for sig in signatures:
sel = get_selector(sig).hex()
if sel in seen:
collisions[sel] = [seen[sel], sig]
seen[sel] = sig
return collisions
# Demo
sigs = [
"burn(uint256)",
"gasprice_bit_ether(int128)", # known to collide with burn(uint256)
"transfer(address,uint256)",
"withdraw(uint256)",
]
print(check_contract_selectors(sigs))
# Output: {'42966c68': ['burn(uint256)', 'gasprice_bit_ether(int128)']}
The Canonical Collision: burn and gasprice_bit_ether
The most-cited real selector collision in Solidity:
burn(uint256)— selector0x42966c68gasprice_bit_ether(int128)— selector0x42966c68
Both hash to the same 4 bytes. This is a documented, known collision from the Ethereum signature registry. Neither function is rare — burn(uint256) appears in thousands of ERC-20 tokens.
A contract that exposes both functions cannot be compiled. But a proxy with burn(uint256) in the implementation and a facet or library with gasprice_bit_ether(int128) will route both selectors to whichever function registers last. One function becomes permanently unreachable.
This specific collision is included in most selector clash detection tools as a test case. Its existence proves the threat is real — and that known-bad signatures can be screened out deterministically.
Vulnerable vs. Secure Patterns
Vulnerable: naive proxy with no selector isolation
// Proxy and implementation share selector space — no routing protection
contract NaiveProxy {
address impl;
function upgradeTo(address newImpl) external onlyAdmin {
impl = newImpl; // selector 0x3659cfe6
}
fallback() external {
// ALL non-admin calls go here — including 0x3659cfe6 from non-admin
impl.delegatecall(msg.data);
}
}
// Problem: if impl has a function with selector 0x3659cfe6, non-admin calls
// to that function are silently handled by the implementation instead of
// reverting — and admin calls never reach the implementation's version
Secure: Transparent Proxy with caller-based routing
// Only admin can call proxy functions; admin cannot call implementation
// Non-admin always reaches implementation — no selector ambiguity
contract TransparentProxy {
function _fallback() internal {
if (msg.sender == _admin()) {
// Admin: route to proxy logic only
if (msg.sig == bytes4(keccak256("upgradeTo(address)"))) {
_upgradeTo(abi.decode(msg.data[4:], (address)));
} else {
revert("admin: call via ProxyAdmin");
}
} else {
_delegate(_implementation());
}
}
}
Secure: UUPS — upgrade logic in implementation, no proxy-side selector
// Proxy has NO admin functions — no selectors to clash against
contract UUPSProxy {
fallback() external payable {
_delegate(_getImplementation());
// All calls, including upgradeTo, go to implementation
// Upgrade authorization is enforced by the implementation's onlyOwner check
}
}
The UUPS proxy has zero admin selectors. There is nothing in the proxy to clash against. The tradeoff: if the implementation's _authorizeUpgrade guard is missing or misconfigured, upgrades are unprotected — the clash problem is eliminated, but access control responsibility shifts entirely to the implementation.
What ContractScan Detects
| Vulnerability | Detection Method |
|---|---|
| Selector collision between proxy admin functions and implementation | AI analysis + selector enumeration |
Diamond facet selector overlap on diamondCut |
Static analysis + facet selector diff |
| Hardcoded selector usage without interface type-checking | Static analysis (Slither, Semgrep) |
| Known collision pairs (burn/gasprice_bit_ether and others) | Signature registry lookup |
Missing ProxyAdmin pattern in Transparent Proxy |
AI pattern matching |
UUPS proxy without _disableInitializers in implementation |
Static analysis |
| Custom proxy with no caller-based routing isolation | AI semantic analysis |
Audit Your Proxy Contract
Selector clashes are silent — no revert, no event, no warning. The wrong function executes and the call succeeds. That makes them particularly dangerous in production upgrades, where a new implementation or facet can silently shadow existing functionality.
Before deploying or upgrading any proxy, enumerate all selectors on both sides of the delegatecall boundary and check for overlap. For Diamond contracts, do this before every diamondCut.
Audit your proxy contract at https://contract-scanner.raccoonworld.xyz
Related: Proxy Pattern Vulnerabilities: UUPS, Transparent, and Diamond — initialization bugs, storage collisions, and DiamondCut access control.
Related: delegatecall Vulnerabilities in Solidity — arbitrary delegatecall, context confusion, and selfdestruct via delegatecall.
Important Notes
This post is for informational and educational purposes only. It does not constitute financial, legal, or investment advice. The security analysis provided is based on available data and automated tools, which may not capture all potential vulnerabilities. Always conduct a professional audit before deploying smart contracts.