add weth peg tests and docs
5376202 parent: 92dc0c7 modified
.env.example +3 -0 | @@ -1,6 +1,9 @@ | ||
| 1 | 1 | # RPC endpoints |
| 2 | 2 | BASE_SEPOLIA_RPC_URL=https://sepolia.base.org |
| 3 | 3 | |
| 4 | +# wETH the token is pegged to — defaults to the OP-Stack address below | |
| 5 | +WETH=0x4200000000000000000000000000000000000006 | |
| 6 | + | |
| 4 | 7 | # Block explorer verification |
| 5 | 8 | ETHERSCAN_API_KEY= |
| 6 | 9 | |
| @@ -1,6 +1,9 @@ | |||
| 1 | # RPC endpoints | 1 | # RPC endpoints |
| 2 | BASE_SEPOLIA_RPC_URL=https://sepolia.base.org | 2 | BASE_SEPOLIA_RPC_URL=https://sepolia.base.org |
| 3 | 3 | ||
| 4 | +# wETH the token is pegged to — defaults to the OP-Stack address below | ||
| 5 | +WETH=0x4200000000000000000000000000000000000006 | ||
| 6 | + | ||
| 4 | # Block explorer verification | 7 | # Block explorer verification |
| 5 | ETHERSCAN_API_KEY= | 8 | ETHERSCAN_API_KEY= |
| 6 | 9 | ||
modified
CHANGELOG.md +16 -7 | @@ -10,17 +10,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | ||
| 10 | 10 | ### Added |
| 11 | 11 | |
| 12 | 12 | - Foundry project scaffolding (`forge-std`, CI workflow, `.gitignore`). |
| 13 | -- `Love`, an ERC-20 token (`LOVE`, 18 decimals) on OpenZeppelin v5, with an | |
| 14 | - unrestricted public `mint(address,uint256)` — anyone can mint any amount. | |
| 15 | -- Metadata, mint, transfer, approve/`transferFrom`, revert and fuzz tests. | |
| 13 | +- `Love`, an ERC-20 token (`LOVE`, 18 decimals) on OpenZeppelin v5, pegged to | |
| 14 | + wETH at 100000 LOVE per wETH. No mint function and no owner: supply moves | |
| 15 | + only through the permissionless `deposit(uint256)` and `withdraw(uint256)`, | |
| 16 | + so circulating LOVE is always backed by wETH held by the contract. | |
| 17 | + `withdraw` reverts with `AmountNotDivisibleByRate` unless the amount is a | |
| 18 | + multiple of the rate, keeping the peg free of rounding dust. | |
| 19 | +- `MockWETH`, a test-only WETH9 stand-in, and tests covering deposit, withdraw, | |
| 20 | + the divisibility revert, backing invariants, round-trips, transfers, | |
| 21 | + approve/`transferFrom`, reverts and fuzzing. | |
| 16 | 22 | - Deployment script `script/Love.s.sol`, deploying with CREATE2 through the |
| 17 | - canonical deterministic deployer so `Love` gets the same address on every EVM | |
| 18 | - network; salt defaults to `keccak256("LOVE")` and is overridable via `SALT`. | |
| 23 | + canonical deterministic deployer; salt defaults to `keccak256("LOVE")` and is | |
| 24 | + overridable via `SALT`, wETH defaults to the OP-Stack address and is | |
| 25 | + overridable via `WETH`. The address embeds the wETH argument, so it is | |
| 26 | + identical across chains that share one wETH deployment. | |
| 19 | 27 | - `predict()` entrypoint printing the deployment address without broadcasting, |
| 20 | - and CREATE2 tests pinning address determinism across chain IDs. | |
| 28 | + and CREATE2 tests pinning address determinism across chain IDs and its | |
| 29 | + dependence on the wETH argument. | |
| 21 | 30 | - `foundry.toml` with pinned solc 0.8.30, optimizer, fmt rules, fuzz/invariant |
| 22 | 31 | defaults, and Base Sepolia RPC/Etherscan endpoints driven by env vars. |
| 23 | 32 | - `script/multichain-check.sh`, deploying `Love` on two local anvil nodes with |
| 24 | 33 | different chain IDs and failing if the two addresses don't match. |
| 25 | 34 | - `remappings.txt` for `@openzeppelin/contracts/` and `forge-std/`. |
| 26 | -- `.env.example` documenting required environment variables. | |
| 35 | +- `.env.example` documenting required environment variables, including `WETH`. | |
| @@ -10,17 +10,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | |||
| 10 | ### Added | 10 | ### Added |
| 11 | 11 | ||
| 12 | - Foundry project scaffolding (`forge-std`, CI workflow, `.gitignore`). | 12 | - Foundry project scaffolding (`forge-std`, CI workflow, `.gitignore`). |
| 13 | -- `Love`, an ERC-20 token (`LOVE`, 18 decimals) on OpenZeppelin v5, with an | 13 | +- `Love`, an ERC-20 token (`LOVE`, 18 decimals) on OpenZeppelin v5, pegged to |
| 14 | - unrestricted public `mint(address,uint256)` — anyone can mint any amount. | 14 | + wETH at 100000 LOVE per wETH. No mint function and no owner: supply moves |
| 15 | -- Metadata, mint, transfer, approve/`transferFrom`, revert and fuzz tests. | 15 | + only through the permissionless `deposit(uint256)` and `withdraw(uint256)`, |
| 16 | + so circulating LOVE is always backed by wETH held by the contract. | ||
| 17 | + `withdraw` reverts with `AmountNotDivisibleByRate` unless the amount is a | ||
| 18 | + multiple of the rate, keeping the peg free of rounding dust. | ||
| 19 | +- `MockWETH`, a test-only WETH9 stand-in, and tests covering deposit, withdraw, | ||
| 20 | + the divisibility revert, backing invariants, round-trips, transfers, | ||
| 21 | + approve/`transferFrom`, reverts and fuzzing. | ||
| 16 | - Deployment script `script/Love.s.sol`, deploying with CREATE2 through the | 22 | - Deployment script `script/Love.s.sol`, deploying with CREATE2 through the |
| 17 | - canonical deterministic deployer so `Love` gets the same address on every EVM | 23 | + canonical deterministic deployer; salt defaults to `keccak256("LOVE")` and is |
| 18 | - network; salt defaults to `keccak256("LOVE")` and is overridable via `SALT`. | 24 | + overridable via `SALT`, wETH defaults to the OP-Stack address and is |
| 25 | + overridable via `WETH`. The address embeds the wETH argument, so it is | ||
| 26 | + identical across chains that share one wETH deployment. | ||
| 19 | - `predict()` entrypoint printing the deployment address without broadcasting, | 27 | - `predict()` entrypoint printing the deployment address without broadcasting, |
| 20 | - and CREATE2 tests pinning address determinism across chain IDs. | 28 | + and CREATE2 tests pinning address determinism across chain IDs and its |
| 29 | + dependence on the wETH argument. | ||
| 21 | - `foundry.toml` with pinned solc 0.8.30, optimizer, fmt rules, fuzz/invariant | 30 | - `foundry.toml` with pinned solc 0.8.30, optimizer, fmt rules, fuzz/invariant |
| 22 | defaults, and Base Sepolia RPC/Etherscan endpoints driven by env vars. | 31 | defaults, and Base Sepolia RPC/Etherscan endpoints driven by env vars. |
| 23 | - `script/multichain-check.sh`, deploying `Love` on two local anvil nodes with | 32 | - `script/multichain-check.sh`, deploying `Love` on two local anvil nodes with |
| 24 | different chain IDs and failing if the two addresses don't match. | 33 | different chain IDs and failing if the two addresses don't match. |
| 25 | - `remappings.txt` for `@openzeppelin/contracts/` and `forge-std/`. | 34 | - `remappings.txt` for `@openzeppelin/contracts/` and `forge-std/`. |
| 26 | -- `.env.example` documenting required environment variables. | 35 | +- `.env.example` documenting required environment variables, including `WETH`. |
modified
script/Love.s.sol +1 -1 | @@ -45,7 +45,7 @@ contract LoveScript is Script { | ||
| 45 | 45 | console.log("Love deployed at", address(love)); |
| 46 | 46 | console.log("name ", love.name()); |
| 47 | 47 | console.log("symbol ", love.symbol()); |
| 48 | - console.log("weth ", address(love.weth())); | |
| 48 | + console.log("weth ", address(love.WETH())); | |
| 49 | 49 | console.log("rate ", love.RATE()); |
| 50 | 50 | console.log("totalSupply ", love.totalSupply()); |
| 51 | 51 | |
| @@ -45,7 +45,7 @@ contract LoveScript is Script { | |||
| 45 | console.log("Love deployed at", address(love)); | 45 | console.log("Love deployed at", address(love)); |
| 46 | console.log("name ", love.name()); | 46 | console.log("name ", love.name()); |
| 47 | console.log("symbol ", love.symbol()); | 47 | console.log("symbol ", love.symbol()); |
| 48 | - console.log("weth ", address(love.weth())); | 48 | + console.log("weth ", address(love.WETH())); |
| 49 | console.log("rate ", love.RATE()); | 49 | console.log("rate ", love.RATE()); |
| 50 | console.log("totalSupply ", love.totalSupply()); | 50 | console.log("totalSupply ", love.totalSupply()); |
| 51 | 51 | ||
modified
script/multichain-check.sh +1 -1 | @@ -83,7 +83,7 @@ report() { | ||
| 83 | 83 | "$label" "$addr" "$(((${#code} - 2) / 2))" \ |
| 84 | 84 | "$(cast call "$addr" 'name()(string)' --rpc-url "$rpc")" \ |
| 85 | 85 | "$(cast call "$addr" 'symbol()(string)' --rpc-url "$rpc")" \ |
| 86 | - "$(cast call "$addr" 'weth()(address)' --rpc-url "$rpc")" | |
| 86 | + "$(cast call "$addr" 'WETH()(address)' --rpc-url "$rpc")" | |
| 87 | 87 | } |
| 88 | 88 | |
| 89 | 89 | cd "$ROOT" |
| @@ -83,7 +83,7 @@ report() { | |||
| 83 | "$label" "$addr" "$(((${#code} - 2) / 2))" \ | 83 | "$label" "$addr" "$(((${#code} - 2) / 2))" \ |
| 84 | "$(cast call "$addr" 'name()(string)' --rpc-url "$rpc")" \ | 84 | "$(cast call "$addr" 'name()(string)' --rpc-url "$rpc")" \ |
| 85 | "$(cast call "$addr" 'symbol()(string)' --rpc-url "$rpc")" \ | 85 | "$(cast call "$addr" 'symbol()(string)' --rpc-url "$rpc")" \ |
| 86 | - "$(cast call "$addr" 'weth()(address)' --rpc-url "$rpc")" | 86 | + "$(cast call "$addr" 'WETH()(address)' --rpc-url "$rpc")" |
| 87 | } | 87 | } |
| 88 | 88 | ||
| 89 | cd "$ROOT" | 89 | cd "$ROOT" |
modified
src/Love.sol +4 -4 | @@ -15,7 +15,7 @@ contract Love is ERC20 { | ||
| 15 | 15 | |
| 16 | 16 | uint256 public constant RATE = 100_000; |
| 17 | 17 | |
| 18 | - IERC20 public immutable weth; | |
| 18 | + IERC20 public immutable WETH; | |
| 19 | 19 | |
| 20 | 20 | error AmountNotDivisibleByRate(uint256 loveAmount, uint256 rate); |
| 21 | 21 | |
| @@ -23,14 +23,14 @@ contract Love is ERC20 { | ||
| 23 | 23 | event Withdraw(address indexed account, uint256 loveAmount, uint256 wethAmount); |
| 24 | 24 | |
| 25 | 25 | constructor(IERC20 weth_) ERC20("Love", "LOVE") { |
| 26 | - weth = weth_; | |
| 26 | + WETH = weth_; | |
| 27 | 27 | } |
| 28 | 28 | |
| 29 | 29 | /// @notice Lock `wethAmount` wETH and mint `wethAmount * RATE` LOVE to the caller. |
| 30 | 30 | function deposit(uint256 wethAmount) external { |
| 31 | 31 | uint256 loveAmount = wethAmount * RATE; |
| 32 | 32 | |
| 33 | - weth.safeTransferFrom(msg.sender, address(this), wethAmount); | |
| 33 | + WETH.safeTransferFrom(msg.sender, address(this), wethAmount); | |
| 34 | 34 | _mint(msg.sender, loveAmount); |
| 35 | 35 | |
| 36 | 36 | emit Deposit(msg.sender, wethAmount, loveAmount); |
| @@ -45,7 +45,7 @@ contract Love is ERC20 { | ||
| 45 | 45 | uint256 wethAmount = loveAmount / RATE; |
| 46 | 46 | |
| 47 | 47 | _burn(msg.sender, loveAmount); |
| 48 | - weth.safeTransfer(msg.sender, wethAmount); | |
| 48 | + WETH.safeTransfer(msg.sender, wethAmount); | |
| 49 | 49 | |
| 50 | 50 | emit Withdraw(msg.sender, loveAmount, wethAmount); |
| 51 | 51 | } |
| @@ -15,7 +15,7 @@ contract Love is ERC20 { | |||
| 15 | 15 | ||
| 16 | uint256 public constant RATE = 100_000; | 16 | uint256 public constant RATE = 100_000; |
| 17 | 17 | ||
| 18 | - IERC20 public immutable weth; | 18 | + IERC20 public immutable WETH; |
| 19 | 19 | ||
| 20 | error AmountNotDivisibleByRate(uint256 loveAmount, uint256 rate); | 20 | error AmountNotDivisibleByRate(uint256 loveAmount, uint256 rate); |
| 21 | 21 | ||
| @@ -23,14 +23,14 @@ contract Love is ERC20 { | |||
| 23 | event Withdraw(address indexed account, uint256 loveAmount, uint256 wethAmount); | 23 | event Withdraw(address indexed account, uint256 loveAmount, uint256 wethAmount); |
| 24 | 24 | ||
| 25 | constructor(IERC20 weth_) ERC20("Love", "LOVE") { | 25 | constructor(IERC20 weth_) ERC20("Love", "LOVE") { |
| 26 | - weth = weth_; | 26 | + WETH = weth_; |
| 27 | } | 27 | } |
| 28 | 28 | ||
| 29 | /// @notice Lock `wethAmount` wETH and mint `wethAmount * RATE` LOVE to the caller. | 29 | /// @notice Lock `wethAmount` wETH and mint `wethAmount * RATE` LOVE to the caller. |
| 30 | function deposit(uint256 wethAmount) external { | 30 | function deposit(uint256 wethAmount) external { |
| 31 | uint256 loveAmount = wethAmount * RATE; | 31 | uint256 loveAmount = wethAmount * RATE; |
| 32 | 32 | ||
| 33 | - weth.safeTransferFrom(msg.sender, address(this), wethAmount); | 33 | + WETH.safeTransferFrom(msg.sender, address(this), wethAmount); |
| 34 | _mint(msg.sender, loveAmount); | 34 | _mint(msg.sender, loveAmount); |
| 35 | 35 | ||
| 36 | emit Deposit(msg.sender, wethAmount, loveAmount); | 36 | emit Deposit(msg.sender, wethAmount, loveAmount); |
| @@ -45,7 +45,7 @@ contract Love is ERC20 { | |||
| 45 | uint256 wethAmount = loveAmount / RATE; | 45 | uint256 wethAmount = loveAmount / RATE; |
| 46 | 46 | ||
| 47 | _burn(msg.sender, loveAmount); | 47 | _burn(msg.sender, loveAmount); |
| 48 | - weth.safeTransfer(msg.sender, wethAmount); | 48 | + WETH.safeTransfer(msg.sender, wethAmount); |
| 49 | 49 | ||
| 50 | emit Withdraw(msg.sender, loveAmount, wethAmount); | 50 | emit Withdraw(msg.sender, loveAmount, wethAmount); |
| 51 | } | 51 | } |
modified
test/Love.t.sol +262 -50 | @@ -2,19 +2,27 @@ | ||
| 2 | 2 | pragma solidity ^0.8.30; |
| 3 | 3 | |
| 4 | 4 | import {Love} from "../src/Love.sol"; |
| 5 | +import {MockWETH} from "./mocks/MockWETH.sol"; | |
| 5 | 6 | import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; |
| 6 | 7 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; |
| 7 | 8 | import {Test} from "forge-std/Test.sol"; |
| 8 | 9 | |
| 9 | 10 | contract LoveTest is Test { |
| 10 | 11 | Love public love; |
| 12 | + MockWETH public weth; | |
| 11 | 13 | |
| 12 | 14 | address alice = makeAddr("alice"); |
| 13 | 15 | address bob = makeAddr("bob"); |
| 14 | 16 | address stranger = makeAddr("stranger"); |
| 15 | 17 | |
| 18 | + uint256 constant RATE = 100_000; | |
| 19 | + | |
| 16 | 20 | function setUp() public { |
| 17 | - love = new Love(); | |
| 21 | + weth = new MockWETH(); | |
| 22 | + love = new Love(IERC20(address(weth))); | |
| 23 | + | |
| 24 | + _fund(alice, 10 ether); | |
| 25 | + _fund(bob, 10 ether); | |
| 18 | 26 | } |
| 19 | 27 | |
| 20 | 28 | /*////////////////////////////////////////////////////////////// |
| @@ -31,56 +39,191 @@ contract LoveTest is Test { | ||
| 31 | 39 | assertEq(love.totalSupply(), 0); |
| 32 | 40 | } |
| 33 | 41 | |
| 42 | + function test_PegParameters() public view { | |
| 43 | + assertEq(love.RATE(), RATE); | |
| 44 | + assertEq(address(love.WETH()), address(weth)); | |
| 45 | + } | |
| 46 | + | |
| 47 | + /// @dev No mint entrypoint may survive: supply moves only through the peg. | |
| 48 | + function test_NoMintFunction() public view { | |
| 49 | + assertEq(address(love).code.length > 0, true); | |
| 50 | + (bool ok,) = address(love).staticcall(abi.encodeWithSignature("mint(address,uint256)", alice, 1e18)); | |
| 51 | + assertFalse(ok); | |
| 52 | + } | |
| 53 | + | |
| 34 | 54 | /*////////////////////////////////////////////////////////////// |
| 35 | - MINT | |
| 55 | + DEPOSIT | |
| 36 | 56 | //////////////////////////////////////////////////////////////*/ |
| 37 | 57 | |
| 38 | - function test_Mint() public { | |
| 39 | - vm.prank(alice); | |
| 40 | - love.mint(alice, 100e18); | |
| 58 | + function test_Deposit() public { | |
| 59 | + _deposit(alice, 1 ether); | |
| 41 | 60 | |
| 42 | - assertEq(love.balanceOf(alice), 100e18); | |
| 43 | - assertEq(love.totalSupply(), 100e18); | |
| 61 | + assertEq(love.balanceOf(alice), 1 ether * RATE); | |
| 62 | + assertEq(love.totalSupply(), 1 ether * RATE); | |
| 63 | + assertEq(weth.balanceOf(address(love)), 1 ether); | |
| 64 | + assertEq(weth.balanceOf(alice), 9 ether); | |
| 44 | 65 | } |
| 45 | 66 | |
| 46 | - /// @dev The point of this contract: `mint` has no access control. | |
| 47 | - function test_AnyoneCanMintToAnyone() public { | |
| 48 | - vm.prank(stranger); | |
| 49 | - love.mint(bob, 1000e18); | |
| 67 | + function test_DepositIsPermissionless() public { | |
| 68 | + _fund(stranger, 1 ether); | |
| 69 | + _deposit(stranger, 1 ether); | |
| 70 | + | |
| 71 | + assertEq(love.balanceOf(stranger), 1 ether * RATE); | |
| 72 | + } | |
| 73 | + | |
| 74 | + function test_DepositEmitsDeposit() public { | |
| 75 | + vm.prank(alice); | |
| 76 | + weth.approve(address(love), 2 ether); | |
| 77 | + | |
| 78 | + vm.expectEmit(true, false, false, true); | |
| 79 | + emit Love.Deposit(alice, 2 ether, 2 ether * RATE); | |
| 50 | 80 | |
| 51 | - assertEq(love.balanceOf(bob), 1000e18); | |
| 52 | - assertEq(love.balanceOf(stranger), 0); | |
| 53 | - assertEq(love.totalSupply(), 1000e18); | |
| 81 | + vm.prank(alice); | |
| 82 | + love.deposit(2 ether); | |
| 54 | 83 | } |
| 55 | 84 | |
| 56 | - function test_MintEmitsTransferFromZero() public { | |
| 85 | + function test_DepositEmitsTransferFromZero() public { | |
| 86 | + vm.prank(alice); | |
| 87 | + weth.approve(address(love), 1 ether); | |
| 88 | + | |
| 57 | 89 | vm.expectEmit(true, true, false, true); |
| 58 | - emit IERC20.Transfer(address(0), alice, 5e18); | |
| 90 | + emit IERC20.Transfer(address(0), alice, 1 ether * RATE); | |
| 59 | 91 | |
| 60 | 92 | vm.prank(alice); |
| 61 | - love.mint(alice, 5e18); | |
| 93 | + love.deposit(1 ether); | |
| 62 | 94 | } |
| 63 | 95 | |
| 64 | - function test_MintAccumulates() public { | |
| 65 | - vm.startPrank(alice); | |
| 66 | - love.mint(alice, 1e18); | |
| 67 | - love.mint(alice, 2e18); | |
| 68 | - vm.stopPrank(); | |
| 96 | + function test_DepositAccumulates() public { | |
| 97 | + _deposit(alice, 1 ether); | |
| 98 | + _deposit(alice, 3 ether); | |
| 69 | 99 | |
| 70 | - assertEq(love.balanceOf(alice), 3e18); | |
| 71 | - assertEq(love.totalSupply(), 3e18); | |
| 100 | + assertEq(love.balanceOf(alice), 4 ether * RATE); | |
| 101 | + assertEq(weth.balanceOf(address(love)), 4 ether); | |
| 72 | 102 | } |
| 73 | 103 | |
| 74 | - function test_RevertWhen_MintingToZeroAddress() public { | |
| 75 | - vm.expectRevert(abi.encodeWithSelector(IERC20Errors.ERC20InvalidReceiver.selector, address(0))); | |
| 76 | - love.mint(address(0), 1e18); | |
| 104 | + function test_DepositZero() public { | |
| 105 | + _deposit(alice, 0); | |
| 106 | + | |
| 107 | + assertEq(love.balanceOf(alice), 0); | |
| 108 | + assertEq(love.totalSupply(), 0); | |
| 77 | 109 | } |
| 78 | 110 | |
| 79 | - function test_RevertWhen_MintOverflowsTotalSupply() public { | |
| 80 | - love.mint(alice, type(uint256).max); | |
| 111 | + function test_RevertWhen_DepositWithoutApproval() public { | |
| 112 | + vm.prank(alice); | |
| 113 | + vm.expectRevert( | |
| 114 | + abi.encodeWithSelector(IERC20Errors.ERC20InsufficientAllowance.selector, address(love), 0, 1 ether) | |
| 115 | + ); | |
| 116 | + love.deposit(1 ether); | |
| 117 | + } | |
| 118 | + | |
| 119 | + function test_RevertWhen_DepositExceedsWethBalance() public { | |
| 120 | + vm.prank(alice); | |
| 121 | + weth.approve(address(love), 100 ether); | |
| 122 | + | |
| 123 | + vm.prank(alice); | |
| 124 | + vm.expectRevert( | |
| 125 | + abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, alice, 10 ether, 100 ether) | |
| 126 | + ); | |
| 127 | + love.deposit(100 ether); | |
| 128 | + } | |
| 81 | 129 | |
| 130 | + function test_RevertWhen_DepositOverflows() public { | |
| 131 | + vm.prank(alice); | |
| 82 | 132 | vm.expectRevert(); |
| 83 | - love.mint(bob, 1); | |
| 133 | + love.deposit(type(uint256).max); | |
| 134 | + } | |
| 135 | + | |
| 136 | + /*////////////////////////////////////////////////////////////// | |
| 137 | + WITHDRAW | |
| 138 | + //////////////////////////////////////////////////////////////*/ | |
| 139 | + | |
| 140 | + function test_Withdraw() public { | |
| 141 | + _deposit(alice, 1 ether); | |
| 142 | + | |
| 143 | + vm.prank(alice); | |
| 144 | + love.withdraw(1 ether * RATE); | |
| 145 | + | |
| 146 | + assertEq(love.balanceOf(alice), 0); | |
| 147 | + assertEq(love.totalSupply(), 0); | |
| 148 | + assertEq(weth.balanceOf(alice), 10 ether); | |
| 149 | + assertEq(weth.balanceOf(address(love)), 0); | |
| 150 | + } | |
| 151 | + | |
| 152 | + function test_PartialWithdraw() public { | |
| 153 | + _deposit(alice, 1 ether); | |
| 154 | + | |
| 155 | + vm.prank(alice); | |
| 156 | + love.withdraw(0.25 ether * RATE); | |
| 157 | + | |
| 158 | + assertEq(love.balanceOf(alice), 0.75 ether * RATE); | |
| 159 | + assertEq(weth.balanceOf(alice), 9.25 ether); | |
| 160 | + assertEq(weth.balanceOf(address(love)), 0.75 ether); | |
| 161 | + } | |
| 162 | + | |
| 163 | + function test_WithdrawEmitsWithdraw() public { | |
| 164 | + _deposit(alice, 1 ether); | |
| 165 | + | |
| 166 | + vm.expectEmit(true, false, false, true); | |
| 167 | + emit Love.Withdraw(alice, 1 ether * RATE, 1 ether); | |
| 168 | + | |
| 169 | + vm.prank(alice); | |
| 170 | + love.withdraw(1 ether * RATE); | |
| 171 | + } | |
| 172 | + | |
| 173 | + /// @dev LOVE received from someone else is redeemable all the same. | |
| 174 | + function test_WithdrawAfterTransfer() public { | |
| 175 | + _deposit(alice, 1 ether); | |
| 176 | + | |
| 177 | + vm.prank(alice); | |
| 178 | + // forge-lint: disable-next-line(erc20-unchecked-transfer) | |
| 179 | + love.transfer(bob, 1 ether * RATE); | |
| 180 | + | |
| 181 | + vm.prank(bob); | |
| 182 | + love.withdraw(1 ether * RATE); | |
| 183 | + | |
| 184 | + assertEq(weth.balanceOf(bob), 11 ether); | |
| 185 | + assertEq(love.totalSupply(), 0); | |
| 186 | + } | |
| 187 | + | |
| 188 | + function test_WithdrawZero() public { | |
| 189 | + _deposit(alice, 1 ether); | |
| 190 | + | |
| 191 | + vm.prank(alice); | |
| 192 | + love.withdraw(0); | |
| 193 | + | |
| 194 | + assertEq(love.balanceOf(alice), 1 ether * RATE); | |
| 195 | + } | |
| 196 | + | |
| 197 | + /// @dev Anything not a multiple of the rate would round wETH out of the peg. | |
| 198 | + function test_RevertWhen_WithdrawNotDivisibleByRate() public { | |
| 199 | + _deposit(alice, 1 ether); | |
| 200 | + | |
| 201 | + vm.prank(alice); | |
| 202 | + vm.expectRevert(abi.encodeWithSelector(Love.AmountNotDivisibleByRate.selector, RATE + 1, RATE)); | |
| 203 | + love.withdraw(RATE + 1); | |
| 204 | + } | |
| 205 | + | |
| 206 | + function test_RevertWhen_WithdrawExceedsBalance() public { | |
| 207 | + _deposit(alice, 1 ether); | |
| 208 | + | |
| 209 | + vm.prank(alice); | |
| 210 | + vm.expectRevert( | |
| 211 | + abi.encodeWithSelector( | |
| 212 | + IERC20Errors.ERC20InsufficientBalance.selector, alice, 1 ether * RATE, 2 ether * RATE | |
| 213 | + ) | |
| 214 | + ); | |
| 215 | + love.withdraw(2 ether * RATE); | |
| 216 | + } | |
| 217 | + | |
| 218 | + /// @dev One holder cannot redeem against another holder's collateral. | |
| 219 | + function test_RevertWhen_WithdrawWithoutDepositing() public { | |
| 220 | + _deposit(alice, 1 ether); | |
| 221 | + | |
| 222 | + vm.prank(stranger); | |
| 223 | + vm.expectRevert( | |
| 224 | + abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, stranger, 0, 1 ether * RATE) | |
| 225 | + ); | |
| 226 | + love.withdraw(1 ether * RATE); | |
| 84 | 227 | } |
| 85 | 228 | |
| 86 | 229 | /*////////////////////////////////////////////////////////////// |
| @@ -88,20 +231,18 @@ contract LoveTest is Test { | ||
| 88 | 231 | //////////////////////////////////////////////////////////////*/ |
| 89 | 232 | |
| 90 | 233 | function test_Transfer() public { |
| 91 | - love.mint(alice, 10e18); | |
| 234 | + _deposit(alice, 1 ether); | |
| 92 | 235 | |
| 93 | 236 | vm.prank(alice); |
| 94 | 237 | assertTrue(love.transfer(bob, 4e18)); |
| 95 | 238 | |
| 96 | - assertEq(love.balanceOf(alice), 6e18); | |
| 239 | + assertEq(love.balanceOf(alice), 1 ether * RATE - 4e18); | |
| 97 | 240 | assertEq(love.balanceOf(bob), 4e18); |
| 98 | 241 | } |
| 99 | 242 | |
| 100 | 243 | function test_RevertWhen_TransferExceedsBalance() public { |
| 101 | - love.mint(alice, 1e18); | |
| 102 | - | |
| 103 | 244 | vm.prank(alice); |
| 104 | - vm.expectRevert(abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, alice, 1e18, 2e18)); | |
| 245 | + vm.expectRevert(abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, alice, 0, 2e18)); | |
| 105 | 246 | // forge-lint: disable-next-line(erc20-unchecked-transfer) |
| 106 | 247 | love.transfer(bob, 2e18); |
| 107 | 248 | } |
| @@ -111,7 +252,7 @@ contract LoveTest is Test { | ||
| 111 | 252 | //////////////////////////////////////////////////////////////*/ |
| 112 | 253 | |
| 113 | 254 | function test_ApproveAndTransferFrom() public { |
| 114 | - love.mint(alice, 10e18); | |
| 255 | + _deposit(alice, 1 ether); | |
| 115 | 256 | |
| 116 | 257 | vm.prank(alice); |
| 117 | 258 | assertTrue(love.approve(bob, 6e18)); |
| @@ -120,13 +261,13 @@ contract LoveTest is Test { | ||
| 120 | 261 | vm.prank(bob); |
| 121 | 262 | assertTrue(love.transferFrom(alice, stranger, 6e18)); |
| 122 | 263 | |
| 123 | - assertEq(love.balanceOf(alice), 4e18); | |
| 264 | + assertEq(love.balanceOf(alice), 1 ether * RATE - 6e18); | |
| 124 | 265 | assertEq(love.balanceOf(stranger), 6e18); |
| 125 | 266 | assertEq(love.allowance(alice, bob), 0); |
| 126 | 267 | } |
| 127 | 268 | |
| 128 | 269 | function test_RevertWhen_TransferFromExceedsAllowance() public { |
| 129 | - love.mint(alice, 10e18); | |
| 270 | + _deposit(alice, 1 ether); | |
| 130 | 271 | |
| 131 | 272 | vm.prank(alice); |
| 132 | 273 | love.approve(bob, 1e18); |
| @@ -137,29 +278,100 @@ contract LoveTest is Test { | ||
| 137 | 278 | love.transferFrom(alice, stranger, 2e18); |
| 138 | 279 | } |
| 139 | 280 | |
| 281 | + /*////////////////////////////////////////////////////////////// | |
| 282 | + PEG | |
| 283 | + //////////////////////////////////////////////////////////////*/ | |
| 284 | + | |
| 285 | + function test_RoundTripIsLossless() public { | |
| 286 | + uint256 before = weth.balanceOf(alice); | |
| 287 | + | |
| 288 | + _deposit(alice, 3 ether); | |
| 289 | + vm.prank(alice); | |
| 290 | + love.withdraw(3 ether * RATE); | |
| 291 | + | |
| 292 | + assertEq(weth.balanceOf(alice), before); | |
| 293 | + assertEq(love.balanceOf(alice), 0); | |
| 294 | + assertEq(love.totalSupply(), 0); | |
| 295 | + } | |
| 296 | + | |
| 297 | + function test_SupplyStaysFullyBacked() public { | |
| 298 | + _deposit(alice, 1 ether); | |
| 299 | + _deposit(bob, 2 ether); | |
| 300 | + | |
| 301 | + assertEq(love.totalSupply(), weth.balanceOf(address(love)) * RATE); | |
| 302 | + | |
| 303 | + vm.prank(alice); | |
| 304 | + love.withdraw(0.5 ether * RATE); | |
| 305 | + | |
| 306 | + assertEq(love.totalSupply(), weth.balanceOf(address(love)) * RATE); | |
| 307 | + } | |
| 308 | + | |
| 309 | + /// @dev wETH sent straight to the contract is not claimable — it only lifts | |
| 310 | + /// the backing, it never mints LOVE. | |
| 311 | + function test_DonatedWethDoesNotMint() public { | |
| 312 | + _deposit(alice, 1 ether); | |
| 313 | + | |
| 314 | + vm.prank(bob); | |
| 315 | + // forge-lint: disable-next-line(erc20-unchecked-transfer) | |
| 316 | + weth.transfer(address(love), 5 ether); | |
| 317 | + | |
| 318 | + assertEq(love.totalSupply(), 1 ether * RATE); | |
| 319 | + assertEq(weth.balanceOf(address(love)), 6 ether); | |
| 320 | + } | |
| 321 | + | |
| 140 | 322 | /*////////////////////////////////////////////////////////////// |
| 141 | 323 | FUZZ |
| 142 | 324 | //////////////////////////////////////////////////////////////*/ |
| 143 | 325 | |
| 144 | - function testFuzz_MintByAnyCaller(address caller, address to, uint256 amount) public { | |
| 145 | - vm.assume(to != address(0)); | |
| 326 | + function testFuzz_DepositByAnyCaller(address caller, uint256 wethAmount) public { | |
| 327 | + vm.assume(caller != address(0) && caller != address(love) && caller != address(weth)); | |
| 328 | + wethAmount = bound(wethAmount, 0, type(uint256).max / RATE); | |
| 146 | 329 | |
| 147 | - vm.prank(caller); | |
| 148 | - love.mint(to, amount); | |
| 330 | + _fund(caller, wethAmount); | |
| 331 | + _deposit(caller, wethAmount); | |
| 149 | 332 | |
| 150 | - assertEq(love.balanceOf(to), amount); | |
| 151 | - assertEq(love.totalSupply(), amount); | |
| 333 | + assertEq(love.balanceOf(caller), wethAmount * RATE); | |
| 334 | + assertEq(love.totalSupply(), wethAmount * RATE); | |
| 335 | + assertEq(weth.balanceOf(address(love)), wethAmount); | |
| 152 | 336 | } |
| 153 | 337 | |
| 154 | - function testFuzz_Transfer(uint256 minted, uint256 amount) public { | |
| 155 | - amount = bound(amount, 0, minted); | |
| 338 | + function testFuzz_RoundTrip(uint256 wethAmount) public { | |
| 339 | + wethAmount = bound(wethAmount, 0, type(uint256).max / RATE); | |
| 156 | 340 | |
| 157 | - love.mint(alice, minted); | |
| 341 | + _fund(alice, wethAmount); | |
| 342 | + uint256 before = weth.balanceOf(alice); | |
| 158 | 343 | |
| 344 | + _deposit(alice, wethAmount); | |
| 159 | 345 | vm.prank(alice); |
| 160 | - assertTrue(love.transfer(bob, amount)); | |
| 346 | + love.withdraw(wethAmount * RATE); | |
| 347 | + | |
| 348 | + assertEq(weth.balanceOf(alice), before); | |
| 349 | + assertEq(love.totalSupply(), 0); | |
| 350 | + } | |
| 351 | + | |
| 352 | + function testFuzz_WithdrawRevertsUnlessDivisible(uint256 loveAmount) public { | |
| 353 | + _deposit(alice, 10 ether); | |
| 161 | 354 | |
| 162 | - assertEq(love.balanceOf(alice), minted - amount); | |
| 163 | - assertEq(love.balanceOf(bob), amount); | |
| 355 | + vm.prank(alice); | |
| 356 | + if (loveAmount % RATE == 0 && loveAmount <= 10 ether * RATE) { | |
| 357 | + love.withdraw(loveAmount); | |
| 358 | + assertEq(weth.balanceOf(address(love)), 10 ether - loveAmount / RATE); | |
| 359 | + } else { | |
| 360 | + vm.expectRevert(); | |
| 361 | + love.withdraw(loveAmount); | |
| 362 | + } | |
| 363 | + } | |
| 364 | + | |
| 365 | + function _fund(address account, uint256 amount) internal { | |
| 366 | + vm.deal(account, amount); | |
| 367 | + vm.prank(account); | |
| 368 | + weth.deposit{value: amount}(); | |
| 369 | + } | |
| 370 | + | |
| 371 | + function _deposit(address account, uint256 wethAmount) internal { | |
| 372 | + vm.startPrank(account); | |
| 373 | + weth.approve(address(love), wethAmount); | |
| 374 | + love.deposit(wethAmount); | |
| 375 | + vm.stopPrank(); | |
| 164 | 376 | } |
| 165 | 377 | } |
| @@ -2,19 +2,27 @@ | |||
| 2 | pragma solidity ^0.8.30; | 2 | pragma solidity ^0.8.30; |
| 3 | 3 | ||
| 4 | import {Love} from "../src/Love.sol"; | 4 | import {Love} from "../src/Love.sol"; |
| 5 | +import {MockWETH} from "./mocks/MockWETH.sol"; | ||
| 5 | import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; | 6 | import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; |
| 6 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | 7 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; |
| 7 | import {Test} from "forge-std/Test.sol"; | 8 | import {Test} from "forge-std/Test.sol"; |
| 8 | 9 | ||
| 9 | contract LoveTest is Test { | 10 | contract LoveTest is Test { |
| 10 | Love public love; | 11 | Love public love; |
| 12 | + MockWETH public weth; | ||
| 11 | 13 | ||
| 12 | address alice = makeAddr("alice"); | 14 | address alice = makeAddr("alice"); |
| 13 | address bob = makeAddr("bob"); | 15 | address bob = makeAddr("bob"); |
| 14 | address stranger = makeAddr("stranger"); | 16 | address stranger = makeAddr("stranger"); |
| 15 | 17 | ||
| 18 | + uint256 constant RATE = 100_000; | ||
| 19 | + | ||
| 16 | function setUp() public { | 20 | function setUp() public { |
| 17 | - love = new Love(); | 21 | + weth = new MockWETH(); |
| 22 | + love = new Love(IERC20(address(weth))); | ||
| 23 | + | ||
| 24 | + _fund(alice, 10 ether); | ||
| 25 | + _fund(bob, 10 ether); | ||
| 18 | } | 26 | } |
| 19 | 27 | ||
| 20 | /*////////////////////////////////////////////////////////////// | 28 | /*////////////////////////////////////////////////////////////// |
| @@ -31,56 +39,191 @@ contract LoveTest is Test { | |||
| 31 | assertEq(love.totalSupply(), 0); | 39 | assertEq(love.totalSupply(), 0); |
| 32 | } | 40 | } |
| 33 | 41 | ||
| 42 | + function test_PegParameters() public view { | ||
| 43 | + assertEq(love.RATE(), RATE); | ||
| 44 | + assertEq(address(love.WETH()), address(weth)); | ||
| 45 | + } | ||
| 46 | + | ||
| 47 | + /// @dev No mint entrypoint may survive: supply moves only through the peg. | ||
| 48 | + function test_NoMintFunction() public view { | ||
| 49 | + assertEq(address(love).code.length > 0, true); | ||
| 50 | + (bool ok,) = address(love).staticcall(abi.encodeWithSignature("mint(address,uint256)", alice, 1e18)); | ||
| 51 | + assertFalse(ok); | ||
| 52 | + } | ||
| 53 | + | ||
| 34 | /*////////////////////////////////////////////////////////////// | 54 | /*////////////////////////////////////////////////////////////// |
| 35 | - MINT | 55 | + DEPOSIT |
| 36 | //////////////////////////////////////////////////////////////*/ | 56 | //////////////////////////////////////////////////////////////*/ |
| 37 | 57 | ||
| 38 | - function test_Mint() public { | 58 | + function test_Deposit() public { |
| 39 | - vm.prank(alice); | 59 | + _deposit(alice, 1 ether); |
| 40 | - love.mint(alice, 100e18); | ||
| 41 | 60 | ||
| 42 | - assertEq(love.balanceOf(alice), 100e18); | 61 | + assertEq(love.balanceOf(alice), 1 ether * RATE); |
| 43 | - assertEq(love.totalSupply(), 100e18); | 62 | + assertEq(love.totalSupply(), 1 ether * RATE); |
| 63 | + assertEq(weth.balanceOf(address(love)), 1 ether); | ||
| 64 | + assertEq(weth.balanceOf(alice), 9 ether); | ||
| 44 | } | 65 | } |
| 45 | 66 | ||
| 46 | - /// @dev The point of this contract: `mint` has no access control. | 67 | + function test_DepositIsPermissionless() public { |
| 47 | - function test_AnyoneCanMintToAnyone() public { | 68 | + _fund(stranger, 1 ether); |
| 48 | - vm.prank(stranger); | 69 | + _deposit(stranger, 1 ether); |
| 49 | - love.mint(bob, 1000e18); | 70 | + |
| 71 | + assertEq(love.balanceOf(stranger), 1 ether * RATE); | ||
| 72 | + } | ||
| 73 | + | ||
| 74 | + function test_DepositEmitsDeposit() public { | ||
| 75 | + vm.prank(alice); | ||
| 76 | + weth.approve(address(love), 2 ether); | ||
| 77 | + | ||
| 78 | + vm.expectEmit(true, false, false, true); | ||
| 79 | + emit Love.Deposit(alice, 2 ether, 2 ether * RATE); | ||
| 50 | 80 | ||
| 51 | - assertEq(love.balanceOf(bob), 1000e18); | 81 | + vm.prank(alice); |
| 52 | - assertEq(love.balanceOf(stranger), 0); | 82 | + love.deposit(2 ether); |
| 53 | - assertEq(love.totalSupply(), 1000e18); | ||
| 54 | } | 83 | } |
| 55 | 84 | ||
| 56 | - function test_MintEmitsTransferFromZero() public { | 85 | + function test_DepositEmitsTransferFromZero() public { |
| 86 | + vm.prank(alice); | ||
| 87 | + weth.approve(address(love), 1 ether); | ||
| 88 | + | ||
| 57 | vm.expectEmit(true, true, false, true); | 89 | vm.expectEmit(true, true, false, true); |
| 58 | - emit IERC20.Transfer(address(0), alice, 5e18); | 90 | + emit IERC20.Transfer(address(0), alice, 1 ether * RATE); |
| 59 | 91 | ||
| 60 | vm.prank(alice); | 92 | vm.prank(alice); |
| 61 | - love.mint(alice, 5e18); | 93 | + love.deposit(1 ether); |
| 62 | } | 94 | } |
| 63 | 95 | ||
| 64 | - function test_MintAccumulates() public { | 96 | + function test_DepositAccumulates() public { |
| 65 | - vm.startPrank(alice); | 97 | + _deposit(alice, 1 ether); |
| 66 | - love.mint(alice, 1e18); | 98 | + _deposit(alice, 3 ether); |
| 67 | - love.mint(alice, 2e18); | ||
| 68 | - vm.stopPrank(); | ||
| 69 | 99 | ||
| 70 | - assertEq(love.balanceOf(alice), 3e18); | 100 | + assertEq(love.balanceOf(alice), 4 ether * RATE); |
| 71 | - assertEq(love.totalSupply(), 3e18); | 101 | + assertEq(weth.balanceOf(address(love)), 4 ether); |
| 72 | } | 102 | } |
| 73 | 103 | ||
| 74 | - function test_RevertWhen_MintingToZeroAddress() public { | 104 | + function test_DepositZero() public { |
| 75 | - vm.expectRevert(abi.encodeWithSelector(IERC20Errors.ERC20InvalidReceiver.selector, address(0))); | 105 | + _deposit(alice, 0); |
| 76 | - love.mint(address(0), 1e18); | 106 | + |
| 107 | + assertEq(love.balanceOf(alice), 0); | ||
| 108 | + assertEq(love.totalSupply(), 0); | ||
| 77 | } | 109 | } |
| 78 | 110 | ||
| 79 | - function test_RevertWhen_MintOverflowsTotalSupply() public { | 111 | + function test_RevertWhen_DepositWithoutApproval() public { |
| 80 | - love.mint(alice, type(uint256).max); | 112 | + vm.prank(alice); |
| 113 | + vm.expectRevert( | ||
| 114 | + abi.encodeWithSelector(IERC20Errors.ERC20InsufficientAllowance.selector, address(love), 0, 1 ether) | ||
| 115 | + ); | ||
| 116 | + love.deposit(1 ether); | ||
| 117 | + } | ||
| 118 | + | ||
| 119 | + function test_RevertWhen_DepositExceedsWethBalance() public { | ||
| 120 | + vm.prank(alice); | ||
| 121 | + weth.approve(address(love), 100 ether); | ||
| 122 | + | ||
| 123 | + vm.prank(alice); | ||
| 124 | + vm.expectRevert( | ||
| 125 | + abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, alice, 10 ether, 100 ether) | ||
| 126 | + ); | ||
| 127 | + love.deposit(100 ether); | ||
| 128 | + } | ||
| 81 | 129 | ||
| 130 | + function test_RevertWhen_DepositOverflows() public { | ||
| 131 | + vm.prank(alice); | ||
| 82 | vm.expectRevert(); | 132 | vm.expectRevert(); |
| 83 | - love.mint(bob, 1); | 133 | + love.deposit(type(uint256).max); |
| 134 | + } | ||
| 135 | + | ||
| 136 | + /*////////////////////////////////////////////////////////////// | ||
| 137 | + WITHDRAW | ||
| 138 | + //////////////////////////////////////////////////////////////*/ | ||
| 139 | + | ||
| 140 | + function test_Withdraw() public { | ||
| 141 | + _deposit(alice, 1 ether); | ||
| 142 | + | ||
| 143 | + vm.prank(alice); | ||
| 144 | + love.withdraw(1 ether * RATE); | ||
| 145 | + | ||
| 146 | + assertEq(love.balanceOf(alice), 0); | ||
| 147 | + assertEq(love.totalSupply(), 0); | ||
| 148 | + assertEq(weth.balanceOf(alice), 10 ether); | ||
| 149 | + assertEq(weth.balanceOf(address(love)), 0); | ||
| 150 | + } | ||
| 151 | + | ||
| 152 | + function test_PartialWithdraw() public { | ||
| 153 | + _deposit(alice, 1 ether); | ||
| 154 | + | ||
| 155 | + vm.prank(alice); | ||
| 156 | + love.withdraw(0.25 ether * RATE); | ||
| 157 | + | ||
| 158 | + assertEq(love.balanceOf(alice), 0.75 ether * RATE); | ||
| 159 | + assertEq(weth.balanceOf(alice), 9.25 ether); | ||
| 160 | + assertEq(weth.balanceOf(address(love)), 0.75 ether); | ||
| 161 | + } | ||
| 162 | + | ||
| 163 | + function test_WithdrawEmitsWithdraw() public { | ||
| 164 | + _deposit(alice, 1 ether); | ||
| 165 | + | ||
| 166 | + vm.expectEmit(true, false, false, true); | ||
| 167 | + emit Love.Withdraw(alice, 1 ether * RATE, 1 ether); | ||
| 168 | + | ||
| 169 | + vm.prank(alice); | ||
| 170 | + love.withdraw(1 ether * RATE); | ||
| 171 | + } | ||
| 172 | + | ||
| 173 | + /// @dev LOVE received from someone else is redeemable all the same. | ||
| 174 | + function test_WithdrawAfterTransfer() public { | ||
| 175 | + _deposit(alice, 1 ether); | ||
| 176 | + | ||
| 177 | + vm.prank(alice); | ||
| 178 | + // forge-lint: disable-next-line(erc20-unchecked-transfer) | ||
| 179 | + love.transfer(bob, 1 ether * RATE); | ||
| 180 | + | ||
| 181 | + vm.prank(bob); | ||
| 182 | + love.withdraw(1 ether * RATE); | ||
| 183 | + | ||
| 184 | + assertEq(weth.balanceOf(bob), 11 ether); | ||
| 185 | + assertEq(love.totalSupply(), 0); | ||
| 186 | + } | ||
| 187 | + | ||
| 188 | + function test_WithdrawZero() public { | ||
| 189 | + _deposit(alice, 1 ether); | ||
| 190 | + | ||
| 191 | + vm.prank(alice); | ||
| 192 | + love.withdraw(0); | ||
| 193 | + | ||
| 194 | + assertEq(love.balanceOf(alice), 1 ether * RATE); | ||
| 195 | + } | ||
| 196 | + | ||
| 197 | + /// @dev Anything not a multiple of the rate would round wETH out of the peg. | ||
| 198 | + function test_RevertWhen_WithdrawNotDivisibleByRate() public { | ||
| 199 | + _deposit(alice, 1 ether); | ||
| 200 | + | ||
| 201 | + vm.prank(alice); | ||
| 202 | + vm.expectRevert(abi.encodeWithSelector(Love.AmountNotDivisibleByRate.selector, RATE + 1, RATE)); | ||
| 203 | + love.withdraw(RATE + 1); | ||
| 204 | + } | ||
| 205 | + | ||
| 206 | + function test_RevertWhen_WithdrawExceedsBalance() public { | ||
| 207 | + _deposit(alice, 1 ether); | ||
| 208 | + | ||
| 209 | + vm.prank(alice); | ||
| 210 | + vm.expectRevert( | ||
| 211 | + abi.encodeWithSelector( | ||
| 212 | + IERC20Errors.ERC20InsufficientBalance.selector, alice, 1 ether * RATE, 2 ether * RATE | ||
| 213 | + ) | ||
| 214 | + ); | ||
| 215 | + love.withdraw(2 ether * RATE); | ||
| 216 | + } | ||
| 217 | + | ||
| 218 | + /// @dev One holder cannot redeem against another holder's collateral. | ||
| 219 | + function test_RevertWhen_WithdrawWithoutDepositing() public { | ||
| 220 | + _deposit(alice, 1 ether); | ||
| 221 | + | ||
| 222 | + vm.prank(stranger); | ||
| 223 | + vm.expectRevert( | ||
| 224 | + abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, stranger, 0, 1 ether * RATE) | ||
| 225 | + ); | ||
| 226 | + love.withdraw(1 ether * RATE); | ||
| 84 | } | 227 | } |
| 85 | 228 | ||
| 86 | /*////////////////////////////////////////////////////////////// | 229 | /*////////////////////////////////////////////////////////////// |
| @@ -88,20 +231,18 @@ contract LoveTest is Test { | |||
| 88 | //////////////////////////////////////////////////////////////*/ | 231 | //////////////////////////////////////////////////////////////*/ |
| 89 | 232 | ||
| 90 | function test_Transfer() public { | 233 | function test_Transfer() public { |
| 91 | - love.mint(alice, 10e18); | 234 | + _deposit(alice, 1 ether); |
| 92 | 235 | ||
| 93 | vm.prank(alice); | 236 | vm.prank(alice); |
| 94 | assertTrue(love.transfer(bob, 4e18)); | 237 | assertTrue(love.transfer(bob, 4e18)); |
| 95 | 238 | ||
| 96 | - assertEq(love.balanceOf(alice), 6e18); | 239 | + assertEq(love.balanceOf(alice), 1 ether * RATE - 4e18); |
| 97 | assertEq(love.balanceOf(bob), 4e18); | 240 | assertEq(love.balanceOf(bob), 4e18); |
| 98 | } | 241 | } |
| 99 | 242 | ||
| 100 | function test_RevertWhen_TransferExceedsBalance() public { | 243 | function test_RevertWhen_TransferExceedsBalance() public { |
| 101 | - love.mint(alice, 1e18); | ||
| 102 | - | ||
| 103 | vm.prank(alice); | 244 | vm.prank(alice); |
| 104 | - vm.expectRevert(abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, alice, 1e18, 2e18)); | 245 | + vm.expectRevert(abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, alice, 0, 2e18)); |
| 105 | // forge-lint: disable-next-line(erc20-unchecked-transfer) | 246 | // forge-lint: disable-next-line(erc20-unchecked-transfer) |
| 106 | love.transfer(bob, 2e18); | 247 | love.transfer(bob, 2e18); |
| 107 | } | 248 | } |
| @@ -111,7 +252,7 @@ contract LoveTest is Test { | |||
| 111 | //////////////////////////////////////////////////////////////*/ | 252 | //////////////////////////////////////////////////////////////*/ |
| 112 | 253 | ||
| 113 | function test_ApproveAndTransferFrom() public { | 254 | function test_ApproveAndTransferFrom() public { |
| 114 | - love.mint(alice, 10e18); | 255 | + _deposit(alice, 1 ether); |
| 115 | 256 | ||
| 116 | vm.prank(alice); | 257 | vm.prank(alice); |
| 117 | assertTrue(love.approve(bob, 6e18)); | 258 | assertTrue(love.approve(bob, 6e18)); |
| @@ -120,13 +261,13 @@ contract LoveTest is Test { | |||
| 120 | vm.prank(bob); | 261 | vm.prank(bob); |
| 121 | assertTrue(love.transferFrom(alice, stranger, 6e18)); | 262 | assertTrue(love.transferFrom(alice, stranger, 6e18)); |
| 122 | 263 | ||
| 123 | - assertEq(love.balanceOf(alice), 4e18); | 264 | + assertEq(love.balanceOf(alice), 1 ether * RATE - 6e18); |
| 124 | assertEq(love.balanceOf(stranger), 6e18); | 265 | assertEq(love.balanceOf(stranger), 6e18); |
| 125 | assertEq(love.allowance(alice, bob), 0); | 266 | assertEq(love.allowance(alice, bob), 0); |
| 126 | } | 267 | } |
| 127 | 268 | ||
| 128 | function test_RevertWhen_TransferFromExceedsAllowance() public { | 269 | function test_RevertWhen_TransferFromExceedsAllowance() public { |
| 129 | - love.mint(alice, 10e18); | 270 | + _deposit(alice, 1 ether); |
| 130 | 271 | ||
| 131 | vm.prank(alice); | 272 | vm.prank(alice); |
| 132 | love.approve(bob, 1e18); | 273 | love.approve(bob, 1e18); |
| @@ -137,29 +278,100 @@ contract LoveTest is Test { | |||
| 137 | love.transferFrom(alice, stranger, 2e18); | 278 | love.transferFrom(alice, stranger, 2e18); |
| 138 | } | 279 | } |
| 139 | 280 | ||
| 281 | + /*////////////////////////////////////////////////////////////// | ||
| 282 | + PEG | ||
| 283 | + //////////////////////////////////////////////////////////////*/ | ||
| 284 | + | ||
| 285 | + function test_RoundTripIsLossless() public { | ||
| 286 | + uint256 before = weth.balanceOf(alice); | ||
| 287 | + | ||
| 288 | + _deposit(alice, 3 ether); | ||
| 289 | + vm.prank(alice); | ||
| 290 | + love.withdraw(3 ether * RATE); | ||
| 291 | + | ||
| 292 | + assertEq(weth.balanceOf(alice), before); | ||
| 293 | + assertEq(love.balanceOf(alice), 0); | ||
| 294 | + assertEq(love.totalSupply(), 0); | ||
| 295 | + } | ||
| 296 | + | ||
| 297 | + function test_SupplyStaysFullyBacked() public { | ||
| 298 | + _deposit(alice, 1 ether); | ||
| 299 | + _deposit(bob, 2 ether); | ||
| 300 | + | ||
| 301 | + assertEq(love.totalSupply(), weth.balanceOf(address(love)) * RATE); | ||
| 302 | + | ||
| 303 | + vm.prank(alice); | ||
| 304 | + love.withdraw(0.5 ether * RATE); | ||
| 305 | + | ||
| 306 | + assertEq(love.totalSupply(), weth.balanceOf(address(love)) * RATE); | ||
| 307 | + } | ||
| 308 | + | ||
| 309 | + /// @dev wETH sent straight to the contract is not claimable — it only lifts | ||
| 310 | + /// the backing, it never mints LOVE. | ||
| 311 | + function test_DonatedWethDoesNotMint() public { | ||
| 312 | + _deposit(alice, 1 ether); | ||
| 313 | + | ||
| 314 | + vm.prank(bob); | ||
| 315 | + // forge-lint: disable-next-line(erc20-unchecked-transfer) | ||
| 316 | + weth.transfer(address(love), 5 ether); | ||
| 317 | + | ||
| 318 | + assertEq(love.totalSupply(), 1 ether * RATE); | ||
| 319 | + assertEq(weth.balanceOf(address(love)), 6 ether); | ||
| 320 | + } | ||
| 321 | + | ||
| 140 | /*////////////////////////////////////////////////////////////// | 322 | /*////////////////////////////////////////////////////////////// |
| 141 | FUZZ | 323 | FUZZ |
| 142 | //////////////////////////////////////////////////////////////*/ | 324 | //////////////////////////////////////////////////////////////*/ |
| 143 | 325 | ||
| 144 | - function testFuzz_MintByAnyCaller(address caller, address to, uint256 amount) public { | 326 | + function testFuzz_DepositByAnyCaller(address caller, uint256 wethAmount) public { |
| 145 | - vm.assume(to != address(0)); | 327 | + vm.assume(caller != address(0) && caller != address(love) && caller != address(weth)); |
| 328 | + wethAmount = bound(wethAmount, 0, type(uint256).max / RATE); | ||
| 146 | 329 | ||
| 147 | - vm.prank(caller); | 330 | + _fund(caller, wethAmount); |
| 148 | - love.mint(to, amount); | 331 | + _deposit(caller, wethAmount); |
| 149 | 332 | ||
| 150 | - assertEq(love.balanceOf(to), amount); | 333 | + assertEq(love.balanceOf(caller), wethAmount * RATE); |
| 151 | - assertEq(love.totalSupply(), amount); | 334 | + assertEq(love.totalSupply(), wethAmount * RATE); |
| 335 | + assertEq(weth.balanceOf(address(love)), wethAmount); | ||
| 152 | } | 336 | } |
| 153 | 337 | ||
| 154 | - function testFuzz_Transfer(uint256 minted, uint256 amount) public { | 338 | + function testFuzz_RoundTrip(uint256 wethAmount) public { |
| 155 | - amount = bound(amount, 0, minted); | 339 | + wethAmount = bound(wethAmount, 0, type(uint256).max / RATE); |
| 156 | 340 | ||
| 157 | - love.mint(alice, minted); | 341 | + _fund(alice, wethAmount); |
| 342 | + uint256 before = weth.balanceOf(alice); | ||
| 158 | 343 | ||
| 344 | + _deposit(alice, wethAmount); | ||
| 159 | vm.prank(alice); | 345 | vm.prank(alice); |
| 160 | - assertTrue(love.transfer(bob, amount)); | 346 | + love.withdraw(wethAmount * RATE); |
| 347 | + | ||
| 348 | + assertEq(weth.balanceOf(alice), before); | ||
| 349 | + assertEq(love.totalSupply(), 0); | ||
| 350 | + } | ||
| 351 | + | ||
| 352 | + function testFuzz_WithdrawRevertsUnlessDivisible(uint256 loveAmount) public { | ||
| 353 | + _deposit(alice, 10 ether); | ||
| 161 | 354 | ||
| 162 | - assertEq(love.balanceOf(alice), minted - amount); | 355 | + vm.prank(alice); |
| 163 | - assertEq(love.balanceOf(bob), amount); | 356 | + if (loveAmount % RATE == 0 && loveAmount <= 10 ether * RATE) { |
| 357 | + love.withdraw(loveAmount); | ||
| 358 | + assertEq(weth.balanceOf(address(love)), 10 ether - loveAmount / RATE); | ||
| 359 | + } else { | ||
| 360 | + vm.expectRevert(); | ||
| 361 | + love.withdraw(loveAmount); | ||
| 362 | + } | ||
| 363 | + } | ||
| 364 | + | ||
| 365 | + function _fund(address account, uint256 amount) internal { | ||
| 366 | + vm.deal(account, amount); | ||
| 367 | + vm.prank(account); | ||
| 368 | + weth.deposit{value: amount}(); | ||
| 369 | + } | ||
| 370 | + | ||
| 371 | + function _deposit(address account, uint256 wethAmount) internal { | ||
| 372 | + vm.startPrank(account); | ||
| 373 | + weth.approve(address(love), wethAmount); | ||
| 374 | + love.deposit(wethAmount); | ||
| 375 | + vm.stopPrank(); | ||
| 164 | } | 376 | } |
| 165 | } | 377 | } |
modified
test/LoveCreate2.t.sol +30 -17 | @@ -5,11 +5,13 @@ 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 | -/// @notice The token is meant to live at the same address on every EVM network, | |
| 9 | -/// so these tests pin the CREATE2 deployment path and the inputs the | |
| 10 | -/// address derives from. | |
| 8 | +/// @notice The deployment address derives from the salt, the creation code and | |
| 9 | +/// 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 | +/// wETH gives a different one. | |
| 11 | 12 | contract LoveCreate2Test is Test { |
| 12 | 13 | bytes32 constant SALT = keccak256("LOVE"); |
| 14 | + address constant WETH = 0x4200000000000000000000000000000000000006; | |
| 13 | 15 | |
| 14 | 16 | LoveScript script; |
| 15 | 17 | |
| @@ -21,53 +23,64 @@ contract LoveCreate2Test is Test { | ||
| 21 | 23 | } |
| 22 | 24 | |
| 23 | 25 | function test_DeploysAtPredictedAddress() public { |
| 24 | - address predicted = vm.computeCreate2Address(SALT, keccak256(type(Love).creationCode), CREATE2_FACTORY); | |
| 26 | + address predicted = vm.computeCreate2Address(SALT, keccak256(script.initCode(WETH)), CREATE2_FACTORY); | |
| 25 | 27 | |
| 26 | - Love deployed = Love(_deploy(SALT)); | |
| 28 | + Love deployed = Love(_deploy(SALT, WETH)); | |
| 27 | 29 | |
| 28 | 30 | assertEq(address(deployed), predicted); |
| 29 | 31 | assertEq(deployed.symbol(), "LOVE"); |
| 32 | + assertEq(address(deployed.WETH()), WETH); | |
| 30 | 33 | } |
| 31 | 34 | |
| 32 | 35 | /// @dev What the deploy script prints must be what the chain gives back. |
| 33 | 36 | function test_ScriptPredictionMatchesDeployment() public { |
| 34 | - assertEq(script.predict(SALT), _deploy(SALT)); | |
| 37 | + assertEq(script.predict(SALT, WETH), _deploy(SALT, WETH)); | |
| 35 | 38 | } |
| 36 | 39 | |
| 37 | 40 | function test_ScriptUsesKeccakOfLoveAsDefaultSalt() public view { |
| 38 | 41 | assertEq(script.DEFAULT_SALT(), SALT); |
| 39 | 42 | } |
| 40 | 43 | |
| 41 | - /// @dev Same salt, same code, same address — that is the whole point. | |
| 44 | + function test_ScriptDefaultsToOpStackWeth() public view { | |
| 45 | + assertEq(script.DEFAULT_WETH(), WETH); | |
| 46 | + } | |
| 47 | + | |
| 48 | + /// @dev Same salt, same code, same wETH, same address. | |
| 42 | 49 | function test_PredictionIsChainAgnostic() public { |
| 43 | - address onThisChain = script.predict(SALT); | |
| 50 | + address onThisChain = script.predict(SALT, WETH); | |
| 44 | 51 | |
| 45 | 52 | vm.chainId(137); |
| 46 | - assertEq(script.predict(SALT), onThisChain); | |
| 53 | + assertEq(script.predict(SALT, WETH), onThisChain); | |
| 47 | 54 | |
| 48 | 55 | vm.chainId(42_161); |
| 49 | - assertEq(script.predict(SALT), onThisChain); | |
| 56 | + assertEq(script.predict(SALT, WETH), onThisChain); | |
| 50 | 57 | } |
| 51 | 58 | |
| 52 | 59 | function test_DifferentSaltsGiveDifferentAddresses() public view { |
| 53 | - assertTrue(script.predict(SALT) != script.predict(keccak256("LOVE2"))); | |
| 60 | + assertTrue(script.predict(SALT, WETH) != script.predict(keccak256("LOVE2"), WETH)); | |
| 61 | + } | |
| 62 | + | |
| 63 | + /// @dev The wETH address is part of the creation code, so chains with their | |
| 64 | + /// own wETH get their own token address. | |
| 65 | + function test_DifferentWethGivesDifferentAddress() public view { | |
| 66 | + assertTrue(script.predict(SALT, WETH) != script.predict(SALT, address(0xBEEF))); | |
| 54 | 67 | } |
| 55 | 68 | |
| 56 | 69 | /// @dev Redeploying with the same salt must fail, not silently return the |
| 57 | 70 | /// existing token. |
| 58 | 71 | function test_RevertWhen_RedeployingWithSameSalt() public { |
| 59 | - _deploy(SALT); | |
| 72 | + _deploy(SALT, WETH); | |
| 60 | 73 | |
| 61 | - (bool ok,) = CREATE2_FACTORY.call(abi.encodePacked(SALT, type(Love).creationCode)); | |
| 74 | + (bool ok,) = CREATE2_FACTORY.call(abi.encodePacked(SALT, script.initCode(WETH))); | |
| 62 | 75 | assertFalse(ok); |
| 63 | 76 | } |
| 64 | 77 | |
| 65 | - function testFuzz_PredictionMatchesDeployment(bytes32 salt) public { | |
| 66 | - assertEq(script.predict(salt), _deploy(salt)); | |
| 78 | + function testFuzz_PredictionMatchesDeployment(bytes32 salt, address weth) public { | |
| 79 | + assertEq(script.predict(salt, weth), _deploy(salt, weth)); | |
| 67 | 80 | } |
| 68 | 81 | |
| 69 | - function _deploy(bytes32 salt) internal returns (address deployed) { | |
| 70 | - (bool ok, bytes memory ret) = CREATE2_FACTORY.call(abi.encodePacked(salt, type(Love).creationCode)); | |
| 82 | + 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))); | |
| 71 | 84 | require(ok, "create2 deployment failed"); |
| 72 | 85 | // casting to 'bytes20' is safe because the deployer returns the 20-byte address, raw |
| 73 | 86 | // forge-lint: disable-next-line(unsafe-typecast) |
| @@ -5,11 +5,13 @@ 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 | -/// @notice The token is meant to live at the same address on every EVM network, | 8 | +/// @notice The deployment address derives from the salt, the creation code and |
| 9 | -/// so these tests pin the CREATE2 deployment path and the inputs the | 9 | +/// the wETH constructor argument. These tests pin that derivation: same |
| 10 | -/// address derives from. | 10 | +/// salt and same wETH give the same address on any chain, a different |
| 11 | +/// wETH gives a different one. | ||
| 11 | contract LoveCreate2Test is Test { | 12 | contract LoveCreate2Test is Test { |
| 12 | bytes32 constant SALT = keccak256("LOVE"); | 13 | bytes32 constant SALT = keccak256("LOVE"); |
| 14 | + address constant WETH = 0x4200000000000000000000000000000000000006; | ||
| 13 | 15 | ||
| 14 | LoveScript script; | 16 | LoveScript script; |
| 15 | 17 | ||
| @@ -21,53 +23,64 @@ contract LoveCreate2Test is Test { | |||
| 21 | } | 23 | } |
| 22 | 24 | ||
| 23 | function test_DeploysAtPredictedAddress() public { | 25 | function test_DeploysAtPredictedAddress() public { |
| 24 | - address predicted = vm.computeCreate2Address(SALT, keccak256(type(Love).creationCode), CREATE2_FACTORY); | 26 | + address predicted = vm.computeCreate2Address(SALT, keccak256(script.initCode(WETH)), CREATE2_FACTORY); |
| 25 | 27 | ||
| 26 | - Love deployed = Love(_deploy(SALT)); | 28 | + Love deployed = Love(_deploy(SALT, WETH)); |
| 27 | 29 | ||
| 28 | assertEq(address(deployed), predicted); | 30 | assertEq(address(deployed), predicted); |
| 29 | assertEq(deployed.symbol(), "LOVE"); | 31 | assertEq(deployed.symbol(), "LOVE"); |
| 32 | + assertEq(address(deployed.WETH()), WETH); | ||
| 30 | } | 33 | } |
| 31 | 34 | ||
| 32 | /// @dev What the deploy script prints must be what the chain gives back. | 35 | /// @dev What the deploy script prints must be what the chain gives back. |
| 33 | function test_ScriptPredictionMatchesDeployment() public { | 36 | function test_ScriptPredictionMatchesDeployment() public { |
| 34 | - assertEq(script.predict(SALT), _deploy(SALT)); | 37 | + assertEq(script.predict(SALT, WETH), _deploy(SALT, WETH)); |
| 35 | } | 38 | } |
| 36 | 39 | ||
| 37 | function test_ScriptUsesKeccakOfLoveAsDefaultSalt() public view { | 40 | function test_ScriptUsesKeccakOfLoveAsDefaultSalt() public view { |
| 38 | assertEq(script.DEFAULT_SALT(), SALT); | 41 | assertEq(script.DEFAULT_SALT(), SALT); |
| 39 | } | 42 | } |
| 40 | 43 | ||
| 41 | - /// @dev Same salt, same code, same address — that is the whole point. | 44 | + function test_ScriptDefaultsToOpStackWeth() public view { |
| 45 | + assertEq(script.DEFAULT_WETH(), WETH); | ||
| 46 | + } | ||
| 47 | + | ||
| 48 | + /// @dev Same salt, same code, same wETH, same address. | ||
| 42 | function test_PredictionIsChainAgnostic() public { | 49 | function test_PredictionIsChainAgnostic() public { |
| 43 | - address onThisChain = script.predict(SALT); | 50 | + address onThisChain = script.predict(SALT, WETH); |
| 44 | 51 | ||
| 45 | vm.chainId(137); | 52 | vm.chainId(137); |
| 46 | - assertEq(script.predict(SALT), onThisChain); | 53 | + assertEq(script.predict(SALT, WETH), onThisChain); |
| 47 | 54 | ||
| 48 | vm.chainId(42_161); | 55 | vm.chainId(42_161); |
| 49 | - assertEq(script.predict(SALT), onThisChain); | 56 | + assertEq(script.predict(SALT, WETH), onThisChain); |
| 50 | } | 57 | } |
| 51 | 58 | ||
| 52 | function test_DifferentSaltsGiveDifferentAddresses() public view { | 59 | function test_DifferentSaltsGiveDifferentAddresses() public view { |
| 53 | - assertTrue(script.predict(SALT) != script.predict(keccak256("LOVE2"))); | 60 | + assertTrue(script.predict(SALT, WETH) != script.predict(keccak256("LOVE2"), WETH)); |
| 61 | + } | ||
| 62 | + | ||
| 63 | + /// @dev The wETH address is part of the creation code, so chains with their | ||
| 64 | + /// own wETH get their own token address. | ||
| 65 | + function test_DifferentWethGivesDifferentAddress() public view { | ||
| 66 | + assertTrue(script.predict(SALT, WETH) != script.predict(SALT, address(0xBEEF))); | ||
| 54 | } | 67 | } |
| 55 | 68 | ||
| 56 | /// @dev Redeploying with the same salt must fail, not silently return the | 69 | /// @dev Redeploying with the same salt must fail, not silently return the |
| 57 | /// existing token. | 70 | /// existing token. |
| 58 | function test_RevertWhen_RedeployingWithSameSalt() public { | 71 | function test_RevertWhen_RedeployingWithSameSalt() public { |
| 59 | - _deploy(SALT); | 72 | + _deploy(SALT, WETH); |
| 60 | 73 | ||
| 61 | - (bool ok,) = CREATE2_FACTORY.call(abi.encodePacked(SALT, type(Love).creationCode)); | 74 | + (bool ok,) = CREATE2_FACTORY.call(abi.encodePacked(SALT, script.initCode(WETH))); |
| 62 | assertFalse(ok); | 75 | assertFalse(ok); |
| 63 | } | 76 | } |
| 64 | 77 | ||
| 65 | - function testFuzz_PredictionMatchesDeployment(bytes32 salt) public { | 78 | + function testFuzz_PredictionMatchesDeployment(bytes32 salt, address weth) public { |
| 66 | - assertEq(script.predict(salt), _deploy(salt)); | 79 | + assertEq(script.predict(salt, weth), _deploy(salt, weth)); |
| 67 | } | 80 | } |
| 68 | 81 | ||
| 69 | - function _deploy(bytes32 salt) internal returns (address deployed) { | 82 | + function _deploy(bytes32 salt, address weth) internal returns (address deployed) { |
| 70 | - (bool ok, bytes memory ret) = CREATE2_FACTORY.call(abi.encodePacked(salt, type(Love).creationCode)); | 83 | + (bool ok, bytes memory ret) = CREATE2_FACTORY.call(abi.encodePacked(salt, script.initCode(weth))); |
| 71 | require(ok, "create2 deployment failed"); | 84 | require(ok, "create2 deployment failed"); |
| 72 | // casting to 'bytes20' is safe because the deployer returns the 20-byte address, raw | 85 | // casting to 'bytes20' is safe because the deployer returns the 20-byte address, raw |
| 73 | // forge-lint: disable-next-line(unsafe-typecast) | 86 | // forge-lint: disable-next-line(unsafe-typecast) |