Created
June 3, 2026 15:48
-
-
Save dicethedev/0ef000b2e9d7624621a95ab11753a3bc to your computer and use it in GitHub Desktop.
Dynamic AMM fee strategy using trade size, reserve imbalance, volatility proxy, and same-side flow to widen fees against toxic flow while discounting inventory-rebalancing trades.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // SPDX-License-Identifier: MIT | |
| pragma solidity ^0.8.24; | |
| import {AMMStrategyBase} from "./AMMStrategyBase.sol"; | |
| import {TradeInfo} from "./IAMMStrategy.sol"; | |
| /// @title Noise Volatility Plus Strategy | |
| /// @notice Dynamic AMM fee strategy tuned for volatility protection with controlled retail capture. | |
| contract Strategy is AMMStrategyBase { | |
| uint256 private constant LOCAL_WAD = 1e18; | |
| uint256 private constant BPS_DENOMINATOR = 10_000; | |
| uint256 private constant BASE_FEE_BPS = 35; | |
| uint256 private constant MIN_FEE_BPS = 16; | |
| uint256 private constant MAX_TARGET_FEE_BPS = 122; | |
| uint256 private constant INVENTORY_WEIGHT = 122; | |
| uint256 private constant FLOW_WEIGHT = 8; | |
| uint256 private constant VOLUME_WEIGHT = 7; | |
| uint256 private constant VOL_WEIGHT = 9; | |
| uint256 private constant GAP_WEIGHT = 2; | |
| uint256 private constant REBALANCE_DISCOUNT_BPS = 13; | |
| /// @notice Initialize the strategy with starting reserves. | |
| function afterInitialize(uint256 initialX, uint256 initialY) | |
| external | |
| override | |
| returns (uint256 bidFee, uint256 askFee) | |
| { | |
| slots[0] = initialX; | |
| slots[1] = initialY; | |
| slots[2] = initialX; | |
| slots[3] = initialY; | |
| slots[4] = BASE_FEE_BPS; | |
| slots[5] = 0; | |
| slots[6] = 0; | |
| slots[7] = _ratio(initialX, initialY); | |
| slots[8] = 0; | |
| uint256 fee = bpsToWad(BASE_FEE_BPS); | |
| return (fee, fee); | |
| } | |
| /// @notice Called after each trade to update bid and ask fees. | |
| function afterSwap(TradeInfo calldata trade) | |
| external | |
| override | |
| returns (uint256 bidFee, uint256 askFee) | |
| { | |
| uint256 previousFeeBps = slots[4] == 0 ? BASE_FEE_BPS : slots[4]; | |
| uint256 gap = trade.timestamp > slots[5] ? trade.timestamp - slots[5] : 1; | |
| (uint256 dynamicBps, uint256 imbalanceBps, uint256 volEmaBps) = | |
| _nextDynamicFeeBps(trade, previousFeeBps, gap); | |
| (uint256 bidBps, uint256 askBps) = _skewFees(dynamicBps, imbalanceBps, trade.reserveX, trade.reserveY); | |
| slots[2] = trade.reserveX; | |
| slots[3] = trade.reserveY; | |
| slots[4] = dynamicBps; | |
| slots[5] = trade.timestamp; | |
| slots[7] = _ratio(trade.reserveX, trade.reserveY); | |
| slots[8] = volEmaBps; | |
| return (clampFee(bpsToWad(bidBps)), clampFee(bpsToWad(askBps))); | |
| } | |
| /// @notice Get the strategy name for display. | |
| function getName() external pure override returns (string memory) { | |
| return "Noise Volatility Plus"; | |
| } | |
| function _nextDynamicFeeBps(TradeInfo calldata trade, uint256 previousFeeBps, uint256 gap) | |
| private | |
| returns (uint256 dynamicBps, uint256 imbalanceBps, uint256 volEmaBps) | |
| { | |
| uint256 volumeBps = _tradeSizeBps(trade.amountX, trade.reserveX, trade.amountY, trade.reserveY); | |
| uint256 ratioMoveBps = _ratioMoveBps(trade.reserveX, trade.reserveY); | |
| volEmaBps = ((slots[8] * 5) + ratioMoveBps) / 6; | |
| uint256 sameSidePressure = _updateFlowPressure(trade.isBuy); | |
| imbalanceBps = _inventoryImbalanceBps(trade.reserveX, trade.reserveY); | |
| dynamicBps = BASE_FEE_BPS; | |
| dynamicBps += (imbalanceBps * INVENTORY_WEIGHT) / BPS_DENOMINATOR; | |
| dynamicBps += (volEmaBps * VOL_WEIGHT) / 100; | |
| dynamicBps += (volumeBps * VOLUME_WEIGHT) / 100; | |
| if (sameSidePressure > 1) { | |
| dynamicBps += (sameSidePressure - 1) * FLOW_WEIGHT; | |
| } | |
| if (gap > 5) { | |
| uint256 cappedGap = gap > 25 ? 25 : gap; | |
| dynamicBps += cappedGap * GAP_WEIGHT; | |
| } | |
| if (volumeBps > 145 || ratioMoveBps > 125) { | |
| dynamicBps += 21; | |
| } | |
| if (volumeBps > 330 || ratioMoveBps > 270) { | |
| dynamicBps += 23; | |
| } | |
| if (dynamicBps < previousFeeBps) { | |
| dynamicBps = ((previousFeeBps * 2) + dynamicBps) / 3; | |
| } | |
| if (dynamicBps > MAX_TARGET_FEE_BPS) { | |
| dynamicBps = MAX_TARGET_FEE_BPS; | |
| } | |
| } | |
| function _skewFees(uint256 dynamicBps, uint256 imbalanceBps, uint256 reserveX, uint256 reserveY) | |
| private | |
| view | |
| returns (uint256 bidBps, uint256 askBps) | |
| { | |
| bidBps = dynamicBps; | |
| askBps = dynamicBps; | |
| uint256 initialX = slots[0]; | |
| uint256 initialY = slots[1]; | |
| if (initialX == 0 || initialY == 0 || reserveX == 0 || reserveY == 0) { | |
| return (_floorFee(bidBps), _floorFee(askBps)); | |
| } | |
| uint256 currentRatio = (reserveX * LOCAL_WAD) / reserveY; | |
| uint256 targetRatio = (initialX * LOCAL_WAD) / initialY; | |
| uint256 skew = (imbalanceBps * 52) / BPS_DENOMINATOR; | |
| if (currentRatio > targetRatio) { | |
| bidBps += skew; | |
| askBps = _discount(askBps, REBALANCE_DISCOUNT_BPS + skew); | |
| } else if (currentRatio < targetRatio) { | |
| askBps += skew; | |
| bidBps = _discount(bidBps, REBALANCE_DISCOUNT_BPS + skew); | |
| } | |
| return (_floorFee(bidBps), _floorFee(askBps)); | |
| } | |
| function _updateFlowPressure(bool isBuy) private returns (uint256 pressure) { | |
| uint256 encoded = slots[6]; | |
| bool previousIsBuy = encoded >> 255 == 1; | |
| uint256 streak = encoded & type(uint128).max; | |
| if (streak == 0 || previousIsBuy != isBuy) { | |
| streak = 1; | |
| } else if (streak < 12) { | |
| streak += 1; | |
| } | |
| slots[6] = (isBuy ? (uint256(1) << 255) : 0) | streak; | |
| return streak; | |
| } | |
| function _inventoryImbalanceBps(uint256 reserveX, uint256 reserveY) private view returns (uint256) { | |
| uint256 initialX = slots[0]; | |
| uint256 initialY = slots[1]; | |
| if (initialX == 0 || initialY == 0 || reserveX == 0 || reserveY == 0) { | |
| return 0; | |
| } | |
| uint256 currentRatio = (reserveX * LOCAL_WAD) / reserveY; | |
| uint256 targetRatio = (initialX * LOCAL_WAD) / initialY; | |
| uint256 diff = currentRatio > targetRatio ? currentRatio - targetRatio : targetRatio - currentRatio; | |
| uint256 imbalance = (diff * BPS_DENOMINATOR) / targetRatio; | |
| return imbalance > BPS_DENOMINATOR ? BPS_DENOMINATOR : imbalance; | |
| } | |
| function _ratioMoveBps(uint256 reserveX, uint256 reserveY) private view returns (uint256) { | |
| uint256 previousRatio = slots[7]; | |
| uint256 currentRatio = _ratio(reserveX, reserveY); | |
| if (previousRatio == 0 || currentRatio == 0) { | |
| return 0; | |
| } | |
| uint256 diff = currentRatio > previousRatio | |
| ? currentRatio - previousRatio | |
| : previousRatio - currentRatio; | |
| uint256 moveBps = (diff * BPS_DENOMINATOR) / previousRatio; | |
| return moveBps > BPS_DENOMINATOR ? BPS_DENOMINATOR : moveBps; | |
| } | |
| function _ratio(uint256 reserveX, uint256 reserveY) private pure returns (uint256) { | |
| if (reserveX == 0 || reserveY == 0) { | |
| return 0; | |
| } | |
| return (reserveX * LOCAL_WAD) / reserveY; | |
| } | |
| function _tradeSizeBps(uint256 amountX, uint256 reserveX, uint256 amountY, uint256 reserveY) | |
| private | |
| pure | |
| returns (uint256) | |
| { | |
| uint256 xBps = reserveX == 0 ? 0 : (amountX * BPS_DENOMINATOR) / reserveX; | |
| uint256 yBps = reserveY == 0 ? 0 : (amountY * BPS_DENOMINATOR) / reserveY; | |
| uint256 sizeBps = (xBps + yBps) / 2; | |
| return sizeBps > BPS_DENOMINATOR ? BPS_DENOMINATOR : sizeBps; | |
| } | |
| function _discount(uint256 feeBps, uint256 discountBps) private pure returns (uint256) { | |
| if (discountBps >= feeBps) { | |
| return MIN_FEE_BPS; | |
| } | |
| return feeBps - discountBps; | |
| } | |
| function _floorFee(uint256 feeBps) private pure returns (uint256) { | |
| if (feeBps < MIN_FEE_BPS) { | |
| return MIN_FEE_BPS; | |
| } | |
| return feeBps; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment