From b42fbcb98b03183783672fc5765ae62ecda1c9cd Mon Sep 17 00:00:00 2001 From: Jun Kim <64379343+junkim012@users.noreply.github.com> Date: Fri, 24 May 2024 01:36:38 -0400 Subject: [PATCH 1/6] fix: if amount to supply will lead to invalid mint amount and is the last remaining amount of the requested deposit amount, do not revert fix: do not allow the fee recipient to be set to the zero address --- src/vault/Vault.sol | 34 +- test/helpers/VaultSharedSetup.sol | 3 + test/unit/concrete/vault/Vault.t.sol | 289 ++++++++++++- test/unit/fuzz/vault/Vault.t.sol | 608 ++++++++++++++++++++++++++- 4 files changed, 903 insertions(+), 31 deletions(-) diff --git a/src/vault/Vault.sol b/src/vault/Vault.sol index b4854dd8..29a6fc61 100644 --- a/src/vault/Vault.sol +++ b/src/vault/Vault.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity 0.8.21; -import { IIonPool } from "./../interfaces/IIonPool.sol"; import { IIonPool } from "./../interfaces/IIonPool.sol"; import { RAY } from "./../libraries/math/WadRayMath.sol"; @@ -48,6 +47,7 @@ contract Vault is ERC4626, Multicall, AccessControlDefaultAdminRules, Reentrancy error MarketsAndAllocationCapLengthMustBeEqual(); error IonPoolsArrayAndNewCapsArrayMustBeOfEqualLength(); error InvalidFeePercentage(); + error InvalidFeeRecipient(); error MaxSupportedMarketsReached(); event UpdateSupplyQueue(address indexed caller, IIonPool[] newSupplyQueue); @@ -144,6 +144,7 @@ contract Vault is ERC4626, Multicall, AccessControlDefaultAdminRules, Reentrancy * @param _feeRecipient The recipient address of the shares minted as fees. */ function updateFeeRecipient(address _feeRecipient) external onlyRole(OWNER_ROLE) { + if (_feeRecipient == address(0)) revert InvalidFeeRecipient(); feeRecipient = _feeRecipient; } @@ -459,21 +460,29 @@ contract Vault is ERC4626, Multicall, AccessControlDefaultAdminRules, Reentrancy for (uint256 i; i != supplyQueueLength;) { IIonPool pool = supplyQueue[i]; - uint256 depositable = pool == IDLE ? _zeroFloorSub(caps[pool], currentIdleDeposits) : _depositable(pool); - if (depositable != 0) { uint256 toSupply = Math.min(depositable, assets); - // For the IDLE pool, decrement the accumulator at the end of this - // loop, but no external interactions need to be made as the assets - // are already on this contract' balance. If the pool supply - // reverts, simply skip to the next iteration. if (pool != IDLE) { - try pool.supply(address(this), toSupply, new bytes32[](0)) { - assets -= toSupply; - // solhint-disable-next-line no-empty-blocks - } catch { } + // Early exit ok since this is the last remaining part of + // the user's requested amount and the deposit will + // normalize to zero. Note that this dust amount has already + // been transferred to the vault but is not a 'donation' as + // this amount was accounted for when calculating the amount + // of shares to mint. + uint256 normalizedSupply = toSupply.mulDiv(RAY, pool.supplyFactor()); + if (toSupply == assets && normalizedSupply == 0) { + return; + } else { + // If this call reverts by trying to mint zero shares + // with a small supply amount, skip to the next + // iteration. + try pool.supply(address(this), toSupply, new bytes32[](0)) { + assets -= toSupply; + // solhint-disable-next-line no-empty-blocks + } catch { } + } } else { assets -= toSupply; } @@ -513,6 +522,9 @@ contract Vault is ERC4626, Multicall, AccessControlDefaultAdminRules, Reentrancy // transfer. If the pool withdraw reverts, simply skip to the // next iteration. if (pool != IDLE) { + // This will never throw InvalidBurnAmount since + // `toWithdraw` is non-zero which means the normalized + // shares to burn inside the IonPool must be non-zero. try pool.withdraw(address(this), toWithdraw) { assets -= toWithdraw; // solhint-disable-next-line no-empty-blocks diff --git a/test/helpers/VaultSharedSetup.sol b/test/helpers/VaultSharedSetup.sol index 1fead9b9..4fd5e314 100644 --- a/test/helpers/VaultSharedSetup.sol +++ b/test/helpers/VaultSharedSetup.sol @@ -68,6 +68,9 @@ contract VaultSharedSetup is IonPoolSharedSetup { address constant NULL = address(0); + bytes32 public constant ION_POOL_SUPPLY_CAP_SLOT = + 0xceba3d526b4d5afd91d1b752bf1fd37917c20a6daf576bcb41dd1c57c1f67e09; + function setUp() public virtual override { super.setUp(); diff --git a/test/unit/concrete/vault/Vault.t.sol b/test/unit/concrete/vault/Vault.t.sol index 4b8e15bf..66731f03 100644 --- a/test/unit/concrete/vault/Vault.t.sol +++ b/test/unit/concrete/vault/Vault.t.sol @@ -164,7 +164,23 @@ contract VaultSetUpTest is VaultSharedSetup { vm.stopPrank(); } - function test_Revert_AddSupportedMarkets_MarketAlreadySupported() public { } + function test_Revert_AddSupportedMarkets_MarketAlreadySupported() public { + IIonPool[] memory newMarkets = new IIonPool[](1); + newMarkets[0] = weEthIonPool; + + uint256[] memory newAllocationCaps = new uint256[](1); + newAllocationCaps[0] = 1e18; + + IIonPool[] memory queue = new IIonPool[](4); + queue[0] = weEthIonPool; + queue[1] = rsEthIonPool; + queue[2] = rswEthIonPool; + queue[3] = weEthIonPool; + + vm.startPrank(OWNER); + vm.expectRevert(abi.encodeWithSelector(Vault.MarketAlreadySupported.selector, weEthIonPool)); + vault.addSupportedMarkets(newMarkets, newAllocationCaps, queue, queue); + } function test_Revert_AddSupportedMarkets_MaxSupportedMarketsReached() public { vault = new Vault( @@ -452,11 +468,82 @@ contract VaultSetUpTest is VaultSharedSetup { vault.updateSupplyQueue(notSupportedQueue); } - function test_UpdateWithdrawQueue() public { } + function test_UpdateSupplyQueue_Revert_DuplicateIonPool() public { + IIonPool[] memory duplicateQueue = new IIonPool[](3); + duplicateQueue[0] = weEthIonPool; + duplicateQueue[1] = rswEthIonPool; + duplicateQueue[2] = weEthIonPool; - function test_Revert_UpdateWithdrawQueue() public { } + vm.startPrank(OWNER); + vm.expectRevert(Vault.InvalidQueueContainsDuplicates.selector); + vault.updateSupplyQueue(duplicateQueue); + } - function test_Revert_DuplicateIonPoolArray() public { } + function test_UpdateWithdrawQueue() public { + IIonPool[] memory withdrawQueue = new IIonPool[](3); + withdrawQueue[0] = rsEthIonPool; + withdrawQueue[1] = rswEthIonPool; + withdrawQueue[2] = weEthIonPool; + + vm.startPrank(OWNER); + vault.updateWithdrawQueue(withdrawQueue); + + assertEq(address(vault.withdrawQueue(0)), address(withdrawQueue[0]), "updated withdraw queue"); + assertEq(address(vault.withdrawQueue(1)), address(withdrawQueue[1]), "updated withdraw queue"); + assertEq(address(vault.withdrawQueue(2)), address(withdrawQueue[2]), "updated withdraw queue"); + } + + function test_UpdateWithdrawQueue_Revert_InvalidQueueLength() public { + IIonPool[] memory smallerQueue = new IIonPool[](2); + smallerQueue[0] = rsEthIonPool; + smallerQueue[1] = rswEthIonPool; + + vm.startPrank(OWNER); + vm.expectRevert(abi.encodeWithSelector(Vault.InvalidQueueLength.selector, 2, 3)); + vault.updateWithdrawQueue(smallerQueue); + + IIonPool[] memory biggerQueue = new IIonPool[](4); + biggerQueue[0] = rsEthIonPool; + biggerQueue[1] = rswEthIonPool; + + vm.startPrank(OWNER); + vm.expectRevert(abi.encodeWithSelector(Vault.InvalidQueueLength.selector, 4, 3)); + vault.updateWithdrawQueue(biggerQueue); + } + + function test_UpdateWithdrawQueue_Revert_MarketNotSupported_IonPoolNotSupported() public { + IIonPool wrongIonPool = IIonPool(address(uint160(uint256(keccak256("address not in supported markets"))))); + IIonPool[] memory queue = new IIonPool[](3); + queue[0] = rsEthIonPool; + queue[1] = rswEthIonPool; + queue[2] = wrongIonPool; + + vm.startPrank(OWNER); + vm.expectRevert(abi.encodeWithSelector(Vault.MarketNotSupported.selector, wrongIonPool)); + vault.updateWithdrawQueue(queue); + } + + function test_UpdateWithdrawQueue_Revert_MarketNotSupported_ZeroAddress() public { + IIonPool[] memory queue = new IIonPool[](3); + queue[0] = IIonPool(address(0)); + queue[1] = rswEthIonPool; + queue[2] = weEthIonPool; + + vm.startPrank(OWNER); + vm.expectRevert(abi.encodeWithSelector(Vault.MarketNotSupported.selector, address(0))); + vault.updateWithdrawQueue(queue); + } + + function test_UpdateWithdrawQueue_Revert_DuplicateIonPool() public { + IIonPool[] memory duplicateQueue = new IIonPool[](3); + duplicateQueue[0] = weEthIonPool; + duplicateQueue[1] = rswEthIonPool; + duplicateQueue[2] = weEthIonPool; + + vm.startPrank(OWNER); + vm.expectRevert(Vault.InvalidQueueContainsDuplicates.selector); + vault.updateWithdrawQueue(duplicateQueue); + } function test_UpdateFeePercentage() public { vm.prank(OWNER); @@ -473,6 +560,13 @@ contract VaultSetUpTest is VaultSharedSetup { assertEq(newFeeRecipient, vault.feeRecipient(), "fee recipient"); } + + function test_UpdateFeeRecipient_Revert_ZeroAddress() public { + address zeroAddress = address(0); + vm.prank(OWNER); + vm.expectRevert(Vault.InvalidFeeRecipient.selector); + vault.updateFeeRecipient(zeroAddress); + } } contract VaultRolesAndPrivilegedFunctions is VaultSharedSetup { @@ -779,7 +873,98 @@ abstract contract VaultDeposit is VaultSharedSetup { vault.deposit(depositAmount, address(this)); } - function test_SupplyToIonPool_AllocationCapAndSupplyCapDiffs() public { } + // allocation cap diff less than supply cap diff + // Allocation Cap: 10 out of 15 (room = 5) + // Supply Cap: 10 out of 20 (room = 10) + // Depositing 7e18 should deposit 5e18 to the first pool, and deposit the rest to the second + function test_SupplyToIonPool_AllocationCapDiffBelowSupplyCapDiff() public { + uint256 allocationCap = 15e18; + uint256 supplyCap = 20e18; + + updateAllocationCaps(vault, allocationCap, type(uint256).max, type(uint256).max); + updateSupplyCaps(vault, supplyCap, type(uint256).max, type(uint256).max); + + uint256 initialDeposit = 10e18; + deal(address(BASE_ASSET), address(this), initialDeposit); + vault.deposit(initialDeposit, address(this)); + + uint256 initialIonPoolClaim = vault.supplyQueue(0).balanceOf(address(vault)); + uint256 firstIonPoolRoundingError = vault.supplyQueue(0).supplyFactor() / RAY + 1; + assertLe(initialDeposit - initialIonPoolClaim, firstIonPoolRoundingError, "vault has initial ionPool claim"); + + uint256 depositAmt = 7e18; + deal(address(BASE_ASSET), address(this), depositAmt); + vault.deposit(depositAmt, address(this)); + + uint256 resultingFirstIonPoolClaim = vault.supplyQueue(0).balanceOf(address(vault)); + assertLe(allocationCap - resultingFirstIonPoolClaim, firstIonPoolRoundingError, "vault resulting ionPool claim"); + + uint256 secondIonPoolRoundingError = vault.supplyQueue(1).supplyFactor() / RAY + 1; + uint256 resultingSecondIonPoolClaim = vault.supplyQueue(1).balanceOf(address(vault)); + uint256 expectedSecondIonPoolClaim = initialDeposit + depositAmt - allocationCap; + assertLe( + expectedSecondIonPoolClaim - resultingSecondIonPoolClaim, + secondIonPoolRoundingError, + "vault second ionPool claim" + ); + } + + // Supply cap diff less than allocation cap diff + // Allocation Cap: 10e18 out of 35e18 (room = 25e18) + // Supply Cap: 30e18 out of 45e18 (room = 15e18) + // Depositing 20e18 should deposit 15e18 to the first pool, then deposit 5 to the next. + function test_SupplyToIonPool_SupplyCapDiffBelowAllocationCapDiff() public { + uint256 initialTotalSupply = 20e18; // becomes 30 with the `initialDeposit` + uint256 initialDeposit = 10e18; + + uint256 allocationCap = 35e18; + uint256 supplyCap = 45e18; + + uint256 depositAmt = 20e18; + + updateAllocationCaps(vault, allocationCap, type(uint256).max, type(uint256).max); + updateSupplyCaps(vault, supplyCap, type(uint256).max, type(uint256).max); + + // Initialize total supply in first ionPool + deal(address(BASE_ASSET), address(this), initialTotalSupply); + BASE_ASSET.approve(address(vault.supplyQueue(0)), initialTotalSupply); + vault.supplyQueue(0).supply(address(this), initialTotalSupply, new bytes32[](0)); + uint256 firstIonPoolRoundingError = vault.supplyQueue(0).supplyFactor() / RAY + 1; + assertApproxEqAbs( + vault.supplyQueue(0).totalSupply(), + initialTotalSupply, + firstIonPoolRoundingError, + "first ionPool has initial total supply" + ); + + // Initialize vault's first deposit into the first ionPool (separate from the initial total supply) + deal(address(BASE_ASSET), address(this), initialDeposit); + vault.deposit(initialDeposit, address(this)); + + uint256 initialIonPoolClaim = vault.supplyQueue(0).balanceOf(address(vault)); + assertLe(initialDeposit - initialIonPoolClaim, firstIonPoolRoundingError, "vault has initial ionPool claim"); + + // The resulting supply cap should be filled. + uint256 totalSupplyBeforeDeposit = vault.supplyQueue(0).totalSupply(); + uint256 supplyCapBeforeDeposit = uint256(vm.load(address(vault.supplyQueue(0)), ION_POOL_SUPPLY_CAP_SLOT)); + uint256 supplyCapDiff = supplyCapBeforeDeposit - totalSupplyBeforeDeposit; + + deal(address(BASE_ASSET), address(this), depositAmt); + vault.deposit(depositAmt, address(this)); + + uint256 resultingFirstIonPoolClaim = vault.supplyQueue(0).balanceOf(address(vault)); + // the resulting first ionpool claim should be 10 (initialDeposit) + 15 (out of depositAmt) + assertLe(25e18 - resultingFirstIonPoolClaim, firstIonPoolRoundingError, "vault resulting ionPool claim"); + + uint256 resultingSecondIonPoolClaim = vault.supplyQueue(1).balanceOf(address(vault)); + uint256 expectedSecondIonPoolClaim = depositAmt - supplyCapDiff; + uint256 secondIonPoolRoundingError = vault.supplyQueue(1).supplyFactor() / RAY + 1; + assertLe( + expectedSecondIonPoolClaim - resultingSecondIonPoolClaim, + secondIonPoolRoundingError, + "vault second ionPool claim" + ); + } /** * - Exact shares to mint must be minted to the user. @@ -861,7 +1046,74 @@ abstract contract VaultDeposit is VaultSharedSetup { vault.deposit(depositAmt, address(this)); } - function test_Mint_AllMarkets() public { } + function test_Deposit_Revert_AllSupplyCapsReachedWithAllocationCap() public { + updateAllocationCaps(vault, 10 ether, 10 ether, 10 ether); + + uint256 depositAmt = 30 ether + 1; + deal(address(BASE_ASSET), address(this), depositAmt); + + vm.expectRevert(Vault.AllSupplyCapsReached.selector); + vault.deposit(depositAmt, address(this)); + + // this should pass + vault.deposit(depositAmt - 1, address(this)); + } + + function test_Deposit_Revert_AllSupplyCapsReachedWithSupplyCap() public { + updateAllocationCaps(vault, type(uint256).max, type(uint256).max, type(uint256).max); + updateSupplyCaps(vault, 15 ether, 15 ether, 15 ether); + + uint256 depositAmt = 45 ether + 1; + deal(address(BASE_ASSET), address(this), depositAmt); + + vm.expectRevert(Vault.AllSupplyCapsReached.selector); + vault.deposit(depositAmt, address(this)); + + // this should pass + vault.deposit(depositAmt - 1, address(this)); + } + + /** + * Supplying an amount small enough to the IonPool that truncates to mint + * zero shares will revert. The `Vault` contract handles this by keeping + * this dust amount that was already transferred in on the IDLE balance. + */ + function test_Deposit_IonPoolInvalidMintAmountWithoutIteration() public { + updateAllocationCaps(vault, type(uint256).max, type(uint256).max, type(uint256).max); + + uint256 depositAmt = 1; // 1 wei + deal(address(BASE_ASSET), address(this), depositAmt); + vault.deposit(depositAmt, address(this)); + + uint256 expectedTotalAssets; + uint256 expectedVaultIdleBalance; + uint256 expectedVaultIonPoolBalance; + + IIonPool pool = vault.supplyQueue(0); + if (depositAmt.mulDiv(RAY, pool.supplyFactor()) == 0) { + expectedTotalAssets = 0; + expectedVaultIdleBalance = depositAmt; + expectedVaultIonPoolBalance = 0; + } else { + expectedTotalAssets = depositAmt; + expectedVaultIdleBalance = 0; + expectedVaultIonPoolBalance = depositAmt; + } + + assertEq(BASE_ASSET.balanceOf(address(vault)), expectedVaultIdleBalance, "vault base asset balance"); + assertEq(vault.totalAssets(), expectedTotalAssets, "vault total assets"); + + // No deposit was actually made to the underlying IonPool + assertEq(pool.balanceOf(address(vault)), expectedVaultIonPoolBalance, "IonPool balance"); + } + + function test_Deposit_IonPoolInvalidMintAmountWithIteration() public { } + + /** + * If the try catch encounters an error that is not `InvalidMintAmount`, it + * should simply skip the iteration. + */ + function test_Deposit_IonPoolThrowsUnrecognizedError() public { } } abstract contract VaultWithdraw is VaultSharedSetup { @@ -869,6 +1121,10 @@ abstract contract VaultWithdraw is VaultSharedSetup { super.setUp(); } + function test_Withdraw_SenderIsNotTheOwner() public { + // TODO + } + function test_Withdraw_SingleMarket() public { uint256 depositAmount = 10e18; uint256 withdrawAmount = 5e18; @@ -1034,6 +1290,27 @@ abstract contract VaultWithdraw is VaultSharedSetup { ); } + /** + * Case where the last remaining asset being withdrawn would lead to + * InvalidBurnAmount error. This should revert if there are no IDLE assets + * to be withdrawn. + */ + function test_Withdraw_InvalidBurnAmountAtTheLastWithdraw() public { + updateAllocationCaps(vault, 1e18, 2e18, 3e18); + + uint256 depositAmt = vault.maxDeposit(NULL); + deal(address(BASE_ASSET), address(this), depositAmt); + vault.deposit(depositAmt, address(this)); + + uint256 withdrawable = weEthIonPool.balanceOf(address(vault)); // withdrawable from the first pool + // Tries to withdraw 1 more than the withdrawable from the first pool. + // This should attempt a 1 wei withdrawal on the second pool. + uint256 withdrawAmt = withdrawable + 1; + + require(BASE_ASSET.balanceOf(address(vault)) == 0, "vault IDLE pool is zero"); + vault.withdraw(withdrawAmt, address(this), address(this)); + } + // try to deposit and withdraw same amounts function test_Withdraw_FullWithdraw() public { } diff --git a/test/unit/fuzz/vault/Vault.t.sol b/test/unit/fuzz/vault/Vault.t.sol index 00aacecb..ccc22318 100644 --- a/test/unit/fuzz/vault/Vault.t.sol +++ b/test/unit/fuzz/vault/Vault.t.sol @@ -13,7 +13,11 @@ import { IERC20 } from "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; using Math for uint256; using WadRayMath for uint256; +import { console2 } from "forge-std/console2.sol"; + contract Vault_Fuzz is VaultSharedSetup { + using Math for uint256; + function setUp() public override { super.setUp(); @@ -128,16 +132,102 @@ contract Vault_Fuzz is VaultSharedSetup { assertLe(resultingTotalClaim, allocationCap, "expected claim le to allocation cap"); assertLe(actualTotalClaim, allocationCap, "actual claim le to allocation cap"); } + + /** + * Should confirm that maxBound is the true maxBound. + * - If maxBound is increased by one, then the equals assert should fail. + */ + function testFuzz_MaximumDepositAmountThatTruncatesToZeroNormalized(uint256 assets, uint256 supplyFactor) public { + supplyFactor = bound(supplyFactor, 1e27, type(uint256).max); + + // this amount should always trunate to zero after division + // ceil(supplyFactor / RAY) - 1 passes [correctly constrained] + // ceil(supplyFactor / RAY) does not pass + uint256 divRoundUp = supplyFactor % RAY == 0 ? supplyFactor / RAY : supplyFactor / RAY + 1; + uint256 assetsMaxBound = divRoundUp - 1; // supplyFactor / RAY - 1 passes but overconstrained? + + assets = bound(assets, 0, assetsMaxBound); + + uint256 normalized = assets.mulDiv(RAY, supplyFactor); + + assertEq(normalized, 0, "normalized must be zero"); + } + + /** + * Assume `supplyFactor` is less than 2e27. + * 1. If `supplyFactor` is 2e27, the maxBound ceil(2e27 / 1e27) - 1 = 1. + * This means anything above 1 wei will not truncate to zero. + * 2. If `supplyFactor` is 2e27 - 1, the maxBound ceil((2e27 - 1) / 1e27) - 1 is 1. + * This means the maxBound is 1. So anything above 1 (i.e. 2 and above) + * should not normalize to zero. + */ + function testFuzz_MaximumDepositAmountThatTruncatesWhenSupplyFactorIsLessThanTwoRay( + uint256 assets, + uint256 supplyFactor + ) + public + { + supplyFactor = bound(supplyFactor, 1e27, 2e27); + assets = bound(assets, 2, type(uint256).max); + + // all assets amount other than 0 will NOT truncate to zero. + uint256 normalized = assets.mulDiv(RAY, supplyFactor); + + assertTrue(normalized != 0, "normalized must NOT be zero"); + } + + /** + * If `assets` is not 0, then the result of the muldiv should never be 0 due + * to the ceiling. + */ + function testFuzz_MulDivCeilingCanNotBeZero(uint256 assets, uint256 supplyFactor) public { + assets = bound(assets, 1, type(uint128).max); + supplyFactor = bound(supplyFactor, 1, type(uint128).max); + + uint256 result = assets.mulDiv(RAY, supplyFactor, Math.Rounding.Ceil); + + assertTrue(result != 0, "result must NOT be zero"); + } } -contract VaultWithYieldAndFee_Fuzz is VaultSharedSetup { +contract VaultWithYieldAndFeeSharedSetup is VaultSharedSetup { uint256 constant INITIAL_SUPPLY_AMT = 1000e18; + uint256 constant MAX_DAYS = 10_000 days; + uint256 constant MINIMUM_FEE_PERC = 0.02e27; + uint256 constant MAXIMUM_FEE_PERC = 1e27; - function setUp() public override { + uint256 constant MINIMUM_INITIAL_DEPOSIT = 0; + uint256 constant MAXIMUM_INITIAL_DEPOSIT = type(uint128).max; + + IIonPool[] internal queue = new IIonPool[](4); + + function setUp() public virtual override { super.setUp(); - uint256 initialSupplyAmt = 1000e18; + IIonPool[] memory marketsToAdd = new IIonPool[](1); + marketsToAdd[0] = IDLE; + + uint256[] memory newMarketAllocationCap = new uint256[](1); + newMarketAllocationCap[0] = 0; + + queue[0] = IDLE; + queue[1] = weEthIonPool; + queue[2] = rsEthIonPool; + queue[3] = rswEthIonPool; + + vm.prank(OWNER); + vault.addSupportedMarkets(marketsToAdd, newMarketAllocationCap, queue, queue); + + uint256[] memory allocationCaps = new uint256[](4); + allocationCaps[0] = 10e18; + allocationCaps[1] = 20e18; + allocationCaps[2] = 30e18; + allocationCaps[3] = 40e18; + vm.prank(OWNER); + vault.updateAllocationCaps(queue, allocationCaps); + + // Setup IonPools weEthIonPool.updateSupplyCap(type(uint256).max); rsEthIonPool.updateSupplyCap(type(uint256).max); rswEthIonPool.updateSupplyCap(type(uint256).max); @@ -154,15 +244,10 @@ contract VaultWithYieldAndFee_Fuzz is VaultSharedSetup { supply(address(this), rswEthIonPool, INITIAL_SUPPLY_AMT); borrow(address(this), rswEthIonPool, rswEthGemJoin, 100e18, 70e18); - - uint256 weEthIonPoolCap = 10e18; - uint256 rsEthIonPoolCap = 20e18; - uint256 rswEthIonPoolCap = 30e18; - - vm.prank(OWNER); - updateAllocationCaps(vault, weEthIonPoolCap, rsEthIonPoolCap, rswEthIonPoolCap); } +} +contract VaultWithYieldAndFee_Fuzz_FeeAccrual is VaultWithYieldAndFeeSharedSetup { function testFuzz_AccruedFeeShares(uint256 initialDeposit, uint256 feePerc, uint256 daysAccrued) public { // fee percentage feePerc = bound(feePerc, 0, RAY - 1); @@ -172,7 +257,7 @@ contract VaultWithYieldAndFee_Fuzz is VaultSharedSetup { // initial deposit uint256 initialMaxDeposit = vault.maxDeposit(NULL); - initialDeposit = bound(initialDeposit, 1e18, initialMaxDeposit); + initialDeposit = bound(initialDeposit, 15e18, initialMaxDeposit); setERC20Balance(address(BASE_ASSET), address(this), initialDeposit); vault.deposit(initialDeposit, address(this)); @@ -183,7 +268,7 @@ contract VaultWithYieldAndFee_Fuzz is VaultSharedSetup { uint256 prevUserAssets = vault.previewRedeem(prevUserShares); // interest accrues over a year - daysAccrued = bound(daysAccrued, 1, 10_000 days); + daysAccrued = bound(daysAccrued, 1, MAX_DAYS); vm.warp(block.timestamp + daysAccrued); (uint256 totalSupplyFactorIncrease,,,,) = weEthIonPool.calculateRewardAndDebtDistribution(); @@ -240,10 +325,505 @@ contract VaultWithYieldAndFee_Fuzz is VaultSharedSetup { assertEq(userShares + feeRecipientShares, vault.totalSupply(), "vault total supply"); assertLe(vault.totalAssets() - (userAssets + feeRecipientAssets), 2, "vault total assets"); } +} + +contract VaultWithYieldAndFee_Fuzz_Previews_SinglePool is VaultWithYieldAndFeeSharedSetup { + function setUp() public override { + super.setUp(); + + // Only funnel deposits into one IonPool that's not IDLE. + uint256[] memory allocationCaps = new uint256[](4); + allocationCaps[0] = 0; + allocationCaps[1] = type(uint128).max; + allocationCaps[2] = 0; + allocationCaps[3] = 0; + + vm.prank(OWNER); + vault.updateAllocationCaps(queue, allocationCaps); + } + + function testFuzz_previewDeposit_SinglePool( + uint256 assets, + uint256 feePerc, + uint256 daysAccrued, + uint256 supplyFactor + ) + public + { + // 1. Set `supplyFactor` + supplyFactor = bound(supplyFactor, 1e27, 10e27); + IonPoolExposed(address(weEthIonPool)).setSupplyFactor(supplyFactor); + + // 2. Make initial vault deposit + uint256 initialDeposit = bound(assets, MINIMUM_INITIAL_DEPOSIT, vault.maxDeposit(NULL)); + deal(address(BASE_ASSET), address(this), initialDeposit); + vault.deposit(initialDeposit, address(this)); + + // 3. Set fee percentage + feePerc = bound(feePerc, MINIMUM_FEE_PERC, MAXIMUM_FEE_PERC); + + vm.prank(OWNER); + vault.updateFeePercentage(feePerc); + + // 4. Accrue interest + daysAccrued = bound(daysAccrued, 100 days, MAX_DAYS); + vm.warp(block.timestamp + daysAccrued); + + // 5. Compare preview deposit with real deposit + // - Minimum deposit amount is amount that won't be truncated by zer + // - amt * RAY / supplyFactor > 0 + uint256 minimumDeposit = RAY / vault.supplyQueue(1).supplyFactor() + 1; + uint256 previewDepositAmt = bound(assets, 0, vault.maxDeposit(NULL)); + + console2.log("vault.maxDeposit(NULL): ", vault.maxDeposit(NULL)); + console2.log("previewDepositAmt: ", previewDepositAmt); + + console2.log("--- preview deposit ---"); + uint256 expectedShares = vault.previewDeposit(previewDepositAmt); + console2.log("--- preview deposit done ---"); + + deal(address(BASE_ASSET), address(this), previewDepositAmt); + uint256 resultingShares = vault.deposit(previewDepositAmt, address(this)); + + uint256 resultingAssets = vault.previewRedeem(resultingShares); + + uint256 resultingAssetsRoundingError = vault.supplyQueue(1).supplyFactor() / RAY + 1; + + assertEq(BASE_ASSET.balanceOf(address(this)), 0, "resulting user asset balance"); + assertEq(resultingShares, expectedShares, "resulting shares must be equal to expected shares"); + assertApproxEqAbs( + resultingAssets, previewDepositAmt, resultingAssetsRoundingError, "resulting assets with rounding error" + ); + } + + function testFuzz_previewMint_SinglePool( + uint256 assets, + uint256 feePerc, + uint256 daysAccrued, + uint256 supplyFactor + ) + public + { + // 1. Set `supplyFactor` + supplyFactor = bound(supplyFactor, 1e27, 10e27); + IonPoolExposed(address(weEthIonPool)).setSupplyFactor(supplyFactor); + + // 2. Make initial vault deposit + uint256 initialDeposit = bound(assets, MINIMUM_INITIAL_DEPOSIT, vault.maxDeposit(NULL)); + deal(address(BASE_ASSET), address(this), initialDeposit); + vault.deposit(initialDeposit, address(this)); + + // 3. Set fee percentage + feePerc = bound(feePerc, MINIMUM_FEE_PERC, MAXIMUM_FEE_PERC); + + // 4. Accrue interest + daysAccrued = bound(daysAccrued, 100 days, MAX_DAYS); + vm.warp(block.timestamp + daysAccrued); + + // 5. Compare `previewMint` with `mint` + + uint256 previewMintAmt = bound(assets, 0, vault.maxMint(NULL)); + + uint256 expectedAssets = vault.previewMint(previewMintAmt); + + uint256 prevShares = vault.balanceOf(address(this)); + + deal(address(BASE_ASSET), address(this), expectedAssets); + uint256 resultingAssets = vault.mint(previewMintAmt, address(this)); + + uint256 sharesDiff = vault.balanceOf(address(this)) - prevShares; + + assertEq(BASE_ASSET.balanceOf(address(this)), 0, "resulting user asset balance"); + assertEq(resultingAssets, expectedAssets, "resulting assets must be equal to expected assets"); + assertEq(sharesDiff, previewMintAmt, "resulting shares must be equal to preview mint amount"); + } + + function testFuzz_previewWithdraw_SinglePool( + uint256 assets, + uint256 feePerc, + uint256 daysAccrued, + uint256 supplyFactor + ) + public + { + // 1. Set `supplyFactor` + supplyFactor = bound(supplyFactor, 1e27, 10e27); + IonPoolExposed(address(weEthIonPool)).setSupplyFactor(supplyFactor); + + // 2. Make initial vault deposit + require(vault.maxDeposit(NULL) > 0); + uint256 initialDeposit = bound(assets, MINIMUM_INITIAL_DEPOSIT, vault.maxDeposit(NULL)); + deal(address(BASE_ASSET), address(this), initialDeposit); + vault.deposit(initialDeposit, address(this)); + + // require(vault.balanceOf(address(this)) > 0, 'shares minted'); + + // 3. Set fee percentage + feePerc = bound(feePerc, MINIMUM_FEE_PERC, MAXIMUM_FEE_PERC); + + // 4. Accrue interest + daysAccrued = bound(daysAccrued, 100 days, MAX_DAYS); + vm.warp(block.timestamp + daysAccrued); + + // 5. Compoare `previewWithdraw` and `withdraw` + uint256 previewWithdrawAmt = bound(assets, 0, vault.maxWithdraw(address(this))); + uint256 expectedShares = vault.previewWithdraw(previewWithdrawAmt); + + uint256 prevShares = vault.balanceOf(address(this)); + uint256 prevBalance = BASE_ASSET.balanceOf(address(this)); + + uint256 resultingShares = vault.withdraw(previewWithdrawAmt, address(this), address(this)); + + uint256 sharesDiff = prevShares - vault.balanceOf(address(this)); + uint256 balanceDiff = BASE_ASSET.balanceOf(address(this)) - prevBalance; + + assertEq(sharesDiff, resultingShares, "actual shares diff should be equal to the returned shares"); + assertEq(resultingShares, expectedShares, "resulting shares must be equal to preview shares"); + assertEq(balanceDiff, previewWithdrawAmt, "resulting balance should be the exact request withdraw amount"); + } + + function testFuzz_previewRedeem_SinglePool( + uint256 assets, + uint256 feePerc, + uint256 daysAccrued, + uint256 supplyFactor + ) + public + { + // 1. Set `supplyFactor` + supplyFactor = bound(supplyFactor, 1e27, 10e27); + IonPoolExposed(address(weEthIonPool)).setSupplyFactor(supplyFactor); + + // 2. Make initial vault deposit + uint256 initialDeposit = bound(assets, MINIMUM_INITIAL_DEPOSIT, vault.maxDeposit(NULL)); + deal(address(BASE_ASSET), address(this), initialDeposit); + vault.deposit(initialDeposit, address(this)); + + // 3. Set fee percentage + feePerc = bound(feePerc, MINIMUM_FEE_PERC, MAXIMUM_FEE_PERC); + + // 4. Accrue interest + daysAccrued = bound(daysAccrued, 100 days, MAX_DAYS); + vm.warp(block.timestamp + daysAccrued); + + // 5. Compare `previewRedeem` and `redeem` + uint256 prevShares = vault.balanceOf(address(this)); + + uint256 previewRedeemAmt = bound(assets, 0, vault.maxRedeem(address(this))); + uint256 expectedAssets = vault.previewRedeem(previewRedeemAmt); + + uint256 resultingAssets = vault.redeem(previewRedeemAmt, address(this), address(this)); + + uint256 sharesDiff = prevShares - vault.balanceOf(address(this)); + + assertEq(resultingAssets, expectedAssets, "resulting assets must be equal to expected assets"); + assertEq(sharesDiff, previewRedeemAmt, "shares burned must be equal to redeem amount"); + } +} + +contract VaultWithYieldAndFee_Fuzz_Previews_MultiplePools is VaultWithYieldAndFeeSharedSetup { + /** + * Fuzz variables + * - `supplyFactor` + * - `newTotalAssets` (through interest accrual) + * - `feeShares` minted (through fee percentage) + */ + function testFuzz_previewDeposit_MultiplePools( + uint256 assets, + uint256 feePerc, + uint256 daysAccrued, + uint256 supplyFactor + ) + public + { + // preview deposit and try to mint those shares. + // compare `previewDeposit` with actual minted amounts. + + // 1. Set `supplyFactor` + supplyFactor = bound(supplyFactor, 1e27, 10e27); + IonPoolExposed(address(weEthIonPool)).setSupplyFactor(supplyFactor); + + supplyFactor = bound(supplyFactor, 1e27, 10e27); + IonPoolExposed(address(rsEthIonPool)).setSupplyFactor(supplyFactor); + + supplyFactor = bound(supplyFactor, 1e27, 10e27); + IonPoolExposed(address(rswEthIonPool)).setSupplyFactor(supplyFactor); + + uint256 firstRoundingError = weEthIonPool.supplyFactor() / RAY + 1; + uint256 secondRoundingError = rsEthIonPool.supplyFactor() / RAY + 1; + uint256 thirdRoundingError = rswEthIonPool.supplyFactor() / RAY + 1; + uint256 roundingError = firstRoundingError + secondRoundingError + thirdRoundingError; + + // 1. Set fee percentage + feePerc = bound(feePerc, 0, RAY - 1); + + vm.prank(OWNER); + vault.updateFeePercentage(feePerc); + + // 2. Accrue interest + daysAccrued = bound(daysAccrued, 1, MAX_DAYS); + vm.warp(block.timestamp + daysAccrued); + + require(vault.caps(weEthIonPool) > 0, "weEthIonPool cap"); + require(vault.caps(rsEthIonPool) > 0, "rsEthIonPool cap"); + require(vault.caps(rswEthIonPool) > 0, "rswEthIonPool cap"); + + // 3. Preview deposit + // - Minimum deposit amount is amount that won't be truncated by zer + // - amt * RAY / supplyFactor > 0 + uint256 previewDepositAmt = bound(assets, 0, vault.maxDeposit(NULL)); + uint256 expectedShares = vault.previewDeposit(previewDepositAmt); + + console2.log("previewDepositAmt: ", previewDepositAmt); + console2.log("expectedShares: ", expectedShares); + + deal(address(BASE_ASSET), address(this), previewDepositAmt); + uint256 resultingShares = vault.deposit(previewDepositAmt, address(this)); + + uint256 resultingAssets = vault.previewRedeem(resultingShares); + + uint256 resultingAssetsRoundingError = supplyFactor / RAY + 1; + + assertEq(BASE_ASSET.balanceOf(address(this)), 0, "resulting user asset balance"); + assertEq( + resultingShares, expectedShares, "resulting shares minted must be equal to shares from preview deposit." + ); + assertApproxEqAbs( + resultingAssets, + previewDepositAmt, + roundingError, + "resulting assets must be equal to preview deposit amount" + ); + } + + function testFuzz_previewMint_MultiplePools( + uint256 assets, + uint256 feePerc, + uint256 daysAccrued, + uint256 supplyFactor + ) + public + { + // 1. Set `supplyFactor` + supplyFactor = bound(supplyFactor, 1e27, 10e27); + IonPoolExposed(address(weEthIonPool)).setSupplyFactor(supplyFactor); + + supplyFactor = bound(supplyFactor, 1e27, 10e27); + IonPoolExposed(address(rsEthIonPool)).setSupplyFactor(supplyFactor); + + supplyFactor = bound(supplyFactor, 1e27, 10e27); + IonPoolExposed(address(rswEthIonPool)).setSupplyFactor(supplyFactor); + + uint256 firstRoundingError = weEthIonPool.supplyFactor() / RAY + 1; + uint256 secondRoundingError = rsEthIonPool.supplyFactor() / RAY + 1; + uint256 thirdRoundingError = rswEthIonPool.supplyFactor() / RAY + 1; + uint256 roundingError = firstRoundingError + secondRoundingError + thirdRoundingError; + + // 1. Set fee percentage + feePerc = bound(feePerc, 0, RAY - 1); + + vm.prank(OWNER); + vault.updateFeePercentage(feePerc); + + // 2. Accrue interest + daysAccrued = bound(daysAccrued, 1, MAX_DAYS); + vm.warp(block.timestamp + daysAccrued); + + // 3. Preview mint + uint256 previewMintAmt = bound(assets, 0, vault.maxMint(NULL)); + console2.log("previewMintAmt: ", previewMintAmt); + uint256 expectedAssets = vault.previewMint(previewMintAmt); + console2.log("expectedAssets: ", expectedAssets); + + uint256 prevShares = vault.balanceOf(address(this)); + + deal(address(BASE_ASSET), address(this), expectedAssets); + + uint256 prevBalance = BASE_ASSET.balanceOf(address(this)); + + uint256 resultingAssets = vault.mint(previewMintAmt, address(this)); + + console2.log("resultingAssets: ", resultingAssets); + console2.log("newBalance: ", BASE_ASSET.balanceOf(address(this))); + + uint256 sharesDiff = vault.balanceOf(address(this)) - prevShares; + uint256 balanceDiff = prevBalance - BASE_ASSET.balanceOf(address(this)); + + uint256 sharesToRedeem = vault.previewWithdraw(expectedAssets); + + // 1. The `previewMintAmt` must be the change in user's shares. + // 2. The `expectedAssets` from the `previewMint` must be the same as + // the actual change in user's token balance. + // 3. The `previewWithdraw` of the `expectedAssets` from `previewMint` must be the same as `previewMintAmt`. + + assertEq(sharesDiff, previewMintAmt, "shares diff must be equal to preview mint amount"); + assertEq(expectedAssets, balanceDiff, "expected assets must be equal to balance diff"); + // assertApproxEqAbs(sharesToRedeem, previewMintAmt, roundingError, "shares to redeem must be equal to preview + // mint amount"); + } + + /** + * The edge case of withdraw reverting when withdrawing 1 wei as the last + * withdraw action does not revert if there are IDLE balances. + */ + function testFuzz_previewWithdraw_MultiplePools_WithIdleBalance( + uint256 assets, + uint256 feePerc, + uint256 daysAccrued, + uint256 supplyFactor + ) + public + { + // 1. Set `supplyFactor` + supplyFactor = bound(supplyFactor, 1e27, 10e27); + IonPoolExposed(address(weEthIonPool)).setSupplyFactor(supplyFactor); + + supplyFactor = bound(supplyFactor, 1e27, 10e27); + IonPoolExposed(address(rsEthIonPool)).setSupplyFactor(supplyFactor); + + supplyFactor = bound(supplyFactor, 1e27, 10e27); + IonPoolExposed(address(rswEthIonPool)).setSupplyFactor(supplyFactor); + + // 1. Set fee percentage + feePerc = bound(feePerc, 0, RAY - 1); + + vm.prank(OWNER); + vault.updateFeePercentage(feePerc); + + // 2. Make initial vault deposit + uint256 initialDeposit = bound(assets, MINIMUM_INITIAL_DEPOSIT, vault.maxDeposit(NULL)); + deal(address(BASE_ASSET), address(this), initialDeposit); + vault.deposit(initialDeposit, address(this)); + + // 3. Accrue interest + daysAccrued = bound(daysAccrued, 1, MAX_DAYS); + vm.warp(block.timestamp + daysAccrued); + + uint256 previewWithdrawAmt = bound(assets, 0, vault.maxWithdraw(address(this))); + uint256 expectedShares = vault.previewWithdraw(previewWithdrawAmt); + + uint256 prevBalance = BASE_ASSET.balanceOf(address(this)); + + uint256 resultingShares = vault.withdraw(previewWithdrawAmt, address(this), address(this)); + + uint256 balanceDiff = BASE_ASSET.balanceOf(address(this)) - prevBalance; + + // 1. Compare the withdrawn assets between the input withdraw assets and + // real change in token balance. + // 2. Compare the redeemed shares between `previewWithdraw` and `withdraw` + assertEq(balanceDiff, previewWithdrawAmt, "balance diff must be equal to preview withdraw amount"); + assertEq(resultingShares, expectedShares, "resulting shares must be equal to expected shares"); + } - function testFuzz_MaxWithdrawAndMaxRedeem(uint256 assets) public { } + /** + * The edge case of withdraw reverting when withdrawing 1 wei as the last + * withdraw action should revert if there are no IDLE balances. + */ + function testFuzz_previewWithdraw_MultiplePools_WithoutIdleBalance( + uint256 assets, + uint256 feePerc, + uint256 daysAccrued, + uint256 supplyFactor + ) + public + { + // 1. Set `supplyFactor` + supplyFactor = bound(supplyFactor, 1e27, 10e27); + IonPoolExposed(address(weEthIonPool)).setSupplyFactor(supplyFactor); + + supplyFactor = bound(supplyFactor, 1e27, 10e27); + IonPoolExposed(address(rsEthIonPool)).setSupplyFactor(supplyFactor); + + supplyFactor = bound(supplyFactor, 1e27, 10e27); + IonPoolExposed(address(rswEthIonPool)).setSupplyFactor(supplyFactor); + + // 1. Set fee percentage + feePerc = bound(feePerc, 0, RAY - 1); - function testFuzz_MaxDepositAndMaxMint(uint256 assets) public { } + vm.prank(OWNER); + vault.updateFeePercentage(feePerc); + + // 2. Make initial vault deposit + uint256 initialDeposit = bound(assets, MINIMUM_INITIAL_DEPOSIT, vault.maxDeposit(NULL)); + deal(address(BASE_ASSET), address(this), initialDeposit); + vault.deposit(initialDeposit, address(this)); + + // 3. Empty out the IDLE pool. + vault.withdraw(BASE_ASSET.balanceOf(address(vault)), address(this), address(this)); + require(BASE_ASSET.balanceOf(address(vault)) == 0, "empty IDLE balance"); + + // 3. Accrue interest + daysAccrued = bound(daysAccrued, 1, MAX_DAYS); + vm.warp(block.timestamp + daysAccrued); + + console2.log("vault.maxWithdraw(address(this)): ", vault.maxWithdraw(address(this))); + uint256 previewWithdrawAmt = bound(assets, 0, vault.maxWithdraw(address(this))); + console2.log("previewWithdrawAmt: ", previewWithdrawAmt); + uint256 expectedShares = vault.previewWithdraw(previewWithdrawAmt); + + uint256 prevBalance = BASE_ASSET.balanceOf(address(this)); + + uint256 resultingShares = vault.withdraw(previewWithdrawAmt, address(this), address(this)); + + uint256 balanceDiff = BASE_ASSET.balanceOf(address(this)) - prevBalance; + + // 1. Compare the withdrawn assets between the input withdraw assets and + // real change in token balance. + // 2. Compare the redeemed shares between `previewWithdraw` and `withdraw` + assertEq(balanceDiff, previewWithdrawAmt, "balance diff must be equal to preview withdraw amount"); + assertEq(resultingShares, expectedShares, "resulting shares must be equal to expected shares"); + } + + function testFuzz_previewRedeem_MultiplePools( + uint256 assets, + uint256 feePerc, + uint256 daysAccrued, + uint256 supplyFactor + ) + public + { + // 1. Set `supplyFactor` + supplyFactor = bound(supplyFactor, 1e27, 10e27); + IonPoolExposed(address(weEthIonPool)).setSupplyFactor(supplyFactor); + + supplyFactor = bound(supplyFactor, 1e27, 10e27); + IonPoolExposed(address(rsEthIonPool)).setSupplyFactor(supplyFactor); + + supplyFactor = bound(supplyFactor, 1e27, 10e27); + IonPoolExposed(address(rswEthIonPool)).setSupplyFactor(supplyFactor); + + // 1. Set fee percentage + feePerc = bound(feePerc, 0, RAY - 1); + + vm.prank(OWNER); + vault.updateFeePercentage(feePerc); + + // 2. Make initial vault deposit + uint256 initialDeposit = bound(assets, MINIMUM_INITIAL_DEPOSIT, vault.maxDeposit(NULL)); + deal(address(BASE_ASSET), address(this), initialDeposit); + vault.deposit(initialDeposit, address(this)); + + // 3. Accrue interest + daysAccrued = bound(daysAccrued, 1, MAX_DAYS); + vm.warp(block.timestamp + daysAccrued); + + uint256 prevShares = vault.balanceOf(address(this)); + uint256 prevBalance = BASE_ASSET.balanceOf(address(this)); + + uint256 previewRedeemAmt = bound(assets, 0, vault.maxRedeem(address(this))); + uint256 expectedWithdraw = vault.previewRedeem(previewRedeemAmt); + + uint256 resultingWithdraw = vault.redeem(previewRedeemAmt, address(this), address(this)); + + uint256 sharesDiff = prevShares - vault.balanceOf(address(this)); + uint256 balanceDiff = BASE_ASSET.balanceOf(address(this)) - prevBalance; + + // 1. Compare the change in shares balance with the `previewRedeemAmt` + // 2. Compare the resultingWithdrawAmt with the preview expected withdraw amouont. + assertEq(sharesDiff, previewRedeemAmt, "shares diff must be equal to preview redeem amount"); + assertEq(resultingWithdraw, expectedWithdraw, "resulting withdraw must be equal to expected withdraw"); + assertEq(balanceDiff, resultingWithdraw, "balance diff must be equal to expected withdraw"); + } } contract VaultInflationAttack is VaultSharedSetup { From 62a3ed0b4cc73e465a4a2f171da10ddfbc61a3b6 Mon Sep 17 00:00:00 2001 From: Jun Kim <64379343+junkim012@users.noreply.github.com> Date: Fri, 24 May 2024 14:50:42 -0400 Subject: [PATCH 2/6] feat: msg.sender protection for create2 deployment in vault factory --- src/vault/VaultFactory.sol | 23 ++- test/fork/concrete/vault/VaultFactory.t.sol | 179 +++++++++++++++++++- 2 files changed, 193 insertions(+), 9 deletions(-) diff --git a/src/vault/VaultFactory.sol b/src/vault/VaultFactory.sol index b2915058..070bfabc 100644 --- a/src/vault/VaultFactory.sol +++ b/src/vault/VaultFactory.sol @@ -12,6 +12,9 @@ import { SafeERC20 } from "openzeppelin-contracts/contracts/token/ERC20/utils/Sa */ contract VaultFactory { using SafeERC20 for IERC20; + + // --- Errors --- + error SaltMustBeginWithMsgSender(); // --- Events --- @@ -25,6 +28,22 @@ contract VaultFactory { address indexed initialDefaultAdmin ); + // --- Modifier --- + /** + * @dev Guarantees msg.sender protection for salts. If another caller + * frontruns a deployment transaction, the salt cannot be stolen. + * Taken from 0age's `Create2Factory`. + * https://github.com/0age/Pr000xy/blob/master/contracts/Create2Factory.sol + * @param salt The salt used for CREATE2 deployment. The first 20 bytes must + * be the msg.sender. + */ + modifier containsCaller(bytes32 salt) { + if (address(bytes20(salt)) != msg.sender) { + revert SaltMustBeginWithMsgSender(); + } + _; + } + // --- External --- /** @@ -42,7 +61,8 @@ contract VaultFactory { * @param symbol Symbol of the vault token. * @param initialDelay The initial delay for default admin transfers. * @param initialDefaultAdmin The initial default admin for the vault. - * @param salt The salt used for CREATE2 deployment. + * @param salt The salt used for CREATE2 deployment. The first 20 bytes must + * be the msg.sender. * @param marketsArgs Arguments for the markets to be added to the vault. * @param initialDeposit The initial deposit to be made to the vault. */ @@ -59,6 +79,7 @@ contract VaultFactory { uint256 initialDeposit ) external + containsCaller(salt) returns (Vault vault) { vault = new Vault{ salt: salt }( diff --git a/test/fork/concrete/vault/VaultFactory.t.sol b/test/fork/concrete/vault/VaultFactory.t.sol index b7201181..aafa46db 100644 --- a/test/fork/concrete/vault/VaultFactory.t.sol +++ b/test/fork/concrete/vault/VaultFactory.t.sol @@ -24,6 +24,12 @@ contract VaultFactoryTest is VaultSharedSetup { IIonPool[] internal newSupplyQueue; IIonPool[] internal newWithdrawQueue; + function _getSalt(address caller, bytes memory str) public returns (bytes32 salt) { + bytes32 keccak = keccak256(str); + salt = bytes32(abi.encodePacked(caller, keccak)); + require(address(bytes20(salt)) == caller, 'invalid salt creation'); + } + function setUp() public override { super.setUp(); @@ -54,8 +60,9 @@ contract VaultFactoryTest is VaultSharedSetup { BASE_ASSET.approve(address(factory), MIN_INITIAL_DEPOSIT); } - function test_CreateVault() public { - bytes32 salt = keccak256("random salt"); + function test_CreateVault_Basic() public { + bytes32 salt = _getSalt(address(this), "random salt"); + Vault vault = factory.createVault( baseAsset, feeRecipient, @@ -101,8 +108,9 @@ contract VaultFactoryTest is VaultSharedSetup { assertEq(vault.balanceOf(address(this)), MIN_INITIAL_DEPOSIT - 1e3, "deployer gets 1e3 less shares"); } - function test_CreateVault_Twice() public { - bytes32 salt = keccak256("first random salt"); + function test_CreateVault_SameBytecodeDifferentSalt() public { + bytes32 salt = _getSalt(address(this), "random salt"); + Vault vault = factory.createVault( baseAsset, feeRecipient, @@ -119,7 +127,7 @@ contract VaultFactoryTest is VaultSharedSetup { setERC20Balance(address(BASE_ASSET), address(this), MIN_INITIAL_DEPOSIT); BASE_ASSET.approve(address(factory), MIN_INITIAL_DEPOSIT); - bytes32 salt2 = keccak256("second random salt"); + bytes32 salt2 = _getSalt(address(this), "second random salt"); Vault vault2 = factory.createVault( baseAsset, feeRecipient, @@ -143,7 +151,8 @@ contract VaultFactoryTest is VaultSharedSetup { } function test_Revert_CreateVault_SameSaltTwice() public { - bytes32 salt = keccak256("random salt"); + bytes32 salt = _getSalt(address(this), "random salt"); + Vault vault = factory.createVault( baseAsset, feeRecipient, @@ -171,9 +180,71 @@ contract VaultFactoryTest is VaultSharedSetup { MIN_INITIAL_DEPOSIT ); } + + function test_Revert_SaltMustBeginWithMsgSender() public { + bytes32 salt = _getSalt(address(1), "random salt"); + require(address(this) != address(1)); + + vm.expectRevert(VaultFactory.SaltMustBeginWithMsgSender.selector); + Vault vault = factory.createVault( + baseAsset, + feeRecipient, + feePercentage, + name, + symbol, + INITIAL_DELAY, + VAULT_ADMIN, + salt, + marketsArgs, + MIN_INITIAL_DEPOSIT + ); + } + + /** + * If the salt begins with the same sender, but the ending bytes are + * different, it should deploy to different addresses. + */ + function test_Revert_SaltBeginsWithMsgSenderButDiffEnding() public { + bytes32 salt1 = _getSalt(address(this), "first random salt"); + bytes32 salt2 = _getSalt(address(this), "second random salt"); + + require(salt1 != salt2, 'salt must be different'); + + deal(address(BASE_ASSET), address(this), MIN_INITIAL_DEPOSIT); + BASE_ASSET.approve(address(factory), MIN_INITIAL_DEPOSIT); + Vault vault1 = factory.createVault( + baseAsset, + feeRecipient, + feePercentage, + name, + symbol, + INITIAL_DELAY, + VAULT_ADMIN, + salt1, + marketsArgs, + MIN_INITIAL_DEPOSIT + ); + + deal(address(BASE_ASSET), address(this), MIN_INITIAL_DEPOSIT); + BASE_ASSET.approve(address(factory), MIN_INITIAL_DEPOSIT); + Vault vault2 = factory.createVault( + baseAsset, + feeRecipient, + feePercentage, + name, + symbol, + INITIAL_DELAY, + VAULT_ADMIN, + salt2, + marketsArgs, + MIN_INITIAL_DEPOSIT + ); + + assertTrue(address(vault1) != address(vault2), "deployment addresses must be different"); + } function test_CreateVault_SameSaltDifferentBytecode() public { - bytes32 salt = keccak256("random salt"); + bytes32 salt = _getSalt(address(this), "random salt"); Vault vault = factory.createVault( BASE_ASSET, @@ -245,7 +316,7 @@ contract VaultFactoryTest is VaultSharedSetup { address deployer = newAddress("DEPLOYER"); // deploy using the factory which enforces minimum deposit of 1e9 assets // and the 1e3 shares burn. - bytes32 salt = keccak256("random salt"); + bytes32 salt = _getSalt(deployer, "random salt"); setERC20Balance(address(BASE_ASSET), deployer, MIN_INITIAL_DEPOSIT); @@ -348,4 +419,96 @@ contract VaultFactoryTest is VaultSharedSetup { assertLe(afterAssetBalance, initialAssetBalance, "attacker must not be profitable"); assertLe(1e18, initialAssetBalance - afterAssetBalance, "attacker loss greater than amount griefed"); } + + function test_Revert_Create2FrontrunSameConstructorArgDiffMsgSender() public { + address deployer = newAddress("DEPLOYER"); + address attacker = newAddress("ATTACKER"); + + deal(address(BASE_ASSET), deployer, MIN_INITIAL_DEPOSIT); + deal(address(BASE_ASSET), attacker, MIN_INITIAL_DEPOSIT); + + bytes32 deployerSalt = _getSalt(deployer, "random salt"); + + vm.startPrank(deployer); + BASE_ASSET.approve(address(factory), MIN_INITIAL_DEPOSIT); + Vault deployerVault = factory.createVault( + baseAsset, + feeRecipient, + feePercentage, + name, + symbol, + INITIAL_DELAY, + VAULT_ADMIN, + deployerSalt, + marketsArgs, + MIN_INITIAL_DEPOSIT + ); + vm.stopPrank(); + + vm.startPrank(attacker); + BASE_ASSET.approve(address(factory), MIN_INITIAL_DEPOSIT); + vm.expectRevert(); // create collision + Vault attackerVault = factory.createVault( + baseAsset, + feeRecipient, + feePercentage, + name, + symbol, + INITIAL_DELAY, + VAULT_ADMIN, + deployerSalt, + marketsArgs, + MIN_INITIAL_DEPOSIT + ); + vm.stopPrank(); + } + + /** + * Deployed with the same salt, but because the `feeRecipient` input address + * was changed, the attacker transaction deploys to a different address. + */ + function test_Create2FrontrunDifferentConstructorArgsAndDifferentSalt() public { + address deployer = newAddress("DEPLOYER"); + address attacker = newAddress("ATTACKER"); + + deal(address(BASE_ASSET), deployer, MIN_INITIAL_DEPOSIT); + deal(address(BASE_ASSET), attacker, MIN_INITIAL_DEPOSIT); + + bytes32 deployerSalt = _getSalt(deployer, "random"); + bytes32 attackerSalt = _getSalt(attacker, "random"); + + vm.startPrank(deployer); + BASE_ASSET.approve(address(factory), MIN_INITIAL_DEPOSIT); + Vault deployerVault = factory.createVault( + baseAsset, + feeRecipient, + feePercentage, + name, + symbol, + INITIAL_DELAY, + VAULT_ADMIN, + deployerSalt, + marketsArgs, + MIN_INITIAL_DEPOSIT + ); + vm.stopPrank(); + + vm.startPrank(attacker); + BASE_ASSET.approve(address(factory), MIN_INITIAL_DEPOSIT); + Vault attackerVault = factory.createVault( + baseAsset, + newAddress("ATTACKER_FRONTRUN_FEE_RECIPIENT"), + feePercentage, + name, + symbol, + INITIAL_DELAY, + VAULT_ADMIN, + attackerSalt, + marketsArgs, + MIN_INITIAL_DEPOSIT + ); + vm.stopPrank(); + + assertTrue(address(deployerVault) != address(attackerVault), "different deployment address"); + } } From 45f82ca9d595f7529c9a5aea606564c2d0098ce9 Mon Sep 17 00:00:00 2001 From: Jun Kim <64379343+junkim012@users.noreply.github.com> Date: Fri, 24 May 2024 18:57:45 -0400 Subject: [PATCH 3/6] test: fork fuzz test for vaults --- src/vault/VaultFactory.sol | 8 +- test/fork/concrete/vault/Vault.t.sol | 272 ++++++++------------ test/fork/concrete/vault/VaultFactory.t.sol | 38 ++- test/fork/fuzz/vault/Vault.t.sol | 112 ++++++++ test/helpers/VaultForkSharedSetup.sol | 112 ++++++++ test/helpers/VaultSharedSetup.sol | 6 + test/unit/fuzz/vault/Vault.t.sol | 2 +- 7 files changed, 355 insertions(+), 195 deletions(-) create mode 100644 test/fork/fuzz/vault/Vault.t.sol create mode 100644 test/helpers/VaultForkSharedSetup.sol diff --git a/src/vault/VaultFactory.sol b/src/vault/VaultFactory.sol index 070bfabc..dc3b92fb 100644 --- a/src/vault/VaultFactory.sol +++ b/src/vault/VaultFactory.sol @@ -12,8 +12,8 @@ import { SafeERC20 } from "openzeppelin-contracts/contracts/token/ERC20/utils/Sa */ contract VaultFactory { using SafeERC20 for IERC20; - - // --- Errors --- + + // --- Errors --- error SaltMustBeginWithMsgSender(); // --- Events --- @@ -28,11 +28,11 @@ contract VaultFactory { address indexed initialDefaultAdmin ); - // --- Modifier --- + // --- Modifier --- /** * @dev Guarantees msg.sender protection for salts. If another caller * frontruns a deployment transaction, the salt cannot be stolen. - * Taken from 0age's `Create2Factory`. + * Taken from 0age's `Create2Factory`. * https://github.com/0age/Pr000xy/blob/master/contracts/Create2Factory.sol * @param salt The salt used for CREATE2 deployment. The first 20 bytes must * be the msg.sender. diff --git a/test/fork/concrete/vault/Vault.t.sol b/test/fork/concrete/vault/Vault.t.sol index c2dd2fa6..eaad6692 100644 --- a/test/fork/concrete/vault/Vault.t.sol +++ b/test/fork/concrete/vault/Vault.t.sol @@ -1,168 +1,104 @@ -// // SPDX-License-Identifier: MIT -// pragma solidity 0.8.21; - -// import { Vault } from "./../../../../src/vault/Vault.sol"; -// import { IonPool } from "./../../../../src/IonPool.sol"; -// import { Whitelist } from "./../../../../src/Whitelist.sol"; -// import { IIonPool } from "./../../../../src/interfaces/IIonPool.sol"; -// import { IonLens } from "./../../../../src/periphery/IonLens.sol"; -// import { WSTETH_ADDRESS } from "./../../../../src/Constants.sol"; -// import { IERC20 } from "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; -// import { EnumerableSet } from "openzeppelin-contracts/contracts/utils/structs/EnumerableSet.sol"; -// // import { StdStorage, stdStorage } from "../../../../lib/forge-safe/lib/forge-std/src/StdStorage.sol"; - -// import "forge-std/Test.sol"; -// import { console2 } from "forge-std/console2.sol"; - -// using EnumerableSet for EnumerableSet.AddressSet; - -// IIonPool constant WEETH_IONPOOL = IIonPool(0x0000000000eaEbd95dAfcA37A39fd09745739b78); -// IIonPool constant RSETH_IONPOOL = IIonPool(0x0000000000E33e35EE6052fae87bfcFac61b1da9); -// IIonPool constant RSWETH_IONPOOL = IIonPool(0x00000000007C8105548f9d0eE081987378a6bE93); -// Whitelist constant WHITELIST = Whitelist(0x7E317f99aA313669AaCDd8dB3927ff3aCB562dAD); -// bytes32 constant ION_ROLE = 0x5ab1a5ffb29c47d95dec8c5f9ad49a551754822b51a3359ed1c21e2be24beefa; - -// address constant VAULT_OWNER = address(1); -// address constant FEE_RECIPIENT = address(2); -// IERC20 constant BASE_ASSET = WSTETH_ADDRESS; - -// contract VaultForkBase is Test { -// using stdStorage for StdStorage; - -// StdStorage stdstore1; -// IonLens public ionLens; -// Vault vault; - -// uint256 internal forkBlock = 0; -// address internal poolAdmin; - -// IIonPool[] markets; - -// function setERC20Balance(address token, address usr, uint256 amt) public { -// stdstore1.target(token).sig(IERC20(token).balanceOf.selector).with_key(usr).checked_write(amt); -// require(IERC20(token).balanceOf(usr) == amt, "balance not set"); -// } - -// function updateSupplyCap(IIonPool pool, uint256 cap) internal { -// vm.startPrank(poolAdmin); -// pool.updateSupplyCap(cap); -// vm.stopPrank(); -// assertEq(ionLens.wethSupplyCap(pool), cap, "supply cap update"); -// } - -// function _updateImpl(IIonPool proxy, IIonPool impl) internal { -// vm.store( -// address(proxy), -// 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc, -// bytes32(uint256(uint160(address(impl)))) -// ); -// } - -// function setUp() public virtual { -// if (forkBlock == 0) vm.createSelectFork(vm.envString("MAINNET_ARCHIVE_RPC_URL")); -// else vm.createSelectFork(vm.envString("MAINNET_ARCHIVE_RPC_URL"), forkBlock); - -// poolAdmin = WEETH_IONPOOL.defaultAdmin(); -// // update fork contract to the latest implementation with -// // transferradbility + non-rebasing + removed getters -// ionLens = new IonLens(); - -// IonPool updatedImpl = new IonPool(); - -// _updateImpl(WEETH_IONPOOL, IIonPool(address(updatedImpl))); -// _updateImpl(RSETH_IONPOOL, IIonPool(address(updatedImpl))); -// _updateImpl(RSWETH_IONPOOL, IIonPool(address(updatedImpl))); - -// vault = new Vault(VAULT_OWNER, FEE_RECIPIENT, BASE_ASSET, ionLens, "Ion Vault Token", "IVT"); - -// markets = new IIonPool[](3); -// markets[0] = WEETH_IONPOOL; -// markets[1] = RSETH_IONPOOL; -// markets[2] = RSWETH_IONPOOL; - -// vm.startPrank(vault.owner()); - -// vault.addSupportedMarkets(markets); -// vault.updateSupplyQueue(markets); -// vault.updateWithdrawQueue(markets); - -// vm.stopPrank(); - -// BASE_ASSET.approve(address(vault), type(uint256).max); - -// vm.prank(poolAdmin); -// WHITELIST.updateLendersRoot(bytes32(0)); -// } -// } - -// /** -// * Vault state that needs to be checked -// * - Vault's shares total supply -// * - Vault's total iToken balance -// * - User's vault shares balance -// */ -// contract Vault_ForkTest is VaultForkBase { -// function setUp() public override { -// super.setUp(); - -// uint256[] memory newCaps = new uint256[](3); -// newCaps[0] = 100e18; -// newCaps[1] = 100e18; -// newCaps[2] = 100e18; -// vm.prank(vault.owner()); -// vault.updateAllocationCaps(markets, newCaps); -// } - -// /** -// * Because the first market's allocation cap and supply cap is high enough, -// * all deposits go into the first market. -// */ -// function test_DepositFirstMarketOnly() public { -// setERC20Balance(address(BASE_ASSET), address(this), 100e18); -// console2.log("BASE_ASSET.balanceOf(address(this)): ", BASE_ASSET.balanceOf(address(this))); - -// IIonPool pool1 = vault.supplyQueue(0); -// IIonPool pool2 = vault.supplyQueue(1); -// IIonPool pool3 = vault.supplyQueue(2); - -// console2.log("pool1: ", address(pool1)); -// updateSupplyCap(pool1, type(uint256).max); - -// vault.deposit(100e18, address(this)); - -// // vault shares -// // vault iToken balance -// } - -// function test_Withdraw() public { } -// } - -// contract Vault_ForkTest_WithRateAccrual is VaultForkBase { } - -// contract Vault_ForkFuzzTest is VaultForkBase { -// function setUp() public override { -// super.setUp(); -// } - -// /** -// * Start each pool at a random total supply amount and a supply cap. -// * The total supply amount must be less than or equal to the supply cap. -// * Deposit random amount of assets into the vault. -// * The deposit amount must be equal to the change in deposits across all lending pools. -// * It should only revert if the total available supply amount is less than the deposit amount. -// */ -// function testForkFuzz_DepositAllMarkets(uint256 assets) public { } - -// function testFuzz_Withdraw(uint256 assets) public { } - -// /** -// * The totalAssets should return the total rebased claim across all markets at a given time. -// */ -// function testFuzz_totalAssets() public { } - -// /** -// * The total amount of fees collected at a random time in the futuer should be split -// * between the vault depositors and the fee recipient. -// */ -// function testFuzz_Fees() public { } -// } +// SPDX-License-Identifier: MIT +pragma solidity 0.8.21; + +import { Vault } from "./../../../../src/vault/Vault.sol"; + +import { VaultForkBase } from "./../../../helpers/VaultForkSharedSetup.sol"; + +contract Vault_ForkTest is VaultForkBase { + function test_Deposit_MaxDeposit_MaxWithdraw() public { + uint256 totalAssets = vault.totalAssets(); + uint256 totalSupply = vault.totalSupply(); + + uint256 maxDeposit = vault.maxDeposit(NULL); + require(maxDeposit > 0, "max deposit"); + + uint256 expectedSharesMinted = vault.previewDeposit(maxDeposit); + + deal(address(BASE_ASSET), address(this), maxDeposit); + BASE_ASSET.approve(address(vault), maxDeposit); + + uint256 resultingSharesMinted = vault.deposit(maxDeposit, address(this)); + + uint256 totalAssetsAfterDeposit = vault.totalAssets(); + uint256 totalSupplyAfterDeposit = vault.totalSupply(); + + // user + assertEq(BASE_ASSET.balanceOf(address(this)), 0, "requested assets deposited"); + assertEq(resultingSharesMinted, expectedSharesMinted, "shares minted"); + assertEq(vault.balanceOf(address(this)), resultingSharesMinted, "vault shares"); + + // vault + assertEq(resultingSharesMinted, totalSupplyAfterDeposit - totalSupply, "vault total supply after deposit"); + assertApproxEqAbs(maxDeposit, totalAssetsAfterDeposit - totalAssets, 3, "vault total assets after deposit"); // 1 + // wei error per market + + uint256 withdrawAmt = vault.maxWithdraw(address(this)); + + uint256 expectedSharesRedeemed = vault.previewWithdraw(withdrawAmt); + uint256 resultingSharesRedeemed = vault.withdraw(withdrawAmt, address(this), address(this)); + + uint256 totalAssetsAfterWithdraw = vault.totalAssets(); + uint256 totalSupplyAfterWithdraw = vault.totalSupply(); + + // user + assertEq(BASE_ASSET.balanceOf(address(this)), withdrawAmt, "requested assets withdrawn"); + assertEq(resultingSharesRedeemed, expectedSharesRedeemed, "shares redeemed"); + + // vault + assertEq( + resultingSharesRedeemed, totalSupplyAfterDeposit - totalSupplyAfterWithdraw, "total supply after withdraw" + ); + assertEq(withdrawAmt, totalAssetsAfterDeposit - totalAssetsAfterWithdraw, "total assets after withdraw"); + } + + function test_Deposit_MaxMint_MaxRedeem() public { + uint256 totalAssets = vault.totalAssets(); + uint256 totalSupply = vault.totalSupply(); + + uint256 maxMint = vault.maxMint(NULL); + require(maxMint > 0, "max mint"); + + uint256 expectedAssetsDeposited = vault.previewMint(maxMint); + + deal(address(BASE_ASSET), address(this), expectedAssetsDeposited); + BASE_ASSET.approve(address(vault), expectedAssetsDeposited); + + uint256 resultingAssetsDeposited = vault.mint(maxMint, address(this)); + + uint256 totalAssetsAfterMint = vault.totalAssets(); + uint256 totalSupplyAfterMint = vault.totalSupply(); + + // user + assertEq(BASE_ASSET.balanceOf(address(this)), 0, "requested assets deposited"); + assertEq(resultingAssetsDeposited, expectedAssetsDeposited, "assets deposited"); + assertEq(vault.balanceOf(address(this)), maxMint, "vault shares"); + + // vault + assertEq(maxMint, totalSupplyAfterMint - totalSupply, "vault total supply after deposit"); + assertApproxEqAbs( + resultingAssetsDeposited, totalAssetsAfterMint - totalAssets, 3, "vault total assets after deposit" + ); // 1 wei error per market + + uint256 prevShares = vault.balanceOf(address(this)); + + uint256 redeemAmt = vault.maxRedeem(address(this)); + uint256 expectedWithdrawAmt = vault.previewRedeem(redeemAmt); + uint256 resultingWithdrawAmt = vault.redeem(redeemAmt, address(this), address(this)); + + uint256 totalAssetsAfterRedeem = vault.totalAssets(); + uint256 totalSupplyAfterRedeem = vault.totalSupply(); + uint256 sharesDiff = prevShares - vault.balanceOf(address(this)); + + // user + assertEq(BASE_ASSET.balanceOf(address(this)), expectedWithdrawAmt, "requested assets withdrawn"); + assertEq(resultingWithdrawAmt, expectedWithdrawAmt, "withdraw amount"); + assertEq(sharesDiff, redeemAmt, "redeem amount"); + + // vault + assertEq(redeemAmt, totalSupplyAfterMint - totalSupplyAfterRedeem, "total supply after withdraw"); + assertApproxEqAbs( + expectedWithdrawAmt, totalAssetsAfterMint - totalAssetsAfterRedeem, 1, "total assets after withdraw" + ); + } +} diff --git a/test/fork/concrete/vault/VaultFactory.t.sol b/test/fork/concrete/vault/VaultFactory.t.sol index aafa46db..1e56f518 100644 --- a/test/fork/concrete/vault/VaultFactory.t.sol +++ b/test/fork/concrete/vault/VaultFactory.t.sol @@ -24,12 +24,6 @@ contract VaultFactoryTest is VaultSharedSetup { IIonPool[] internal newSupplyQueue; IIonPool[] internal newWithdrawQueue; - function _getSalt(address caller, bytes memory str) public returns (bytes32 salt) { - bytes32 keccak = keccak256(str); - salt = bytes32(abi.encodePacked(caller, keccak)); - require(address(bytes20(salt)) == caller, 'invalid salt creation'); - } - function setUp() public override { super.setUp(); @@ -61,7 +55,7 @@ contract VaultFactoryTest is VaultSharedSetup { } function test_CreateVault_Basic() public { - bytes32 salt = _getSalt(address(this), "random salt"); + bytes32 salt = _getSalt(address(this), "random salt"); Vault vault = factory.createVault( baseAsset, @@ -109,7 +103,7 @@ contract VaultFactoryTest is VaultSharedSetup { } function test_CreateVault_SameBytecodeDifferentSalt() public { - bytes32 salt = _getSalt(address(this), "random salt"); + bytes32 salt = _getSalt(address(this), "random salt"); Vault vault = factory.createVault( baseAsset, @@ -127,7 +121,7 @@ contract VaultFactoryTest is VaultSharedSetup { setERC20Balance(address(BASE_ASSET), address(this), MIN_INITIAL_DEPOSIT); BASE_ASSET.approve(address(factory), MIN_INITIAL_DEPOSIT); - bytes32 salt2 = _getSalt(address(this), "second random salt"); + bytes32 salt2 = _getSalt(address(this), "second random salt"); Vault vault2 = factory.createVault( baseAsset, feeRecipient, @@ -151,7 +145,7 @@ contract VaultFactoryTest is VaultSharedSetup { } function test_Revert_CreateVault_SameSaltTwice() public { - bytes32 salt = _getSalt(address(this), "random salt"); + bytes32 salt = _getSalt(address(this), "random salt"); Vault vault = factory.createVault( baseAsset, @@ -180,7 +174,7 @@ contract VaultFactoryTest is VaultSharedSetup { MIN_INITIAL_DEPOSIT ); } - + function test_Revert_SaltMustBeginWithMsgSender() public { bytes32 salt = _getSalt(address(1), "random salt"); require(address(this) != address(1)); @@ -197,7 +191,7 @@ contract VaultFactoryTest is VaultSharedSetup { salt, marketsArgs, MIN_INITIAL_DEPOSIT - ); + ); } /** @@ -205,10 +199,10 @@ contract VaultFactoryTest is VaultSharedSetup { * different, it should deploy to different addresses. */ function test_Revert_SaltBeginsWithMsgSenderButDiffEnding() public { - bytes32 salt1 = _getSalt(address(this), "first random salt"); - bytes32 salt2 = _getSalt(address(this), "second random salt"); + bytes32 salt1 = _getSalt(address(this), "first random salt"); + bytes32 salt2 = _getSalt(address(this), "second random salt"); - require(salt1 != salt2, 'salt must be different'); + require(salt1 != salt2, "salt must be different"); deal(address(BASE_ASSET), address(this), MIN_INITIAL_DEPOSIT); BASE_ASSET.approve(address(factory), MIN_INITIAL_DEPOSIT); @@ -238,13 +232,13 @@ contract VaultFactoryTest is VaultSharedSetup { salt2, marketsArgs, MIN_INITIAL_DEPOSIT - ); + ); assertTrue(address(vault1) != address(vault2), "deployment addresses must be different"); } function test_CreateVault_SameSaltDifferentBytecode() public { - bytes32 salt = _getSalt(address(this), "random salt"); + bytes32 salt = _getSalt(address(this), "random salt"); Vault vault = factory.createVault( BASE_ASSET, @@ -316,7 +310,7 @@ contract VaultFactoryTest is VaultSharedSetup { address deployer = newAddress("DEPLOYER"); // deploy using the factory which enforces minimum deposit of 1e9 assets // and the 1e3 shares burn. - bytes32 salt = _getSalt(deployer, "random salt"); + bytes32 salt = _getSalt(deployer, "random salt"); setERC20Balance(address(BASE_ASSET), deployer, MIN_INITIAL_DEPOSIT); @@ -460,7 +454,7 @@ contract VaultFactoryTest is VaultSharedSetup { marketsArgs, MIN_INITIAL_DEPOSIT ); - vm.stopPrank(); + vm.stopPrank(); } /** @@ -474,8 +468,8 @@ contract VaultFactoryTest is VaultSharedSetup { deal(address(BASE_ASSET), deployer, MIN_INITIAL_DEPOSIT); deal(address(BASE_ASSET), attacker, MIN_INITIAL_DEPOSIT); - bytes32 deployerSalt = _getSalt(deployer, "random"); - bytes32 attackerSalt = _getSalt(attacker, "random"); + bytes32 deployerSalt = _getSalt(deployer, "random"); + bytes32 attackerSalt = _getSalt(attacker, "random"); vm.startPrank(deployer); BASE_ASSET.approve(address(factory), MIN_INITIAL_DEPOSIT); @@ -507,7 +501,7 @@ contract VaultFactoryTest is VaultSharedSetup { marketsArgs, MIN_INITIAL_DEPOSIT ); - vm.stopPrank(); + vm.stopPrank(); assertTrue(address(deployerVault) != address(attackerVault), "different deployment address"); } diff --git a/test/fork/fuzz/vault/Vault.t.sol b/test/fork/fuzz/vault/Vault.t.sol new file mode 100644 index 00000000..ecbfba44 --- /dev/null +++ b/test/fork/fuzz/vault/Vault.t.sol @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.21; + +import { VaultForkBase } from "./../../../helpers/VaultForkSharedSetup.sol"; + +contract Vault_ForkFuzzTest is VaultForkBase { + function test_Deposit_BelowMaxDeposit_Withdraw_BelowMaxWithdraw(uint256 assets) public { + uint256 totalAssets = vault.totalAssets(); + uint256 totalSupply = vault.totalSupply(); + + uint256 maxDeposit = vault.maxDeposit(NULL); + require(maxDeposit > 0, "max deposit"); + + uint256 depositAmt = bound(assets, 0, maxDeposit); + + uint256 expectedSharesMinted = vault.previewDeposit(depositAmt); + + deal(address(BASE_ASSET), address(this), depositAmt); + BASE_ASSET.approve(address(vault), depositAmt); + + uint256 resultingSharesMinted = vault.deposit(depositAmt, address(this)); + + uint256 totalAssetsAfterDeposit = vault.totalAssets(); + uint256 totalSupplyAfterDeposit = vault.totalSupply(); + + // user + assertEq(BASE_ASSET.balanceOf(address(this)), 0, "requested assets deposited"); + assertEq(resultingSharesMinted, expectedSharesMinted, "shares minted"); + assertEq(vault.balanceOf(address(this)), resultingSharesMinted, "vault shares"); + + // vault + assertEq(resultingSharesMinted, totalSupplyAfterDeposit - totalSupply, "vault total supply after deposit"); + assertApproxEqAbs(depositAmt, totalAssetsAfterDeposit - totalAssets, 3, "vault total assets after deposit"); // 1 + // wei error per market + + // tries to withdraw max + uint256 maxWithdraw = vault.maxWithdraw(address(this)); + uint256 withdrawAmt = bound(assets, 0, maxWithdraw); + + uint256 expectedSharesRedeemed = vault.previewWithdraw(withdrawAmt); + uint256 resultingSharesRedeemed = vault.withdraw(withdrawAmt, address(this), address(this)); + + uint256 totalAssetsAfterWithdraw = vault.totalAssets(); + uint256 totalSupplyAfterWithdraw = vault.totalSupply(); + + // user + assertEq(BASE_ASSET.balanceOf(address(this)), withdrawAmt, "requested assets withdrawn"); + assertEq(resultingSharesRedeemed, expectedSharesRedeemed, "shares redeemed"); + + // vault + assertEq( + resultingSharesRedeemed, totalSupplyAfterDeposit - totalSupplyAfterWithdraw, "total supply after withdraw" + ); + assertApproxEqAbs( + withdrawAmt, totalAssetsAfterDeposit - totalAssetsAfterWithdraw, 1, "total assets after withdraw" + ); + } + + function test_Mint_BelowMaxMint_Redeem_BelowMaxRedeem(uint256 assets) public { + uint256 totalAssets = vault.totalAssets(); + uint256 totalSupply = vault.totalSupply(); + + uint256 maxMint = vault.maxMint(NULL); + require(maxMint > 0, "max mint"); + + uint256 mintAmt = bound(assets, 0, maxMint); + + uint256 expectedAssetsDeposited = vault.previewMint(mintAmt); + + deal(address(BASE_ASSET), address(this), expectedAssetsDeposited); + BASE_ASSET.approve(address(vault), expectedAssetsDeposited); + + uint256 resultingAssetsDeposited = vault.mint(mintAmt, address(this)); + + uint256 totalAssetsAfterMint = vault.totalAssets(); + uint256 totalSupplyAfterMint = vault.totalSupply(); + + // user + assertEq(BASE_ASSET.balanceOf(address(this)), 0, "requested assets deposited"); + assertEq(resultingAssetsDeposited, expectedAssetsDeposited, "assets deposited"); + assertEq(vault.balanceOf(address(this)), mintAmt, "vault shares"); + + // vault + assertEq(mintAmt, totalSupplyAfterMint - totalSupply, "vault total supply after deposit"); + assertApproxEqAbs( + resultingAssetsDeposited, totalAssetsAfterMint - totalAssets, 3, "vault total assets after deposit" + ); // 1 wei error per market + + uint256 prevShares = vault.balanceOf(address(this)); + + uint256 maxRedeem = vault.maxRedeem(address(this)); + uint256 redeemAmt = bound(assets, 0, maxRedeem); + + uint256 expectedWithdrawAmt = vault.previewRedeem(redeemAmt); + uint256 resultingWithdrawAmt = vault.redeem(redeemAmt, address(this), address(this)); + + uint256 totalAssetsAfterRedeem = vault.totalAssets(); + uint256 totalSupplyAfterRedeem = vault.totalSupply(); + uint256 sharesDiff = prevShares - vault.balanceOf(address(this)); + + // user + assertEq(BASE_ASSET.balanceOf(address(this)), expectedWithdrawAmt, "requested assets withdrawn"); + assertEq(resultingWithdrawAmt, expectedWithdrawAmt, "withdraw amount"); + assertEq(sharesDiff, redeemAmt, "redeem amount"); + + // vault + assertEq(redeemAmt, totalSupplyAfterMint - totalSupplyAfterRedeem, "total supply after withdraw"); + assertApproxEqAbs( + expectedWithdrawAmt, totalAssetsAfterMint - totalAssetsAfterRedeem, 1, "total assets after withdraw" + ); + } +} diff --git a/test/helpers/VaultForkSharedSetup.sol b/test/helpers/VaultForkSharedSetup.sol new file mode 100644 index 00000000..19ef3239 --- /dev/null +++ b/test/helpers/VaultForkSharedSetup.sol @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.21; + +import { VaultFactory } from "./../../../../src/vault/VaultFactory.sol"; +import { Vault } from "./../../../../src/vault/Vault.sol"; +import { IIonPool } from "./../../../../src/interfaces/IIonPool.sol"; +import { IonLens } from "./../../../../src/periphery/IonLens.sol"; +import { WSTETH_ADDRESS } from "./../../../../src/Constants.sol"; +import { IERC20 } from "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; +import { EnumerableSet } from "openzeppelin-contracts/contracts/utils/structs/EnumerableSet.sol"; + +import "forge-std/Test.sol"; + +using EnumerableSet for EnumerableSet.AddressSet; + +contract VaultForkBase is Test { + IIonPool constant WEETH_IONPOOL = IIonPool(0x0000000000eaEbd95dAfcA37A39fd09745739b78); + IIonPool constant RSETH_IONPOOL = IIonPool(0x0000000000E33e35EE6052fae87bfcFac61b1da9); + IIonPool constant RSWETH_IONPOOL = IIonPool(0x00000000007C8105548f9d0eE081987378a6bE93); + IonLens constant LENS = IonLens(0xe89AF12af000C4f76a57A3aD16ef8277a727DC81); + bytes32 constant ION_ROLE = 0x5ab1a5ffb29c47d95dec8c5f9ad49a551754822b51a3359ed1c21e2be24beefa; + + IERC20 constant BASE_ASSET = WSTETH_ADDRESS; + + address FEE_RECIPIENT = address(1); + + uint48 constant INITIAL_DELAY = 0; + uint256 constant MIN_INITIAL_DEPOSIT = 1e3; + + address VAULT_ADMIN = 0x0000000000417626Ef34D62C4DC189b021603f2F; + + IIonPool constant IDLE = IIonPool(address(uint160(uint256(keccak256("IDLE_ASSET_HOLDINGS"))))); + + address constant NULL = address(0); + uint256 constant DEFAULT_ALLO_CAO = type(uint128).max; + + IIonPool[] markets; + IIonPool[] supplyQueue; + IIonPool[] withdrawQueue; + + Vault.MarketsArgs marketsArgs; + + uint256[] allocationCaps; + + VaultFactory factory; + Vault vault; + + uint256 internal forkBlock = 0; + + function updateSupplyCap(IIonPool pool, uint256 cap) internal { + vm.startPrank(pool.defaultAdmin()); + pool.updateSupplyCap(cap); + vm.stopPrank(); + assertEq(LENS.supplyCap(pool), cap, "supply cap update"); + } + + function setUp() public virtual { + if (forkBlock == 0) vm.createSelectFork(vm.envString("MAINNET_ARCHIVE_RPC_URL")); + else vm.createSelectFork(vm.envString("MAINNET_ARCHIVE_RPC_URL"), forkBlock); + + factory = new VaultFactory(); + + markets.push(IDLE); + markets.push(WEETH_IONPOOL); + markets.push(RSETH_IONPOOL); + markets.push(RSWETH_IONPOOL); + + allocationCaps.push(DEFAULT_ALLO_CAO); + allocationCaps.push(DEFAULT_ALLO_CAO); + allocationCaps.push(DEFAULT_ALLO_CAO); + allocationCaps.push(DEFAULT_ALLO_CAO); + + supplyQueue.push(WEETH_IONPOOL); + supplyQueue.push(RSETH_IONPOOL); + supplyQueue.push(RSWETH_IONPOOL); + supplyQueue.push(IDLE); + + withdrawQueue.push(IDLE); + withdrawQueue.push(RSWETH_IONPOOL); + withdrawQueue.push(RSETH_IONPOOL); + withdrawQueue.push(WEETH_IONPOOL); + + marketsArgs.marketsToAdd = markets; + marketsArgs.allocationCaps = allocationCaps; + marketsArgs.newSupplyQueue = supplyQueue; + marketsArgs.newWithdrawQueue = withdrawQueue; + + uint256 feePercentage = 0.02e27; + + bytes32 salt = bytes32(abi.encodePacked(address(this), keccak256("random salt"))); + + deal(address(BASE_ASSET), address(this), MIN_INITIAL_DEPOSIT); + BASE_ASSET.approve(address(factory), MIN_INITIAL_DEPOSIT); + vault = factory.createVault( + BASE_ASSET, + FEE_RECIPIENT, + feePercentage, + "Ion Vault Token", + "IVT", + INITIAL_DELAY, + VAULT_ADMIN, + salt, + marketsArgs, + MIN_INITIAL_DEPOSIT + ); + + require(vault.supplyQueue(0) == WEETH_IONPOOL); + require(vault.supplyQueue(1) == RSETH_IONPOOL); + require(vault.supplyQueue(2) == RSWETH_IONPOOL); + require(vault.supplyQueue(3) == IDLE); + } +} diff --git a/test/helpers/VaultSharedSetup.sol b/test/helpers/VaultSharedSetup.sol index 4fd5e314..da1a5987 100644 --- a/test/helpers/VaultSharedSetup.sol +++ b/test/helpers/VaultSharedSetup.sol @@ -167,6 +167,12 @@ contract VaultSharedSetup is IonPoolSharedSetup { ionPool.grantRole(ionPool.GEM_JOIN_ROLE(), address(gemJoin)); } + function _getSalt(address caller, bytes memory str) internal returns (bytes32 salt) { + bytes32 keccak = keccak256(str); + salt = bytes32(abi.encodePacked(caller, keccak)); + require(address(bytes20(salt)) == caller, "invalid salt creation"); + } + function claimAfterDeposit(uint256 currShares, uint256 amount, uint256 supplyFactor) internal returns (uint256) { uint256 sharesMinted = amount.rayDivDown(supplyFactor); uint256 resultingShares = currShares + sharesMinted; diff --git a/test/unit/fuzz/vault/Vault.t.sol b/test/unit/fuzz/vault/Vault.t.sol index ccc22318..2384b5a9 100644 --- a/test/unit/fuzz/vault/Vault.t.sol +++ b/test/unit/fuzz/vault/Vault.t.sol @@ -952,7 +952,7 @@ contract VaultInflationAttack is VaultSharedSetup { // deploy using the factory which enforces minimum deposit of 1e9 assets // and the 1e3 shares burn. - bytes32 salt = keccak256("random salt"); + bytes32 salt = _getSalt(deployer, "random salt"); setERC20Balance(address(BASE_ASSET), deployer, MIN_INITIAL_DEPOSIT); From ddd66e3b1b10344ddac8ec11bbed53de9caa2e06 Mon Sep 17 00:00:00 2001 From: Jun Kim <64379343+junkim012@users.noreply.github.com> Date: Fri, 24 May 2024 19:22:21 -0400 Subject: [PATCH 4/6] feat: add in invalid fee recipient and fee percentage checks to the constructor --- src/vault/Vault.sol | 3 +++ test/unit/concrete/vault/Vault.t.sol | 16 ++++++++++++++++ test/unit/fuzz/vault/Vault.t.sol | 2 +- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/vault/Vault.sol b/src/vault/Vault.sol index 29a6fc61..4b82c1c0 100644 --- a/src/vault/Vault.sol +++ b/src/vault/Vault.sol @@ -114,6 +114,9 @@ contract Vault is ERC4626, Multicall, AccessControlDefaultAdminRules, Reentrancy { BASE_ASSET = _baseAsset; + if (_feePercentage > RAY) revert InvalidFeePercentage(); + if (_feeRecipient == address(0)) revert InvalidFeeRecipient(); + feePercentage = _feePercentage; feeRecipient = _feeRecipient; diff --git a/test/unit/concrete/vault/Vault.t.sol b/test/unit/concrete/vault/Vault.t.sol index 66731f03..45b83c64 100644 --- a/test/unit/concrete/vault/Vault.t.sol +++ b/test/unit/concrete/vault/Vault.t.sol @@ -24,6 +24,22 @@ contract VaultSetUpTest is VaultSharedSetup { super.setUp(); } + function test_Revert_InvalidFeeRecipientInConstructor() public { + address feeRecipient = address(0); + vm.expectRevert(Vault.InvalidFeeRecipient.selector); + vault = new Vault( + BASE_ASSET, address(0), ZERO_FEES, "Ion Vault Token", "IVT", INITIAL_DELAY, VAULT_ADMIN, emptyMarketsArgs + ); + } + + function test_Revert_InvalidFeePercentageInConstructor() public { + uint256 feePerc = 1e27 + 1; + vm.expectRevert(Vault.InvalidFeePercentage.selector); + vault = new Vault( + BASE_ASSET, FEE_RECIPIENT, feePerc, "Ion Vault Token", "IVT", INITIAL_DELAY, VAULT_ADMIN, emptyMarketsArgs + ); + } + function test_AddSupportedMarketsSeparately() public { vault = new Vault( BASE_ASSET, FEE_RECIPIENT, ZERO_FEES, "Ion Vault Token", "IVT", INITIAL_DELAY, VAULT_ADMIN, emptyMarketsArgs diff --git a/test/unit/fuzz/vault/Vault.t.sol b/test/unit/fuzz/vault/Vault.t.sol index 2384b5a9..2eb4923b 100644 --- a/test/unit/fuzz/vault/Vault.t.sol +++ b/test/unit/fuzz/vault/Vault.t.sol @@ -250,7 +250,7 @@ contract VaultWithYieldAndFeeSharedSetup is VaultSharedSetup { contract VaultWithYieldAndFee_Fuzz_FeeAccrual is VaultWithYieldAndFeeSharedSetup { function testFuzz_AccruedFeeShares(uint256 initialDeposit, uint256 feePerc, uint256 daysAccrued) public { // fee percentage - feePerc = bound(feePerc, 0, RAY - 1); + feePerc = bound(feePerc, 0, RAY); vm.prank(OWNER); vault.updateFeePercentage(feePerc); From 6408847fe71be27249925be7a11f85b214f95d4c Mon Sep 17 00:00:00 2001 From: Jun Kim <64379343+junkim012@users.noreply.github.com> Date: Sat, 25 May 2024 02:29:23 -0400 Subject: [PATCH 5/6] fix: split embedded Vault bytecode from the factory to obey codesize limit --- src/vault/VaultBytecode.sol | 55 +++++++++++++++++++++ src/vault/VaultFactory.sol | 9 +++- test/fork/concrete/vault/VaultFactory.t.sol | 7 +-- test/helpers/VaultForkSharedSetup.sol | 25 ++++++---- test/helpers/VaultSharedSetup.sol | 13 +++++ test/unit/fuzz/vault/Vault.t.sol | 3 -- 6 files changed, 93 insertions(+), 19 deletions(-) create mode 100644 src/vault/VaultBytecode.sol diff --git a/src/vault/VaultBytecode.sol b/src/vault/VaultBytecode.sol new file mode 100644 index 00000000..6f778395 --- /dev/null +++ b/src/vault/VaultBytecode.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity 0.8.21; + +import { Vault } from "./Vault.sol"; +import { IERC20 } from "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; + +/** + * @title VaultBytecode + * @author Molecular Labs + * @notice The sole job of this contract is to deploy the embedded `Vault` + * contract's bytecode with the constructor args. `VaultFactory` handles rest of + * the verification and post-deployment logic. + */ +contract VaultBytecode { + error OnlyFactory(); + + address constant VAULT_FACTORY = 0x0000000000D7DC416dFe993b0E3dd53BA3E27Fc8; + + /** + * @notice Deploys the embedded `Vault` bytecode with the given constructor + * args. Only the `VaultFactory` contract can call this function. + * @dev This contract was separated from `VaultFactory` to reduce the + * codesize of the factory contract. + * @param baseAsset The asset that is being lent out to IonPools. + * @param feeRecipient Address that receives the accrued manager fees. + * @param feePercentage Fee percentage to be set. + * @param name Name of the vault token. + * @param symbol Symbol of the vault token. + * @param initialDelay The initial delay for default admin transfers. + * @param initialDefaultAdmin The initial default admin for the vault. + * @param salt The salt used for CREATE2 deployment. The first 20 bytes must + * be the msg.sender. + * @param marketsArgs Arguments for the markets to be added to the vault. + */ + function deploy( + IERC20 baseAsset, + address feeRecipient, + uint256 feePercentage, + string memory name, + string memory symbol, + uint48 initialDelay, + address initialDefaultAdmin, + bytes32 salt, + Vault.MarketsArgs memory marketsArgs + ) + external + returns (Vault vault) + { + if (msg.sender != VAULT_FACTORY) revert OnlyFactory(); + + vault = new Vault{ salt: salt }( + baseAsset, feeRecipient, feePercentage, name, symbol, initialDelay, initialDefaultAdmin, marketsArgs + ); + } +} diff --git a/src/vault/VaultFactory.sol b/src/vault/VaultFactory.sol index dc3b92fb..25ca1a70 100644 --- a/src/vault/VaultFactory.sol +++ b/src/vault/VaultFactory.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity 0.8.21; +import { VaultBytecode } from "./VaultBytecode.sol"; import { Vault } from "./Vault.sol"; import { IERC20 } from "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; import { SafeERC20 } from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; @@ -14,6 +15,7 @@ contract VaultFactory { using SafeERC20 for IERC20; // --- Errors --- + error SaltMustBeginWithMsgSender(); // --- Events --- @@ -28,7 +30,10 @@ contract VaultFactory { address indexed initialDefaultAdmin ); + VaultBytecode constant BYTECODE_DEPLOYER = VaultBytecode(0x0000000000382a154e4A696A8C895b4292fA3D82); + // --- Modifier --- + /** * @dev Guarantees msg.sender protection for salts. If another caller * frontruns a deployment transaction, the salt cannot be stolen. @@ -82,8 +87,8 @@ contract VaultFactory { containsCaller(salt) returns (Vault vault) { - vault = new Vault{ salt: salt }( - baseAsset, feeRecipient, feePercentage, name, symbol, initialDelay, initialDefaultAdmin, marketsArgs + vault = BYTECODE_DEPLOYER.deploy( + baseAsset, feeRecipient, feePercentage, name, symbol, initialDelay, initialDefaultAdmin, salt, marketsArgs ); baseAsset.safeTransferFrom(msg.sender, address(this), initialDeposit); diff --git a/test/fork/concrete/vault/VaultFactory.t.sol b/test/fork/concrete/vault/VaultFactory.t.sol index 1e56f518..d94b12d5 100644 --- a/test/fork/concrete/vault/VaultFactory.t.sol +++ b/test/fork/concrete/vault/VaultFactory.t.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.21; import { Vault } from "./../../../../src/vault/Vault.sol"; +import { VaultBytecode } from "./../../../../src/vault/VaultBytecode.sol"; import { VaultFactory } from "./../../../../src/vault/VaultFactory.sol"; import { VaultSharedSetup } from "../../../helpers/VaultSharedSetup.sol"; import { ERC20PresetMinterPauser } from "../../../helpers/ERC20PresetMinterPauser.sol"; @@ -11,8 +12,6 @@ import { IIonPool } from "./../../../../src/interfaces/IIonPool.sol"; import { console2 } from "forge-std/console2.sol"; contract VaultFactoryTest is VaultSharedSetup { - VaultFactory factory; - address internal feeRecipient = address(2); uint256 internal feePercentage = 0.02e27; IERC20 internal baseAsset = BASE_ASSET; @@ -25,9 +24,7 @@ contract VaultFactoryTest is VaultSharedSetup { IIonPool[] internal newWithdrawQueue; function setUp() public override { - super.setUp(); - - factory = new VaultFactory(); + super.setUp(); // factory deployed in parent marketsToAdd.push(weEthIonPool); marketsToAdd.push(rsEthIonPool); diff --git a/test/helpers/VaultForkSharedSetup.sol b/test/helpers/VaultForkSharedSetup.sol index 19ef3239..bd01fbf9 100644 --- a/test/helpers/VaultForkSharedSetup.sol +++ b/test/helpers/VaultForkSharedSetup.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.21; import { VaultFactory } from "./../../../../src/vault/VaultFactory.sol"; +import { VaultBytecode } from "./../../src/vault/VaultBytecode.sol"; import { Vault } from "./../../../../src/vault/Vault.sol"; import { IIonPool } from "./../../../../src/interfaces/IIonPool.sol"; import { IonLens } from "./../../../../src/periphery/IonLens.sol"; @@ -14,24 +15,24 @@ import "forge-std/Test.sol"; using EnumerableSet for EnumerableSet.AddressSet; contract VaultForkBase is Test { + // Mainnet addresses IIonPool constant WEETH_IONPOOL = IIonPool(0x0000000000eaEbd95dAfcA37A39fd09745739b78); IIonPool constant RSETH_IONPOOL = IIonPool(0x0000000000E33e35EE6052fae87bfcFac61b1da9); IIonPool constant RSWETH_IONPOOL = IIonPool(0x00000000007C8105548f9d0eE081987378a6bE93); IonLens constant LENS = IonLens(0xe89AF12af000C4f76a57A3aD16ef8277a727DC81); - bytes32 constant ION_ROLE = 0x5ab1a5ffb29c47d95dec8c5f9ad49a551754822b51a3359ed1c21e2be24beefa; - IERC20 constant BASE_ASSET = WSTETH_ADDRESS; - address FEE_RECIPIENT = address(1); - - uint48 constant INITIAL_DELAY = 0; - uint256 constant MIN_INITIAL_DEPOSIT = 1e3; - + bytes32 constant ION_ROLE = 0x5ab1a5ffb29c47d95dec8c5f9ad49a551754822b51a3359ed1c21e2be24beefa; address VAULT_ADMIN = 0x0000000000417626Ef34D62C4DC189b021603f2F; + // Test addresses IIonPool constant IDLE = IIonPool(address(uint160(uint256(keccak256("IDLE_ASSET_HOLDINGS"))))); + address constant FEE_RECIPIENT = address(1); address constant NULL = address(0); + + uint48 constant INITIAL_DELAY = 0; + uint256 constant MIN_INITIAL_DEPOSIT = 1e3; uint256 constant DEFAULT_ALLO_CAO = type(uint128).max; IIonPool[] markets; @@ -42,7 +43,8 @@ contract VaultForkBase is Test { uint256[] allocationCaps; - VaultFactory factory; + VaultBytecode bytecodeDeployer = VaultBytecode(0x0000000000382a154e4A696A8C895b4292fA3D82); + VaultFactory factory = VaultFactory(0x0000000000D7DC416dFe993b0E3dd53BA3E27Fc8); Vault vault; uint256 internal forkBlock = 0; @@ -58,7 +60,12 @@ contract VaultForkBase is Test { if (forkBlock == 0) vm.createSelectFork(vm.envString("MAINNET_ARCHIVE_RPC_URL")); else vm.createSelectFork(vm.envString("MAINNET_ARCHIVE_RPC_URL"), forkBlock); - factory = new VaultFactory(); + // The factory stores a constant address for `VaultBytecode` + VaultBytecode _bytecodeDeployer = new VaultBytecode(); + VaultFactory _factory = new VaultFactory(); + + vm.etch(address(bytecodeDeployer), address(_bytecodeDeployer).code); + vm.etch(address(factory), address(_factory).code); markets.push(IDLE); markets.push(WEETH_IONPOOL); diff --git a/test/helpers/VaultSharedSetup.sol b/test/helpers/VaultSharedSetup.sol index da1a5987..a1f329e9 100644 --- a/test/helpers/VaultSharedSetup.sol +++ b/test/helpers/VaultSharedSetup.sol @@ -3,6 +3,8 @@ pragma solidity 0.8.21; import { WadRayMath, RAY } from "./../../src/libraries/math/WadRayMath.sol"; import { Vault } from "./../../src/vault/Vault.sol"; +import { VaultBytecode } from "./../../src/vault/VaultBytecode.sol"; +import { VaultFactory } from "./../../src/vault/VaultFactory.sol"; import { IonPool } from "./../../src/IonPool.sol"; import { IIonPool } from "./../../src/interfaces/IIonPool.sol"; import { GemJoin } from "./../../src/join/GemJoin.sol"; @@ -71,6 +73,9 @@ contract VaultSharedSetup is IonPoolSharedSetup { bytes32 public constant ION_POOL_SUPPLY_CAP_SLOT = 0xceba3d526b4d5afd91d1b752bf1fd37917c20a6daf576bcb41dd1c57c1f67e09; + VaultBytecode bytecodeDeployer = VaultBytecode(0x0000000000382a154e4A696A8C895b4292fA3D82); + VaultFactory factory = VaultFactory(0x0000000000D7DC416dFe993b0E3dd53BA3E27Fc8); + function setUp() public virtual override { super.setUp(); @@ -88,6 +93,7 @@ contract VaultSharedSetup is IonPoolSharedSetup { marketsArgs.newSupplyQueue = markets; marketsArgs.newWithdrawQueue = markets; + // deployed without factory for unit tests vault = new Vault{ salt: SALT }( BASE_ASSET, FEE_RECIPIENT, ZERO_FEES, "Ion Vault Token", "IVT", INITIAL_DELAY, VAULT_ADMIN, marketsArgs ); @@ -113,6 +119,13 @@ contract VaultSharedSetup is IonPoolSharedSetup { weEthIonPool.grantRole(weEthIonPool.GEM_JOIN_ROLE(), address(weEthGemJoin)); rsEthIonPool.grantRole(rsEthIonPool.GEM_JOIN_ROLE(), address(rsEthGemJoin)); rswEthIonPool.grantRole(rswEthIonPool.GEM_JOIN_ROLE(), address(rswEthGemJoin)); + + // separate vault factory deployment + VaultBytecode _bytecodeDeployer = new VaultBytecode(); + VaultFactory _factory = new VaultFactory(); + + vm.etch(address(bytecodeDeployer), address(_bytecodeDeployer).code); + vm.etch(address(factory), address(_factory).code); } function setERC20Balance(address token, address usr, uint256 amt) public { diff --git a/test/unit/fuzz/vault/Vault.t.sol b/test/unit/fuzz/vault/Vault.t.sol index 2eb4923b..e381fbf2 100644 --- a/test/unit/fuzz/vault/Vault.t.sol +++ b/test/unit/fuzz/vault/Vault.t.sol @@ -2,7 +2,6 @@ pragma solidity 0.8.21; import { Vault } from "./../../../../src/vault/Vault.sol"; -import { VaultFactory } from "./../../../../src/vault/VaultFactory.sol"; import { IIonPool } from "./../../../../src/interfaces/IIonPool.sol"; import { IonPoolExposed } from "../../../helpers/IonPoolSharedSetup.sol"; import { VaultSharedSetup } from "../../../helpers/VaultSharedSetup.sol"; @@ -956,8 +955,6 @@ contract VaultInflationAttack is VaultSharedSetup { setERC20Balance(address(BASE_ASSET), deployer, MIN_INITIAL_DEPOSIT); - VaultFactory factory = new VaultFactory(); - vm.startPrank(deployer); BASE_ASSET.approve(address(factory), MIN_INITIAL_DEPOSIT); From 03cca10de2157644fb7375eddb83f02bb45186ed Mon Sep 17 00:00:00 2001 From: Jun Kim <64379343+junkim012@users.noreply.github.com> Date: Sat, 25 May 2024 03:32:06 -0400 Subject: [PATCH 6/6] feat: deployment scripts for vaults --- package.json | 9 +- script/Base.s.sol | 3 + script/ValidateInterface.s.sol | 5 + script/deploy/vault/DeployVault.s.sol | 163 ++++++++++++++++++ script/deploy/vault/DeployVaultBytecode.s.sol | 20 +++ script/deploy/vault/DeployVaultFactory.s.sol | 21 +++ test/fork/concrete/vault/Vault.t.sol | 8 +- test/fork/fuzz/vault/Vault.t.sol | 8 +- 8 files changed, 229 insertions(+), 8 deletions(-) create mode 100644 script/deploy/vault/DeployVault.s.sol create mode 100644 script/deploy/vault/DeployVaultBytecode.s.sol create mode 100644 script/deploy/vault/DeployVaultFactory.s.sol diff --git a/package.json b/package.json index 893b2da9..1c40ceb6 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,14 @@ "10_AdminTransfer:deployment:deploy:anvil": "forge script script/deploy/10_AdminTransfer.s.sol --rpc-url http://localhost:8545 --private-key $PRIVATE_KEY --slow", "10_AdminTransfer:deployment:deploy:tenderly": "forge script script/deploy/10_AdminTransfer.s.sol --rpc-url $TENDERLY_RPC_URL --private-key $PRIVATE_KEY --slow", "10_AdminTransfer:deployment:deploy:sepolia": "forge script script/deploy/10_AdminTransfer.s.sol --rpc-url $SEPOLIA_RPC_URL --private-key $PRIVATE_KEY --slow", - "10_AdminTransfer:deployment:deploy:mainnet": "forge script script/deploy/10_AdminTransfer.s.sol --rpc-url $MAINNET_RPC_URL --private-key $PRIVATE_KEY --slow" + "10_AdminTransfer:deployment:deploy:mainnet": "forge script script/deploy/10_AdminTransfer.s.sol --rpc-url $MAINNET_RPC_URL --private-key $PRIVATE_KEY --slow", + "VaultBytecode:deployment:deploy:tenderly": "forge script script/deploy/vault/DeployVaultBytecode.s.sol --rpc-url $TENDERLY_RPC_URL --private-key $PRIVATE_KEY --slow", + "VaultBytecode:deployment:deploy:mainnet": "forge script script/deploy/vault/DeployVaultBytecode.s.sol --rpc-url $MAINNET_RPC_URL --private-key $PRIVATE_KEY --slow", + "VaultFactory:deployment:deploy:anvil": "forge script script/deploy/vault/DeployVaultFactory.s.sol --rpc-url http://localhost:8545 --private-key $PRIVATE_KEY --slow", + "VaultFactory:deployment:deploy:tenderly": "forge script script/deploy/vault/DeployVaultFactory.s.sol --rpc-url $TENDERLY_RPC_URL --private-key $PRIVATE_KEY --slow", + "VaultFactory:deployment:deploy:mainnet": "forge script script/deploy/vault/DeployVaultFactory.s.sol --rpc-url $MAINNET_RPC_URL --private-key $PRIVATE_KEY --slow", + "Vault:deployment:deploy:tenderly": "forge script script/deploy/vault/DeployVault.s.sol --rpc-url $TENDERLY_RPC_URL --private-key $PRIVATE_KEY --slow", + "Vault:deployment:deploy:mainnet": "forge script script/deploy/vault/DeployVault.s.sol --rpc-url $MAINNET_RPC_URL --private-key $PRIVATE_KEY --slow" }, "dependencies": { "date-fns": "^2.30.0", diff --git a/script/Base.s.sol b/script/Base.s.sol index df839dd4..ee111c01 100644 --- a/script/Base.s.sol +++ b/script/Base.s.sol @@ -6,6 +6,7 @@ import { ValidateInterface } from "./ValidateInterface.s.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { Script, stdJson } from "forge-std/Script.sol"; +import { console2 } from "forge-std/console2.sol"; abstract contract BaseScript is Script, ValidateInterface { using stdJson for string; @@ -41,6 +42,8 @@ abstract contract BaseScript is Script, ValidateInterface { mnemonic = vm.envOr({ name: "MNEMONIC", defaultValue: TEST_MNEMONIC }); (broadcaster,) = deriveRememberKey({ mnemonic: mnemonic, index: 0 }); } + + console2.log("broadcaster", broadcaster); } modifier broadcast() { diff --git a/script/ValidateInterface.s.sol b/script/ValidateInterface.s.sol index d60272e5..97358d9c 100644 --- a/script/ValidateInterface.s.sol +++ b/script/ValidateInterface.s.sol @@ -19,6 +19,11 @@ abstract contract ValidateInterface { function _validateInterfaceIonPool(IonPool ionPool) internal view { require(address(ionPool).code.length > 0, "ionPool address must have code"); ionPool.balanceOf(address(this)); + ionPool.totalSupply(); + ionPool.GEM_JOIN_ROLE(); + ionPool.LIQUIDATOR_ROLE(); + ionPool.PAUSE_ROLE(); + ionPool.calculateRewardAndDebtDistribution(); } function _validateInterface(IERC20 ilkAddress) internal view { diff --git a/script/deploy/vault/DeployVault.s.sol b/script/deploy/vault/DeployVault.s.sol new file mode 100644 index 00000000..2c3aea7b --- /dev/null +++ b/script/deploy/vault/DeployVault.s.sol @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.21; + +import { Vault } from "./../../../src/vault/Vault.sol"; +import { VaultFactory } from "./../../../src/vault/VaultFactory.sol"; +import { IIonPool } from "./../../../src/interfaces/IIonPool.sol"; +import { IonPool } from "./../../../src/IonPool.sol"; +import { IERC20 } from "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; + +import { BaseScript } from "./../../Base.s.sol"; + +import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import { stdJson as StdJson } from "forge-std/StdJson.sol"; + +VaultFactory constant factory = VaultFactory(address(0)); +/** + * Always use the factory to deploy a vault. + */ + +contract DeployVault is BaseScript { + using EnumerableSet for EnumerableSet.AddressSet; + using StdJson for string; + using SafeCast for uint256; + + string configPath = "./deployment-config/vault/DeployVault.json"; + string config = vm.readFile(configPath); + + VaultFactory factory = VaultFactory(config.readAddress(".factory")); + + address baseAsset = config.readAddress(".baseAsset"); + + address feeRecipient = config.readAddress(".feeRecipient"); + uint256 feePercentage = config.readUint(".feePercentage"); + + string name = config.readString(".name"); + string symbol = config.readString(".symbol"); + + uint48 initialDelay = config.readUint(".initialDelay").toUint48(); + address initialDefaultAdmin = config.readAddress(".initialDefaultAdmin"); + + bytes32 salt = config.readBytes32(".salt"); + + uint256 initialDeposit = config.readUint(".initialDeposit"); + + address[] marketsToAdd = config.readAddressArray(".marketsToAdd"); + uint256[] allocationCaps = config.readUintArray(".allocationCaps"); + address[] supplyQueue = config.readAddressArray(".supplyQueue"); + address[] withdrawQueue = config.readAddressArray(".withdrawQueue"); + + IIonPool public constant IDLE = IIonPool(address(uint160(uint256(keccak256("IDLE_ASSET_HOLDINGS"))))); + + EnumerableSet.AddressSet marketsCheck; + + /** + * Validate that the salt is msg.sender protected. + */ + function _validateSalt(bytes32 salt) internal { + if (address(bytes20(salt)) != broadcaster) { + revert("Invalid Salt"); + } + } + + /** + * No duplicates. No zero addresses. IonPool Interface. + */ + function _validateIonPoolArray(address[] memory ionPools) internal returns (IIonPool[] memory typedIonPools) { + typedIonPools = new IIonPool[](ionPools.length); + + for (uint8 i = 0; i < ionPools.length; i++) { + address pool = ionPools[i]; + + require(pool != address(0), "zero address"); + + marketsCheck.add(pool); + + // If not the IDLE address, then validate the IonPool interface. + if (pool != address(IDLE)) { + _validateInterfaceIonPool(IonPool(pool)); + } + + // Check for duplicates in this array + if (i != ionPools.length - 1) { + for (uint8 j = i + 1; j < ionPools.length; j++) { + require(ionPools[i] != ionPools[j], "duplicate"); + } + } + + typedIonPools[i] = IIonPool(pool); + } + } + + function run() public broadcast returns (Vault vault) { + require(baseAsset != address(0), "baseAsset"); + + require(feeRecipient != address(0), "feeRecipient"); + require(feePercentage <= 0.2e27, "feePercentage"); + + // require(initialDelay != 0, "initialDelay"); + require(initialDefaultAdmin != address(0), "initialDefaultAdmin"); + + require(initialDeposit >= 1e3, "initialDeposit"); + require(IERC20(baseAsset).balanceOf(broadcaster) >= initialDeposit, "sender balance"); + // require(IERC20(baseAsset).allowance(broadcaster, address(factory)) >= initialDeposit, "sender allowance"); + + if (IERC20(baseAsset).allowance(broadcaster, address(factory)) < initialDeposit) { + IERC20(baseAsset).approve(address(factory), 1e9); + } + + // The length of all the arrays must be the same. + require(marketsToAdd.length > 0); + require(allocationCaps.length > 0); + require(supplyQueue.length > 0); + require(withdrawQueue.length > 0); + + uint256 marketsLength = marketsToAdd.length; + + require(marketsToAdd.length == marketsLength, "array length"); + require(allocationCaps.length == marketsLength, "array length"); + require(supplyQueue.length == marketsLength, "array length"); + + _validateSalt(salt); + + IIonPool[] memory typedMarketsToAdd = _validateIonPoolArray(marketsToAdd); + IIonPool[] memory typedSupplyQueue = _validateIonPoolArray(supplyQueue); + IIonPool[] memory typedWithdrawQueue = _validateIonPoolArray(withdrawQueue); + + // If the length of the `uniqueMarketsCheck` set is greater than 4, that + // means not all of the IonPool arrays had the same set of markets. + // `_validateIonPoolArray` must be called before this. + require(marketsToAdd.length == marketsCheck.length(), "markets not consistent"); + + Vault.MarketsArgs memory marketsArgs = Vault.MarketsArgs({ + marketsToAdd: typedMarketsToAdd, + allocationCaps: allocationCaps, + newSupplyQueue: typedSupplyQueue, + newWithdrawQueue: typedWithdrawQueue + }); + + vault = factory.createVault( + IERC20(baseAsset), + feeRecipient, + feePercentage, + name, + symbol, + initialDelay, + initialDefaultAdmin, + salt, + marketsArgs, + initialDeposit + ); + + require(vault.feeRecipient() == feeRecipient, "feeRecipient"); + require(vault.feePercentage() == feePercentage, "feePercentage"); + require(vault.defaultAdminDelay() == initialDelay, "initialDelay"); + require(vault.defaultAdmin() == initialDefaultAdmin, "initialDefaultAdmin"); + for (uint8 i = 0; i < marketsLength; i++) { + require(vault.supplyQueue(i) == typedSupplyQueue[i], "supplyQueue"); + require(vault.withdrawQueue(i) == typedWithdrawQueue[i], "withdrawQueue"); + } + require(vault.supportedMarketsLength() == marketsLength, "supportedMarkets"); + } +} diff --git a/script/deploy/vault/DeployVaultBytecode.s.sol b/script/deploy/vault/DeployVaultBytecode.s.sol new file mode 100644 index 00000000..97a3c08f --- /dev/null +++ b/script/deploy/vault/DeployVaultBytecode.s.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.21; + +import { VaultBytecode } from "./../../../src/vault/VaultBytecode.sol"; +import { CREATEX } from "./../../../src/Constants.sol"; + +import { DeployScript } from "./../../Deploy.s.sol"; + +bytes32 constant SALT = 0xbcde1e1dd0bdb803514d8e000000000000000000000000000000000000000000; + +contract DeployVaultBytecode is DeployScript { + function run() public broadcast returns (VaultBytecode vaultBytecode) { + bytes memory initCode = type(VaultBytecode).creationCode; + + require(initCode.length > 0, "initCode"); + require(SALT != bytes32(0), "salt"); + + vaultBytecode = VaultBytecode(CREATEX.deployCreate3(SALT, initCode)); + } +} diff --git a/script/deploy/vault/DeployVaultFactory.s.sol b/script/deploy/vault/DeployVaultFactory.s.sol new file mode 100644 index 00000000..120b1c2d --- /dev/null +++ b/script/deploy/vault/DeployVaultFactory.s.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.21; + +import { VaultFactory } from "./../../../src/vault/VaultFactory.sol"; +import { CREATEX } from "./../../../src/Constants.sol"; + +import { DeployScript } from "./../../Deploy.s.sol"; + +// deploys to 0x0000000000d7dc416dfe993b0e3dd53ba3e27fc8 +bytes32 constant SALT = 0x2f428c0d9f1d9e00034c85000000000000000000000000000000000000000000; + +contract DeployVaultFactory is DeployScript { + function run() public broadcast returns (VaultFactory vaultFactory) { + bytes memory initCode = type(VaultFactory).creationCode; + + require(initCode.length > 0, "initCode"); + require(SALT != bytes32(0), "salt"); + + vaultFactory = VaultFactory(CREATEX.deployCreate3(SALT, initCode)); + } +} diff --git a/test/fork/concrete/vault/Vault.t.sol b/test/fork/concrete/vault/Vault.t.sol index eaad6692..2b962fc5 100644 --- a/test/fork/concrete/vault/Vault.t.sol +++ b/test/fork/concrete/vault/Vault.t.sol @@ -30,7 +30,7 @@ contract Vault_ForkTest is VaultForkBase { // vault assertEq(resultingSharesMinted, totalSupplyAfterDeposit - totalSupply, "vault total supply after deposit"); - assertApproxEqAbs(maxDeposit, totalAssetsAfterDeposit - totalAssets, 3, "vault total assets after deposit"); // 1 + assertApproxEqAbs(maxDeposit, totalAssetsAfterDeposit - totalAssets, 4, "vault total assets after deposit"); // 1 // wei error per market uint256 withdrawAmt = vault.maxWithdraw(address(this)); @@ -49,7 +49,9 @@ contract Vault_ForkTest is VaultForkBase { assertEq( resultingSharesRedeemed, totalSupplyAfterDeposit - totalSupplyAfterWithdraw, "total supply after withdraw" ); - assertEq(withdrawAmt, totalAssetsAfterDeposit - totalAssetsAfterWithdraw, "total assets after withdraw"); + assertApproxEqAbs( + withdrawAmt, totalAssetsAfterDeposit - totalAssetsAfterWithdraw, 3, "total assets after withdraw" + ); } function test_Deposit_MaxMint_MaxRedeem() public { @@ -77,7 +79,7 @@ contract Vault_ForkTest is VaultForkBase { // vault assertEq(maxMint, totalSupplyAfterMint - totalSupply, "vault total supply after deposit"); assertApproxEqAbs( - resultingAssetsDeposited, totalAssetsAfterMint - totalAssets, 3, "vault total assets after deposit" + resultingAssetsDeposited, totalAssetsAfterMint - totalAssets, 4, "vault total assets after deposit" ); // 1 wei error per market uint256 prevShares = vault.balanceOf(address(this)); diff --git a/test/fork/fuzz/vault/Vault.t.sol b/test/fork/fuzz/vault/Vault.t.sol index ecbfba44..67970094 100644 --- a/test/fork/fuzz/vault/Vault.t.sol +++ b/test/fork/fuzz/vault/Vault.t.sol @@ -30,7 +30,7 @@ contract Vault_ForkFuzzTest is VaultForkBase { // vault assertEq(resultingSharesMinted, totalSupplyAfterDeposit - totalSupply, "vault total supply after deposit"); - assertApproxEqAbs(depositAmt, totalAssetsAfterDeposit - totalAssets, 3, "vault total assets after deposit"); // 1 + assertApproxEqAbs(depositAmt, totalAssetsAfterDeposit - totalAssets, 4, "vault total assets after deposit"); // 1 // wei error per market // tries to withdraw max @@ -52,7 +52,7 @@ contract Vault_ForkFuzzTest is VaultForkBase { resultingSharesRedeemed, totalSupplyAfterDeposit - totalSupplyAfterWithdraw, "total supply after withdraw" ); assertApproxEqAbs( - withdrawAmt, totalAssetsAfterDeposit - totalAssetsAfterWithdraw, 1, "total assets after withdraw" + withdrawAmt, totalAssetsAfterDeposit - totalAssetsAfterWithdraw, 4, "total assets after withdraw" ); } @@ -83,7 +83,7 @@ contract Vault_ForkFuzzTest is VaultForkBase { // vault assertEq(mintAmt, totalSupplyAfterMint - totalSupply, "vault total supply after deposit"); assertApproxEqAbs( - resultingAssetsDeposited, totalAssetsAfterMint - totalAssets, 3, "vault total assets after deposit" + resultingAssetsDeposited, totalAssetsAfterMint - totalAssets, 4, "vault total assets after deposit" ); // 1 wei error per market uint256 prevShares = vault.balanceOf(address(this)); @@ -106,7 +106,7 @@ contract Vault_ForkFuzzTest is VaultForkBase { // vault assertEq(redeemAmt, totalSupplyAfterMint - totalSupplyAfterRedeem, "total supply after withdraw"); assertApproxEqAbs( - expectedWithdrawAmt, totalAssetsAfterMint - totalAssetsAfterRedeem, 1, "total assets after withdraw" + expectedWithdrawAmt, totalAssetsAfterMint - totalAssetsAfterRedeem, 3, "total assets after withdraw" ); } }