Wrapped Native Token Confusion
Detects contracts that conflate WETH and native ETH handling, causing stuck funds or incorrect transfer logic.
Wrapped Native Token Confusion
Overview
The wrapped native token detector identifies contracts that inconsistently handle WETH (Wrapped Ether) and native ETH. Common bugs include accepting ETH deposits but accounting for WETH balances, failing to unwrap WETH before native transfers, or double-counting when both WETH and ETH paths exist for the same operation.
Why This Is an Issue
ETH and WETH are economically equivalent but mechanically different. ETH transfers use msg.value and call{value: x}, while WETH uses ERC-20 transfer/transferFrom. Contracts that handle both must:
- Track which asset was deposited and withdraw the same type
- Avoid double-counting when both paths credit the same internal balance
- Handle the WETH deposit/withdrawal gas correctly
Millions in ETH have been permanently stuck in contracts due to WETH/ETH confusion, particularly in DEX routers, bridges, and yield aggregators.
How to Resolve
// Before: Accepts both ETH and WETH but only withdraws as WETH
function deposit() external payable {
balances[msg.sender] += msg.value; // ETH credited
// But withdraw() only does WETH.transfer() -- ETH is stuck
}
// After: Consistent handling with explicit unwrap
function deposit() external payable {
WETH.deposit{value: msg.value}(); // Convert ETH to WETH immediately
balances[msg.sender] += msg.value;
}
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
WETH.withdraw(amount); // Convert back to ETH
payable(msg.sender).call{value: amount}("");
}
Examples
Vulnerable Code
contract VulnerableRouter {
function swapExactETH(address tokenOut, uint256 minOut) external payable {
// Wraps ETH to WETH for the swap
WETH.deposit{value: msg.value}();
// Performs swap via WETH
uint256 amountOut = _swap(address(WETH), tokenOut, msg.value);
require(amountOut >= minOut);
// Refund logic sends remaining WETH balance as ETH
uint256 remaining = WETH.balanceOf(address(this));
if (remaining > 0) {
// BUG: forgets to unwrap -- sends WETH tokens, not ETH
WETH.transfer(msg.sender, remaining);
// User expected ETH refund but received WETH
}
}
}
Fixed Code
contract SafeRouter {
function swapExactETH(address tokenOut, uint256 minOut) external payable {
WETH.deposit{value: msg.value}();
uint256 amountOut = _swap(address(WETH), tokenOut, msg.value);
require(amountOut >= minOut);
uint256 remaining = WETH.balanceOf(address(this));
if (remaining > 0) {
WETH.withdraw(remaining); // Unwrap first
(bool success, ) = msg.sender.call{value: remaining}("");
require(success, "ETH refund failed");
}
}
}
Sample Sigvex Output
{
"detector_id": "wrapped-native-token",
"severity": "high",
"confidence": 0.76,
"description": "Function swapExactETH() accepts native ETH (msg.value) and wraps to WETH, but refund path transfers WETH tokens instead of native ETH. Users expecting ETH refund receive ERC-20 WETH.",
"location": { "function": "swapExactETH(address,uint256)", "offset": 184 }
}
Detection Methodology
- WETH pattern detection: Identifies WETH deposit/withdraw calls by selector and known WETH contract addresses.
- ETH flow tracking: Traces
msg.valuethrough the function and identifies where it is consumed (WETH.deposit, call{value}, etc.). - Consistency check: Verifies that ETH deposits result in ETH withdrawals (or that WETH conversion is properly handled in both directions).
- Balance analysis: Detects potential trapped ETH when native ETH is received but only ERC-20 transfers are used for outputs.
Limitations
- The detector relies on recognizing WETH contract addresses. Non-standard wrapped token contracts on newer L2s may not be identified.
- Contracts that intentionally accept ETH and hold it in WETH form by design will be flagged.
- Multi-hop swaps where WETH conversion happens in a called contract are not tracked.
Related Detectors
- Fee On Transfer — token transfer amount assumptions
- Balance Accounting — balance accounting mismatches
- Locked Ether — contracts that can receive but not withdraw ETH