ClankerHookDynamicFee

Documentation for Clanker's Dynamic Fee Hook

The ClankerHookDynamicFee hook inherits from the ClankerHook and is used to set a dynamic fee for both the deployed token and the paired token. The math governing the dynamic fee took inspiration from Meteora's Dynamic Liquidity Market Maker's fee model.

The goal of the dynamic fee is to charge higher fees when there is volatility is present on the pool. We measure the volatility by simulating the price impact of a swap (measured in the difference between the beforeTick and afterTick) and applying a fee based on the magnitude. The resulting fee we apply is a mixture of the tick difference, recent swaps on the pool, and safeguards to prevent bot manipulation.

User Controlled Configuration Variables

The following configuration is used to control the range and sensitivity of the dynamic fees.

struct PoolDynamicConfigVars {
    uint24 baseFee; // the minimum LP fee to be taken on a swap
    uint24 maxLpFee; // the maximum LP fee to be taken on a swap
    uint256 referenceTickFilterPeriod; 
    uint256 resetPeriod;
    int24 resetTickFilter;
    uint256 feeControlNumerator; // the denominator is set to 10_000_000_000
    uint24 decayFilterBps;
}

Users are able to tune the dynamic fees as they choose, but we do provide a set of recommended configuration values on deployments through our website and API. We also have a spreadsheet that replicates this process and can provide it upon request.

Dynamic Fee Calculation

The dynamic fee is calculated by the following equation:

uint24 dynamicFee = 
  MIN(
      (baseFee + (feeControlNumerator * (volatilityAccumulator ** 2)) / FEE_CONTROL_DENOMINATOR),
      maxLpFee
  );

The volatilityAccumulator is a value that is updated on each swap and is calculated by the process described below.

Volatility Accumulator

The volatility accumulator is our way of measuring the amount of volatility on the pool. It is a value that is updated on each swap and is affected by recent swaps on the pool under different time frames. We use two lagging tick values, two time periods, and a decayed version of a previous volatility accumulator to calculate a swap's volatility accumulator.

The equation for the volatility accumulator is:

volatilityAccumulator = |afterTick - referenceTick| + prevVR;

The values of referenceTick and prevVR are updated based on snapshots of pool activity captured by the lagging ticks and time periods.

The two lagging tick values are:

  • referenceTick: When the referenceTickFilterPeriod has passed or if a resetPeriod has been triggered with a failing reset condition, this tick value is updated to the beforeTick of a swap. This tick value is kept the same until hitting one of these update conditions.

  • resetTick: This tick is updated to the beforeTick of a swap when the referenceTick is updated, and is additionally updated when the resetPeriod has passed and a swap's beforeTick is at least resetTickFilter ticks away from the resetTick (a passing reset condition).

The two time periods and their uses are:

  • referenceTickFilterPeriod: If the previous swap was less than referenceTickFilterPeriod seconds away, we use the recorded referenceTick for the dynamic fee calculation instead of the beforeTick of the current swap. This prevents bots from avoiding the dynamic fee by cutting their swap into smaller swaps.

  • resetPeriod: If a referenceTick has not been updated for over resetPeriod seconds, we want to ensure that there is true volatility happening on the pool. The goal is to prevent bots from keeping the pool's fee high by sending in micro swaps to avoid updating the referenceTick. If the resetPeriod has passed and the beforeTick's difference from the recorded resetTick is smaller than the resetTickFilter (a failing reset condition), we update both the referenceTick and resetTick to the beforeTick of the current swap to clear out the stored volatility. If the resetPeriod has passed and the beforeTick's difference from the recorded resetTick is larger than the resetTickFilter (a passing reset condition), we only update the resetTick to the beforeTick of the current swap and keep the referenceTick the same.

Additionally, the two time periods are used to determine the prevVR value. The prevVR is used to smooth volatility over a period of time and is set to a value when the referenceTickFilterPeriod has passed but the resetPeriod has not. The value is set to the last swap's volatility accumulator multiplied by the decayFilterBps value. The prevVR is set to zero when the resetPeriod passes with a failed condition or if a resetPeriod amount of time has passed without a swap. The same prevVR is used as long as the referenceTick has not been updated.

Note: there is a slight drift between the simulated afterTick and the resulting afterTick. This is because the applied fee for the simulated swap is an estimate either reusing the previous swap fee or just the prevVR under some reset conditions. We deemed that this drift is acceptable for our intended outcomes.

Last updated