add natspec
3cc0f3b parent: 5376202 modified
script/Love.s.sol +24 -0 | @@ -5,6 +5,8 @@ import {Love} from "../src/Love.sol"; | ||
| 5 | 5 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; |
| 6 | 6 | import {Script, console} from "forge-std/Script.sol"; |
| 7 | 7 | |
| 8 | +/// @title LoveScript | |
| 9 | +/// @author Julien Béranger | |
| 8 | 10 | /// @notice Deploys `Love` with CREATE2 through the canonical deterministic |
| 9 | 11 | /// deployer. The address depends on the salt and the creation code, and |
| 10 | 12 | /// the creation code embeds the wETH address — so the token only gets |
| @@ -12,13 +14,20 @@ import {Script, console} from "forge-std/Script.sol"; | ||
| 12 | 14 | /// `solc`, the optimizer settings and `bytecode_hash = "none"` as they |
| 13 | 15 | /// are in `foundry.toml`, or the address changes too. |
| 14 | 16 | contract LoveScript is Script { |
| 17 | + /// @notice Salt used when `SALT` is not set in the environment. | |
| 15 | 18 | bytes32 public constant DEFAULT_SALT = keccak256("LOVE"); |
| 16 | 19 | |
| 20 | + /// @notice wETH address used when `WETH` is not set in the environment. | |
| 17 | 21 | /// @dev wETH on Base, Optimism and every other OP-Stack chain. |
| 18 | 22 | address public constant DEFAULT_WETH = 0x4200000000000000000000000000000000000006; |
| 19 | 23 | |
| 24 | + /// @notice The token, once `run()` has deployed or found it. | |
| 20 | 25 | Love public love; |
| 21 | 26 | |
| 27 | + /// @notice Deploy `Love`, or return it untouched if it is already there. | |
| 28 | + /// @dev Reads `SALT` and `WETH` from the environment, falling back to the | |
| 29 | + /// defaults, and asserts the deployed address matches the prediction. | |
| 30 | + /// @return The deployed token. | |
| 22 | 31 | function run() public returns (Love) { |
| 23 | 32 | bytes32 s = salt(); |
| 24 | 33 | address w = wethAddress(); |
| @@ -53,6 +62,7 @@ contract LoveScript is Script { | ||
| 53 | 62 | } |
| 54 | 63 | |
| 55 | 64 | /// @notice Print the address `run()` would deploy to, without broadcasting. |
| 65 | + /// @return predicted The address the current salt and wETH derive to. | |
| 56 | 66 | function predict() public view returns (address predicted) { |
| 57 | 67 | bytes32 s = salt(); |
| 58 | 68 | address w = wethAddress(); |
| @@ -64,24 +74,38 @@ contract LoveScript is Script { | ||
| 64 | 74 | console.log("predicted ", predicted); |
| 65 | 75 | } |
| 66 | 76 | |
| 77 | + /// @notice Derive the deployment address from a salt and a wETH address. | |
| 78 | + /// @param s The CREATE2 salt. | |
| 79 | + /// @param w The wETH the token would be pegged to. | |
| 80 | + /// @return The address `Love` would land at. | |
| 67 | 81 | function predict(bytes32 s, address w) public pure returns (address) { |
| 68 | 82 | return vm.computeCreate2Address(s, initCodeHash(w), CREATE2_FACTORY); |
| 69 | 83 | } |
| 70 | 84 | |
| 85 | + /// @notice The creation code CREATE2 is handed, constructor argument included. | |
| 86 | + /// @param w The wETH the token would be pegged to. | |
| 87 | + /// @return The creation code, with `w` ABI-encoded onto it. | |
| 71 | 88 | function initCode(address w) public pure returns (bytes memory) { |
| 72 | 89 | return abi.encodePacked(type(Love).creationCode, abi.encode(w)); |
| 73 | 90 | } |
| 74 | 91 | |
| 92 | + /// @notice Hash of the creation code, the second input to the address derivation. | |
| 93 | + /// @param w The wETH the token would be pegged to. | |
| 94 | + /// @return The keccak256 of `initCode(w)`. | |
| 75 | 95 | function initCodeHash(address w) public pure returns (bytes32) { |
| 76 | 96 | return keccak256(initCode(w)); |
| 77 | 97 | } |
| 78 | 98 | |
| 99 | + /// @notice The salt to deploy with. | |
| 79 | 100 | /// @dev `SALT` overrides the default, e.g. to mine a vanity address. |
| 101 | + /// @return The configured salt, or `DEFAULT_SALT`. | |
| 80 | 102 | function salt() public view returns (bytes32) { |
| 81 | 103 | return vm.envOr("SALT", DEFAULT_SALT); |
| 82 | 104 | } |
| 83 | 105 | |
| 106 | + /// @notice The wETH address to peg the deployment to. | |
| 84 | 107 | /// @dev `WETH` overrides the default, which only holds on OP-Stack chains. |
| 108 | + /// @return The configured wETH address, or `DEFAULT_WETH`. | |
| 85 | 109 | function wethAddress() public view returns (address) { |
| 86 | 110 | return vm.envOr("WETH", DEFAULT_WETH); |
| 87 | 111 | } |
| @@ -5,6 +5,8 @@ import {Love} from "../src/Love.sol"; | |||
| 5 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | 5 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; |
| 6 | import {Script, console} from "forge-std/Script.sol"; | 6 | import {Script, console} from "forge-std/Script.sol"; |
| 7 | 7 | ||
| 8 | +/// @title LoveScript | ||
| 9 | +/// @author Julien Béranger | ||
| 8 | /// @notice Deploys `Love` with CREATE2 through the canonical deterministic | 10 | /// @notice Deploys `Love` with CREATE2 through the canonical deterministic |
| 9 | /// deployer. The address depends on the salt and the creation code, and | 11 | /// deployer. The address depends on the salt and the creation code, and |
| 10 | /// the creation code embeds the wETH address — so the token only gets | 12 | /// the creation code embeds the wETH address — so the token only gets |
| @@ -12,13 +14,20 @@ import {Script, console} from "forge-std/Script.sol"; | |||
| 12 | /// `solc`, the optimizer settings and `bytecode_hash = "none"` as they | 14 | /// `solc`, the optimizer settings and `bytecode_hash = "none"` as they |
| 13 | /// are in `foundry.toml`, or the address changes too. | 15 | /// are in `foundry.toml`, or the address changes too. |
| 14 | contract LoveScript is Script { | 16 | contract LoveScript is Script { |
| 17 | + /// @notice Salt used when `SALT` is not set in the environment. | ||
| 15 | bytes32 public constant DEFAULT_SALT = keccak256("LOVE"); | 18 | bytes32 public constant DEFAULT_SALT = keccak256("LOVE"); |
| 16 | 19 | ||
| 20 | + /// @notice wETH address used when `WETH` is not set in the environment. | ||
| 17 | /// @dev wETH on Base, Optimism and every other OP-Stack chain. | 21 | /// @dev wETH on Base, Optimism and every other OP-Stack chain. |
| 18 | address public constant DEFAULT_WETH = 0x4200000000000000000000000000000000000006; | 22 | address public constant DEFAULT_WETH = 0x4200000000000000000000000000000000000006; |
| 19 | 23 | ||
| 24 | + /// @notice The token, once `run()` has deployed or found it. | ||
| 20 | Love public love; | 25 | Love public love; |
| 21 | 26 | ||
| 27 | + /// @notice Deploy `Love`, or return it untouched if it is already there. | ||
| 28 | + /// @dev Reads `SALT` and `WETH` from the environment, falling back to the | ||
| 29 | + /// defaults, and asserts the deployed address matches the prediction. | ||
| 30 | + /// @return The deployed token. | ||
| 22 | function run() public returns (Love) { | 31 | function run() public returns (Love) { |
| 23 | bytes32 s = salt(); | 32 | bytes32 s = salt(); |
| 24 | address w = wethAddress(); | 33 | address w = wethAddress(); |
| @@ -53,6 +62,7 @@ contract LoveScript is Script { | |||
| 53 | } | 62 | } |
| 54 | 63 | ||
| 55 | /// @notice Print the address `run()` would deploy to, without broadcasting. | 64 | /// @notice Print the address `run()` would deploy to, without broadcasting. |
| 65 | + /// @return predicted The address the current salt and wETH derive to. | ||
| 56 | function predict() public view returns (address predicted) { | 66 | function predict() public view returns (address predicted) { |
| 57 | bytes32 s = salt(); | 67 | bytes32 s = salt(); |
| 58 | address w = wethAddress(); | 68 | address w = wethAddress(); |
| @@ -64,24 +74,38 @@ contract LoveScript is Script { | |||
| 64 | console.log("predicted ", predicted); | 74 | console.log("predicted ", predicted); |
| 65 | } | 75 | } |
| 66 | 76 | ||
| 77 | + /// @notice Derive the deployment address from a salt and a wETH address. | ||
| 78 | + /// @param s The CREATE2 salt. | ||
| 79 | + /// @param w The wETH the token would be pegged to. | ||
| 80 | + /// @return The address `Love` would land at. | ||
| 67 | function predict(bytes32 s, address w) public pure returns (address) { | 81 | function predict(bytes32 s, address w) public pure returns (address) { |
| 68 | return vm.computeCreate2Address(s, initCodeHash(w), CREATE2_FACTORY); | 82 | return vm.computeCreate2Address(s, initCodeHash(w), CREATE2_FACTORY); |
| 69 | } | 83 | } |
| 70 | 84 | ||
| 85 | + /// @notice The creation code CREATE2 is handed, constructor argument included. | ||
| 86 | + /// @param w The wETH the token would be pegged to. | ||
| 87 | + /// @return The creation code, with `w` ABI-encoded onto it. | ||
| 71 | function initCode(address w) public pure returns (bytes memory) { | 88 | function initCode(address w) public pure returns (bytes memory) { |
| 72 | return abi.encodePacked(type(Love).creationCode, abi.encode(w)); | 89 | return abi.encodePacked(type(Love).creationCode, abi.encode(w)); |
| 73 | } | 90 | } |
| 74 | 91 | ||
| 92 | + /// @notice Hash of the creation code, the second input to the address derivation. | ||
| 93 | + /// @param w The wETH the token would be pegged to. | ||
| 94 | + /// @return The keccak256 of `initCode(w)`. | ||
| 75 | function initCodeHash(address w) public pure returns (bytes32) { | 95 | function initCodeHash(address w) public pure returns (bytes32) { |
| 76 | return keccak256(initCode(w)); | 96 | return keccak256(initCode(w)); |
| 77 | } | 97 | } |
| 78 | 98 | ||
| 99 | + /// @notice The salt to deploy with. | ||
| 79 | /// @dev `SALT` overrides the default, e.g. to mine a vanity address. | 100 | /// @dev `SALT` overrides the default, e.g. to mine a vanity address. |
| 101 | + /// @return The configured salt, or `DEFAULT_SALT`. | ||
| 80 | function salt() public view returns (bytes32) { | 102 | function salt() public view returns (bytes32) { |
| 81 | return vm.envOr("SALT", DEFAULT_SALT); | 103 | return vm.envOr("SALT", DEFAULT_SALT); |
| 82 | } | 104 | } |
| 83 | 105 | ||
| 106 | + /// @notice The wETH address to peg the deployment to. | ||
| 84 | /// @dev `WETH` overrides the default, which only holds on OP-Stack chains. | 107 | /// @dev `WETH` overrides the default, which only holds on OP-Stack chains. |
| 108 | + /// @return The configured wETH address, or `DEFAULT_WETH`. | ||
| 85 | function wethAddress() public view returns (address) { | 109 | function wethAddress() public view returns (address) { |
| 86 | return vm.envOr("WETH", DEFAULT_WETH); | 110 | return vm.envOr("WETH", DEFAULT_WETH); |
| 87 | } | 111 | } |
modified
src/Love.sol +31 -2 | @@ -6,27 +6,53 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | ||
| 6 | 6 | import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; |
| 7 | 7 | |
| 8 | 8 | /// @title Love |
| 9 | +/// @author Julien Béranger | |
| 9 | 10 | /// @notice An ERC-20 pegged to wETH at a fixed rate of 100000 LOVE per wETH. |
| 10 | 11 | /// Every LOVE in circulation is backed by wETH held by this contract: |
| 11 | 12 | /// supply only moves through `deposit` and `withdraw`, and both are |
| 12 | 13 | /// permissionless. |
| 14 | +/// @dev There is no mint entrypoint, no owner and no upgrade path, so the peg | |
| 15 | +/// cannot be diluted. `totalSupply() == WETH.balanceOf(address(this)) * RATE` | |
| 16 | +/// holds after every call; wETH transferred straight to this contract | |
| 17 | +/// raises the backing and is not redeemable. | |
| 13 | 18 | contract Love is ERC20 { |
| 14 | 19 | using SafeERC20 for IERC20; |
| 15 | 20 | |
| 21 | + /// @notice LOVE minted per unit of wETH, and burned per unit released. | |
| 16 | 22 | uint256 public constant RATE = 100_000; |
| 17 | 23 | |
| 24 | + /// @notice The wETH this token is pegged to and collateralised with. | |
| 25 | + /// @dev Immutable, and part of the creation code — two chains only give this | |
| 26 | + /// contract the same CREATE2 address if they share a wETH address. | |
| 18 | 27 | IERC20 public immutable WETH; |
| 19 | 28 | |
| 29 | + /// @notice Thrown when a withdrawal amount is not a multiple of `RATE`. | |
| 30 | + /// @param loveAmount The rejected LOVE amount. | |
| 31 | + /// @param rate The rate it has to be a multiple of. | |
| 20 | 32 | error AmountNotDivisibleByRate(uint256 loveAmount, uint256 rate); |
| 21 | 33 | |
| 34 | + /// @notice Emitted when wETH is locked and LOVE minted. | |
| 35 | + /// @param account The depositor, who pays the wETH and receives the LOVE. | |
| 36 | + /// @param wethAmount The wETH pulled in. | |
| 37 | + /// @param loveAmount The LOVE minted, `wethAmount * RATE`. | |
| 22 | 38 | event Deposit(address indexed account, uint256 wethAmount, uint256 loveAmount); |
| 39 | + | |
| 40 | + /// @notice Emitted when LOVE is burned and wETH released. | |
| 41 | + /// @param account The redeemer, who burns the LOVE and receives the wETH. | |
| 42 | + /// @param loveAmount The LOVE burned. | |
| 43 | + /// @param wethAmount The wETH released, `loveAmount / RATE`. | |
| 23 | 44 | event Withdraw(address indexed account, uint256 loveAmount, uint256 wethAmount); |
| 24 | 45 | |
| 46 | + /// @param weth_ The wETH to peg to. Set once, never changed. | |
| 25 | 47 | constructor(IERC20 weth_) ERC20("Love", "LOVE") { |
| 26 | 48 | WETH = weth_; |
| 27 | 49 | } |
| 28 | 50 | |
| 29 | 51 | /// @notice Lock `wethAmount` wETH and mint `wethAmount * RATE` LOVE to the caller. |
| 52 | + /// @dev The caller must have approved this contract for `wethAmount` first. | |
| 53 | + /// Reverts on overflow of `wethAmount * RATE`, and on the wETH transfer | |
| 54 | + /// failing for want of balance or allowance. | |
| 55 | + /// @param wethAmount The wETH to lock, in wei. | |
| 30 | 56 | function deposit(uint256 wethAmount) external { |
| 31 | 57 | uint256 loveAmount = wethAmount * RATE; |
| 32 | 58 | |
| @@ -37,8 +63,11 @@ contract Love is ERC20 { | ||
| 37 | 63 | } |
| 38 | 64 | |
| 39 | 65 | /// @notice Burn `loveAmount` LOVE and release `loveAmount / RATE` wETH to the caller. |
| 40 | - /// @dev Reverts unless `loveAmount` is a multiple of `RATE`, so the peg never | |
| 41 | - /// rounds against the caller or the remaining holders. | |
| 66 | + /// @dev Reverts with `AmountNotDivisibleByRate` unless `loveAmount` is a | |
| 67 | + /// multiple of `RATE`, so the peg never rounds against the caller or | |
| 68 | + /// the remaining holders. LOVE is burned before the wETH leaves, and | |
| 69 | + /// the burn already caps the amount at the caller's balance. | |
| 70 | + /// @param loveAmount The LOVE to burn, a multiple of `RATE`. | |
| 42 | 71 | function withdraw(uint256 loveAmount) external { |
| 43 | 72 | if (loveAmount % RATE != 0) revert AmountNotDivisibleByRate(loveAmount, RATE); |
| 44 | 73 | |
| @@ -6,27 +6,53 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | |||
| 6 | import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; | 6 | import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; |
| 7 | 7 | ||
| 8 | /// @title Love | 8 | /// @title Love |
| 9 | +/// @author Julien Béranger | ||
| 9 | /// @notice An ERC-20 pegged to wETH at a fixed rate of 100000 LOVE per wETH. | 10 | /// @notice An ERC-20 pegged to wETH at a fixed rate of 100000 LOVE per wETH. |
| 10 | /// Every LOVE in circulation is backed by wETH held by this contract: | 11 | /// Every LOVE in circulation is backed by wETH held by this contract: |
| 11 | /// supply only moves through `deposit` and `withdraw`, and both are | 12 | /// supply only moves through `deposit` and `withdraw`, and both are |
| 12 | /// permissionless. | 13 | /// permissionless. |
| 14 | +/// @dev There is no mint entrypoint, no owner and no upgrade path, so the peg | ||
| 15 | +/// cannot be diluted. `totalSupply() == WETH.balanceOf(address(this)) * RATE` | ||
| 16 | +/// holds after every call; wETH transferred straight to this contract | ||
| 17 | +/// raises the backing and is not redeemable. | ||
| 13 | contract Love is ERC20 { | 18 | contract Love is ERC20 { |
| 14 | using SafeERC20 for IERC20; | 19 | using SafeERC20 for IERC20; |
| 15 | 20 | ||
| 21 | + /// @notice LOVE minted per unit of wETH, and burned per unit released. | ||
| 16 | uint256 public constant RATE = 100_000; | 22 | uint256 public constant RATE = 100_000; |
| 17 | 23 | ||
| 24 | + /// @notice The wETH this token is pegged to and collateralised with. | ||
| 25 | + /// @dev Immutable, and part of the creation code — two chains only give this | ||
| 26 | + /// contract the same CREATE2 address if they share a wETH address. | ||
| 18 | IERC20 public immutable WETH; | 27 | IERC20 public immutable WETH; |
| 19 | 28 | ||
| 29 | + /// @notice Thrown when a withdrawal amount is not a multiple of `RATE`. | ||
| 30 | + /// @param loveAmount The rejected LOVE amount. | ||
| 31 | + /// @param rate The rate it has to be a multiple of. | ||
| 20 | error AmountNotDivisibleByRate(uint256 loveAmount, uint256 rate); | 32 | error AmountNotDivisibleByRate(uint256 loveAmount, uint256 rate); |
| 21 | 33 | ||
| 34 | + /// @notice Emitted when wETH is locked and LOVE minted. | ||
| 35 | + /// @param account The depositor, who pays the wETH and receives the LOVE. | ||
| 36 | + /// @param wethAmount The wETH pulled in. | ||
| 37 | + /// @param loveAmount The LOVE minted, `wethAmount * RATE`. | ||
| 22 | event Deposit(address indexed account, uint256 wethAmount, uint256 loveAmount); | 38 | event Deposit(address indexed account, uint256 wethAmount, uint256 loveAmount); |
| 39 | + | ||
| 40 | + /// @notice Emitted when LOVE is burned and wETH released. | ||
| 41 | + /// @param account The redeemer, who burns the LOVE and receives the wETH. | ||
| 42 | + /// @param loveAmount The LOVE burned. | ||
| 43 | + /// @param wethAmount The wETH released, `loveAmount / RATE`. | ||
| 23 | event Withdraw(address indexed account, uint256 loveAmount, uint256 wethAmount); | 44 | event Withdraw(address indexed account, uint256 loveAmount, uint256 wethAmount); |
| 24 | 45 | ||
| 46 | + /// @param weth_ The wETH to peg to. Set once, never changed. | ||
| 25 | constructor(IERC20 weth_) ERC20("Love", "LOVE") { | 47 | constructor(IERC20 weth_) ERC20("Love", "LOVE") { |
| 26 | WETH = weth_; | 48 | WETH = weth_; |
| 27 | } | 49 | } |
| 28 | 50 | ||
| 29 | /// @notice Lock `wethAmount` wETH and mint `wethAmount * RATE` LOVE to the caller. | 51 | /// @notice Lock `wethAmount` wETH and mint `wethAmount * RATE` LOVE to the caller. |
| 52 | + /// @dev The caller must have approved this contract for `wethAmount` first. | ||
| 53 | + /// Reverts on overflow of `wethAmount * RATE`, and on the wETH transfer | ||
| 54 | + /// failing for want of balance or allowance. | ||
| 55 | + /// @param wethAmount The wETH to lock, in wei. | ||
| 30 | function deposit(uint256 wethAmount) external { | 56 | function deposit(uint256 wethAmount) external { |
| 31 | uint256 loveAmount = wethAmount * RATE; | 57 | uint256 loveAmount = wethAmount * RATE; |
| 32 | 58 | ||
| @@ -37,8 +63,11 @@ contract Love is ERC20 { | |||
| 37 | } | 63 | } |
| 38 | 64 | ||
| 39 | /// @notice Burn `loveAmount` LOVE and release `loveAmount / RATE` wETH to the caller. | 65 | /// @notice Burn `loveAmount` LOVE and release `loveAmount / RATE` wETH to the caller. |
| 40 | - /// @dev Reverts unless `loveAmount` is a multiple of `RATE`, so the peg never | 66 | + /// @dev Reverts with `AmountNotDivisibleByRate` unless `loveAmount` is a |
| 41 | - /// rounds against the caller or the remaining holders. | 67 | + /// multiple of `RATE`, so the peg never rounds against the caller or |
| 68 | + /// the remaining holders. LOVE is burned before the wETH leaves, and | ||
| 69 | + /// the burn already caps the amount at the caller's balance. | ||
| 70 | + /// @param loveAmount The LOVE to burn, a multiple of `RATE`. | ||
| 42 | function withdraw(uint256 loveAmount) external { | 71 | function withdraw(uint256 loveAmount) external { |
| 43 | if (loveAmount % RATE != 0) revert AmountNotDivisibleByRate(loveAmount, RATE); | 72 | if (loveAmount % RATE != 0) revert AmountNotDivisibleByRate(loveAmount, RATE); |
| 44 | 73 | ||
modified
test/Love.t.sol +9 -0 | @@ -7,8 +7,15 @@ import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.so | ||
| 7 | 7 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; |
| 8 | 8 | import {Test} from "forge-std/Test.sol"; |
| 9 | 9 | |
| 10 | +/// @title LoveTest | |
| 11 | +/// @notice Exercises the peg end to end against a WETH9 stand-in: minting on | |
| 12 | +/// deposit, redemption on withdraw, the divisibility rule, and the | |
| 13 | +/// invariant that supply always equals the wETH backing times the rate. | |
| 10 | 14 | contract LoveTest is Test { |
| 15 | + /// @notice The token under test. | |
| 11 | 16 | Love public love; |
| 17 | + | |
| 18 | + /// @notice The wETH it is pegged to. | |
| 12 | 19 | MockWETH public weth; |
| 13 | 20 | |
| 14 | 21 | address alice = makeAddr("alice"); |
| @@ -362,12 +369,14 @@ contract LoveTest is Test { | ||
| 362 | 369 | } |
| 363 | 370 | } |
| 364 | 371 | |
| 372 | + /// @dev Gives `account` `amount` wETH by wrapping fresh ETH. | |
| 365 | 373 | function _fund(address account, uint256 amount) internal { |
| 366 | 374 | vm.deal(account, amount); |
| 367 | 375 | vm.prank(account); |
| 368 | 376 | weth.deposit{value: amount}(); |
| 369 | 377 | } |
| 370 | 378 | |
| 379 | + /// @dev Approves and deposits `wethAmount` as `account`, in one step. | |
| 371 | 380 | function _deposit(address account, uint256 wethAmount) internal { |
| 372 | 381 | vm.startPrank(account); |
| 373 | 382 | weth.approve(address(love), wethAmount); |
| @@ -7,8 +7,15 @@ import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.so | |||
| 7 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | 7 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; |
| 8 | import {Test} from "forge-std/Test.sol"; | 8 | import {Test} from "forge-std/Test.sol"; |
| 9 | 9 | ||
| 10 | +/// @title LoveTest | ||
| 11 | +/// @notice Exercises the peg end to end against a WETH9 stand-in: minting on | ||
| 12 | +/// deposit, redemption on withdraw, the divisibility rule, and the | ||
| 13 | +/// invariant that supply always equals the wETH backing times the rate. | ||
| 10 | contract LoveTest is Test { | 14 | contract LoveTest is Test { |
| 15 | + /// @notice The token under test. | ||
| 11 | Love public love; | 16 | Love public love; |
| 17 | + | ||
| 18 | + /// @notice The wETH it is pegged to. | ||
| 12 | MockWETH public weth; | 19 | MockWETH public weth; |
| 13 | 20 | ||
| 14 | address alice = makeAddr("alice"); | 21 | address alice = makeAddr("alice"); |
| @@ -362,12 +369,14 @@ contract LoveTest is Test { | |||
| 362 | } | 369 | } |
| 363 | } | 370 | } |
| 364 | 371 | ||
| 372 | + /// @dev Gives `account` `amount` wETH by wrapping fresh ETH. | ||
| 365 | function _fund(address account, uint256 amount) internal { | 373 | function _fund(address account, uint256 amount) internal { |
| 366 | vm.deal(account, amount); | 374 | vm.deal(account, amount); |
| 367 | vm.prank(account); | 375 | vm.prank(account); |
| 368 | weth.deposit{value: amount}(); | 376 | weth.deposit{value: amount}(); |
| 369 | } | 377 | } |
| 370 | 378 | ||
| 379 | + /// @dev Approves and deposits `wethAmount` as `account`, in one step. | ||
| 371 | function _deposit(address account, uint256 wethAmount) internal { | 380 | function _deposit(address account, uint256 wethAmount) internal { |
| 372 | vm.startPrank(account); | 381 | vm.startPrank(account); |
| 373 | weth.approve(address(love), wethAmount); | 382 | weth.approve(address(love), wethAmount); |
modified
test/LoveCreate2.t.sol +2 -0 | @@ -5,6 +5,7 @@ import {LoveScript} from "../script/Love.s.sol"; | ||
| 5 | 5 | import {Love} from "../src/Love.sol"; |
| 6 | 6 | import {Test} from "forge-std/Test.sol"; |
| 7 | 7 | |
| 8 | +/// @title LoveCreate2Test | |
| 8 | 9 | /// @notice The deployment address derives from the salt, the creation code and |
| 9 | 10 | /// the wETH constructor argument. These tests pin that derivation: same |
| 10 | 11 | /// salt and same wETH give the same address on any chain, a different |
| @@ -79,6 +80,7 @@ contract LoveCreate2Test is Test { | ||
| 79 | 80 | assertEq(script.predict(salt, weth), _deploy(salt, weth)); |
| 80 | 81 | } |
| 81 | 82 | |
| 83 | + /// @dev Deploys through the canonical deterministic deployer, as the script does. | |
| 82 | 84 | function _deploy(bytes32 salt, address weth) internal returns (address deployed) { |
| 83 | 85 | (bool ok, bytes memory ret) = CREATE2_FACTORY.call(abi.encodePacked(salt, script.initCode(weth))); |
| 84 | 86 | require(ok, "create2 deployment failed"); |
| @@ -5,6 +5,7 @@ import {LoveScript} from "../script/Love.s.sol"; | |||
| 5 | import {Love} from "../src/Love.sol"; | 5 | import {Love} from "../src/Love.sol"; |
| 6 | import {Test} from "forge-std/Test.sol"; | 6 | import {Test} from "forge-std/Test.sol"; |
| 7 | 7 | ||
| 8 | +/// @title LoveCreate2Test | ||
| 8 | /// @notice The deployment address derives from the salt, the creation code and | 9 | /// @notice The deployment address derives from the salt, the creation code and |
| 9 | /// the wETH constructor argument. These tests pin that derivation: same | 10 | /// the wETH constructor argument. These tests pin that derivation: same |
| 10 | /// salt and same wETH give the same address on any chain, a different | 11 | /// salt and same wETH give the same address on any chain, a different |
| @@ -79,6 +80,7 @@ contract LoveCreate2Test is Test { | |||
| 79 | assertEq(script.predict(salt, weth), _deploy(salt, weth)); | 80 | assertEq(script.predict(salt, weth), _deploy(salt, weth)); |
| 80 | } | 81 | } |
| 81 | 82 | ||
| 83 | + /// @dev Deploys through the canonical deterministic deployer, as the script does. | ||
| 82 | function _deploy(bytes32 salt, address weth) internal returns (address deployed) { | 84 | function _deploy(bytes32 salt, address weth) internal returns (address deployed) { |
| 83 | (bool ok, bytes memory ret) = CREATE2_FACTORY.call(abi.encodePacked(salt, script.initCode(weth))); | 85 | (bool ok, bytes memory ret) = CREATE2_FACTORY.call(abi.encodePacked(salt, script.initCode(weth))); |
| 84 | require(ok, "create2 deployment failed"); | 86 | require(ok, "create2 deployment failed"); |
modified
test/mocks/MockWETH.sol +5 -0 | @@ -3,19 +3,24 @@ pragma solidity ^0.8.30; | ||
| 3 | 3 | |
| 4 | 4 | import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; |
| 5 | 5 | |
| 6 | +/// @title MockWETH | |
| 6 | 7 | /// @notice Minimal stand-in for WETH9: wraps ETH 1:1, same 18 decimals, same |
| 7 | 8 | /// `deposit`/`withdraw` entrypoints. Test-only. |
| 8 | 9 | contract MockWETH is ERC20 { |
| 9 | 10 | constructor() ERC20("Wrapped Ether", "WETH") {} |
| 10 | 11 | |
| 12 | + /// @notice Wrap any ETH sent with no calldata. | |
| 11 | 13 | receive() external payable { |
| 12 | 14 | deposit(); |
| 13 | 15 | } |
| 14 | 16 | |
| 17 | + /// @notice Wrap the ETH sent with this call, minting the same amount of wETH. | |
| 15 | 18 | function deposit() public payable { |
| 16 | 19 | _mint(msg.sender, msg.value); |
| 17 | 20 | } |
| 18 | 21 | |
| 22 | + /// @notice Burn `amount` wETH and send the caller the same amount of ETH. | |
| 23 | + /// @param amount The wETH to unwrap, in wei. | |
| 19 | 24 | function withdraw(uint256 amount) external { |
| 20 | 25 | _burn(msg.sender, amount); |
| 21 | 26 | payable(msg.sender).transfer(amount); |
| @@ -3,19 +3,24 @@ pragma solidity ^0.8.30; | |||
| 3 | 3 | ||
| 4 | import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; | 4 | import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; |
| 5 | 5 | ||
| 6 | +/// @title MockWETH | ||
| 6 | /// @notice Minimal stand-in for WETH9: wraps ETH 1:1, same 18 decimals, same | 7 | /// @notice Minimal stand-in for WETH9: wraps ETH 1:1, same 18 decimals, same |
| 7 | /// `deposit`/`withdraw` entrypoints. Test-only. | 8 | /// `deposit`/`withdraw` entrypoints. Test-only. |
| 8 | contract MockWETH is ERC20 { | 9 | contract MockWETH is ERC20 { |
| 9 | constructor() ERC20("Wrapped Ether", "WETH") {} | 10 | constructor() ERC20("Wrapped Ether", "WETH") {} |
| 10 | 11 | ||
| 12 | + /// @notice Wrap any ETH sent with no calldata. | ||
| 11 | receive() external payable { | 13 | receive() external payable { |
| 12 | deposit(); | 14 | deposit(); |
| 13 | } | 15 | } |
| 14 | 16 | ||
| 17 | + /// @notice Wrap the ETH sent with this call, minting the same amount of wETH. | ||
| 15 | function deposit() public payable { | 18 | function deposit() public payable { |
| 16 | _mint(msg.sender, msg.value); | 19 | _mint(msg.sender, msg.value); |
| 17 | } | 20 | } |
| 18 | 21 | ||
| 22 | + /// @notice Burn `amount` wETH and send the caller the same amount of ETH. | ||
| 23 | + /// @param amount The wETH to unwrap, in wei. | ||
| 19 | function withdraw(uint256 amount) external { | 24 | function withdraw(uint256 amount) external { |
| 20 | _burn(msg.sender, amount); | 25 | _burn(msg.sender, amount); |
| 21 | payable(msg.sender).transfer(amount); | 26 | payable(msg.sender).transfer(amount); |