Skip to content

Commit 4c3bda9

Browse files
authored
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
1 parent cadf05d commit 4c3bda9

File tree

7 files changed

+333
-311
lines changed

7 files changed

+333
-311
lines changed

contracts/tokenbridge/libraries/vault/MasterVault.sol

Lines changed: 134 additions & 152 deletions
Large diffs are not rendered by default.

contracts/tokenbridge/libraries/vault/MasterVaultFactory.sol

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,19 @@
22

33
pragma solidity ^0.8.0;
44

5-
import "@openzeppelin/contracts/utils/Create2.sol";
65
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
76
import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol";
7+
import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
88
import "../ClonableBeaconProxy.sol";
99
import "./IMasterVault.sol";
1010
import "./IMasterVaultFactory.sol";
1111
import "./MasterVault.sol";
1212

13+
contract DefaultSubVault is ERC4626 {
14+
constructor(address token) ERC4626(IERC20(token)) ERC20("Default SubVault", "DSV") {}
15+
}
16+
17+
// todo: slim down this contract
1318
contract MasterVaultFactory is IMasterVaultFactory, Initializable {
1419
error ZeroAddress();
1520
error BeaconNotDeployed();
@@ -27,23 +32,13 @@ contract MasterVaultFactory is IMasterVaultFactory, Initializable {
2732
}
2833

2934
function deployVault(address token) public returns (address vault) {
30-
if (token == address(0)) {
31-
revert ZeroAddress();
32-
}
33-
if (
34-
address(beaconProxyFactory) == address(0) && beaconProxyFactory.beacon() == address(0)
35-
) {
36-
revert BeaconNotDeployed();
37-
}
38-
3935
bytes32 userSalt = _getUserSalt(token);
4036
vault = beaconProxyFactory.createProxy(userSalt);
4137

42-
IERC20Metadata tokenMetadata = IERC20Metadata(token);
43-
string memory name = string(abi.encodePacked("Master ", tokenMetadata.name()));
44-
string memory symbol = string(abi.encodePacked("m", tokenMetadata.symbol()));
38+
string memory name = string(abi.encodePacked("Master ", _tryGetTokenName(token)));
39+
string memory symbol = string(abi.encodePacked("m", _tryGetTokenSymbol(token)));
4540

46-
MasterVault(vault).initialize(IERC20(token), name, symbol, owner);
41+
MasterVault(vault).initialize(new DefaultSubVault(token), name, symbol, owner);
4742

4843
emit VaultDeployed(token, vault);
4944
}
@@ -64,4 +59,20 @@ contract MasterVaultFactory is IMasterVaultFactory, Initializable {
6459
}
6560
return vault;
6661
}
62+
63+
function _tryGetTokenName(address token) internal view returns (string memory) {
64+
try IERC20Metadata(token).name() returns (string memory name) {
65+
return name;
66+
} catch {
67+
return "";
68+
}
69+
}
70+
71+
function _tryGetTokenSymbol(address token) internal view returns (string memory) {
72+
try IERC20Metadata(token).symbol() returns (string memory symbol) {
73+
return symbol;
74+
} catch {
75+
return "";
76+
}
77+
}
6778
}

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

