Lack of Fallback Function
id: LS12I
title: Lack of Fallback Function
baseSeverity: I
category: ether-receiving
language: solidity
blockchain: [ethereum]
impact: Rejected ETH transfers due to missing fallback/receive handler
status: draft
complexity: low
attack_vector: external
mitigation_difficulty: easy
versions: [">=0.6.0", "<0.8.23"]
cwe: CWE-703
swc: SWC-111
๐ Description
- A contract without a receive() or fallback() function cannot accept direct ETH transfers via send() or transfer() unless the transaction includes function data that matches an existing signature.
- This omission can cause ETH sent to the contract to be rejected (and reverted), especially in integrations where third-party contracts or users attempt ETH transfers.
- In scenarios such as treasury contracts, fundraising contracts, or minimal proxies, the absence of a fallback mechanism can lead to operational failure, loss of funds (if sent by mistake), or user confusion.
๐จ Vulnerable Code
// No fallback or receive function defined
contract Vault {
mapping(address => uint) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw(uint amount) public {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
}
๐งช Exploit Scenario
Step-by-step exploit simulation:
- A user or another contract calls address(vault).transfer(1 ether);.
- Since the contract lacks a receive() or fallback() function, the transaction reverts.
- If this transfer was intended as a payment, the operation fails and may cause disruption.
- In protocols expecting passive ETH receipt (e.g., donations, royalties), funds are lost or stuck on the sender side.
Assumptions:
- ETH is sent directly via .transfer() or .send().
- No calldata or unmatched function selector is present.
โ Fixed Code
// Securely handle plain ETH transfers
contract Vault {
mapping(address => uint) public balances;
receive() external payable {
balances[msg.sender] += msg.value;
}
function withdraw(uint amount) public {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
}
๐งญ Contextual Severity
- context: "Default"
severity: I
reasoning: "Affects ETH receiving under specific call conditions but not exploitable."
- context: "Donation or Vault Contract"
severity: M
reasoning: "Expected to passively receive ETH; failure results in operational loss."
- context: "Proxy Contract or Governance Contract"
severity: H
reasoning: "May block delegation, admin recovery, or proxy upgrades triggered by ETH fallback flows."
๐ก๏ธ Prevention
Primary Defenses
- Always include a receive() function to handle plain ETH transfers.
- Optionally define a fallback() for future compatibility and graceful degradation.
Additional Safeguards
- Use logging inside receive() to track unintended transfers.
- Return excess ETH with logic caps if necessary.
Detection Methods
- Solidity compiler warnings when payable fallback is missing.
- Slither missing-receive detector.
- Solhint: no-empty-blocks, compiler-receive-payable.
๐ฐ๏ธ Historical Exploits
- Name: TokenVault Rejection Bug
- Date: 2020-11-08
- Loss: 14 ETH
- Post-mortem: Link to post-mortem
๐ Further Reading
- SWC-111: Use of Deprecated Functions or Lack of Receive
- Solidity Docs โ Receive Ether Function
- Slither: missing-receive Detector
โ Vulnerability Report
id: LS12I
title: Lack of Fallback Function
severity: I
score:
impact: 2
exploitability: 1
reachability: 4
complexity: 1
detectability: 4
finalScore: 2.15
๐ Justifications & Analysis
- Impact: May cause failed payments or refunds, but no active loss or exploitability.
- Exploitability: Requires mistake or third-party transfer with incorrect assumptions.
- Reachability: ETH transfers from wallets or contracts are common.
- Complexity: Simple to understand and exploit (or trigger by accident).
- Detectability: Easily visible in code reviews or static analysis.