Inconsistent ETH Handling
id: LS39M
title: Inconsistent ETH Handling
baseSeverity: M
category: ether-transfer
language: solidity
blockchain: [ethereum, polygon, bsc, arbitrum, optimism]
impact: Inadvertent ETH loss, unclaimable funds, or logic bypass
status: draft
complexity: medium
attack_vector: external
mitigation_difficulty: medium
versions: [">=0.6.0", "<=0.8.25"]
cwe: CWE-703
swc: SWC-113
π Description
- Proxy contracts often forward function calls and storage to an implementation contract via delegatecall. However, if ETH handling (i.e., msg.value) is not carefully managed, especially when using low-level delegatecall, this can lead to:
- Transactions that revert unexpectedly due to missing payable modifiers,
- ETH being sent to a contract that doesnβt receive it, causing loss,
- Inconsistencies in behavior between the proxy and implementation,
- ETH stuck in the proxy with no mechanism to withdraw.
π¨ Vulnerable Code
pragma solidity ^0.8.0;
contract Proxy {
address public implementation;
constructor(address _impl) {
implementation = _impl;
}
fallback() external payable {
(bool success, ) = implementation.delegatecall(msg.data); // β msg.value is ignored by delegatecall target unless explicitly handled
require(success, "Delegatecall failed");
}
}
π§ͺ Exploit Scenario
Step-by-step exploit process:
- A developer deploys a proxy contract with a payable fallback() and sets an implementation contract that includes a logic function like deposit().
- A user sends a transaction to the proxy with msg.value > 0, expecting ETH to reach the logicβs deposit() function.
- However, the implementation function is not marked payable, or msg.value is ignored due to improper forwarding.
Assumptions:
- Proxy uses raw delegatecall without enforcing payable enforcement or value management.
- Implementation is not built to handle native ETH transfers.
β Fixed Code
pragma solidity ^0.8.0;
contract SafeProxy {
address public implementation;
constructor(address _impl) {
implementation = _impl;
}
fallback() external payable {
_delegate(implementation);
}
receive() external payable {}
function _delegate(address _impl) internal {
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()) }
}
}
}
π§ Contextual Severity
- context: "DeFi protocol with refund, claim, or treasury logic tied to raw ETH balance"
severity: M
reasoning: "Leads to unclaimable funds or logic breakage"
- context: "Contract has strict internal accounting and rejects unsolicited ETH"
severity: L
reasoning: "Risk significantly mitigated"
- context: "Non-payable contract with no ETH handling features"
severity: I
reasoning: "Vulnerability irrelevant"
π‘οΈ Prevention
Primary Defenses
- Design proxy/implementation pairs to both handle ETH coherently.
- Mark fallback and delegate targets as payable if ETH is expected.
- Implement tests for ETH transfers via proxy paths.
Additional Safeguards
- Emit an ETHReceived event on proxy to trace all inbound ETH.
- Consider forwarding all ETH explicitly to implementation, if safe.
- Log a warning or revert if ETH is received but not handled.
Detection Methods
- Static analysis of fallback function with msg.value > 0 but no payable in implementation.
- Tools: Slither (missing-payable, unhandled-value), Echidna fuzzing
π°οΈ Historical Exploits
- Name: EIP-1967 Proxy ETH Trap
- Date: 2020
- Loss: N/A (design issue caused ETH to be stuck in the proxy with no withdrawal path)
- Post-mortem: Link to post-mortem
π Further Reading
- SWC-104: Unchecked Call Return Value
- CWE-703: Improper Check or Handling of Exceptional Conditions
- Solidity Docs β Fallback and Receive
β Vulnerability Report
id: LS39M
title: Inconsistent ETH Handling
severity: M
score:
impact: 4
exploitability: 3
reachability: 4
complexity: 3
detectability: 4
finalScore: 3.75
π Justifications & Analysis
- Impact: ETH may be permanently locked or critical flows may revert
- Exploitability: Requires user interaction, but attacker may craft confusion
- Reachability: Present in nearly every upgradeable proxy architecture
- Complexity: Arises from subtle differences in call, delegatecall, and value semantics
- Detectability: Easy to catch via test or audit once value routing is traced