All audits
PassedEthereumIndependent

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

0
Critical
0
High
1
Medium
3
Low
4
Informational

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 withdrawalwithdraw() 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 balanceOf and 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 approve pattern 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:

  1. State mutation precedes the external call. Even though transfer() hands control to the recipient, the sender's balance has already been debited, so re-entering withdraw() 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.
  2. 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).
  3. No privileged mint path. The only way balanceOf increases is through deposit() or transferFrom() 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 credit balanceOf locks 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 approve and transferFrom, 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