mirror of
https://github.com/Instadapp/fluid-contracts-public.git
synced 2024-07-29 21:57:37 +00:00
441 lines
18 KiB
Solidity
441 lines
18 KiB
Solidity
|
// 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);
|
||
|
}
|
||
|
}
|