| add love contract 9bf25a7 julienbrg 9h ago | 1 | // SPDX-License-Identifier: MIT |
| 2 | pragma solidity ^0.8.30; |
| 3 | |
| 4 | import {Love} from "../src/Love.sol"; |
| Merge 3-peg-love-to-weth into main ce13509 rickub 7h ago | 5 | import {MockWETH} from "./mocks/MockWETH.sol"; |
| make love an erc20 with public mint 4c8048e julienbrg 8h ago | 6 | import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; |
| 7 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; |
| add love contract 9bf25a7 julienbrg 9h ago | 8 | import {Test} from "forge-std/Test.sol"; |
| 9 | |
| Merge 3-peg-love-to-weth into main ce13509 rickub 7h ago | 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. |
| add love contract 9bf25a7 julienbrg 9h ago | 14 | contract LoveTest is Test { |
| Merge 3-peg-love-to-weth into main ce13509 rickub 7h ago | 15 | /// @notice The token under test. |
| add love contract 9bf25a7 julienbrg 9h ago | 16 | Love public love; |
| 17 | |
| Merge 3-peg-love-to-weth into main ce13509 rickub 7h ago | 18 | /// @notice The wETH it is pegged to. |
| 19 | MockWETH public weth; |
| 20 | |
| add love contract 9bf25a7 julienbrg 9h ago | 21 | address alice = makeAddr("alice"); |
| 22 | address bob = makeAddr("bob"); |
| make love an erc20 with public mint 4c8048e julienbrg 8h ago | 23 | address stranger = makeAddr("stranger"); |
| add love contract 9bf25a7 julienbrg 9h ago | 24 | |
| Merge 3-peg-love-to-weth into main ce13509 rickub 7h ago | 25 | uint256 constant RATE = 100_000; |
| 26 | |
| add love contract 9bf25a7 julienbrg 9h ago | 27 | function setUp() public { |
| Merge 3-peg-love-to-weth into main ce13509 rickub 7h ago | 28 | weth = new MockWETH(); |
| 29 | love = new Love(IERC20(address(weth))); |
| 30 | |
| 31 | _fund(alice, 10 ether); |
| 32 | _fund(bob, 10 ether); |
| add love contract 9bf25a7 julienbrg 9h ago | 33 | } |
| 34 | |
| make love an erc20 with public mint 4c8048e julienbrg 8h ago | 35 | /*////////////////////////////////////////////////////////////// |
| 36 | METADATA |
| 37 | //////////////////////////////////////////////////////////////*/ |
| 38 | |
| 39 | function test_Metadata() public view { |
| 40 | assertEq(love.name(), "Love"); |
| 41 | assertEq(love.symbol(), "LOVE"); |
| 42 | assertEq(love.decimals(), 18); |
| 43 | } |
| 44 | |
| 45 | function test_InitialSupplyIsZero() public view { |
| 46 | assertEq(love.totalSupply(), 0); |
| 47 | } |
| 48 | |
| Merge 3-peg-love-to-weth into main ce13509 rickub 7h ago | 49 | function test_PegParameters() public view { |
| 50 | assertEq(love.RATE(), RATE); |
| 51 | assertEq(address(love.WETH()), address(weth)); |
| 52 | } |
| 53 | |
| 54 | /// @dev No mint entrypoint may survive: supply moves only through the peg. |
| 55 | function test_NoMintFunction() public view { |
| 56 | assertEq(address(love).code.length > 0, true); |
| 57 | (bool ok,) = address(love).staticcall(abi.encodeWithSignature("mint(address,uint256)", alice, 1e18)); |
| 58 | assertFalse(ok); |
| 59 | } |
| 60 | |
| make love an erc20 with public mint 4c8048e julienbrg 8h ago | 61 | /*////////////////////////////////////////////////////////////// |
| Merge 3-peg-love-to-weth into main ce13509 rickub 7h ago | 62 | DEPOSIT |
| make love an erc20 with public mint 4c8048e julienbrg 8h ago | 63 | //////////////////////////////////////////////////////////////*/ |
| 64 | |
| Merge 3-peg-love-to-weth into main ce13509 rickub 7h ago | 65 | function test_Deposit() public { |
| 66 | _deposit(alice, 1 ether); |
| make love an erc20 with public mint 4c8048e julienbrg 8h ago | 67 | |
| Merge 3-peg-love-to-weth into main ce13509 rickub 7h ago | 68 | assertEq(love.balanceOf(alice), 1 ether * RATE); |
| 69 | assertEq(love.totalSupply(), 1 ether * RATE); |
| 70 | assertEq(weth.balanceOf(address(love)), 1 ether); |
| 71 | assertEq(weth.balanceOf(alice), 9 ether); |
| make love an erc20 with public mint 4c8048e julienbrg 8h ago | 72 | } |
| 73 | |
| Merge 3-peg-love-to-weth into main ce13509 rickub 7h ago | 74 | function test_DepositIsPermissionless() public { |
| 75 | _fund(stranger, 1 ether); |
| 76 | _deposit(stranger, 1 ether); |
| add love contract 9bf25a7 julienbrg 9h ago | 77 | |
| Merge 3-peg-love-to-weth into main ce13509 rickub 7h ago | 78 | assertEq(love.balanceOf(stranger), 1 ether * RATE); |
| add love contract 9bf25a7 julienbrg 9h ago | 79 | } |
| 80 | |
| Merge 3-peg-love-to-weth into main ce13509 rickub 7h ago | 81 | function test_DepositEmitsDeposit() public { |
| 82 | vm.prank(alice); |
| 83 | weth.approve(address(love), 2 ether); |
| 84 | |
| 85 | vm.expectEmit(true, false, false, true); |
| 86 | emit Love.Deposit(alice, 2 ether, 2 ether * RATE); |
| 87 | |
| 88 | vm.prank(alice); |
| 89 | love.deposit(2 ether); |
| 90 | } |
| 91 | |
| 92 | function test_DepositEmitsTransferFromZero() public { |
| 93 | vm.prank(alice); |
| 94 | weth.approve(address(love), 1 ether); |
| 95 | |
| make love an erc20 with public mint 4c8048e julienbrg 8h ago | 96 | vm.expectEmit(true, true, false, true); |
| Merge 3-peg-love-to-weth into main ce13509 rickub 7h ago | 97 | emit IERC20.Transfer(address(0), alice, 1 ether * RATE); |
| make love an erc20 with public mint 4c8048e julienbrg 8h ago | 98 | |
| 99 | vm.prank(alice); |
| Merge 3-peg-love-to-weth into main ce13509 rickub 7h ago | 100 | love.deposit(1 ether); |
| make love an erc20 with public mint 4c8048e julienbrg 8h ago | 101 | } |
| 102 | |
| Merge 3-peg-love-to-weth into main ce13509 rickub 7h ago | 103 | function test_DepositAccumulates() public { |
| 104 | _deposit(alice, 1 ether); |
| 105 | _deposit(alice, 3 ether); |
| add love contract 9bf25a7 julienbrg 9h ago | 106 | |
| Merge 3-peg-love-to-weth into main ce13509 rickub 7h ago | 107 | assertEq(love.balanceOf(alice), 4 ether * RATE); |
| 108 | assertEq(weth.balanceOf(address(love)), 4 ether); |
| add love contract 9bf25a7 julienbrg 9h ago | 109 | } |
| 110 | |
| Merge 3-peg-love-to-weth into main ce13509 rickub 7h ago | 111 | function test_DepositZero() public { |
| 112 | _deposit(alice, 0); |
| 113 | |
| 114 | assertEq(love.balanceOf(alice), 0); |
| 115 | assertEq(love.totalSupply(), 0); |
| make love an erc20 with public mint 4c8048e julienbrg 8h ago | 116 | } |
| 117 | |
| Merge 3-peg-love-to-weth into main ce13509 rickub 7h ago | 118 | function test_RevertWhen_DepositWithoutApproval() public { |
| 119 | vm.prank(alice); |
| 120 | vm.expectRevert( |
| 121 | abi.encodeWithSelector(IERC20Errors.ERC20InsufficientAllowance.selector, address(love), 0, 1 ether) |
| 122 | ); |
| 123 | love.deposit(1 ether); |
| 124 | } |
| 125 | |
| 126 | function test_RevertWhen_DepositExceedsWethBalance() public { |
| 127 | vm.prank(alice); |
| 128 | weth.approve(address(love), 100 ether); |
| 129 | |
| 130 | vm.prank(alice); |
| 131 | vm.expectRevert( |
| 132 | abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, alice, 10 ether, 100 ether) |
| 133 | ); |
| 134 | love.deposit(100 ether); |
| 135 | } |
| make love an erc20 with public mint 4c8048e julienbrg 8h ago | 136 | |
| Merge 3-peg-love-to-weth into main ce13509 rickub 7h ago | 137 | function test_RevertWhen_DepositOverflows() public { |
| 138 | vm.prank(alice); |
| make love an erc20 with public mint 4c8048e julienbrg 8h ago | 139 | vm.expectRevert(); |
| Merge 3-peg-love-to-weth into main ce13509 rickub 7h ago | 140 | love.deposit(type(uint256).max); |
| 141 | } |
| 142 | |
| 143 | /*////////////////////////////////////////////////////////////// |
| 144 | WITHDRAW |
| 145 | //////////////////////////////////////////////////////////////*/ |
| 146 | |
| 147 | function test_Withdraw() public { |
| 148 | _deposit(alice, 1 ether); |
| 149 | |
| 150 | vm.prank(alice); |
| 151 | love.withdraw(1 ether * RATE); |
| 152 | |
| 153 | assertEq(love.balanceOf(alice), 0); |
| 154 | assertEq(love.totalSupply(), 0); |
| 155 | assertEq(weth.balanceOf(alice), 10 ether); |
| 156 | assertEq(weth.balanceOf(address(love)), 0); |
| 157 | } |
| 158 | |
| 159 | function test_PartialWithdraw() public { |
| 160 | _deposit(alice, 1 ether); |
| 161 | |
| 162 | vm.prank(alice); |
| 163 | love.withdraw(0.25 ether * RATE); |
| 164 | |
| 165 | assertEq(love.balanceOf(alice), 0.75 ether * RATE); |
| 166 | assertEq(weth.balanceOf(alice), 9.25 ether); |
| 167 | assertEq(weth.balanceOf(address(love)), 0.75 ether); |
| 168 | } |
| 169 | |
| 170 | function test_WithdrawEmitsWithdraw() public { |
| 171 | _deposit(alice, 1 ether); |
| 172 | |
| 173 | vm.expectEmit(true, false, false, true); |
| 174 | emit Love.Withdraw(alice, 1 ether * RATE, 1 ether); |
| 175 | |
| 176 | vm.prank(alice); |
| 177 | love.withdraw(1 ether * RATE); |
| 178 | } |
| 179 | |
| 180 | /// @dev LOVE received from someone else is redeemable all the same. |
| 181 | function test_WithdrawAfterTransfer() public { |
| 182 | _deposit(alice, 1 ether); |
| 183 | |
| 184 | vm.prank(alice); |
| 185 | // forge-lint: disable-next-line(erc20-unchecked-transfer) |
| 186 | love.transfer(bob, 1 ether * RATE); |
| 187 | |
| 188 | vm.prank(bob); |
| 189 | love.withdraw(1 ether * RATE); |
| 190 | |
| 191 | assertEq(weth.balanceOf(bob), 11 ether); |
| 192 | assertEq(love.totalSupply(), 0); |
| 193 | } |
| 194 | |
| 195 | function test_WithdrawZero() public { |
| 196 | _deposit(alice, 1 ether); |
| 197 | |
| 198 | vm.prank(alice); |
| 199 | love.withdraw(0); |
| 200 | |
| 201 | assertEq(love.balanceOf(alice), 1 ether * RATE); |
| 202 | } |
| 203 | |
| 204 | /// @dev Anything not a multiple of the rate would round wETH out of the peg. |
| 205 | function test_RevertWhen_WithdrawNotDivisibleByRate() public { |
| 206 | _deposit(alice, 1 ether); |
| 207 | |
| 208 | vm.prank(alice); |
| 209 | vm.expectRevert(abi.encodeWithSelector(Love.AmountNotDivisibleByRate.selector, RATE + 1, RATE)); |
| 210 | love.withdraw(RATE + 1); |
| 211 | } |
| 212 | |
| 213 | function test_RevertWhen_WithdrawExceedsBalance() public { |
| 214 | _deposit(alice, 1 ether); |
| 215 | |
| 216 | vm.prank(alice); |
| 217 | vm.expectRevert( |
| 218 | abi.encodeWithSelector( |
| 219 | IERC20Errors.ERC20InsufficientBalance.selector, alice, 1 ether * RATE, 2 ether * RATE |
| 220 | ) |
| 221 | ); |
| 222 | love.withdraw(2 ether * RATE); |
| 223 | } |
| 224 | |
| 225 | /// @dev One holder cannot redeem against another holder's collateral. |
| 226 | function test_RevertWhen_WithdrawWithoutDepositing() public { |
| 227 | _deposit(alice, 1 ether); |
| 228 | |
| 229 | vm.prank(stranger); |
| 230 | vm.expectRevert( |
| 231 | abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, stranger, 0, 1 ether * RATE) |
| 232 | ); |
| 233 | love.withdraw(1 ether * RATE); |
| make love an erc20 with public mint 4c8048e julienbrg 8h ago | 234 | } |
| 235 | |
| 236 | /*////////////////////////////////////////////////////////////// |
| 237 | TRANSFER |
| 238 | //////////////////////////////////////////////////////////////*/ |
| 239 | |
| 240 | function test_Transfer() public { |
| Merge 3-peg-love-to-weth into main ce13509 rickub 7h ago | 241 | _deposit(alice, 1 ether); |
| make love an erc20 with public mint 4c8048e julienbrg 8h ago | 242 | |
| 243 | vm.prank(alice); |
| 244 | assertTrue(love.transfer(bob, 4e18)); |
| 245 | |
| Merge 3-peg-love-to-weth into main ce13509 rickub 7h ago | 246 | assertEq(love.balanceOf(alice), 1 ether * RATE - 4e18); |
| make love an erc20 with public mint 4c8048e julienbrg 8h ago | 247 | assertEq(love.balanceOf(bob), 4e18); |
| 248 | } |
| 249 | |
| 250 | function test_RevertWhen_TransferExceedsBalance() public { |
| add love contract 9bf25a7 julienbrg 9h ago | 251 | vm.prank(alice); |
| Merge 3-peg-love-to-weth into main ce13509 rickub 7h ago | 252 | vm.expectRevert(abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, alice, 0, 2e18)); |
| make love an erc20 with public mint 4c8048e julienbrg 8h ago | 253 | // forge-lint: disable-next-line(erc20-unchecked-transfer) |
| 254 | love.transfer(bob, 2e18); |
| add love contract 9bf25a7 julienbrg 9h ago | 255 | } |
| 256 | |
| make love an erc20 with public mint 4c8048e julienbrg 8h ago | 257 | /*////////////////////////////////////////////////////////////// |
| 258 | APPROVE / TRANSFERFROM |
| 259 | //////////////////////////////////////////////////////////////*/ |
| 260 | |
| 261 | function test_ApproveAndTransferFrom() public { |
| Merge 3-peg-love-to-weth into main ce13509 rickub 7h ago | 262 | _deposit(alice, 1 ether); |
| make love an erc20 with public mint 4c8048e julienbrg 8h ago | 263 | |
| add love contract 9bf25a7 julienbrg 9h ago | 264 | vm.prank(alice); |
| make love an erc20 with public mint 4c8048e julienbrg 8h ago | 265 | assertTrue(love.approve(bob, 6e18)); |
| 266 | assertEq(love.allowance(alice, bob), 6e18); |
| 267 | |
| 268 | vm.prank(bob); |
| 269 | assertTrue(love.transferFrom(alice, stranger, 6e18)); |
| 270 | |
| Merge 3-peg-love-to-weth into main ce13509 rickub 7h ago | 271 | assertEq(love.balanceOf(alice), 1 ether * RATE - 6e18); |
| make love an erc20 with public mint 4c8048e julienbrg 8h ago | 272 | assertEq(love.balanceOf(stranger), 6e18); |
| 273 | assertEq(love.allowance(alice, bob), 0); |
| add love contract 9bf25a7 julienbrg 9h ago | 274 | } |
| 275 | |
| make love an erc20 with public mint 4c8048e julienbrg 8h ago | 276 | function test_RevertWhen_TransferFromExceedsAllowance() public { |
| Merge 3-peg-love-to-weth into main ce13509 rickub 7h ago | 277 | _deposit(alice, 1 ether); |
| make love an erc20 with public mint 4c8048e julienbrg 8h ago | 278 | |
| add love contract 9bf25a7 julienbrg 9h ago | 279 | vm.prank(alice); |
| make love an erc20 with public mint 4c8048e julienbrg 8h ago | 280 | love.approve(bob, 1e18); |
| 281 | |
| 282 | vm.prank(bob); |
| 283 | vm.expectRevert(abi.encodeWithSelector(IERC20Errors.ERC20InsufficientAllowance.selector, bob, 1e18, 2e18)); |
| 284 | // forge-lint: disable-next-line(erc20-unchecked-transfer) |
| 285 | love.transferFrom(alice, stranger, 2e18); |
| 286 | } |
| 287 | |
| Merge 3-peg-love-to-weth into main ce13509 rickub 7h ago | 288 | /*////////////////////////////////////////////////////////////// |
| 289 | PEG |
| 290 | //////////////////////////////////////////////////////////////*/ |
| 291 | |
| 292 | function test_RoundTripIsLossless() public { |
| 293 | uint256 before = weth.balanceOf(alice); |
| 294 | |
| 295 | _deposit(alice, 3 ether); |
| 296 | vm.prank(alice); |
| 297 | love.withdraw(3 ether * RATE); |
| 298 | |
| 299 | assertEq(weth.balanceOf(alice), before); |
| 300 | assertEq(love.balanceOf(alice), 0); |
| 301 | assertEq(love.totalSupply(), 0); |
| 302 | } |
| 303 | |
| 304 | function test_SupplyStaysFullyBacked() public { |
| 305 | _deposit(alice, 1 ether); |
| 306 | _deposit(bob, 2 ether); |
| 307 | |
| 308 | assertEq(love.totalSupply(), weth.balanceOf(address(love)) * RATE); |
| 309 | |
| 310 | vm.prank(alice); |
| 311 | love.withdraw(0.5 ether * RATE); |
| 312 | |
| 313 | assertEq(love.totalSupply(), weth.balanceOf(address(love)) * RATE); |
| 314 | } |
| 315 | |
| 316 | /// @dev wETH sent straight to the contract is not claimable — it only lifts |
| 317 | /// the backing, it never mints LOVE. |
| 318 | function test_DonatedWethDoesNotMint() public { |
| 319 | _deposit(alice, 1 ether); |
| 320 | |
| 321 | vm.prank(bob); |
| 322 | // forge-lint: disable-next-line(erc20-unchecked-transfer) |
| 323 | weth.transfer(address(love), 5 ether); |
| 324 | |
| 325 | assertEq(love.totalSupply(), 1 ether * RATE); |
| 326 | assertEq(weth.balanceOf(address(love)), 6 ether); |
| 327 | } |
| 328 | |
| make love an erc20 with public mint 4c8048e julienbrg 8h ago | 329 | /*////////////////////////////////////////////////////////////// |
| 330 | FUZZ |
| 331 | //////////////////////////////////////////////////////////////*/ |
| 332 | |
| Merge 3-peg-love-to-weth into main ce13509 rickub 7h ago | 333 | function testFuzz_DepositByAnyCaller(address caller, uint256 wethAmount) public { |
| 334 | vm.assume(caller != address(0) && caller != address(love) && caller != address(weth)); |
| 335 | wethAmount = bound(wethAmount, 0, type(uint256).max / RATE); |
| make love an erc20 with public mint 4c8048e julienbrg 8h ago | 336 | |
| Merge 3-peg-love-to-weth into main ce13509 rickub 7h ago | 337 | _fund(caller, wethAmount); |
| 338 | _deposit(caller, wethAmount); |
| make love an erc20 with public mint 4c8048e julienbrg 8h ago | 339 | |
| Merge 3-peg-love-to-weth into main ce13509 rickub 7h ago | 340 | assertEq(love.balanceOf(caller), wethAmount * RATE); |
| 341 | assertEq(love.totalSupply(), wethAmount * RATE); |
| 342 | assertEq(weth.balanceOf(address(love)), wethAmount); |
| add love contract 9bf25a7 julienbrg 9h ago | 343 | } |
| 344 | |
| Merge 3-peg-love-to-weth into main ce13509 rickub 7h ago | 345 | function testFuzz_RoundTrip(uint256 wethAmount) public { |
| 346 | wethAmount = bound(wethAmount, 0, type(uint256).max / RATE); |
| add love contract 9bf25a7 julienbrg 9h ago | 347 | |
| Merge 3-peg-love-to-weth into main ce13509 rickub 7h ago | 348 | _fund(alice, wethAmount); |
| 349 | uint256 before = weth.balanceOf(alice); |
| make love an erc20 with public mint 4c8048e julienbrg 8h ago | 350 | |
| Merge 3-peg-love-to-weth into main ce13509 rickub 7h ago | 351 | _deposit(alice, wethAmount); |
| make love an erc20 with public mint 4c8048e julienbrg 8h ago | 352 | vm.prank(alice); |
| Merge 3-peg-love-to-weth into main ce13509 rickub 7h ago | 353 | love.withdraw(wethAmount * RATE); |
| 354 | |
| 355 | assertEq(weth.balanceOf(alice), before); |
| 356 | assertEq(love.totalSupply(), 0); |
| 357 | } |
| 358 | |
| 359 | function testFuzz_WithdrawRevertsUnlessDivisible(uint256 loveAmount) public { |
| 360 | _deposit(alice, 10 ether); |
| add love contract 9bf25a7 julienbrg 9h ago | 361 | |
| Merge 3-peg-love-to-weth into main ce13509 rickub 7h ago | 362 | vm.prank(alice); |
| 363 | if (loveAmount % RATE == 0 && loveAmount <= 10 ether * RATE) { |
| 364 | love.withdraw(loveAmount); |
| 365 | assertEq(weth.balanceOf(address(love)), 10 ether - loveAmount / RATE); |
| 366 | } else { |
| 367 | vm.expectRevert(); |
| 368 | love.withdraw(loveAmount); |
| 369 | } |
| 370 | } |
| 371 | |
| 372 | /// @dev Gives `account` `amount` wETH by wrapping fresh ETH. |
| 373 | function _fund(address account, uint256 amount) internal { |
| 374 | vm.deal(account, amount); |
| 375 | vm.prank(account); |
| 376 | weth.deposit{value: amount}(); |
| 377 | } |
| 378 | |
| 379 | /// @dev Approves and deposits `wethAmount` as `account`, in one step. |
| 380 | function _deposit(address account, uint256 wethAmount) internal { |
| 381 | vm.startPrank(account); |
| 382 | weth.approve(address(love), wethAmount); |
| 383 | love.deposit(wethAmount); |
| 384 | vm.stopPrank(); |
| add love contract 9bf25a7 julienbrg 9h ago | 385 | } |
| 386 | } |