diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index bd324220..d8a3fc6c 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -362,7 +362,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { // base input expressed in liquidity terms, multiply by buyPrice to get liquidity output. amountOut = convertedAmountIn * config.buyPrice / PRICE_SCALE; - _accrueSwapFee(config.buyPrice, amountOut); + _accrueSwapFee(config.buyPrice, config.crossPrice, amountOut); _ensureLiquidityAvailableForSwap(amountOut); } @@ -405,7 +405,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { // Divide the exact liquidity output by buyPrice to solve for the required base input. amountIn = convertedAmountOut * PRICE_SCALE / config.buyPrice + 3; - _accrueSwapFee(config.buyPrice, amountOut); + _accrueSwapFee(config.buyPrice, config.crossPrice, amountOut); _ensureLiquidityAvailableForSwap(amountOut); } @@ -486,12 +486,13 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { return IAssetAdapter(config.adapter).convertToShares(assets); } - /// @dev Accrue fees on discounted buy-side swaps using the executed base buy price. + /// @dev Accrue fees on discounted buy-side swaps using the recognized NAV gain. /// @param buyPrice Price the ARM paid for the base asset. + /// @param crossPrice Price used to value the base asset in totalAssets(). /// @param amountOut Liquidity asset amount paid out by the ARM. - function _accrueSwapFee(uint256 buyPrice, uint256 amountOut) internal { + function _accrueSwapFee(uint256 buyPrice, uint256 crossPrice, uint256 amountOut) internal { uint256 feeMultiplier = - buyPrice == 0 ? 0 : (PRICE_SCALE - buyPrice) * uint256(fee) * PRICE_SCALE / (buyPrice * FEE_SCALE); + buyPrice == 0 ? 0 : (crossPrice - buyPrice) * uint256(fee) * PRICE_SCALE / (buyPrice * FEE_SCALE); feesAccrued = SafeCast.toUint128(feesAccrued + amountOut * feeMultiplier / PRICE_SCALE); } diff --git a/test/fork/EthenaARM/SwapExactTokensForTokens.t.sol b/test/fork/EthenaARM/SwapExactTokensForTokens.t.sol index e4a12ffb..ea3f6150 100644 --- a/test/fork/EthenaARM/SwapExactTokensForTokens.t.sol +++ b/test/fork/EthenaARM/SwapExactTokensForTokens.t.sol @@ -89,8 +89,8 @@ contract Fork_Concrete_EthenaARM_swapExactTokensForTokens_Test_ is Fork_Shared_T // Precompute expected amount out uint256 traderate = _buyPrice(); uint256 expectedAmountOut = (susde.convertToAssets(AMOUNT_IN) * traderate) / 1e36; - uint256 expectedFee = - expectedAmountOut * _swapFeeMultiplier(_buyPrice(), ethenaARM.fee()) / ethenaARM.PRICE_SCALE(); + uint256 expectedFee = expectedAmountOut * _swapFeeMultiplier(_buyPrice(), _crossPrice(), ethenaARM.fee()) + / ethenaARM.PRICE_SCALE(); // Expected events vm.expectEmit({emitter: address(susde)}); diff --git a/test/fork/EthenaARM/shared/Shared.sol b/test/fork/EthenaARM/shared/Shared.sol index ab35f8ff..93a2c60e 100644 --- a/test/fork/EthenaARM/shared/Shared.sol +++ b/test/fork/EthenaARM/shared/Shared.sol @@ -119,10 +119,15 @@ abstract contract Fork_Shared_Test is Base_Test_ { sellPrice = sellPriceMem; } - function _swapFeeMultiplier(uint256 buyPrice, uint256 fee) internal view returns (uint256) { + function _crossPrice() internal view returns (uint256 crossPrice) { + (,,,, uint128 crossPriceMem,,,) = ethenaARM.baseAssetConfigs(address(susde)); + crossPrice = crossPriceMem; + } + + function _swapFeeMultiplier(uint256 buyPrice, uint256 crossPrice, uint256 fee) internal view returns (uint256) { uint256 priceScale = ethenaARM.PRICE_SCALE(); if (buyPrice == 0 || fee == 0) return 0; - return (priceScale - buyPrice) * fee * priceScale / (buyPrice * ethenaARM.FEE_SCALE()); + return (crossPrice - buyPrice) * fee * priceScale / (buyPrice * ethenaARM.FEE_SCALE()); } function _ignite() internal virtual { diff --git a/test/unit/OriginARM/CollectFees.sol b/test/unit/OriginARM/CollectFees.sol index 1ccc679b..acd8aea9 100644 --- a/test/unit/OriginARM/CollectFees.sol +++ b/test/unit/OriginARM/CollectFees.sol @@ -13,7 +13,8 @@ contract Unit_Concrete_OriginARM_CollectFees_Test_ is Unit_Shared_Test { vm.stopPrank(); amountIn = amounts[0]; - expectedFee = amountOut * _swapFeeMultiplier(_buyPrice(), originARM.fee()) / originARM.PRICE_SCALE(); + expectedFee = + amountOut * _swapFeeMultiplier(_buyPrice(), _crossPrice(), originARM.fee()) / originARM.PRICE_SCALE(); } function test_RevertWhen_CollectFees_Because_InsufficientLiquidity() public deposit(alice, DEFAULT_AMOUNT) { @@ -61,4 +62,34 @@ contract Unit_Concrete_OriginARM_CollectFees_Test_ is Unit_Shared_Test { // Ensure there nothing has been allocated assertEq(weth.balanceOf(feeCollector), collectorBalance + expectedFees, "Collector balance should change"); } + + function test_SwapFee_IsBoundedByCrossPriceNavGain() public { + uint256 crossPrice = 9998 * 1e32; + uint256 buyPrice = 9997 * 1e32; + uint256 amountIn = 100 ether; + + vm.startPrank(governor); + originARM.setFee(originARM.FEE_SCALE() / 2); + originARM.setCrossPrice(address(oeth), crossPrice); + originARM.setPrices(address(oeth), buyPrice, crossPrice, type(uint128).max, type(uint128).max); + vm.stopPrank(); + + deal(address(weth), address(originARM), amountIn); + deal(address(oeth), bob, amountIn); + uint256 totalAssetsBefore = originARM.totalAssets(); + + vm.startPrank(bob); + oeth.approve(address(originARM), amountIn); + uint256[] memory amounts = originARM.swapExactTokensForTokens(oeth, weth, amountIn, 0, bob); + vm.stopPrank(); + + uint256 amountOut = amounts[1]; + uint256 recognizedNavGain = amountOut * (crossPrice - buyPrice) / buyPrice; + uint256 expectedFee = + amountOut * _swapFeeMultiplier(buyPrice, crossPrice, originARM.fee()) / originARM.PRICE_SCALE(); + + assertEq(originARM.feesAccrued(), expectedFee, "Wrong bounded swap fee"); + assertLe(originARM.feesAccrued(), recognizedNavGain, "Fee exceeds recognized NAV gain"); + assertGe(originARM.totalAssets(), totalAssetsBefore, "Swap fee should not reduce total assets"); + } } diff --git a/test/unit/OriginARM/Setters.sol b/test/unit/OriginARM/Setters.sol index 0b6b7f39..ccbc04b7 100644 --- a/test/unit/OriginARM/Setters.sol +++ b/test/unit/OriginARM/Setters.sol @@ -161,8 +161,8 @@ contract Unit_Concrete_OriginARM_Setters_Test_ is Unit_Shared_Test { originARM.setFee(newFee); assertEq(originARM.fee(), newFee, "Wrong fee"); assertEq( - _swapFeeMultiplier(_buyPrice(), originARM.fee()), - _expectedSwapFeeMultiplier(_buyPrice(), newFee), + _swapFeeMultiplier(_buyPrice(), _crossPrice(), originARM.fee()), + _expectedSwapFeeMultiplier(_buyPrice(), _crossPrice(), newFee), "Wrong swap fee multiplier" ); assertEq(weth.balanceOf(originARM.feeCollector()), feeCollectorBalanceBefore, "Wrong fee collector balance"); @@ -185,8 +185,8 @@ contract Unit_Concrete_OriginARM_Setters_Test_ is Unit_Shared_Test { originARM.setFee(newFee); assertEq(originARM.fee(), newFee, "Wrong fee"); assertEq( - _swapFeeMultiplier(_buyPrice(), originARM.fee()), - _expectedSwapFeeMultiplier(_buyPrice(), newFee), + _swapFeeMultiplier(_buyPrice(), _crossPrice(), originARM.fee()), + _expectedSwapFeeMultiplier(_buyPrice(), _crossPrice(), newFee), "Wrong swap fee multiplier" ); assertEq(weth.balanceOf(feeCollector), feeCollectorBalanceBefore + feeToCollect, "Wrong fee collector balance"); @@ -250,8 +250,8 @@ contract Unit_Concrete_OriginARM_Setters_Test_ is Unit_Shared_Test { assertEq(_buyLiquidityRemaining(), newBuyLiquidity, "Wrong buy liquidity"); assertEq(_sellLiquidityRemaining(), newSellLiquidity, "Wrong sell liquidity"); assertEq( - _swapFeeMultiplier(_buyPrice(), originARM.fee()), - _expectedSwapFeeMultiplier(newBuyPrice, originARM.fee()), + _swapFeeMultiplier(_buyPrice(), _crossPrice(), originARM.fee()), + _expectedSwapFeeMultiplier(newBuyPrice, _crossPrice(), originARM.fee()), "Wrong swap fee multiplier" ); } @@ -305,9 +305,13 @@ contract Unit_Concrete_OriginARM_Setters_Test_ is Unit_Shared_Test { assertEq(_crossPrice(), crossPrice + 1, "Wrong cross price"); } - function _expectedSwapFeeMultiplier(uint256 buyT1, uint256 fee) internal view returns (uint256) { + function _expectedSwapFeeMultiplier(uint256 buyT1, uint256 crossPrice, uint256 fee) + internal + view + returns (uint256) + { uint256 priceScale = originARM.PRICE_SCALE(); if (buyT1 == 0 || fee == 0) return 0; - return (priceScale - buyT1) * fee * priceScale / (buyT1 * originARM.FEE_SCALE()); + return (crossPrice - buyT1) * fee * priceScale / (buyT1 * originARM.FEE_SCALE()); } } diff --git a/test/unit/shared/Shared.sol b/test/unit/shared/Shared.sol index a09c0b25..bcf3c973 100644 --- a/test/unit/shared/Shared.sol +++ b/test/unit/shared/Shared.sol @@ -168,9 +168,9 @@ abstract contract Unit_Shared_Test is Base_Test_, Modifiers { crossPrice = crossPriceMem; } - function _swapFeeMultiplier(uint256 buyPrice, uint256 fee) internal view returns (uint256) { + function _swapFeeMultiplier(uint256 buyPrice, uint256 crossPrice, uint256 fee) internal view returns (uint256) { uint256 priceScale = originARM.PRICE_SCALE(); if (buyPrice == 0 || fee == 0) return 0; - return (priceScale - buyPrice) * fee * priceScale / (buyPrice * originARM.FEE_SCALE()); + return (crossPrice - buyPrice) * fee * priceScale / (buyPrice * originARM.FEE_SCALE()); } }