@@ -13,6 +13,8 @@ import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IER
1313import {MathUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/math/MathUpgradeable.sol " ;
1414import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol " ;
1515
16+ // todo: should we have an arbitrary call function for the vault manager to do stuff with the subvault? like queue withdrawals etc
17+
1618/// @notice MasterVault is an ERC4626 metavault that deposits assets to an admin defined subVault.
1719/// @dev If a subVault is not set, MasterVault shares entitle holders to a pro-rata share of the underlying held by the MasterVault.
1820/// If a subVault is set, MasterVault shares entitle holders to a pro-rata share of subVault shares held by the MasterVault.
@@ -24,25 +26,14 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol
2426/// - It must be able to handle arbitrarily large deposits and withdrawals
2527/// - Deposit size or withdrawal size must not affect the exchange rate (i.e. no slippage)
2628/// - Must not have deposit or withdrawal fees (or the deposit/withdrawal fee beneficiary is trusted by all MasterVault users)
27- ///
28- /// For performance fees to be enabled, the subVault should also have a manipulation resistant
29- /// convertToAssets function. If convertToAssets can be manipulated,
30- /// an incorrect profit calculation may occur, leading to incorrect performance fee withdrawals.
31- /// If the subVault has a manipulable convertToAssets function, and performance fees are desired,
32- /// consider whitelisting a specific FEE_MANAGER_ROLE that is allowed to call withdrawPerformanceFees().
33- /// The fee manager is then trusted to not manipulate the subVault or be a victim of manipulation when withdrawing performance fees.
34- /// By default, only the owner has the FEE_MANAGER_ROLE.
29+ /// - convertToAssets and convertToShares must not be manipulable
3530contract MasterVault is Initializable , ERC4626Upgradeable , AccessControlUpgradeable , PausableUpgradeable {
3631 using SafeERC20 for IERC20 ;
3732 using MathUpgradeable for uint256 ;
3833
3934 /// @notice Vault manager role can set/revoke subvaults, toggle performance fees and set the performance fee beneficiary
4035 /// @dev Should never be granted to the zero address
4136 bytes32 public constant VAULT_MANAGER_ROLE = keccak256 ("VAULT_MANAGER_ROLE " );
42- /// @notice Fee manager role can call withdrawPerformanceFees()
43- /// @dev It is important that the convertToAssets function of the subVault is not manipulated prior to calling withdrawPerformanceFees().
44- /// See contract notice for more details.
45- bytes32 public constant FEE_MANAGER_ROLE = keccak256 ("FEE_MANAGER_ROLE " );
4637 /// @notice Pauser role can pause/unpause deposits and withdrawals (todo: pause should pause EVERYTHING)
4738 bytes32 public constant PAUSER_ROLE = keccak256 ("PAUSER_ROLE " );
4839
@@ -86,12 +77,10 @@ contract MasterVault is Initializable, ERC4626Upgradeable, AccessControlUpgradea
8677 __Pausable_init ();
8778
8879 _setRoleAdmin (VAULT_MANAGER_ROLE, DEFAULT_ADMIN_ROLE);
89- _setRoleAdmin (FEE_MANAGER_ROLE, DEFAULT_ADMIN_ROLE);
9080 _setRoleAdmin (PAUSER_ROLE, DEFAULT_ADMIN_ROLE);
9181
9282 _grantRole (DEFAULT_ADMIN_ROLE, _owner);
9383 _grantRole (VAULT_MANAGER_ROLE, _owner);
94- _grantRole (FEE_MANAGER_ROLE, _owner);
9584 _grantRole (PAUSER_ROLE, _owner);
9685 }
9786
@@ -156,47 +145,21 @@ contract MasterVault is Initializable, ERC4626Upgradeable, AccessControlUpgradea
156145 emit SubvaultChanged (address (oldSubVault), address (0 ));
157146 }
158147
159- function masterSharesToSubShares (uint256 masterShares , MathUpgradeable.Rounding rounding ) public view returns (uint256 ) {
160- // masterShares * totalSubVaultShares / totalMasterShares
161- return masterShares.mulDiv (subVault.balanceOf (address (this )), totalSupply (), rounding);
162- }
163-
164- function subSharesToMasterShares (uint256 subShares , MathUpgradeable.Rounding rounding ) public view returns (uint256 ) {
165- // subShares * totalMasterShares / totalSubVaultShares
166- return subShares.mulDiv (totalSupply (), subVault.balanceOf (address (this )), rounding);
167- }
168-
169148 /// @notice Toggle performance fee collection on/off
170149 /// @param enabled True to enable performance fees, false to disable
171150 function setPerformanceFee (bool enabled ) external onlyRole (VAULT_MANAGER_ROLE) {
172- enablePerformanceFee = enabled;
151+ enablePerformanceFee = enabled; // todo: this should set totalPrincipal to current totalAssets() to avoid sudden fee realization
173152 emit PerformanceFeeToggled (enabled);
174153 }
175154
176155 /// @notice Set the beneficiary address for performance fees
177156 /// @param newBeneficiary Address to receive performance fees, zero address defaults to owner
178- function setBeneficiary (address newBeneficiary ) external onlyRole (FEE_MANAGER_ROLE ) {
157+ function setBeneficiary (address newBeneficiary ) external onlyRole (VAULT_MANAGER_ROLE ) {
179158 address oldBeneficiary = beneficiary;
180159 beneficiary = newBeneficiary;
181160 emit BeneficiaryUpdated (oldBeneficiary, newBeneficiary);
182161 }
183162
184- /// @notice Withdraw all accumulated performance fees to beneficiary
185- /// @dev Only callable by fee manager when performance fees are enabled
186- function withdrawPerformanceFees () external onlyRole (FEE_MANAGER_ROLE) {
187- if (! enablePerformanceFee) revert PerformanceFeeDisabled ();
188- if (beneficiary == address (0 )) revert BeneficiaryNotSet ();
189-
190- uint256 totalProfits = totalProfit ();
191- if (totalProfits > 0 ) {
192- IERC4626 _subVault = subVault;
193- if (address (_subVault) != address (0 )) {
194- _subVault.withdraw (totalProfits, address (this ), address (this ));
195- }
196- IERC20 (asset ()).safeTransfer (beneficiary, totalProfits);
197- }
198- }
199-
200163 function pause () external onlyRole (PAUSER_ROLE) {
201164 _pause ();
202165 }
@@ -222,17 +185,17 @@ contract MasterVault is Initializable, ERC4626Upgradeable, AccessControlUpgradea
222185 return subVault.maxDeposit (address (this ));
223186 }
224187
225- /** @dev See {IERC4626-maxMint}. */
226- function maxMint (address ) public view virtual override returns (uint256 ) {
227- if (address (subVault) == address (0 )) {
228- return type (uint256 ).max;
229- }
230- uint256 subShares = subVault.maxMint (address (this ));
231- if (subShares == type (uint256 ).max) {
232- return type (uint256 ).max;
233- }
234- return subSharesToMasterShares (subShares, MathUpgradeable.Rounding.Down);
235- }
188+ // / ** @dev See {IERC4626-maxMint}. */
189+ // function maxMint(address) public view virtual override returns (uint256) {
190+ // if (address(subVault) == address(0)) {
191+ // return type(uint256).max;
192+ // }
193+ // uint256 subShares = subVault.maxMint(address(this));
194+ // if (subShares == type(uint256).max) {
195+ // return type(uint256).max;
196+ // }
197+ // return subSharesToMasterShares(subShares, MathUpgradeable.Rounding.Down);
198+ // }
236199
237200 /**
238201 * @dev Internal conversion function (from assets to shares) with support for rounding direction.
@@ -242,25 +205,70 @@ contract MasterVault is Initializable, ERC4626Upgradeable, AccessControlUpgradea
242205 */
243206 function _convertToShares (uint256 assets , MathUpgradeable.Rounding rounding ) internal view virtual override returns (uint256 shares ) {
244207 IERC4626 _subVault = subVault;
208+ uint256 _totalAssets = totalAssets ();
209+ uint256 _totalPrincipal = totalPrincipal;
210+ uint256 _totalSupply = totalSupply ();
211+
245212 if (address (_subVault) == address (0 )) {
246- return super ._convertToShares (assets, rounding);
213+ uint256 effectiveTotalAssets = enablePerformanceFee ? _min (_totalAssets, _totalPrincipal) : _totalAssets;
214+ return _totalSupply.mulDiv (assets, effectiveTotalAssets, rounding);
215+ }
216+
217+ uint256 subShares = _convertToSubShares (_subVault, assets, rounding);
218+ uint256 totalSubShares = _subVault.balanceOf (address (this ));
219+ uint256 profit = _totalAssets > _totalPrincipal ? _totalAssets - _totalPrincipal : 0 ;
220+
221+ if (enablePerformanceFee) {
222+ // subSharesFee is the amount of subVault shares set aside as performance fee
223+ uint256 subSharesFee = _convertToSubShares (_subVault, profit, rounding);
224+ totalSubShares -= subSharesFee;
247225 }
248- uint256 subShares = rounding == MathUpgradeable.Rounding.Up ? _subVault. previewWithdraw (assets) : _subVault. previewDeposit (assets);
249- return subSharesToMasterShares (subShares, rounding);
226+
227+ return _totalSupply. mulDiv (subShares, totalSubShares , rounding);
250228 }
251229
252230 /**
253231 * @dev Internal conversion function (from shares to assets) with support for rounding direction.
254232 */
255233 function _convertToAssets (uint256 shares , MathUpgradeable.Rounding rounding ) internal view virtual override returns (uint256 assets ) {
256234 IERC4626 _subVault = subVault;
235+ uint256 _totalAssets = totalAssets ();
236+ uint256 _totalPrincipal = totalPrincipal;
237+ uint256 _totalSupply = totalSupply ();
238+
257239 if (address (_subVault) == address (0 )) {
258- return super ._convertToAssets (shares, rounding);
240+ uint256 effectiveTotalAssets = enablePerformanceFee ? _min (_totalAssets, _totalPrincipal) : _totalAssets;
241+ return effectiveTotalAssets.mulDiv (shares, _totalSupply, rounding);
242+ }
243+
244+ uint256 totalSubShares = _subVault.balanceOf (address (this ));
245+ uint256 profit = _totalAssets > _totalPrincipal ? _totalAssets - _totalPrincipal : 0 ;
246+
247+ if (profit > 0 && enablePerformanceFee) {
248+ // subSharesFee is the amount of subVault shares set aside as performance fee
249+ uint256 subSharesFee = _convertToSubShares (_subVault, profit, rounding == MathUpgradeable.Rounding.Up ? MathUpgradeable.Rounding.Down : MathUpgradeable.Rounding.Up);
250+ totalSubShares -= subSharesFee;
259251 }
260- uint256 subShares = masterSharesToSubShares (shares, rounding);
252+
253+ // totalSubShares * shares / totalMasterShares
254+ uint256 subShares = totalSubShares.mulDiv (shares, _totalSupply, rounding);
255+
256+ return _convertToSubAssets (_subVault, subShares, rounding);
257+ }
258+
259+ function _convertToSubShares (IERC4626 _subVault , uint256 assets , MathUpgradeable.Rounding rounding ) internal view returns (uint256 subShares ) {
260+ return rounding == MathUpgradeable.Rounding.Up ? _subVault.previewWithdraw (assets) : _subVault.previewDeposit (assets);
261+ }
262+
263+ function _convertToSubAssets (IERC4626 _subVault , uint256 subShares , MathUpgradeable.Rounding rounding ) internal view returns (uint256 assets ) {
261264 return rounding == MathUpgradeable.Rounding.Up ? _subVault.previewMint (subShares) : _subVault.previewRedeem (subShares);
262265 }
263266
267+ function _min (uint256 a , uint256 b ) internal pure returns (uint256 ) {
268+ return a <= b ? a : b;
269+ }
270+
271+
264272 function totalProfit () public view returns (uint256 ) {
265273 uint256 _totalAssets = totalAssets ();
266274 return _totalAssets > totalPrincipal ? _totalAssets - totalPrincipal : 0 ;
0 commit comments