Skip to content

Commit 4470279

Browse files
committed
wip: continuous fee accounting
1 parent b37aec9 commit 4470279

File tree

1 file changed

+66
-58
lines changed

1 file changed

+66
-58
lines changed

contracts/tokenbridge/libraries/vault/MasterVault.sol

Lines changed: 66 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IER
1313
import {MathUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/math/MathUpgradeable.sol";
1414
import {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
3530
contract 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

Comments
 (0)