Skip to content

Conversation

@waelsy123
Copy link
Contributor

@waelsy123 waelsy123 commented Dec 1, 2025

waelsy123 and others added 27 commits September 29, 2025 14:43
Make master vault upgradable
* feat: access control roles

* remove OwnableUpgradeable

* add note about permissionless fee manager

---------

Co-authored-by: Henry <[email protected]>
MasterVault: remove stored subvault exchange rate
Comment on lines +152 to +168
function sharePrice() public view returns (uint256) {
uint256 _totalAssets = totalAssets();
uint256 _totalSupply = totalSupply();

// todo: should we also consider _totalAssets == 0 case?
if (_totalSupply == 0 || _totalAssets == 0) {
return 1 * MULTIPLIER;
}

uint256 _sharePrice = MathUpgradeable.mulDiv(_totalAssets, MULTIPLIER, _totalSupply);

if (enablePerformanceFee) {
_sharePrice = MathUpgradeable.min(_sharePrice, 1e18);
}

return _sharePrice;
}

Check warning

Code scanning / Slither

Dangerous strict equalities Medium

Comment on lines 207 to 223
function _convertToAssets(
uint256 shares,
MathUpgradeable.Rounding rounding
) internal view virtual override returns (uint256) {
uint256 _totalAssets = totalAssets();
uint256 _totalSupply = totalSupply();
uint256 _effectiveAssets = enablePerformanceFee
? MathUpgradeable.min(_totalAssets, uint256(totalPrincipal))
: _totalAssets;

if (_totalSupply == 0) {
return 1;
}

uint256 _assets = MathUpgradeable.mulDiv(shares, _effectiveAssets, _totalSupply, rounding);
return _assets;
}

Check warning

Code scanning / Slither

Dangerous strict equalities Medium

Comment on lines 271 to 284
function revokeSubVault(uint256 minAssetExchRateWad) external onlyRole(VAULT_MANAGER_ROLE) {
IERC4626 oldSubVault = subVault;
if (address(oldSubVault) == address(0)) revert NoExistingSubVault();

subVault = IERC4626(address(0));

oldSubVault.redeem(oldSubVault.balanceOf(address(this)), address(this), address(this));
IERC20(asset()).safeApprove(address(oldSubVault), 0);

uint256 assetExchRateWad = MathUpgradeable.mulDiv(IERC20(asset()).balanceOf(address(this)), 1e18, totalSupply(), MathUpgradeable.Rounding.Down);
if (assetExchRateWad < minAssetExchRateWad) revert SubVaultExchangeRateTooLow();

emit SubvaultChanged(address(oldSubVault), address(0));
}

Check warning

Code scanning / Slither

Unused return Medium

Comment on lines +134 to +148
function withdrawPerformanceFees() external onlyRole(FEE_MANAGER_ROLE) {
if (!enablePerformanceFee) revert PerformanceFeeDisabled();
if (beneficiary == address(0)) revert BeneficiaryNotSet();

int256 _totalProfits = totalProfit();
if (_totalProfits > 0) {
if (address(subVault) == address(0)) {
SafeERC20.safeTransfer(IERC20(asset()), beneficiary, uint256(_totalProfits));
} else {
subVault.withdraw(uint256(_totalProfits), beneficiary, address(this));
}

emit PerformanceFeesWithdrawn(beneficiary, uint256(_totalProfits));
}
}

Check warning

Code scanning / Slither

Unused return Medium

Comment on lines +231 to +247
function _deposit(
address caller,
address receiver,
uint256 assets,
uint256 shares
) internal virtual override whenNotPaused {
super._deposit(caller, receiver, assets, shares);

if (address(subVault) != address(0)) {
IERC20 underlyingAsset = IERC20(asset());
// todo: should we deposit only users assets and account for trasnfer fee or keep depositing _idleAssets?
uint256 _idleAssets = underlyingAsset.balanceOf(address(this));
subVault.deposit(_idleAssets, address(this));
}

totalPrincipal += int256(assets);
}

Check warning

Code scanning / Slither

Unused return Medium

Comment on lines +250 to +264
function _withdraw(
address caller,
address receiver,
address owner,
uint256 assets,
uint256 shares
) internal virtual override whenNotPaused {
if (address(subVault) != address(0)) {
subVault.withdraw(assets, address(this), address(this));
}

// todo: account trasnfer fee? should we withdraw all? should we validate against users assets if transfer fee accure?
super._withdraw(caller, receiver, owner, assets, shares);
totalPrincipal -= int256(assets);
}

Check warning

Code scanning / Slither

Unused return Medium

Comment on lines +271 to +297
function setSubVault(
IERC4626 _subVault,
uint256 minSubVaultExchRateWad
) external onlyRole(VAULT_MANAGER_ROLE) {
IERC20 underlyingAsset = IERC20(asset());
if (address(subVault) != address(0)) revert SubVaultAlreadySet();
if (address(_subVault.asset()) != address(underlyingAsset)) revert SubVaultAssetMismatch();

subVault = _subVault;

IERC20(asset()).safeApprove(address(_subVault), type(uint256).max);
_subVault.deposit(underlyingAsset.balanceOf(address(this)), address(this));

uint256 _totalSupply = totalSupply();
if (_totalSupply > 0) {
uint256 subVaultExchRateWad = MathUpgradeable.mulDiv(
_subVault.balanceOf(address(this)),
1e18,
totalSupply(),
MathUpgradeable.Rounding.Down
);
if (subVaultExchRateWad < minSubVaultExchRateWad)
revert NewSubVaultExchangeRateTooLow();
}

emit SubvaultChanged(address(0), address(_subVault));
}

Check warning

Code scanning / Slither

Unused return Medium

Comment on lines +99 to +101
if (enabled) {
totalPrincipal = 0;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we're turning it on, and we set principal to 0, then wouldn't that make the vault entirely profit?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should set totalprincipal to totalAssets rounded up

Comment on lines +158 to +159
uint256 _totalAssets = totalAssets();
uint256 _totalSupply = totalSupply();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think when we use totalAssets to calculate the mastervault share price we could run into rounding trouble.

the subvault will have different rounding error than the calculation here. maybe this could lead to us issuing more mastervault shares than we should

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here's a POC that confirms that this rounding error can be exploited for profit. i believe we need to use the preview* functions on the subvault to implement the convertTo* functions on the mastervault.

here is the POC: 0f55ee5

#136 implements the convertTo* functions using the subvault's preview* functions so it shouldn't have this issue

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants