> ## Documentation Index
> Fetch the complete documentation index at: https://pulse-hook.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Penalty Curve

Penalty-curve model enables dynamic fees in step 5. It is the part where MEV bots really receive what they deserve.

The hook charges every swap a small baseline fee (0.1%), plus an extra penalty if the swap's priority fee is unusually high compared to the recent reference median. The penalty only kicks in past a threshold, then grows smoothly up to a hard cap.

To decide the penalty, the hook first computes the ratio of the swap's priority fee to the reference median priority fee. If that ratio is above **2.7x**, the penalty kicks in (see [The formula](#the-formula) below for the exact math).

Check the table below to see what ratio will result in what total fee.

| Ratio      | Basic Fee | Penalty Part | Total Fee  |
| ---------- | --------- | ------------ | ---------- |
| **≤ 2.7x** | 0.1%      | 0%           | 0.1%       |
| **\~4-5x** | 0.1%      | \~1-2%       | \~1.1-2.1% |
| **\~7x**   | 0.1%      | \~3-5%       | \~3.1-5.1% |
| **10x**    | 0.1%      | 10% (capped) | 10.1%      |
| **20x**    | 0.1%      | 10% (capped) | 10.1%      |

**So the penalty curve looks like this:**

<Frame>
  <img alt="Frame 49" title="Frame 49" lightAlt="Frame 49" darkAlt="Frame 49" src="https://mintcdn.com/pulse-hook/aCc5bHEvzSklvwEK/images/Frame-52-1.png?fit=max&auto=format&n=aCc5bHEvzSklvwEK&q=85&s=6664023456b5379c56421b7f16380196" className="dark:hidden" width="1100" height="700" data-path="images/Frame-52-1.png" />

  <img alt="Frame 49" title="Frame 49" lightAlt="Frame 49" darkAlt="Frame 49" src="https://mintcdn.com/pulse-hook/aCc5bHEvzSklvwEK/images/Frame-49.png?fit=max&auto=format&n=aCc5bHEvzSklvwEK&q=85&s=1d270692e72eeb937053b5c8678b3f88" className="hidden dark:block" width="1100" height="700" data-path="images/Frame-49.png" />
</Frame>

We can see it is 1.5 curve that has delayed start and has a strict 10% penalty cap.

## Design questions

<Card title="Why is the basic fee 0.1%?">
  2
</Card>

<Card title="Why does the penalty start only from 2.7x?">
  1
</Card>

<Card title="Why is the cap 10%?">
  3
</Card>

## The formula

The curve is computed on-chain with the formula below, using OpenZeppelin's `Math` library.

```text theme={null}
r     = priority_fee / M                        // M = reference median priority fee
f(r)  = min((r - 2.7) / 7.3, 1)                  // 2.7 = threshold, 7.3 = saturation range

penalty(r) = 0                                   if r < 2.7
penalty(r) = 10% * f(r)^1.5                      if r >= 2.7

fee = 0.1% + penalty(r)                          // 0.1% = basic fee
```

### Why `frac^1.5` and not a "real" power function

Solidity has no cheap general `pow` for fractional exponents. But `frac^1.5` can be rewritten as:

```text theme={null}
frac^1.5 = frac * frac^0.5 = frac * sqrt(frac)
```

and `sqrt` is cheap on-chain via `Math.sqrt`. So the whole curve costs one multiplication and one integer square root, with no fixed-point `ln` / `exp` / `pow` library needed.

### Constants

| Constant              | Value    | Meaning                                                               |
| --------------------- | -------- | --------------------------------------------------------------------- |
| `PRECISION`           | 1000     | Fixed-point scale for the ratio (`1000` = 1.0x)                       |
| `WAD`                 | 1e18     | Fixed-point scale for the `frac^1.5` calculation                      |
| `RATIO_THRESHOLD`     | 2700     | Penalty starts at 2.7x the reference median                           |
| `D_CAP`               | 7300     | Width of the penalty range; penalty saturates at `2.7x + 7.3 = 10.0x` |
| `BASIC_FEE`           | 1000 ppm | Baseline fee, 0.1%, always charged                                    |
| `MAX_PENALTY_PERCENT` | 10       | Hard cap on the penalty, reached exactly at 10x                       |
| `PENALTY_UNIT`        | 10000    | Converts a plain percent into ppm (1% = 10,000 ppm)                   |

### Formula → code mapping

| Formula                        | Code                     |
| ------------------------------ | ------------------------ |
| `r` (scaled by 1000)           | `priorityFeeRatioScaled` |
| `2.7` threshold                | `RATIO_THRESHOLD = 2700` |
| `r - 2.7`                      | `excessRatioScaled`      |
| `7.3` saturation range         | `D_CAP = 7300`           |
| `f(r)`, in WAD                 | `fracWad`                |
| `sqrt(f(r))`                   | `sqrtFracWad`            |
| `f(r)^1.5 = f(r) * sqrt(f(r))` | `frac1_5Wad`             |
| `10% * f(r)^1.5`, in ppm       | `penaltyPpm`             |
| `fee`                          | `totalFee`               |

### How this looks in the code

All fixed-point math here uses two scales: `PRECISION = 1000` for the ratio (so `2700` means "2.7x"), and `WAD = 1e18` for the fractional-exponent part.

```solidity theme={null}
function getDynamicFee_(uint256 priorityFee, int256 referenceMedian) internal virtual returns (uint24) {
    // No reference data yet, fall back to the baseline fee to avoid
    // dividing by zero (mirrors the old medianPriorityFee == 0 check).
    if (referenceMedian <= 0) return BASIC_FEE;

    uint256 medianPriorityFee = uint256(referenceMedian);

    // How many times (scaled by PRECISION) this swap's priority fee
    // exceeds the smoothed reference. E.g. 2700 means "2.7x the
    // reference".
    uint256 priorityFeeRatioScaled = (priorityFee * PRECISION) / medianPriorityFee;

    uint256 penaltyPpm;
    if (priorityFeeRatioScaled < RATIO_THRESHOLD) {
        // Priority fee is within the tolerated range, no penalty.
        penaltyPpm = 0;
    } else {
        // How far above the threshold this swap's ratio is, scaled by
        // PRECISION (e.g. ratio 3.7 with threshold 2.7 gives an
        // excess of 1.0 * PRECISION).
        uint256 excessRatioScaled = priorityFeeRatioScaled - RATIO_THRESHOLD;

        // Fraction of the way through the penalty range [0, D_CAP],
        // expressed in WAD (1e18 = "fully saturated"). Clamped to
        // WAD instead of computed further once excess reaches D_CAP,
        // both to save gas and to guarantee no overflow regardless
        // of how large priorityFeeRatioScaled is.
        uint256 fracWad = excessRatioScaled >= D_CAP ? WAD : (excessRatioScaled * WAD) / D_CAP;

        // frac^1.5 = frac * sqrt(frac), computed in WAD fixed point.
        // Math.sqrt(fracWad * WAD) rescales sqrt(x/1e18) back to a
        // 1e18-scaled result (sqrt(1e36) == 1e18).
        uint256 sqrtFracWad = Math.sqrt(fracWad * WAD);
        uint256 frac1_5Wad = (fracWad * sqrtFracWad) / WAD;

        penaltyPpm = (frac1_5Wad * MAX_PENALTY_PERCENT * PENALTY_UNIT) / WAD;
    }

    // Final fee = baseline fee + penalty, in ppm.
    uint24 totalFee = BASIC_FEE + penaltyPpm.toUint24();

    return totalFee;
}
```

### Notes

* **`M` (reference median)** is a smoothed value, not the live running median directly — see \[link] for how it's derived.
* **Edge case:** if `referenceMedian <= 0` (no data yet), `getDynamicFee_` returns `BASIC_FEE` directly, skipping the ratio/penalty calculation to avoid division by zero.

## Key takeaway

* Every swap pays a flat 0.1% baseline.
* Nothing extra is charged until the priority fee is 2.7x the reference median.
* Above that, a smooth `frac^1.5` curve adds up to 10% more, saturating at 10x.
* The whole thing is computed with one multiplication and one `Math.sqrt`, cheap enough to run on every swap.