Lines changed: 147 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -5,145 +5,176 @@ import { MasterVaultCoreTest } from "./MasterVaultCore.t.sol";
55
import { MockSubVault } from "../../../contracts/tokenbridge/test/MockSubVault.sol";
66
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
77
import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol";
8+
import { Math } from "@openzeppelin/contracts/utils/math/Math.sol";
9+
import {console2} from "forge-std/console2.sol";
10+
11+
contract MasterVaultFirstDepositTest is MasterVaultCoreTest {
12+
using Math for uint256;
13+
14+
uint256 constant FRESH_STATE_PLACEHOLDER = uint256(keccak256("FRESH_STATE_PLACEHOLDER"));
15+
uint256 constant DEAD_SHARES = 10**18;
16+
17+
struct State {
18+
uint256 userShares;
19+
uint256 masterVaultTotalAssets;
20+
uint256 masterVaultTotalSupply;
21+
uint256 masterVaultTokenBalance;
22+
uint256 masterVaultSubVaultShareBalance;
23+
uint256 subVaultTotalAssets;
24+
uint256 subVaultTotalSupply;
25+
uint256 subVaultTokenBalance;
26+
}
827

9-
contract MasterVaultTest is MasterVaultCoreTest {
1028
// first deposit
11-
function test_deposit() public {
12-
address _assetsHoldingVault = address(vault.subVault()) == address(0)
13-
? address(vault)
14-
: address(vault.subVault());
15-
uint256 _assetsHoldingVaultBalanceBefore = token.balanceOf(_assetsHoldingVault);
16-
29+
function test_deposit(uint96 _depositAmount) public {
30+
uint256 depositAmount = _depositAmount;
1731
vm.startPrank(user);
18-
token.mint();
19-
uint256 depositAmount = 100;
20-
32+
token.mint(depositAmount);
2133
token.approve(address(vault), depositAmount);
22-
2334
uint256 shares = vault.deposit(depositAmount, user);
24-
25-
uint256 _assetsHoldingVaultBalanceAfter = token.balanceOf(_assetsHoldingVault);
26-
uint256 diff = _assetsHoldingVaultBalanceAfter - _assetsHoldingVaultBalanceBefore;
27-
28-
assertEq(vault.balanceOf(user), shares, "User should receive shares");
29-
assertEq(vault.totalAssets(), depositAmount, "Vault should hold deposited assets");
30-
assertEq(vault.totalSupply(), shares, "Total supply should equal shares minted");
31-
32-
assertEq(diff, depositAmount, "Vault should increase holding of assets");
33-
assertGt(token.balanceOf(_assetsHoldingVault), 0, "Vault should hold the tokens");
34-
35-
assertEq(vault.totalSupply(), diff, "First deposit should be at a rate of 1");
36-
3735
vm.stopPrank();
36+
_checkState(State({
37+
userShares: depositAmount * DEAD_SHARES,
38+
masterVaultTotalAssets: depositAmount,
39+
masterVaultTotalSupply: (1 + depositAmount) * DEAD_SHARES,
40+
masterVaultTokenBalance: depositAmount,
41+
masterVaultSubVaultShareBalance: 0,
42+
subVaultTotalAssets: 0,
43+
subVaultTotalSupply: 0,
44+
subVaultTokenBalance: 0
45+
}));
46+
assertEq(shares, depositAmount * DEAD_SHARES, "shares mismatch deposit return value");
3847
}
3948

40-
// first mint
41-
function test_mint() public {
42-
address _assetsHoldingVault = address(vault.subVault()) == address(0)
43-
? address(vault)
44-
: address(vault.subVault());
45-
46-
uint256 _assetsHoldingVaultBalanceBefore = token.balanceOf(_assetsHoldingVault);
47-
49+
function test_mint(uint96 _mintAmount) public {
50+
uint256 mintAmount = _mintAmount;
4851
vm.startPrank(user);
49-
token.mint();
50-
uint256 sharesToMint = 100;
51-
52-
token.approve(address(vault), type(uint256).max);
53-
54-
// assertEq(1, vault.totalAssets(), "First mint should be at a rate of 1"); // 0
55-
// assertEq(1, vault.totalSupply(), "First mint should be at a rate of 1"); // 0
56-
57-
58-
uint256 assetsCost = vault.mint(sharesToMint, user);
59-
60-
uint256 _assetsHoldingVaultBalanceAfter = token.balanceOf(_assetsHoldingVault);
61-
62-
assertEq(vault.balanceOf(user), sharesToMint, "User should receive requested shares");
63-
assertEq(vault.totalSupply(), sharesToMint, "Total supply should equal shares minted");
64-
assertEq(vault.totalAssets(), assetsCost, "Vault should hold the assets deposited");
65-
assertEq(
66-
_assetsHoldingVaultBalanceAfter - _assetsHoldingVaultBalanceBefore,
67-
assetsCost,
68-
"Vault should hold the tokens"
69-
);
70-
71-
assertEq(vault.totalSupply(), vault.totalAssets(), "First mint should be at a rate of 1");
52+
token.mint(mintAmount);
53+
token.approve(address(vault), mintAmount);
54+
uint256 assets = vault.mint(mintAmount, user);
7255
vm.stopPrank();
56+
_checkState(State({
57+
userShares: mintAmount,
58+
masterVaultTotalAssets: mintAmount.ceilDiv(1e18),
59+
masterVaultTotalSupply: mintAmount + DEAD_SHARES,
60+
masterVaultTokenBalance: mintAmount.ceilDiv(1e18),
61+
masterVaultSubVaultShareBalance: 0,
62+
subVaultTotalAssets: 0,
63+
subVaultTotalSupply: 0,
64+
subVaultTokenBalance: 0
65+
}));
66+
assertEq(assets, mintAmount.ceilDiv(1e18), "assets mismatch mint return value");
7367
}
7468

75-
function test_withdraw() public {
69+
function test_withdraw(uint96 _firstDeposit, uint96 _withdrawAmount) public {
70+
uint256 firstDeposit = _firstDeposit;
71+
uint256 withdrawAmount = _withdrawAmount;
72+
vm.assume(withdrawAmount <= firstDeposit);
73+
test_deposit(_firstDeposit);
7674
vm.startPrank(user);
77-
token.mint();
78-
uint256 depositAmount = token.balanceOf(user);
79-
token.approve(address(vault), depositAmount);
80-
vault.deposit(depositAmount, user);
81-
82-
uint256 userSharesBefore = vault.balanceOf(user);
83-
uint256 withdrawAmount = depositAmount; // withdraw all assets
84-
8575
uint256 sharesRedeemed = vault.withdraw(withdrawAmount, user, user);
86-
87-
assertEq(vault.balanceOf(user), 0, "User should have no shares left");
88-
assertEq(token.balanceOf(user), depositAmount, "User should receive all withdrawn tokens");
89-
assertEq(vault.totalAssets(), 0, "Vault should have no assets left");
90-
assertEq(vault.totalSupply(), 0, "Total supply should be zero");
91-
assertEq(token.balanceOf(address(vault)), 0, "Vault should have no tokens left");
92-
assertEq(sharesRedeemed, userSharesBefore, "All shares should be redeemed");
93-
9476
vm.stopPrank();
77+
_checkState(State({
78+
userShares: (firstDeposit - withdrawAmount) * DEAD_SHARES,
79+
masterVaultTotalAssets: firstDeposit - withdrawAmount,
80+
masterVaultTotalSupply: (1 + firstDeposit - withdrawAmount) * DEAD_SHARES,
81+
masterVaultTokenBalance: firstDeposit - withdrawAmount,
82+
masterVaultSubVaultShareBalance: 0,
83+
subVaultTotalAssets: 0,
84+
subVaultTotalSupply: 0,
85+
subVaultTokenBalance: 0
86+
}));
87+
assertEq(sharesRedeemed, withdrawAmount * DEAD_SHARES, "sharesRedeemed mismatch withdraw return value");
9588
}
9689

97-
function test_redeem() public {
90+
function test_redeem(uint96 _firstMint, uint96 _redeemAmount) public {
91+
uint256 firstMint = _firstMint;
92+
uint256 redeemAmount = _redeemAmount;
93+
vm.assume(redeemAmount <= firstMint);
94+
test_mint(_firstMint);
95+
State memory beforeState = _getState();
9896
vm.startPrank(user);
99-
token.mint();
100-
uint256 depositAmount = token.balanceOf(user);
101-
token.approve(address(vault), depositAmount);
102-
uint256 shares = vault.deposit(depositAmount, user);
103-
104-
uint256 sharesToRedeem = shares; // redeem all shares
105-
106-
uint256 assetsReceived = vault.redeem(sharesToRedeem, user, user);
107-
108-
assertEq(vault.balanceOf(user), 0, "User should have no shares left");
109-
assertEq(token.balanceOf(user), depositAmount, "User should receive all assets back");
110-
assertEq(vault.totalAssets(), 0, "Vault should have no assets left");
111-
assertEq(vault.totalSupply(), 0, "Total supply should be zero");
112-
assertEq(token.balanceOf(address(vault)), 0, "Vault should have no tokens left");
113-
assertEq(assetsReceived, depositAmount, "All assets should be received");
114-
97+
uint256 assets = vault.redeem(redeemAmount, user, user);
98+
uint256 expectedAssets = (1 + beforeState.masterVaultTotalAssets) * redeemAmount / (beforeState.masterVaultTotalSupply);
11599
vm.stopPrank();
100+
_checkState(State({
101+
userShares: beforeState.userShares - redeemAmount,
102+
masterVaultTotalAssets: beforeState.masterVaultTotalAssets - expectedAssets,
103+
masterVaultTotalSupply: beforeState.masterVaultTotalSupply - redeemAmount,
104+
masterVaultTokenBalance: beforeState.masterVaultTokenBalance - expectedAssets,
105+
masterVaultSubVaultShareBalance: 0,
106+
subVaultTotalAssets: 0,
107+
subVaultTotalSupply: 0,
108+
subVaultTokenBalance: 0
109+
}));
110+
assertEq(assets, expectedAssets, "assets mismatch redeem return value");
116111
}
117-
}
118112

119-
contract MasterVaultTestWithSubvaultFresh is MasterVaultTest {
120-
function setUp() public override {
121-
super.setUp();
122-
MockSubVault _subvault = new MockSubVault(IERC20(address(token)), "TestSubvault", "TSV");
123-
vault.setSubVault(IERC4626(address(_subvault)), 0);
113+
function _checkState(State memory expectedState) internal {
114+
assertEq(expectedState.userShares, vault.balanceOf(user), "userShares mismatch");
115+
assertEq(expectedState.masterVaultTotalAssets, vault.totalAssets(), "masterVaultTotalAssets mismatch");
116+
assertEq(expectedState.masterVaultTotalSupply, vault.totalSupply(), "masterVaultTotalSupply mismatch");
117+
assertEq(expectedState.masterVaultTokenBalance, token.balanceOf(address(vault)), "masterVaultTokenBalance mismatch");
118+
assertEq(expectedState.masterVaultSubVaultShareBalance, vault.subVault().balanceOf(address(vault)), "masterVaultSubVaultShareBalance mismatch");
119+
assertEq(expectedState.subVaultTotalAssets, vault.subVault().totalAssets(), "subVaultTotalAssets mismatch");
120+
assertEq(expectedState.subVaultTotalSupply, vault.subVault().totalSupply(), "subVaultTotalSupply mismatch");
121+
assertEq(expectedState.subVaultTokenBalance, token.balanceOf(address(vault.subVault())), "subVaultTokenBalance mismatch");
124122
}
125-
}
126-
127-
contract MasterVaultTestWithSubvaultHoldingAssets is MasterVaultTest {
128-
function setUp() public override {
129-
super.setUp();
130123

131-
MockSubVault _subvault = new MockSubVault(IERC20(address(token)), "TestSubvault", "TSV");
132-
uint256 _initAmount = 97659743;
133-
token.mint(_initAmount);
134-
token.approve(address(_subvault), _initAmount);
135-
_subvault.deposit(_initAmount, address(this));
136-
assertEq(
137-
_initAmount,
138-
_subvault.totalAssets(),
139-
"subvault should be initiated with assets = _initAmount"
140-
);
141-
assertEq(
142-
_initAmount,
143-
_subvault.totalSupply(),
144-
"subvault should be initiated with shares = _initAmount"
145-
);
124+
function _getState() internal view returns (State memory) {
125+
return State({
126+
userShares: vault.balanceOf(user),
127+
masterVaultTotalAssets: vault.totalAssets(),
128+
masterVaultTotalSupply: vault.totalSupply(),
129+
masterVaultTokenBalance: token.balanceOf(address(vault)),
130+
masterVaultSubVaultShareBalance: vault.subVault().balanceOf(address(vault)),
131+
subVaultTotalAssets: vault.subVault().totalAssets(),
132+
subVaultTotalSupply: vault.subVault().totalSupply(),
133+
subVaultTokenBalance: token.balanceOf(address(vault.subVault()))
134+
});
135+
}
146136

147-
vault.setSubVault(IERC4626(address(_subvault)), 0);
137+
function _logState(string memory label, State memory state) internal view {
138+
console2.log(label);
139+
console2.log(" userShares:", state.userShares);
140+
console2.log(" masterVaultTotalAssets:", state.masterVaultTotalAssets);
141+
console2.log(" masterVaultTotalSupply:", state.masterVaultTotalSupply);
142+
console2.log(" masterVaultTokenBalance:", state.masterVaultTokenBalance);
143+
console2.log(" masterVaultSubVaultShareBalance:", state.masterVaultSubVaultShareBalance);
144+
console2.log(" subVaultTotalAssets:", state.subVaultTotalAssets);
145+
console2.log(" subVaultTotalSupply:", state.subVaultTotalSupply);
146+
console2.log(" subVaultTokenBalance:", state.subVaultTokenBalance);
148147
}
149148
}
149+
150+
// contract MasterVaultTestWithSubvaultFresh is MasterVaultTest {
151+
// function setUp() public override {
152+
// super.setUp();
153+
// MockSubVault _subvault = new MockSubVault(IERC20(address(token)), "TestSubvault", "TSV");
154+
// vault.setSubVault(IERC4626(address(_subvault)));
155+
// }
156+
// }
157+
158+
// contract MasterVaultTestWithSubvaultHoldingAssets is MasterVaultTest {
159+
// function setUp() public override {
160+
// super.setUp();
161+
162+
// MockSubVault _subvault = new MockSubVault(IERC20(address(token)), "TestSubvault", "TSV");
163+
// uint256 _initAmount = 97659743;
164+
// token.mint(_initAmount);
165+
// token.approve(address(_subvault), _initAmount);
166+
// _subvault.deposit(_initAmount, address(this));
167+
// assertEq(
168+
// _initAmount,
169+
// _subvault.totalAssets(),
170+
// "subvault should be initiated with assets = _initAmount"
171+
// );
172+
// assertEq(
173+
// _initAmount,
174+
// _subvault.totalSupply(),
175+
// "subvault should be initiated with shares = _initAmount"
176+
// );
177+
178+
// vault.setSubVault(IERC4626(address(_subvault)));
179+
// }
180+
// }

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@
22
pragma solidity ^0.8.0;
33

44
import "forge-std/console2.sol";
5-
import { MasterVaultTest } from "./MasterVault.t.sol";
5+
import { MasterVaultCoreTest } from "./MasterVaultCore.t.sol";
66
import { MockSubVault } from "../../../contracts/tokenbridge/test/MockSubVault.sol";
77
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
88
import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol";
99

10-
contract MasterVaultTestWithSubvaultFresh is MasterVaultTest {
10+
contract MasterVaultTestWithSubvaultFresh is MasterVaultCoreTest {
1111
function setUp() public override {
1212
super.setUp();
1313
MockSubVault _subvault = new MockSubVault(IERC20(address(token)), "TestSubvault", "TSV");
14-
vault.setSubVault(IERC4626(address(_subvault)), 0);
14+
vault.setSubVault(IERC4626(address(_subvault)));
1515
}
1616
}
1717

0 commit comments

Comments
 (0)