diff --git a/foundry.toml b/foundry.toml index e1575ca..d68a58e 100644 --- a/foundry.toml +++ b/foundry.toml @@ -8,7 +8,7 @@ optimizer = false via_ir = false [fuzz] -runs = 10 +runs = 100 [profile.optimized] optimizer = true diff --git a/src/BrrETH.sol b/src/BrrETH.sol index 370fdad..cdba1e4 100644 --- a/src/BrrETH.sol +++ b/src/BrrETH.sol @@ -4,12 +4,14 @@ pragma solidity ^0.8.0; import {ERC4626} from "solady/tokens/ERC4626.sol"; import {Ownable} from "solady/auth/Ownable.sol"; import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; +import {FixedPointMathLib} from "solady/utils/FixedPointMathLib.sol"; import {IComet} from "src/interfaces/IComet.sol"; import {ICometRewards} from "src/interfaces/ICometRewards.sol"; import {IRouter} from "src/interfaces/IRouter.sol"; contract BrrETH is Ownable, ERC4626 { using SafeTransferLib for address; + using FixedPointMathLib for uint256; string private constant _NAME = "Brrito-Compound WETH"; string private constant _SYMBOL = "brr-cWETHv3"; @@ -106,18 +108,35 @@ contract BrrETH is Ownable, ERC4626 { tokenBalance ); - IComet(_COMET).supply( + // `swap` returns the entire WETH amount received from the swap. + uint256 actualOutput = _ROUTER.swap( + rewardConfig.token, _WETH, - // `swap` returns the entire WETH amount received from the swap. - _ROUTER.swap( - rewardConfig.token, - _WETH, - tokenBalance, - output, - index, - address(0) - ) + tokenBalance, + output, + index, + address(0) ); + + // Calculate and take out the fees from the output before supplying to Comet. + uint256 rewardFeeShare = actualOutput.mulDiv(rewardFee, _FEE_BASE); + + unchecked { + // `rewardFeeShare` is a percentage of the output so we can safely subtract it. + actualOutput -= rewardFeeShare; + + if (rewardFeeShare != 0) { + uint256 ownerFeeShare = rewardFeeShare / 2; + + _WETH.safeTransfer(owner(), ownerFeeShare); + _WETH.safeTransfer( + feeDistributor, + rewardFeeShare - ownerFeeShare + ); + } + + IComet(_COMET).supply(_WETH, actualOutput); + } } /** diff --git a/test/BrrETH.t.sol b/test/BrrETH.t.sol index 4e2df1f..0352dd2 100644 --- a/test/BrrETH.t.sol +++ b/test/BrrETH.t.sol @@ -5,6 +5,7 @@ import "forge-std/Test.sol"; import {Ownable} from "solady/auth/Ownable.sol"; import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; import {ERC20} from "solady/tokens/ERC20.sol"; +import {FixedPointMathLib} from "solady/utils/FixedPointMathLib.sol"; import {Helper} from "test/Helper.sol"; import {BrrETH} from "src/BrrETH.sol"; import {IComet} from "src/interfaces/IComet.sol"; @@ -16,18 +17,19 @@ interface IComet2 { function withdraw(address asset, uint amount) external; } -contract BrrETHTest is Helper, Test { +contract BrrETHTest is Helper { using SafeTransferLib for address; + using FixedPointMathLib for uint256; address public immutable owner = address(this); - BrrETH public immutable vault = new BrrETH(); + BrrETH public immutable vault = new BrrETH(address(this)); constructor() { _WETH.safeApproveWithRetry(_COMET, type(uint256).max); _COMET.safeApproveWithRetry(address(vault), type(uint256).max); } - function _getCWETH(uint256 amount) private returns (uint256 balance) { + function _getCWETH(uint256 amount) internal returns (uint256 balance) { deal(_WETH, address(this), amount); balance = _COMET.balanceOf(address(this)); @@ -37,6 +39,15 @@ contract BrrETHTest is Helper, Test { balance = _COMET.balanceOf(address(this)) - balance; } + function _calculateFees( + uint256 amount + ) internal view returns (uint256 ownerShare, uint256 feeDistributorShare) { + uint256 rewardFee = vault.rewardFee(); + uint256 rewardFeeShare = amount.mulDiv(rewardFee, _FEE_BASE); + ownerShare = rewardFeeShare / 2; + feeDistributorShare = rewardFeeShare - ownerShare; + } + /*////////////////////////////////////////////////////////////// constructor //////////////////////////////////////////////////////////////*/ @@ -104,11 +115,14 @@ contract BrrETHTest is Helper, Test { //////////////////////////////////////////////////////////////*/ function testRebase() external { - _getCWETH(10 ether); + uint256 assets = 10000000000000401; + uint256 accrualTime = 187; + + _getCWETH(assets); - vault.deposit(10 ether, address(this)); + vault.deposit(assets, address(this)); - skip(10_000); + skip(accrualTime); IComet(_COMET).accrueAccount(address(vault)); @@ -116,18 +130,91 @@ contract BrrETHTest is Helper, Test { address(vault) ); uint256 rewardsBalance = userBasic.baseTrackingAccrued * 1e12; + + if (rewardsBalance == 0) return; + + (, uint256 output) = IRouter(_ROUTER).getSwapOutput( + keccak256(abi.encodePacked(_COMP, _WETH)), + rewardsBalance + ); + (uint256 ownerShare, uint256 feeDistributorShare) = _calculateFees( + output + ); + output -= ownerShare + feeDistributorShare; + uint256 newAssets = output - 1; + uint256 totalAssets = vault.totalAssets(); + uint256 totalSupply = vault.totalSupply(); + uint256 ownerBalance = _WETH.balanceOf(vault.owner()); + uint256 feeDistributorBalance = _WETH.balanceOf(vault.feeDistributor()); + + vault.rebase(); + + assertEq(totalAssets + newAssets, vault.totalAssets()); + assertEq(totalSupply, vault.totalSupply()); + + if (vault.owner() == vault.feeDistributor()) { + assertEq( + ownerBalance + ownerShare + feeDistributorShare, + _WETH.balanceOf(vault.owner()) + ); + } else { + assertEq(ownerBalance + ownerShare, _WETH.balanceOf(vault.owner())); + assertEq( + feeDistributorBalance + feeDistributorShare, + _WETH.balanceOf(vault.feeDistributor()) + ); + } + } + + function testRebaseFuzz(uint80 assets, uint24 accrualTime) external { + vm.assume(assets > 0.01 ether && accrualTime > 100); + + _getCWETH(assets); + + vault.deposit(assets, address(this)); + + skip(accrualTime); + + IComet(_COMET).accrueAccount(address(vault)); + + IComet.UserBasic memory userBasic = IComet(_COMET).userBasic( + address(vault) + ); + uint256 rewardsBalance = uint256(userBasic.baseTrackingAccrued) * 1e12; + + if (rewardsBalance == 0) return; + (, uint256 output) = IRouter(_ROUTER).getSwapOutput( keccak256(abi.encodePacked(_COMP, _WETH)), rewardsBalance ); + (uint256 ownerShare, uint256 feeDistributorShare) = _calculateFees( + output + ); + output -= ownerShare + feeDistributorShare; uint256 newAssets = output - 1; uint256 totalAssets = vault.totalAssets(); uint256 totalSupply = vault.totalSupply(); + uint256 ownerBalance = _WETH.balanceOf(vault.owner()); + uint256 feeDistributorBalance = _WETH.balanceOf(vault.feeDistributor()); vault.rebase(); assertEq(totalAssets + newAssets, vault.totalAssets()); assertEq(totalSupply, vault.totalSupply()); + + if (vault.owner() == vault.feeDistributor()) { + assertEq( + ownerBalance + ownerShare + feeDistributorShare, + _WETH.balanceOf(vault.owner()) + ); + } else { + assertEq(ownerBalance + ownerShare, _WETH.balanceOf(vault.owner())); + assertEq( + feeDistributorBalance + feeDistributorShare, + _WETH.balanceOf(vault.feeDistributor()) + ); + } } /*////////////////////////////////////////////////////////////// diff --git a/test/BrrETHManager.t.sol b/test/BrrETHManager.t.sol index 7ed53e3..3013c15 100644 --- a/test/BrrETHManager.t.sol +++ b/test/BrrETHManager.t.sol @@ -8,10 +8,10 @@ import {Helper} from "test/Helper.sol"; import {BrrETH} from "src/BrrETH.sol"; import {BrrETHManager} from "src/BrrETHManager.sol"; -contract BrrETHManagerTest is Helper, Test { +contract BrrETHManagerTest is Helper { using SafeTransferLib for address; - BrrETH public immutable vault = new BrrETH(); + BrrETH public immutable vault = new BrrETH(address(this)); BrrETHManager public immutable depositor = new BrrETHManager(address(vault)); diff --git a/test/Helper.sol b/test/Helper.sol index f6c24fb..4653bfd 100644 --- a/test/Helper.sol +++ b/test/Helper.sol @@ -1,13 +1,17 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; +import "forge-std/Test.sol"; +import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; import {ERC20} from "solady/tokens/ERC20.sol"; import {BrrETH} from "src/BrrETH.sol"; import {IComet} from "src/interfaces/IComet.sol"; import {ICometRewards} from "src/interfaces/ICometRewards.sol"; import {IWETH} from "src/interfaces/IWETH.sol"; -contract Helper { +contract Helper is Test { + using SafeTransferLib for address; + string internal constant _NAME = "Brrito-Compound WETH"; string internal constant _SYMBOL = "brr-cWETHv3"; address internal constant _WETH = @@ -20,4 +24,7 @@ contract Helper { 0x9e1028F5F1D5eDE59748FFceE5532509976840E0; ICometRewards internal constant _COMET_REWARDS = ICometRewards(0x123964802e6ABabBE1Bc9547D72Ef1B69B00A6b1); + uint256 internal constant _FEE_BASE = 10_000; + uint256 internal constant _MAX_REWARD_FEE = 2_000; + uint256 internal constant _MAX_WITHDRAW_FEE = 5; }