Skip to content

Commit 76fccff

Browse files
authored
Merge pull request #136 from OffchainLabs/ha/mv-fix-perf-fee-principal-tracking
WIP: fix perf fee principal tracking
2 parents 6b68210 + bc27b40 commit 76fccff

File tree

1 file changed

+99
-98
lines changed

1 file changed

+99
-98
lines changed

contracts/tokenbridge/libraries/vault/MasterVault.sol

Lines changed: 99 additions & 98 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.
@@ -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
3429
contract 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

Comments
 (0)