From cc7f316d1e4c10cb03290272e24c2feb0ead8c30 Mon Sep 17 00:00:00 2001 From: RedVeil Date: Sat, 25 Feb 2023 16:51:47 +0100 Subject: [PATCH 1/7] multistrategy vault - wip --- .../foundry/src/vault/MultiStrategyVault.sol | 701 ++++++++++++++++++ 1 file changed, 701 insertions(+) create mode 100644 packages/foundry/src/vault/MultiStrategyVault.sol diff --git a/packages/foundry/src/vault/MultiStrategyVault.sol b/packages/foundry/src/vault/MultiStrategyVault.sol new file mode 100644 index 00000000..f9137729 --- /dev/null +++ b/packages/foundry/src/vault/MultiStrategyVault.sol @@ -0,0 +1,701 @@ +// 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"; + +/** + * @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 adapter_ Adapter which will be used to interact with the wrapped protocol. + * @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_, + IERC4626 adapter_, + 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(); + if (address(asset_) != adapter_.asset()) revert InvalidAdapter(); + + adapter = adapter_; + + asset_.approve(address(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); + } + + /** + * @notice Deposit exactly `assets` amount of tokens, issuing vault shares to `receiver`. + * @param assets Quantity of tokens to deposit. + * @param receiver Receiver of issued vault shares. + * @return shares Quantity of vault shares issued to `receiver`. + */ + function deposit(uint256 assets, address receiver) + public + override + nonReentrant + whenNotPaused + 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 (shares == 0) revert ZeroAmount(); + + if (feeShares > 0) _mint(feeRecipient, feeShares); + + _mint(receiver, shares); + + IERC20(asset()).safeTransferFrom(msg.sender, address(this), assets); + + uint8 len = uint8(adapters.length); + for (uint8 i = 0; i < len; i++) { + adapters[i].deposit(assets.mulDiv(allocation[i], 1e18, Math.Rounding.Down), address(this)); + } + + emit Deposit(msg.sender, receiver, assets, shares); + } + + function mint(uint256 shares) external returns (uint256) { + return mint(shares, msg.sender); + } + + /** + * @notice Mint exactly `shares` vault shares to `receiver`, taking the necessary amount of `asset` from the caller. + * @param shares Quantity of shares to mint. + * @param receiver Receiver of issued vault shares. + * @return assets Quantity of assets deposited by caller. + */ + function mint(uint256 shares, address receiver) public override nonReentrant whenNotPaused returns (uint256 assets) { + if (receiver == address(0)) revert InvalidReceiver(); + if (shares == 0) revert ZeroAmount(); + + uint256 depositFee = uint256(fees.deposit); + + uint256 feeShares = shares.mulDiv(depositFee, 1e18 - depositFee, Math.Rounding.Down); + + assets = _convertToAssets(shares + feeShares, Math.Rounding.Up); + + if (assets > maxMint(receiver)) revert MaxError(assets); + + if (feeShares > 0) _mint(feeRecipient, feeShares); + + _mint(receiver, shares); + + IERC20(asset()).safeTransferFrom(msg.sender, address(this), assets); + + adapter.deposit(assets, address(this)); + + emit Deposit(msg.sender, 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 nonReentrant returns (uint256 shares) { + if (receiver == address(0)) revert InvalidReceiver(); + if (assets > maxWithdraw(owner)) revert MaxError(assets); + + shares = _convertToShares(assets, Math.Rounding.Up); + if (shares == 0) revert ZeroAmount(); + + uint256 withdrawalFee = uint256(fees.withdrawal); + + uint256 feeShares = shares.mulDiv(withdrawalFee, 1e18 - withdrawalFee, Math.Rounding.Down); + + shares += feeShares; + + if (msg.sender != owner) _approve(owner, msg.sender, allowance(owner, msg.sender) - shares); + + _burn(owner, shares); + + if (feeShares > 0) _mint(feeRecipient, feeShares); + + adapter.withdraw(assets, receiver, address(this)); + + emit Withdraw(msg.sender, receiver, owner, assets, 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 nonReentrant returns (uint256 assets) { + if (receiver == address(0)) revert InvalidReceiver(); + if (shares == 0) revert ZeroAmount(); + if (shares > maxRedeem(owner)) revert MaxError(shares); + + if (msg.sender != owner) _approve(owner, msg.sender, allowance(owner, msg.sender) - shares); + + uint256 feeShares = shares.mulDiv(uint256(fees.withdrawal), 1e18, Math.Rounding.Down); + + assets = _convertToAssets(shares - feeShares, Math.Rounding.Up); + + _burn(owner, shares); + + if (feeShares > 0) _mint(feeRecipient, feeShares); + + adapter.withdraw(assets, receiver, address(this)); + + emit Withdraw(msg.sender, receiver, owner, assets, shares); + } + + /*////////////////////////////////////////////////////////////// + ACCOUNTING LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @return Total amount of underlying `asset` token managed by vault. Delegates to adapter. + function totalAssets() public view override returns (uint256 assets) { + uint8 len = uint8(adapters.length); + for (uint8 i = 0; i < len; i++) { + assets += adapters[i].convertToAssets(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); + + uint8 len = uint8(adapters.length); + for (uint8 i = 0; i < len; i++) { + shares += adapters[i].previewDeposit(assets.mulDiv(allocation[i], 1e18, 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); + + uint8 len = uint8(adapters.length); + for (uint8 i = 0; i < len; i++) { + assets += adapters[i].previewMint(shares.mulDiv(allocation[i], 1e18, Math.Rounding.Down)); + } + } + + /** + * @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) { + uint256 withdrawalFee = uint256(fees.withdrawal); + assets += assets.mulDiv(withdrawalFee, 1e18 - withdrawalFee, Math.Rounding.Up); + + uint8 len = uint8(adapters.length); + for (uint8 i = 0; i < len; i++) { + shares += adapters[i].previewWithdraw(assets.mulDiv(allocation[i], 1e18, Math.Rounding.Down)); + } + } + + /** + * @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) { + uint8 len = uint8(adapters.length); + for (uint8 i = 0; i < len; i++) { + assets += adapters[i].previewRedeem(shares.mulDiv(allocation[i], 1e18, Math.Rounding.Down)); + } + assets -= assets.mulDiv(uint256(fees.withdrawal), 1e18, Math.Rounding.Down); + } + + 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 adapter. + function maxDeposit(address) public view override returns (uint256) { + uint256 assets = totalAssets(); + uint256 depositLimit_ = depositLimit; + if (paused() || assets >= depositLimit_) return 0; + return Math.min(depositLimit_ - assets, adapter.maxDeposit(address(this))); + } + + /// @return Maximum amount of vault shares that may be minted to given address. Delegates to adapter. + function maxMint(address) public view override returns (uint256) { + uint256 assets = totalAssets(); + uint256 depositLimit_ = depositLimit; + if (paused() || assets >= depositLimit_) return 0; + return Math.min(depositLimit_ - assets, adapter.maxMint(address(this))); + } + + /// @return Maximum amount of underlying `asset` token that can be withdrawn by `caller` address. Delegates to adapter. + function maxWithdraw(address) public view override returns (uint256) { + return adapter.maxWithdraw(address(this)); + } + + /// @return Maximum amount of shares that may be redeemed by `caller` address. Delegates to adapter. + function maxRedeem(address) public view override returns (uint256) { + return adapter.maxRedeem(address(this)); + } + + /*////////////////////////////////////////////////////////////// + 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 + //////////////////////////////////////////////////////////////*/ + + IERC4626 public adapter; + IERC4626 public proposedAdapter; + uint256 public proposedAdapterTime; + + event NewAdapterProposed(IERC4626 newAdapter, uint256 timestamp); + event ChangedAdapter(IERC4626 oldAdapter, IERC4626 newAdapter); + + error VaultAssetMismatchNewAdapterAsset(); + + /** + * @notice Propose a new adapter for this vault. Caller must be Owner. + * @param newAdapter A new ERC4626 that should be used as a yield adapter for this asset. + */ + function proposeAdapter(IERC4626 newAdapter) external onlyOwner { + if (newAdapter.asset() != asset()) revert VaultAssetMismatchNewAdapterAsset(); + + proposedAdapter = newAdapter; + proposedAdapterTime = block.timestamp; + + emit NewAdapterProposed(newAdapter, block.timestamp); + } + + /** + * @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 changeAdapter() external takeFees { + if (proposedAdapterTime == 0 || block.timestamp < proposedAdapterTime + quitPeriod) + revert NotPassedQuitPeriod(quitPeriod); + + adapter.redeem(adapter.balanceOf(address(this)), address(this), address(this)); + + IERC20(asset()).approve(address(adapter), 0); + + emit ChangedAdapter(adapter, proposedAdapter); + + adapter = proposedAdapter; + + IERC20(asset()).approve(address(adapter), type(uint256).max); + + adapter.deposit(IERC20(asset()).balanceOf(address(this)), address(this)); + + delete proposedAdapterTime; + delete proposedAdapter; + } + + /*////////////////////////////////////////////////////////////// + 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) + ) + ); + } +} From 2ce5e7bb53d6216fe3d027d72f20df74eeb23d27 Mon Sep 17 00:00:00 2001 From: RedVeil Date: Mon, 27 Feb 2023 15:22:45 +0100 Subject: [PATCH 2/7] wip testing --- .../foundry/src/vault/MultiStrategyVault.sol | 199 ++- packages/foundry/test/Tester.t.sol | 35 + .../test/vault/MultiStrategyVault.t.sol | 1075 +++++++++++++++++ 3 files changed, 1256 insertions(+), 53 deletions(-) create mode 100644 packages/foundry/test/Tester.t.sol create mode 100644 packages/foundry/test/vault/MultiStrategyVault.t.sol diff --git a/packages/foundry/src/vault/MultiStrategyVault.sol b/packages/foundry/src/vault/MultiStrategyVault.sol index f9137729..123a14a3 100644 --- a/packages/foundry/src/vault/MultiStrategyVault.sol +++ b/packages/foundry/src/vault/MultiStrategyVault.sol @@ -11,6 +11,11 @@ import { MathUpgradeable as Math } from "openzeppelin-contracts-upgradeable/util import { OwnedUpgradeable } from "../utils/OwnedUpgradeable.sol"; import { VaultFees, IERC4626, IERC20 } from "../interfaces/vault/IVault.sol"; +struct AdapterConfig { + IERC4626 adapter; + uint256 allocation; +} + /** * @title Vault * @author RedVeil @@ -47,7 +52,8 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P /** * @notice Initialize a new Vault. * @param asset_ Underlying Asset which users will deposit. - * @param adapter_ Adapter which will be used to interact with the wrapped protocol. + * @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. @@ -57,7 +63,8 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P */ function initialize( IERC20 asset_, - IERC4626 adapter_, + AdapterConfig[10] calldata adapters_, + uint8 adapterCount_, VaultFees calldata fees_, address feeRecipient_, uint256 depositLimit_, @@ -67,11 +74,15 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P __Owned_init(owner); if (address(asset_) == address(0)) revert InvalidAsset(); - if (address(asset_) != adapter_.asset()) revert InvalidAdapter(); - - adapter = adapter_; + _verifyAdapterConfig(adapters_, adapterCount_); - asset_.approve(address(adapter_), type(uint256).max); + uint8 adapterCount = adapterCount_; + for (uint8 i; i < adapterCount_; i++) { + adapters[i] = adapters_[i]; + + // TODO use save approve + asset_.approve(address(adapters_[i].adapter), type(uint256).max); + } _decimals = IERC20Metadata(address(asset_)).decimals() + decimalOffset; // Asset decimals + decimal offset to combat inflation attacks @@ -152,9 +163,11 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P IERC20(asset()).safeTransferFrom(msg.sender, address(this), assets); - uint8 len = uint8(adapters.length); - for (uint8 i = 0; i < len; i++) { - adapters[i].deposit(assets.mulDiv(allocation[i], 1e18, Math.Rounding.Down), address(this)); + for (uint8 i; i < adapterCount; i++) { + adapters[i].adapter.deposit( + assets.mulDiv(adapters[i].allocation, 1e18, Math.Rounding.Down), + address(this) + ); } emit Deposit(msg.sender, receiver, assets, shares); @@ -188,7 +201,12 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P IERC20(asset()).safeTransferFrom(msg.sender, address(this), assets); - adapter.deposit(assets, address(this)); + for (uint8 i; i < adapterCount; i++) { + adapters[i].adapter.deposit( + assets.mulDiv(adapters[i].allocation, 1e18, Math.Rounding.Down), + address(this) + ); + } emit Deposit(msg.sender, receiver, assets, shares); } @@ -227,7 +245,13 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P if (feeShares > 0) _mint(feeRecipient, feeShares); - adapter.withdraw(assets, receiver, address(this)); + for (uint8 i; i < adapterCount; i++) { + adapters[i].adapter.withdraw( + assets.mulDiv(adapters[i].allocation, 1e18, Math.Rounding.Down), + receiver, + address(this) + ); + } emit Withdraw(msg.sender, receiver, owner, assets, shares); } @@ -262,7 +286,13 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P if (feeShares > 0) _mint(feeRecipient, feeShares); - adapter.withdraw(assets, receiver, address(this)); + for (uint8 i; i < adapterCount; i++) { + adapters[i].adapter.withdraw( + assets.mulDiv(adapters[i].allocation, 1e18, Math.Rounding.Down), + receiver, + address(this) + ); + } emit Withdraw(msg.sender, receiver, owner, assets, shares); } @@ -271,11 +301,10 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P ACCOUNTING LOGIC //////////////////////////////////////////////////////////////*/ - /// @return Total amount of underlying `asset` token managed by vault. Delegates to adapter. + /// @return assets Total amount of underlying `asset` token managed by vault. Delegates to adapter. function totalAssets() public view override returns (uint256 assets) { - uint8 len = uint8(adapters.length); - for (uint8 i = 0; i < len; i++) { - assets += adapters[i].convertToAssets(adapter.balanceOf(address(this))); + for (uint8 i; i < adapterCount; i++) { + assets += adapters[i].adapter.convertToAssets(adapters[i].adapter.balanceOf(address(this))); } } @@ -288,9 +317,10 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P function previewDeposit(uint256 assets) public view override returns (uint256 shares) { assets -= assets.mulDiv(uint256(fees.deposit), 1e18, Math.Rounding.Down); - uint8 len = uint8(adapters.length); - for (uint8 i = 0; i < len; i++) { - shares += adapters[i].previewDeposit(assets.mulDiv(allocation[i], 1e18, Math.Rounding.Down)); + for (uint8 i; i < adapterCount; i++) { + shares += adapters[i].adapter.previewDeposit( + assets.mulDiv(adapters[i].allocation, 1e18, Math.Rounding.Down) + ); } } @@ -304,9 +334,10 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P uint256 depositFee = uint256(fees.deposit); shares += shares.mulDiv(depositFee, 1e18 - depositFee, Math.Rounding.Up); - uint8 len = uint8(adapters.length); - for (uint8 i = 0; i < len; i++) { - assets += adapters[i].previewMint(shares.mulDiv(allocation[i], 1e18, Math.Rounding.Down)); + for (uint8 i; i < adapterCount; i++) { + assets += adapters[i].adapter.previewMint( + shares.mulDiv(adapters[i].allocation, 1e18, Math.Rounding.Down) + ); } } @@ -320,9 +351,10 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P uint256 withdrawalFee = uint256(fees.withdrawal); assets += assets.mulDiv(withdrawalFee, 1e18 - withdrawalFee, Math.Rounding.Up); - uint8 len = uint8(adapters.length); - for (uint8 i = 0; i < len; i++) { - shares += adapters[i].previewWithdraw(assets.mulDiv(allocation[i], 1e18, Math.Rounding.Down)); + for (uint8 i; i < adapterCount; i++) { + shares += adapters[i].adapter.previewWithdraw( + assets.mulDiv(adapters[i].allocation, 1e18, Math.Rounding.Down) + ); } } @@ -333,9 +365,10 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P * @dev This method accounts for both issuance of fee shares and withdrawal fee. */ function previewRedeem(uint256 shares) public view override returns (uint256 assets) { - uint8 len = uint8(adapters.length); - for (uint8 i = 0; i < len; i++) { - assets += adapters[i].previewRedeem(shares.mulDiv(allocation[i], 1e18, Math.Rounding.Down)); + for (uint8 i; i < adapterCount; i++) { + assets += adapters[i].adapter.previewRedeem( + shares.mulDiv(adapters[i].allocation, 1e18, Math.Rounding.Down) + ); } assets -= assets.mulDiv(uint256(fees.withdrawal), 1e18, Math.Rounding.Down); } @@ -358,30 +391,48 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P DEPOSIT/WITHDRAWAL LIMIT LOGIC //////////////////////////////////////////////////////////////*/ - /// @return Maximum amount of underlying `asset` token that may be deposited for a given address. Delegates to adapter. + /// @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; - return Math.min(depositLimit_ - assets, adapter.maxDeposit(address(this))); + + uint256 maxDeposit_ = depositLimit_; + for (uint8 i; i < adapterCount; i++) { + maxDeposit_ = Math.min(maxDeposit_, adapters[i].adapter.maxDeposit(address(this))); + } + return maxDeposit_; } - /// @return Maximum amount of vault shares that may be minted to given address. Delegates to adapter. + /// @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; - return Math.min(depositLimit_ - assets, adapter.maxMint(address(this))); + + uint256 maxMint_ = depositLimit_; + for (uint8 i; i < adapterCount; i++) { + maxMint_ = Math.min(maxMint_, adapters[i].adapter.maxMint(address(this))); + } + return maxMint_; } - /// @return Maximum amount of underlying `asset` token that can be withdrawn by `caller` address. Delegates to adapter. + /// @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) { - return adapter.maxWithdraw(address(this)); + uint256 maxWithdraw_ = adapters[0].adapter.maxWithdraw(address(this)); + for (uint8 i = 1; i < adapterCount; i++) { + maxWithdraw_ = Math.min(maxWithdraw_, adapters[i].adapter.maxWithdraw(address(this))); + } + return maxWithdraw_; } - /// @return Maximum amount of shares that may be redeemed by `caller` address. Delegates to adapter. + /// @return Maximum amount of shares that may be redeemed by `caller` address. Delegates to adapters. function maxRedeem(address) public view override returns (uint256) { - return adapter.maxRedeem(address(this)); + uint256 maxRedeem_ = adapters[0].adapter.maxRedeem(address(this)); + for (uint8 i = 1; i < adapterCount; i++) { + maxRedeem_ = Math.min(maxRedeem_, adapters[i].adapter.maxRedeem(address(this))); + } + return maxRedeem_; } /*////////////////////////////////////////////////////////////// @@ -524,26 +575,52 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P ADAPTER LOGIC //////////////////////////////////////////////////////////////*/ - IERC4626 public adapter; - IERC4626 public proposedAdapter; + AdapterConfig[10] public adapters; + AdapterConfig[10] public proposedAdapters; + + uint8 public adapterCount; + uint8 public proposedAdapterCount; + uint256 public proposedAdapterTime; - event NewAdapterProposed(IERC4626 newAdapter, uint256 timestamp); - event ChangedAdapter(IERC4626 oldAdapter, IERC4626 newAdapter); + event NewAdaptersProposed(AdapterConfig[10] newAdapter, uint256 timestamp); + event ChangedAdapters(AdapterConfig[10] oldAdapter, AdapterConfig[10] newAdapter); - error VaultAssetMismatchNewAdapterAsset(); + error AssetInvalid(); + error InvalidConfig(); /** * @notice Propose a new adapter for this vault. Caller must be Owner. - * @param newAdapter A new ERC4626 that should be used as a yield adapter for this asset. + * @param newAdapters A new ERC4626 that should be used as a yield adapter for this asset. + * @param newAdapterCount Amount of new adapters. */ - function proposeAdapter(IERC4626 newAdapter) external onlyOwner { - if (newAdapter.asset() != asset()) revert VaultAssetMismatchNewAdapterAsset(); + 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; - proposedAdapter = newAdapter; proposedAdapterTime = block.timestamp; - emit NewAdapterProposed(newAdapter, block.timestamp); + emit NewAdaptersProposed(newAdapters, 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 = newAdapters[i].allocation; + if (allocation == 0) revert InvalidConfig(); + + totalAllocation += allocation; + } + if (totalAllocation != 1e18) revert InvalidConfig(); } /** @@ -556,20 +633,36 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P if (proposedAdapterTime == 0 || block.timestamp < proposedAdapterTime + quitPeriod) revert NotPassedQuitPeriod(quitPeriod); - adapter.redeem(adapter.balanceOf(address(this)), address(this), address(this)); + for (uint8 i; i < adapterCount; i++) { + adapters[i].adapter.redeem( + adapters[i].adapter.balanceOf(address(this)), + address(this), + address(this) + ); - IERC20(asset()).approve(address(adapter), 0); + // TODO use save approve + IERC20(asset()).approve(address(adapters[i].adapter), 0); + } + + emit ChangedAdapters(adapters, proposedAdapters); - emit ChangedAdapter(adapter, proposedAdapter); + adapters = proposedAdapters; + adapterCount = proposedAdapterCount; - adapter = proposedAdapter; + uint256 totalAssets_ = IERC20(asset()).balanceOf(address(this)); - IERC20(asset()).approve(address(adapter), type(uint256).max); + for (uint8 i; i < adapterCount; i++) { + IERC20(asset()).approve(address(adapters[i].adapter), type(uint256).max); - adapter.deposit(IERC20(asset()).balanceOf(address(this)), address(this)); + adapters[i].adapter.deposit( + totalAssets_.mulDiv(adapters[i].allocation, 1e18, Math.Rounding.Down), + address(this) + ); + } + delete proposedAdapters; + delete proposedAdapterCount; delete proposedAdapterTime; - delete proposedAdapter; } /*////////////////////////////////////////////////////////////// diff --git a/packages/foundry/test/Tester.t.sol b/packages/foundry/test/Tester.t.sol new file mode 100644 index 00000000..0ab9c3ef --- /dev/null +++ b/packages/foundry/test/Tester.t.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.15 + +pragma solidity ^0.8.15; +import { Test } from "forge-std/Test.sol"; + +struct Sample { + address addr; + uint256 num; +} + +contract Sampler { + Sample[2] public samples; + + event log(uint256); + + function setSample(Sample[2] memory _samples) public { + for(uint256 i; i < _samples.length; i++) { + samples[i] = _samples[i]; + } + } +} + +contract Tester is Test { + Sampler public sampler = new Sampler(); + + function setUp() public {} + + function test_smth() public { + Sample[2] memory samples; + samples[0] = Sample({addr: address(0x1), num: 1}); + samples[1] = Sample({addr: address(0x2), num: 2}); + sampler.setSample(samples); + } +} diff --git a/packages/foundry/test/vault/MultiStrategyVault.t.sol b/packages/foundry/test/vault/MultiStrategyVault.t.sol new file mode 100644 index 00000000..817221fe --- /dev/null +++ b/packages/foundry/test/vault/MultiStrategyVault.t.sol @@ -0,0 +1,1075 @@ +// 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, uint256 timestamp); + event ChangedAdapters(AdapterConfig[10] oldAdapter, AdapterConfig[10] newAdapter); + 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); + + assertEq(address(newVault.asset()), address(asset)); + assertEq(address(newVault.adapter()), address(adapter)); + 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); + assertEq(asset.allowance(address(newVault), address(adapter)), type(uint256).max); + } + + 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_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_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_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_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 == 0) amount = 1; + + 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); + + vm.prank(alice); + uint256 aliceShareAmount = vault.deposit(aliceassetAmount, alice); + + assertEq(adapter.afterDepositHookCalledCounter(), 1); + + // Expect exchange rate to be 1:1e9 on initial deposit. + assertEq(aliceassetAmount * 1e9, aliceShareAmount); + assertEq(vault.previewWithdraw(aliceassetAmount), aliceShareAmount); + assertEq(vault.previewDeposit(aliceassetAmount), aliceShareAmount); + assertEq(vault.totalSupply(), aliceShareAmount); + assertEq(vault.totalAssets(), aliceassetAmount); + assertEq(vault.balanceOf(alice), aliceShareAmount); + assertEq(vault.convertToAssets(vault.balanceOf(alice)), aliceassetAmount); + assertEq(asset.balanceOf(alice), alicePreDepositBal - aliceassetAmount); + + vm.prank(alice); + vault.withdraw(aliceassetAmount, alice, alice); + + assertEq(adapter.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__deposit_zero() public { + vault.deposit(0, address(this)); + } + + function testFail__withdraw_zero() public { + vault.withdraw(0, address(this), address(this)); + } + + 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(adapter.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(adapter.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_zero() public { + vault.mint(0, address(this)); + } + + function testFail__redeem_zero() public { + vault.redeem(0, address(this), address(this)); + } + + 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); + + vm.prank(alice); + uint256 actualShares = vault.withdraw(withdrawAmount, alice, alice); + 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(adapter), 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(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__proposeAdapter() public { + // MockERC4626 newAdapter = _createAdapter(IERC20(address(asset))); + + // uint256 callTime = block.timestamp; + // vm.expectEmit(false, false, false, true, address(vault)); + // emit NewAdapterProposed(IERC4626(address(newAdapter)), callTime); + + // vault.proposeAdapter(IERC4626(address(newAdapter))); + + // assertEq(vault.proposedAdapterTime(), callTime); + // assertEq(address(vault.proposedAdapter()), address(newAdapter)); + // } + + // function testFail__proposeAdapter_nonOwner() public { + // MockERC4626 newAdapter = _createAdapter(IERC20(address(asset))); + + // vm.prank(alice); + // vault.proposeAdapter(IERC4626(address(newAdapter))); + // } + + // function testFail__proposeAdapter_asset_missmatch() public { + // MockERC20 newAsset = new MockERC20("New Mock Token", "NTKN", 18); + // MockERC4626 newAdapter = _createAdapter(IERC20(address(newAsset))); + + // vm.prank(alice); + // vault.proposeAdapter(IERC4626(address(newAdapter))); + // } + + // // Change Adapter + // function test__changeAdapter() public { + // MockERC4626 newAdapter = _createAdapter(IERC20(address(asset))); + // 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(adapter), depositAmount); + // vault.takeManagementAndPerformanceFees(); + // uint256 oldHWM = vault.highWaterMark(); + + // // Preparation to change the adapter + // vault.proposeAdapter(IERC4626(address(newAdapter))); + + // vm.warp(block.timestamp + 3 days); + + // vm.expectEmit(false, false, false, true, address(vault)); + // emit ChangedAdapter(IERC4626(address(adapter)), IERC4626(address(newAdapter))); + + // vault.changeAdapter(); + + // // Annoyingly Math fails us here and leaves 1 asset in the adapter + // assertEq(asset.allowance(address(vault), address(adapter)), 0); + // assertEq(asset.balanceOf(address(adapter)), 1); + // assertEq(adapter.balanceOf(address(vault)), 0); + + // assertEq(asset.balanceOf(address(newAdapter)), (depositAmount * 2) - 1); + // assertEq(newAdapter.balanceOf(address(vault)), (depositAmount * 2e9) - 1e9); + // assertEq(asset.allowance(address(vault), address(newAdapter)), type(uint256).max); + + // assertEq(vault.highWaterMark(), oldHWM); + + // assertEq(vault.proposedAdapterTime(), 0); + // assertEq(address(vault.proposedAdapter()), address(0)); + // } + + // function testFail__changeAdapter_NonOwner() public { + // vm.prank(alice); + // vault.changeAdapter(); + // } + + // function testFail__changeAdapter_respect_rageQuit() public { + // MockERC4626 newAdapter = _createAdapter(IERC20(address(asset))); + + // vault.proposeAdapter(IERC4626(address(newAdapter))); + + // // Didnt respect 3 days before propsal and change + // vault.changeAdapter(); + // } + + // function testFail__changeAdapter_after_init() public { + // vault.changeAdapter(); + // } + + // function testFail__changeAdapter_instantly_again() public { + // MockERC4626 newAdapter = _createAdapter(IERC20(address(asset))); + // 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(adapter), depositAmount); + // vault.takeManagementAndPerformanceFees(); + // uint256 oldHWM = vault.highWaterMark(); + + // // Preparation to change the adapter + // vault.proposeAdapter(IERC4626(address(newAdapter))); + + // vm.warp(block.timestamp + 3 days); + + // vm.expectEmit(false, false, false, true, address(vault)); + // emit ChangedAdapter(IERC4626(address(adapter)), IERC4626(address(newAdapter))); + + // vault.changeAdapter(); + // vault.changeAdapter(); + // } + + /*////////////////////////////////////////////////////////////// + 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.proposeAdapter(IERC4626(address(newAdapter))); + + 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)); + 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("Pausable: paused"); + vault.deposit(depositAmount, alice); + + vm.prank(alice); + vm.expectRevert("Pausable: paused"); + 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); + } +} From ac4bee01703897d9729548ccb8d6d247df018c48 Mon Sep 17 00:00:00 2001 From: RedVeil Date: Mon, 27 Feb 2023 16:14:36 +0100 Subject: [PATCH 3/7] wip - multiStrategyVault test --- .../foundry/src/vault/MultiStrategyVault.sol | 22 ++++----- .../test/vault/MultiStrategyVault.t.sol | 48 +++++++++---------- 2 files changed, 33 insertions(+), 37 deletions(-) diff --git a/packages/foundry/src/vault/MultiStrategyVault.sol b/packages/foundry/src/vault/MultiStrategyVault.sol index 123a14a3..433041f2 100644 --- a/packages/foundry/src/vault/MultiStrategyVault.sol +++ b/packages/foundry/src/vault/MultiStrategyVault.sol @@ -13,7 +13,7 @@ import { VaultFees, IERC4626, IERC20 } from "../interfaces/vault/IVault.sol"; struct AdapterConfig { IERC4626 adapter; - uint256 allocation; + uint64 allocation; } /** @@ -165,7 +165,7 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P for (uint8 i; i < adapterCount; i++) { adapters[i].adapter.deposit( - assets.mulDiv(adapters[i].allocation, 1e18, Math.Rounding.Down), + assets.mulDiv(uint256(adapters[i].allocation), 1e18, Math.Rounding.Down), address(this) ); } @@ -203,7 +203,7 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P for (uint8 i; i < adapterCount; i++) { adapters[i].adapter.deposit( - assets.mulDiv(adapters[i].allocation, 1e18, Math.Rounding.Down), + assets.mulDiv(uint256(adapters[i].allocation), 1e18, Math.Rounding.Down), address(this) ); } @@ -247,7 +247,7 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P for (uint8 i; i < adapterCount; i++) { adapters[i].adapter.withdraw( - assets.mulDiv(adapters[i].allocation, 1e18, Math.Rounding.Down), + assets.mulDiv(uint256(adapters[i].allocation), 1e18, Math.Rounding.Down), receiver, address(this) ); @@ -288,7 +288,7 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P for (uint8 i; i < adapterCount; i++) { adapters[i].adapter.withdraw( - assets.mulDiv(adapters[i].allocation, 1e18, Math.Rounding.Down), + assets.mulDiv(uint256(adapters[i].allocation), 1e18, Math.Rounding.Down), receiver, address(this) ); @@ -319,7 +319,7 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P for (uint8 i; i < adapterCount; i++) { shares += adapters[i].adapter.previewDeposit( - assets.mulDiv(adapters[i].allocation, 1e18, Math.Rounding.Down) + assets.mulDiv(uint256(adapters[i].allocation), 1e18, Math.Rounding.Down) ); } } @@ -336,7 +336,7 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P for (uint8 i; i < adapterCount; i++) { assets += adapters[i].adapter.previewMint( - shares.mulDiv(adapters[i].allocation, 1e18, Math.Rounding.Down) + shares.mulDiv(uint256(adapters[i].allocation), 1e18, Math.Rounding.Down) ); } } @@ -353,7 +353,7 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P for (uint8 i; i < adapterCount; i++) { shares += adapters[i].adapter.previewWithdraw( - assets.mulDiv(adapters[i].allocation, 1e18, Math.Rounding.Down) + assets.mulDiv(uint256(adapters[i].allocation), 1e18, Math.Rounding.Down) ); } } @@ -367,7 +367,7 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P function previewRedeem(uint256 shares) public view override returns (uint256 assets) { for (uint8 i; i < adapterCount; i++) { assets += adapters[i].adapter.previewRedeem( - shares.mulDiv(adapters[i].allocation, 1e18, Math.Rounding.Down) + shares.mulDiv(uint256(adapters[i].allocation), 1e18, Math.Rounding.Down) ); } assets -= assets.mulDiv(uint256(fees.withdrawal), 1e18, Math.Rounding.Down); @@ -615,7 +615,7 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P for (uint8 i; i < adapterCount_; i++) { if (newAdapters[i].adapter.asset() != asset()) revert AssetInvalid(); - uint256 allocation = newAdapters[i].allocation; + uint256 allocation = uint256(newAdapters[i].allocation); if (allocation == 0) revert InvalidConfig(); totalAllocation += allocation; @@ -655,7 +655,7 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P IERC20(asset()).approve(address(adapters[i].adapter), type(uint256).max); adapters[i].adapter.deposit( - totalAssets_.mulDiv(adapters[i].allocation, 1e18, Math.Rounding.Down), + totalAssets_.mulDiv(uint256(adapters[i].allocation), 1e18, Math.Rounding.Down), address(this) ); } diff --git a/packages/foundry/test/vault/MultiStrategyVault.t.sol b/packages/foundry/test/vault/MultiStrategyVault.t.sol index 817221fe..b7f26f5d 100644 --- a/packages/foundry/test/vault/MultiStrategyVault.t.sol +++ b/packages/foundry/test/vault/MultiStrategyVault.t.sol @@ -130,8 +130,16 @@ contract MultiStrategyVaultTest is Test { 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(adapter1)); + 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(address(newVault.asset()), address(asset)); - assertEq(address(newVault.adapter()), address(adapter)); assertEq(newVault.owner(), bob); (uint256 deposit, uint256 withdrawal, uint256 management, uint256 performance) = newVault.fees(); @@ -144,7 +152,6 @@ contract MultiStrategyVaultTest is Test { assertEq(newVault.feesUpdatedAt(), callTime); assertEq(newVault.quitPeriod(), 3 days); - assertEq(asset.allowance(address(newVault), address(adapter)), type(uint256).max); } function testFail__initialize_asset_is_zero() public { @@ -232,21 +239,6 @@ contract MultiStrategyVaultTest is Test { ); } - 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_adapterCount_too_high() public { address vaultAddress = address(new MultiStrategyVault()); @@ -262,14 +254,14 @@ contract MultiStrategyVaultTest is Test { ); } - function testFail__initialize_adapterCount_too_high() public { + function testFail__initialize_adapterCount_too_low() public { address vaultAddress = address(new MultiStrategyVault()); vault = MultiStrategyVault(vaultAddress); vault.initialize( IERC20(address(asset)), adapters, - 11, + 0, VaultFees({ deposit: 0, withdrawal: 0, management: 0, performance: 0 }), feeRecipient, type(uint256).max, @@ -312,7 +304,8 @@ contract MultiStrategyVaultTest is Test { vm.prank(alice); uint256 aliceShareAmount = vault.deposit(aliceassetAmount, alice); - assertEq(adapter.afterDepositHookCalledCounter(), 1); + assertEq(adapter1.afterDepositHookCalledCounter(), 1); + assertEq(adapter2.afterDepositHookCalledCounter(), 1); // Expect exchange rate to be 1:1e9 on initial deposit. assertEq(aliceassetAmount * 1e9, aliceShareAmount); @@ -327,7 +320,8 @@ contract MultiStrategyVaultTest is Test { vm.prank(alice); vault.withdraw(aliceassetAmount, alice, alice); - assertEq(adapter.beforeWithdrawHookCalledCounter(), 1); + assertEq(adapter1.beforeWithdrawHookCalledCounter(), 1); + assertEq(adapter2.beforeWithdrawHookCalledCounter(), 1); assertEq(vault.totalAssets(), 0); assertEq(vault.balanceOf(alice), 0); @@ -387,7 +381,8 @@ contract MultiStrategyVaultTest is Test { vm.prank(alice); uint256 aliceAssetAmount = vault.mint(aliceShareAmount, alice); - assertEq(adapter.afterDepositHookCalledCounter(), 1); + 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 @@ -404,7 +399,8 @@ contract MultiStrategyVaultTest is Test { vm.prank(alice); vault.redeem(aliceShareAmount, alice, alice); - assertEq(adapter.beforeWithdrawHookCalledCounter(), 1); + assertEq(adapter1.beforeWithdrawHookCalledCounter(), 1); + assertEq(adapter2.beforeWithdrawHookCalledCounter(), 1); assertEq(vault.totalAssets(), 0); assertEq(vault.balanceOf(alice), 0); @@ -615,7 +611,7 @@ contract MultiStrategyVaultTest is Test { vm.stopPrank(); // Increase asset assets to trigger performanceFee - asset.mint(address(adapter), amount); + asset.mint(address(adapter1), amount); uint256 expectedFeeInAsset = vault.accruedPerformanceFee(); @@ -659,7 +655,7 @@ contract MultiStrategyVaultTest is Test { vault.deposit(depositAmount, alice); vm.stopPrank(); - asset.mint(address(adapter), 1e6); + asset.mint(address(adapters[0].adapter), 1e6); // Take 10% of 1e6 assertEq(vault.accruedPerformanceFee(), 1e5 - 1); @@ -926,7 +922,7 @@ contract MultiStrategyVaultTest is Test { // Pass the inital quit period vm.warp(block.timestamp + 3 days); - vault.proposeAdapter(IERC4626(address(newAdapter))); + vault.proposeAdapters(adapters, 2); vault.setQuitPeriod(1 days); } From c03016c2ba629c7956e41ae0c65c26039201f78e Mon Sep 17 00:00:00 2001 From: RedVeil Date: Tue, 28 Feb 2023 16:26:55 +0100 Subject: [PATCH 4/7] solved allocation issue --- .../foundry/src/vault/MultiStrategyVault.sol | 181 +++++++++++------- packages/foundry/test/Tester.t.sol | 144 ++++++++++++-- .../test/utils/mocks/MultiMockERC4626.sol | 140 ++++++++++++++ .../test/vault/MultiStrategyVault.t.sol | 89 +++++---- 4 files changed, 431 insertions(+), 123 deletions(-) create mode 100644 packages/foundry/test/utils/mocks/MultiMockERC4626.sol diff --git a/packages/foundry/src/vault/MultiStrategyVault.sol b/packages/foundry/src/vault/MultiStrategyVault.sol index 433041f2..e13f6c47 100644 --- a/packages/foundry/src/vault/MultiStrategyVault.sol +++ b/packages/foundry/src/vault/MultiStrategyVault.sol @@ -76,7 +76,7 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P if (address(asset_) == address(0)) revert InvalidAsset(); _verifyAdapterConfig(adapters_, adapterCount_); - uint8 adapterCount = adapterCount_; + adapterCount = adapterCount_; for (uint8 i; i < adapterCount_; i++) { adapters[i] = adapters_[i]; @@ -133,6 +133,17 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P return deposit(assets, msg.sender); } + /** + * @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 Deposit exactly `assets` amount of tokens, issuing vault shares to `receiver`. * @param assets Quantity of tokens to deposit. @@ -157,10 +168,6 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P shares = _convertToShares(assets, Math.Rounding.Down) - feeShares; if (shares == 0) revert ZeroAmount(); - if (feeShares > 0) _mint(feeRecipient, feeShares); - - _mint(receiver, shares); - IERC20(asset()).safeTransferFrom(msg.sender, address(this), assets); for (uint8 i; i < adapterCount; i++) { @@ -170,9 +177,17 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P ); } + if (feeShares > 0) _mint(feeRecipient, feeShares); + + _mint(receiver, shares); + emit Deposit(msg.sender, receiver, assets, shares); } + event log_uint(uint256); + event log_address(address); + event log(string); + function mint(uint256 shares) external returns (uint256) { return mint(shares, msg.sender); } @@ -215,6 +230,19 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P return withdraw(assets, msg.sender, msg.sender); } + /** + * @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) { + uint256 withdrawalFee = uint256(fees.withdrawal); + assets += assets.mulDiv(withdrawalFee, 1e18 - withdrawalFee, Math.Rounding.Up); + + shares = _convertToShares(assets, Math.Rounding.Up); + } + /** * @notice Burn shares from `owner` in exchange for `assets` amount of underlying token. * @param assets Quantity of underlying `asset` token to withdraw. @@ -232,26 +260,35 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P shares = _convertToShares(assets, Math.Rounding.Up); if (shares == 0) revert ZeroAmount(); + emit log("got here1"); uint256 withdrawalFee = uint256(fees.withdrawal); uint256 feeShares = shares.mulDiv(withdrawalFee, 1e18 - withdrawalFee, Math.Rounding.Down); shares += feeShares; - + emit log("got here2"); + emit log_address(msg.sender); + emit log_address(owner); if (msg.sender != owner) _approve(owner, msg.sender, allowance(owner, msg.sender) - shares); + emit log("got here3"); _burn(owner, shares); + emit log("got here4"); if (feeShares > 0) _mint(feeRecipient, feeShares); for (uint8 i; i < adapterCount; i++) { adapters[i].adapter.withdraw( assets.mulDiv(uint256(adapters[i].allocation), 1e18, Math.Rounding.Down), - receiver, + address(this), address(this) ); } + emit log("got here5"); + + IERC20(asset()).safeTransfer(receiver, assets); + emit log("got here6"); emit Withdraw(msg.sender, receiver, owner, assets, shares); } @@ -274,7 +311,6 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P ) public override nonReentrant returns (uint256 assets) { if (receiver == address(0)) revert InvalidReceiver(); if (shares == 0) revert ZeroAmount(); - if (shares > maxRedeem(owner)) revert MaxError(shares); if (msg.sender != owner) _approve(owner, msg.sender, allowance(owner, msg.sender) - shares); @@ -282,6 +318,8 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P assets = _convertToAssets(shares - feeShares, Math.Rounding.Up); + if (assets > maxRedeem(owner)) revert MaxError(assets); + _burn(owner, shares); if (feeShares > 0) _mint(feeRecipient, feeShares); @@ -303,24 +341,10 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P /// @return assets Total amount of underlying `asset` token managed by vault. Delegates to adapter. function totalAssets() public view override returns (uint256 assets) { - 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); + assets = IERC20(asset()).balanceOf(address(this)); for (uint8 i; i < adapterCount; i++) { - shares += adapters[i].adapter.previewDeposit( - assets.mulDiv(uint256(adapters[i].allocation), 1e18, Math.Rounding.Down) - ); + assets += adapters[i].adapter.convertToAssets(adapters[i].adapter.balanceOf(address(this))); } } @@ -341,23 +365,6 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P } } - /** - * @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) { - uint256 withdrawalFee = uint256(fees.withdrawal); - assets += assets.mulDiv(withdrawalFee, 1e18 - withdrawalFee, Math.Rounding.Up); - - for (uint8 i; i < adapterCount; i++) { - shares += adapters[i].adapter.previewWithdraw( - assets.mulDiv(uint256(adapters[i].allocation), 1e18, Math.Rounding.Down) - ); - } - } - /** * @notice Simulate the effects of a redemption at the current block, given current on-chain conditions. * @param shares Exact amount of `shares` to redeem @@ -392,17 +399,27 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P //////////////////////////////////////////////////////////////*/ /// @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; + // 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++) { - maxDeposit_ = Math.min(maxDeposit_, adapters[i].adapter.maxDeposit(address(this))); - } - return maxDeposit_; - } + // 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) { @@ -412,26 +429,58 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P uint256 maxMint_ = depositLimit_; for (uint8 i; i < adapterCount; i++) { - maxMint_ = Math.min(maxMint_, adapters[i].adapter.maxMint(address(this))); + 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_ = adapters[0].adapter.maxWithdraw(address(this)); - for (uint8 i = 1; i < adapterCount; i++) { - maxWithdraw_ = Math.min(maxWithdraw_, adapters[i].adapter.maxWithdraw(address(this))); - } - return maxWithdraw_; - } + // function maxWithdraw(address) public view override returns (uint256) { + // uint256 maxWithdraw_ = type(uint256).max; + + // 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 = adapterMax.mulDiv(1e18, uint256(adapters[i].allocation), Math.Rounding.Down); + // } + + // maxWithdraw_ = Math.min(maxWithdraw_, adapterMax); + // } + + // 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_ = adapters[0].adapter.maxRedeem(address(this)); - for (uint8 i = 1; i < adapterCount; i++) { - maxRedeem_ = Math.min(maxRedeem_, adapters[i].adapter.maxRedeem(address(this))); + uint256 maxRedeem_ = type(uint256).max; + + 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); } + return maxRedeem_; } @@ -629,16 +678,12 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P * @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 changeAdapter() external takeFees { + 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) - ); + adapters[i].adapter.redeem(adapters[i].adapter.balanceOf(address(this)), address(this), address(this)); // TODO use save approve IERC20(asset()).approve(address(adapters[i].adapter), 0); diff --git a/packages/foundry/test/Tester.t.sol b/packages/foundry/test/Tester.t.sol index 0ab9c3ef..4546ef0e 100644 --- a/packages/foundry/test/Tester.t.sol +++ b/packages/foundry/test/Tester.t.sol @@ -3,33 +3,137 @@ 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"; -struct Sample { - address addr; - uint256 num; -} +contract Tester is Test { + using Math for uint256; -contract Sampler { - Sample[2] public samples; + MultiMockERC4626 multiMockERC4626; + MockERC20 asset; + MockERC4626 adapter1; + MockERC4626 adapter2; + MockERC4626 adapter3; - event log(uint256); + IERC4626[10] adapters; + uint256[10] allocations; - function setSample(Sample[2] memory _samples) public { - for(uint256 i; i < _samples.length; i++) { - samples[i] = _samples[i]; - } + 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); } -} -contract Tester is Test { - Sampler public sampler = new Sampler(); + 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); - function setUp() public {} + uint256 prevW = multiMockERC4626.previewWithdraw(totalAssets); + uint256 returned = multiMockERC4626.withdraw(totalAssets, address(this), address(this)); + totalSupply = multiMockERC4626.totalSupply(); + totalAssets = multiMockERC4626.totalAssets(); - function test_smth() public { - Sample[2] memory samples; - samples[0] = Sample({addr: address(0x1), num: 1}); - samples[1] = Sample({addr: address(0x2), num: 2}); - sampler.setSample(samples); + 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 index b7f26f5d..251056ea 100644 --- a/packages/foundry/test/vault/MultiStrategyVault.t.sol +++ b/packages/foundry/test/vault/MultiStrategyVault.t.sol @@ -134,11 +134,13 @@ contract MultiStrategyVaultTest is Test { (IERC4626 adapter1_, uint64 allocation1_) = newVault.adapters(1); assertEq(address(adapter0_), address(adapter1)); assertEq(allocation0_, 0.5e18); - assertEq(address(adapter1_), address(adapter1)); + 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); @@ -289,44 +291,61 @@ contract MultiStrategyVaultTest is Test { //////////////////////////////////////////////////////////////*/ function test__deposit_withdraw(uint128 amount) public { - if (amount == 0) amount = 1; + if (amount != 5749) amount = 5749; - uint256 aliceassetAmount = amount; + uint256 aliceAssetAmount = amount; - asset.mint(alice, aliceassetAmount); + asset.mint(alice, aliceAssetAmount); vm.prank(alice); - asset.approve(address(vault), aliceassetAmount); - assertEq(asset.allowance(alice, address(vault)), aliceassetAmount); + 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); + uint256 aliceShareAmount = vault.deposit(aliceAssetAmount, alice); - assertEq(adapter1.afterDepositHookCalledCounter(), 1); - assertEq(adapter2.afterDepositHookCalledCounter(), 1); + 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(aliceassetAmount * 1e9, aliceShareAmount); - assertEq(vault.previewWithdraw(aliceassetAmount), aliceShareAmount); - assertEq(vault.previewDeposit(aliceassetAmount), aliceShareAmount); - assertEq(vault.totalSupply(), aliceShareAmount); - assertEq(vault.totalAssets(), aliceassetAmount); - assertEq(vault.balanceOf(alice), aliceShareAmount); - assertEq(vault.convertToAssets(vault.balanceOf(alice)), aliceassetAmount); - assertEq(asset.balanceOf(alice), alicePreDepositBal - aliceassetAmount); + 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); + vault.withdraw(aliceAssetAmount, alice, alice); - assertEq(adapter1.beforeWithdrawHookCalledCounter(), 1); - assertEq(adapter2.beforeWithdrawHookCalledCounter(), 1); + // assertEq(adapter1.beforeWithdrawHookCalledCounter(), 1, "w adapter1"); + // assertEq(adapter2.beforeWithdrawHookCalledCounter(), 1, "w adapter2"); - assertEq(vault.totalAssets(), 0); - assertEq(vault.balanceOf(alice), 0); - assertEq(vault.convertToAssets(vault.balanceOf(alice)), 0); - assertEq(asset.balanceOf(alice), alicePreDepositBal); + // 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"); + + // vm.prank(alice); + // vault.redeem(aliceShareAmount, alice, alice); + + // 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_zero() public { @@ -795,7 +814,7 @@ contract MultiStrategyVaultTest is Test { // } // // Change Adapter - // function test__changeAdapter() public { + // function test__changeAdapters() public { // MockERC4626 newAdapter = _createAdapter(IERC20(address(asset))); // uint256 depositAmount = 1 ether; @@ -819,7 +838,7 @@ contract MultiStrategyVaultTest is Test { // vm.expectEmit(false, false, false, true, address(vault)); // emit ChangedAdapter(IERC4626(address(adapter)), IERC4626(address(newAdapter))); - // vault.changeAdapter(); + // vault.changeAdapters(); // // Annoyingly Math fails us here and leaves 1 asset in the adapter // assertEq(asset.allowance(address(vault), address(adapter)), 0); @@ -836,25 +855,25 @@ contract MultiStrategyVaultTest is Test { // assertEq(address(vault.proposedAdapter()), address(0)); // } - // function testFail__changeAdapter_NonOwner() public { + // function testFail__changeAdapters_NonOwner() public { // vm.prank(alice); - // vault.changeAdapter(); + // vault.changeAdapters(); // } - // function testFail__changeAdapter_respect_rageQuit() public { + // function testFail__changeAdapters_respect_rageQuit() public { // MockERC4626 newAdapter = _createAdapter(IERC20(address(asset))); // vault.proposeAdapter(IERC4626(address(newAdapter))); // // Didnt respect 3 days before propsal and change - // vault.changeAdapter(); + // vault.changeAdapters(); // } - // function testFail__changeAdapter_after_init() public { - // vault.changeAdapter(); + // function testFail__changeAdapters_after_init() public { + // vault.changeAdapters(); // } - // function testFail__changeAdapter_instantly_again() public { + // function testFail__changeAdapters_instantly_again() public { // MockERC4626 newAdapter = _createAdapter(IERC20(address(asset))); // uint256 depositAmount = 1 ether; @@ -878,8 +897,8 @@ contract MultiStrategyVaultTest is Test { // vm.expectEmit(false, false, false, true, address(vault)); // emit ChangedAdapter(IERC4626(address(adapter)), IERC4626(address(newAdapter))); - // vault.changeAdapter(); - // vault.changeAdapter(); + // vault.changeAdapters(); + // vault.changeAdapters(); // } /*////////////////////////////////////////////////////////////// From 233e0bf7bbda442b1c7f2845875130dad1170da7 Mon Sep 17 00:00:00 2001 From: RedVeil Date: Tue, 28 Feb 2023 18:00:39 +0100 Subject: [PATCH 5/7] wip - multi deposit solved --- .../foundry/src/vault/MultiStrategyVault.sol | 159 ++++++++---------- .../integration/aaveV2/AaveV2Adapter.t.sol | 3 + .../integration/aaveV3/AaveV3Adapter.t.sol | 3 + .../integration/beefy/BeefyAdapter.t.sol | 12 +- .../compoundV2/CompoundV2Adapter.t.sol | 3 + 5 files changed, 94 insertions(+), 86 deletions(-) diff --git a/packages/foundry/src/vault/MultiStrategyVault.sol b/packages/foundry/src/vault/MultiStrategyVault.sol index e13f6c47..3ad176a8 100644 --- a/packages/foundry/src/vault/MultiStrategyVault.sol +++ b/packages/foundry/src/vault/MultiStrategyVault.sol @@ -133,6 +133,10 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P return deposit(assets, msg.sender); } + function mint(uint256 shares) external returns (uint256) { + return mint(shares, msg.sender); + } + /** * @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 @@ -145,85 +149,67 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P } /** - * @notice Deposit exactly `assets` amount of tokens, issuing vault shares to `receiver`. - * @param assets Quantity of tokens to deposit. - * @param receiver Receiver of issued vault shares. - * @return shares Quantity of vault shares issued to `receiver`. + * @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 deposit(uint256 assets, address receiver) - public - override - nonReentrant - whenNotPaused - 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 (shares == 0) revert ZeroAmount(); - - IERC20(asset()).safeTransferFrom(msg.sender, address(this), assets); + 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); + } - for (uint8 i; i < adapterCount; i++) { - adapters[i].adapter.deposit( - assets.mulDiv(uint256(adapters[i].allocation), 1e18, Math.Rounding.Down), - address(this) - ); - } + // TODO move feeShare calc into deposit and mint + // TODO create second _deposit with feeShare param to deposit assets and mint fee shares + // TODO do the same for withdraw and redeem + // TODO add rebalance + // TODO Test with adapter that it actually works - if (feeShares > 0) _mint(feeRecipient, feeShares); + + function deposit(uint256 assets, address receiver) public override returns (uint256) { + require(assets <= maxDeposit(receiver), "ERC4626: deposit more than max"); - _mint(receiver, shares); + uint256 shares = previewDeposit(assets); + _deposit(_msgSender(), receiver, assets, shares); - emit Deposit(msg.sender, receiver, assets, shares); + return shares; } - event log_uint(uint256); - event log_address(address); - event log(string); + function mint(uint256 shares, address receiver) public override returns (uint256) { + require(shares <= maxMint(receiver), "ERC4626: mint more than max"); - function mint(uint256 shares) external returns (uint256) { - return mint(shares, msg.sender); + uint256 assets = previewMint(shares); + _deposit(_msgSender(), receiver, assets, shares); + + return assets; } - /** - * @notice Mint exactly `shares` vault shares to `receiver`, taking the necessary amount of `asset` from the caller. - * @param shares Quantity of shares to mint. - * @param receiver Receiver of issued vault shares. - * @return assets Quantity of assets deposited by caller. - */ - function mint(uint256 shares, address receiver) public override nonReentrant whenNotPaused returns (uint256 assets) { + function _deposit( + address caller, + address receiver, + uint256 assets, + uint256 shares + ) internal override nonReentrant whenNotPaused { if (receiver == address(0)) revert InvalidReceiver(); - if (shares == 0) revert ZeroAmount(); - uint256 depositFee = uint256(fees.deposit); - - uint256 feeShares = shares.mulDiv(depositFee, 1e18 - depositFee, Math.Rounding.Down); + IERC20(asset()).safeTransferFrom(caller, address(this), assets); - assets = _convertToAssets(shares + feeShares, Math.Rounding.Up); + uint256 feeShares = _convertToShares( + assets.mulDiv(uint256(fees.deposit), 1e18, Math.Rounding.Down), + Math.Rounding.Down + ); + shares -= feeShares; - if (assets > maxMint(receiver)) revert MaxError(assets); + for (uint8 i; i < adapterCount; i++) { + adapters[i].adapter.deposit(assets.mulDiv(adapters[i].allocation, 1e18, Math.Rounding.Down), address(this)); + } if (feeShares > 0) _mint(feeRecipient, feeShares); _mint(receiver, shares); - IERC20(asset()).safeTransferFrom(msg.sender, address(this), assets); - - for (uint8 i; i < adapterCount; i++) { - adapters[i].adapter.deposit( - assets.mulDiv(uint256(adapters[i].allocation), 1e18, Math.Rounding.Down), - address(this) - ); - } - - emit Deposit(msg.sender, receiver, assets, shares); + emit Deposit(caller, receiver, assets, shares); } function withdraw(uint256 assets) public returns (uint256) { @@ -260,21 +246,16 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P shares = _convertToShares(assets, Math.Rounding.Up); if (shares == 0) revert ZeroAmount(); - emit log("got here1"); uint256 withdrawalFee = uint256(fees.withdrawal); uint256 feeShares = shares.mulDiv(withdrawalFee, 1e18 - withdrawalFee, Math.Rounding.Down); shares += feeShares; - emit log("got here2"); - emit log_address(msg.sender); - emit log_address(owner); + if (msg.sender != owner) _approve(owner, msg.sender, allowance(owner, msg.sender) - shares); - emit log("got here3"); _burn(owner, shares); - emit log("got here4"); if (feeShares > 0) _mint(feeRecipient, feeShares); @@ -285,14 +266,39 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P address(this) ); } - emit log("got here5"); IERC20(asset()).safeTransfer(receiver, assets); - emit log("got here6"); emit Withdraw(msg.sender, receiver, owner, 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)); + } + + uint256 withdrawalFee = uint256(fees.withdrawal); + uint256 feeShares = shares.mulDiv(withdrawalFee, 1e18 - withdrawalFee, Math.Rounding.Down); + if (feeShares > 0) _mint(feeRecipient, feeShares); + + IERC20(asset()).safeTransfer(receiver, assets); + + emit Withdraw(caller, receiver, owner, assets, shares); + } + function redeem(uint256 shares) external returns (uint256) { return redeem(shares, msg.sender, msg.sender); } @@ -348,23 +354,6 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P } } - /** - * @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); - - for (uint8 i; i < adapterCount; i++) { - assets += adapters[i].adapter.previewMint( - shares.mulDiv(uint256(adapters[i].allocation), 1e18, Math.Rounding.Down) - ); - } - } - /** * @notice Simulate the effects of a redemption at the current block, given current on-chain conditions. * @param shares Exact amount of `shares` to redeem 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 } From 2c67f5db057fdadaff98cd4eb979cb580c9ae89e Mon Sep 17 00:00:00 2001 From: RedVeil Date: Fri, 3 Mar 2023 18:53:07 +0100 Subject: [PATCH 6/7] MultiStrategy vault tests green --- .../foundry/src/vault/MultiStrategyVault.sol | 288 ++++++++---------- .../test/vault/MultiStrategyVault.t.sol | 43 +-- 2 files changed, 146 insertions(+), 185 deletions(-) diff --git a/packages/foundry/src/vault/MultiStrategyVault.sol b/packages/foundry/src/vault/MultiStrategyVault.sol index 3ad176a8..29723ce3 100644 --- a/packages/foundry/src/vault/MultiStrategyVault.sol +++ b/packages/foundry/src/vault/MultiStrategyVault.sol @@ -80,7 +80,6 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P for (uint8 i; i < adapterCount_; i++) { adapters[i] = adapters_[i]; - // TODO use save approve asset_.approve(address(adapters_[i].adapter), type(uint256).max); } @@ -133,53 +132,40 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P return deposit(assets, msg.sender); } - function mint(uint256 shares) external returns (uint256) { - return mint(shares, msg.sender); - } - - /** - * @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); - } + function deposit(uint256 assets, address receiver) public override returns (uint256 shares) { + if (receiver == address(0)) revert InvalidReceiver(); + if (assets > maxDeposit(receiver)) revert MaxError(assets); - /** - * @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); - } + uint256 feeShares = _convertToShares( + assets.mulDiv(uint256(fees.deposit), 1e18, Math.Rounding.Down), + Math.Rounding.Down + ); - // TODO move feeShare calc into deposit and mint - // TODO create second _deposit with feeShare param to deposit assets and mint fee shares - // TODO do the same for withdraw and redeem - // TODO add rebalance - // TODO Test with adapter that it actually works + shares = _convertToShares(assets, Math.Rounding.Down) - feeShares; - - function deposit(uint256 assets, address receiver) public override returns (uint256) { - require(assets <= maxDeposit(receiver), "ERC4626: deposit more than max"); + if (feeShares > 0) _mint(feeRecipient, feeShares); - uint256 shares = previewDeposit(assets); _deposit(_msgSender(), receiver, assets, shares); return shares; } - function mint(uint256 shares, address receiver) public override returns (uint256) { - require(shares <= maxMint(receiver), "ERC4626: mint more than max"); + 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); - uint256 assets = previewMint(shares); _deposit(_msgSender(), receiver, assets, shares); return assets; @@ -195,18 +181,10 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P IERC20(asset()).safeTransferFrom(caller, address(this), assets); - uint256 feeShares = _convertToShares( - assets.mulDiv(uint256(fees.deposit), 1e18, Math.Rounding.Down), - Math.Rounding.Down - ); - shares -= feeShares; - for (uint8 i; i < adapterCount; i++) { adapters[i].adapter.deposit(assets.mulDiv(adapters[i].allocation, 1e18, Math.Rounding.Down), address(this)); } - if (feeShares > 0) _mint(feeRecipient, feeShares); - _mint(receiver, shares); emit Deposit(caller, receiver, assets, shares); @@ -216,19 +194,6 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P return withdraw(assets, msg.sender, msg.sender); } - /** - * @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) { - uint256 withdrawalFee = uint256(fees.withdrawal); - assets += assets.mulDiv(withdrawalFee, 1e18 - withdrawalFee, Math.Rounding.Up); - - shares = _convertToShares(assets, Math.Rounding.Up); - } - /** * @notice Burn shares from `owner` in exchange for `assets` amount of underlying token. * @param assets Quantity of underlying `asset` token to withdraw. @@ -240,12 +205,11 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P uint256 assets, address receiver, address owner - ) public override nonReentrant returns (uint256 shares) { + ) public override returns (uint256 shares) { if (receiver == address(0)) revert InvalidReceiver(); if (assets > maxWithdraw(owner)) revert MaxError(assets); - shares = _convertToShares(assets, Math.Rounding.Up); - if (shares == 0) revert ZeroAmount(); + uint256 shares = _convertToShares(assets, Math.Rounding.Up); uint256 withdrawalFee = uint256(fees.withdrawal); @@ -253,50 +217,11 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P shares += feeShares; - if (msg.sender != owner) _approve(owner, msg.sender, allowance(owner, msg.sender) - shares); - - _burn(owner, shares); - - if (feeShares > 0) _mint(feeRecipient, feeShares); - - for (uint8 i; i < adapterCount; i++) { - adapters[i].adapter.withdraw( - assets.mulDiv(uint256(adapters[i].allocation), 1e18, Math.Rounding.Down), - address(this), - address(this) - ); - } - - IERC20(asset()).safeTransfer(receiver, assets); - - emit Withdraw(msg.sender, receiver, owner, 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)); - } - - uint256 withdrawalFee = uint256(fees.withdrawal); - uint256 feeShares = shares.mulDiv(withdrawalFee, 1e18 - withdrawalFee, Math.Rounding.Down); if (feeShares > 0) _mint(feeRecipient, feeShares); - IERC20(asset()).safeTransfer(receiver, assets); + _withdraw(_msgSender(), receiver, owner, assets, shares); - emit Withdraw(caller, receiver, owner, assets, shares); + return shares; } function redeem(uint256 shares) external returns (uint256) { @@ -314,31 +239,49 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P uint256 shares, address receiver, address owner - ) public override nonReentrant returns (uint256 assets) { + ) public override returns (uint256 assets) { if (receiver == address(0)) revert InvalidReceiver(); - if (shares == 0) revert ZeroAmount(); - - if (msg.sender != owner) _approve(owner, msg.sender, allowance(owner, msg.sender) - shares); + 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 (assets > maxRedeem(owner)) revert MaxError(assets); + if (feeShares > 0) _mint(feeRecipient, feeShares); - _burn(owner, shares); + _withdraw(_msgSender(), receiver, owner, assets, shares); - if (feeShares > 0) _mint(feeRecipient, feeShares); + 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( - assets.mulDiv(uint256(adapters[i].allocation), 1e18, Math.Rounding.Down), - receiver, + allocation, + address(this), address(this) ); } - emit Withdraw(msg.sender, receiver, owner, assets, shares); + IERC20(asset()).safeTransfer(receiver, assets); + + emit Withdraw(caller, receiver, owner, assets, shares); } /*////////////////////////////////////////////////////////////// @@ -354,6 +297,42 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P } } + /** + * @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 @@ -361,12 +340,9 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P * @dev This method accounts for both issuance of fee shares and withdrawal fee. */ function previewRedeem(uint256 shares) public view override returns (uint256 assets) { - for (uint8 i; i < adapterCount; i++) { - assets += adapters[i].adapter.previewRedeem( - shares.mulDiv(uint256(adapters[i].allocation), 1e18, Math.Rounding.Down) - ); - } - assets -= assets.mulDiv(uint256(fees.withdrawal), 1e18, Math.Rounding.Down); + 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) @@ -388,27 +364,27 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P //////////////////////////////////////////////////////////////*/ /// @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; + 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); + 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; - // } + if (adapterMax > type(uint256).max / scalar) { + adapterMax = type(uint256).max; + } else { + adapterMax *= scalar; + } - // maxDeposit_ = Math.min(maxDeposit_, adapterMax); - // } + maxDeposit_ = Math.min(maxDeposit_, adapterMax); + } - // return maxDeposit_; - // } + 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) { @@ -416,7 +392,10 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P uint256 depositLimit_ = depositLimit; if (paused() || assets >= depositLimit_) return 0; - uint256 maxMint_ = depositLimit_; + 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); @@ -434,28 +413,32 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P } /// @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; + 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); + 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 = adapterMax.mulDiv(1e18, uint256(adapters[i].allocation), Math.Rounding.Down); - // } + if (adapterMax > type(uint256).max / scalar) { + adapterMax = type(uint256).max; + } else { + adapterMax *= scalar; + } - // maxWithdraw_ = Math.min(maxWithdraw_, adapterMax); - // } + maxWithdraw_ = Math.min(maxWithdraw_, adapterMax + leftover); + } - // return maxWithdraw_; - // } + 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)); @@ -467,7 +450,7 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P adapterMax *= scalar; } - maxRedeem_ = Math.min(maxRedeem_, adapterMax); + maxRedeem_ = Math.min(maxRedeem_, adapterMax + leftover); } return maxRedeem_; @@ -674,7 +657,6 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P for (uint8 i; i < adapterCount; i++) { adapters[i].adapter.redeem(adapters[i].adapter.balanceOf(address(this)), address(this), address(this)); - // TODO use save approve IERC20(asset()).approve(address(adapters[i].adapter), 0); } diff --git a/packages/foundry/test/vault/MultiStrategyVault.t.sol b/packages/foundry/test/vault/MultiStrategyVault.t.sol index 251056ea..8c377b01 100644 --- a/packages/foundry/test/vault/MultiStrategyVault.t.sol +++ b/packages/foundry/test/vault/MultiStrategyVault.t.sol @@ -331,30 +331,15 @@ contract MultiStrategyVaultTest is Test { vm.prank(alice); vault.withdraw(aliceAssetAmount, alice, alice); - // assertEq(adapter1.beforeWithdrawHookCalledCounter(), 1, "w adapter1"); - // assertEq(adapter2.beforeWithdrawHookCalledCounter(), 1, "w adapter2"); + 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"); - - // vm.prank(alice); - // vault.redeem(aliceShareAmount, alice, alice); - - // 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"); + 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_zero() public { - vault.deposit(0, address(this)); - } - - function testFail__withdraw_zero() public { - vault.withdraw(0, address(this), address(this)); - } function testFail__deposit_with_no_approval() public { vault.deposit(1e18, address(this)); @@ -427,14 +412,6 @@ contract MultiStrategyVaultTest is Test { assertEq(asset.balanceOf(alice), alicePreDepositBal); } - function testFail__mint_zero() public { - vault.mint(0, address(this)); - } - - function testFail__redeem_zero() public { - vault.redeem(0, address(this), address(this)); - } - function testFail__mint_with_no_approval() public { vault.mint(1e18, address(this)); } @@ -551,9 +528,11 @@ contract MultiStrategyVaultTest is Test { // 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 @@ -974,7 +953,7 @@ contract MultiStrategyVaultTest is Test { vm.expectRevert(abi.encodeWithSelector(MultiStrategyVault.MaxError.selector, 101)); vault.deposit(101, address(this)); - vm.expectRevert(abi.encodeWithSelector(MultiStrategyVault.MaxError.selector, 101)); + vm.expectRevert(abi.encodeWithSelector(MultiStrategyVault.MaxError.selector, 101 * 1e9)); vault.mint(101 * 1e9, address(this)); } @@ -1006,11 +985,11 @@ contract MultiStrategyVaultTest is Test { assertTrue(vault.paused()); vm.prank(alice); - vm.expectRevert("Pausable: paused"); + vm.expectRevert(abi.encodeWithSelector(MultiStrategyVault.MaxError.selector, depositAmount)); vault.deposit(depositAmount, alice); vm.prank(alice); - vm.expectRevert("Pausable: paused"); + vm.expectRevert(abi.encodeWithSelector(MultiStrategyVault.MaxError.selector, depositAmount)); vault.mint(depositAmount, alice); vm.prank(alice); From bc3b82b43644fb326b24ae1e841f5026758e056d Mon Sep 17 00:00:00 2001 From: RedVeil Date: Fri, 3 Mar 2023 19:09:26 +0100 Subject: [PATCH 7/7] change adapter tests --- .../foundry/src/vault/MultiStrategyVault.sol | 19 +- .../test/vault/MultiStrategyVault.t.sol | 194 ++++++++++-------- 2 files changed, 116 insertions(+), 97 deletions(-) diff --git a/packages/foundry/src/vault/MultiStrategyVault.sol b/packages/foundry/src/vault/MultiStrategyVault.sol index 29723ce3..e46ec3ac 100644 --- a/packages/foundry/src/vault/MultiStrategyVault.sol +++ b/packages/foundry/src/vault/MultiStrategyVault.sol @@ -272,11 +272,7 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P if (assets % 2 == 1 && i == 0 && IERC20(asset()).balanceOf(address(this)) == 0) allocation += 1; - adapters[i].adapter.withdraw( - allocation, - address(this), - address(this) - ); + adapters[i].adapter.withdraw(allocation, address(this), address(this)); } IERC20(asset()).safeTransfer(receiver, assets); @@ -604,8 +600,13 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P uint256 public proposedAdapterTime; - event NewAdaptersProposed(AdapterConfig[10] newAdapter, uint256 timestamp); - event ChangedAdapters(AdapterConfig[10] oldAdapter, AdapterConfig[10] newAdapter); + 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(); @@ -626,7 +627,7 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P proposedAdapterTime = block.timestamp; - emit NewAdaptersProposed(newAdapters, block.timestamp); + emit NewAdaptersProposed(newAdapters, proposedAdapterCount, block.timestamp); } function _verifyAdapterConfig(AdapterConfig[10] calldata newAdapters, uint8 adapterCount_) internal { @@ -660,7 +661,7 @@ contract MultiStrategyVault is ERC4626Upgradeable, ReentrancyGuardUpgradeable, P IERC20(asset()).approve(address(adapters[i].adapter), 0); } - emit ChangedAdapters(adapters, proposedAdapters); + emit ChangedAdapters(adapters, adapterCount, proposedAdapters, proposedAdapterCount); adapters = proposedAdapters; adapterCount = proposedAdapterCount; diff --git a/packages/foundry/test/vault/MultiStrategyVault.t.sol b/packages/foundry/test/vault/MultiStrategyVault.t.sol index 8c377b01..688a19c8 100644 --- a/packages/foundry/test/vault/MultiStrategyVault.t.sol +++ b/packages/foundry/test/vault/MultiStrategyVault.t.sol @@ -38,8 +38,13 @@ contract MultiStrategyVaultTest is Test { event NewFeesProposed(VaultFees newFees, uint256 timestamp); event ChangedFees(VaultFees oldFees, VaultFees newFees); event FeeRecipientUpdated(address oldFeeRecipient, address newFeeRecipient); - event NewAdaptersProposed(AdapterConfig[10] newAdapter, uint256 timestamp); - event ChangedAdapters(AdapterConfig[10] oldAdapter, AdapterConfig[10] newAdapter); + 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); @@ -340,7 +345,6 @@ contract MultiStrategyVaultTest is Test { assertEq(asset.balanceOf(alice), alicePreDepositBal, "asset bal2"); } - function testFail__deposit_with_no_approval() public { vault.deposit(1e18, address(this)); } @@ -764,121 +768,135 @@ contract MultiStrategyVaultTest is Test { //////////////////////////////////////////////////////////////*/ // Propose Adapter - // function test__proposeAdapter() public { - // MockERC4626 newAdapter = _createAdapter(IERC20(address(asset))); + 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 NewAdapterProposed(IERC4626(address(newAdapter)), callTime); + uint256 callTime = block.timestamp; + vm.expectEmit(false, false, false, true, address(vault)); + emit NewAdaptersProposed(adapters, 1, callTime); - // vault.proposeAdapter(IERC4626(address(newAdapter))); + vault.proposeAdapters(adapters, 1); - // assertEq(vault.proposedAdapterTime(), callTime); - // assertEq(address(vault.proposedAdapter()), address(newAdapter)); - // } + assertEq(vault.proposedAdapterTime(), callTime); + //assertEq(address(vault.proposedAdapters()), address(newAdapter)); + assertEq(uint256(vault.proposedAdapterCount()), uint256(1)); + } - // function testFail__proposeAdapter_nonOwner() public { - // MockERC4626 newAdapter = _createAdapter(IERC20(address(asset))); + function testFail__proposeAdapters_nonOwner() public { + MockERC4626 newAdapter = _createAdapter(IERC20(address(asset))); + adapters[0].adapter = newAdapter; + adapters[0].allocation = 1e18; - // vm.prank(alice); - // vault.proposeAdapter(IERC4626(address(newAdapter))); - // } + vm.prank(alice); + vault.proposeAdapters(adapters, 2); + } - // function testFail__proposeAdapter_asset_missmatch() public { - // MockERC20 newAsset = new MockERC20("New Mock Token", "NTKN", 18); - // MockERC4626 newAdapter = _createAdapter(IERC20(address(newAsset))); + 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); + } - // vm.prank(alice); - // vault.proposeAdapter(IERC4626(address(newAdapter))); - // } + // Change Adapter + function test__changeAdapters() public { + MockERC4626 newAdapter = _createAdapter(IERC20(address(asset))); + adapters[0].adapter = newAdapter; + adapters[0].allocation = 1e18; + + uint256 depositAmount = 1 ether; - // // Change Adapter - // function test__changeAdapters() public { - // MockERC4626 newAdapter = _createAdapter(IERC20(address(asset))); - // 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(); - // // 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(); - // // Increase assets in asset Adapter to check hwm and assetCheckpoint later - // asset.mint(address(adapter), depositAmount); - // vault.takeManagementAndPerformanceFees(); - // uint256 oldHWM = vault.highWaterMark(); + // Preparation to change the adapter + vault.proposeAdapters(adapters, 1); - // // Preparation to change the adapter - // vault.proposeAdapter(IERC4626(address(newAdapter))); + vm.warp(block.timestamp + 3 days); - // vm.warp(block.timestamp + 3 days); + // vm.expectEmit(false, false, false, true, address(vault)); + // emit ChangedAdapter(IERC4626(address(adapter)), IERC4626(address(newAdapter))); - // vm.expectEmit(false, false, false, true, address(vault)); - // emit ChangedAdapter(IERC4626(address(adapter)), IERC4626(address(newAdapter))); + vault.changeAdapters(); - // 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); - // // Annoyingly Math fails us here and leaves 1 asset in the adapter - // assertEq(asset.allowance(address(vault), address(adapter)), 0); - // assertEq(asset.balanceOf(address(adapter)), 1); - // assertEq(adapter.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(asset.balanceOf(address(newAdapter)), (depositAmount * 2) - 1); - // assertEq(newAdapter.balanceOf(address(vault)), (depositAmount * 2e9) - 1e9); - // assertEq(asset.allowance(address(vault), address(newAdapter)), type(uint256).max); + assertEq(vault.highWaterMark(), oldHWM); - // assertEq(vault.highWaterMark(), oldHWM); + assertEq(vault.proposedAdapterTime(), 0); + //assertEq(address(vault.proposedAdapters()), address(0)); + } - // assertEq(vault.proposedAdapterTime(), 0); - // assertEq(address(vault.proposedAdapter()), address(0)); - // } + function testFail__changeAdapters_NonOwner() public { + vm.prank(alice); + vault.changeAdapters(); + } - // 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; - // function testFail__changeAdapters_respect_rageQuit() public { - // MockERC4626 newAdapter = _createAdapter(IERC20(address(asset))); + vault.proposeAdapters(adapters, 1); - // vault.proposeAdapter(IERC4626(address(newAdapter))); + // Didnt respect 3 days before propsal and change + vault.changeAdapters(); + } - // // Didnt respect 3 days before propsal and change - // vault.changeAdapters(); - // } + function testFail__changeAdapters_after_init() public { + 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; - // function testFail__changeAdapters_instantly_again() public { - // MockERC4626 newAdapter = _createAdapter(IERC20(address(asset))); - // uint256 depositAmount = 1 ether; + 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(); + // 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(adapter), depositAmount); - // vault.takeManagementAndPerformanceFees(); - // uint256 oldHWM = vault.highWaterMark(); + // 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.proposeAdapter(IERC4626(address(newAdapter))); + // Preparation to change the adapter + vault.proposeAdapters(adapters, 1); - // vm.warp(block.timestamp + 3 days); + vm.warp(block.timestamp + 3 days); - // vm.expectEmit(false, false, false, true, address(vault)); - // emit ChangedAdapter(IERC4626(address(adapter)), IERC4626(address(newAdapter))); + // vm.expectEmit(false, false, false, true, address(vault)); + // emit ChangedAdapter(IERC4626(address(adapter)), IERC4626(address(newAdapter))); - // vault.changeAdapters(); - // vault.changeAdapters(); - // } + vault.changeAdapters(); + vault.changeAdapters(); + } /*////////////////////////////////////////////////////////////// SET RAGE QUIT