Skip to content

feat: add master vault#126

Open
waelsy123 wants to merge 130 commits intomainfrom
wa/master-vault-isolated
Open

feat: add master vault#126
waelsy123 wants to merge 130 commits intomainfrom
wa/master-vault-isolated

Conversation

@waelsy123
Copy link
Contributor

This isolate the implementation for the master vault and its related factory and subvault.

@cla-bot cla-bot bot added the cla-signed label Sep 29, 2025
@waelsy123 waelsy123 force-pushed the wa/master-vault-isolated branch from 590a713 to fc89964 Compare September 30, 2025 13:43
@godzillaba godzillaba marked this pull request as draft October 1, 2025 13:33
if (address(oldSubVault) == address(0)) revert NoExistingSubVault();

uint256 _totalSupply = totalSupply();
uint256 assetReceived = oldSubVault.withdraw(oldSubVault.maxWithdraw(address(this)), address(this), address(this));
Copy link
Contributor

Choose a reason for hiding this comment

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

there is an edge case here - the subvault may not have enough liquidity to serve this big withdrawal all at once.

we probably need to make switching vaults more robust to those liquidity constaints.

the same could be said about depositing to the new vault, it could be such a large deposit that slippage starts to become a serious issue

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 could do check whether maxWithdraw will return same amount of what master vault actually own

Copy link
Member

Choose a reason for hiding this comment

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

function withdraw(uint256 assets, address receiver, address owner) public returns (uint256 shares)

note ERC4626.withdraw returns share withdrawn not assetReceived

@waelsy123 waelsy123 force-pushed the wa/master-vault-isolated branch from fdd512f to e9d49e5 Compare November 6, 2025 14:11
waelsy123 and others added 8 commits November 7, 2025 09:35
* feat: access control roles

* remove OwnableUpgradeable

* add note about permissionless fee manager

---------

Co-authored-by: Henry <11198460+godzillaba@users.noreply.github.com>
MasterVault: remove stored subvault exchange rate
@waelsy123 waelsy123 marked this pull request as ready for review January 22, 2026 21:26
@waelsy123 waelsy123 requested a review from gzeoneth January 22, 2026 21:26
pragma solidity ^0.8.0;

interface IMasterVault {
function setSubVault(address subVault, uint256 minSubVaultExchRateWad) external;
Copy link
Member

Choose a reason for hiding this comment

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

wrong sig


/// @dev Overridden to add EXTRA_DECIMALS to the underlying asset decimals
function decimals() public view override returns (uint8) {
return IERC20Metadata(address(asset)).decimals() + EXTRA_DECIMALS;
Copy link
Member

Choose a reason for hiding this comment

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

nit: erc20 can have no 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.

if a token has no decimals method, can we safely assume it's 18 decimals or it would default to zero?

/// @dev Can only be called by the token bridge gateway
/// @param assets The amount of underlying assets to deposit
/// @return shares The amount of vault shares minted to the depositor
function deposit(uint256 assets)
Copy link
Member

Choose a reason for hiding this comment

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

lack slippage protection

Copy link
Contributor

Choose a reason for hiding this comment

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

slippage will be handled by the gateway, similar to #166

Comment on lines +265 to +266
uint256 totalAssetsUp = _totalAssetsLessProfit(MathUpgradeable.Rounding.Up);
uint256 totalAssetsDown = _totalAssetsLessProfit(MathUpgradeable.Rounding.Down);
Copy link
Member

Choose a reason for hiding this comment

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

maybe we should also allocate profit?

Copy link
Contributor

Choose a reason for hiding this comment

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

i'm indifferent, but i guess it is more capital efficient that way

asset.safeTransfer(beneficiary, amountToTransfer);
}
if (amountToWithdraw > 0) {
subVault.withdraw(amountToWithdraw, beneficiary, address(this));
Copy link
Member

Choose a reason for hiding this comment

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

this does not check withdrawal limit with preview, might revert, should allow partial withdrawal
same applies to other subvault withdraw

false, withdrawAmount, desiredWithdraw, minimumRebalanceAmount
);
}
subVault.withdraw(withdrawAmount, address(this), address(this));
Copy link
Member

Choose a reason for hiding this comment

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

dangerous no slippage protection rebalance, can be sandwiched

Copy link
Contributor

Choose a reason for hiding this comment

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

we used to have an exchange rate based slippage here. I'll reintroduce one

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I believe if we use exchange rate approach then keeper can't be permissionless

Copy link
Contributor

Choose a reason for hiding this comment

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

why not?

Copy link
Contributor

Choose a reason for hiding this comment

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

true, depositAmount, desiredDeposit, minimumRebalanceAmount
);
}
subVault.deposit(depositAmount, address(this));
Copy link
Member

Choose a reason for hiding this comment

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

dangerous no slippage protection rebalance, can be sandwiched

