julien/lovepublic Fork 0
main
Commits
Clone
git clone https://git.rickub.com/julien/love.git
git clone ssh://git@rickub.com/julien/love.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

WETHRegistry.sol · 203 lines · 10.6 KBSolidity Blame HistoryRaw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

import {IWETH} from "./IWETH.sol";

/// @title WETHRegistry
/// @author Julien Béranger
/// @notice Names the wETH that `Love` pegs to on this chain, and will only
///         accept an address whose code is one of a fixed set of reviewed wETH
///         implementations.
/// @dev The point is to take the wETH address out of `Love`'s creation code.
///      Anyone can call `register`, on any chain, without a factory, an owner
///      or a per-chain deployer — the contract decides for itself whether to
///      accept the candidate. Because it takes no constructor arguments, its
///      own creation code is identical everywhere, so CREATE2 puts it at one
///      address on every chain and `Love` can hardcode that address.
///
///      Two gates run at registration:
///
///      1. `EXTCODEHASH` against the allowlist below. This is the real
///         defence: matching means the candidate is provably one of a handful
///         of implementations whose source has been read, not merely something
///         that behaves well today. It is what catches a hidden mint, an
///         upgrade hook or a backdoor, none of which a behavioural test can
///         see.
///      2. A one-wei deposit/withdraw round trip, which catches the plain
///         mistake of an address that is not wETH at all, and proves the
///         wrapper actually works on this chain.
///
///      The registration is write-once. There is no setter, no owner and no
///      way to revoke an entry, so a `Love` deployed against this registry can
///      never have the token under its peg swapped.
///
///      Known limitation, and it is not a small one: the allowlist proves a
///      candidate *is* a reviewed wETH implementation, not that it is *the*
///      wETH the chain's ecosystem uses. Anyone can deploy their own copy of
///      WETH9 — identical code, identical codehash, no liquidity — and
///      register it first. The result is still fully backed and redeemable,
///      since it is real WETH9, but it is not the token anyone else holds, and
///      write-once means the mistake cannot be corrected on that chain. So
///      always check `weth()` against the chain's canonical wETH before
///      treating a `Love` instance as the real one; a squatted registry is
///      visible to anyone who looks, and the answer to it is a fresh salt.
contract WETHRegistry {
    /// @notice The wETH registered on this chain, or the zero address if none
    ///         has been registered yet.
    /// @dev Written exactly once, by whoever calls `register` first with a
    ///      candidate that passes both gates.
    IWETH public weth;

    /// @notice The ether moved through the candidate to prove it wraps.
    /// @dev Stays in the registry afterwards; see `register`.
    uint256 public constant PROBE = 1 wei;

    /// @dev Open only for the duration of the round trip, so the registry
    ///      cannot be used as a place to park ether. Transient, so it costs
    ///      almost nothing and cannot survive the call that set it.
    bool private transient _probing;

    /// @notice Thrown when a wETH has already been registered on this chain.
    /// @param registered The wETH registered by the earlier call.
    error AlreadyRegistered(IWETH registered);

    /// @notice Thrown when the candidate's code is not a reviewed wETH.
    /// @param candidate The rejected address.
    /// @param codeHash Its `EXTCODEHASH`, zero if there is no code there.
    error UnknownImplementation(IWETH candidate, bytes32 codeHash);

    /// @notice Thrown when the call does not carry exactly `PROBE` wei.
    /// @param sent The value that came with the call.
    error ProbeValueRequired(uint256 sent);

    /// @notice Thrown when wrapping `PROBE` wei did not mint `PROBE` wrapped.
    /// @param expected The balance a real wrapper would have produced.
    /// @param actual The balance the candidate produced.
    error DepositMismatch(uint256 expected, uint256 actual);

    /// @notice Thrown when unwrapping did not burn the wrapped token.
    /// @param expected The balance a real wrapper would have left behind.
    /// @param actual The balance the candidate left behind.
    error WithdrawMismatch(uint256 expected, uint256 actual);

    /// @notice Thrown when unwrapping did not return the ether.
    /// @param expected The ether balance the round trip should have restored.
    /// @param actual The ether balance it actually left.
    error EtherNotReturned(uint256 expected, uint256 actual);

    /// @notice Thrown when ether is sent outside a round trip.
    error NotProbing();

    /// @notice Emitted once, when a chain's wETH is settled.
    /// @param weth The accepted wETH.
    /// @param registrar Whoever supplied and paid for it.
    /// @param codeHash The allowlisted hash its code matched.
    event Registered(IWETH indexed weth, address indexed registrar, bytes32 codeHash);

    /// @notice Accept `candidate` as this chain's wETH, if its code is one of
    ///         the reviewed implementations and it wraps ether correctly.
    /// @dev Send exactly `PROBE` wei. It is not refunded: it stays here, which
    ///      is the cheap way to let a contract register. Paying it back would
    ///      mean calling the registrar with value, and a registrar with no
    ///      payable fallback — a script, a multisig, a deployer contract —
    ///      would then be unable to register at all. One wei, once per chain,
    ///      buys that away.
    ///
    ///      Reverts rather than degrading when the candidate is unrecognised —
    ///      unreviewed bytecode cannot be shown safe by any test, static or
    ///      behavioural, so a chain running its own wETH is a chain `Love`
    ///      does not deploy on until that implementation is reviewed and
    ///      added.
    /// @param candidate The wETH to register.
    /// @return The registered wETH, for the convenience of scripts.
    function register(IWETH candidate) external payable returns (IWETH) {
        if (address(weth) != address(0)) revert AlreadyRegistered(weth);
        if (msg.value != PROBE) revert ProbeValueRequired(msg.value);

        bytes32 codeHash = address(candidate).codehash;
        if (!isKnownImplementation(codeHash)) revert UnknownImplementation(candidate, codeHash);

        _probe(candidate);

        weth = candidate;
        emit Registered(candidate, msg.sender, codeHash);

        return candidate;
    }

    /// @notice Whether `codeHash` is one of the reviewed wETH implementations.
    /// @dev Compile-time, so the list is ownerless and append-only by
    ///      construction: extending it means publishing a new registry, which
    ///      leaves every existing deployment exactly as it was. No key can
    ///      revoke an entry and strand a live `Love`.
    ///
    ///      These are exact `EXTCODEHASH` values, metadata included. Hashing
    ///      the code with solc's trailing metadata stripped would fold each
    ///      family into a single entry, but it would also accept a known
    ///      implementation followed by arbitrary appended bytes, and the
    ///      safety of that rests on control flow never reaching them — a
    ///      property that holds for every family here and would have to keep
    ///      holding for every family added later. Ten constants is the cheaper
    ///      side of that trade.
    ///
    ///      Grouped by implementation. Every value is reproducible from chain
    ///      state with `script/weth-codehashes.sh`.
    /// @param codeHash The `EXTCODEHASH` to check.
    /// @return True if a candidate with this code may be registered.
    function isKnownImplementation(bytes32 codeHash) public pure returns (bool) {
        // OP Stack legacy WETH9, 2041 bytes, solc 0.5.17. One implementation,
        // four hashes: these chains run byte-identical code and differ only
        // inside solc's metadata blob, which never executes.
        if (codeHash == 0x779bbf2a738ef09d961c945116197e2ac764c1b39304b2b4418cd4e42668b173) return true; // optimism
        if (codeHash == 0x8a3a1f6a9f9dce633117adee5b458245835a8645a8c8726a26382a4622508b1c) return true; // base, mode, zora
        if (codeHash == 0x557c8e14d33f7cd67cad0141e1a49ebf3488a447fc3df7aa66b127778a0383d1) return true; // world-chain
        if (codeHash == 0xf35fe602ba2a3b96f2e27ff7c8b8010800a8d0d616a5fb1f902e087b590355f3) return true; // lisk

        // Canonical WETH9, 3124 bytes, solc 0.4.19.
        if (codeHash == 0xd0a06b12ac47863b5c7be4185c2deaad1c61557033f56c7d4ea74429cbb25e23) return true; // ethereum
        if (codeHash == 0xa670ec6c272ddec6d328d6f3d5cad65a841a6ab45e8e5cf825150eb458be4f1f) return true; // linea
        if (codeHash == 0x032e9cab14331328530468e54f1b91777b4d5c9dbbb400884badb32bc4113585) return true; // polygon-zkevm

        // OP Stack WETH, 2865 bytes, solc 0.8.15.
        if (codeHash == 0xd0f1614c5dacfbd34f1c6f500f397009e4c9a8bfd4e02db353edb2253d9a8012) return true; // unichain, soneium, ink

        // Taiko, 3204 bytes.
        if (codeHash == 0x9f3d95086909fce850d997158aba31abe26c3aad6a413107ca0bf9d53a7c42e9) return true; // taiko

        // Scroll, 5871 bytes.
        if (codeHash == 0xe8c4073351c26b9831c1e5af153b9be4713a4af9edfdf32b58077b735e120f14) return true; // scroll

        return false;
    }

    /// @notice Take `PROBE` wei through the candidate and back.
    /// @dev Costs the registrar nothing but gas: the wei returns. Balances are
    ///      read before and after rather than assumed to start at zero, so a
    ///      candidate that was sent wETH beforehand cannot skew the check.
    /// @param candidate The wETH being probed.
    function _probe(IWETH candidate) private {
        uint256 etherBefore = address(this).balance;
        uint256 wrappedBefore = candidate.balanceOf(address(this));

        _probing = true;

        candidate.deposit{value: PROBE}();
        uint256 wrapped = candidate.balanceOf(address(this));
        if (wrapped != wrappedBefore + PROBE) revert DepositMismatch(wrappedBefore + PROBE, wrapped);

        candidate.withdraw(PROBE);
        uint256 unwrapped = candidate.balanceOf(address(this));
        if (unwrapped != wrappedBefore) revert WithdrawMismatch(wrappedBefore, unwrapped);

        _probing = false;

        if (address(this).balance != etherBefore) revert EtherNotReturned(etherBefore, address(this).balance);
    }

    /// @notice Takes the ether a candidate returns mid-probe, and nothing else.
    /// @dev The registry is not a wallet. Outside a round trip this reverts,
    ///      so ether cannot be stranded in a contract with no way to move it.
    receive() external payable {
        if (!_probing) revert NotProbing();
    }
}