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
|
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
import {IWETH} from "./IWETH.sol";
import {WETHRegistry} from "./WETHRegistry.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
/// @title Love
/// @author Julien Béranger
/// @notice An ERC-20 pegged to wETH at a fixed rate of 100000 LOVE per wETH.
/// Every LOVE in circulation is backed by wETH held by this contract:
/// supply only moves through `deposit` and `withdraw`, and both are
/// permissionless.
/// @dev There is no mint entrypoint, no owner and no upgrade path, so the peg
/// cannot be diluted. `totalSupply() == WETH.balanceOf(address(this)) * RATE`
/// holds after every call; wETH transferred straight to this contract
/// raises the backing and is not redeemable.
contract Love is ERC20 {
using SafeERC20 for IERC20;
/// @notice LOVE minted per unit of wETH, and burned per unit released.
uint256 public constant RATE = 100_000;
/// @notice The registry this token asks which wETH to peg to.
/// @dev `WETHRegistry` takes no constructor arguments, so CREATE2 puts it
/// at this address on every chain. Asking it at construction time,
/// rather than taking wETH as a constructor argument, is what keeps
/// this contract's creation code byte-identical everywhere — and so
/// what gives LOVE one address on every chain instead of one per wETH
/// deployment.
///
/// Derived from the registry's creation code, which means it moves if
/// `WETHRegistry`, the solc version or the optimizer settings change.
/// `LoveRegistryAddressTest` recomputes it and fails if this constant
/// has drifted.
address public constant REGISTRY = 0x9Cf17430fEdEC487518416D1Cdc849b1eE9CDbA3;
/// @notice The wETH this token is pegged to and collateralised with.
/// @dev Read once from the registry and immutable thereafter. The registry
/// is itself write-once, so nothing can move the token under the peg.
IERC20 public immutable WETH;
/// @notice Thrown when a withdrawal amount is not a multiple of `RATE`.
/// @param loveAmount The rejected LOVE amount.
/// @param rate The rate it has to be a multiple of.
error AmountNotDivisibleByRate(uint256 loveAmount, uint256 rate);
/// @notice Thrown when there is no registry on this chain yet.
/// @param registry The address the registry would be at.
error RegistryNotDeployed(address registry);
/// @notice Thrown when the registry exists but holds no wETH yet.
/// @param registry The registry that was asked.
error WethNotRegistered(address registry);
/// @notice Emitted when wETH is locked and LOVE minted.
/// @param account The depositor, who pays the wETH and receives the LOVE.
/// @param wethAmount The wETH pulled in.
/// @param loveAmount The LOVE minted, `wethAmount * RATE`.
event Deposit(address indexed account, uint256 wethAmount, uint256 loveAmount);
/// @notice Emitted when LOVE is burned and wETH released.
/// @param account The redeemer, who burns the LOVE and receives the wETH.
/// @param loveAmount The LOVE burned.
/// @param wethAmount The wETH released, `loveAmount / RATE`.
event Withdraw(address indexed account, uint256 loveAmount, uint256 wethAmount);
/// @notice Peg to whatever wETH the registry has accepted on this chain.
/// @dev Takes no arguments on purpose. Anything passed in here would land
/// in the creation code and give the token a different address on
/// every chain whose wETH sits elsewhere, which is exactly what this
/// design exists to avoid.
///
/// Reverts when the registry is missing or empty, so a chain with no
/// reviewed wETH gets no half-configured token: deploy the registry
/// and register wETH first, then deploy this.
constructor() ERC20("Love", "LOVE") {
if (REGISTRY.code.length == 0) revert RegistryNotDeployed(REGISTRY);
IWETH registered = WETHRegistry(payable(REGISTRY)).weth();
if (address(registered) == address(0)) revert WethNotRegistered(REGISTRY);
WETH = IERC20(address(registered));
}
/// @notice Lock `wethAmount` wETH and mint `wethAmount * RATE` LOVE to the caller.
/// @dev The caller must have approved this contract for `wethAmount` first.
/// Reverts on overflow of `wethAmount * RATE`, and on the wETH transfer
/// failing for want of balance or allowance.
/// @param wethAmount The wETH to lock, in wei.
function deposit(uint256 wethAmount) external {
uint256 loveAmount = wethAmount * RATE;
WETH.safeTransferFrom(msg.sender, address(this), wethAmount);
_mint(msg.sender, loveAmount);
emit Deposit(msg.sender, wethAmount, loveAmount);
}
/// @notice Burn `loveAmount` LOVE and release `loveAmount / RATE` wETH to the caller.
/// @dev Reverts with `AmountNotDivisibleByRate` unless `loveAmount` is a
/// multiple of `RATE`, so the peg never rounds against the caller or
/// the remaining holders. LOVE is burned before the wETH leaves, and
/// the burn already caps the amount at the caller's balance.
/// @param loveAmount The LOVE to burn, a multiple of `RATE`.
function withdraw(uint256 loveAmount) external {
if (loveAmount % RATE != 0) revert AmountNotDivisibleByRate(loveAmount, RATE);
uint256 wethAmount = loveAmount / RATE;
_burn(msg.sender, loveAmount);
WETH.safeTransfer(msg.sender, wethAmount);
emit Withdraw(msg.sender, loveAmount, wethAmount);
}
}
|