@@ -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.
@@ -23,25 +25,14 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol
2325/// For a subVault to be compatible with the MasterVault, it must adhere to the following:
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)
26- ///
27- /// For performance fees to be enabled, the subVault should also have a manipulation resistant
28- /// convertToAssets function. If convertToAssets can be manipulated,
29- /// an incorrect profit calculation may occur, leading to incorrect performance fee withdrawals.
30- /// If the subVault has a manipulable convertToAssets function, and performance fees are desired,
31- /// consider whitelisting a specific FEE_MANAGER_ROLE that is allowed to call withdrawPerformanceFees().
32- /// The fee manager is then trusted to not manipulate the subVault or be a victim of manipulation when withdrawing performance fees.
33- /// By default, only the owner has the FEE_MANAGER_ROLE.
28+ /// - convertToAssets and convertToShares must not be manipulable
3429contract MasterVault is Initializable , ERC4626Upgradeable , AccessControlUpgradeable , PausableUpgradeable {
3530 using SafeERC20 for IERC20 ;
3631 using MathUpgradeable for uint256 ;
3732
3833 /// @notice Vault manager role can set/revoke subvaults, toggle performance fees and set the performance fee beneficiary
3934 /// @dev Should never be granted to the zero address
4035 bytes32 public constant VAULT_MANAGER_ROLE = keccak256 ("VAULT_MANAGER_ROLE " );
41- /// @notice Fee manager role can call withdrawPerformanceFees()
42- /// @dev It is important that the convertToAssets function of the subVault is not manipulated prior to calling withdrawPerformanceFees().
43- /// See contract notice for more details.
44- bytes32 public constant FEE_MANAGER_ROLE = keccak256 ("FEE_MANAGER_ROLE " );
4536 /// @notice Pauser role can pause/unpause deposits and withdrawals (todo: pause should pause EVERYTHING)
4637 bytes32 public constant PAUSER_ROLE = keccak256 ("PAUSER_ROLE " );
4738
@@ -85,38 +76,17 @@ contract MasterVault is Initializable, ERC4626Upgradeable, AccessControlUpgradea
8576 __Pausable_init ();
8677
8778 _setRoleAdmin (VAULT_MANAGER_ROLE, DEFAULT_ADMIN_ROLE);
88- _setRoleAdmin (FEE_MANAGER_ROLE, DEFAULT_ADMIN_ROLE);
8979 _setRoleAdmin (PAUSER_ROLE, DEFAULT_ADMIN_ROLE);
9080
9181 _grantRole (DEFAULT_ADMIN_ROLE, _owner);
9282 _grantRole (VAULT_MANAGER_ROLE, _owner);
93- _grantRole (FEE_MANAGER_ROLE, _owner);
9483 _grantRole (PAUSER_ROLE, _owner);
9584 }
9685
97-
98- function deposit (uint256 assets , address receiver , uint256 minSharesMinted ) public returns (uint256 ) {
99- uint256 shares = deposit (assets, receiver);
100- if (shares < minSharesMinted) revert TooFewSharesReceived ();
101- return shares;
102- }
103-
104- function withdraw (uint256 assets , address receiver , address _owner , uint256 maxSharesBurned ) public returns (uint256 ) {
105- uint256 shares = withdraw (assets, receiver, _owner);
106- if (shares > maxSharesBurned) revert TooManySharesBurned ();
107- return shares;
108- }
109-
110- function mint (uint256 shares , address receiver , uint256 maxAssetsDeposited ) public returns (uint256 ) {
111- uint256 assets = super .mint (shares, receiver);
112- if (assets > maxAssetsDeposited) revert TooManyAssetsDeposited ();
113- return assets;
114- }
115-
116- function redeem (uint256 shares , address receiver , address _owner , uint256 minAssetsReceived ) public returns (uint256 ) {
117- uint256 assets = super .redeem (shares, receiver, _owner);
118- if (assets < minAssetsReceived) revert TooFewAssetsReceived ();
119- return assets;
86+ function distributePerformanceFee () external whenNotPaused {
87+ if (! enablePerformanceFee) revert PerformanceFeeDisabled ();
88+ subVault.redeem (totalProfitInSubVaultShares (MathUpgradeable.Rounding.Down), beneficiary, address (this ));
89+ // todo emit event
12090 }
12191
12292 /// @notice Set a subvault. Can only be called if there is not already a subvault set.
@@ -155,47 +125,26 @@ contract MasterVault is Initializable, ERC4626Upgradeable, AccessControlUpgradea
155125 emit SubvaultChanged (address (oldSubVault), address (0 ));
156126 }
157127
158- function masterSharesToSubShares (uint256 masterShares , MathUpgradeable.Rounding rounding ) public view returns (uint256 ) {
159- // masterShares * totalSubVaultShares / totalMasterShares
160- return masterShares.mulDiv (subVault.balanceOf (address (this )), totalSupply (), rounding);
161- }
162-
163- function subSharesToMasterShares (uint256 subShares , MathUpgradeable.Rounding rounding ) public view returns (uint256 ) {
164- // subShares * totalMasterShares / totalSubVaultShares
165- return subShares.mulDiv (totalSupply (), subVault.balanceOf (address (this )), rounding);
166- }
167-
168128 /// @notice Toggle performance fee collection on/off
169129 /// @param enabled True to enable performance fees, false to disable
170130 function setPerformanceFee (bool enabled ) external onlyRole (VAULT_MANAGER_ROLE) {
171131 enablePerformanceFee = enabled;
132+ // reset totalPrincipal to current totalAssets when enabling performance fee
133+ // this prevents a sudden large profit
134+ if (enabled) {
135+ totalPrincipal = _totalAssets (MathUpgradeable.Rounding.Up);
136+ }
172137 emit PerformanceFeeToggled (enabled);
173138 }
174139
175140 /// @notice Set the beneficiary address for performance fees
176141 /// @param newBeneficiary Address to receive performance fees, zero address defaults to owner
177- function setBeneficiary (address newBeneficiary ) external onlyRole (FEE_MANAGER_ROLE ) {
142+ function setBeneficiary (address newBeneficiary ) external onlyRole (VAULT_MANAGER_ROLE ) {
178143 address oldBeneficiary = beneficiary;
179144 beneficiary = newBeneficiary;
180145 emit BeneficiaryUpdated (oldBeneficiary, newBeneficiary);
181146 }
182147
183- /// @notice Withdraw all accumulated performance fees to beneficiary
184- /// @dev Only callable by fee manager when performance fees are enabled
185- function withdrawPerformanceFees () external onlyRole (FEE_MANAGER_ROLE) {
186- if (! enablePerformanceFee) revert PerformanceFeeDisabled ();
187- if (beneficiary == address (0 )) revert BeneficiaryNotSet ();
188-
189- uint256 totalProfits = totalProfit ();
190- if (totalProfits > 0 ) {
191- IERC4626 _subVault = subVault;
192- if (address (_subVault) != address (0 )) {
193- _subVault.withdraw (totalProfits, address (this ), address (this ));
194- }
195- IERC20 (asset ()).safeTransfer (beneficiary, totalProfits);
196- }
197- }
198-
199148 function pause () external onlyRole (PAUSER_ROLE) {
200149 _pause ();
201150 }
@@ -206,11 +155,7 @@ contract MasterVault is Initializable, ERC4626Upgradeable, AccessControlUpgradea
206155
207156 /** @dev See {IERC4626-totalAssets}. */
208157 function totalAssets () public view virtual override returns (uint256 ) {
209- IERC4626 _subVault = subVault;
210- if (address (_subVault) == address (0 )) {
211- return super .totalAssets ();
212- }
213- return _subVault.convertToAssets (_subVault.balanceOf (address (this )));
158+ return _totalAssets (MathUpgradeable.Rounding.Down);
214159 }
215160
216161 /** @dev See {IERC4626-maxDeposit}. */
@@ -221,7 +166,7 @@ contract MasterVault is Initializable, ERC4626Upgradeable, AccessControlUpgradea
221166 return subVault.maxDeposit (address (this ));
222167 }
223168
224- /** @dev See {IERC4626-maxMint}. */
169+ // / ** @dev See {IERC4626-maxMint}. */
225170 function maxMint (address ) public view virtual override returns (uint256 ) {
226171 if (address (subVault) == address (0 )) {
227172 return type (uint256 ).max;
@@ -230,39 +175,23 @@ contract MasterVault is Initializable, ERC4626Upgradeable, AccessControlUpgradea
230175 if (subShares == type (uint256 ).max) {
231176 return type (uint256 ).max;
232177 }
233- return subSharesToMasterShares ( subShares, MathUpgradeable.Rounding.Down);
178+ return totalSupply (). mulDiv ( subShares, subVault. balanceOf ( address ( this )), MathUpgradeable.Rounding.Down); // todo: check rounding direction
234179 }
235180
236- /**
237- * @dev Internal conversion function (from assets to shares) with support for rounding direction.
238- *
239- * Will revert if assets > 0, totalSupply > 0 and totalAssets = 0. That corresponds to a case where any asset
240- * would represent an infinite amount of shares.
241- */
242- function _convertToShares (uint256 assets , MathUpgradeable.Rounding rounding ) internal view virtual override returns (uint256 shares ) {
243- IERC4626 _subVault = subVault;
244- if (address (_subVault) == address (0 )) {
245- return super ._convertToShares (assets, rounding);
246- }
247- uint256 subShares = rounding == MathUpgradeable.Rounding.Up ? _subVault.previewWithdraw (assets) : _subVault.previewDeposit (assets);
248- return subSharesToMasterShares (subShares, rounding);
181+ function totalProfit (MathUpgradeable.Rounding rounding ) public view returns (uint256 ) {
182+ uint256 __totalAssets = _totalAssets (rounding);
183+ return __totalAssets > totalPrincipal ? __totalAssets - totalPrincipal : 0 ;
249184 }
250185
251- /**
252- * @dev Internal conversion function (from shares to assets) with support for rounding direction.
253- */
254- function _convertToAssets (uint256 shares , MathUpgradeable.Rounding rounding ) internal view virtual override returns (uint256 assets ) {
255- IERC4626 _subVault = subVault;
256- if (address (_subVault) == address (0 )) {
257- return super ._convertToAssets (shares, rounding);
186+ function totalProfitInSubVaultShares (MathUpgradeable.Rounding rounding ) public view returns (uint256 ) {
187+ if (address (subVault) == address (0 )) {
188+ revert ("Subvault not set " );
258189 }
259- uint256 subShares = masterSharesToSubShares (shares, rounding);
260- return rounding == MathUpgradeable.Rounding.Up ? _subVault.previewMint (subShares) : _subVault.previewRedeem (subShares);
261- }
262-
263- function totalProfit () public view returns (uint256 ) {
264- uint256 _totalAssets = totalAssets ();
265- return _totalAssets > totalPrincipal ? _totalAssets - totalPrincipal : 0 ;
190+ uint256 profitAssets = totalProfit (rounding);
191+ if (profitAssets == 0 ) {
192+ return 0 ;
193+ }
194+ return _assetsToSubVaultShares (profitAssets, rounding);
266195 }
267196
268197 /**
@@ -302,4 +231,76 @@ contract MasterVault is Initializable, ERC4626Upgradeable, AccessControlUpgradea
302231
303232 super ._withdraw (caller, receiver, _owner, assets, shares);
304233 }
234+
235+ function _totalAssets (MathUpgradeable.Rounding rounding ) internal view returns (uint256 ) {
236+ if (address (subVault) == address (0 )) {
237+ return IERC20 (asset ()).balanceOf (address (this ));
238+ }
239+ return _subVaultSharesToAssets (subVault.balanceOf (address (this )), rounding);
240+ }
241+
242+ /**
243+ * @dev Internal conversion function (from assets to shares) with support for rounding direction.
244+ *
245+ * Will revert if assets > 0, totalSupply > 0 and totalAssets = 0. That corresponds to a case where any asset
246+ * would represent an infinite amount of shares.
247+ */
248+ function _convertToShares (uint256 assets , MathUpgradeable.Rounding rounding ) internal view virtual override returns (uint256 shares ) {
249+ if (address (subVault) == address (0 )) {
250+ uint256 effectiveTotalAssets = enablePerformanceFee ? _min (totalAssets (), totalPrincipal) : totalAssets ();
251+ return totalSupply ().mulDiv (assets, effectiveTotalAssets, rounding);
252+ }
253+
254+ uint256 totalSubShares = subVault.balanceOf (address (this ));
255+
256+ if (enablePerformanceFee) {
257+ // since we use totalSubShares in the denominator of the final calculation,
258+ // and we are subtracting profit from it, we should use the same rounding direction for profit
259+ totalSubShares -= totalProfitInSubVaultShares (_flipRounding (rounding));
260+ }
261+
262+ uint256 subShares = _assetsToSubVaultShares (assets, rounding);
263+
264+ return totalSupply ().mulDiv (subShares, totalSubShares, rounding);
265+ }
266+
267+ /**
268+ * @dev Internal conversion function (from shares to assets) with support for rounding direction.
269+ */
270+ function _convertToAssets (uint256 shares , MathUpgradeable.Rounding rounding ) internal view virtual override returns (uint256 assets ) {
271+ // if we have no subvault, we just do normal pro-rata calculation
272+ if (address (subVault) == address (0 )) {
273+ uint256 effectiveTotalAssets = enablePerformanceFee ? _min (totalAssets (), totalPrincipal) : totalAssets ();
274+ return effectiveTotalAssets.mulDiv (shares, totalSupply (), rounding);
275+ }
276+
277+ uint256 totalSubShares = subVault.balanceOf (address (this ));
278+
279+ if (enablePerformanceFee) {
280+ // since we use totalSubShares in the numerator of the final calculation,
281+ // and we are subtracting profit from it, we should use the opposite rounding direction for profit
282+ totalSubShares -= totalProfitInSubVaultShares (_flipRounding (rounding));
283+ }
284+
285+ // totalSubShares * shares / totalMasterShares
286+ uint256 subShares = totalSubShares.mulDiv (shares, totalSupply (), rounding);
287+
288+ return _subVaultSharesToAssets (subShares, rounding);
289+ }
290+
291+ function _assetsToSubVaultShares (uint256 assets , MathUpgradeable.Rounding rounding ) internal view returns (uint256 subShares ) {
292+ return rounding == MathUpgradeable.Rounding.Up ? subVault.previewWithdraw (assets) : subVault.previewDeposit (assets);
293+ }
294+
295+ function _subVaultSharesToAssets (uint256 subShares , MathUpgradeable.Rounding rounding ) internal view returns (uint256 assets ) {
296+ return rounding == MathUpgradeable.Rounding.Up ? subVault.previewMint (subShares) : subVault.previewRedeem (subShares);
297+ }
298+
299+ function _min (uint256 a , uint256 b ) internal pure returns (uint256 ) {
300+ return a <= b ? a : b;
301+ }
302+
303+ function _flipRounding (MathUpgradeable.Rounding rounding ) internal pure returns (MathUpgradeable.Rounding) {
304+ return rounding == MathUpgradeable.Rounding.Up ? MathUpgradeable.Rounding.Down : MathUpgradeable.Rounding.Up;
305+ }
305306}
0 commit comments