Skip to content

Instantly share code, notes, and snippets.

@dicethedev
Created June 3, 2026 15:48
Show Gist options
  • Select an option

  • Save dicethedev/0ef000b2e9d7624621a95ab11753a3bc to your computer and use it in GitHub Desktop.

Select an option

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.
// 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