| add love contract 9bf25a7 julienbrg 11h ago | 1 | // SPDX-License-Identifier: MIT |
| 2 | pragma solidity ^0.8.30; |
| 3 | |
| 4 | import {Love} from "../src/Love.sol"; |
| add weth peg tests and docs 5376202 julienbrg 9h ago | 5 | import {MockWETH} from "./mocks/MockWETH.sol"; |
| make love an erc20 with public mint 4c8048e julienbrg 11h 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 11h ago | 8 | import {Test} from "forge-std/Test.sol"; |
| 9 | |
| 10 | contract LoveTest is Test { |
| 11 | Love public love; |
| add weth peg tests and docs 5376202 julienbrg 9h ago | 12 | MockWETH public weth; |
| add love contract 9bf25a7 julienbrg 11h ago | 13 | |
| 14 | address alice = makeAddr("alice"); |
| 15 | address bob = makeAddr("bob"); |
| make love an erc20 with public mint 4c8048e julienbrg 11h ago | 16 | address stranger = makeAddr("stranger"); |
| add love contract 9bf25a7 julienbrg 11h ago | 17 | |
| add weth peg tests and docs 5376202 julienbrg 9h ago | 18 | uint256 constant RATE = 100_000; |
| 19 | |
| add love contract 9bf25a7 julienbrg 11h ago | 20 | function setUp() public { |
| add weth peg tests and docs 5376202 julienbrg 9h ago | 21 | weth = new MockWETH(); |
| 22 | love = new Love(IERC20(address(weth))); |
| 23 | |
| 24 | _fund(alice, 10 ether); |
| 25 | _fund(bob, 10 ether); |
| add love contract 9bf25a7 julienbrg 11h ago | 26 | } |
| 27 | |
| make love an erc20 with public mint 4c8048e julienbrg 11h ago | 28 | /*////////////////////////////////////////////////////////////// |
| 29 | METADATA |
| 30 | //////////////////////////////////////////////////////////////*/ |
| 31 | |
| 32 | function test_Metadata() public view { |
| 33 | assertEq(love.name(), "Love"); |
| 34 | assertEq(love.symbol(), "LOVE"); |
| 35 | assertEq(love.decimals(), 18); |
| 36 | } |
| 37 | |
| 38 | function test_InitialSupplyIsZero() public view { |
| 39 | assertEq(love.totalSupply(), 0); |
| 40 | } |
| 41 | |
| add weth peg tests and docs 5376202 julienbrg 9h ago | 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 | |
| make love an erc20 with public mint 4c8048e julienbrg 11h ago | 54 | /*////////////////////////////////////////////////////////////// |
| add weth peg tests and docs 5376202 julienbrg 9h ago | 55 | DEPOSIT |
| make love an erc20 with public mint 4c8048e julienbrg 11h ago | 56 | //////////////////////////////////////////////////////////////*/ |
| 57 | |
| add weth peg tests and docs 5376202 julienbrg 9h ago | 58 | function test_Deposit() public { |
| 59 | _deposit(alice, 1 ether); |
| make love an erc20 with public mint 4c8048e julienbrg 11h ago | 60 | |
| add weth peg tests and docs 5376202 julienbrg 9h ago | 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); |
| make love an erc20 with public mint 4c8048e julienbrg 11h ago | 65 | } |
| 66 | |
| add weth peg tests and docs 5376202 julienbrg 9h ago | 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); |
| add love contract 9bf25a7 julienbrg 11h ago | 80 | |
| add weth peg tests and docs 5376202 julienbrg 9h ago | 81 | vm.prank(alice); |
| 82 | love.deposit(2 ether); |
| add love contract 9bf25a7 julienbrg 11h ago | 83 | } |
| 84 | |
| add weth peg tests and docs 5376202 julienbrg 9h ago | 85 | function test_DepositEmitsTransferFromZero() public { |
| 86 | vm.prank(alice); |
| 87 | weth.approve(address(love), 1 ether); |
| 88 | |
| make love an erc20 with public mint 4c8048e julienbrg 11h ago | 89 | vm.expectEmit(true, true, false, true); |
| add weth peg tests and docs 5376202 julienbrg 9h ago | 90 | emit IERC20.Transfer(address(0), alice, 1 ether * RATE); |
| make love an erc20 with public mint 4c8048e julienbrg 11h ago | 91 | |
| 92 | vm.prank(alice); |
| add weth peg tests and docs 5376202 julienbrg 9h ago | 93 | love.deposit(1 ether); |
| make love an erc20 with public mint 4c8048e julienbrg 11h ago | 94 | } |
| 95 | |
| add weth peg tests and docs 5376202 julienbrg 9h ago | 96 | function test_DepositAccumulates() public { |
| 97 | _deposit(alice, 1 ether); |
| 98 | _deposit(alice, 3 ether); |
| add love contract 9bf25a7 julienbrg 11h ago | 99 | |
| add weth peg tests and docs 5376202 julienbrg 9h ago | 100 | assertEq(love.balanceOf(alice), 4 ether * RATE); |
| 101 | assertEq(weth.balanceOf(address(love)), 4 ether); |
| add love contract 9bf25a7 julienbrg 11h ago | 102 | } |
| 103 | |
| add weth peg tests and docs 5376202 julienbrg 9h ago | 104 | function test_DepositZero() public { |
| 105 | _deposit(alice, 0); |
| 106 | |
| 107 | assertEq(love.balanceOf(alice), 0); |
| 108 | assertEq(love.totalSupply(), 0); |
| make love an erc20 with public mint 4c8048e julienbrg 11h ago | 109 | } |
| 110 | |
| add weth peg tests and docs 5376202 julienbrg 9h ago | 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 | } |
| make love an erc20 with public mint 4c8048e julienbrg 11h ago | 129 | |
| add weth peg tests and docs 5376202 julienbrg 9h ago | 130 | function test_RevertWhen_DepositOverflows() public { |
| 131 | vm.prank(alice); |
| make love an erc20 with public mint 4c8048e julienbrg 11h ago | 132 | vm.expectRevert(); |
| add weth peg tests and docs 5376202 julienbrg 9h ago | 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); |
| make love an erc20 with public mint 4c8048e julienbrg 11h ago | 227 | } |
| 228 | |
| 229 | /*////////////////////////////////////////////////////////////// |
| 230 | TRANSFER |
| 231 | //////////////////////////////////////////////////////////////*/ |
| 232 | |
| 233 | function test_Transfer() public { |
| add weth peg tests and docs 5376202 julienbrg 9h ago | 234 | _deposit(alice, 1 ether); |
| make love an erc20 with public mint 4c8048e julienbrg 11h ago | 235 | |
| 236 | vm.prank(alice); |
| 237 | assertTrue(love.transfer(bob, 4e18)); |
| 238 | |
| add weth peg tests and docs 5376202 julienbrg 9h ago | 239 | assertEq(love.balanceOf(alice), 1 ether * RATE - 4e18); |
| make love an erc20 with public mint 4c8048e julienbrg 11h ago | 240 | assertEq(love.balanceOf(bob), 4e18); |
| 241 | } |
| 242 | |
| 243 | function test_RevertWhen_TransferExceedsBalance() public { |
| add love contract 9bf25a7 julienbrg 11h ago | 244 | vm.prank(alice); |
| add weth peg tests and docs 5376202 julienbrg 9h ago | 245 | vm.expectRevert(abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, alice, 0, 2e18)); |
| make love an erc20 with public mint 4c8048e julienbrg 11h ago | 246 | // forge-lint: disable-next-line(erc20-unchecked-transfer) |
| 247 | love.transfer(bob, 2e18); |
| add love contract 9bf25a7 julienbrg 11h ago | 248 | } |
| 249 | |
| make love an erc20 with public mint 4c8048e julienbrg 11h ago | 250 | /*////////////////////////////////////////////////////////////// |
| 251 | APPROVE / TRANSFERFROM |
| 252 | //////////////////////////////////////////////////////////////*/ |
| 253 | |
| 254 | function test_ApproveAndTransferFrom() public { |
| add weth peg tests and docs 5376202 julienbrg 9h ago | 255 | _deposit(alice, 1 ether); |
| make love an erc20 with public mint 4c8048e julienbrg 11h ago | 256 | |
| add love contract 9bf25a7 julienbrg 11h ago | 257 | vm.prank(alice); |
| make love an erc20 with public mint 4c8048e julienbrg 11h ago | 258 | assertTrue(love.approve(bob, 6e18)); |
| 259 | assertEq(love.allowance(alice, bob), 6e18); |
| 260 | |
| 261 | vm.prank(bob); |
| 262 | assertTrue(love.transferFrom(alice, stranger, 6e18)); |
| 263 | |
| add weth peg tests and docs 5376202 julienbrg 9h ago | 264 | assertEq(love.balanceOf(alice), 1 ether * RATE - 6e18); |
| make love an erc20 with public mint 4c8048e julienbrg 11h ago | 265 | assertEq(love.balanceOf(stranger), 6e18); |
| 266 | assertEq(love.allowance(alice, bob), 0); |
| add love contract 9bf25a7 julienbrg 11h ago | 267 | } |
| 268 | |
| make love an erc20 with public mint 4c8048e julienbrg 11h ago | 269 | function test_RevertWhen_TransferFromExceedsAllowance() public { |
| add weth peg tests and docs 5376202 julienbrg 9h ago | 270 | _deposit(alice, 1 ether); |
| make love an erc20 with public mint 4c8048e julienbrg 11h ago | 271 | |
| add love contract 9bf25a7 julienbrg 11h ago | 272 | vm.prank(alice); |
| make love an erc20 with public mint 4c8048e julienbrg 11h ago | 273 | love.approve(bob, 1e18); |
| 274 | |
| 275 | vm.prank(bob); |
| 276 | vm.expectRevert(abi.encodeWithSelector(IERC20Errors.ERC20InsufficientAllowance.selector, bob, 1e18, 2e18)); |
| 277 | // forge-lint: disable-next-line(erc20-unchecked-transfer) |
| 278 | love.transferFrom(alice, stranger, 2e18); |
| 279 | } |
| 280 | |
| add weth peg tests and docs 5376202 julienbrg 9h ago | 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 | |
| make love an erc20 with public mint 4c8048e julienbrg 11h ago | 322 | /*////////////////////////////////////////////////////////////// |
| 323 | FUZZ |
| 324 | //////////////////////////////////////////////////////////////*/ |
| 325 | |
| add weth peg tests and docs 5376202 julienbrg 9h ago | 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); |
| make love an erc20 with public mint 4c8048e julienbrg 11h ago | 329 | |
| add weth peg tests and docs 5376202 julienbrg 9h ago | 330 | _fund(caller, wethAmount); |
| 331 | _deposit(caller, wethAmount); |
| make love an erc20 with public mint 4c8048e julienbrg 11h ago | 332 | |
| add weth peg tests and docs 5376202 julienbrg 9h ago | 333 | assertEq(love.balanceOf(caller), wethAmount * RATE); |
| 334 | assertEq(love.totalSupply(), wethAmount * RATE); |
| 335 | assertEq(weth.balanceOf(address(love)), wethAmount); |
| add love contract 9bf25a7 julienbrg 11h ago | 336 | } |
| 337 | |
| add weth peg tests and docs 5376202 julienbrg 9h ago | 338 | function testFuzz_RoundTrip(uint256 wethAmount) public { |
| 339 | wethAmount = bound(wethAmount, 0, type(uint256).max / RATE); |
| add love contract 9bf25a7 julienbrg 11h ago | 340 | |
| add weth peg tests and docs 5376202 julienbrg 9h ago | 341 | _fund(alice, wethAmount); |
| 342 | uint256 before = weth.balanceOf(alice); |
| make love an erc20 with public mint 4c8048e julienbrg 11h ago | 343 | |
| add weth peg tests and docs 5376202 julienbrg 9h ago | 344 | _deposit(alice, wethAmount); |
| make love an erc20 with public mint 4c8048e julienbrg 11h ago | 345 | vm.prank(alice); |
| add weth peg tests and docs 5376202 julienbrg 9h ago | 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); |
| add love contract 9bf25a7 julienbrg 11h ago | 354 | |
| add weth peg tests and docs 5376202 julienbrg 9h ago | 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(); |
| add love contract 9bf25a7 julienbrg 11h ago | 376 | } |
| 377 | } |