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.

Love.sol · 118 lines · 5.7 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
// 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);
    }
}