Comment on lines +268 to +317
function rebalance() external whenNotPaused nonReentrant onlyKeeper {
// Check cooldown
uint256 timeSinceLastRebalance = block.timestamp - lastRebalanceTime;
if (timeSinceLastRebalance < rebalanceCooldown) {
revert RebalanceCooldownNotMet(timeSinceLastRebalance, rebalanceCooldown);
}

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 = asset.balanceOf(address(this));

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

if (idleBalance < idleTargetDown) {
// we need to withdraw from subvault
uint256 desiredWithdraw = idleTargetDown - idleBalance;
uint256 maxWithdrawable = subVault.maxWithdraw(address(this));
uint256 withdrawAmount =
desiredWithdraw < maxWithdrawable ? desiredWithdraw : maxWithdrawable;
if (withdrawAmount < minimumRebalanceAmount) {
revert RebalanceAmountTooSmall(
false, withdrawAmount, desiredWithdraw, minimumRebalanceAmount
);
}
subVault.withdraw(withdrawAmount, address(this), address(this));
emit Rebalanced(false, desiredWithdraw, withdrawAmount);
} else {
// we need to deposit into subvault
uint256 desiredDeposit = idleBalance - idleTargetUp;
uint256 maxDepositable = subVault.maxDeposit(address(this));
uint256 depositAmount =
desiredDeposit < maxDepositable ? desiredDeposit : maxDepositable;
if (depositAmount < minimumRebalanceAmount) {
revert RebalanceAmountTooSmall(
true, depositAmount, desiredDeposit, minimumRebalanceAmount
);
}
asset.safeApprove(address(subVault), depositAmount);
subVault.deposit(depositAmount, address(this));
emit Rebalanced(true, desiredDeposit, depositAmount);
}

lastRebalanceTime = uint40(block.timestamp);
}

Check warning

Code scanning / Slither

Reentrancy vulnerabilities Medium

Comment on lines +268 to +317
function rebalance() external whenNotPaused nonReentrant onlyKeeper {
// Check cooldown
uint256 timeSinceLastRebalance = block.timestamp - lastRebalanceTime;
if (timeSinceLastRebalance < rebalanceCooldown) {
revert RebalanceCooldownNotMet(timeSinceLastRebalance, rebalanceCooldown);
}

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 = asset.balanceOf(address(this));

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

if (idleBalance < idleTargetDown) {
// we need to withdraw from subvault
uint256 desiredWithdraw = idleTargetDown - idleBalance;
uint256 maxWithdrawable = subVault.maxWithdraw(address(this));
uint256 withdrawAmount =
desiredWithdraw < maxWithdrawable ? desiredWithdraw : maxWithdrawable;
if (withdrawAmount < minimumRebalanceAmount) {
revert RebalanceAmountTooSmall(
false, withdrawAmount, desiredWithdraw, minimumRebalanceAmount
);
}
subVault.withdraw(withdrawAmount, address(this), address(this));
emit Rebalanced(false, desiredWithdraw, withdrawAmount);
} else {
// we need to deposit into subvault
uint256 desiredDeposit = idleBalance - idleTargetUp;
uint256 maxDepositable = subVault.maxDeposit(address(this));
uint256 depositAmount =
desiredDeposit < maxDepositable ? desiredDeposit : maxDepositable;
if (depositAmount < minimumRebalanceAmount) {
revert RebalanceAmountTooSmall(
true, depositAmount, desiredDeposit, minimumRebalanceAmount
);
}
asset.safeApprove(address(subVault), depositAmount);
subVault.deposit(depositAmount, address(this));
emit Rebalanced(true, desiredDeposit, depositAmount);
}

lastRebalanceTime = uint40(block.timestamp);
}

Check warning

Code scanning / Slither

Unused return Medium

Comment on lines +268 to +317
function rebalance() external whenNotPaused nonReentrant onlyKeeper {
// Check cooldown
uint256 timeSinceLastRebalance = block.timestamp - lastRebalanceTime;
if (timeSinceLastRebalance < rebalanceCooldown) {
revert RebalanceCooldownNotMet(timeSinceLastRebalance, rebalanceCooldown);
}

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 = asset.balanceOf(address(this));

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

if (idleBalance < idleTargetDown) {
// we need to withdraw from subvault
uint256 desiredWithdraw = idleTargetDown - idleBalance;
uint256 maxWithdrawable = subVault.maxWithdraw(address(this));
uint256 withdrawAmount =
desiredWithdraw < maxWithdrawable ? desiredWithdraw : maxWithdrawable;
if (withdrawAmount < minimumRebalanceAmount) {
revert RebalanceAmountTooSmall(
false, withdrawAmount, desiredWithdraw, minimumRebalanceAmount
);
}
subVault.withdraw(withdrawAmount, address(this), address(this));
emit Rebalanced(false, desiredWithdraw, withdrawAmount);
} else {
// we need to deposit into subvault
uint256 desiredDeposit = idleBalance - idleTargetUp;
uint256 maxDepositable = subVault.maxDeposit(address(this));
uint256 depositAmount =
desiredDeposit < maxDepositable ? desiredDeposit : maxDepositable;
if (depositAmount < minimumRebalanceAmount) {
revert RebalanceAmountTooSmall(
true, depositAmount, desiredDeposit, minimumRebalanceAmount
);
}
asset.safeApprove(address(subVault), depositAmount);
subVault.deposit(depositAmount, address(this));
emit Rebalanced(true, desiredDeposit, depositAmount);
}

lastRebalanceTime = uint40(block.timestamp);
}

Check warning

Code scanning / Slither

Unused return Medium

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants