diff --git a/test/unit/accounting.t.sol b/test/unit/accounting.t.sol new file mode 100644 index 0000000..a1a6981 --- /dev/null +++ b/test/unit/accounting.t.sol @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity ^0.8.24; + +import {Test} from "lib/forge-std/src/Test.sol"; +import {Vault} from "src/Vault.sol"; +import {TransparentUpgradeableProxy} from "src/Common.sol"; +import {MainnetContracts} from "script/Contracts.sol"; +import {MainnetActors} from "script/Actors.sol"; +import {Etches} from "test/helpers/Etches.sol"; +import {WETH9} from "test/mocks/MockWETH.sol"; +import {SetupVault} from "test/helpers/SetupVault.sol"; + +contract VaultAccountingUnitTest is Test, MainnetContracts, MainnetActors, Etches { + Vault public vaultImplementation; + TransparentUpgradeableProxy public vaultProxy; + + Vault public vault; + WETH9 public weth; + + address public alice = address(0x1); + uint256 public constant INITIAL_BALANCE = 100_000 ether; + + function setUp() public { + SetupVault setupVault = new SetupVault(); + (vault, weth) = setupVault.setup(); + + // Give Alice some tokens + deal(alice, INITIAL_BALANCE); + weth.deposit{value: INITIAL_BALANCE}(); + weth.transfer(alice, INITIAL_BALANCE); + + // Approve vault to spend Alice's tokens + vm.prank(alice); + weth.approve(address(vault), type(uint256).max); + } + + function allocateToBuffer(uint256 amount) public { + address[] memory targets = new address[](2); + targets[0] = WETH; + targets[1] = BUFFER_STRATEGY; + + uint256[] memory values = new uint256[](2); + values[0] = 0; + values[1] = 0; + + bytes[] memory data = new bytes[](2); + data[0] = abi.encodeWithSignature("approve(address,uint256)", vault.bufferStrategy(), amount); + data[1] = abi.encodeWithSignature("deposit(uint256,address)", amount, address(vault)); + + vm.prank(ADMIN); + vault.processor(targets, values, data); + } + + function test_Vault_Accounting_convertToShares() public view { + uint256 assets = 1000 ether; + uint256 shares = vault.convertToShares(assets); + assertEq(shares, vault.previewDeposit(assets), "Shares should match previewDeposit"); + } + + function test_Vault_Accounting_convertToAssets() public view { + uint256 shares = 1000 ether; + uint256 assets = vault.convertToAssets(shares); + assertEq(assets, vault.previewRedeem(shares), "Assets should match previewRedeem"); + } + + function test_Vault_Accounting_totalAssets_afterDeposit() public { + uint256 depositAmount = 1000 ether; + vm.prank(alice); + vault.deposit(depositAmount, alice); + uint256 totalAssets = vault.totalAssets(); + assertEq(totalAssets, depositAmount, "Total assets should match the deposit amount"); + } + + function test_Vault_Accounting_totalAssets_afterMultipleDeposits() public { + uint256 depositAmount1 = 1000 ether; + uint256 depositAmount2 = 2000 ether; + vm.prank(alice); + vault.deposit(depositAmount1, alice); + vm.prank(alice); + vault.deposit(depositAmount2, alice); + uint256 totalAssets = vault.totalAssets(); + assertEq(totalAssets, depositAmount1 + depositAmount2, "Total assets should match the sum of deposit amounts"); + } + + function test_Vault_Accounting_totalAssets_afterWithdraw() public { + uint256 depositAmount = 1000 ether; + uint256 withdrawAmount = 500 ether; + vm.prank(alice); + vault.deposit(depositAmount, alice); + + allocateToBuffer(depositAmount); + + vm.prank(alice); + vault.withdraw(withdrawAmount, alice, alice); + uint256 totalAssets = vault.totalAssets(); + assertEq( + totalAssets, + depositAmount - withdrawAmount, + "Total assets should match the remaining amount after withdrawal" + ); + } + + function test_Vault_Accounting_totalAssets_afterMultipleWithdrawals() public { + uint256 depositAmount = 3000 ether; + uint256 withdrawAmount1 = 1000 ether; + uint256 withdrawAmount2 = 500 ether; + vm.prank(alice); + vault.deposit(depositAmount, alice); + + allocateToBuffer(depositAmount); + + vm.prank(alice); + vault.withdraw(withdrawAmount1, alice, alice); + vm.prank(alice); + vault.withdraw(withdrawAmount2, alice, alice); + uint256 totalAssets = vault.totalAssets(); + assertEq( + totalAssets, + depositAmount - withdrawAmount1 - withdrawAmount2, + "Total assets should match the remaining amount after multiple withdrawals" + ); + } + + function test_Vault_Accounting_totalSupply_afterDeposit() public { + uint256 depositAmount = 1000 ether; + vm.prank(alice); + vault.deposit(depositAmount, alice); + uint256 totalSupply = vault.totalSupply(); + assertEq(totalSupply, depositAmount, "Total supply should match the deposit amount"); + } + + function test_Vault_Accounting_totalSupply_afterMultipleDeposits() public { + uint256 depositAmount1 = 1000 ether; + uint256 depositAmount2 = 2000 ether; + vm.prank(alice); + vault.deposit(depositAmount1, alice); + vm.prank(alice); + vault.deposit(depositAmount2, alice); + uint256 totalSupply = vault.totalSupply(); + assertEq(totalSupply, depositAmount1 + depositAmount2, "Total supply should match the sum of deposit amounts"); + } + + function test_Vault_Accounting_totalSupply_afterWithdraw() public { + uint256 depositAmount = 1000 ether; + uint256 bufferRatio = 5; + + vm.prank(alice); + vault.deposit(depositAmount, alice); + + allocateToBuffer(depositAmount / bufferRatio); + + vm.prank(alice); + uint256 maxWithdraw = vault.maxWithdraw(alice); + vault.withdraw(maxWithdraw, alice, alice); + uint256 totalSupply = vault.totalSupply(); + assertEq( + totalSupply, depositAmount - maxWithdraw, "Total supply should match the remaining amount after withdrawal" + ); + } + + function test_Vault_Accounting_totalSupply_afterMultipleWithdrawals() public { + uint256 depositAmount = 3000 ether; + uint256 bufferRatio = 5; + + uint256 withdrawAmount1 = vault.maxWithdraw(alice) / 3; + uint256 withdrawAmount2 = vault.maxWithdraw(alice) / 7; + + vm.prank(alice); + vault.deposit(depositAmount, alice); + + allocateToBuffer(depositAmount / bufferRatio); + + vm.prank(alice); + vault.withdraw(withdrawAmount1, alice, alice); + vm.prank(alice); + vault.withdraw(withdrawAmount2, alice, alice); + uint256 totalSupply = vault.totalSupply(); + assertEq( + totalSupply, + depositAmount - withdrawAmount1 - withdrawAmount2, + "Total supply should match the remaining amount after multiple withdrawals" + ); + } +} diff --git a/test/unit/compliance.t.sol b/test/unit/compliance.t.sol new file mode 100644 index 0000000..5781cba --- /dev/null +++ b/test/unit/compliance.t.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity ^0.8.24; + +import {Test} from "lib/forge-std/src/Test.sol"; +import {Vault} from "src/Vault.sol"; +import {TransparentUpgradeableProxy} from "src/Common.sol"; +import {MainnetContracts} from "script/Contracts.sol"; +import {MainnetActors} from "script/Actors.sol"; +import {Etches} from "test/helpers/Etches.sol"; +import {WETH9} from "test/mocks/MockWETH.sol"; +import {SetupVault} from "test/helpers/SetupVault.sol"; + +contract Vault4626ComplianceUnitTest is Test, MainnetContracts, MainnetActors, Etches { + Vault public vaultImplementation; + TransparentUpgradeableProxy public vaultProxy; + + Vault public vault; + WETH9 public weth; + + address public alice = address(0x1); + uint256 public constant INITIAL_BALANCE = 100_000 ether; + + function setUp() public { + SetupVault setupVault = new SetupVault(); + (vault, weth) = setupVault.setup(); + + // Give Alice some tokens + deal(alice, INITIAL_BALANCE); + weth.deposit{value: INITIAL_BALANCE}(); + weth.transfer(alice, INITIAL_BALANCE); + + // Approve vault to spend Alice's tokens + vm.prank(alice); + weth.approve(address(vault), type(uint256).max); + } + + /* The maxWithdraw function should return the maximum amount of underlying assets that + can be withdrawn by the owner without affecting the vault's ability to meet its obligations. + Paused State: The function correctly returns 0 if the vault is paused + */ + function test_Vault_Compliance_maxWithdraw_paused() public { + vm.prank(ADMIN); + vault.pause(true); + uint256 maxWithdrawAmount = vault.maxWithdraw(alice); + assertEq(maxWithdrawAmount, 0, "Max withdraw amount should be 0 when paused"); + } + + /* Asset Conversion: The function calculates the baseConvertedAssets by converting the + owner's balance of vault tokens to the equivalent amount of underlying assets. + This is consistent with the ERC-4626 requirement to determine the maximum withdrawable + amount based on the owner's share balance. + */ + function test_Vault_Compliance_maxWithdraw_assetConversion() public view { + uint256 aliceBalance = vault.balanceOf(alice); + uint256 baseConvertedAssets = vault.convertToAssets(aliceBalance); + uint256 maxWithdrawAmount = vault.maxWithdraw(alice); + assertEq(maxWithdrawAmount, baseConvertedAssets, "Max withdraw amount should be equal to base converted assets"); + } + + /* + Available Assets Check: The function checks the available assets in the buffer strategy + and ensures that the withdrawal does not exceed this amount. This is a prudent measure + to ensure the vault maintains sufficient liquidity. + */ + function test_Vault_Compliance_maxWithdraw_availableAssetsCheck() public view { + uint256 aliceBalance = vault.balanceOf(alice); + uint256 baseConvertedAssets = vault.convertToAssets(aliceBalance); + uint256 availableAssets = vault.maxWithdraw(address(vault)); + uint256 maxWithdrawAmount = vault.maxWithdraw(alice); + assertEq( + maxWithdrawAmount, + availableAssets < baseConvertedAssets ? 0 : baseConvertedAssets, + "Max withdraw amount should be the lesser of available assets or base converted assets" + ); + } + + /* + 5. Return Value: The function returns the lesser of the converted asset value or the available assets, + which aligns with the intent of ensuring that the vault can fulfill the withdrawal request without + compromising its liquidity. + */ + function test_Vault_Compliance_maxWithdraw_returnValue() public view { + uint256 aliceBalance = vault.balanceOf(alice); + uint256 baseConvertedAssets = vault.convertToAssets(aliceBalance); + uint256 availableAssets = vault.maxWithdraw(address(vault)); + uint256 maxWithdrawAmount = vault.maxWithdraw(alice); + uint256 expectedValue = availableAssets < baseConvertedAssets ? 0 : baseConvertedAssets; + assertEq( + maxWithdrawAmount, + expectedValue, + "Max withdraw amount should be the lesser of available assets or base converted assets" + ); + } +}