// 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); } }