diff --git a/test/peripheral/OracleVaultController.t.sol b/test/peripheral/OracleVaultController.t.sol new file mode 100644 index 00000000..dc104a3a --- /dev/null +++ b/test/peripheral/OracleVaultController.t.sol @@ -0,0 +1,343 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.25; + +import {Test} from "forge-std/Test.sol"; +import {MockERC20} from "test/mocks/MockERC20.sol"; +import {OracleVaultController, Limit} from "src/peripheral/OracleVaultController.sol"; +import {MockPushOracle} from "test/mocks/MockPushOracle.sol"; +import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; + +contract OracleVaultControllerTest is Test { + using FixedPointMathLib for uint256; + + OracleVaultController controller; + MockPushOracle oracle; + MockPausable vault; + MockERC20 asset; + + address owner = address(0x1); + address keeper = address(0x2); + address alice = address(0x3); + + uint256 constant ONE = 1e18; + uint256 constant INITIAL_PRICE = 1e18; + + event KeeperUpdated(address vault, address previous, address current); + event LimitUpdated(address vault, Limit previous, Limit current); + + function setUp() public { + vm.label(owner, "owner"); + vm.label(keeper, "keeper"); + vm.label(alice, "alice"); + + oracle = new MockPushOracle(); + vault = new MockPausable(); + asset = new MockERC20("Test Token", "TEST", 18); + + controller = new OracleVaultController(address(oracle), owner); + + // Setup initial state + oracle.setPrice(address(vault), address(asset), INITIAL_PRICE, ONE); + } + + /*////////////////////////////////////////////////////////////// + KEEPER MANAGEMENT TESTS + //////////////////////////////////////////////////////////////*/ + + function testSetKeeper() public { + vm.startPrank(owner); + + vm.expectEmit(true, true, true, true); + emit KeeperUpdated(address(vault), address(0), keeper); + controller.setKeeper(address(vault), keeper); + + assertEq(controller.keepers(address(vault)), keeper); + vm.stopPrank(); + } + + function testSetKeeperUnauthorized() public { + vm.prank(alice); + vm.expectRevert("UNAUTHORIZED"); + controller.setKeeper(address(vault), keeper); + } + + function testUpdatePriceAsKeeper() public { + // Setup keeper + vm.prank(owner); + controller.setKeeper(address(vault), keeper); + + // Update price as keeper + vm.prank(keeper); + controller.updatePrice( + OracleVaultController.PriceUpdate({ + vault: address(vault), + asset: address(asset), + shareValueInAssets: INITIAL_PRICE, + assetValueInShares: ONE + }) + ); + } + + /*////////////////////////////////////////////////////////////// + LIMIT MANAGEMENT TESTS + //////////////////////////////////////////////////////////////*/ + + function testSetLimit() public { + Limit memory limit = Limit({ + jump: 0.1e18, // 10% + drawdown: 0.2e18 // 20% + }); + + vm.startPrank(owner); + + vm.expectEmit(true, true, true, true); + emit LimitUpdated(address(vault), Limit(0, 0), limit); + controller.setLimit(address(vault), limit); + + Limit memory storedLimit = controller.limits(address(vault)); + assertEq(storedLimit.jump, limit.jump); + assertEq(storedLimit.drawdown, limit.drawdown); + vm.stopPrank(); + } + + function testSetLimitUnauthorized() public { + Limit memory limit = Limit({ + jump: 0.1e18, + drawdown: 0.2e18 + }); + + vm.prank(alice); + vm.expectRevert("UNAUTHORIZED"); + controller.setLimit(address(vault), limit); + } + + function testSetLimitInvalid() public { + Limit memory limit = Limit({ + jump: 1.1e18, // 110% - invalid + drawdown: 0.2e18 + }); + + vm.prank(owner); + vm.expectRevert("Invalid limit"); + controller.setLimit(address(vault), limit); + } + + function testSetMultipleLimits() public { + address[] memory vaults = new address[](2); + vaults[0] = address(vault); + vaults[1] = address(0x123); + + Limit[] memory limits = new Limit[](2); + limits[0] = Limit({ + jump: 0.1e18, + drawdown: 0.2e18 + }); + limits[1] = Limit({ + jump: 0.15e18, + drawdown: 0.25e18 + }); + + vm.prank(owner); + controller.setLimits(vaults, limits); + + Limit memory storedLimit0 = controller.limits(vaults[0]); + Limit memory storedLimit1 = controller.limits(vaults[1]); + + assertEq(storedLimit0.jump, limits[0].jump); + assertEq(storedLimit0.drawdown, limits[0].drawdown); + assertEq(storedLimit1.jump, limits[1].jump); + assertEq(storedLimit1.drawdown, limits[1].drawdown); + } + + /*////////////////////////////////////////////////////////////// + PRICE UPDATE TESTS + //////////////////////////////////////////////////////////////*/ + + function testUpdatePrice() public { + vm.prank(owner); + controller.setLimit(address(vault), Limit({ + jump: 0.1e18, // 10% + drawdown: 0.2e18 // 20% + })); + + vm.prank(owner); + controller.updatePrice( + OracleVaultController.PriceUpdate({ + vault: address(vault), + asset: address(asset), + shareValueInAssets: INITIAL_PRICE, + assetValueInShares: ONE + }) + ); + + assertEq( + oracle.prices(address(vault), address(asset)), + INITIAL_PRICE + ); + } + + function testUpdatePriceWithJumpUp() public { + // Set limit + vm.prank(owner); + controller.setLimit(address(vault), Limit({ + jump: 0.1e18, // 10% + drawdown: 0.2e18 // 20% + })); + + // Update price with >10% jump up + uint256 newPrice = INITIAL_PRICE.mulDivUp(11, 10); // 11% increase + + vm.prank(owner); + controller.updatePrice( + OracleVaultController.PriceUpdate({ + vault: address(vault), + asset: address(asset), + shareValueInAssets: newPrice, + assetValueInShares: ONE.mulDivDown(ONE, newPrice) + }) + ); + + assertTrue(vault.paused()); + } + + function testUpdatePriceWithJumpDown() public { + // Set limit + vm.prank(owner); + controller.setLimit(address(vault), Limit({ + jump: 0.1e18, // 10% + drawdown: 0.2e18 // 20% + })); + + // Update price with >10% jump down + uint256 newPrice = INITIAL_PRICE.mulDivDown(89, 100); // 11% decrease + + vm.prank(owner); + controller.updatePrice( + OracleVaultController.PriceUpdate({ + vault: address(vault), + asset: address(asset), + shareValueInAssets: newPrice, + assetValueInShares: ONE.mulDivDown(ONE, newPrice) + }) + ); + + assertTrue(vault.paused()); + } + + function testUpdatePriceWithDrawdown() public { + // Set limit and initial high water mark + vm.startPrank(owner); + controller.setLimit(address(vault), Limit({ + jump: 0.1e18, // 10% + drawdown: 0.2e18 // 20% + })); + + // Set initial price and HWM + controller.updatePrice( + OracleVaultController.PriceUpdate({ + vault: address(vault), + asset: address(asset), + shareValueInAssets: INITIAL_PRICE, + assetValueInShares: ONE + }) + ); + vm.stopPrank(); + + // Update price with >20% drawdown from HWM + uint256 newPrice = INITIAL_PRICE.mulDivDown(79, 100); // 21% decrease + + vm.prank(owner); + controller.updatePrice( + OracleVaultController.PriceUpdate({ + vault: address(vault), + asset: address(asset), + shareValueInAssets: newPrice, + assetValueInShares: ONE.mulDivDown(ONE, newPrice) + }) + ); + + assertTrue(vault.paused()); + } + + function testUpdateMultiplePrices() public { + OracleVaultController.PriceUpdate[] memory updates = new OracleVaultController.PriceUpdate[](2); + + updates[0] = OracleVaultController.PriceUpdate({ + vault: address(vault), + asset: address(asset), + shareValueInAssets: INITIAL_PRICE, + assetValueInShares: ONE + }); + + updates[1] = OracleVaultController.PriceUpdate({ + vault: address(0x123), + asset: address(asset), + shareValueInAssets: INITIAL_PRICE, + assetValueInShares: ONE + }); + + vm.prank(owner); + controller.updatePrices(updates); + + assertEq( + oracle.prices(address(vault), address(asset)), + INITIAL_PRICE + ); + assertEq( + oracle.prices(address(0x123), address(asset)), + INITIAL_PRICE + ); + } + + /*////////////////////////////////////////////////////////////// + ORACLE OWNERSHIP TESTS + //////////////////////////////////////////////////////////////*/ + + function testAcceptOracleOwnership() public { + vm.prank(owner); + controller.acceptOracleOwnership(); + } + + function testAcceptOracleOwnershipUnauthorized() public { + vm.prank(alice); + vm.expectRevert("UNAUTHORIZED"); + controller.acceptOracleOwnership(); + } +} + +// Mock contracts needed for testing +contract MockPushOracle { + mapping(address => mapping(address => uint256)) public prices; + + function setPrice( + address base, + address quote, + uint256 bqPrice, + uint256 qbPrice + ) external { + prices[base][quote] = bqPrice; + } + + function setPrices( + address[] memory bases, + address[] memory quotes, + uint256[] memory bqPrices, + uint256[] memory qbPrices + ) external { + for (uint256 i = 0; i < bases.length; i++) { + prices[bases[i]][quotes[i]] = bqPrices[i]; + } + } +} + +contract MockPausable { + bool public paused; + + function pause() external { + paused = true; + } + + function unpause() external { + paused = false; + } +} \ No newline at end of file diff --git a/test/peripheral/gnosis/transactionGuard/MainGuard.t.sol b/test/peripheral/gnosis/transactionGuard/MainGuard.t.sol new file mode 100644 index 00000000..3ef4b49c --- /dev/null +++ b/test/peripheral/gnosis/transactionGuard/MainGuard.t.sol @@ -0,0 +1,248 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.25; + +import {Test} from "forge-std/Test.sol"; +import {MainTransactionGuard} from "src/peripheral/gnosis/transactionGuard/MainTransactionGuard.sol"; +import {IGnosisSafe} from "src/interfaces/external/IGnosisSafe.sol"; + +contract MainGuardTest is Test { + // Constants + address constant SAFE = 0x3C99dEa58119DE3962253aea656e61E5fBE21613; + address constant SAFE_OWNER = 0x9E1028F5F1D5eDE59748FFceE5532509976840E0; // Replace with actual owner + uint256 constant FORK_BLOCK = 164793616; // Replace with appropriate block + + // Contracts + IGnosisSafe safe; + MainTransactionGuard guard; + + function setUp() public { + // Fork arbitrum at specific block + vm.createSelectFork("arbitrum", FORK_BLOCK); + + // Label addresses for better trace output + vm.label(SAFE, "Gnosis Safe"); + vm.label(SAFE_OWNER, "Safe Owner"); + + // Get Safe contract instance + safe = IGnosisSafe(SAFE); + + // Deploy guard + guard = new MainTransactionGuard(SAFE_OWNER); + vm.label(address(guard), "Transaction Guard"); + } + + function testSetGuard() public { + // Prepare setGuard transaction data + bytes memory data = abi.encodeWithSignature( + "setGuard(address)", + address(guard) + ); + + // Get current nonce + uint256 nonce = safe.nonce(); + + // Calculate transaction hash + bytes32 txHash = safe.getTransactionHash( + address(safe), // to + 0, // value + data, // data + Enum.Operation.Call, // operation + 0, // safeTxGas + 0, // baseGas + 0, // gasPrice + address(0), // gasToken + address(0), // refundReceiver + nonce // nonce + ); + + // Get signature from owner (assuming single owner for simplicity) + (uint8 v, bytes32 r, bytes32 s) = vm.sign( + uint256(keccak256(abi.encodePacked(SAFE_OWNER))), // Owner's private key + txHash + ); + bytes memory signature = abi.encodePacked(r, s, v); + + // Execute transaction + vm.prank(SAFE_OWNER); + safe.execTransaction( + address(safe), // to + 0, // value + data, // data + Enum.Operation.Call, // operation + 0, // safeTxGas + 0, // baseGas + 0, // gasPrice + address(0), // gasToken + payable(address(0)), // refundReceiver + signature // signatures + ); + + // Verify guard was set + assertEq(safe.getGuard(), address(guard)); + } + + function testGuardFunctionality() public { + // First set the guard + testSetGuard(); + + // Test a basic transaction through the guard + bytes memory transferData = abi.encodeWithSignature( + "transfer(address,uint256)", + address(0x123), // random recipient + 1 ether + ); + + uint256 nonce = safe.nonce(); + + bytes32 txHash = safe.getTransactionHash( + address(0x123), // to + 0, // value + transferData, // data + Enum.Operation.Call, // operation + 0, // safeTxGas + 0, // baseGas + 0, // gasPrice + address(0), // gasToken + address(0), // refundReceiver + nonce // nonce + ); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign( + uint256(keccak256(abi.encodePacked(SAFE_OWNER))), + txHash + ); + bytes memory signature = abi.encodePacked(r, s, v); + + // Execute transaction through guard + vm.prank(SAFE_OWNER); + safe.execTransaction( + address(0x123), // to + 0, // value + transferData, // data + Enum.Operation.Call, // operation + 0, // safeTxGas + 0, // baseGas + 0, // gasPrice + address(0), // gasToken + payable(address(0)), // refundReceiver + signature // signatures + ); + } + + function testAddHook() public { + // First set the guard + testSetGuard(); + + // Deploy a mock hook + address mockHook = address(0x123); + + // Prepare addHook transaction data + bytes memory data = abi.encodeWithSignature( + "addHook(address)", + mockHook + ); + + uint256 nonce = safe.nonce(); + + bytes32 txHash = safe.getTransactionHash( + address(guard), // to + 0, // value + data, // data + Enum.Operation.Call, // operation + 0, // safeTxGas + 0, // baseGas + 0, // gasPrice + address(0), // gasToken + address(0), // refundReceiver + nonce // nonce + ); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign( + uint256(keccak256(abi.encodePacked(SAFE_OWNER))), + txHash + ); + bytes memory signature = abi.encodePacked(r, s, v); + + // Add hook through safe transaction + vm.prank(SAFE_OWNER); + safe.execTransaction( + address(guard), // to + 0, // value + data, // data + Enum.Operation.Call, // operation + 0, // safeTxGas + 0, // baseGas + 0, // gasPrice + address(0), // gasToken + payable(address(0)), // refundReceiver + signature // signatures + ); + + // Verify hook was added + assertTrue(guard.isHook(mockHook)); + } + + function testRemoveHook() public { + // First add a hook + testAddHook(); + + address mockHook = address(0x123); + + // Find previous hook in linked list + address prevHook = address(0x1); // SENTINEL_HOOK + address[] memory hooks = guard.getHooks(); + for (uint256 i = 0; i < hooks.length; i++) { + if (hooks[i] == mockHook) { + break; + } + prevHook = hooks[i]; + } + + // Prepare swapHook transaction data (removing by swapping with 0 address) + bytes memory data = abi.encodeWithSignature( + "swapHook(address,address,address)", + prevHook, + mockHook, + address(0) + ); + + uint256 nonce = safe.nonce(); + + bytes32 txHash = safe.getTransactionHash( + address(guard), // to + 0, // value + data, // data + Enum.Operation.Call, // operation + 0, // safeTxGas + 0, // baseGas + 0, // gasPrice + address(0), // gasToken + address(0), // refundReceiver + nonce // nonce + ); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign( + uint256(keccak256(abi.encodePacked(SAFE_OWNER))), + txHash + ); + bytes memory signature = abi.encodePacked(r, s, v); + + // Remove hook through safe transaction + vm.prank(SAFE_OWNER); + safe.execTransaction( + address(guard), // to + 0, // value + data, // data + Enum.Operation.Call, // operation + 0, // safeTxGas + 0, // baseGas + 0, // gasPrice + address(0), // gasToken + payable(address(0)), // refundReceiver + signature // signatures + ); + + // Verify hook was removed + assertFalse(guard.isHook(mockHook)); + } +} diff --git a/test/vaults/multisig/phase1/AsyncVault.t.sol b/test/vaults/multisig/phase1/AsyncVault.t.sol new file mode 100644 index 00000000..581ca24e --- /dev/null +++ b/test/vaults/multisig/phase1/AsyncVault.t.sol @@ -0,0 +1,324 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.25; + +import {Test} from "forge-std/Test.sol"; +import {MockERC20} from "test/mocks/MockERC20.sol"; +import {AsyncVault, InitializeParams, Limits, Fees, Bounds} from "src/vaults/multisig/phase1/AsyncVault.sol"; +import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; + +contract MockAsyncVault is AsyncVault { + constructor( + InitializeParams memory params + ) AsyncVault(params) {} + + function totalAssets() public view override returns (uint256) { + return asset.balanceOf(address(this)); + } +} + +contract AsyncVaultTest is Test { + using FixedPointMathLib for uint256; + + MockAsyncVault vault; + MockERC20 asset; + + address owner = address(0x1); + address alice = address(0x2); + address bob = address(0x3); + address feeRecipient = address(0x4); + + uint256 constant INITIAL_DEPOSIT = 1000e18; + uint256 constant ONE = 1e18; + + event FeesUpdated(Fees prev, Fees next); + event LimitsUpdated(Limits prev, Limits next); + + function setUp() public { + vm.label(owner, "owner"); + vm.label(alice, "alice"); + vm.label(bob, "bob"); + vm.label(feeRecipient, "feeRecipient"); + + asset = new MockERC20("Test Token", "TEST", 18); + + InitializeParams memory params = InitializeParams({ + asset: address(asset), + name: "Vault Token", + symbol: "vTEST", + owner: owner, + limits: Limits({ + depositLimit: 10000e18, + minAmount: 1e18 + }), + fees: Fees({ + performanceFee: 1e17, // 10% + managementFee: 1e16, // 1% + withdrawalIncentive: 1e16, // 1% + feesUpdatedAt: uint64(block.timestamp), + highWaterMark: ONE, + feeRecipient: feeRecipient + }) + }); + + vault = new MockAsyncVault(params); + + // Setup initial state + asset.mint(alice, INITIAL_DEPOSIT); + vm.startPrank(alice); + asset.approve(address(vault), type(uint256).max); + vault.deposit(INITIAL_DEPOSIT, alice); + vm.stopPrank(); + } + + /*////////////////////////////////////////////////////////////// + ACCOUNTING TESTS + //////////////////////////////////////////////////////////////*/ + + function testPreviewDeposit() public { + uint256 depositAmount = 100e18; + uint256 expectedShares = vault.convertToShares(depositAmount); + assertEq(vault.previewDeposit(depositAmount), expectedShares); + } + + function testPreviewDepositBelowMin() public { + uint256 depositAmount = 0.5e18; + assertEq(vault.previewDeposit(depositAmount), 0); + } + + function testPreviewMint() public { + uint256 mintAmount = 100e18; + uint256 expectedAssets = vault.convertToAssets(mintAmount); + assertEq(vault.previewMint(mintAmount), expectedAssets); + } + + function testPreviewMintBelowMin() public { + uint256 mintAmount = 0.5e18; + assertEq(vault.previewMint(mintAmount), 0); + } + + function testConvertToLowBoundAssets() public { + // Set bounds + Bounds memory bounds = Bounds({ + upper: 1.1e18, // 110% + lower: 0.9e18 // 90% + }); + vm.prank(owner); + vault.setBounds(bounds); + + uint256 shares = 100e18; + uint256 expectedAssets = vault.totalAssets().mulDivDown(bounds.lower, 1e18); + uint256 expectedShares = shares.mulDivDown(expectedAssets, vault.totalSupply()); + + assertEq(vault.convertToLowBoundAssets(shares), expectedShares); + } + + /*////////////////////////////////////////////////////////////// + DEPOSIT/WITHDRAWAL LIMIT TESTS + //////////////////////////////////////////////////////////////*/ + + function testMaxDeposit() public { + uint256 depositLimit = vault.limits().depositLimit; + uint256 currentAssets = vault.totalAssets(); + assertEq(vault.maxDeposit(alice), depositLimit - currentAssets); + } + + function testMaxDepositWhenPaused() public { + vm.prank(owner); + vault.pause(); + assertEq(vault.maxDeposit(alice), 0); + } + + function testMaxMint() public { + uint256 depositLimit = vault.limits().depositLimit; + uint256 currentAssets = vault.totalAssets(); + uint256 expectedShares = vault.convertToShares(depositLimit - currentAssets); + assertEq(vault.maxMint(alice), expectedShares); + } + + function testMaxMintWhenPaused() public { + vm.prank(owner); + vault.pause(); + assertEq(vault.maxMint(alice), 0); + } + + /*////////////////////////////////////////////////////////////// + DEPOSIT/WITHDRAWAL TESTS + //////////////////////////////////////////////////////////////*/ + + function testDeposit() public { + uint256 depositAmount = 100e18; + vm.startPrank(alice); + uint256 shares = vault.deposit(depositAmount); + assertGt(shares, 0); + assertEq(vault.balanceOf(alice), INITIAL_DEPOSIT + shares); + vm.stopPrank(); + } + + function testMint() public { + uint256 mintAmount = 100e18; + vm.startPrank(alice); + uint256 assets = vault.mint(mintAmount); + assertGt(assets, 0); + assertEq(vault.balanceOf(alice), INITIAL_DEPOSIT + mintAmount); + vm.stopPrank(); + } + + /*////////////////////////////////////////////////////////////// + REDEEM REQUEST TESTS + //////////////////////////////////////////////////////////////*/ + + function testRequestRedeem() public { + uint256 redeemAmount = 100e18; + vm.startPrank(alice); + vault.approve(address(vault), redeemAmount); + uint256 requestId = vault.requestRedeem(redeemAmount, alice, alice); + assertGt(requestId, 0); + vm.stopPrank(); + } + + function testRequestRedeemBelowMin() public { + uint256 redeemAmount = 0.5e18; + vm.startPrank(alice); + vault.approve(address(vault), redeemAmount); + vm.expectRevert("ERC7540Vault/min-amount"); + vault.requestRedeem(redeemAmount, alice, alice); + vm.stopPrank(); + } + + function testFulfillRedeem() public { + uint256 redeemAmount = 100e18; + + // Setup redeem request + vm.startPrank(alice); + vault.approve(address(vault), redeemAmount); + uint256 requestId = vault.requestRedeem(redeemAmount, alice, alice); + vm.stopPrank(); + + // Fulfill request + vm.startPrank(owner); + uint256 assets = vault.fulfillRedeem(redeemAmount, alice); + assertGt(assets, 0); + vm.stopPrank(); + } + + function testFulfillMultipleRedeems() public { + uint256 redeemAmount = 100e18; + + // Setup redeem requests + vm.startPrank(alice); + vault.approve(address(vault), redeemAmount * 2); + uint256 request1 = vault.requestRedeem(redeemAmount, alice, alice); + uint256 request2 = vault.requestRedeem(redeemAmount, alice, alice); + vm.stopPrank(); + + uint256[] memory shares = new uint256[](2); + shares[0] = redeemAmount; + shares[1] = redeemAmount; + + address[] memory controllers = new address[](2); + controllers[0] = alice; + controllers[1] = alice; + + vm.prank(owner); + uint256 totalAssets = vault.fulfillMultipleRedeems(shares, controllers); + assertGt(totalAssets, 0); + } + + /*////////////////////////////////////////////////////////////// + FEE TESTS + //////////////////////////////////////////////////////////////*/ + + function testAccruedFees() public { + // Simulate some yield + asset.mint(address(vault), 100e18); + + vm.warp(block.timestamp + 365 days); + + uint256 fees = vault.accruedFees(); + assertGt(fees, 0); + } + + function testSetFees() public { + Fees memory newFees = Fees({ + performanceFee: 0.15e18, // 15% + managementFee: 0.02e18, // 2% + withdrawalIncentive: 0.02e18, // 2% + feesUpdatedAt: uint64(block.timestamp), + highWaterMark: ONE, + feeRecipient: feeRecipient + }); + + vm.prank(owner); + vault.setFees(newFees); + + Fees memory currentFees = vault.fees(); + assertEq(currentFees.performanceFee, newFees.performanceFee); + assertEq(currentFees.managementFee, newFees.managementFee); + assertEq(currentFees.withdrawalIncentive, newFees.withdrawalIncentive); + } + + function testSetFeesRevertsTooHigh() public { + Fees memory newFees = Fees({ + performanceFee: 0.3e18, // 30% - too high + managementFee: 0.02e18, + withdrawalIncentive: 0.02e18, + feesUpdatedAt: uint64(block.timestamp), + highWaterMark: ONE, + feeRecipient: feeRecipient + }); + + vm.prank(owner); + vm.expectRevert(AsyncVault.Misconfigured.selector); + vault.setFees(newFees); + } + + function testTakeFees() public { + // Simulate some yield + asset.mint(address(vault), 100e18); + + vm.warp(block.timestamp + 365 days); + + uint256 feeRecipientBalanceBefore = vault.balanceOf(feeRecipient); + + vm.prank(owner); + vault.takeFees(); + + assertGt(vault.balanceOf(feeRecipient), feeRecipientBalanceBefore); + } + + /*////////////////////////////////////////////////////////////// + LIMIT TESTS + //////////////////////////////////////////////////////////////*/ + + function testSetLimits() public { + Limits memory newLimits = Limits({ + depositLimit: 20000e18, + minAmount: 2e18 + }); + + vm.prank(owner); + vault.setLimits(newLimits); + + Limits memory currentLimits = vault.limits(); + assertEq(currentLimits.depositLimit, newLimits.depositLimit); + assertEq(currentLimits.minAmount, newLimits.minAmount); + } + + /*////////////////////////////////////////////////////////////// + BOUNDS TESTS + //////////////////////////////////////////////////////////////*/ + + function testSetBounds() public { + Bounds memory newBounds = Bounds({ + upper: 1.2e18, // 120% + lower: 0.8e18 // 80% + }); + + vm.prank(owner); + vault.setBounds(newBounds); + + Bounds memory currentBounds = vault.bounds(); + assertEq(currentBounds.upper, newBounds.upper); + assertEq(currentBounds.lower, newBounds.lower); + } +} \ No newline at end of file diff --git a/test/vaults/multisig/phase1/BaseControlledAsyncRedeem.t.sol b/test/vaults/multisig/phase1/BaseControlledAsyncRedeem.t.sol new file mode 100644 index 00000000..5470ef7f --- /dev/null +++ b/test/vaults/multisig/phase1/BaseControlledAsyncRedeem.t.sol @@ -0,0 +1,333 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.25; + +import {Test} from "forge-std/Test.sol"; +import {MockERC20} from "test/mocks/MockERC20.sol"; +import {BaseControlledAsyncRedeem} from "src/vaults/multisig/phase1/BaseControlledAsyncRedeem.sol"; +import {RequestBalance} from "src/vaults/multisig/phase1/BaseControlledAsyncRedeem.sol"; + +contract MockControlledAsyncRedeem is BaseControlledAsyncRedeem { + constructor( + address _owner, + address _asset, + string memory _name, + string memory _symbol + ) BaseERC7540(_owner, _asset, _name, _symbol) {} + + function totalAssets() public view override returns (uint256) { + return asset.balanceOf(address(this)); + } +} + +contract BaseControlledAsyncRedeemTest is Test { + MockControlledAsyncRedeem vault; + MockERC20 asset; + + address owner = address(0x1); + address alice = address(0x2); + address bob = address(0x3); + address charlie = address(0x4); + + uint256 constant INITIAL_DEPOSIT = 1000e18; + uint256 constant REQUEST_ID = 1; + + event RedeemRequest( + address indexed controller, + address indexed owner, + uint256 indexed requestId, + address operator, + uint256 shares + ); + + event RedeemRequestCanceled( + address indexed controller, + address indexed receiver, + uint256 shares + ); + + function setUp() public { + vm.label(owner, "owner"); + vm.label(alice, "alice"); + vm.label(bob, "bob"); + vm.label(charlie, "charlie"); + + asset = new MockERC20("Test Token", "TEST", 18); + vault = new MockControlledAsyncRedeem(owner, address(asset), "Vault Token", "vTEST"); + + // Setup initial state + asset.mint(alice, INITIAL_DEPOSIT); + vm.startPrank(alice); + asset.approve(address(vault), type.max); + vault.deposit(INITIAL_DEPOSIT, alice); + vm.stopPrank(); + } + + /*////////////////////////////////////////////////////////////// + REQUEST REDEEM TESTS + //////////////////////////////////////////////////////////////*/ + + function testRequestRedeem() public { + uint256 redeemAmount = 100e18; + + vm.startPrank(alice); + vault.approve(address(vault), redeemAmount); + + vm.expectEmit(true, true, true, true); + emit RedeemRequest(alice, alice, REQUEST_ID, alice, redeemAmount); + vault.requestRedeem(redeemAmount, alice, alice); + + RequestBalance memory balance = vault.requestBalances(alice); + assertEq(balance.pendingShares, redeemAmount); + assertEq(balance.requestTime, block.timestamp); + assertEq(balance.claimableShares, 0); + assertEq(balance.claimableAssets, 0); + vm.stopPrank(); + } + + function testRequestRedeemWithOperator() public { + uint256 redeemAmount = 100e18; + + vm.prank(alice); + vault.setOperator(bob, true); + + vm.startPrank(bob); + vault.approve(address(vault), redeemAmount); + + vm.expectEmit(true, true, true, true); + emit RedeemRequest(alice, alice, REQUEST_ID, bob, redeemAmount); + vault.requestRedeem(redeemAmount, alice, alice); + vm.stopPrank(); + } + + function testFailRequestRedeemUnauthorized() public { + vm.prank(bob); + vault.requestRedeem(100e18, alice, alice); + } + + function testFailRequestRedeemInsufficientBalance() public { + vm.prank(alice); + vault.requestRedeem(INITIAL_DEPOSIT + 1, alice, alice); + } + + /*////////////////////////////////////////////////////////////// + CANCEL REDEEM REQUEST TESTS + //////////////////////////////////////////////////////////////*/ + + function testCancelRedeemRequest() public { + uint256 redeemAmount = 100e18; + + // Setup redeem request + vm.startPrank(alice); + vault.approve(address(vault), redeemAmount); + vault.requestRedeem(redeemAmount, alice, alice); + + vm.expectEmit(true, true, true, true); + emit RedeemRequestCanceled(alice, alice, redeemAmount); + vault.cancelRedeemRequest(alice); + + RequestBalance memory balance = vault.requestBalances(alice); + assertEq(balance.pendingShares, 0); + assertEq(balance.requestTime, 0); + vm.stopPrank(); + } + + function testCancelRedeemRequestWithReceiver() public { + uint256 redeemAmount = 100e18; + + // Setup redeem request + vm.startPrank(alice); + vault.approve(address(vault), redeemAmount); + vault.requestRedeem(redeemAmount, alice, alice); + + vm.expectEmit(true, true, true, true); + emit RedeemRequestCanceled(alice, bob, redeemAmount); + vault.cancelRedeemRequest(alice, bob); + vm.stopPrank(); + } + + function testFailCancelRedeemRequestUnauthorized() public { + vm.prank(bob); + vault.cancelRedeemRequest(alice); + } + + /*////////////////////////////////////////////////////////////// + FULFILL REDEEM REQUEST TESTS + //////////////////////////////////////////////////////////////*/ + + function testFulfillRedeem() public { + uint256 redeemAmount = 100e18; + + // Setup redeem request + vm.startPrank(alice); + vault.approve(address(vault), redeemAmount); + vault.requestRedeem(redeemAmount, alice, alice); + vm.stopPrank(); + + // Fulfill request + vm.startPrank(owner); + asset.mint(owner, redeemAmount); + asset.approve(address(vault), redeemAmount); + uint256 assets = vault.fulfillRedeem(redeemAmount, alice); + + RequestBalance memory balance = vault.requestBalances(alice); + assertEq(balance.pendingShares, 0); + assertEq(balance.claimableShares, redeemAmount); + assertEq(balance.claimableAssets, assets); + vm.stopPrank(); + } + + function testPartialFulfillRedeem() public { + uint256 redeemAmount = 100e18; + uint256 partialAmount = 60e18; + + // Setup redeem request + vm.startPrank(alice); + vault.approve(address(vault), redeemAmount); + vault.requestRedeem(redeemAmount, alice, alice); + vm.stopPrank(); + + // Partially fulfill request + vm.startPrank(owner); + asset.mint(owner, partialAmount); + asset.approve(address(vault), partialAmount); + uint256 assets = vault.fulfillRedeem(partialAmount, alice); + + RequestBalance memory balance = vault.requestBalances(alice); + assertEq(balance.pendingShares, redeemAmount - partialAmount); + assertEq(balance.claimableShares, partialAmount); + assertEq(balance.claimableAssets, assets); + vm.stopPrank(); + } + + /*////////////////////////////////////////////////////////////// + WITHDRAW TESTS + //////////////////////////////////////////////////////////////*/ + + function testWithdraw() public { + uint256 redeemAmount = 100e18; + + // Setup and fulfill redeem request + vm.startPrank(alice); + vault.approve(address(vault), redeemAmount); + vault.requestRedeem(redeemAmount, alice, alice); + vm.stopPrank(); + + vm.startPrank(owner); + asset.mint(owner, redeemAmount); + asset.approve(address(vault), redeemAmount); + uint256 assets = vault.fulfillRedeem(redeemAmount, alice); + vm.stopPrank(); + + // Withdraw + vm.prank(alice); + uint256 shares = vault.withdraw(assets, alice, alice); + + assertEq(shares, redeemAmount); + assertEq(asset.balanceOf(alice), assets); + } + + function testWithdrawWithOperator() public { + uint256 redeemAmount = 100e18; + + // Setup operator + vm.prank(alice); + vault.setOperator(bob, true); + + // Setup and fulfill redeem request + vm.startPrank(alice); + vault.approve(address(vault), redeemAmount); + vault.requestRedeem(redeemAmount, alice, alice); + vm.stopPrank(); + + vm.startPrank(owner); + asset.mint(owner, redeemAmount); + asset.approve(address(vault), redeemAmount); + uint256 assets = vault.fulfillRedeem(redeemAmount, alice); + vm.stopPrank(); + + // Withdraw using operator + vm.prank(bob); + vault.withdraw(assets, bob, alice); + + assertEq(asset.balanceOf(bob), assets); + } + + /*////////////////////////////////////////////////////////////// + VIEW FUNCTION TESTS + //////////////////////////////////////////////////////////////*/ + + function testPendingRedeemRequest() public { + uint256 redeemAmount = 100e18; + + vm.startPrank(alice); + vault.approve(address(vault), redeemAmount); + vault.requestRedeem(redeemAmount, alice, alice); + vm.stopPrank(); + + assertEq(vault.pendingRedeemRequest(REQUEST_ID, alice), redeemAmount); + } + + function testClaimableRedeemRequest() public { + uint256 redeemAmount = 100e18; + + // Setup and fulfill redeem request + vm.startPrank(alice); + vault.approve(address(vault), redeemAmount); + vault.requestRedeem(redeemAmount, alice, alice); + vm.stopPrank(); + + vm.startPrank(owner); + asset.mint(owner, redeemAmount); + asset.approve(address(vault), redeemAmount); + vault.fulfillRedeem(redeemAmount, alice); + vm.stopPrank(); + + assertEq(vault.claimableRedeemRequest(REQUEST_ID, alice), redeemAmount); + } + + function testMaxWithdraw() public { + uint256 redeemAmount = 100e18; + + // Setup and fulfill redeem request + vm.startPrank(alice); + vault.approve(address(vault), redeemAmount); + vault.requestRedeem(redeemAmount, alice, alice); + vm.stopPrank(); + + vm.startPrank(owner); + asset.mint(owner, redeemAmount); + asset.approve(address(vault), redeemAmount); + uint256 assets = vault.fulfillRedeem(redeemAmount, alice); + vm.stopPrank(); + + assertEq(vault.maxWithdraw(alice), assets); + } + + function testMaxRedeem() public { + uint256 redeemAmount = 100e18; + + // Setup and fulfill redeem request + vm.startPrank(alice); + vault.approve(address(vault), redeemAmount); + vault.requestRedeem(redeemAmount, alice, alice); + vm.stopPrank(); + + vm.startPrank(owner); + asset.mint(owner, redeemAmount); + asset.approve(address(vault), redeemAmount); + vault.fulfillRedeem(redeemAmount, alice); + vm.stopPrank(); + + assertEq(vault.maxRedeem(alice), redeemAmount); + } + + function testPreviewWithdrawReverts() public { + vm.expectRevert("ERC7540Vault/async-flow"); + vault.previewWithdraw(100e18); + } + + function testPreviewRedeemReverts() public { + vm.expectRevert("ERC7540Vault/async-flow"); + vault.previewRedeem(100e18); + } +} \ No newline at end of file diff --git a/test/vaults/multisig/phase1/BaseERC7540.t.sol b/test/vaults/multisig/phase1/BaseERC7540.t.sol new file mode 100644 index 00000000..bde0b30d --- /dev/null +++ b/test/vaults/multisig/phase1/BaseERC7540.t.sol @@ -0,0 +1,256 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.25; + +import {Test} from "forge-std/Test.sol"; +import {MockERC20} from "test/mocks/MockERC20.sol"; +import {BaseERC7540} from "src/vaults/multisig/phase1/BaseERC7540.sol"; + +contract MockERC7540 is BaseERC7540 { + constructor( + address _owner, + address _asset, + string memory _name, + string memory _symbol + ) BaseERC7540(_owner, _asset, _name, _symbol) {} + + function totalAssets() public view override returns (uint256) { + return asset.balanceOf(address(this)); + } +} + +contract BaseERC7540Test is Test { + MockERC7540 vault; + MockERC20 asset; + + address owner = address(0x1); + address alice = address(0x2); + address bob = address(0x3); + address charlie = address(0x4); + + bytes32 constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + + event RoleUpdated(bytes32 role, address account, bool approved); + event OperatorSet(address indexed controller, address indexed operator, bool approved); + + function setUp() public { + vm.label(owner, "owner"); + vm.label(alice, "alice"); + vm.label(bob, "bob"); + vm.label(charlie, "charlie"); + + asset = new MockERC20("Test Token", "TEST", 18); + vault = new MockERC7540(owner, address(asset), "Vault Token", "vTEST"); + } + + /*////////////////////////////////////////////////////////////// + ROLE TESTS + //////////////////////////////////////////////////////////////*/ + + function testUpdateRole() public { + vm.startPrank(owner); + + vm.expectEmit(true, true, true, true); + emit RoleUpdated(PAUSER_ROLE, alice, true); + vault.updateRole(PAUSER_ROLE, alice, true); + + assertTrue(vault.hasRole(PAUSER_ROLE, alice)); + + vm.expectEmit(true, true, true, true); + emit RoleUpdated(PAUSER_ROLE, alice, false); + vault.updateRole(PAUSER_ROLE, alice, false); + + assertFalse(vault.hasRole(PAUSER_ROLE, alice)); + vm.stopPrank(); + } + + function testUpdateRoleNonOwner() public { + vm.prank(alice); + vm.expectRevert("UNAUTHORIZED"); + vault.updateRole(PAUSER_ROLE, bob, true); + } + + /*////////////////////////////////////////////////////////////// + PAUSE TESTS + //////////////////////////////////////////////////////////////*/ + + function testPause() public { + // Grant PAUSER_ROLE to alice + vm.prank(owner); + vault.updateRole(PAUSER_ROLE, alice, true); + + // Test pause as role holder + vm.prank(alice); + vault.pause(); + assertTrue(vault.paused()); + + // Test pause as owner + vm.prank(owner); + vault.unpause(); + assertFalse(vault.paused()); + vault.pause(); + assertTrue(vault.paused()); + } + + function testPauseUnauthorized() public { + vm.prank(alice); + vm.expectRevert("BaseERC7540/not-authorized"); + vault.pause(); + } + + function testUnpauseOnlyOwner() public { + // First pause + vm.prank(owner); + vault.pause(); + + // Try to unpause as non-owner + vm.prank(alice); + vm.expectRevert("UNAUTHORIZED"); + vault.unpause(); + + // Unpause as owner + vm.prank(owner); + vault.unpause(); + assertFalse(vault.paused()); + } + + /*////////////////////////////////////////////////////////////// + OPERATOR TESTS + //////////////////////////////////////////////////////////////*/ + + function testSetOperator() public { + vm.startPrank(alice); + + vm.expectEmit(true, true, true, true); + emit OperatorSet(alice, bob, true); + assertTrue(vault.setOperator(bob, true)); + assertTrue(vault.isOperator(alice, bob)); + + vm.expectEmit(true, true, true, true); + emit OperatorSet(alice, bob, false); + assertTrue(vault.setOperator(bob, false)); + assertFalse(vault.isOperator(alice, bob)); + + vm.stopPrank(); + } + + function testCannotSetSelfAsOperator() public { + vm.prank(alice); + vm.expectRevert("ERC7540Vault/cannot-set-self-as-operator"); + vault.setOperator(alice, true); + } + + /*////////////////////////////////////////////////////////////// + AUTHORIZE OPERATOR TESTS + //////////////////////////////////////////////////////////////*/ + + function testAuthorizeOperator() public { + bytes32 nonce = bytes32(uint256(1)); + uint256 deadline = block.timestamp + 1 hours; + + bytes32 domainSeparator = vault.DOMAIN_SEPARATOR(); + bytes32 structHash = keccak256( + abi.encode( + keccak256( + "AuthorizeOperator(address controller,address operator,bool approved,bytes32 nonce,uint256 deadline)" + ), + alice, + bob, + true, + nonce, + deadline + ) + ); + + bytes32 hash = keccak256( + abi.encodePacked("\x19\x01", domainSeparator, structHash) + ); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, hash); // alice's private key + bytes memory signature = abi.encodePacked(r, s, v); + + assertTrue( + vault.authorizeOperator( + alice, + bob, + true, + nonce, + deadline, + signature + ) + ); + assertTrue(vault.isOperator(alice, bob)); + } + + function testAuthorizeOperatorExpired() public { + bytes32 nonce = bytes32(uint256(1)); + uint256 deadline = block.timestamp - 1; + + vm.expectRevert("ERC7540Vault/expired"); + vault.authorizeOperator( + alice, + bob, + true, + nonce, + deadline, + new bytes(65) + ); + } + + function testAuthorizeOperatorUsedNonce() public { + bytes32 nonce = bytes32(uint256(1)); + uint256 deadline = block.timestamp + 1 hours; + + // First use + bytes32 domainSeparator = vault.DOMAIN_SEPARATOR(); + bytes32 structHash = keccak256( + abi.encode( + keccak256( + "AuthorizeOperator(address controller,address operator,bool approved,bytes32 nonce,uint256 deadline)" + ), + alice, + bob, + true, + nonce, + deadline + ) + ); + + bytes32 hash = keccak256( + abi.encodePacked("\x19\x01", domainSeparator, structHash) + ); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, hash); + bytes memory signature = abi.encodePacked(r, s, v); + + vault.authorizeOperator( + alice, + bob, + true, + nonce, + deadline, + signature + ); + + // Try to use same nonce again + vm.expectRevert("ERC7540Vault/authorization-used"); + vault.authorizeOperator( + alice, + bob, + true, + nonce, + deadline, + signature + ); + } + + /*////////////////////////////////////////////////////////////// + ERC165 TESTS + //////////////////////////////////////////////////////////////*/ + + function testSupportsInterface() public { + assertTrue(vault.supportsInterface(type(IERC7575).interfaceId)); + assertTrue(vault.supportsInterface(type(IERC7540Operator).interfaceId)); + assertTrue(vault.supportsInterface(type(IERC165).interfaceId)); + assertFalse(vault.supportsInterface(bytes4(0xdeadbeef))); + } +} \ No newline at end of file diff --git a/test/vaults/multisig/phase1/OracleVault.t.sol b/test/vaults/multisig/phase1/OracleVault.t.sol new file mode 100644 index 00000000..9b9ca253 --- /dev/null +++ b/test/vaults/multisig/phase1/OracleVault.t.sol @@ -0,0 +1,288 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.25; + +import {Test} from "forge-std/Test.sol"; +import {MockERC20} from "test/mocks/MockERC20.sol"; +import {MockOracle} from "test/mocks/MockOracle.sol"; +import {OracleVault, InitializeParams} from "src/vaults/multisig/phase1/OracleVault.sol"; +import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; + +contract OracleVaultTest is Test { + using FixedPointMathLib for uint256; + + OracleVault vault; + MockERC20 asset; + MockERC20 share; + MockOracle oracle; + + address owner = address(0x1); + address alice = address(0x2); + address bob = address(0x3); + address multisig = address(0x4); + + uint256 constant INITIAL_DEPOSIT = 1000e18; + uint256 constant ONE = 1e18; + + function setUp() public { + vm.label(owner, "owner"); + vm.label(alice, "alice"); + vm.label(bob, "bob"); + vm.label(multisig, "multisig"); + + asset = new MockERC20("Test Token", "TEST", 18); + share = new MockERC20("Share Token", "SHARE", 18); + oracle = new MockOracle(); + + // Set initial oracle price (1:1 for simplicity) + oracle.setPrice(ONE); + + InitializeParams memory params = InitializeParams({ + asset: address(asset), + name: "Vault Token", + symbol: "vTEST", + owner: owner, + limits: Limits({ + depositLimit: 10000e18, + minAmount: 1e18 + }), + fees: Fees({ + performanceFee: 1e17, // 10% + managementFee: 1e16, // 1% + withdrawalIncentive: 1e16, // 1% + feesUpdatedAt: uint64(block.timestamp), + highWaterMark: ONE, + feeRecipient: feeRecipient + }), + multisig: multisig + }); + + vault = new OracleVault(params, address(oracle), multisig); + + // Setup initial state + asset.mint(alice, INITIAL_DEPOSIT); + vm.startPrank(alice); + asset.approve(address(vault), type(uint256).max); + vault.deposit(INITIAL_DEPOSIT, alice); + vm.stopPrank(); + } + + /*////////////////////////////////////////////////////////////// + INITIALIZATION TESTS + //////////////////////////////////////////////////////////////*/ + + function testInitialization() public { + assertEq(address(vault.oracle()), address(oracle)); + assertEq(vault.multisig(), multisig); + } + + function testInitializationWithZeroMultisig() public { + InitializeParams memory params = InitializeParams({ + asset: address(asset), + name: "Vault Token", + symbol: "vTEST", + owner: owner, + limits: Limits({ + depositLimit: 10000e18, + minAmount: 1e18 + }), + fees: Fees({ + performanceFee: 1e17, + managementFee: 1e16, + withdrawalIncentive: 1e16, + feesUpdatedAt: uint64(block.timestamp), + highWaterMark: ONE, + feeRecipient: feeRecipient + }), + multisig: address(0) + }); + + vm.expectRevert(OracleVault.Misconfigured.selector); + new OracleVault(params, address(oracle), address(0)); + } + + /*////////////////////////////////////////////////////////////// + ACCOUNTING TESTS + //////////////////////////////////////////////////////////////*/ + + function testTotalAssets() public { + // Set oracle price to 2:1 (2 assets per share) + oracle.setPrice(2e18); + + uint256 expectedAssets = vault.totalSupply().mulDivDown(2e18, ONE); + assertEq(vault.totalAssets(), expectedAssets); + } + + function testTotalAssetsWithZeroPrice() public { + oracle.setPrice(0); + assertEq(vault.totalAssets(), 0); + } + + /*////////////////////////////////////////////////////////////// + DEPOSIT TESTS + //////////////////////////////////////////////////////////////*/ + + function testDeposit() public { + uint256 depositAmount = 100e18; + + vm.startPrank(alice); + uint256 sharesBefore = vault.balanceOf(alice); + uint256 shares = vault.deposit(depositAmount, alice); + + assertGt(shares, 0); + assertEq(vault.balanceOf(alice), sharesBefore + shares); + assertEq(asset.balanceOf(multisig), depositAmount); + vm.stopPrank(); + } + + function testDepositWithDifferentPrice() public { + uint256 depositAmount = 100e18; + oracle.setPrice(2e18); // 2 assets per share + + vm.startPrank(alice); + uint256 sharesBefore = vault.balanceOf(alice); + uint256 shares = vault.deposit(depositAmount, alice); + + // Should receive fewer shares since each share is worth more assets + assertEq(shares, depositAmount.mulDivDown(ONE, 2e18)); + assertEq(vault.balanceOf(alice), sharesBefore + shares); + assertEq(asset.balanceOf(multisig), depositAmount); + vm.stopPrank(); + } + + function testDepositWhenPaused() public { + vm.prank(owner); + vault.pause(); + + uint256 depositAmount = 100e18; + vm.startPrank(alice); + uint256 shares = vault.deposit(depositAmount, alice); + + // Should still work but not take fees + assertGt(shares, 0); + assertEq(asset.balanceOf(multisig), depositAmount); + vm.stopPrank(); + } + + /*////////////////////////////////////////////////////////////// + FEE TESTS + //////////////////////////////////////////////////////////////*/ + + function testTakeFeesOnDeposit() public { + uint256 depositAmount = 100e18; + + vm.startPrank(alice); + uint256 sharesBefore = vault.balanceOf(alice); + uint256 shares = vault.deposit(depositAmount, alice); + + // Verify fees were taken + assertLt(shares, depositAmount); // Shares should be less due to fees + assertEq(vault.balanceOf(alice), sharesBefore + shares); + assertEq(asset.balanceOf(multisig), depositAmount); + vm.stopPrank(); + } + + function testNoFeesOnDepositWhenPaused() public { + vm.prank(owner); + vault.pause(); + + uint256 depositAmount = 100e18; + + vm.startPrank(alice); + uint256 sharesBefore = vault.balanceOf(alice); + uint256 shares = vault.deposit(depositAmount, alice); + + // Verify no fees were taken + assertEq(shares, depositAmount); // Shares should equal deposit since no fees + assertEq(vault.balanceOf(alice), sharesBefore + shares); + assertEq(asset.balanceOf(multisig), depositAmount); + vm.stopPrank(); + } + + /*////////////////////////////////////////////////////////////// + ORACLE PRICE IMPACT TESTS + //////////////////////////////////////////////////////////////*/ + + function testPriceImpactOnConversions() public { + // Test with different oracle prices + uint256[] memory prices = new uint256[](3); + prices[0] = 0.5e18; // 0.5 assets per share + prices[1] = 1e18; // 1:1 + prices[2] = 2e18; // 2 assets per share + + uint256 amount = 100e18; + + for (uint256 i = 0; i < prices.length; i++) { + oracle.setPrice(prices[i]); + + uint256 assets = vault.convertToAssets(amount); + uint256 shares = vault.convertToShares(amount); + + assertEq(assets, amount.mulDivDown(prices[i], ONE)); + assertEq(shares, amount.mulDivDown(ONE, prices[i])); + } + } + + function testPriceImpactOnMaxOperations() public { + oracle.setPrice(2e18); // 2 assets per share + + uint256 depositLimit = vault.limits().depositLimit; + uint256 currentAssets = vault.totalAssets(); + + uint256 maxDeposit = vault.maxDeposit(alice); + uint256 maxMint = vault.maxMint(alice); + + assertEq(maxDeposit, depositLimit - currentAssets); + assertEq(maxMint, vault.convertToShares(maxDeposit)); + } + + /*////////////////////////////////////////////////////////////// + MULTISIG TRANSFER TESTS + //////////////////////////////////////////////////////////////*/ + + function testAssetTransferToMultisig() public { + uint256 depositAmount = 100e18; + + uint256 multisigBalanceBefore = asset.balanceOf(multisig); + + vm.startPrank(alice); + vault.deposit(depositAmount, alice); + vm.stopPrank(); + + assertEq( + asset.balanceOf(multisig), + multisigBalanceBefore + depositAmount + ); + } + + /*////////////////////////////////////////////////////////////// + INTEGRATION TESTS + //////////////////////////////////////////////////////////////*/ + + function testFullDepositWithdrawCycle() public { + uint256 depositAmount = 100e18; + + // Initial deposit + vm.startPrank(alice); + uint256 shares = vault.deposit(depositAmount, alice); + + // Request withdrawal + vault.approve(address(vault), shares); + uint256 requestId = vault.requestRedeem(shares, alice, alice); + vm.stopPrank(); + + // Oracle price changes + oracle.setPrice(1.5e18); // 1.5 assets per share + + // Fulfill redemption + vm.prank(owner); + uint256 assets = vault.fulfillRedeem(shares, alice); + + // Final withdrawal + vm.prank(alice); + uint256 finalAssets = vault.withdraw(assets, alice, alice); + + // Verify final state + assertEq(vault.balanceOf(alice), 0); + assertGt(finalAssets, depositAmount); // Should get more assets due to price increase + } +} \ No newline at end of file