Skip to content

Commit 7922536

Browse files
committed
feat: make master vault beacon upgradable
1 parent fc89964 commit 7922536

File tree

5 files changed

+94
-55
lines changed

5 files changed

+94
-55
lines changed

contracts/tokenbridge/libraries/vault/IMasterVault.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
pragma solidity ^0.8.0;
33

44
interface IMasterVault {
5-
function setSubVault(address subVault) external;
5+
function setSubVault(address subVault, uint256 minSubVaultExchRateWad) external;
66
}

contracts/tokenbridge/libraries/vault/IMasterVaultFactory.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ interface IMasterVaultFactory {
99
function deployVault(address token) external returns (address vault);
1010
function calculateVaultAddress(address token) external view returns (address);
1111
function getVault(address token) external returns (address);
12-
function setSubVault(address masterVault, address subVault) external;
12+
function setSubVault(address masterVault, address subVault, uint256 minSubVaultExchRateWad) external;
1313
}

contracts/tokenbridge/libraries/vault/MasterVault.sol

Lines changed: 47 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
// SPDX-License-Identifier: Apache-2.0
22
pragma solidity ^0.8.0;
33

4-
import {ERC4626} from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
5-
import { IERC20, ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
6-
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
4+
import {ERC4626Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol";
5+
import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
6+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
7+
import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol";
8+
import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol";
9+
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
10+
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
711
import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
8-
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
12+
import {MathUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/math/MathUpgradeable.sol";
913
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
1014

11-
contract MasterVault is ERC4626, Ownable {
15+
contract MasterVault is Initializable, ERC4626Upgradeable, OwnableUpgradeable {
1216
using SafeERC20 for IERC20;
13-
using Math for uint256;
17+
using MathUpgradeable for uint256;
1418

1519
error TooFewSharesReceived();
1620
error TooManySharesBurned();
@@ -29,13 +33,13 @@ contract MasterVault is ERC4626, Ownable {
2933

3034
// todo: avoid inflation, rounding, other common 4626 vulns
3135
// we may need a minimum asset or master share amount when setting subvaults (bc of exchange rate calc)
32-
ERC4626 public subVault;
36+
IERC4626 public subVault;
3337

3438
// how many subVault shares one MV2 share can be redeemed for
3539
// initially 1 to 1
3640
// constant per subvault
3741
// changes when subvault is set
38-
uint256 public subVaultExchRateWad = 1e18;
42+
uint256 public subVaultExchRateWad;
3943

4044
// note: the performance fee can be avoided if the underlying strategy can be sandwiched (eg ETH to wstETH dex swap)
4145
// maybe a simpler and more robust implementation would be for the owner to adjust the subVaultExchRateWad directly
@@ -49,16 +53,27 @@ contract MasterVault is ERC4626, Ownable {
4953
event PerformanceFeeToggled(bool enabled);
5054
event BeneficiaryUpdated(address indexed oldBeneficiary, address indexed newBeneficiary);
5155

52-
constructor(IERC20 _asset, string memory _name, string memory _symbol) ERC20(_name, _symbol) ERC4626(_asset) Ownable() {}
56+
function vaultInit(IERC20 _asset, string memory _name, string memory _symbol, address _owner) external initializer {
57+
require(address(_asset) != address(0), "INVALID_ASSET");
58+
require(_owner != address(0), "INVALID_OWNER");
59+
60+
__ERC20_init(_name, _symbol);
61+
__ERC4626_init(IERC20Upgradeable(address(_asset)));
62+
__Ownable_init();
63+
_transferOwnership(_owner);
64+
65+
subVaultExchRateWad = 1e18;
66+
}
67+
5368

5469
function deposit(uint256 assets, address receiver, uint256 minSharesMinted) public returns (uint256) {
55-
uint256 shares = super.deposit(assets, receiver);
70+
uint256 shares = deposit(assets, receiver);
5671
if (shares < minSharesMinted) revert TooFewSharesReceived();
5772
return shares;
5873
}
5974

6075
function withdraw(uint256 assets, address receiver, address _owner, uint256 maxSharesBurned) public returns (uint256) {
61-
uint256 shares = super.withdraw(assets, receiver, _owner);
76+
uint256 shares = withdraw(assets, receiver, _owner);
6277
if (shares > maxSharesBurned) revert TooManySharesBurned();
6378
return shares;
6479
}
@@ -78,7 +93,7 @@ contract MasterVault is ERC4626, Ownable {
7893
/// @notice Set a subvault. Can only be called if there is not already a subvault set.
7994
/// @param _subVault The subvault to set. Must be an ERC4626 vault with the same asset as this MasterVault.
8095
/// @param minSubVaultExchRateWad Minimum acceptable ratio (times 1e18) of new subvault shares to outstanding MasterVault shares after deposit.
81-
function setSubVault(ERC4626 _subVault, uint256 minSubVaultExchRateWad) external onlyOwner {
96+
function setSubVault(IERC4626 _subVault, uint256 minSubVaultExchRateWad) external onlyOwner {
8297
if (address(subVault) != address(0)) revert SubVaultAlreadySet();
8398
_setSubVault(_subVault, minSubVaultExchRateWad);
8499
}
@@ -89,15 +104,15 @@ contract MasterVault is ERC4626, Ownable {
89104
_revokeSubVault(minAssetExchRateWad);
90105
}
91106

92-
function _setSubVault(ERC4626 _subVault, uint256 minSubVaultExchRateWad) internal {
107+
function _setSubVault(IERC4626 _subVault, uint256 minSubVaultExchRateWad) internal {
93108
if (address(_subVault) == address(0)) revert SubVaultCannotBeZeroAddress();
94109
if (totalSupply() == 0) revert MustHaveSupplyBeforeSettingSubVault();
95110
if (address(_subVault.asset()) != address(asset())) revert SubVaultAssetMismatch();
96111

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

100-
uint256 _subVaultExchRateWad = subShares.mulDiv(1e18, totalSupply(), Math.Rounding.Down);
115+
uint256 _subVaultExchRateWad = subShares.mulDiv(1e18, totalSupply(), MathUpgradeable.Rounding.Down);
101116
if (_subVaultExchRateWad < minSubVaultExchRateWad) revert SubVaultExchangeRateTooLow();
102117
subVaultExchRateWad = _subVaultExchRateWad;
103118

@@ -107,16 +122,16 @@ contract MasterVault is ERC4626, Ownable {
107122
}
108123

109124
function _revokeSubVault(uint256 minAssetExchRateWad) internal {
110-
ERC4626 oldSubVault = subVault;
125+
IERC4626 oldSubVault = subVault;
111126
if (address(oldSubVault) == address(0)) revert NoExistingSubVault();
112127

113128
uint256 _totalSupply = totalSupply();
114129
uint256 assetReceived = oldSubVault.withdraw(oldSubVault.maxWithdraw(address(this)), address(this), address(this));
115-
uint256 effectiveAssetExchRateWad = assetReceived.mulDiv(1e18, _totalSupply, Math.Rounding.Down);
130+
uint256 effectiveAssetExchRateWad = assetReceived.mulDiv(1e18, _totalSupply, MathUpgradeable.Rounding.Down);
116131
if (effectiveAssetExchRateWad < minAssetExchRateWad) revert TooFewAssetsReceived();
117132

118133
IERC20(asset()).safeApprove(address(oldSubVault), 0);
119-
subVault = ERC4626(address(0));
134+
subVault = IERC4626(address(0));
120135
subVaultExchRateWad = 1e18;
121136

122137
emit SubvaultChanged(address(oldSubVault), address(0));
@@ -126,19 +141,19 @@ contract MasterVault is ERC4626, Ownable {
126141
/// @param newSubVault The new subvault to switch to, or zero address to revoke current subvault
127142
/// @param minAssetExchRateWad Minimum acceptable ratio (times 1e18) of assets received from old subvault to outstanding MasterVault shares
128143
/// @param minNewSubVaultExchRateWad Minimum acceptable ratio (times 1e18) of new subvault shares to outstanding MasterVault shares after deposit
129-
function switchSubVault(ERC4626 newSubVault, uint256 minAssetExchRateWad, uint256 minNewSubVaultExchRateWad) external onlyOwner {
144+
function switchSubVault(IERC4626 newSubVault, uint256 minAssetExchRateWad, uint256 minNewSubVaultExchRateWad) external onlyOwner {
130145
_revokeSubVault(minAssetExchRateWad);
131146

132147
if (address(newSubVault) != address(0)) {
133148
_setSubVault(newSubVault, minNewSubVaultExchRateWad);
134149
}
135150
}
136151

137-
function masterSharesToSubShares(uint256 masterShares, Math.Rounding rounding) public view returns (uint256) {
152+
function masterSharesToSubShares(uint256 masterShares, MathUpgradeable.Rounding rounding) public view returns (uint256) {
138153
return masterShares.mulDiv(subVaultExchRateWad, 1e18, rounding);
139154
}
140155

141-
function subSharesToMasterShares(uint256 subShares, Math.Rounding rounding) public view returns (uint256) {
156+
function subSharesToMasterShares(uint256 subShares, MathUpgradeable.Rounding rounding) public view returns (uint256) {
142157
return subShares.mulDiv(1e18, subVaultExchRateWad, rounding);
143158
}
144159

@@ -165,7 +180,7 @@ contract MasterVault is ERC4626, Ownable {
165180

166181
uint256 totalProfits = totalProfit();
167182
if (totalProfits > 0) {
168-
ERC4626 _subVault = subVault;
183+
IERC4626 _subVault = subVault;
169184
if (address(_subVault) != address(0)) {
170185
_subVault.withdraw(totalProfits, address(this), address(this));
171186
}
@@ -175,7 +190,7 @@ contract MasterVault is ERC4626, Ownable {
175190

176191
/** @dev See {IERC4626-totalAssets}. */
177192
function totalAssets() public view virtual override returns (uint256) {
178-
ERC4626 _subVault = subVault;
193+
IERC4626 _subVault = subVault;
179194
if (address(_subVault) == address(0)) {
180195
return super.totalAssets();
181196
}
@@ -196,7 +211,7 @@ contract MasterVault is ERC4626, Ownable {
196211
if (subShares == type(uint256).max) {
197212
return type(uint256).max;
198213
}
199-
return subSharesToMasterShares(subShares, Math.Rounding.Down);
214+
return subSharesToMasterShares(subShares, MathUpgradeable.Rounding.Down);
200215
}
201216

202217
/**
@@ -205,25 +220,25 @@ contract MasterVault is ERC4626, Ownable {
205220
* Will revert if assets > 0, totalSupply > 0 and totalAssets = 0. That corresponds to a case where any asset
206221
* would represent an infinite amount of shares.
207222
*/
208-
function _convertToShares(uint256 assets, Math.Rounding rounding) internal view virtual override returns (uint256 shares) {
209-
ERC4626 _subVault = subVault;
223+
function _convertToShares(uint256 assets, MathUpgradeable.Rounding rounding) internal view virtual override returns (uint256 shares) {
224+
IERC4626 _subVault = subVault;
210225
if (address(_subVault) == address(0)) {
211226
return super._convertToShares(assets, rounding);
212227
}
213-
uint256 subShares = rounding == Math.Rounding.Up ? _subVault.previewWithdraw(assets) : _subVault.previewDeposit(assets);
228+
uint256 subShares = rounding == MathUpgradeable.Rounding.Up ? _subVault.previewWithdraw(assets) : _subVault.previewDeposit(assets);
214229
return subSharesToMasterShares(subShares, rounding);
215230
}
216231

217232
/**
218233
* @dev Internal conversion function (from shares to assets) with support for rounding direction.
219234
*/
220-
function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view virtual override returns (uint256 assets) {
221-
ERC4626 _subVault = subVault;
235+
function _convertToAssets(uint256 shares, MathUpgradeable.Rounding rounding) internal view virtual override returns (uint256 assets) {
236+
IERC4626 _subVault = subVault;
222237
if (address(_subVault) == address(0)) {
223238
return super._convertToAssets(shares, rounding);
224239
}
225240
uint256 subShares = masterSharesToSubShares(shares, rounding);
226-
return rounding == Math.Rounding.Up ? _subVault.previewMint(subShares) : _subVault.previewRedeem(subShares);
241+
return rounding == MathUpgradeable.Rounding.Up ? _subVault.previewMint(subShares) : _subVault.previewRedeem(subShares);
227242
}
228243

229244
function totalProfit() public view returns (uint256) {
@@ -241,8 +256,9 @@ contract MasterVault is ERC4626, Ownable {
241256
uint256 shares
242257
) internal virtual override {
243258
super._deposit(caller, receiver, assets, shares);
259+
244260
totalPrincipal += assets;
245-
ERC4626 _subVault = subVault;
261+
IERC4626 _subVault = subVault;
246262
if (address(_subVault) != address(0)) {
247263
_subVault.deposit(assets, address(this));
248264
}
@@ -260,7 +276,7 @@ contract MasterVault is ERC4626, Ownable {
260276
) internal virtual override {
261277
totalPrincipal -= assets;
262278

263-
ERC4626 _subVault = subVault;
279+
IERC4626 _subVault = subVault;
264280
if (address(_subVault) != address(0)) {
265281
_subVault.withdraw(assets, address(this), address(this));
266282
}

contracts/tokenbridge/libraries/vault/MasterVaultFactory.sol

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,49 +5,56 @@ pragma solidity ^0.8.0;
55
import "@openzeppelin/contracts/utils/Create2.sol";
66
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
77
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
8+
import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol";
9+
import "../ClonableBeaconProxy.sol";
810
import "./IMasterVault.sol";
911
import "./IMasterVaultFactory.sol";
1012
import "./MasterVault.sol";
1113

1214
contract MasterVaultFactory is IMasterVaultFactory, OwnableUpgradeable {
13-
1415
error ZeroAddress();
16+
error BeaconNotDeployed();
17+
18+
UpgradeableBeacon public beacon;
19+
BeaconProxyFactory public beaconProxyFactory;
1520

1621
function initialize(address _owner) public initializer {
1722
_transferOwnership(_owner);
23+
24+
MasterVault masterVaultImplementation = new MasterVault();
25+
beacon = new UpgradeableBeacon(address(masterVaultImplementation));
26+
beaconProxyFactory = new BeaconProxyFactory();
27+
beaconProxyFactory.initialize(address(beacon));
28+
beacon.transferOwnership(address(this));
1829
}
1930

2031
function deployVault(address token) public returns (address vault) {
2132
if (token == address(0)) {
2233
revert ZeroAddress();
2334
}
35+
if (address(beaconProxyFactory) == address(0)) {
36+
revert BeaconNotDeployed();
37+
}
38+
39+
bytes32 userSalt = _getUserSalt(token);
40+
vault = beaconProxyFactory.createProxy(userSalt);
2441

2542
IERC20Metadata tokenMetadata = IERC20Metadata(token);
2643
string memory name = string(abi.encodePacked("Master ", tokenMetadata.name()));
2744
string memory symbol = string(abi.encodePacked("m", tokenMetadata.symbol()));
2845

29-
bytes memory bytecode = abi.encodePacked(
30-
type(MasterVault).creationCode,
31-
abi.encode(token, name, symbol)
32-
);
33-
34-
vault = Create2.deploy(0, bytes32(0), bytecode);
46+
MasterVault(vault).vaultInit(IERC20(token), name, symbol, address(this));
3547

3648
emit VaultDeployed(token, vault);
3749
}
3850

39-
function calculateVaultAddress(address token) public view returns (address) {
40-
IERC20Metadata tokenMetadata = IERC20Metadata(token);
41-
string memory name = string(abi.encodePacked("Master ", tokenMetadata.name()));
42-
string memory symbol = string(abi.encodePacked("m", tokenMetadata.symbol()));
51+
function _getUserSalt(address token) internal pure returns (bytes32) {
52+
return keccak256(abi.encode(token));
53+
}
4354

44-
bytes32 bytecodeHash = keccak256(
45-
abi.encodePacked(
46-
type(MasterVault).creationCode,
47-
abi.encode(token, name, symbol)
48-
)
49-
);
50-
return Create2.computeAddress(bytes32(0), bytecodeHash);
55+
function calculateVaultAddress(address token) public view returns (address) {
56+
bytes32 userSalt = _getUserSalt(token);
57+
return beaconProxyFactory.calculateExpectedAddress(address(this), userSalt);
5158
}
5259

5360
function getVault(address token) external returns (address) {
@@ -61,9 +68,10 @@ contract MasterVaultFactory is IMasterVaultFactory, OwnableUpgradeable {
6168
// todo: consider a method to enable bridge owner to transfer specific master vault ownership to new address
6269
function setSubVault(
6370
address masterVault,
64-
address subVault
71+
address subVault,
72+
uint256 minSubVaultExchRateWad
6573
) external onlyOwner {
66-
IMasterVault(masterVault).setSubVault(subVault);
74+
IMasterVault(masterVault).setSubVault(subVault, minSubVaultExchRateWad);
6775
emit SubVaultSet(masterVault, subVault);
6876
}
69-
}
77+
}

test-foundry/libraries/vault/MasterVault.t.sol

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@ import { MasterVault } from "../../../contracts/tokenbridge/libraries/vault/Mast
66
import { TestERC20 } from "../../../contracts/tokenbridge/test/TestERC20.sol";
77
import { MockSubVault } from "../../../contracts/tokenbridge/test/MockSubVault.sol";
88
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
9+
import { UpgradeableBeacon } from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol";
10+
import { BeaconProxyFactory, ClonableBeaconProxy } from "../../../contracts/tokenbridge/libraries/ClonableBeaconProxy.sol";
911

1012
contract MasterVaultTest is Test {
1113
MasterVault public vault;
1214
TestERC20 public token;
15+
UpgradeableBeacon public beacon;
16+
BeaconProxyFactory public beaconProxyFactory;
1317

1418
event SubvaultChanged(address indexed oldSubvault, address indexed newSubvault);
1519

@@ -19,7 +23,18 @@ contract MasterVaultTest is Test {
1923

2024
function setUp() public {
2125
token = new TestERC20();
22-
vault = new MasterVault(IERC20(address(token)), name, symbol);
26+
27+
MasterVault implementation = new MasterVault();
28+
beacon = new UpgradeableBeacon(address(implementation));
29+
30+
beaconProxyFactory = new BeaconProxyFactory();
31+
beaconProxyFactory.initialize(address(beacon));
32+
33+
bytes32 salt = keccak256("test");
34+
address proxyAddress = beaconProxyFactory.createProxy(salt);
35+
vault = MasterVault(proxyAddress);
36+
37+
vault.vaultInit(IERC20(address(token)), name, symbol, address(this));
2338
}
2439

2540
function test_initialize() public {

0 commit comments

Comments
 (0)