Skip to content

Conversation

@godzillaba
Copy link
Contributor

@godzillaba godzillaba commented Dec 12, 2025

  • simpler convertTo* (more similar to the Ybb/simplified #137 way but should avoid the rounding issue)
  • decimals offset + dead shares + 1 virtual asset for first deposit mitigation
  • subvault is always set. switching requires 0 allocation

testing in progress, but the implementation should be solid unless testing uncovers issues

* wip: dead shares and fix div by zero

* doc

* add one
@godzillaba godzillaba requested a review from waelsy123 December 15, 2025 21:41
@godzillaba godzillaba marked this pull request as ready for review December 15, 2025 22:15
}

function deployVault(address token) public returns (address vault) {
if (token == address(0)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

do we actually need a vault with asset with zero address?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

no, i don't think we need to prevent it though

subVault.withdraw(assetsToWithdraw, address(this), address(this));
}

super._withdraw(caller, receiver, _owner, assets, shares);
Copy link
Contributor

Choose a reason for hiding this comment

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

can this fail silently?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

no

    function _withdraw(
        address caller,
        address receiver,
        address owner,
        uint256 assets,
        uint256 shares
    ) internal virtual {
        if (caller != owner) {
            _spendAllowance(owner, caller, shares);
        }

        // If _asset is ERC777, `transfer` can trigger a reentrancy AFTER the transfer happens through the
        // `tokensReceived` hook. On the other hand, the `tokensToSend` hook, that is triggered before the transfer,
        // calls the vault, which is assumed not malicious.
        //
        // Conclusion: we need to do the transfer after the burn so that any reentrancy would happen after the
        // shares are burned and after the assets are transferred, which is a valid state.
        _burn(owner, shares);
        SafeERC20Upgradeable.safeTransfer(_asset, receiver, assets);

        emit Withdraw(caller, receiver, owner, assets, shares);
    }

@waelsy123
Copy link
Contributor

looks solid implementation to me, we may need a bit of fuzz testing to capture edge cases


oldSubVault.redeem(oldSubVault.balanceOf(address(this)), address(this), address(this));
IERC20(asset()).safeApprove(address(oldSubVault), 0);
function rebalance() public {
Copy link
Contributor

Choose a reason for hiding this comment

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

since that this method is public, could be exploited by slippage by manipulating sub assets to shares ratio?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

hmm, maybe? what would be the exploit?

@sherlock-ai-beta
Copy link

Sherlock AI

Thank you for using Sherlock AI!

**Security Review Unavailable**: Your account doesn't have an active subscription.     To enable automated security reviews on your pull requests, please visit     [https://ai.sherlock.xyz](https://ai.sherlock.xyz) to set up your subscription.

Once activated, Sherlock AI will automatically analyze your code changes and provide     detailed security findings directly in your pull requests.

Need help? Reach out to us at [[email protected]](mailto:[email protected]).

Comment on lines +207 to +228
function _rebalance() internal {
uint256 totalAssetsUp = _totalAssetsLessProfit(MathUpgradeable.Rounding.Up);
uint256 totalAssetsDown = _totalAssetsLessProfit(MathUpgradeable.Rounding.Down);
uint256 idleTargetUp = totalAssetsUp.mulDiv(1e18 - targetAllocationWad, 1e18, MathUpgradeable.Rounding.Up);
uint256 idleTargetDown = totalAssetsDown.mulDiv(1e18 - targetAllocationWad, 1e18, MathUpgradeable.Rounding.Down);
uint256 idleBalance = IERC20(asset()).balanceOf(address(this));

if (idleTargetDown <= idleBalance && idleBalance <= idleTargetUp) {
return;
}

if (idleBalance < idleTargetDown) {
// we need to withdraw from subvault
uint256 assetsToWithdraw = idleTargetDown - idleBalance;
subVault.withdraw(assetsToWithdraw, address(this), address(this));
}
else {
// we need to deposit into subvault
uint256 assetsToDeposit = idleBalance - idleTargetUp;
subVault.deposit(assetsToDeposit, address(this));
}
}

Check warning

Code scanning / Slither

Unused return Medium

Comment on lines +207 to +228
function _rebalance() internal {
uint256 totalAssetsUp = _totalAssetsLessProfit(MathUpgradeable.Rounding.Up);
uint256 totalAssetsDown = _totalAssetsLessProfit(MathUpgradeable.Rounding.Down);
uint256 idleTargetUp = totalAssetsUp.mulDiv(1e18 - targetAllocationWad, 1e18, MathUpgradeable.Rounding.Up);
uint256 idleTargetDown = totalAssetsDown.mulDiv(1e18 - targetAllocationWad, 1e18, MathUpgradeable.Rounding.Down);
uint256 idleBalance = IERC20(asset()).balanceOf(address(this));

if (idleTargetDown <= idleBalance && idleBalance <= idleTargetUp) {
return;
}

if (idleBalance < idleTargetDown) {
// we need to withdraw from subvault
uint256 assetsToWithdraw = idleTargetDown - idleBalance;
subVault.withdraw(assetsToWithdraw, address(this), address(this));
}
else {
// we need to deposit into subvault
uint256 assetsToDeposit = idleBalance - idleTargetUp;
subVault.deposit(assetsToDeposit, address(this));
}
}

Check warning

Code scanning / Slither

Unused return Medium

/// @notice Extra decimals added to the ERC20 decimals of the underlying asset to determine the decimals of the MasterVault
/// @dev This is done to mitigate the "first depositor" problem described in the OpenZeppelin ERC4626 documentation.
/// See https://docs.openzeppelin.com/contracts/5.x/erc4626 for more details on the mitigation.
uint8 public constant EXTRA_DECIMALS = 18;
Copy link
Contributor

@waelsy123 waelsy123 Dec 18, 2025

Choose a reason for hiding this comment

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

inspired by morpho:

DECIMALS_OFFSET = uint8(uint256(18).zeroFloorSub(IERC20Metadata(_asset).decimals()));

where zeroFloorSub is max(0, x-y) = max(0, 18-asset.decimals)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

we don't really want an offset of 0, because then the mitigation no longer works

@godzillaba godzillaba requested a review from waelsy123 December 19, 2025 00:40
@godzillaba godzillaba changed the title WIP: MasterVault reserve MasterVault reserve Dec 22, 2025
@godzillaba godzillaba merged commit 4c3bda9 into ybb/migrate-tests Dec 23, 2025
3 of 9 checks passed
@godzillaba godzillaba deleted the ha/mastervault-reserve branch December 23, 2025 17:27
godzillaba added a commit that referenced this pull request Dec 23, 2025
* tests: core functionality

* handle total supply or total asset zero values

* fix mint method when zero totalAssets or totalSupply

* perf fees tests and fixes

* remove ds store

* MasterVault reserve (#141)

* wip

* wip: conversion and deposit/withdraw functions look okay

* settargetAllocationWad

* deploy with initial vault

* use factory in core test setup

* slippage tolerance on setTargetAllocationWad

* big simplify

* WIP: mastervault donation attack mitigation (#142)

* wip: dead shares and fix div by zero

* doc

* add one

* initial approval and small refactor

* tests

* fix PerformanceFeesWithdrawn event

* move event

* rebalancing does not count profit

* distribute fees before disabling

* fix maxMint

* fix outdated comment

* remove unused errors

* nonReentrant

* handle max in maxMint

* sanity check we have no sub shares when switching

* init reentrancy guard

* rebalance after setting target

* update docs

* update outdated comment

* fix nonReentrant modifier placement

* remove testFoo

* fix tests

---------

Co-authored-by: Henry <[email protected]>
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