WETH9 Design Review
Smart contract security audit by TierZero · 2023-03-15
Independent research. This is an unsolicited security analysis published by TierZero — not a commissioned audit, and no certificate is issued. It reflects our own review of public code and on-chain events.
Findings summary
Independent research — not a commissioned audit.
What it is
WETH9 is the canonical wrapped-Ether contract deployed on Ethereum mainnet at 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2. It converts native ETH into an ERC-20-shaped balance so that ETH can move through the approve/transferFrom interface that DeFi tooling expects. The contract is small — under 100 lines — unowned, non-upgradeable, and has not been modified since deployment. That immutability is itself part of the design: there is no admin key, no pause switch, no proxy, nothing to compromise except the logic that was already reviewed by thousands of integrators over the years. It has become the de facto settlement asset for AMMs, lending markets, and order-book DEXs precisely because its behavior is boring and unchanging.
Threat model
For a contract that custodies a chain's native asset at this scale, the relevant attack surface is narrow but consequential:
- Reentrancy during withdrawal —
withdraw()sends ETH back to the caller before returning; if state weren't updated first, a malicious contract could re-enter and drain more than its balance. - Accounting drift — any path where
balanceOfand the contract's actual ETH balance diverge undermines the 1:1 peg every downstream protocol assumes. - Griefing on transfer — the wrapped token must remain transferable and withdrawable for any address, including smart-contract wallets with nontrivial fallback logic.
- Approval race conditions — inherited from the ERC-20
approvepattern generally, not specific to wrapping ETH.
Why the design holds (key invariants & mitigations)
The core invariant is simple: sum(balanceOf) <= address(this).balance, enforced by strict checks-effects-interactions ordering.
function deposit() public payable {
balanceOf[msg.sender] += msg.value; // effect happens with the value transfer
Deposit(msg.sender, msg.value);
}
function withdraw(uint wad) public {
require(balanceOf[msg.sender] >= wad); // check
balanceOf[msg.sender] -= wad; // effect, before external call
msg.sender.transfer(wad); // interaction
Withdrawal(msg.sender, wad);
}
Three things do the real work here:
- State mutation precedes the external call. Even though
transfer()hands control to the recipient, the sender's balance has already been debited, so re-enteringwithdraw()finds an insufficient balance and reverts. This is the same checks-effects-interactions discipline we insist on in every solidity engagement — it's the difference between a contract that survives a hostile callback and one that doesn't. transfer()'s 2300-gas stipend limits what the receiving contract can do on callback, which was the original reentrancy backstop before CEI became the default assumption. It's a secondary mitigation, not the primary one — the balance-before-call ordering is what actually matters, which is worth internalizing since the stipend's safety margin has eroded with gas repricing (EIP-1884).- No privileged mint path. The only way
balanceOfincreases is throughdeposit()ortransferFrom()moving existing balance, and both are value-conserving. There's no owner-only mint, no rebasing, no fee-on-transfer — the token is exactly as boring as a wrapper should be.
One subtlety worth flagging explicitly: totalSupply() returns this.balance, not a running sum of balanceOf. Forced ETH sent via selfdestruct or as a block reward to the contract address inflates this.balance without crediting any account. This doesn't let anyone steal funds — the extra ETH just sits unclaimed — but it does mean totalSupply() == sum(balanceOf) is not a hard guarantee, only an observed condition. Integrators who hardcode that equivalence are relying on an assumption the contract doesn't actually enforce.
Where implementers get it wrong
Most incidents involving "wrapped ETH" are not in WETH9 itself but in forks and reimplementations that deviate from this template:
- Reordering effects after interactions. Teams building a wrapped-asset variant with extra logic (fees, allowlists, hooks) sometimes move the balance decrement after the external call to accommodate the new logic, reintroducing classic reentrancy. This is the single most common regression we see in a code review audit of ETH-wrapper forks.
- Silently breaking the 1:1 peg. Adding transfer fees, rebasing, or elastic supply to a token that downstream contracts assume is fungible 1:1 with ETH causes accounting breaks in every integrated protocol, not just the wrapper.
- Fallback functions that don't call
deposit(). A payable fallback that accepts ETH but forgets to creditbalanceOflocks user funds with no path to recovery — a pure loss-of-funds bug, not a value-transfer exploit. - Assuming
transfer()'s stipend is a complete reentrancy guard. It was never sufficient on its own even before EIP-1884; contracts that skip explicit ordering and lean entirely on the gas stipend are one gas-cost change away from a broken assumption. - Approval front-running, the standard ERC-20 race between
approveandtransferFrom, applies here as it does to any token using this interface — it's part of why patterns like the one covered in our piece on solidity Permit2 gasless approvals exist in the first place.
Takeaways
WETH9 holds up because it does almost nothing: no admin surface, no mutable logic, one invariant enforced by strict ordering. That's the lesson for anyone reviewing a wrapper, vault, or custody contract — the safety property you're actually checking for is whether effects happen before interactions, every single time, with no exception carved out for a "small" feature addition. If you're shipping a wrapped-asset contract or anything that moves native value against an internal ledger, get a second set of eyes on the ordering before it goes to mainnet — that's exactly the kind of review our smart contract audit engagements are built around.
Thinking about a review like this for your own contracts? Get a smart contract audit from an independent team.
Need your contracts audited?
Manual review + tooling across TON, Solana and EVM — certificate and full report included.
Get an audit