From 4470279a9f1d762918f987a7beead4354504cba1 Mon Sep 17 00:00:00 2001 From: Henry <11198460+godzillaba@users.noreply.github.com> Date: Wed, 12 Nov 2025 16:03:34 -0700 Subject: [PATCH 1/4] wip: continuous fee accounting --- .../libraries/vault/MasterVault.sol | 124 ++++++++++-------- 1 file changed, 66 insertions(+), 58 deletions(-) diff --git a/contracts/tokenbridge/libraries/vault/MasterVault.sol b/contracts/tokenbridge/libraries/vault/MasterVault.sol index d66725451..ff7ba6085 100644 --- a/contracts/tokenbridge/libraries/vault/MasterVault.sol +++ b/contracts/tokenbridge/libraries/vault/MasterVault.sol @@ -13,6 +13,8 @@ import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IER import {MathUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/math/MathUpgradeable.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +// todo: should we have an arbitrary call function for the vault manager to do stuff with the subvault? like queue withdrawals etc + /// @notice MasterVault is an ERC4626 metavault that deposits assets to an admin defined subVault. /// @dev If a subVault is not set, MasterVault shares entitle holders to a pro-rata share of the underlying held by the MasterVault. /// If a subVault is set, MasterVault shares entitle holders to a pro-rata share of subVault shares held by the MasterVault. @@ -24,14 +26,7 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol /// - It must be able to handle arbitrarily large deposits and withdrawals /// - Deposit size or withdrawal size must not affect the exchange rate (i.e. no slippage) /// - Must not have deposit or withdrawal fees (or the deposit/withdrawal fee beneficiary is trusted by all MasterVault users) -/// -/// For performance fees to be enabled, the subVault should also have a manipulation resistant -/// convertToAssets function. If convertToAssets can be manipulated, -/// an incorrect profit calculation may occur, leading to incorrect performance fee withdrawals. -/// If the subVault has a manipulable convertToAssets function, and performance fees are desired, -/// consider whitelisting a specific FEE_MANAGER_ROLE that is allowed to call withdrawPerformanceFees(). -/// The fee manager is then trusted to not manipulate the subVault or be a victim of manipulation when withdrawing performance fees. -/// By default, only the owner has the FEE_MANAGER_ROLE. +/// - convertToAssets and convertToShares must not be manipulable contract MasterVault is Initializable, ERC4626Upgradeable, AccessControlUpgradeable, PausableUpgradeable { using SafeERC20 for IERC20; using MathUpgradeable for uint256; @@ -39,10 +34,6 @@ contract MasterVault is Initializable, ERC4626Upgradeable, AccessControlUpgradea /// @notice Vault manager role can set/revoke subvaults, toggle performance fees and set the performance fee beneficiary /// @dev Should never be granted to the zero address bytes32 public constant VAULT_MANAGER_ROLE = keccak256("VAULT_MANAGER_ROLE"); - /// @notice Fee manager role can call withdrawPerformanceFees() - /// @dev It is important that the convertToAssets function of the subVault is not manipulated prior to calling withdrawPerformanceFees(). - /// See contract notice for more details. - bytes32 public constant FEE_MANAGER_ROLE = keccak256("FEE_MANAGER_ROLE"); /// @notice Pauser role can pause/unpause deposits and withdrawals (todo: pause should pause EVERYTHING) bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); @@ -86,12 +77,10 @@ contract MasterVault is Initializable, ERC4626Upgradeable, AccessControlUpgradea __Pausable_init(); _setRoleAdmin(VAULT_MANAGER_ROLE, DEFAULT_ADMIN_ROLE); - _setRoleAdmin(FEE_MANAGER_ROLE, DEFAULT_ADMIN_ROLE); _setRoleAdmin(PAUSER_ROLE, DEFAULT_ADMIN_ROLE); _grantRole(DEFAULT_ADMIN_ROLE, _owner); _grantRole(VAULT_MANAGER_ROLE, _owner); - _grantRole(FEE_MANAGER_ROLE, _owner); _grantRole(PAUSER_ROLE, _owner); } @@ -156,47 +145,21 @@ contract MasterVault is Initializable, ERC4626Upgradeable, AccessControlUpgradea emit SubvaultChanged(address(oldSubVault), address(0)); } - function masterSharesToSubShares(uint256 masterShares, MathUpgradeable.Rounding rounding) public view returns (uint256) { - // masterShares * totalSubVaultShares / totalMasterShares - return masterShares.mulDiv(subVault.balanceOf(address(this)), totalSupply(), rounding); - } - - function subSharesToMasterShares(uint256 subShares, MathUpgradeable.Rounding rounding) public view returns (uint256) { - // subShares * totalMasterShares / totalSubVaultShares - return subShares.mulDiv(totalSupply(), subVault.balanceOf(address(this)), rounding); - } - /// @notice Toggle performance fee collection on/off /// @param enabled True to enable performance fees, false to disable function setPerformanceFee(bool enabled) external onlyRole(VAULT_MANAGER_ROLE) { - enablePerformanceFee = enabled; + enablePerformanceFee = enabled; // todo: this should set totalPrincipal to current totalAssets() to avoid sudden fee realization emit PerformanceFeeToggled(enabled); } /// @notice Set the beneficiary address for performance fees /// @param newBeneficiary Address to receive performance fees, zero address defaults to owner - function setBeneficiary(address newBeneficiary) external onlyRole(FEE_MANAGER_ROLE) { + function setBeneficiary(address newBeneficiary) external onlyRole(VAULT_MANAGER_ROLE) { address oldBeneficiary = beneficiary; beneficiary = newBeneficiary; emit BeneficiaryUpdated(oldBeneficiary, newBeneficiary); } - /// @notice Withdraw all accumulated performance fees to beneficiary - /// @dev Only callable by fee manager when performance fees are enabled - function withdrawPerformanceFees() external onlyRole(FEE_MANAGER_ROLE) { - if (!enablePerformanceFee) revert PerformanceFeeDisabled(); - if (beneficiary == address(0)) revert BeneficiaryNotSet(); - - uint256 totalProfits = totalProfit(); - if (totalProfits > 0) { - IERC4626 _subVault = subVault; - if (address(_subVault) != address(0)) { - _subVault.withdraw(totalProfits, address(this), address(this)); - } - IERC20(asset()).safeTransfer(beneficiary, totalProfits); - } - } - function pause() external onlyRole(PAUSER_ROLE) { _pause(); } @@ -222,17 +185,17 @@ contract MasterVault is Initializable, ERC4626Upgradeable, AccessControlUpgradea return subVault.maxDeposit(address(this)); } - /** @dev See {IERC4626-maxMint}. */ - function maxMint(address) public view virtual override returns (uint256) { - if (address(subVault) == address(0)) { - return type(uint256).max; - } - uint256 subShares = subVault.maxMint(address(this)); - if (subShares == type(uint256).max) { - return type(uint256).max; - } - return subSharesToMasterShares(subShares, MathUpgradeable.Rounding.Down); - } + // /** @dev See {IERC4626-maxMint}. */ + // function maxMint(address) public view virtual override returns (uint256) { + // if (address(subVault) == address(0)) { + // return type(uint256).max; + // } + // uint256 subShares = subVault.maxMint(address(this)); + // if (subShares == type(uint256).max) { + // return type(uint256).max; + // } + // return subSharesToMasterShares(subShares, MathUpgradeable.Rounding.Down); + // } /** * @dev Internal conversion function (from assets to shares) with support for rounding direction. @@ -242,11 +205,26 @@ contract MasterVault is Initializable, ERC4626Upgradeable, AccessControlUpgradea */ function _convertToShares(uint256 assets, MathUpgradeable.Rounding rounding) internal view virtual override returns (uint256 shares) { IERC4626 _subVault = subVault; + uint256 _totalAssets = totalAssets(); + uint256 _totalPrincipal = totalPrincipal; + uint256 _totalSupply = totalSupply(); + if (address(_subVault) == address(0)) { - return super._convertToShares(assets, rounding); + uint256 effectiveTotalAssets = enablePerformanceFee ? _min(_totalAssets, _totalPrincipal) : _totalAssets; + return _totalSupply.mulDiv(assets, effectiveTotalAssets, rounding); + } + + uint256 subShares = _convertToSubShares(_subVault, assets, rounding); + uint256 totalSubShares = _subVault.balanceOf(address(this)); + uint256 profit = _totalAssets > _totalPrincipal ? _totalAssets - _totalPrincipal : 0; + + if (enablePerformanceFee) { + // subSharesFee is the amount of subVault shares set aside as performance fee + uint256 subSharesFee = _convertToSubShares(_subVault, profit, rounding); + totalSubShares -= subSharesFee; } - uint256 subShares = rounding == MathUpgradeable.Rounding.Up ? _subVault.previewWithdraw(assets) : _subVault.previewDeposit(assets); - return subSharesToMasterShares(subShares, rounding); + + return _totalSupply.mulDiv(subShares, totalSubShares, rounding); } /** @@ -254,13 +232,43 @@ contract MasterVault is Initializable, ERC4626Upgradeable, AccessControlUpgradea */ function _convertToAssets(uint256 shares, MathUpgradeable.Rounding rounding) internal view virtual override returns (uint256 assets) { IERC4626 _subVault = subVault; + uint256 _totalAssets = totalAssets(); + uint256 _totalPrincipal = totalPrincipal; + uint256 _totalSupply = totalSupply(); + if (address(_subVault) == address(0)) { - return super._convertToAssets(shares, rounding); + uint256 effectiveTotalAssets = enablePerformanceFee ? _min(_totalAssets, _totalPrincipal) : _totalAssets; + return effectiveTotalAssets.mulDiv(shares, _totalSupply, rounding); + } + + uint256 totalSubShares = _subVault.balanceOf(address(this)); + uint256 profit = _totalAssets > _totalPrincipal ? _totalAssets - _totalPrincipal : 0; + + if (profit > 0 && enablePerformanceFee) { + // subSharesFee is the amount of subVault shares set aside as performance fee + uint256 subSharesFee = _convertToSubShares(_subVault, profit, rounding == MathUpgradeable.Rounding.Up ? MathUpgradeable.Rounding.Down : MathUpgradeable.Rounding.Up); + totalSubShares -= subSharesFee; } - uint256 subShares = masterSharesToSubShares(shares, rounding); + + // totalSubShares * shares / totalMasterShares + uint256 subShares = totalSubShares.mulDiv(shares, _totalSupply, rounding); + + return _convertToSubAssets(_subVault, subShares, rounding); + } + + function _convertToSubShares(IERC4626 _subVault, uint256 assets, MathUpgradeable.Rounding rounding) internal view returns (uint256 subShares) { + return rounding == MathUpgradeable.Rounding.Up ? _subVault.previewWithdraw(assets) : _subVault.previewDeposit(assets); + } + + function _convertToSubAssets(IERC4626 _subVault, uint256 subShares, MathUpgradeable.Rounding rounding) internal view returns (uint256 assets) { return rounding == MathUpgradeable.Rounding.Up ? _subVault.previewMint(subShares) : _subVault.previewRedeem(subShares); } + function _min(uint256 a, uint256 b) internal pure returns (uint256) { + return a <= b ? a : b; + } + + function totalProfit() public view returns (uint256) { uint256 _totalAssets = totalAssets(); return _totalAssets > totalPrincipal ? _totalAssets - totalPrincipal : 0; From cb727592f946090a20e598eaebe05bbb4d477fd1 Mon Sep 17 00:00:00 2001 From: Henry <11198460+godzillaba@users.noreply.github.com> Date: Mon, 1 Dec 2025 16:28:47 -0600 Subject: [PATCH 2/4] wip continuous accounting --- .../libraries/vault/MasterVault.sol | 64 ++++++++++++------- 1 file changed, 42 insertions(+), 22 deletions(-) diff --git a/contracts/tokenbridge/libraries/vault/MasterVault.sol b/contracts/tokenbridge/libraries/vault/MasterVault.sol index ba2fcb37d..547ce9420 100644 --- a/contracts/tokenbridge/libraries/vault/MasterVault.sol +++ b/contracts/tokenbridge/libraries/vault/MasterVault.sol @@ -171,7 +171,7 @@ contract MasterVault is Initializable, ERC4626Upgradeable, AccessControlUpgradea function totalAssets() public view virtual override returns (uint256) { IERC4626 _subVault = subVault; if (address(_subVault) == address(0)) { - return super.totalAssets(); + return IERC20(asset()).balanceOf(address(this)); } return _subVault.convertToAssets(_subVault.balanceOf(address(this))); } @@ -204,23 +204,21 @@ contract MasterVault is Initializable, ERC4626Upgradeable, AccessControlUpgradea */ function _convertToShares(uint256 assets, MathUpgradeable.Rounding rounding) internal view virtual override returns (uint256 shares) { IERC4626 _subVault = subVault; - uint256 _totalAssets = totalAssets(); uint256 _totalPrincipal = totalPrincipal; uint256 _totalSupply = totalSupply(); if (address(_subVault) == address(0)) { - uint256 effectiveTotalAssets = enablePerformanceFee ? _min(_totalAssets, _totalPrincipal) : _totalAssets; + uint256 effectiveTotalAssets = enablePerformanceFee ? _min(totalAssets(), _totalPrincipal) : totalAssets(); return _totalSupply.mulDiv(assets, effectiveTotalAssets, rounding); } - uint256 subShares = _convertToSubShares(_subVault, assets, rounding); + uint256 subShares = _assetsToSubVaultShares(_subVault, assets, rounding); uint256 totalSubShares = _subVault.balanceOf(address(this)); - uint256 profit = _totalAssets > _totalPrincipal ? _totalAssets - _totalPrincipal : 0; if (enablePerformanceFee) { - // subSharesFee is the amount of subVault shares set aside as performance fee - uint256 subSharesFee = _convertToSubShares(_subVault, profit, rounding); - totalSubShares -= subSharesFee; + // since we use totalSubShares in the denominator of the final calculation, + // and we are subtracting profit from it, we should use the same rounding direction for profit + totalSubShares -= totalProfitInSubVaultShares(_flipRounding(rounding)); } return _totalSupply.mulDiv(subShares, totalSubShares, rounding); @@ -231,35 +229,34 @@ contract MasterVault is Initializable, ERC4626Upgradeable, AccessControlUpgradea */ function _convertToAssets(uint256 shares, MathUpgradeable.Rounding rounding) internal view virtual override returns (uint256 assets) { IERC4626 _subVault = subVault; - uint256 _totalAssets = totalAssets(); uint256 _totalPrincipal = totalPrincipal; uint256 _totalSupply = totalSupply(); + // if we have no subvault, we just do normal pro-rata calculation if (address(_subVault) == address(0)) { - uint256 effectiveTotalAssets = enablePerformanceFee ? _min(_totalAssets, _totalPrincipal) : _totalAssets; + uint256 effectiveTotalAssets = enablePerformanceFee ? _min(totalAssets(), _totalPrincipal) : totalAssets(); return effectiveTotalAssets.mulDiv(shares, _totalSupply, rounding); } - + uint256 totalSubShares = _subVault.balanceOf(address(this)); - uint256 profit = _totalAssets > _totalPrincipal ? _totalAssets - _totalPrincipal : 0; - if (profit > 0 && enablePerformanceFee) { - // subSharesFee is the amount of subVault shares set aside as performance fee - uint256 subSharesFee = _convertToSubShares(_subVault, profit, rounding == MathUpgradeable.Rounding.Up ? MathUpgradeable.Rounding.Down : MathUpgradeable.Rounding.Up); - totalSubShares -= subSharesFee; + if (enablePerformanceFee) { + // since we use totalSubShares in the numerator of the final calculation, + // and we are subtracting profit from it, we should use the opposite rounding direction for profit + totalSubShares -= totalProfitInSubVaultShares(_flipRounding(rounding)); } // totalSubShares * shares / totalMasterShares uint256 subShares = totalSubShares.mulDiv(shares, _totalSupply, rounding); - return _convertToSubAssets(_subVault, subShares, rounding); + return _subVaultSharesToAssets(_subVault, subShares, rounding); } - function _convertToSubShares(IERC4626 _subVault, uint256 assets, MathUpgradeable.Rounding rounding) internal view returns (uint256 subShares) { + function _assetsToSubVaultShares(IERC4626 _subVault, uint256 assets, MathUpgradeable.Rounding rounding) internal view returns (uint256 subShares) { return rounding == MathUpgradeable.Rounding.Up ? _subVault.previewWithdraw(assets) : _subVault.previewDeposit(assets); } - function _convertToSubAssets(IERC4626 _subVault, uint256 subShares, MathUpgradeable.Rounding rounding) internal view returns (uint256 assets) { + function _subVaultSharesToAssets(IERC4626 _subVault, uint256 subShares, MathUpgradeable.Rounding rounding) internal view returns (uint256 assets) { return rounding == MathUpgradeable.Rounding.Up ? _subVault.previewMint(subShares) : _subVault.previewRedeem(subShares); } @@ -267,10 +264,33 @@ contract MasterVault is Initializable, ERC4626Upgradeable, AccessControlUpgradea return a <= b ? a : b; } + function _flipRounding(MathUpgradeable.Rounding rounding) internal pure returns (MathUpgradeable.Rounding) { + return rounding == MathUpgradeable.Rounding.Up ? MathUpgradeable.Rounding.Down : MathUpgradeable.Rounding.Up; + } + + + function totalProfit(MathUpgradeable.Rounding rounding) public view returns (uint256) { + IERC4626 _subVault = subVault; + if (address(_subVault) == address(0)) { + uint256 _tokenBalance = IERC20(asset()).balanceOf(address(this)); + return _tokenBalance > totalPrincipal ? _tokenBalance - totalPrincipal : 0; + } + uint256 totalSubShares = _subVault.balanceOf(address(this)); + uint256 _totalAssets = _subVaultSharesToAssets(_subVault, totalSubShares, rounding); + uint256 _totalPrincipal = totalPrincipal; + return _totalAssets > _totalPrincipal ? _totalAssets - _totalPrincipal : 0; + } - function totalProfit() public view returns (uint256) { - uint256 _totalAssets = totalAssets(); - return _totalAssets > totalPrincipal ? _totalAssets - totalPrincipal : 0; + function totalProfitInSubVaultShares(MathUpgradeable.Rounding rounding) public view returns (uint256) { + IERC4626 _subVault = subVault; + if (address(_subVault) == address(0)) { + revert("Subvault not set"); + } + uint256 profitAssets = totalProfit(rounding); + if (profitAssets == 0) { + return 0; + } + return _assetsToSubVaultShares(_subVault, profitAssets, rounding); } /** From fe0d73bb6bc5676af4c9d291ceb2ca796e7d9847 Mon Sep 17 00:00:00 2001 From: Henry <11198460+godzillaba@users.noreply.github.com> Date: Wed, 3 Dec 2025 17:29:18 -0600 Subject: [PATCH 3/4] finish continuous accounting --- .../libraries/vault/MasterVault.sol | 125 +++++++----------- 1 file changed, 49 insertions(+), 76 deletions(-) diff --git a/contracts/tokenbridge/libraries/vault/MasterVault.sol b/contracts/tokenbridge/libraries/vault/MasterVault.sol index 547ce9420..e30bef783 100644 --- a/contracts/tokenbridge/libraries/vault/MasterVault.sol +++ b/contracts/tokenbridge/libraries/vault/MasterVault.sol @@ -83,31 +83,6 @@ contract MasterVault is Initializable, ERC4626Upgradeable, AccessControlUpgradea _grantRole(PAUSER_ROLE, _owner); } - - function deposit(uint256 assets, address receiver, uint256 minSharesMinted) public returns (uint256) { - uint256 shares = deposit(assets, receiver); - if (shares < minSharesMinted) revert TooFewSharesReceived(); - return shares; - } - - function withdraw(uint256 assets, address receiver, address _owner, uint256 maxSharesBurned) public returns (uint256) { - uint256 shares = withdraw(assets, receiver, _owner); - if (shares > maxSharesBurned) revert TooManySharesBurned(); - return shares; - } - - function mint(uint256 shares, address receiver, uint256 maxAssetsDeposited) public returns (uint256) { - uint256 assets = super.mint(shares, receiver); - if (assets > maxAssetsDeposited) revert TooManyAssetsDeposited(); - return assets; - } - - function redeem(uint256 shares, address receiver, address _owner, uint256 minAssetsReceived) public returns (uint256) { - uint256 assets = super.redeem(shares, receiver, _owner); - if (assets < minAssetsReceived) revert TooFewAssetsReceived(); - return assets; - } - /// @notice Set a subvault. Can only be called if there is not already a subvault set. /// @param _subVault The subvault to set. Must be an ERC4626 vault with the same asset as this MasterVault. /// @param minSubVaultExchRateWad Minimum acceptable ratio (times 1e18) of new subvault shares to outstanding MasterVault shares after deposit. @@ -147,7 +122,12 @@ contract MasterVault is Initializable, ERC4626Upgradeable, AccessControlUpgradea /// @notice Toggle performance fee collection on/off /// @param enabled True to enable performance fees, false to disable function setPerformanceFee(bool enabled) external onlyRole(VAULT_MANAGER_ROLE) { - enablePerformanceFee = enabled; // todo: this should set totalPrincipal to current totalAssets() to avoid sudden fee realization + enablePerformanceFee = enabled; + // reset totalPrincipal to current totalAssets when enabling performance fee + // this prevents a sudden large profit + if (enabled) { + totalPrincipal = _totalAssets(MathUpgradeable.Rounding.Up); + } emit PerformanceFeeToggled(enabled); } @@ -169,11 +149,14 @@ contract MasterVault is Initializable, ERC4626Upgradeable, AccessControlUpgradea /** @dev See {IERC4626-totalAssets}. */ function totalAssets() public view virtual override returns (uint256) { - IERC4626 _subVault = subVault; - if (address(_subVault) == address(0)) { + return _totalAssets(MathUpgradeable.Rounding.Down); + } + + function _totalAssets(MathUpgradeable.Rounding rounding) internal view returns (uint256) { + if (address(subVault) == address(0)) { return IERC20(asset()).balanceOf(address(this)); } - return _subVault.convertToAssets(_subVault.balanceOf(address(this))); + return _subVaultSharesToAssets(subVault.balanceOf(address(this)), rounding); } /** @dev See {IERC4626-maxDeposit}. */ @@ -185,16 +168,16 @@ contract MasterVault is Initializable, ERC4626Upgradeable, AccessControlUpgradea } // /** @dev See {IERC4626-maxMint}. */ - // function maxMint(address) public view virtual override returns (uint256) { - // if (address(subVault) == address(0)) { - // return type(uint256).max; - // } - // uint256 subShares = subVault.maxMint(address(this)); - // if (subShares == type(uint256).max) { - // return type(uint256).max; - // } - // return subSharesToMasterShares(subShares, MathUpgradeable.Rounding.Down); - // } + function maxMint(address) public view virtual override returns (uint256) { + if (address(subVault) == address(0)) { + return type(uint256).max; + } + uint256 subShares = subVault.maxMint(address(this)); + if (subShares == type(uint256).max) { + return type(uint256).max; + } + return totalSupply().mulDiv(subShares, subVault.balanceOf(address(this)), MathUpgradeable.Rounding.Down); // todo: check rounding direction + } /** * @dev Internal conversion function (from assets to shares) with support for rounding direction. @@ -203,42 +186,35 @@ contract MasterVault is Initializable, ERC4626Upgradeable, AccessControlUpgradea * would represent an infinite amount of shares. */ function _convertToShares(uint256 assets, MathUpgradeable.Rounding rounding) internal view virtual override returns (uint256 shares) { - IERC4626 _subVault = subVault; - uint256 _totalPrincipal = totalPrincipal; - uint256 _totalSupply = totalSupply(); - - if (address(_subVault) == address(0)) { - uint256 effectiveTotalAssets = enablePerformanceFee ? _min(totalAssets(), _totalPrincipal) : totalAssets(); - return _totalSupply.mulDiv(assets, effectiveTotalAssets, rounding); + if (address(subVault) == address(0)) { + uint256 effectiveTotalAssets = enablePerformanceFee ? _min(totalAssets(), totalPrincipal) : totalAssets(); + return totalSupply().mulDiv(assets, effectiveTotalAssets, rounding); } - uint256 subShares = _assetsToSubVaultShares(_subVault, assets, rounding); - uint256 totalSubShares = _subVault.balanceOf(address(this)); + uint256 totalSubShares = subVault.balanceOf(address(this)); if (enablePerformanceFee) { // since we use totalSubShares in the denominator of the final calculation, // and we are subtracting profit from it, we should use the same rounding direction for profit totalSubShares -= totalProfitInSubVaultShares(_flipRounding(rounding)); } + + uint256 subShares = _assetsToSubVaultShares(assets, rounding); - return _totalSupply.mulDiv(subShares, totalSubShares, rounding); + return totalSupply().mulDiv(subShares, totalSubShares, rounding); } /** * @dev Internal conversion function (from shares to assets) with support for rounding direction. */ function _convertToAssets(uint256 shares, MathUpgradeable.Rounding rounding) internal view virtual override returns (uint256 assets) { - IERC4626 _subVault = subVault; - uint256 _totalPrincipal = totalPrincipal; - uint256 _totalSupply = totalSupply(); - // if we have no subvault, we just do normal pro-rata calculation - if (address(_subVault) == address(0)) { - uint256 effectiveTotalAssets = enablePerformanceFee ? _min(totalAssets(), _totalPrincipal) : totalAssets(); - return effectiveTotalAssets.mulDiv(shares, _totalSupply, rounding); + if (address(subVault) == address(0)) { + uint256 effectiveTotalAssets = enablePerformanceFee ? _min(totalAssets(), totalPrincipal) : totalAssets(); + return effectiveTotalAssets.mulDiv(shares, totalSupply(), rounding); } - uint256 totalSubShares = _subVault.balanceOf(address(this)); + uint256 totalSubShares = subVault.balanceOf(address(this)); if (enablePerformanceFee) { // since we use totalSubShares in the numerator of the final calculation, @@ -247,17 +223,17 @@ contract MasterVault is Initializable, ERC4626Upgradeable, AccessControlUpgradea } // totalSubShares * shares / totalMasterShares - uint256 subShares = totalSubShares.mulDiv(shares, _totalSupply, rounding); + uint256 subShares = totalSubShares.mulDiv(shares, totalSupply(), rounding); - return _subVaultSharesToAssets(_subVault, subShares, rounding); + return _subVaultSharesToAssets(subShares, rounding); } - function _assetsToSubVaultShares(IERC4626 _subVault, uint256 assets, MathUpgradeable.Rounding rounding) internal view returns (uint256 subShares) { - return rounding == MathUpgradeable.Rounding.Up ? _subVault.previewWithdraw(assets) : _subVault.previewDeposit(assets); + function _assetsToSubVaultShares(uint256 assets, MathUpgradeable.Rounding rounding) internal view returns (uint256 subShares) { + return rounding == MathUpgradeable.Rounding.Up ? subVault.previewWithdraw(assets) : subVault.previewDeposit(assets); } - function _subVaultSharesToAssets(IERC4626 _subVault, uint256 subShares, MathUpgradeable.Rounding rounding) internal view returns (uint256 assets) { - return rounding == MathUpgradeable.Rounding.Up ? _subVault.previewMint(subShares) : _subVault.previewRedeem(subShares); + function _subVaultSharesToAssets(uint256 subShares, MathUpgradeable.Rounding rounding) internal view returns (uint256 assets) { + return rounding == MathUpgradeable.Rounding.Up ? subVault.previewMint(subShares) : subVault.previewRedeem(subShares); } function _min(uint256 a, uint256 b) internal pure returns (uint256) { @@ -268,29 +244,20 @@ contract MasterVault is Initializable, ERC4626Upgradeable, AccessControlUpgradea return rounding == MathUpgradeable.Rounding.Up ? MathUpgradeable.Rounding.Down : MathUpgradeable.Rounding.Up; } - function totalProfit(MathUpgradeable.Rounding rounding) public view returns (uint256) { - IERC4626 _subVault = subVault; - if (address(_subVault) == address(0)) { - uint256 _tokenBalance = IERC20(asset()).balanceOf(address(this)); - return _tokenBalance > totalPrincipal ? _tokenBalance - totalPrincipal : 0; - } - uint256 totalSubShares = _subVault.balanceOf(address(this)); - uint256 _totalAssets = _subVaultSharesToAssets(_subVault, totalSubShares, rounding); - uint256 _totalPrincipal = totalPrincipal; - return _totalAssets > _totalPrincipal ? _totalAssets - _totalPrincipal : 0; + uint256 __totalAssets = _totalAssets(rounding); + return __totalAssets > totalPrincipal ? __totalAssets - totalPrincipal : 0; } function totalProfitInSubVaultShares(MathUpgradeable.Rounding rounding) public view returns (uint256) { - IERC4626 _subVault = subVault; - if (address(_subVault) == address(0)) { + if (address(subVault) == address(0)) { revert("Subvault not set"); } uint256 profitAssets = totalProfit(rounding); if (profitAssets == 0) { return 0; } - return _assetsToSubVaultShares(_subVault, profitAssets, rounding); + return _assetsToSubVaultShares(profitAssets, rounding); } /** @@ -330,4 +297,10 @@ contract MasterVault is Initializable, ERC4626Upgradeable, AccessControlUpgradea super._withdraw(caller, receiver, _owner, assets, shares); } + + function distributePerformanceFee() external whenNotPaused { + if (!enablePerformanceFee) revert PerformanceFeeDisabled(); + subVault.redeem(totalProfitInSubVaultShares(MathUpgradeable.Rounding.Down), beneficiary, address(this)); + // todo emit event + } } \ No newline at end of file From bc27b407b249fee710ed2e09d95ea0315d15fb8e Mon Sep 17 00:00:00 2001 From: Henry <11198460+godzillaba@users.noreply.github.com> Date: Fri, 5 Dec 2025 15:17:20 -0600 Subject: [PATCH 4/4] rearrange --- .../libraries/vault/MasterVault.sol | 134 +++++++++--------- 1 file changed, 67 insertions(+), 67 deletions(-) diff --git a/contracts/tokenbridge/libraries/vault/MasterVault.sol b/contracts/tokenbridge/libraries/vault/MasterVault.sol index e30bef783..2a2d14178 100644 --- a/contracts/tokenbridge/libraries/vault/MasterVault.sol +++ b/contracts/tokenbridge/libraries/vault/MasterVault.sol @@ -83,6 +83,12 @@ contract MasterVault is Initializable, ERC4626Upgradeable, AccessControlUpgradea _grantRole(PAUSER_ROLE, _owner); } + function distributePerformanceFee() external whenNotPaused { + if (!enablePerformanceFee) revert PerformanceFeeDisabled(); + subVault.redeem(totalProfitInSubVaultShares(MathUpgradeable.Rounding.Down), beneficiary, address(this)); + // todo emit event + } + /// @notice Set a subvault. Can only be called if there is not already a subvault set. /// @param _subVault The subvault to set. Must be an ERC4626 vault with the same asset as this MasterVault. /// @param minSubVaultExchRateWad Minimum acceptable ratio (times 1e18) of new subvault shares to outstanding MasterVault shares after deposit. @@ -152,13 +158,6 @@ contract MasterVault is Initializable, ERC4626Upgradeable, AccessControlUpgradea return _totalAssets(MathUpgradeable.Rounding.Down); } - function _totalAssets(MathUpgradeable.Rounding rounding) internal view returns (uint256) { - if (address(subVault) == address(0)) { - return IERC20(asset()).balanceOf(address(this)); - } - return _subVaultSharesToAssets(subVault.balanceOf(address(this)), rounding); - } - /** @dev See {IERC4626-maxDeposit}. */ function maxDeposit(address) public view virtual override returns (uint256) { if (address(subVault) == address(0)) { @@ -179,6 +178,67 @@ contract MasterVault is Initializable, ERC4626Upgradeable, AccessControlUpgradea return totalSupply().mulDiv(subShares, subVault.balanceOf(address(this)), MathUpgradeable.Rounding.Down); // todo: check rounding direction } + function totalProfit(MathUpgradeable.Rounding rounding) public view returns (uint256) { + uint256 __totalAssets = _totalAssets(rounding); + return __totalAssets > totalPrincipal ? __totalAssets - totalPrincipal : 0; + } + + function totalProfitInSubVaultShares(MathUpgradeable.Rounding rounding) public view returns (uint256) { + if (address(subVault) == address(0)) { + revert("Subvault not set"); + } + uint256 profitAssets = totalProfit(rounding); + if (profitAssets == 0) { + return 0; + } + return _assetsToSubVaultShares(profitAssets, rounding); + } + + /** + * @dev Deposit/mint common workflow. + */ + function _deposit( + address caller, + address receiver, + uint256 assets, + uint256 shares + ) internal virtual override whenNotPaused { + super._deposit(caller, receiver, assets, shares); + + totalPrincipal += assets; + IERC4626 _subVault = subVault; + if (address(_subVault) != address(0)) { + _subVault.deposit(assets, address(this)); + } + } + + /** + * @dev Withdraw/redeem common workflow. + */ + function _withdraw( + address caller, + address receiver, + address _owner, + uint256 assets, + uint256 shares + ) internal virtual override whenNotPaused { + totalPrincipal -= assets; + + IERC4626 _subVault = subVault; + if (address(_subVault) != address(0)) { + _subVault.withdraw(assets, address(this), address(this)); + } + + super._withdraw(caller, receiver, _owner, assets, shares); + } + + function _totalAssets(MathUpgradeable.Rounding rounding) internal view returns (uint256) { + if (address(subVault) == address(0)) { + return IERC20(asset()).balanceOf(address(this)); + } + return _subVaultSharesToAssets(subVault.balanceOf(address(this)), rounding); + } + /** * @dev Internal conversion function (from assets to shares) with support for rounding direction. * @@ -243,64 +303,4 @@ contract MasterVault is Initializable, ERC4626Upgradeable, AccessControlUpgradea function _flipRounding(MathUpgradeable.Rounding rounding) internal pure returns (MathUpgradeable.Rounding) { return rounding == MathUpgradeable.Rounding.Up ? MathUpgradeable.Rounding.Down : MathUpgradeable.Rounding.Up; } - - function totalProfit(MathUpgradeable.Rounding rounding) public view returns (uint256) { - uint256 __totalAssets = _totalAssets(rounding); - return __totalAssets > totalPrincipal ? __totalAssets - totalPrincipal : 0; - } - - function totalProfitInSubVaultShares(MathUpgradeable.Rounding rounding) public view returns (uint256) { - if (address(subVault) == address(0)) { - revert("Subvault not set"); - } - uint256 profitAssets = totalProfit(rounding); - if (profitAssets == 0) { - return 0; - } - return _assetsToSubVaultShares(profitAssets, rounding); - } - - /** - * @dev Deposit/mint common workflow. - */ - function _deposit( - address caller, - address receiver, - uint256 assets, - uint256 shares - ) internal virtual override whenNotPaused { - super._deposit(caller, receiver, assets, shares); - - totalPrincipal += assets; - IERC4626 _subVault = subVault; - if (address(_subVault) != address(0)) { - _subVault.deposit(assets, address(this)); - } - } - - /** - * @dev Withdraw/redeem common workflow. - */ - function _withdraw( - address caller, - address receiver, - address _owner, - uint256 assets, - uint256 shares - ) internal virtual override whenNotPaused { - totalPrincipal -= assets; - - IERC4626 _subVault = subVault; - if (address(_subVault) != address(0)) { - _subVault.withdraw(assets, address(this), address(this)); - } - - super._withdraw(caller, receiver, _owner, assets, shares); - } - - function distributePerformanceFee() external whenNotPaused { - if (!enablePerformanceFee) revert PerformanceFeeDisabled(); - subVault.redeem(totalProfitInSubVaultShares(MathUpgradeable.Rounding.Down), beneficiary, address(this)); - // todo emit event - } } \ No newline at end of file