Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions src/contracts/AbstractARM.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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);
}

Expand Down
4 changes: 2 additions & 2 deletions test/fork/EthenaARM/SwapExactTokensForTokens.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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)});
Expand Down
9 changes: 7 additions & 2 deletions test/fork/EthenaARM/shared/Shared.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
33 changes: 32 additions & 1 deletion test/unit/OriginARM/CollectFees.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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");
}
}
20 changes: 12 additions & 8 deletions test/unit/OriginARM/Setters.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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");
Expand Down Expand Up @@ -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"
);
}
Expand Down Expand Up @@ -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());
}
}
4 changes: 2 additions & 2 deletions test/unit/shared/Shared.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
Loading