// SPDX-License-Identifier: AGPL-3.0-only pragma solidity >=0.8.0 <0.9.0; import "forge-std/Test.sol"; import { MockERC20 } from "solmate/src/test/utils/mocks/MockERC20.sol"; import { MockERC4626 } from "solmate/src/test/utils/mocks/MockERC4626.sol"; contract ERC4626Test is Test { MockERC20 underlying; MockERC4626 vault; function setUp() public virtual { underlying = new MockERC20("Mock Token", "TKN", 18); vault = new MockERC4626(underlying, "Mock Token Vault", "vwTKN"); } /// forge-config: default.invariant.fail-on-revert = false function invariantMetadata() public virtual { assertEq(vault.name(), "Mock Token Vault"); assertEq(vault.symbol(), "vwTKN"); assertEq(vault.decimals(), 18); } function testMetadata(string calldata name, string calldata symbol) public virtual { MockERC4626 vlt = new MockERC4626(underlying, name, symbol); assertEq(vlt.name(), name); assertEq(vlt.symbol(), symbol); assertEq(address(vlt.asset()), address(underlying)); } function testSingleDepositWithdraw(uint128 amount) public virtual { if (amount == 0) amount = 1; uint256 aliceUnderlyingAmount = amount; address alice = address(0xABCD); underlying.mint(alice, aliceUnderlyingAmount); vm.prank(alice); underlying.approve(address(vault), aliceUnderlyingAmount); assertEq(underlying.allowance(alice, address(vault)), aliceUnderlyingAmount); uint256 alicePreDepositBal = underlying.balanceOf(alice); vm.prank(alice); uint256 aliceShareAmount = vault.deposit(aliceUnderlyingAmount, alice); // Expect exchange rate to be 1:1 on initial deposit. assertEq(aliceUnderlyingAmount, aliceShareAmount); assertEq(vault.previewWithdraw(aliceShareAmount), aliceUnderlyingAmount); assertEq(vault.previewDeposit(aliceUnderlyingAmount), aliceShareAmount); assertEq(vault.totalSupply(), aliceShareAmount); assertEq(vault.totalAssets(), aliceUnderlyingAmount); assertEq(vault.balanceOf(alice), aliceShareAmount); assertEq(vault.convertToAssets(vault.balanceOf(alice)), aliceUnderlyingAmount); assertEq(underlying.balanceOf(alice), alicePreDepositBal - aliceUnderlyingAmount); vm.prank(alice); vault.withdraw(aliceUnderlyingAmount, alice, alice); assertEq(vault.totalAssets(), 0); assertEq(vault.balanceOf(alice), 0); assertEq(vault.convertToAssets(vault.balanceOf(alice)), 0); assertEq(underlying.balanceOf(alice), alicePreDepositBal); } function testSingleMintRedeem(uint128 amount) public virtual { if (amount == 0) amount = 1; uint256 aliceShareAmount = amount; address alice = address(0xABCD); underlying.mint(alice, aliceShareAmount); vm.prank(alice); underlying.approve(address(vault), aliceShareAmount); assertEq(underlying.allowance(alice, address(vault)), aliceShareAmount); uint256 alicePreDepositBal = underlying.balanceOf(alice); vm.prank(alice); uint256 aliceUnderlyingAmount = vault.mint(aliceShareAmount, alice); assertEq(vault.afterDepositHookCalledCounter(), 1); // Expect exchange rate to be 1:1 on initial mint. assertEq(aliceShareAmount, aliceUnderlyingAmount); assertEq(vault.previewWithdraw(aliceShareAmount), aliceUnderlyingAmount); assertEq(vault.previewDeposit(aliceUnderlyingAmount), aliceShareAmount); assertEq(vault.totalSupply(), aliceShareAmount); assertEq(vault.totalAssets(), aliceUnderlyingAmount); assertEq(vault.balanceOf(alice), aliceUnderlyingAmount); assertEq(vault.convertToAssets(vault.balanceOf(alice)), aliceUnderlyingAmount); assertEq(underlying.balanceOf(alice), alicePreDepositBal - aliceUnderlyingAmount); vm.prank(alice); vault.redeem(aliceShareAmount, alice, alice); assertEq(vault.totalAssets(), 0); assertEq(vault.balanceOf(alice), 0); assertEq(vault.convertToAssets(vault.balanceOf(alice)), 0); assertEq(underlying.balanceOf(alice), alicePreDepositBal); } function testMultipleMintDepositRedeemWithdraw() public virtual { // Scenario: // A = Alice, B = Bob // ________________________________________________________ // | Vault shares | A share | A assets | B share | B assets | // |========================================================| // | 1. Alice mints 2000 shares (costs 2000 tokens) | // |--------------|---------|----------|---------|----------| // | 2000 | 2000 | 2000 | 0 | 0 | // |--------------|---------|----------|---------|----------| // | 2. Bob deposits 4000 tokens (mints 4000 shares) | // |--------------|---------|----------|---------|----------| // | 6000 | 2000 | 2000 | 4000 | 4000 | // |--------------|---------|----------|---------|----------| // | 3. Vault mutates by +3000 tokens... | // | (simulated yield returned from strategy)... | // |--------------|---------|----------|---------|----------| // | 6000 | 2000 | 3000 | 4000 | 6000 | // |--------------|---------|----------|---------|----------| // | 4. Alice deposits 2000 tokens (mints 1333 shares) | // |--------------|---------|----------|---------|----------| // | 7333 | 3333 | 4999 | 4000 | 6000 | // |--------------|---------|----------|---------|----------| // | 5. Bob mints 2000 shares (costs 3001 assets) | // | NOTE: Bob's assets spent got rounded up | // | NOTE: Alice's vault assets got rounded up | // |--------------|---------|----------|---------|----------| // | 9333 | 3333 | 5000 | 6000 | 9000 | // |--------------|---------|----------|---------|----------| // | 6. Vault mutates by +3000 tokens... | // | (simulated yield returned from strategy) | // | NOTE: Vault holds 17001 tokens, but sum of | // | assetsOf() is 17000. | // |--------------|---------|----------|---------|----------| // | 9333 | 3333 | 6071 | 6000 | 10929 | // |--------------|---------|----------|---------|----------| // | 7. Alice redeem 1333 shares (2428 assets) | // |--------------|---------|----------|---------|----------| // | 8000 | 2000 | 3643 | 6000 | 10929 | // |--------------|---------|----------|---------|----------| // | 8. Bob withdraws 2928 assets (1608 shares) | // |--------------|---------|----------|---------|----------| // | 6392 | 2000 | 3643 | 4392 | 8000 | // |--------------|---------|----------|---------|----------| // | 9. Alice withdraws 3643 assets (2000 shares) | // | NOTE: Bob's assets have been rounded back up | // |--------------|---------|----------|---------|----------| // | 4392 | 0 | 0 | 4392 | 8001 | // |--------------|---------|----------|---------|----------| // | 10. Bob redeem 4392 shares (8001 tokens) | // |--------------|---------|----------|---------|----------| // | 0 | 0 | 0 | 0 | 0 | // |______________|_________|__________|_________|__________| address alice = address(0xABCD); address bob = address(0xDCBA); uint256 mutationUnderlyingAmount = 3000; underlying.mint(alice, 4000); vm.prank(alice); underlying.approve(address(vault), 4000); assertEq(underlying.allowance(alice, address(vault)), 4000); underlying.mint(bob, 7001); vm.prank(bob); underlying.approve(address(vault), 7001); assertEq(underlying.allowance(bob, address(vault)), 7001); // 1. Alice mints 2000 shares (costs 2000 tokens) vm.prank(alice); uint256 aliceUnderlyingAmount = vault.mint(2000, alice); uint256 aliceShareAmount = vault.previewDeposit(aliceUnderlyingAmount); assertEq(vault.afterDepositHookCalledCounter(), 1); // Expect to have received the requested mint amount. assertEq(aliceShareAmount, 2000); assertEq(vault.balanceOf(alice), aliceShareAmount); assertEq(vault.convertToAssets(vault.balanceOf(alice)), aliceUnderlyingAmount); assertEq(vault.convertToShares(aliceUnderlyingAmount), vault.balanceOf(alice)); // Expect a 1:1 ratio before mutation. assertEq(aliceUnderlyingAmount, 2000); // Sanity check. assertEq(vault.totalSupply(), aliceShareAmount); assertEq(vault.totalAssets(), aliceUnderlyingAmount); // 2. Bob deposits 4000 tokens (mints 4000 shares) vm.prank(bob); uint256 bobShareAmount = vault.deposit(4000, bob); uint256 bobUnderlyingAmount = vault.previewWithdraw(bobShareAmount); assertEq(vault.afterDepositHookCalledCounter(), 2); // Expect to have received the requested underlying amount. assertEq(bobUnderlyingAmount, 4000); assertEq(vault.balanceOf(bob), bobShareAmount); assertEq(vault.convertToAssets(vault.balanceOf(bob)), bobUnderlyingAmount); assertEq(vault.convertToShares(bobUnderlyingAmount), vault.balanceOf(bob)); // Expect a 1:1 ratio before mutation. assertEq(bobShareAmount, bobUnderlyingAmount); // Sanity check. uint256 preMutationShareBal = aliceShareAmount + bobShareAmount; uint256 preMutationBal = aliceUnderlyingAmount + bobUnderlyingAmount; assertEq(vault.totalSupply(), preMutationShareBal); assertEq(vault.totalAssets(), preMutationBal); assertEq(vault.totalSupply(), 6000); assertEq(vault.totalAssets(), 6000); // 3. Vault mutates by +3000 tokens... | // (simulated yield returned from strategy)... // The Vault now contains more tokens than deposited which causes the exchange rate to change. // Alice share is 33.33% of the Vault, Bob 66.66% of the Vault. // Alice's share count stays the same but the underlying amount changes from 2000 to 3000. // Bob's share count stays the same but the underlying amount changes from 4000 to 6000. underlying.mint(address(vault), mutationUnderlyingAmount); assertEq(vault.totalSupply(), preMutationShareBal); assertEq(vault.totalAssets(), preMutationBal + mutationUnderlyingAmount); assertEq(vault.balanceOf(alice), aliceShareAmount); assertEq( vault.convertToAssets(vault.balanceOf(alice)), aliceUnderlyingAmount + (mutationUnderlyingAmount / 3) * 1 ); assertEq(vault.balanceOf(bob), bobShareAmount); assertEq(vault.convertToAssets(vault.balanceOf(bob)), bobUnderlyingAmount + (mutationUnderlyingAmount / 3) * 2); // 4. Alice deposits 2000 tokens (mints 1333 shares) vm.prank(alice); vault.deposit(2000, alice); assertEq(vault.totalSupply(), 7333); assertEq(vault.balanceOf(alice), 3333); assertEq(vault.convertToAssets(vault.balanceOf(alice)), 4999); assertEq(vault.balanceOf(bob), 4000); assertEq(vault.convertToAssets(vault.balanceOf(bob)), 6000); // 5. Bob mints 2000 shares (costs 3001 assets) // NOTE: Bob's assets spent got rounded up // NOTE: Alices's vault assets got rounded up vm.prank(bob); vault.mint(2000, bob); assertEq(vault.totalSupply(), 9333); assertEq(vault.balanceOf(alice), 3333); assertEq(vault.convertToAssets(vault.balanceOf(alice)), 5000); assertEq(vault.balanceOf(bob), 6000); assertEq(vault.convertToAssets(vault.balanceOf(bob)), 9000); // Sanity checks: // Alice and bob should have spent all their tokens now assertEq(underlying.balanceOf(alice), 0); assertEq(underlying.balanceOf(bob), 0); // Assets in vault: 4k (alice) + 7k (bob) + 3k (yield) + 1 (round up) assertEq(vault.totalAssets(), 14001); // 6. Vault mutates by +3000 tokens // NOTE: Vault holds 17001 tokens, but sum of assetsOf() is 17000. underlying.mint(address(vault), mutationUnderlyingAmount); assertEq(vault.totalAssets(), 17001); assertEq(vault.convertToAssets(vault.balanceOf(alice)), 6071); assertEq(vault.convertToAssets(vault.balanceOf(bob)), 10929); // 7. Alice redeem 1333 shares (2428 assets) vm.prank(alice); vault.redeem(1333, alice, alice); assertEq(underlying.balanceOf(alice), 2428); assertEq(vault.totalSupply(), 8000); assertEq(vault.totalAssets(), 14573); assertEq(vault.balanceOf(alice), 2000); assertEq(vault.convertToAssets(vault.balanceOf(alice)), 3643); assertEq(vault.balanceOf(bob), 6000); assertEq(vault.convertToAssets(vault.balanceOf(bob)), 10929); // 8. Bob withdraws 2929 assets (1608 shares) vm.prank(bob); vault.withdraw(2929, bob, bob); assertEq(underlying.balanceOf(bob), 2929); assertEq(vault.totalSupply(), 6392); assertEq(vault.totalAssets(), 11644); assertEq(vault.balanceOf(alice), 2000); assertEq(vault.convertToAssets(vault.balanceOf(alice)), 3643); assertEq(vault.balanceOf(bob), 4392); assertEq(vault.convertToAssets(vault.balanceOf(bob)), 8000); // 9. Alice withdraws 3643 assets (2000 shares) // NOTE: Bob's assets have been rounded back up vm.prank(alice); vault.withdraw(3643, alice, alice); assertEq(underlying.balanceOf(alice), 6071); assertEq(vault.totalSupply(), 4392); assertEq(vault.totalAssets(), 8001); assertEq(vault.balanceOf(alice), 0); assertEq(vault.convertToAssets(vault.balanceOf(alice)), 0); assertEq(vault.balanceOf(bob), 4392); assertEq(vault.convertToAssets(vault.balanceOf(bob)), 8001); // 10. Bob redeem 4392 shares (8001 tokens) vm.prank(bob); vault.redeem(4392, bob, bob); assertEq(underlying.balanceOf(bob), 10930); assertEq(vault.totalSupply(), 0); assertEq(vault.totalAssets(), 0); assertEq(vault.balanceOf(alice), 0); assertEq(vault.convertToAssets(vault.balanceOf(alice)), 0); assertEq(vault.balanceOf(bob), 0); assertEq(vault.convertToAssets(vault.balanceOf(bob)), 0); // Sanity check assertEq(underlying.balanceOf(address(vault)), 0); } function testFailDepositWithNotEnoughApproval() public virtual { underlying.mint(address(this), 0.5e18); underlying.approve(address(vault), 0.5e18); assertEq(underlying.allowance(address(this), address(vault)), 0.5e18); vault.deposit(1e18, address(this)); } function testFailWithdrawWithNotEnoughUnderlyingAmount() public virtual { underlying.mint(address(this), 0.5e18); underlying.approve(address(vault), 0.5e18); vault.deposit(0.5e18, address(this)); vault.withdraw(1e18, address(this), address(this)); } function testFailRedeemWithNotEnoughShareAmount() public virtual { underlying.mint(address(this), 0.5e18); underlying.approve(address(vault), 0.5e18); vault.deposit(0.5e18, address(this)); vault.redeem(1e18, address(this), address(this)); } function testFailWithdrawWithNoUnderlyingAmount() public virtual { vault.withdraw(1e18, address(this), address(this)); } function testFailRedeemWithNoShareAmount() public virtual { vault.redeem(1e18, address(this), address(this)); } function testFailDepositWithNoApproval() public virtual { vault.deposit(1e18, address(this)); } function testFailMintWithNoApproval() public virtual { vault.mint(1e18, address(this)); } function testFailDepositZero() public virtual { vault.deposit(0, address(this)); } function testMintZero() public virtual { vault.mint(0, address(this)); assertEq(vault.balanceOf(address(this)), 0); assertEq(vault.convertToAssets(vault.balanceOf(address(this))), 0); assertEq(vault.totalSupply(), 0); assertEq(vault.totalAssets(), 0); } function testFailRedeemZero() public virtual { vault.redeem(0, address(this), address(this)); } function testWithdrawZero() public virtual { vault.withdraw(0, address(this), address(this)); assertEq(vault.balanceOf(address(this)), 0); assertEq(vault.convertToAssets(vault.balanceOf(address(this))), 0); assertEq(vault.totalSupply(), 0); assertEq(vault.totalAssets(), 0); } function testVaultInteractionsForSomeoneElse() public virtual { // init 2 users with a 1e18 balance address alice = address(0xABCD); address bob = address(0xDCBA); underlying.mint(alice, 1e18); underlying.mint(bob, 1e18); vm.prank(alice); underlying.approve(address(vault), 1e18); vm.prank(bob); underlying.approve(address(vault), 1e18); // alice deposits 1e18 for bob vm.prank(alice); vault.deposit(1e18, bob); assertEq(vault.balanceOf(alice), 0); assertEq(vault.balanceOf(bob), 1e18); assertEq(underlying.balanceOf(alice), 0); // bob mint 1e18 for alice vm.prank(bob); vault.mint(1e18, alice); assertEq(vault.balanceOf(alice), 1e18); assertEq(vault.balanceOf(bob), 1e18); assertEq(underlying.balanceOf(bob), 0); // alice redeem 1e18 for bob vm.prank(alice); vault.redeem(1e18, bob, alice); assertEq(vault.balanceOf(alice), 0); assertEq(vault.balanceOf(bob), 1e18); assertEq(underlying.balanceOf(bob), 1e18); // bob withdraw 1e18 for alice vm.prank(bob); vault.withdraw(1e18, alice, bob); assertEq(vault.balanceOf(alice), 0); assertEq(vault.balanceOf(bob), 0); assertEq(underlying.balanceOf(alice), 1e18); } }