How rules work
A rule is a condition on a market metric and a signed change to the pool's economics. Rules are written once at launch and enforced on every swap thereafter.
The format
IF <metric> <GT|GTE|LT|LTE> <threshold>
THEN <signed deltas to holder / staker / lp / tax>
Four operators, up to sixteen rules, applied in index order from the base state. Order matters: rule 2 acts on the state rule 1 produced.
A worked example
Base 5% — holders 2%, stakers 3%:
| Condition | Holders | Stakers | Total |
|---|---|---|---|
| no rule active | 2% | 3% | 5% |
| 1-day change < 0 | 3% | 2% | 5% |
| … and P/E > 50 | 4% | 1% | 5% |
A rule does not have to raise the total. Moving the split between holders and stakers while the trader's cost stays the same is a normal use of the engine.
The hook never evaluates a rule
evaluatePool() collapses oracle data into a single cached
EffectivePoolState. beforeSwap reads only that. Two things follow from that:
- Swap gas is constant whether a pool has one rule or sixteen.
- Evaluation is permissionless. It is deterministic from oracle state, so there is no keeper to trust.
One unusable metric discards everything
If any metric a pool references is unusable, the rule engine discards the whole evaluation and the pool falls back to its high-water maximum — permanently and silently. A P/E rule on a stock with no fundamentals is not a rule that never fires; it is a pool pinned to its most expensive rate forever.
The rule builder gates on per-stock metric support to prevent this. If you write rules against the
contracts directly, check supportsMetric for that specific stock first.
Inactive is not the same as unevaluated
On a pool serving its stale fallback, the active-rule bitmap reads 0 because
nothing was evaluated. That is a different fact from every rule having been
evaluated and found false. An interface that renders the second as the first is telling a
confident lie, so ours renders them distinctly.