diff --git a/packages/foundry/src/vault/MultiStrategyVault.sol b/packages/foundry/src/vault/MultiStrategyVault.sol new file mode 100644 index 00000000..e46ec3ac --- /dev/null +++ b/packages/foundry/src/vault/MultiStrategyVault.sol @@ -0,0 +1,811 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.15 + +pragma solidity ^0.8.15; + +import { ERC4626Upgradeable, IERC20MetadataUpgradeable as IERC20Metadata, ERC20Upgradeable as ERC20 } from "openzeppelin-contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol"; +import { SafeERC20Upgradeable as SafeERC20 } from "openzeppelin-contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; +import { ReentrancyGuardUpgradeable } from "openzeppelin-contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import { PausableUpgradeable } from "openzeppelin-contracts-upgradeable/security/PausableUpgradeable.sol"; +import { MathUpgradeable as Math } from "openzeppelin-contracts-upgradeable/utils/math/MathUpgradeable.sol"; +import { OwnedUpgradeable } from "../utils/OwnedUpgradeable.sol"; +import { VaultFees, IERC4626, IERC20 } from "../interfaces/vault/IVault.sol"; + +struct AdapterConfig { + IERC4626 adapter; + uint64 allocation; +} + +/** + * @title Vault + * @author RedVeil + * @notice See the following for the full EIP-4626 specification https://eips.ethereum.org/EIPS/eip-4626. + * + * A simple ERC4626-Implementation of a Vault. + * The vault delegates any actual protocol interaction to an adapter. + * It allows for multiple type of fees which are taken by issuing new vault shares. + * Adapter and fees can be changed by the owner after a ragequit time. + */ +contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, PausableUpgradeable, OwnedUpgradeable { + using SafeERC20 for IERC20; + using Math for uint256; + + uint256 internal constant SECONDS_PER_YEAR = 365.25 days; + + uint8 internal _decimals; + uint8 public constant decimalOffset = 9; + + string internal _name; + string internal _symbol; + + bytes32 public contractName; + + event VaultInitialized(bytes32 contractName, address indexed asset); + + error InvalidAsset(); + error InvalidAdapter(); + + constructor() { + _disableInitializers(); + } + + /** + * @notice Initialize a new Vault. + * @param asset_ Underlying Asset which users will deposit. + * @param adapters_ Adapter which will be used to interact with the wrapped protocol. + * @param adapterCount_ Amount of adapters to use. + * @param fees_ Desired fees in 1e18. (1e18 = 100%, 1e14 = 1 BPS) + * @param feeRecipient_ Recipient of all vault fees. (Must not be zero address) + * @param depositLimit_ Maximum amount of assets which can be deposited. + * @param owner Owner of the contract. Controls management functions. + * @dev This function is called by the factory contract when deploying a new vault. + * @dev Usually the adapter should already be pre configured. Otherwise a new one can only be added after a ragequit time. + */ + function initialize( + IERC20 asset_, + AdapterConfig[10] calldata adapters_, + uint8 adapterCount_, + VaultFees calldata fees_, + address feeRecipient_, + uint256 depositLimit_, + address owner + ) external initializer { + __ERC4626_init(IERC20Metadata(address(asset_))); + __Owned_init(owner); + + if (address(asset_) == address(0)) revert InvalidAsset(); + _verifyAdapterConfig(adapters_, adapterCount_); + + adapterCount = adapterCount_; + for (uint8 i; i < adapterCount_; i++) { + adapters[i] = adapters_[i]; + + asset_.approve(address(adapters_[i].adapter), type(uint256).max); + } + + _decimals = IERC20Metadata(address(asset_)).decimals() + decimalOffset; // Asset decimals + decimal offset to combat inflation attacks + + INITIAL_CHAIN_ID = block.chainid; + INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator(); + + if (fees_.deposit >= 1e18 || fees_.withdrawal >= 1e18 || fees_.management >= 1e18 || fees_.performance >= 1e18) + revert InvalidVaultFees(); + fees = fees_; + + if (feeRecipient_ == address(0)) revert InvalidFeeRecipient(); + feeRecipient = feeRecipient_; + + contractName = keccak256(abi.encodePacked("Popcorn", name(), block.timestamp, "Vault")); + + feesUpdatedAt = block.timestamp; + highWaterMark = 1e9; + quitPeriod = 3 days; + depositLimit = depositLimit_; + + emit VaultInitialized(contractName, address(asset_)); + + _name = string.concat("Popcorn ", IERC20Metadata(address(asset_)).name(), " Vault"); + _symbol = string.concat("pop-", IERC20Metadata(address(asset_)).symbol()); + } + + function name() public view override(IERC20Metadata, ERC20) returns (string memory) { + return _name; + } + + function symbol() public view override(IERC20Metadata, ERC20) returns (string memory) { + return _symbol; + } + + function decimals() public view override(IERC20Metadata, ERC20) returns (uint8) { + return _decimals; + } + + /*////////////////////////////////////////////////////////////// + DEPOSIT/WITHDRAWAL LOGIC + //////////////////////////////////////////////////////////////*/ + + error ZeroAmount(); + error InvalidReceiver(); + error MaxError(uint256 amount); + + function deposit(uint256 assets) public returns (uint256) { + return deposit(assets, msg.sender); + } + + function deposit(uint256 assets, address receiver) public override returns (uint256 shares) { + if (receiver == address(0)) revert InvalidReceiver(); + if (assets > maxDeposit(receiver)) revert MaxError(assets); + + uint256 feeShares = _convertToShares( + assets.mulDiv(uint256(fees.deposit), 1e18, Math.Rounding.Down), + Math.Rounding.Down + ); + + shares = _convertToShares(assets, Math.Rounding.Down) - feeShares; + + if (feeShares > 0) _mint(feeRecipient, feeShares); + + _deposit(_msgSender(), receiver, assets, shares); + + return shares; + } + + function mint(uint256 shares) external returns (uint256) { + return mint(shares, msg.sender); + } + + function mint(uint256 shares, address receiver) public override returns (uint256 assets) { + if (receiver == address(0)) revert InvalidReceiver(); + if (shares > maxMint(receiver)) revert MaxError(shares); + + uint256 depositFee = uint256(fees.deposit); + + uint256 feeShares = shares.mulDiv(depositFee, 1e18 - depositFee, Math.Rounding.Down); + + assets = _convertToAssets(shares + feeShares, Math.Rounding.Up); + + if (feeShares > 0) _mint(feeRecipient, feeShares); + + _deposit(_msgSender(), receiver, assets, shares); + + return assets; + } + + function _deposit( + address caller, + address receiver, + uint256 assets, + uint256 shares + ) internal override nonReentrant whenNotPaused { + if (receiver == address(0)) revert InvalidReceiver(); + + IERC20(asset()).safeTransferFrom(caller, address(this), assets); + + for (uint8 i; i < adapterCount; i++) { + adapters[i].adapter.deposit(assets.mulDiv(adapters[i].allocation, 1e18, Math.Rounding.Down), address(this)); + } + + _mint(receiver, shares); + + emit Deposit(caller, receiver, assets, shares); + } + + function withdraw(uint256 assets) public returns (uint256) { + return withdraw(assets, msg.sender, msg.sender); + } + + /** + * @notice Burn shares from `owner` in exchange for `assets` amount of underlying token. + * @param assets Quantity of underlying `asset` token to withdraw. + * @param receiver Receiver of underlying token. + * @param owner Owner of burned vault shares. + * @return shares Quantity of vault shares burned in exchange for `assets`. + */ + function withdraw( + uint256 assets, + address receiver, + address owner + ) public override returns (uint256 shares) { + if (receiver == address(0)) revert InvalidReceiver(); + if (assets > maxWithdraw(owner)) revert MaxError(assets); + + uint256 shares = _convertToShares(assets, Math.Rounding.Up); + + uint256 withdrawalFee = uint256(fees.withdrawal); + + uint256 feeShares = shares.mulDiv(withdrawalFee, 1e18 - withdrawalFee, Math.Rounding.Down); + + shares += feeShares; + + if (feeShares > 0) _mint(feeRecipient, feeShares); + + _withdraw(_msgSender(), receiver, owner, assets, shares); + + return shares; + } + + function redeem(uint256 shares) external returns (uint256) { + return redeem(shares, msg.sender, msg.sender); + } + + /** + * @notice Burn exactly `shares` vault shares from `owner` and send underlying `asset` tokens to `receiver`. + * @param shares Quantity of vault shares to exchange for underlying tokens. + * @param receiver Receiver of underlying tokens. + * @param owner Owner of burned vault shares. + * @return assets Quantity of `asset` sent to `receiver`. + */ + function redeem( + uint256 shares, + address receiver, + address owner + ) public override returns (uint256 assets) { + if (receiver == address(0)) revert InvalidReceiver(); + if (shares > maxRedeem(owner)) revert MaxError(shares); + + uint256 feeShares = shares.mulDiv(uint256(fees.withdrawal), 1e18, Math.Rounding.Down); + + assets = _convertToAssets(shares - feeShares, Math.Rounding.Up); + + if (feeShares > 0) _mint(feeRecipient, feeShares); + + _withdraw(_msgSender(), receiver, owner, assets, shares); + + return assets; + } + + function _withdraw( + address caller, + address receiver, + address owner, + uint256 assets, + uint256 shares + ) internal override nonReentrant { + if (caller != owner) { + _spendAllowance(owner, caller, shares); + } + + _burn(owner, shares); + + for (uint8 i; i < adapterCount; i++) { + uint256 allocation = assets.mulDiv(uint256(adapters[i].allocation), 1e18, Math.Rounding.Down); + + if (assets % 2 == 1 && i == 0 && IERC20(asset()).balanceOf(address(this)) == 0) allocation += 1; + + adapters[i].adapter.withdraw(allocation, address(this), address(this)); + } + + IERC20(asset()).safeTransfer(receiver, assets); + + emit Withdraw(caller, receiver, owner, assets, shares); + } + + /*////////////////////////////////////////////////////////////// + ACCOUNTING LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @return assets Total amount of underlying `asset` token managed by vault. Delegates to adapter. + function totalAssets() public view override returns (uint256 assets) { + assets = IERC20(asset()).balanceOf(address(this)); + + for (uint8 i; i < adapterCount; i++) { + assets += adapters[i].adapter.convertToAssets(adapters[i].adapter.balanceOf(address(this))); + } + } + + /** + * @notice Simulate the effects of a deposit at the current block, given current on-chain conditions. + * @param assets Exact amount of underlying `asset` token to deposit + * @return shares of the vault issued in exchange to the user for `assets` + * @dev This method accounts for issuance of accrued fee shares. + */ + function previewDeposit(uint256 assets) public view override returns (uint256 shares) { + assets -= assets.mulDiv(uint256(fees.deposit), 1e18, Math.Rounding.Down); + shares = _convertToShares(assets, Math.Rounding.Down); + } + + /** + * @notice Simulate the effects of a mint at the current block, given current on-chain conditions. + * @param shares Exact amount of vault shares to mint. + * @return assets quantity of underlying needed in exchange to mint `shares`. + * @dev This method accounts for issuance of accrued fee shares. + */ + function previewMint(uint256 shares) public view override returns (uint256 assets) { + uint256 depositFee = uint256(fees.deposit); + shares += shares.mulDiv(depositFee, 1e18 - depositFee, Math.Rounding.Up); + assets = _convertToAssets(shares, Math.Rounding.Up); + } + + /** + * @notice Simulate the effects of a withdrawal at the current block, given current on-chain conditions. + * @param assets Exact amount of `assets` to withdraw + * @return shares to be burned in exchange for `assets` + * @dev This method accounts for both issuance of fee shares and withdrawal fee. + */ + function previewWithdraw(uint256 assets) public view override returns (uint256 shares) { + shares = _convertToShares(assets, Math.Rounding.Up); + + uint256 withdrawalFee = uint256(fees.withdrawal); + shares += shares.mulDiv(withdrawalFee, 1e18 - withdrawalFee, Math.Rounding.Up); + } + + /** + * @notice Simulate the effects of a redemption at the current block, given current on-chain conditions. + * @param shares Exact amount of `shares` to redeem + * @return assets quantity of underlying returned in exchange for `shares`. + * @dev This method accounts for both issuance of fee shares and withdrawal fee. + */ + function previewRedeem(uint256 shares) public view override returns (uint256 assets) { + uint256 feeShares = shares.mulDiv(uint256(fees.withdrawal), 1e18, Math.Rounding.Down); + + assets = _convertToAssets(shares - feeShares, Math.Rounding.Up); + } + + function _convertToShares(uint256 assets, Math.Rounding rounding) + internal + view + virtual + override + returns (uint256 shares) + { + return assets.mulDiv(totalSupply() + 10**decimalOffset, totalAssets() + 1, rounding); + } + + function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view virtual override returns (uint256) { + return shares.mulDiv(totalAssets() + 1, totalSupply() + 10**decimalOffset, rounding); + } + + /*////////////////////////////////////////////////////////////// + DEPOSIT/WITHDRAWAL LIMIT LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @return Maximum amount of underlying `asset` token that may be deposited for a given address. Delegates to adapters. + function maxDeposit(address) public view override returns (uint256) { + uint256 assets = totalAssets(); + uint256 depositLimit_ = depositLimit; + if (paused() || assets >= depositLimit_) return 0; + + uint256 maxDeposit_ = depositLimit_; + for (uint8 i; i < adapterCount; i++) { + uint256 adapterMax = adapters[i].adapter.maxDeposit(address(this)); + uint256 scalar = 1e18 / uint256(adapters[i].allocation); + + if (adapterMax > type(uint256).max / scalar) { + adapterMax = type(uint256).max; + } else { + adapterMax *= scalar; + } + + maxDeposit_ = Math.min(maxDeposit_, adapterMax); + } + + return maxDeposit_; + } + + /// @return Maximum amount of vault shares that may be minted to given address. Delegates to adapters. + function maxMint(address) public view override returns (uint256) { + uint256 assets = totalAssets(); + uint256 depositLimit_ = depositLimit; + if (paused() || assets >= depositLimit_) return 0; + + uint256 maxMint_ = depositLimit > type(uint256).max / (totalSupply() + 10**decimalOffset) + ? type(uint256).max + : _convertToShares(depositLimit_, Math.Rounding.Up); + + for (uint8 i; i < adapterCount; i++) { + uint256 adapterMax = adapters[i].adapter.maxMint(address(this)); + uint256 scalar = 1e18 / uint256(adapters[i].allocation); + + if (adapterMax > type(uint256).max / scalar) { + adapterMax = type(uint256).max; + } else { + adapterMax *= scalar; + } + + maxMint_ = Math.min(maxMint_, adapterMax); + } + + return maxMint_; + } + + /// @return Maximum amount of underlying `asset` token that can be withdrawn by `caller` address. Delegates to adapters. + function maxWithdraw(address) public view override returns (uint256) { + uint256 maxWithdraw_ = type(uint256).max; + uint256 leftover = IERC20(asset()).balanceOf(address(this)); + + for (uint8 i; i < adapterCount; i++) { + uint256 adapterMax = adapters[i].adapter.maxWithdraw(address(this)); + uint256 scalar = 1e18 / uint256(adapters[i].allocation); + + if (adapterMax > type(uint256).max / scalar) { + adapterMax = type(uint256).max; + } else { + adapterMax *= scalar; + } + + maxWithdraw_ = Math.min(maxWithdraw_, adapterMax + leftover); + } + + return maxWithdraw_; + } + + /// @return Maximum amount of shares that may be redeemed by `caller` address. Delegates to adapters. + function maxRedeem(address) public view override returns (uint256) { + uint256 maxRedeem_ = type(uint256).max; + uint256 leftover = IERC20(asset()).balanceOf(address(this)) > 0 + ? _convertToShares(IERC20(asset()).balanceOf(address(this)), Math.Rounding.Down) + : 0; + + for (uint8 i; i < adapterCount; i++) { + uint256 adapterMax = adapters[i].adapter.maxRedeem(address(this)); + uint256 scalar = 1e18 / uint256(adapters[i].allocation); + + if (adapterMax > type(uint256).max / scalar) { + adapterMax = type(uint256).max; + } else { + adapterMax *= scalar; + } + + maxRedeem_ = Math.min(maxRedeem_, adapterMax + leftover); + } + + return maxRedeem_; + } + + /*////////////////////////////////////////////////////////////// + FEE ACCOUNTING LOGIC + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Management fee that has accrued since last fee harvest. + * @return Accrued management fee in underlying `asset` token. + * @dev Management fee is annualized per minute, based on 525,600 minutes per year. Total assets are calculated using + * the average of their current value and the value at the previous fee harvest checkpoint. This method is similar to + * calculating a definite integral using the trapezoid rule. + */ + function accruedManagementFee() public view returns (uint256) { + uint256 managementFee = fees.management; + return + managementFee > 0 + ? managementFee.mulDiv( + totalAssets() * (block.timestamp - feesUpdatedAt), + SECONDS_PER_YEAR, + Math.Rounding.Down + ) / 1e18 + : 0; + } + + /** + * @notice Performance fee that has accrued since last fee harvest. + * @return Accrued performance fee in underlying `asset` token. + * @dev Performance fee is based on a high water mark value. If vault share value has increased above the + * HWM in a fee period, issue fee shares to the vault equal to the performance fee. + */ + function accruedPerformanceFee() public view returns (uint256) { + uint256 highWaterMark_ = highWaterMark; + uint256 shareValue = convertToAssets(1e18); + uint256 performanceFee = fees.performance; + + return + performanceFee > 0 && shareValue > highWaterMark_ + ? performanceFee.mulDiv((shareValue - highWaterMark_) * totalSupply(), 1e36, Math.Rounding.Down) + : 0; + } + + /*////////////////////////////////////////////////////////////// + FEE LOGIC + //////////////////////////////////////////////////////////////*/ + + uint256 public highWaterMark; + uint256 public assetsCheckpoint; + uint256 public feesUpdatedAt; + + error InsufficientWithdrawalAmount(uint256 amount); + + /// @notice Minimal function to call `takeFees` modifier. + function takeManagementAndPerformanceFees() external nonReentrant takeFees {} + + /// @notice Collect management and performance fees and update vault share high water mark. + modifier takeFees() { + uint256 managementFee = accruedManagementFee(); + uint256 totalFee = managementFee + accruedPerformanceFee(); + uint256 currentAssets = totalAssets(); + uint256 shareValue = convertToAssets(1e18); + + if (shareValue > highWaterMark) highWaterMark = shareValue; + + if (totalFee > 0 && currentAssets > 0) { + uint256 supply = totalSupply(); + uint256 feeInShare = supply == 0 + ? totalFee + : totalFee.mulDiv(supply, currentAssets - totalFee, Math.Rounding.Down); + _mint(feeRecipient, feeInShare); + } + + feesUpdatedAt = block.timestamp; + + _; + } + + /*////////////////////////////////////////////////////////////// + FEE MANAGEMENT LOGIC + //////////////////////////////////////////////////////////////*/ + + VaultFees public fees; + + VaultFees public proposedFees; + uint256 public proposedFeeTime; + + address public feeRecipient; + + event NewFeesProposed(VaultFees newFees, uint256 timestamp); + event ChangedFees(VaultFees oldFees, VaultFees newFees); + event FeeRecipientUpdated(address oldFeeRecipient, address newFeeRecipient); + + error InvalidVaultFees(); + error InvalidFeeRecipient(); + error NotPassedQuitPeriod(uint256 quitPeriod); + + /** + * @notice Propose new fees for this vault. Caller must be owner. + * @param newFees Fees for depositing, withdrawal, management and performance in 1e18. + * @dev Fees can be 0 but never 1e18 (1e18 = 100%, 1e14 = 1 BPS) + */ + function proposeFees(VaultFees calldata newFees) external onlyOwner { + if ( + newFees.deposit >= 1e18 || newFees.withdrawal >= 1e18 || newFees.management >= 1e18 || newFees.performance >= 1e18 + ) revert InvalidVaultFees(); + + proposedFees = newFees; + proposedFeeTime = block.timestamp; + + emit NewFeesProposed(newFees, block.timestamp); + } + + /// @notice Change fees to the previously proposed fees after the quit period has passed. + function changeFees() external { + if (proposedFeeTime == 0 || block.timestamp < proposedFeeTime + quitPeriod) revert NotPassedQuitPeriod(quitPeriod); + + emit ChangedFees(fees, proposedFees); + + fees = proposedFees; + feesUpdatedAt = block.timestamp; + + delete proposedFees; + delete proposedFeeTime; + } + + /** + * @notice Change `feeRecipient`. Caller must be Owner. + * @param _feeRecipient The new fee recipient. + * @dev Accrued fees wont be transferred to the new feeRecipient. + */ + function setFeeRecipient(address _feeRecipient) external onlyOwner { + if (_feeRecipient == address(0)) revert InvalidFeeRecipient(); + + emit FeeRecipientUpdated(feeRecipient, _feeRecipient); + + feeRecipient = _feeRecipient; + } + + /*////////////////////////////////////////////////////////////// + ADAPTER LOGIC + //////////////////////////////////////////////////////////////*/ + + AdapterConfig[10] public adapters; + AdapterConfig[10] public proposedAdapters; + + uint8 public adapterCount; + uint8 public proposedAdapterCount; + + uint256 public proposedAdapterTime; + + event NewAdaptersProposed(AdapterConfig[10] newAdapter, uint8 adapterCount, uint256 timestamp); + event ChangedAdapters( + AdapterConfig[10] oldAdapter, + uint8 oldAdapterCount, + AdapterConfig[10] newAdapter, + uint8 newAdapterCount + ); + + error AssetInvalid(); + error InvalidConfig(); + + /** + * @notice Propose a new adapter for this vault. Caller must be Owner. + * @param newAdapters A new ERC4626 that should be used as a yield adapter for this asset. + * @param newAdapterCount Amount of new adapters. + */ + function proposeAdapters(AdapterConfig[10] calldata newAdapters, uint8 newAdapterCount) external onlyOwner { + _verifyAdapterConfig(newAdapters, newAdapterCount); + + for (uint8 i; i < newAdapterCount; i++) { + proposedAdapters[i] = newAdapters[i]; + } + + proposedAdapterCount = newAdapterCount; + + proposedAdapterTime = block.timestamp; + + emit NewAdaptersProposed(newAdapters, proposedAdapterCount, block.timestamp); + } + + function _verifyAdapterConfig(AdapterConfig[10] calldata newAdapters, uint8 adapterCount_) internal { + if (adapterCount_ == 0 || adapterCount_ > 10) revert InvalidConfig(); + + uint256 totalAllocation; + for (uint8 i; i < adapterCount_; i++) { + if (newAdapters[i].adapter.asset() != asset()) revert AssetInvalid(); + + uint256 allocation = uint256(newAdapters[i].allocation); + if (allocation == 0) revert InvalidConfig(); + + totalAllocation += allocation; + } + if (totalAllocation != 1e18) revert InvalidConfig(); + } + + /** + * @notice Set a new Adapter for this Vault after the quit period has passed. + * @dev This migration function will remove all assets from the old Vault and move them into the new vault + * @dev Additionally it will zero old allowances and set new ones + * @dev Last we update HWM and assetsCheckpoint for fees to make sure they adjust to the new adapter + */ + function changeAdapters() external takeFees { + if (proposedAdapterTime == 0 || block.timestamp < proposedAdapterTime + quitPeriod) + revert NotPassedQuitPeriod(quitPeriod); + + for (uint8 i; i < adapterCount; i++) { + adapters[i].adapter.redeem(adapters[i].adapter.balanceOf(address(this)), address(this), address(this)); + + IERC20(asset()).approve(address(adapters[i].adapter), 0); + } + + emit ChangedAdapters(adapters, adapterCount, proposedAdapters, proposedAdapterCount); + + adapters = proposedAdapters; + adapterCount = proposedAdapterCount; + + uint256 totalAssets_ = IERC20(asset()).balanceOf(address(this)); + + for (uint8 i; i < adapterCount; i++) { + IERC20(asset()).approve(address(adapters[i].adapter), type(uint256).max); + + adapters[i].adapter.deposit( + totalAssets_.mulDiv(uint256(adapters[i].allocation), 1e18, Math.Rounding.Down), + address(this) + ); + } + + delete proposedAdapters; + delete proposedAdapterCount; + delete proposedAdapterTime; + } + + /*////////////////////////////////////////////////////////////// + RAGE QUIT LOGIC + //////////////////////////////////////////////////////////////*/ + + uint256 public quitPeriod; + + event QuitPeriodSet(uint256 quitPeriod); + + error InvalidQuitPeriod(); + + /** + * @notice Set a quitPeriod for rage quitting after new adapter or fees are proposed. Caller must be Owner. + * @param _quitPeriod Time to rage quit after proposal. + */ + function setQuitPeriod(uint256 _quitPeriod) external onlyOwner { + if (block.timestamp < proposedAdapterTime + quitPeriod || block.timestamp < proposedFeeTime + quitPeriod) + revert NotPassedQuitPeriod(quitPeriod); + if (_quitPeriod < 1 days || _quitPeriod > 7 days) revert InvalidQuitPeriod(); + + quitPeriod = _quitPeriod; + + emit QuitPeriodSet(quitPeriod); + } + + /*////////////////////////////////////////////////////////////// + DEPOSIT LIMIT LOGIC + //////////////////////////////////////////////////////////////*/ + + uint256 public depositLimit; + + event DepositLimitSet(uint256 depositLimit); + + /** + * @notice Sets a limit for deposits in assets. Caller must be Owner. + * @param _depositLimit Maximum amount of assets that can be deposited. + */ + function setDepositLimit(uint256 _depositLimit) external onlyOwner { + depositLimit = _depositLimit; + + emit DepositLimitSet(_depositLimit); + } + + /*////////////////////////////////////////////////////////////// + PAUSING LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @notice Pause deposits. Caller must be Owner. + function pause() external onlyOwner { + _pause(); + } + + /// @notice Unpause deposits. Caller must be Owner. + function unpause() external onlyOwner { + _unpause(); + } + + /*////////////////////////////////////////////////////////////// + EIP-2612 LOGIC + //////////////////////////////////////////////////////////////*/ + + // EIP-2612 STORAGE + uint256 internal INITIAL_CHAIN_ID; + bytes32 internal INITIAL_DOMAIN_SEPARATOR; + mapping(address => uint256) public nonces; + + error PermitDeadlineExpired(uint256 deadline); + error InvalidSigner(address signer); + + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual { + if (deadline < block.timestamp) revert PermitDeadlineExpired(deadline); + + // Unchecked because the only math done is incrementing + // the owner's nonce which cannot realistically overflow. + unchecked { + address recoveredAddress = ecrecover( + keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), + owner, + spender, + value, + nonces[owner]++, + deadline + ) + ) + ) + ), + v, + r, + s + ); + + if (recoveredAddress == address(0) || recoveredAddress != owner) revert InvalidSigner(recoveredAddress); + + _approve(recoveredAddress, spender, value); + } + } + + function DOMAIN_SEPARATOR() public view returns (bytes32) { + return block.chainid == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : computeDomainSeparator(); + } + + function computeDomainSeparator() internal view virtual returns (bytes32) { + return + keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes(name())), + keccak256("1"), + block.chainid, + address(this) + ) + ); + } +} diff --git a/packages/foundry/test/Tester.t.sol b/packages/foundry/test/Tester.t.sol new file mode 100644 index 00000000..4546ef0e --- /dev/null +++ b/packages/foundry/test/Tester.t.sol @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.15 + +pragma solidity ^0.8.15; +import { Test } from "forge-std/Test.sol"; +import { Math } from "openzeppelin-contracts/utils/math/Math.sol"; +import { MultiMockERC4626, IERC4626, IERC20 } from "./utils/mocks/MultiMockERC4626.sol"; +import { MockERC20 } from "./utils/mocks/MockERC20.sol"; +import { MockERC4626 } from "./utils/mocks/MockERC4626.sol"; +import { Clones } from "openzeppelin-contracts/proxy/Clones.sol"; + +contract Tester is Test { + using Math for uint256; + + MultiMockERC4626 multiMockERC4626; + MockERC20 asset; + MockERC4626 adapter1; + MockERC4626 adapter2; + MockERC4626 adapter3; + + IERC4626[10] adapters; + uint256[10] allocations; + + address adapterImplementation; + address implementation; + + function _createAdapter(IERC20 _asset) internal returns (MockERC4626) { + address adapterAddress = Clones.clone(adapterImplementation); + MockERC4626(adapterAddress).initialize(_asset, "Mock Token Vault", "vwTKN"); + return MockERC4626(adapterAddress); + } + + function setUp() public { + asset = new MockERC20("asset", "asset", 18); + + adapterImplementation = address(new MockERC4626()); + implementation = address(new MultiMockERC4626()); + + adapter1 = _createAdapter(IERC20(address(asset))); + adapter2 = _createAdapter(IERC20(address(asset))); + adapter3 = _createAdapter(IERC20(address(asset))); + + adapters[0] = IERC4626(address(adapter1)); + adapters[1] = IERC4626(address(adapter2)); + adapters[2] = IERC4626(address(adapter3)); + + allocations[0] = 0.25e18; + allocations[1] = 0.25e18; + allocations[2] = 0.5e18; + + multiMockERC4626 = MultiMockERC4626(Clones.clone(implementation)); + multiMockERC4626.initialize( + IERC20(address(asset)), + adapters, + allocations, + 2, + "MultiMockERC4626", + "MultiMockERC4626" + ); + } + + function test_deposit_withdraw() public { + uint256 amount = 4759; + asset.mint(address(this), amount); + asset.approve(address(multiMockERC4626), amount); + + uint256 prevD = multiMockERC4626.previewDeposit(amount); + uint256 shares = multiMockERC4626.deposit(amount, address(this)); + uint256 totalSupply = multiMockERC4626.totalSupply(); + uint256 totalAssets = multiMockERC4626.totalAssets(); + + emit log_named_uint("prevD", prevD); + emit log_named_uint("shares", shares); + emit log_named_uint("totalSupply", totalSupply); + emit log_named_uint("totalAssets", totalAssets); + + uint256 prevW = multiMockERC4626.previewWithdraw(amount); + uint256 burned = multiMockERC4626.withdraw(totalAssets, address(this), address(this)); + totalSupply = multiMockERC4626.totalSupply(); + totalAssets = multiMockERC4626.totalAssets(); + + emit log_named_uint("prevW", prevW); + emit log_named_uint("burned", burned); + emit log_named_uint("totalSupply", totalSupply); + emit log_named_uint("totalAssets", totalAssets); + } + + function test_deposit_redeem() public { + uint256 amount = 4759; + asset.mint(address(this), amount); + asset.approve(address(multiMockERC4626), amount); + + uint256 prevD = multiMockERC4626.previewDeposit(amount); + uint256 shares = multiMockERC4626.deposit(amount, address(this)); + uint256 totalSupply = multiMockERC4626.totalSupply(); + uint256 totalAssets = multiMockERC4626.totalAssets(); + + emit log_named_uint("prevD", prevD); + emit log_named_uint("shares", shares); + emit log_named_uint("totalSupply", totalSupply); + emit log_named_uint("totalAssets", totalAssets); + + uint256 prevR = multiMockERC4626.previewRedeem(totalSupply); + uint256 burned = multiMockERC4626.redeem(totalSupply, address(this), address(this)); + totalSupply = multiMockERC4626.totalSupply(); + totalAssets = multiMockERC4626.totalAssets(); + + emit log_named_uint("prevR", prevR); + emit log_named_uint("burned", burned); + emit log_named_uint("totalSupply", totalSupply); + emit log_named_uint("totalAssets", totalAssets); + } + + function test_mint_withdraw() public { + uint256 amount = 4759000000000; + asset.mint(address(this), amount); + asset.approve(address(multiMockERC4626), amount); + + uint256 prevM = multiMockERC4626.previewMint(amount); + uint256 assets = multiMockERC4626.mint(amount, address(this)); + uint256 totalSupply = multiMockERC4626.totalSupply(); + uint256 totalAssets = multiMockERC4626.totalAssets(); + + emit log_named_uint("prevM", prevM); + emit log_named_uint("assets", assets); + emit log_named_uint("totalSupply", totalSupply); + emit log_named_uint("totalAssets", totalAssets); + + uint256 prevW = multiMockERC4626.previewWithdraw(totalAssets); + uint256 returned = multiMockERC4626.withdraw(totalAssets, address(this), address(this)); + totalSupply = multiMockERC4626.totalSupply(); + totalAssets = multiMockERC4626.totalAssets(); + + emit log_named_uint("prevW", prevW); + emit log_named_uint("returned", returned); + emit log_named_uint("totalSupply", totalSupply); + emit log_named_uint("totalAssets", totalAssets); + } +} diff --git a/packages/foundry/test/utils/mocks/MultiMockERC4626.sol b/packages/foundry/test/utils/mocks/MultiMockERC4626.sol new file mode 100644 index 00000000..bfd39b9b --- /dev/null +++ b/packages/foundry/test/utils/mocks/MultiMockERC4626.sol @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.15 +pragma solidity ^0.8.15; + +import { IERC4626Upgradeable as IERC4626, IERC20Upgradeable as IERC20, IERC20MetadataUpgradeable as IERC20Metadata } from "openzeppelin-contracts-upgradeable/interfaces/IERC4626Upgradeable.sol"; +import { ERC4626Upgradeable, ERC20Upgradeable as ERC20 } from "openzeppelin-contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol"; +import { SafeERC20Upgradeable as SafeERC20 } from "openzeppelin-contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; +import { MathUpgradeable as Math } from "openzeppelin-contracts-upgradeable/utils/math/MathUpgradeable.sol"; + +contract MultiMockERC4626 is ERC4626Upgradeable { + using SafeERC20 for IERC20; + using Math for uint256; + + uint256 public beforeWithdrawHookCalledCounter = 0; + uint256 public afterDepositHookCalledCounter = 0; + + uint8 internal _decimals; + uint8 public constant decimalOffset = 9; + + IERC4626[10] adapters; + uint256[10] allocations; + + uint256 len; + + /*////////////////////////////////////////////////////////////// + IMMUTABLES + //////////////////////////////////////////////////////////////*/ + + function initialize( + IERC20 _asset, + IERC4626[10] calldata _adapters, + uint256[10] calldata _allocations, + uint256 _len, + string memory _name, + string memory _symbol + ) external initializer { + __ERC4626_init(IERC20Metadata(address(_asset))); + _decimals = IERC20Metadata(address(_asset)).decimals() + decimalOffset; + + len = _len; + + for (uint8 i; i < _len; i++) { + adapters[i] = _adapters[i]; + allocations[i] = _allocations[i]; + + _asset.safeApprove(address(_adapters[i]), type(uint256).max); + } + } + + /*////////////////////////////////////////////////////////////// + GENERAL VIEWS + //////////////////////////////////////////////////////////////*/ + + function decimals() public view override(IERC20Metadata, ERC20) returns (uint8) { + return _decimals; + } + + /*////////////////////////////////////////////////////////////// + ACCOUNTING LOGIC + //////////////////////////////////////////////////////////////*/ + + function totalAssets() public view override returns (uint256 assets) { + assets = IERC20(asset()).balanceOf(address(this)); + + for (uint8 i; i < len; i++) { + assets += adapters[i].convertToAssets(adapters[i].balanceOf(address(this))); + } + } + + // function previewDeposit(uint256 assets) public view override returns (uint256 shares) { + // uint256 leftover = assets; + // uint256 allocation_ = assets.mulDiv(allocation, 1e18, Math.Rounding.Down); + + // leftover -= allocation_ * 2; + // shares = convertToShares(assets - leftover); + // } + + // function previewMint(uint256 shares) public view override returns (uint256 assets) { + // assets = convertToAssets(shares); + + // uint256 leftover = assets; + // uint256 allocation_ = assets.mulDiv(allocation, 1e18, Math.Rounding.Down); + + // leftover -= allocation_ * 2; + // assets += leftover; + // } + + function _convertToShares(uint256 assets, Math.Rounding rounding) internal view override returns (uint256 shares) { + return assets.mulDiv(totalSupply() + 10**decimalOffset, totalAssets() + 1, rounding); + } + + function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view override returns (uint256) { + return shares.mulDiv(totalAssets() + 1, totalSupply() + 10**decimalOffset, rounding); + } + + /*////////////////////////////////////////////////////////////// + INTERNAL HOOKS LOGIC + //////////////////////////////////////////////////////////////*/ + + function _deposit( + address caller, + address receiver, + uint256 assets, + uint256 shares + ) internal override { + IERC20(asset()).safeTransferFrom(caller, address(this), assets); + + for (uint8 i; i < len; i++) { + uint256 allocation = assets.mulDiv(allocations[i], 1e18, Math.Rounding.Down); + adapters[i].deposit(allocation, address(this)); + } + + _mint(receiver, shares); + + emit Deposit(caller, receiver, assets, shares); + } + + function _withdraw( + address caller, + address receiver, + address owner, + uint256 assets, + uint256 shares + ) internal override { + if (caller != owner) { + _spendAllowance(owner, caller, shares); + } + + _burn(owner, shares); + + for (uint8 i; i < len; i++) { + uint256 allocation = assets.mulDiv(allocations[i], 1e18, Math.Rounding.Down); + adapters[i].withdraw(allocation, address(this), address(this)); + } + + IERC20(asset()).safeTransfer(receiver, assets); + + emit Withdraw(caller, receiver, owner, assets, shares); + } +} diff --git a/packages/foundry/test/vault/MultiStrategyVault.t.sol b/packages/foundry/test/vault/MultiStrategyVault.t.sol new file mode 100644 index 00000000..688a19c8 --- /dev/null +++ b/packages/foundry/test/vault/MultiStrategyVault.t.sol @@ -0,0 +1,1087 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.15 + +pragma solidity ^0.8.15; + +import { Test } from "forge-std/Test.sol"; +import { MockERC20 } from "../utils/mocks/MockERC20.sol"; +import { MockERC4626 } from "../utils/mocks/MockERC4626.sol"; +import { MultiStrategyVault, AdapterConfig } from "../../src/vault/MultiStrategyVault.sol"; +import { IERC4626Upgradeable as IERC4626, IERC20Upgradeable as IERC20 } from "openzeppelin-contracts-upgradeable/interfaces/IERC4626Upgradeable.sol"; +import { VaultFees } from "../../src/interfaces/vault/IVault.sol"; +import { FixedPointMathLib } from "solmate/utils/FixedPointMathLib.sol"; +import { Clones } from "openzeppelin-contracts/proxy/Clones.sol"; + +contract MultiStrategyVaultTest is Test { + using FixedPointMathLib for uint256; + + bytes32 constant PERMIT_TYPEHASH = + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + + MockERC20 asset; + MockERC4626 adapter1; + MockERC4626 adapter2; + MultiStrategyVault vault; + + AdapterConfig[10] adapters; + + address adapterImplementation; + address implementation; + + uint256 constant ONE = 1e18; + uint256 constant SECONDS_PER_YEAR = 365.25 days; + + address feeRecipient = address(0x4444); + address alice = address(0xABCD); + address bob = address(0xDCBA); + + event NewFeesProposed(VaultFees newFees, uint256 timestamp); + event ChangedFees(VaultFees oldFees, VaultFees newFees); + event FeeRecipientUpdated(address oldFeeRecipient, address newFeeRecipient); + event NewAdaptersProposed(AdapterConfig[10] newAdapter, uint8 adapterCount, uint256 timestamp); + event ChangedAdapters( + AdapterConfig[10] oldAdapter, + uint8 oldAdapterCount, + AdapterConfig[10] newAdapter, + uint8 newAdapterCount + ); + event QuitPeriodSet(uint256 quitPeriod); + event Paused(address account); + event Unpaused(address account); + event DepositLimitSet(uint256 depositLimit); + + function setUp() public { + vm.label(feeRecipient, "feeRecipient"); + vm.label(alice, "alice"); + vm.label(bob, "bob"); + + asset = new MockERC20("Mock Token", "TKN", 18); + + adapterImplementation = address(new MockERC4626()); + implementation = address(new MultiStrategyVault()); + + adapter1 = _createAdapter(IERC20(address(asset))); + adapter2 = _createAdapter(IERC20(address(asset))); + + vm.label(address(adapter1), "adapter1"); + vm.label(address(adapter2), "adapter2"); + + adapters[0] = AdapterConfig({ adapter: IERC4626(address(adapter1)), allocation: 0.5e18 }); + adapters[1] = AdapterConfig({ adapter: IERC4626(address(adapter2)), allocation: 0.5e18 }); + + address vaultAddress = Clones.clone(implementation); + vault = MultiStrategyVault(vaultAddress); + vm.label(vaultAddress, "vault"); + + vault.initialize( + IERC20(address(asset)), + adapters, + 2, + VaultFees({ deposit: 0, withdrawal: 0, management: 0, performance: 0 }), + feeRecipient, + type(uint256).max, + address(this) + ); + } + + /*////////////////////////////////////////////////////////////// + HELPER + //////////////////////////////////////////////////////////////*/ + + function _setFees( + uint64 depositFee, + uint64 withdrawalFee, + uint64 managementFee, + uint64 performanceFee + ) internal { + vault.proposeFees( + VaultFees({ + deposit: depositFee, + withdrawal: withdrawalFee, + management: managementFee, + performance: performanceFee + }) + ); + + vm.warp(block.timestamp + 3 days); + vault.changeFees(); + } + + function _createAdapter(IERC20 _asset) internal returns (MockERC4626) { + address adapterAddress = Clones.clone(adapterImplementation); + MockERC4626(adapterAddress).initialize(_asset, "Mock Token Vault", "vwTKN"); + return MockERC4626(adapterAddress); + } + + /*////////////////////////////////////////////////////////////// + INITIALIZATION + //////////////////////////////////////////////////////////////*/ + function test__metadata() public { + address vaultAddress = Clones.clone(implementation); + MultiStrategyVault newVault = MultiStrategyVault(vaultAddress); + + uint256 callTime = block.timestamp; + newVault.initialize( + IERC20(address(asset)), + adapters, + 2, + VaultFees({ deposit: 100, withdrawal: 100, management: 100, performance: 100 }), + feeRecipient, + type(uint256).max, + bob + ); + + assertEq(newVault.name(), "Popcorn Mock Token Vault"); + assertEq(newVault.symbol(), "pop-TKN"); + assertEq(newVault.decimals(), 27); + + (IERC4626 adapter0_, uint64 allocation0_) = newVault.adapters(0); + (IERC4626 adapter1_, uint64 allocation1_) = newVault.adapters(1); + assertEq(address(adapter0_), address(adapter1)); + assertEq(allocation0_, 0.5e18); + assertEq(address(adapter1_), address(adapter2)); + assertEq(allocation1_, 0.5e18); + assertEq(asset.allowance(address(newVault), address(adapter0_)), type(uint256).max); + assertEq(asset.allowance(address(newVault), address(adapter1_)), type(uint256).max); + + assertEq(newVault.adapterCount(), 2); + + assertEq(address(newVault.asset()), address(asset)); + assertEq(newVault.owner(), bob); + + (uint256 deposit, uint256 withdrawal, uint256 management, uint256 performance) = newVault.fees(); + assertEq(deposit, 100); + assertEq(withdrawal, 100); + assertEq(management, 100); + assertEq(performance, 100); + assertEq(newVault.feeRecipient(), feeRecipient); + assertEq(newVault.highWaterMark(), 1e9); + assertEq(newVault.feesUpdatedAt(), callTime); + + assertEq(newVault.quitPeriod(), 3 days); + } + + function testFail__initialize_asset_is_zero() public { + address vaultAddress = address(new MultiStrategyVault()); + vm.label(vaultAddress, "vault"); + + vault = MultiStrategyVault(vaultAddress); + vault.initialize( + IERC20(address(0)), + adapters, + 2, + VaultFees({ deposit: 0, withdrawal: 0, management: 0, performance: 0 }), + feeRecipient, + type(uint256).max, + address(this) + ); + } + + function testFail__initialize_adapter_asset_is_not_matching() public { + MockERC20 newAsset = new MockERC20("New Mock Token", "NTKN", 18); + adapters[0].adapter = _createAdapter(IERC20(address(newAsset))); + + address vaultAddress = address(new MultiStrategyVault()); + + vault = MultiStrategyVault(vaultAddress); + vault.initialize( + IERC20(address(asset)), + adapters, + 2, + VaultFees({ deposit: 0, withdrawal: 0, management: 0, performance: 0 }), + feeRecipient, + type(uint256).max, + address(this) + ); + } + + function testFail__initialize_adapter_allocation_zero() public { + adapters[0].allocation = 0; + + address vaultAddress = address(new MultiStrategyVault()); + + vault = MultiStrategyVault(vaultAddress); + vault.initialize( + IERC20(address(asset)), + adapters, + 2, + VaultFees({ deposit: 0, withdrawal: 0, management: 0, performance: 0 }), + feeRecipient, + type(uint256).max, + address(this) + ); + } + + function testFail__initialize_adapter_allocation_too_low() public { + adapters[0].allocation = 0.4e18; + + address vaultAddress = address(new MultiStrategyVault()); + + vault = MultiStrategyVault(vaultAddress); + vault.initialize( + IERC20(address(asset)), + adapters, + 2, + VaultFees({ deposit: 0, withdrawal: 0, management: 0, performance: 0 }), + feeRecipient, + type(uint256).max, + address(this) + ); + } + + function testFail__initialize_adapter_allocation_too_high() public { + adapters[0].allocation = 0.6e18; + + address vaultAddress = address(new MultiStrategyVault()); + + vault = MultiStrategyVault(vaultAddress); + vault.initialize( + IERC20(address(asset)), + adapters, + 2, + VaultFees({ deposit: 0, withdrawal: 0, management: 0, performance: 0 }), + feeRecipient, + type(uint256).max, + address(this) + ); + } + + function testFail__initialize_adapterCount_too_high() public { + address vaultAddress = address(new MultiStrategyVault()); + + vault = MultiStrategyVault(vaultAddress); + vault.initialize( + IERC20(address(asset)), + adapters, + 11, + VaultFees({ deposit: 0, withdrawal: 0, management: 0, performance: 0 }), + feeRecipient, + type(uint256).max, + address(this) + ); + } + + function testFail__initialize_adapterCount_too_low() public { + address vaultAddress = address(new MultiStrategyVault()); + + vault = MultiStrategyVault(vaultAddress); + vault.initialize( + IERC20(address(asset)), + adapters, + 0, + VaultFees({ deposit: 0, withdrawal: 0, management: 0, performance: 0 }), + feeRecipient, + type(uint256).max, + address(this) + ); + } + + function testFail__initialize_feeRecipient_addressZero() public { + address vaultAddress = address(new MultiStrategyVault()); + + vault = MultiStrategyVault(vaultAddress); + vault.initialize( + IERC20(address(asset)), + adapters, + 2, + VaultFees({ deposit: 0, withdrawal: 0, management: 0, performance: 0 }), + address(0), + type(uint256).max, + address(this) + ); + } + + /*////////////////////////////////////////////////////////////// + DEPOSIT / WITHDRAW + //////////////////////////////////////////////////////////////*/ + + function test__deposit_withdraw(uint128 amount) public { + if (amount != 5749) amount = 5749; + + uint256 aliceAssetAmount = amount; + + asset.mint(alice, aliceAssetAmount); + + vm.prank(alice); + asset.approve(address(vault), aliceAssetAmount); + assertEq(asset.allowance(alice, address(vault)), aliceAssetAmount); + + uint256 alicePreDepositBal = asset.balanceOf(alice); + + uint256 expectedShareAmount = vault.previewDeposit(aliceAssetAmount); + + vm.prank(alice); + uint256 aliceShareAmount = vault.deposit(aliceAssetAmount, alice); + + emit log_named_uint("pps", vault.convertToAssets(1e18)); + emit log_named_uint("mw", vault.maxWithdraw(alice)); + emit log_named_uint("shares", aliceShareAmount); + + // emit log_named_uint("amount", aliceAssetAmount); + // emit log_named_uint("shares", aliceShareAmount); + + assertEq(adapter1.afterDepositHookCalledCounter(), 1, "d adapter1"); + assertEq(adapter2.afterDepositHookCalledCounter(), 1, "d adapter2"); + + // Expect exchange rate to be 1:1e9 on initial deposit. + assertEq(expectedShareAmount, aliceShareAmount, "preview = deposit"); + assertEq(vault.previewWithdraw(vault.maxWithdraw(alice)), aliceShareAmount, "pw"); + assertEq(vault.previewDeposit(aliceAssetAmount), aliceShareAmount, "pd"); + assertEq(vault.totalSupply(), aliceShareAmount, "ts"); + assertEq(vault.totalAssets(), aliceAssetAmount, "ta"); + assertEq(vault.balanceOf(alice), aliceShareAmount, "vault bal"); + assertEq(vault.convertToAssets(aliceShareAmount), aliceAssetAmount, "convert"); + assertEq(asset.balanceOf(alice), alicePreDepositBal - aliceAssetAmount, "asset bal"); + + vm.prank(alice); + vault.withdraw(aliceAssetAmount, alice, alice); + + assertEq(adapter1.beforeWithdrawHookCalledCounter(), 1, "w adapter1"); + assertEq(adapter2.beforeWithdrawHookCalledCounter(), 1, "w adapter2"); + + assertEq(vault.totalAssets(), 0, "ta2"); + assertEq(vault.balanceOf(alice), 0, "vault bal2"); + assertEq(vault.convertToAssets(vault.balanceOf(alice)), 0, "convert2"); + assertEq(asset.balanceOf(alice), alicePreDepositBal, "asset bal2"); + } + + function testFail__deposit_with_no_approval() public { + vault.deposit(1e18, address(this)); + } + + function testFail__deposit_with_not_enough_approval() public { + asset.mint(address(this), 1e18); + asset.approve(address(vault), 0.5e18); + assertEq(asset.allowance(address(this), address(vault)), 0.5e18); + + vault.deposit(1e18, address(this)); + } + + function testFail__withdraw_with_not_enough_assets() public { + asset.mint(address(this), 0.5e18); + asset.approve(address(vault), 0.5e18); + + vault.deposit(0.5e18, address(this)); + + vault.withdraw(1e18, address(this), address(this)); + } + + function testFail__withdraw_with_no_assets() public { + vault.withdraw(1e18, address(this), address(this)); + } + + /*////////////////////////////////////////////////////////////// + MINT / REDEEM + //////////////////////////////////////////////////////////////*/ + + function test__mint_redeem(uint128 amount) public { + if (amount < 1e9) amount = 1e9; + + uint256 aliceShareAmount = amount; + asset.mint(alice, aliceShareAmount); + + vm.prank(alice); + asset.approve(address(vault), aliceShareAmount); + assertEq(asset.allowance(alice, address(vault)), aliceShareAmount); + + uint256 alicePreDepositBal = asset.balanceOf(alice); + + vm.prank(alice); + uint256 aliceAssetAmount = vault.mint(aliceShareAmount, alice); + + assertEq(adapter1.afterDepositHookCalledCounter(), 1); + assertEq(adapter2.afterDepositHookCalledCounter(), 1); + + // Expect exchange rate to be 1e9:1 on initial mint. + // We allow 1e9 delta since virtual shares lead to amounts between 1e9 to demand/mint more shares + // E.g. (1e9 + 1) to 2e9 assets requires 2e9 shares to withdraw + assertApproxEqAbs(aliceShareAmount / 1e9, aliceAssetAmount, 1e9, "share = assets"); + assertApproxEqAbs(vault.previewWithdraw(aliceAssetAmount), aliceShareAmount, 1e9, "pw"); + assertApproxEqAbs(vault.previewDeposit(aliceAssetAmount), aliceShareAmount, 1e9, "pd"); + assertEq(vault.totalSupply(), aliceShareAmount, "ts"); + assertEq(vault.totalAssets(), aliceAssetAmount, "ta"); + assertEq(vault.balanceOf(alice), aliceShareAmount, "bal"); + assertApproxEqAbs(vault.convertToAssets(vault.balanceOf(alice)), aliceAssetAmount, 1e9, "convert"); + assertEq(asset.balanceOf(alice), alicePreDepositBal - aliceAssetAmount, "a bal"); + + vm.prank(alice); + vault.redeem(aliceShareAmount, alice, alice); + + assertEq(adapter1.beforeWithdrawHookCalledCounter(), 1); + assertEq(adapter2.beforeWithdrawHookCalledCounter(), 1); + + assertEq(vault.totalAssets(), 0); + assertEq(vault.balanceOf(alice), 0); + assertEq(vault.convertToAssets(vault.balanceOf(alice)), 0); + assertEq(asset.balanceOf(alice), alicePreDepositBal); + } + + function testFail__mint_with_no_approval() public { + vault.mint(1e18, address(this)); + } + + function testFail__mint_with_not_enough_approval() public { + asset.mint(address(this), 1e18); + asset.approve(address(vault), 1e6); + assertEq(asset.allowance(address(this), address(vault)), 1e6); + + vault.mint(1e18, address(this)); + } + + function testFail__redeem_with_not_enough_shares() public { + asset.mint(address(this), 0.5e18); + asset.approve(address(vault), 0.5e18); + + vault.deposit(0.5e18, address(this)); + + vault.redeem(1e27, address(this), address(this)); + } + + function testFail__redeem_with_no_shares() public { + vault.redeem(1e18, address(this), address(this)); + } + + /*////////////////////////////////////////////////////////////// + DEPOSIT / MINT / WITHDRAW / REDEEM + //////////////////////////////////////////////////////////////*/ + + function test__interactions_for_someone_else() public { + // init 2 users with a 1e18 balance + asset.mint(alice, 1e18); + asset.mint(bob, 1e18); + + vm.prank(alice); + asset.approve(address(vault), 1e18); + + vm.prank(bob); + asset.approve(address(vault), 1e18); + + // alice deposits 1e18 for bob + vm.prank(alice); + vault.deposit(1e18, bob); + + assertEq(vault.balanceOf(alice), 0); + assertEq(vault.balanceOf(bob), 1e27); + assertEq(asset.balanceOf(alice), 0); + + // bob mint 1e27 for alice + vm.prank(bob); + vault.mint(1e27, alice); + assertEq(vault.balanceOf(alice), 1e27); + assertEq(vault.balanceOf(bob), 1e27); + assertEq(asset.balanceOf(bob), 0); + + // alice redeem 1e27 for bob + vm.prank(alice); + vault.redeem(1e27, bob, alice); + + assertEq(vault.balanceOf(alice), 0); + assertEq(vault.balanceOf(bob), 1e27); + assertEq(asset.balanceOf(bob), 1e18); + + // bob withdraw 1e27 for alice + vm.prank(bob); + vault.withdraw(1e18, alice, bob); + + assertEq(vault.balanceOf(alice), 0); + assertEq(vault.balanceOf(bob), 0); + assertEq(asset.balanceOf(alice), 1e18); + } + + /*////////////////////////////////////////////////////////////// + TAKING FEES + //////////////////////////////////////////////////////////////*/ + + function test__previewDeposit_previewMint_takes_fees_into_account(uint8 fuzzAmount) public { + uint256 amount = bound(uint256(fuzzAmount), 1, 1 ether); + + _setFees(1e17, 0, 0, 0); + + asset.mint(alice, amount); + + vm.prank(alice); + asset.approve(address(vault), amount); + + // Test PreviewDeposit and Deposit + uint256 expectedShares = vault.previewDeposit(amount); + + vm.prank(alice); + uint256 actualShares = vault.deposit(amount, alice); + assertApproxEqAbs(expectedShares, actualShares, 2); + } + + function test__previewWithdraw_previewRedeem_takes_fees_into_account(uint8 fuzzAmount) public { + uint256 amount = bound(uint256(fuzzAmount), 10, 1 ether); + + _setFees(0, 1e17, 0, 0); + + asset.mint(alice, amount); + asset.mint(bob, amount); + + vm.startPrank(alice); + asset.approve(address(vault), amount); + uint256 shares = vault.deposit(amount, alice); + vm.stopPrank(); + + vm.startPrank(bob); + asset.approve(address(vault), amount); + vault.deposit(amount, bob); + vm.stopPrank(); + + // Test PreviewWithdraw and Withdraw + // NOTE: Reduce the amount of assets to withdraw to take withdrawalFee into account (otherwise we would withdraw more than we deposited) + uint256 withdrawAmount = (amount / 10) * 9; + uint256 expectedShares = vault.previewWithdraw(withdrawAmount); + emit log_uint(expectedShares); + + vm.prank(alice); + uint256 actualShares = vault.withdraw(withdrawAmount, alice, alice); + emit log_uint(actualShares); + assertApproxEqAbs(expectedShares, actualShares, 1); + + // Test PreviewRedeem and Redeem + uint256 expectedAssets = vault.previewRedeem(shares); + + vm.prank(bob); + uint256 actualAssets = vault.redeem(shares, bob, bob); + assertApproxEqAbs(expectedAssets, actualAssets, 1); + } + + function test__managementFee(uint128 timeframe) public { + // Test Timeframe less than 10 years + timeframe = uint128(bound(timeframe, 1, 315576000)); + uint256 depositAmount = 1 ether; + + _setFees(0, 0, 1e17, 0); + + asset.mint(alice, depositAmount); + vm.startPrank(alice); + asset.approve(address(vault), depositAmount); + vault.deposit(depositAmount, alice); + vm.stopPrank(); + + // Increase Block Time to trigger managementFee + vm.warp(block.timestamp + timeframe); + + uint256 expectedFeeInAsset = vault.accruedManagementFee(); + + uint256 supply = vault.totalSupply(); + uint256 expectedFeeInShares = supply == 0 + ? expectedFeeInAsset + : expectedFeeInAsset.mulDivDown(supply, 1 ether - expectedFeeInAsset); + + vault.takeManagementAndPerformanceFees(); + + assertEq(vault.totalSupply(), (depositAmount * 1e9) + expectedFeeInShares, "ts"); + assertEq(vault.balanceOf(feeRecipient), expectedFeeInShares, "fee bal"); + assertApproxEqAbs(vault.convertToAssets(expectedFeeInShares), expectedFeeInAsset, 10, "convert back"); + + // High Water Mark should remain unchanged + assertEq(vault.highWaterMark(), 1e9, "hwm"); + } + + function test__managementFee_change_fees_later() public { + uint256 depositAmount = 1 ether; + + asset.mint(alice, depositAmount); + vm.startPrank(alice); + asset.approve(address(vault), depositAmount); + vault.deposit(depositAmount, alice); + vm.stopPrank(); + + // Set it to half the time without any fees + vm.warp(block.timestamp + (SECONDS_PER_YEAR / 2)); + assertEq(vault.accruedManagementFee(), 0); + + _setFees(0, 0, 1e17, 0); + + vm.warp(block.timestamp + (SECONDS_PER_YEAR / 2)); + + assertEq(vault.accruedManagementFee(), ((1 ether * 1e17) / 1e18) / 2); + } + + function test__performanceFee(uint128 amount) public { + vm.assume(amount >= 1e18); + uint256 depositAmount = 1 ether; + + _setFees(0, 0, 0, 1e17); + + asset.mint(alice, depositAmount); + vm.startPrank(alice); + asset.approve(address(vault), depositAmount); + vault.deposit(depositAmount, alice); + vm.stopPrank(); + + // Increase asset assets to trigger performanceFee + asset.mint(address(adapter1), amount); + + uint256 expectedFeeInAsset = vault.accruedPerformanceFee(); + + uint256 supply = vault.totalSupply(); + uint256 totalAssets = vault.totalAssets(); + + uint256 expectedFeeInShares = supply == 0 + ? expectedFeeInAsset + : expectedFeeInAsset.mulDivDown(supply, totalAssets - expectedFeeInAsset); + + vault.takeManagementAndPerformanceFees(); + + assertEq(vault.totalSupply(), (depositAmount * 1e9) + expectedFeeInShares, "ts"); + assertEq(vault.balanceOf(feeRecipient), expectedFeeInShares, "bal"); + + // There should be a new High Water Mark + assertApproxEqRel(vault.highWaterMark(), totalAssets / 1e9, 10, "hwm"); + } + + function test_performanceFee2() public { + asset = new MockERC20("Mock Token", "TKN", 6); + adapters[0].adapter = _createAdapter(IERC20(address(asset))); + adapters[0].allocation = 1e18; + + address vaultAddress = Clones.clone(implementation); + vault = MultiStrategyVault(vaultAddress); + vault.initialize( + IERC20(address(asset)), + adapters, + 1, + VaultFees({ deposit: 0, withdrawal: 0, management: 0, performance: 1e17 }), + feeRecipient, + type(uint256).max, + address(this) + ); + + uint256 depositAmount = 1e6; + asset.mint(alice, depositAmount); + vm.startPrank(alice); + asset.approve(address(vault), depositAmount); + vault.deposit(depositAmount, alice); + vm.stopPrank(); + + asset.mint(address(adapters[0].adapter), 1e6); + + // Take 10% of 1e6 + assertEq(vault.accruedPerformanceFee(), 1e5 - 1); + } + + /*////////////////////////////////////////////////////////////// + CHANGE FEES + //////////////////////////////////////////////////////////////*/ + + // Propose Fees + function test__proposeFees() public { + VaultFees memory newVaultFees = VaultFees({ deposit: 1, withdrawal: 1, management: 1, performance: 1 }); + + uint256 callTime = block.timestamp; + vm.expectEmit(false, false, false, true, address(vault)); + emit NewFeesProposed(newVaultFees, callTime); + + vault.proposeFees(newVaultFees); + + assertEq(vault.proposedFeeTime(), callTime); + (uint256 deposit, uint256 withdrawal, uint256 management, uint256 performance) = vault.proposedFees(); + assertEq(deposit, 1); + assertEq(withdrawal, 1); + assertEq(management, 1); + assertEq(performance, 1); + } + + function testFail__proposeFees_nonOwner() public { + VaultFees memory newVaultFees = VaultFees({ deposit: 1, withdrawal: 1, management: 1, performance: 1 }); + + vm.prank(alice); + vault.proposeFees(newVaultFees); + } + + function testFail__proposeFees_fees_too_high() public { + VaultFees memory newVaultFees = VaultFees({ deposit: 1e18, withdrawal: 1, management: 1, performance: 1 }); + + vault.proposeFees(newVaultFees); + } + + // Change Fees + function test__changeFees() public { + VaultFees memory newVaultFees = VaultFees({ deposit: 1, withdrawal: 1, management: 1, performance: 1 }); + vault.proposeFees(newVaultFees); + + vm.warp(block.timestamp + 3 days); + + vm.expectEmit(false, false, false, true, address(vault)); + emit ChangedFees(VaultFees({ deposit: 0, withdrawal: 0, management: 0, performance: 0 }), newVaultFees); + + vault.changeFees(); + + (uint256 deposit, uint256 withdrawal, uint256 management, uint256 performance) = vault.fees(); + assertEq(deposit, 1); + assertEq(withdrawal, 1); + assertEq(management, 1); + assertEq(performance, 1); + (uint256 propDeposit, uint256 propWithdrawal, uint256 propManagement, uint256 propPerformance) = vault + .proposedFees(); + assertEq(propDeposit, 0); + assertEq(propWithdrawal, 0); + assertEq(propManagement, 0); + assertEq(propPerformance, 0); + assertEq(vault.proposedFeeTime(), 0); + } + + function testFail__changeFees_NonOwner() public { + vm.prank(alice); + vault.changeFees(); + } + + function testFail__changeFees_respect_rageQuit() public { + VaultFees memory newVaultFees = VaultFees({ deposit: 1, withdrawal: 1, management: 1, performance: 1 }); + vault.proposeFees(newVaultFees); + + // Didnt respect 3 days before propsal and change + vault.changeFees(); + } + + function testFail__changeFees_after_init() public { + vault.changeFees(); + } + + /*////////////////////////////////////////////////////////////// + SET FEE_RECIPIENT + //////////////////////////////////////////////////////////////*/ + + function test__setFeeRecipient() public { + vm.expectEmit(false, false, false, true, address(vault)); + emit FeeRecipientUpdated(feeRecipient, alice); + + vault.setFeeRecipient(alice); + + assertEq(vault.feeRecipient(), alice); + } + + function testFail__setFeeRecipient_NonOwner() public { + vm.prank(alice); + vault.setFeeRecipient(alice); + } + + function testFail__setFeeRecipient_addressZero() public { + vault.setFeeRecipient(address(0)); + } + + /*////////////////////////////////////////////////////////////// + CHANGE ADAPTER + //////////////////////////////////////////////////////////////*/ + + // Propose Adapter + function test__proposeAdapters() public { + MockERC4626 newAdapter = _createAdapter(IERC20(address(asset))); + adapters[0].adapter = newAdapter; + adapters[0].allocation = 1e18; + + uint256 callTime = block.timestamp; + vm.expectEmit(false, false, false, true, address(vault)); + emit NewAdaptersProposed(adapters, 1, callTime); + + vault.proposeAdapters(adapters, 1); + + assertEq(vault.proposedAdapterTime(), callTime); + //assertEq(address(vault.proposedAdapters()), address(newAdapter)); + assertEq(uint256(vault.proposedAdapterCount()), uint256(1)); + } + + function testFail__proposeAdapters_nonOwner() public { + MockERC4626 newAdapter = _createAdapter(IERC20(address(asset))); + adapters[0].adapter = newAdapter; + adapters[0].allocation = 1e18; + + vm.prank(alice); + vault.proposeAdapters(adapters, 2); + } + + function testFail__proposeAdapters_asset_missmatch() public { + MockERC20 newAsset = new MockERC20("New Mock Token", "NTKN", 18); + MockERC4626 newAdapter = _createAdapter(IERC20(address(newAsset))); + adapters[0].adapter = newAdapter; + adapters[0].allocation = 1e18; + + vault.proposeAdapters(adapters, 1); + } + + // Change Adapter + function test__changeAdapters() public { + MockERC4626 newAdapter = _createAdapter(IERC20(address(asset))); + adapters[0].adapter = newAdapter; + adapters[0].allocation = 1e18; + + uint256 depositAmount = 1 ether; + + // Deposit funds for testing + asset.mint(alice, depositAmount); + vm.startPrank(alice); + asset.approve(address(vault), depositAmount); + vault.deposit(depositAmount, alice); + vm.stopPrank(); + + // Increase assets in asset Adapter to check hwm and assetCheckpoint later + asset.mint(address(adapter1), depositAmount); + vault.takeManagementAndPerformanceFees(); + uint256 oldHWM = vault.highWaterMark(); + + // Preparation to change the adapter + vault.proposeAdapters(adapters, 1); + + vm.warp(block.timestamp + 3 days); + + // vm.expectEmit(false, false, false, true, address(vault)); + // emit ChangedAdapter(IERC4626(address(adapter)), IERC4626(address(newAdapter))); + + vault.changeAdapters(); + + // Annoyingly Math fails us here and leaves 2 asset in the adapter + assertEq(asset.allowance(address(vault), address(adapter1)), 0); + assertEq(asset.balanceOf(address(adapter1)), 2); + assertEq(adapter1.balanceOf(address(vault)), 0); + + assertEq(asset.balanceOf(address(newAdapter)), (depositAmount*2)- 2); + assertEq(newAdapter.balanceOf(address(vault)), (depositAmount * 2e9) - 2e9); + assertEq(asset.allowance(address(vault), address(newAdapter)), type(uint256).max); + + assertEq(vault.highWaterMark(), oldHWM); + + assertEq(vault.proposedAdapterTime(), 0); + //assertEq(address(vault.proposedAdapters()), address(0)); + } + + function testFail__changeAdapters_NonOwner() public { + vm.prank(alice); + vault.changeAdapters(); + } + + function testFail__changeAdapters_respect_rageQuit() public { + MockERC4626 newAdapter = _createAdapter(IERC20(address(asset))); + adapters[0].adapter = newAdapter; + adapters[0].allocation = 1e18; + + vault.proposeAdapters(adapters, 1); + + // Didnt respect 3 days before propsal and change + vault.changeAdapters(); + } + + function testFail__changeAdapters_after_init() public { + vault.changeAdapters(); + } + + function testFail__changeAdapters_instantly_again() public { + MockERC4626 newAdapter = _createAdapter(IERC20(address(asset))); + adapters[0].adapter = newAdapter; + adapters[0].allocation = 1e18; + + uint256 depositAmount = 1 ether; + + // Deposit funds for testing + asset.mint(alice, depositAmount); + vm.startPrank(alice); + asset.approve(address(vault), depositAmount); + vault.deposit(depositAmount, alice); + vm.stopPrank(); + + // Increase assets in asset Adapter to check hwm and assetCheckpoint later + asset.mint(address(adapter1), depositAmount); + vault.takeManagementAndPerformanceFees(); + uint256 oldHWM = vault.highWaterMark(); + + // Preparation to change the adapter + vault.proposeAdapters(adapters, 1); + + vm.warp(block.timestamp + 3 days); + + // vm.expectEmit(false, false, false, true, address(vault)); + // emit ChangedAdapter(IERC4626(address(adapter)), IERC4626(address(newAdapter))); + + vault.changeAdapters(); + vault.changeAdapters(); + } + + /*////////////////////////////////////////////////////////////// + SET RAGE QUIT + //////////////////////////////////////////////////////////////*/ + + function test__setQuitPeriod() public { + // Pass the inital quit period + vm.warp(block.timestamp + 3 days); + + uint256 newQuitPeriod = 1 days; + vm.expectEmit(false, false, false, true, address(vault)); + emit QuitPeriodSet(newQuitPeriod); + + vault.setQuitPeriod(newQuitPeriod); + + assertEq(vault.quitPeriod(), newQuitPeriod); + } + + function testFail__setQuitPeriod_NonOwner() public { + vm.prank(alice); + vault.setQuitPeriod(1 days); + } + + function testFail__setQuitPeriod_too_low() public { + vault.setQuitPeriod(23 hours); + } + + function testFail__setQuitPeriod_too_high() public { + vault.setQuitPeriod(8 days); + } + + function testFail__setQuitPeriod_during_initial_quitPeriod() public { + vault.setQuitPeriod(1 days); + } + + function testFail__setQuitPeriod_during_adapter_quitPeriod() public { + MockERC4626 newAdapter = _createAdapter(IERC20(address(asset))); + + // Pass the inital quit period + vm.warp(block.timestamp + 3 days); + + vault.proposeAdapters(adapters, 2); + + vault.setQuitPeriod(1 days); + } + + function testFail__setQuitPeriod_during_fee_quitPeriod() public { + // Pass the inital quit period + vm.warp(block.timestamp + 3 days); + + vault.proposeFees(VaultFees({ deposit: 1, withdrawal: 1, management: 1, performance: 1 })); + + vault.setQuitPeriod(1 days); + } + + /*////////////////////////////////////////////////////////////// + SET DEPOSIT LIMIT + //////////////////////////////////////////////////////////////*/ + + function test__setDepositLimit() public { + uint256 newDepositLimit = 100; + vm.expectEmit(false, false, false, true, address(vault)); + emit DepositLimitSet(newDepositLimit); + + vault.setDepositLimit(newDepositLimit); + + assertEq(vault.depositLimit(), newDepositLimit); + + asset.mint(address(this), 101); + asset.approve(address(vault), 101); + + vm.expectRevert(abi.encodeWithSelector(MultiStrategyVault.MaxError.selector, 101)); + vault.deposit(101, address(this)); + + vm.expectRevert(abi.encodeWithSelector(MultiStrategyVault.MaxError.selector, 101 * 1e9)); + vault.mint(101 * 1e9, address(this)); + } + + function testFail__setDepositLimit_NonOwner() public { + vm.prank(alice); + vault.setDepositLimit(uint256(100)); + } + + /*////////////////////////////////////////////////////////////// + PAUSE + //////////////////////////////////////////////////////////////*/ + + // Pause + function test__pause() public { + uint256 depositAmount = 1 ether; + + // Deposit funds for testing + asset.mint(alice, depositAmount * 3); + vm.startPrank(alice); + asset.approve(address(vault), depositAmount * 3); + vault.deposit(depositAmount * 2, alice); + vm.stopPrank(); + + vm.expectEmit(false, false, false, true, address(vault)); + emit Paused(address(this)); + + vault.pause(); + + assertTrue(vault.paused()); + + vm.prank(alice); + vm.expectRevert(abi.encodeWithSelector(MultiStrategyVault.MaxError.selector, depositAmount)); + vault.deposit(depositAmount, alice); + + vm.prank(alice); + vm.expectRevert(abi.encodeWithSelector(MultiStrategyVault.MaxError.selector, depositAmount)); + vault.mint(depositAmount, alice); + + vm.prank(alice); + vault.withdraw(depositAmount, alice, alice); + + vm.prank(alice); + vault.redeem(depositAmount, alice, alice); + } + + function testFail__pause_nonOwner() public { + vm.prank(alice); + vault.pause(); + } + + // Unpause + function test__unpause() public { + uint256 depositAmount = 1 ether; + + // Deposit funds for testing + asset.mint(alice, depositAmount * 2); + vm.prank(alice); + asset.approve(address(vault), depositAmount * 2); + + vault.pause(); + + vm.expectEmit(false, false, false, true, address(vault)); + emit Unpaused(address(this)); + + vault.unpause(); + + assertFalse(vault.paused()); + + vm.prank(alice); + vault.deposit(depositAmount, alice); + + vm.prank(alice); + vault.mint(depositAmount, alice); + + vm.prank(alice); + vault.withdraw(depositAmount, alice, alice); + + vm.prank(alice); + vault.redeem(depositAmount, alice, alice); + } + + function testFail__unpause_nonOwner() public { + vault.pause(); + + vm.prank(alice); + vault.unpause(); + } + + /*////////////////////////////////////////////////////////////// + PERMIT + //////////////////////////////////////////////////////////////*/ + + function test_permit() public { + uint256 privateKey = 0xBEEF; + address owner = vm.addr(privateKey); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign( + privateKey, + keccak256( + abi.encodePacked( + "\x19\x01", + vault.DOMAIN_SEPARATOR(), + keccak256(abi.encode(PERMIT_TYPEHASH, owner, address(0xCAFE), 1e18, 0, block.timestamp)) + ) + ) + ); + + vault.permit(owner, address(0xCAFE), 1e18, block.timestamp, v, r, s); + + assertEq(vault.allowance(owner, address(0xCAFE)), 1e18); + assertEq(vault.nonces(owner), 1); + } +} diff --git a/packages/foundry/test/vault/integration/aaveV2/AaveV2Adapter.t.sol b/packages/foundry/test/vault/integration/aaveV2/AaveV2Adapter.t.sol index a95583b5..de282762 100644 --- a/packages/foundry/test/vault/integration/aaveV2/AaveV2Adapter.t.sol +++ b/packages/foundry/test/vault/integration/aaveV2/AaveV2Adapter.t.sol @@ -97,4 +97,7 @@ contract AaveV2AdapterTest is AbstractAdapterTest { assertEq(asset.allowance(address(adapter), address(lendingPool)), type(uint256).max, "allowance"); } + + // TODO add claim test + // TODO add rewardTokens test } diff --git a/packages/foundry/test/vault/integration/aaveV3/AaveV3Adapter.t.sol b/packages/foundry/test/vault/integration/aaveV3/AaveV3Adapter.t.sol index 372b85b3..c8eb6c9f 100644 --- a/packages/foundry/test/vault/integration/aaveV3/AaveV3Adapter.t.sol +++ b/packages/foundry/test/vault/integration/aaveV3/AaveV3Adapter.t.sol @@ -109,4 +109,7 @@ contract AaveV3AdapterTest is AbstractAdapterTest { uint128 supplyRate = data.currentLiquidityRate; return uint256(supplyRate / 1e9); } + + // TODO add claim test + // TODO add rewardTokens test } diff --git a/packages/foundry/test/vault/integration/beefy/BeefyAdapter.t.sol b/packages/foundry/test/vault/integration/beefy/BeefyAdapter.t.sol index d3bf2389..24e30fe9 100644 --- a/packages/foundry/test/vault/integration/beefy/BeefyAdapter.t.sol +++ b/packages/foundry/test/vault/integration/beefy/BeefyAdapter.t.sol @@ -48,7 +48,14 @@ contract BeefyAdapterTest is AbstractAdapterTest { setPermission(_beefyBooster, true, false); } - setUpBaseTest(IERC20(IBeefyVault(beefyVault).want()), address(new BeefyAdapter()), address(permissionRegistry), 10, "Beefy ", true); + setUpBaseTest( + IERC20(IBeefyVault(beefyVault).want()), + address(new BeefyAdapter()), + address(permissionRegistry), + 10, + "Beefy ", + true + ); vm.label(_beefyVault, "beefyVault"); vm.label(_beefyBooster, "beefyBooster"); @@ -225,4 +232,7 @@ contract BeefyAdapterTest is AbstractAdapterTest { adapter.deposit(defaultAmount, bob); adapter.mint(defaultAmount, bob); } + + // TODO add claim test + // TODO add rewardTokens test } diff --git a/packages/foundry/test/vault/integration/compoundV2/CompoundV2Adapter.t.sol b/packages/foundry/test/vault/integration/compoundV2/CompoundV2Adapter.t.sol index 4b68b90c..54abc0d1 100644 --- a/packages/foundry/test/vault/integration/compoundV2/CompoundV2Adapter.t.sol +++ b/packages/foundry/test/vault/integration/compoundV2/CompoundV2Adapter.t.sol @@ -159,4 +159,7 @@ contract CompoundV2AdapterTest is AbstractAdapterTest { adapter.deposit(1e18, bob); adapter.mint(1e18, bob); } + + // TODO add claim test + // TODO add rewardTokens test }