Skip to content

Conversation

waelsy123
Copy link
Contributor

@waelsy123 waelsy123 commented Oct 1, 2025

This convert MasterVault to be beacon upgradable

@cla-bot cla-bot bot added the cla-signed label Oct 1, 2025
@waelsy123 waelsy123 changed the base branch from main to wa/master-vault-isolated October 1, 2025 13:44
@waelsy123 waelsy123 force-pushed the ybb/upgradable-master-vault branch from b9d6f0d to 7922536 Compare October 1, 2025 13:45
if (token == address(0)) {
revert ZeroAddress();
}
if (address(beaconProxyFactory) == address(0)) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could check the initialized variable?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

will add extra condition to cover that

beacon = new UpgradeableBeacon(address(masterVaultImplementation));
beaconProxyFactory = new BeaconProxyFactory();
beaconProxyFactory.initialize(address(beacon));
beacon.transferOwnership(address(this));
Copy link
Collaborator

Choose a reason for hiding this comment

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

Isnt this contract already the owner? Should this be beacon.transferOwnership(_owner);?

I guess we should also add a test to show the beacon can be upgraded

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good catch yes that should be assigned to the owner

_transferOwnership(_owner);

MasterVault masterVaultImplementation = new MasterVault();
beacon = new UpgradeableBeacon(address(masterVaultImplementation));
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do you think we should deploy these with create2? Or is create fine?

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 don't have strong opinion on it but my philosophy is to keep simple if there is no need for something more

);

vault = Create2.deploy(0, bytes32(0), bytecode);
MasterVault(vault).vaultInit(IERC20(token), name, symbol, address(this));
Copy link
Collaborator

Choose a reason for hiding this comment

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

There a bunch of onlyOwner functions on the master vault which won't be callable - we only have setSubVault. Is that expected?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes that's expected. all of the onlyOwner calls are around:

  • switching between sub vaults.
  • managing perf fee (set, withdraw)

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 only want setSubVault to be callable we should remove the othere external owner functions

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it depends on whether we want to allow owner to switch or revoke already assigned subvault. if this feature isn't required then we can drop switchSubvault and revokeSubvault methods.

Copy link
Contributor

Choose a reason for hiding this comment

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

okay, let's table this for now but we might want to just give the upgradeexecutor ownership of the vaults themselves. that way we don't need to forward calls through the factory

maybe we can include that change in our inevitable roles based access control PR

Copy link
Contributor Author

Choose a reason for hiding this comment

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

agreed - thanks for flagging that, will keep note of it to handle this in the coming PRs related to ownerships

if (address(_subVault) != address(0)) {
_subVault.deposit(assets, address(this));
uint256 subShares = _subVault.deposit(assets, address(this));
if (subShares == 0) revert NoSubvaultShares();
Copy link
Contributor

Choose a reason for hiding this comment

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

what's the rationale behind this check?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it's just to satisfy slither as there is a rule to not have unused variables / unused returns

Copy link
Contributor

Choose a reason for hiding this comment

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

ok i think we can ignore slither

if (address(_subVault) != address(0)) {
_subVault.withdraw(assets, address(this), address(this));
uint256 sharesBurned = _subVault.withdraw(assets, address(this), address(this));
if (sharesBurned == 0) revert NoSharesBurned();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@godzillaba same here


__ERC20_init(_name, _symbol);
__ERC4626_init(IERC20Upgradeable(address(_asset)));
__Ownable_init();
Copy link
Contributor

Choose a reason for hiding this comment

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

_transferOwnership suffices

Suggested change
__Ownable_init();

Comment on lines 117 to 129
uint256 _totalAssets = totalAssets();
uint256 _totalSupply = totalSupply();

subVault = _subVault;

IERC20(asset()).safeApprove(address(_subVault), type(uint256).max);
uint256 subShares = _subVault.deposit(totalAssets(), address(this));
uint256 subShares = _subVault.deposit(_totalAssets, address(this));

uint256 _subVaultExchRateWad = subShares.mulDiv(1e18, totalSupply(), Math.Rounding.Down);
uint256 _subVaultExchRateWad = subShares.mulDiv(1e18, _totalSupply, MathUpgradeable.Rounding.Down);
if (_subVaultExchRateWad < minSubVaultExchRateWad) revert SubVaultExchangeRateTooLow();
subVaultExchRateWad = _subVaultExchRateWad;

subVault = _subVault;

emit SubvaultChanged(address(0), address(_subVault));
Copy link
Contributor

Choose a reason for hiding this comment

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

can we leave this alone except for changing Math to MathUpgradeable? what is the purpose of the changes?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this include a bug for a race condition which cause this line to fail:
uint256 _subVaultExchRateWad = subShares.mulDiv(1e18, totalSupply(), Math.Rounding.Down);

that's not related to the upgradability but the issue is that this line will always fail with error "division by zero" because of totalSupply() returns zero after (master vault deposited all funds into new subvault

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 think an easy fix would be to move subVault = _subVault; in between deposit and _subVaultExchRateWad calculation. just pushed a fix for that.

Comment on lines +137 to +147
uint256 maxWithdrawAmount = oldSubVault.maxWithdraw(address(this));

IERC20(asset()).safeApprove(address(oldSubVault), 0);
subVault = ERC4626(address(0));
subVault = IERC4626(address(0));
subVaultExchRateWad = 1e18;

uint256 assetReceived = oldSubVault.withdraw(maxWithdrawAmount, address(this), address(this));
IERC20(asset()).safeApprove(address(oldSubVault), 0);

uint256 effectiveAssetExchRateWad = assetReceived.mulDiv(1e18, _totalSupply, MathUpgradeable.Rounding.Down);
if (effectiveAssetExchRateWad < minAssetExchRateWad) revert TooFewAssetsReceived();

Copy link
Contributor

Choose a reason for hiding this comment

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

what is the purpose of the changes here?

Comment on lines 196 to 197
uint256 sharesRedeemed = _subVault.withdraw(totalProfits, address(this), address(this));
if (sharesRedeemed == 0) revert NoSharesRedeemed();
Copy link
Contributor

Choose a reason for hiding this comment

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

let's leave this as is since it's not relevant to upgradeability

Suggested change
uint256 sharesRedeemed = _subVault.withdraw(totalProfits, address(this), address(this));
if (sharesRedeemed == 0) revert NoSharesRedeemed();
_subVault.withdraw(totalProfits, address(this), address(this));

error ZeroAddress();
error BeaconNotDeployed();

UpgradeableBeacon public beacon;
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 not sure we need to store the beacon since the factory doesn't use it

event BeneficiaryUpdated(address indexed oldBeneficiary, address indexed newBeneficiary);

constructor(IERC20 _asset, string memory _name, string memory _symbol) ERC20(_name, _symbol) ERC4626(_asset) Ownable() {}
function vaultInit(IERC20 _asset, string memory _name, string memory _symbol, address _owner) external initializer {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit, for convention

Suggested change
function vaultInit(IERC20 _asset, string memory _name, string memory _symbol, address _owner) external initializer {
function initialize(IERC20 _asset, string memory _name, string memory _symbol, address _owner) external initializer {

);

vault = Create2.deploy(0, bytes32(0), bytecode);
MasterVault(vault).vaultInit(IERC20(token), name, symbol, 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.

okay, let's table this for now but we might want to just give the upgradeexecutor ownership of the vaults themselves. that way we don't need to forward calls through the factory

maybe we can include that change in our inevitable roles based access control PR

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]).

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