Skip to content

Commit 590a713

Browse files
committed
permission withdraw performence fees
1 parent 951c33c commit 590a713

File tree

2 files changed

+100
-24
lines changed

2 files changed

+100
-24
lines changed

contracts/tokenbridge/libraries/vault/MasterVault.sol

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ contract MasterVault is ERC4626, Ownable {
2424
error NoExistingSubVault();
2525
error MustHaveSupplyBeforeSwitchingSubVault();
2626
error NewSubVaultExchangeRateTooLow();
27+
error BeneficiaryNotSet();
28+
error PerformanceFeeDisabled();
2729

2830
// todo: avoid inflation, rounding, other common 4626 vulns
2931
// we may need a minimum asset or master share amount when setting subvaults (bc of exchange rate calc)
@@ -39,10 +41,13 @@ contract MasterVault is ERC4626, Ownable {
3941
// maybe a simpler and more robust implementation would be for the owner to adjust the subVaultExchRateWad directly
4042
// this would also avoid the need for totalPrincipal tracking
4143
// however, this would require more trust in the owner
42-
uint256 public performanceFeeBps; // in basis points, e.g. 200 = 2% | todo a way to set this
44+
bool public enablePerformanceFee;
45+
address public beneficiary;
4346
uint256 totalPrincipal; // total assets deposited, used to calculate profit
4447

4548
event SubvaultChanged(address indexed oldSubvault, address indexed newSubvault);
49+
event PerformanceFeeToggled(bool enabled);
50+
event BeneficiaryUpdated(address indexed oldBeneficiary, address indexed newBeneficiary);
4651

4752
constructor(IERC20 _asset, string memory _name, string memory _symbol) ERC20(_name, _symbol) ERC4626(_asset) Ownable() {}
4853

@@ -137,6 +142,37 @@ contract MasterVault is ERC4626, Ownable {
137142
return subShares.mulDiv(1e18, subVaultExchRateWad, rounding);
138143
}
139144

145+
/// @notice Toggle performance fee collection on/off
146+
/// @param enabled True to enable performance fees, false to disable
147+
function setPerformanceFee(bool enabled) external onlyOwner {
148+
enablePerformanceFee = enabled;
149+
emit PerformanceFeeToggled(enabled);
150+
}
151+
152+
/// @notice Set the beneficiary address for performance fees
153+
/// @param newBeneficiary Address to receive performance fees, zero address defaults to owner
154+
function setBeneficiary(address newBeneficiary) external onlyOwner {
155+
address oldBeneficiary = beneficiary;
156+
beneficiary = newBeneficiary;
157+
emit BeneficiaryUpdated(oldBeneficiary, newBeneficiary);
158+
}
159+
160+
/// @notice Withdraw all accumulated performance fees to beneficiary
161+
/// @dev Only callable by owner when performance fees are enabled
162+
function withdrawPerformanceFees() external onlyOwner {
163+
if (!enablePerformanceFee) revert PerformanceFeeDisabled();
164+
if (beneficiary == address(0)) revert BeneficiaryNotSet();
165+
166+
uint256 totalProfits = totalProfit();
167+
if (totalProfits > 0) {
168+
ERC4626 _subVault = subVault;
169+
if (address(_subVault) != address(0)) {
170+
_subVault.withdraw(totalProfits, address(this), address(this));
171+
}
172+
IERC20(asset()).safeTransfer(beneficiary, totalProfits);
173+
}
174+
}
175+
140176
/** @dev See {IERC4626-totalAssets}. */
141177
function totalAssets() public view virtual override returns (uint256) {
142178
ERC4626 _subVault = subVault;
@@ -222,34 +258,13 @@ contract MasterVault is ERC4626, Ownable {
222258
uint256 assets,
223259
uint256 shares
224260
) internal virtual override {
261+
super._withdraw(caller, receiver, _owner, assets, shares);
225262
ERC4626 _subVault = subVault;
226263
if (address(_subVault) != address(0)) {
227264
_subVault.withdraw(assets, address(this), address(this));
228265
}
229266

230-
////// PERF FEE STUFF //////
231-
// determine profit portion and principal portion of assets
232-
uint256 _totalProfit = totalProfit();
233-
// use shares because they are rounded up vs assets which are rounded down
234-
uint256 profitPortion = shares.mulDiv(_totalProfit, totalSupply(), Math.Rounding.Up);
235-
uint256 principalPortion = assets - profitPortion;
236-
237-
// subtract principal portion from totalPrincipal
238-
totalPrincipal -= principalPortion;
239-
240-
// send fee to owner (todo should be a separate beneficiary addr set by owner)
241-
if (performanceFeeBps > 0 && profitPortion > 0) {
242-
uint256 fee = profitPortion.mulDiv(performanceFeeBps, 10000, Math.Rounding.Up);
243-
// send fee to owner
244-
IERC20(asset()).safeTransfer(owner(), fee);
245-
246-
// note subtraction
247-
assets -= fee;
248-
}
267+
totalPrincipal -= assets;
249268

250-
////// END PERF FEE STUFF //////
251-
252-
// call super._withdraw with remaining assets
253-
super._withdraw(caller, receiver, _owner, assets, shares);
254269
}
255270
}

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

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,4 +184,65 @@ contract MasterVaultTest is Test {
184184
assertEq(token.balanceOf(address(vault)), depositAmount, "MasterVault should have assets directly");
185185
}
186186

187+
function test_WithoutSubvault_withdraw() public {
188+
uint256 maxSharesBurned = type(uint256).max;
189+
190+
vm.startPrank(user);
191+
token.mint();
192+
uint256 depositAmount = token.balanceOf(user);
193+
token.approve(address(vault), depositAmount);
194+
vault.deposit(depositAmount, user, 0);
195+
196+
uint256 withdrawAmount = depositAmount / 2;
197+
uint256 userSharesBefore = vault.balanceOf(user);
198+
uint256 totalSupplyBefore = vault.totalSupply();
199+
uint256 totalAssetsBefore = vault.totalAssets();
200+
201+
uint256 shares = vault.withdraw(withdrawAmount, user, user, maxSharesBurned);
202+
203+
assertEq(vault.balanceOf(user), userSharesBefore - shares, "User shares should decrease");
204+
assertEq(vault.totalSupply(), totalSupplyBefore - shares, "Total supply should decrease");
205+
assertEq(vault.totalAssets(), totalAssetsBefore - withdrawAmount, "Total assets should decrease");
206+
assertEq(token.balanceOf(user), withdrawAmount, "User should receive withdrawn assets");
207+
assertEq(token.balanceOf(address(vault)), depositAmount - withdrawAmount, "Vault should have remaining assets");
208+
209+
vm.stopPrank();
210+
}
211+
212+
function test_WithSubvault_withdraw() public {
213+
MockSubVault subVault = new MockSubVault(
214+
IERC20(address(token)),
215+
"Sub Vault Token",
216+
"svTST"
217+
);
218+
219+
vm.startPrank(user);
220+
token.mint();
221+
uint256 depositAmount = token.balanceOf(user);
222+
token.approve(address(vault), depositAmount);
223+
vault.deposit(depositAmount, user, 0);
224+
vm.stopPrank();
225+
226+
vault.setSubVault(subVault, 1e18);
227+
228+
uint256 withdrawAmount = depositAmount / 2;
229+
uint256 maxSharesBurned = type(uint256).max;
230+
231+
vm.startPrank(user);
232+
uint256 userSharesBefore = vault.balanceOf(user);
233+
uint256 totalSupplyBefore = vault.totalSupply();
234+
uint256 totalAssetsBefore = vault.totalAssets();
235+
uint256 subVaultSharesBefore = subVault.balanceOf(address(vault));
236+
237+
uint256 shares = vault.withdraw(withdrawAmount, user, user, maxSharesBurned);
238+
239+
assertEq(vault.balanceOf(user), userSharesBefore - shares, "User shares should decrease");
240+
assertEq(vault.totalSupply(), totalSupplyBefore - shares, "Total supply should decrease");
241+
assertEq(vault.totalAssets(), totalAssetsBefore - withdrawAmount, "Total assets should decrease");
242+
assertEq(token.balanceOf(user), withdrawAmount, "User should receive withdrawn assets");
243+
assertLt(subVault.balanceOf(address(vault)), subVaultSharesBefore, "SubVault shares should decrease");
244+
245+
vm.stopPrank();
246+
}
247+
187248
}

0 commit comments

Comments
 (0)