1+ // SPDX-License-Identifier: Apache-2.0
2+ pragma solidity ^ 0.8.0 ;
3+
4+ import {ERC4626 } from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol " ;
5+ import { IERC20 , ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol " ;
6+ import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol " ;
7+ import {IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol " ;
8+ import {Math} from "@openzeppelin/contracts/utils/math/Math.sol " ;
9+ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol " ;
10+
11+ contract MasterVault is ERC4626 , Ownable {
12+ using SafeERC20 for IERC20 ;
13+ using Math for uint256 ;
14+
15+ error TooFewSharesReceived ();
16+ error TooManySharesBurned ();
17+ error TooManyAssetsDeposited ();
18+ error TooFewAssetsReceived ();
19+ error SubVaultAlreadySet ();
20+ error SubVaultCannotBeZeroAddress ();
21+ error MustHaveSupplyBeforeSettingSubVault ();
22+ error SubVaultAssetMismatch ();
23+ error SubVaultExchangeRateTooLow ();
24+ error NoExistingSubVault ();
25+ error MustHaveSupplyBeforeSwitchingSubVault ();
26+ error NewSubVaultExchangeRateTooLow ();
27+
28+ // todo: avoid inflation, rounding, other common 4626 vulns
29+ // we may need a minimum asset or master share amount when setting subvaults (bc of exchange rate calc)
30+ ERC4626 public subVault;
31+
32+ // how many subVault shares one MV2 share can be redeemed for
33+ // initially 1 to 1
34+ // constant per subvault
35+ // changes when subvault is set
36+ uint256 public subVaultExchRateWad = 1e18 ;
37+
38+ // note: the performance fee can be avoided if the underlying strategy can be sandwiched (eg ETH to wstETH dex swap)
39+ // maybe a simpler and more robust implementation would be for the owner to adjust the subVaultExchRateWad directly
40+ // this would also avoid the need for totalPrincipal tracking
41+ // however, this would require more trust in the owner
42+ uint256 public performanceFeeBps; // in basis points, e.g. 200 = 2% | todo a way to set this
43+ uint256 totalPrincipal; // total assets deposited, used to calculate profit
44+
45+ event SubvaultChanged (address indexed oldSubvault , address indexed newSubvault );
46+
47+ constructor (IERC20 _asset , string memory _name , string memory _symbol ) ERC20 (_name, _symbol) ERC4626 (_asset) Ownable () {}
48+
49+ function deposit (uint256 assets , address receiver , uint256 minSharesMinted ) public returns (uint256 ) {
50+ uint256 shares = super .deposit (assets, receiver);
51+ if (shares < minSharesMinted) revert TooFewSharesReceived ();
52+ return shares;
53+ }
54+
55+ function withdraw (uint256 assets , address receiver , address _owner , uint256 maxSharesBurned ) public returns (uint256 ) {
56+ uint256 shares = super .withdraw (assets, receiver, _owner);
57+ if (shares > maxSharesBurned) revert TooManySharesBurned ();
58+ return shares;
59+ }
60+
61+ function mint (uint256 shares , address receiver , uint256 maxAssetsDeposited ) public returns (uint256 ) {
62+ uint256 assets = super .mint (shares, receiver);
63+ if (assets > maxAssetsDeposited) revert TooManyAssetsDeposited ();
64+ return assets;
65+ }
66+
67+ function redeem (uint256 shares , address receiver , address _owner , uint256 minAssetsReceived ) public returns (uint256 ) {
68+ uint256 assets = super .redeem (shares, receiver, _owner);
69+ if (assets < minAssetsReceived) revert TooFewAssetsReceived ();
70+ return assets;
71+ }
72+
73+ /// @notice Set a subvault. Can only be called if there is not already a subvault set.
74+ /// @param _subVault The subvault to set. Must be an ERC4626 vault with the same asset as this MasterVault.
75+ /// @param minSubVaultExchRateWad Minimum acceptable ratio (times 1e18) of new subvault shares to outstanding MasterVault shares after deposit.
76+ function setSubVault (ERC4626 _subVault , uint256 minSubVaultExchRateWad ) external onlyOwner {
77+ if (address (subVault) != address (0 )) revert SubVaultAlreadySet ();
78+ _setSubVault (_subVault, minSubVaultExchRateWad);
79+ }
80+
81+ /// @notice Revokes the current subvault, moving all assets back to MasterVault
82+ /// @param minAssetExchRateWad Minimum acceptable ratio (times 1e18) of assets received from subvault to outstanding MasterVault shares
83+ function revokeSubVault (uint256 minAssetExchRateWad ) external onlyOwner {
84+ _revokeSubVault (minAssetExchRateWad);
85+ }
86+
87+ function _setSubVault (ERC4626 _subVault , uint256 minSubVaultExchRateWad ) internal {
88+ if (address (_subVault) == address (0 )) revert SubVaultCannotBeZeroAddress ();
89+ if (totalSupply () == 0 ) revert MustHaveSupplyBeforeSettingSubVault ();
90+ if (address (_subVault.asset ()) != address (asset ())) revert SubVaultAssetMismatch ();
91+
92+ IERC20 (asset ()).safeApprove (address (_subVault), type (uint256 ).max);
93+ uint256 subShares = _subVault.deposit (totalAssets (), address (this ));
94+
95+ uint256 _subVaultExchRateWad = subShares.mulDiv (1e18 , totalSupply (), Math.Rounding.Down);
96+ if (_subVaultExchRateWad < minSubVaultExchRateWad) revert SubVaultExchangeRateTooLow ();
97+ subVaultExchRateWad = _subVaultExchRateWad;
98+
99+ subVault = _subVault;
100+
101+ emit SubvaultChanged (address (0 ), address (_subVault));
102+ }
103+
104+ function _revokeSubVault (uint256 minAssetExchRateWad ) internal {
105+ ERC4626 oldSubVault = subVault;
106+ if (address (oldSubVault) == address (0 )) revert NoExistingSubVault ();
107+
108+ uint256 _totalSupply = totalSupply ();
109+ uint256 assetReceived = oldSubVault.withdraw (oldSubVault.maxWithdraw (address (this )), address (this ), address (this ));
110+ uint256 effectiveAssetExchRateWad = assetReceived.mulDiv (1e18 , _totalSupply, Math.Rounding.Down);
111+ if (effectiveAssetExchRateWad < minAssetExchRateWad) revert TooFewAssetsReceived ();
112+
113+ IERC20 (asset ()).safeApprove (address (oldSubVault), 0 );
114+ subVault = ERC4626 (address (0 ));
115+ subVaultExchRateWad = 1e18 ;
116+
117+ emit SubvaultChanged (address (oldSubVault), address (0 ));
118+ }
119+
120+ /// @notice Switches to a new subvault or revokes current subvault if newSubVault is zero address
121+ /// @param newSubVault The new subvault to switch to, or zero address to revoke current subvault
122+ /// @param minAssetExchRateWad Minimum acceptable ratio (times 1e18) of assets received from old subvault to outstanding MasterVault shares
123+ /// @param minNewSubVaultExchRateWad Minimum acceptable ratio (times 1e18) of new subvault shares to outstanding MasterVault shares after deposit
124+ function switchSubVault (ERC4626 newSubVault , uint256 minAssetExchRateWad , uint256 minNewSubVaultExchRateWad ) external onlyOwner {
125+ _revokeSubVault (minAssetExchRateWad);
126+
127+ if (address (newSubVault) != address (0 )) {
128+ _setSubVault (newSubVault, minNewSubVaultExchRateWad);
129+ }
130+ }
131+
132+ function masterSharesToSubShares (uint256 masterShares , Math.Rounding rounding ) public view returns (uint256 ) {
133+ return masterShares.mulDiv (subVaultExchRateWad, 1e18 , rounding);
134+ }
135+
136+ function subSharesToMasterShares (uint256 subShares , Math.Rounding rounding ) public view returns (uint256 ) {
137+ return subShares.mulDiv (1e18 , subVaultExchRateWad, rounding);
138+ }
139+
140+ /** @dev See {IERC4626-totalAssets}. */
141+ function totalAssets () public view virtual override returns (uint256 ) {
142+ ERC4626 _subVault = subVault;
143+ if (address (_subVault) == address (0 )) {
144+ return super .totalAssets ();
145+ }
146+ return _subVault.convertToAssets (_subVault.balanceOf (address (this )));
147+ }
148+
149+ /** @dev See {IERC4626-maxDeposit}. */
150+ function maxDeposit (address ) public view virtual override returns (uint256 ) {
151+ if (address (subVault) == address (0 )) {
152+ return type (uint256 ).max;
153+ }
154+ return subVault.maxDeposit (address (this ));
155+ }
156+
157+ /** @dev See {IERC4626-maxMint}. */
158+ function maxMint (address ) public view virtual override returns (uint256 ) {
159+ uint256 subShares = subVault.maxMint (address (this ));
160+ if (subShares == type (uint256 ).max) {
161+ return type (uint256 ).max;
162+ }
163+ return subSharesToMasterShares (subShares, Math.Rounding.Down);
164+ }
165+
166+ /**
167+ * @dev Internal conversion function (from assets to shares) with support for rounding direction.
168+ *
169+ * Will revert if assets > 0, totalSupply > 0 and totalAssets = 0. That corresponds to a case where any asset
170+ * would represent an infinite amount of shares.
171+ */
172+ function _convertToShares (uint256 assets , Math.Rounding rounding ) internal view virtual override returns (uint256 shares ) {
173+ ERC4626 _subVault = subVault;
174+ if (address (_subVault) == address (0 )) {
175+ return super ._convertToShares (assets, rounding);
176+ }
177+ uint256 subShares = rounding == Math.Rounding.Up ? _subVault.previewWithdraw (assets) : _subVault.previewDeposit (assets);
178+ return subSharesToMasterShares (subShares, rounding);
179+ }
180+
181+ /**
182+ * @dev Internal conversion function (from shares to assets) with support for rounding direction.
183+ */
184+ function _convertToAssets (uint256 shares , Math.Rounding rounding ) internal view virtual override returns (uint256 assets ) {
185+ ERC4626 _subVault = subVault;
186+ if (address (_subVault) == address (0 )) {
187+ return super ._convertToAssets (shares, rounding);
188+ }
189+ uint256 subShares = masterSharesToSubShares (shares, rounding);
190+ return rounding == Math.Rounding.Up ? _subVault.previewMint (subShares) : _subVault.previewRedeem (subShares);
191+ }
192+
193+ function totalProfit () public view returns (uint256 ) {
194+ uint256 _totalAssets = totalAssets ();
195+ return _totalAssets > totalPrincipal ? _totalAssets - totalPrincipal : 0 ;
196+ }
197+
198+ /**
199+ * @dev Deposit/mint common workflow.
200+ */
201+ function _deposit (
202+ address caller ,
203+ address receiver ,
204+ uint256 assets ,
205+ uint256 shares
206+ ) internal virtual override {
207+ super ._deposit (caller, receiver, assets, shares);
208+ totalPrincipal += assets;
209+ ERC4626 _subVault = subVault;
210+ if (address (_subVault) != address (0 )) {
211+ _subVault.deposit (assets, address (this ));
212+ }
213+ }
214+
215+ /**
216+ * @dev Withdraw/redeem common workflow.
217+ */
218+ function _withdraw (
219+ address caller ,
220+ address receiver ,
221+ address _owner ,
222+ uint256 assets ,
223+ uint256 shares
224+ ) internal virtual override {
225+ ERC4626 _subVault = subVault;
226+ if (address (_subVault) != address (0 )) {
227+ _subVault.withdraw (assets, address (this ), address (this ));
228+ }
229+
230+ ////// PERF FEE STUFF //////
231+ // determine profit portion and principal portion of assets
232+ uint256 _totalProfit = totalProfit ();
233+ // use shares because they are rounded up vs assets which are rounded down
234+ uint256 profitPortion = shares.mulDiv (_totalProfit, totalSupply (), Math.Rounding.Up);
235+ uint256 principalPortion = assets - profitPortion;
236+
237+ // subtract principal portion from totalPrincipal
238+ totalPrincipal -= principalPortion;
239+
240+ // send fee to owner (todo should be a separate beneficiary addr set by owner)
241+ if (performanceFeeBps > 0 && profitPortion > 0 ) {
242+ uint256 fee = profitPortion.mulDiv (performanceFeeBps, 10000 , Math.Rounding.Up);
243+ // send fee to owner
244+ IERC20 (asset ()).safeTransfer (owner (), fee);
245+
246+ // note subtraction
247+ assets -= fee;
248+ }
249+
250+ ////// END PERF FEE STUFF //////
251+
252+ // call super._withdraw with remaining assets
253+ super ._withdraw (caller, receiver, _owner, assets, shares);
254+ }
255+ }
0 commit comments