# AFX LP Bonus Window — Accrual Formula

Everything needed to independently recompute every reward from public on-chain
data. The published accrual engine lives in `offchain/lp-accrual/` in this repo.

## Scope

- **Pool:** the AFX/ETH Uniswap V4 pool on Ethereum mainnet, pool id
  `0x0a1f2292e4f2f495c1deb64caf7dfed831fead1a39ce7a11c877eeab574c4ab8`
  (PoolManager `0x000000000004444c5dc75cB358380D2e3dE08A90`). No other pool,
  chain, or venue accrues.
- **Tokens:** ETH (native, `currency0`) and AFX
  `0x9414E9af3a531E47f0F2B48d1a7fAf8178cF0BD7` (`currency1`). Native ETH
  always sorts first in Uniswap V4, so ETH is `currency0` in this pool.
- **Positions:** only positions minted through the canonical V4 PositionManager
  `0xbD216513d74C8cf14cf4747E6AaA6420FF64ee9e` (i.e. "provide liquidity on the
  Uniswap app"). Liquidity added directly to the PoolManager without an NFT is
  not counted. You keep custody of your NFT the whole time; there is nothing to
  deposit and nothing to approve.
- **Owner attribution:** rewards accrue to the NFT's owner address at each
  moment, from the ERC-721 `Transfer` history. Contract-owned positions
  (Safes, smart wallets) accrue normally unless the owner appears on the
  published exclusion list (the project's own liquidity positions, listed in
  `offchain/lp-accrual/config/mainnet.json`). Excluded positions earn nothing
  and are removed from the denominator. A burned or zero-address-owned
  position is treated the same way — its weight is likewise removed from the
  denominator (never attributed to `0x0`), so its share redistributes to the
  remaining eligible LPs exactly as for a configured excluded owner.
- **Exclusion list (final, fixed for the whole window):**

      0x550F25f78DAF6CCBda6c4F77e239B15b6AD48A8B   treasury Safe (3/5)
      0x1cFd452EB369a7B9475B07D1457dd1d0500fD788   deployer

  These are the project's own two liquidity positions — every AFX of the
  300,000,000 goes to third-party liquidity. The list is fixed at programme
  start and cannot grow afterwards: adding an address later would drop a
  published cumulative to zero and strand that account's unclaimed proof, so
  the engine refuses it.

## Weight

At every instant a position's weight is its **ETH-side value if currently
in range, else zero** — the same rule Stratus staking uses on-chain
(`src/pool/LiquidityValue.sol`):

    in-range  ⇔  tickLower ≤ currentTick < tickUpper
    weight    =  amount0 + amount1 · (2^96)² / sqrtPriceX96²      (ETH terms)

`amount0` is the ETH side: ETH is `currency0` and AFX is `currency1` in this
pool, i.e. `projectIsCurrency0 = false` in `LiquidityValue.sol` terms.

where `amount0`/`amount1` are the position's current token amounts from its
liquidity and range at the current price. Concentration does not multiply
weight: weight is capital present, not liquidity units. A position that drifts
out of range accrues nothing until price returns.

## Time integration

Accrual is event-driven and continuous — there are no snapshots to game. The
engine replays every `Initialize`/`Swap`/`ModifyLiquidity` event of the pool
and every PositionManager `Transfer`, in (block, logIndex) order, and between
consecutive events each in-range position earns

    reward_i = rate(t) · Δt · weight_i / Σ weight

`rate(t)` is the epoch's budget divided by its duration. Intervals with zero
in-range eligible weight emit nothing; that emission stays in the treasury and
is reported as `undistributed` (it does not roll over, and it is not re-emitted
later — an interval with no eligible LPs is simply never funded).

Because excluded positions leave the denominator entirely, the project's own
liquidity dilutes nobody: at every instant the emission is divided only among
eligible LPs. Both positions in the pool at programme start are excluded, so
the earliest third-party LPs take the full emission rate for as long as they
are the only eligible in-range positions.

## Schedule (final)

| Epoch | Days | Budget (AFX) |
|-------|------|--------------|
| 1 | 0–30 | 60,000,000 |
| 2 | 30–60 | 55,000,000 |
| 3 | 60–90 | 50,000,000 |
| 4 | 90–120 | 47,500,000 |
| 5 | 120–150 | 45,000,000 |
| 6 | 150–180 | 42,500,000 |

Total 300,000,000 AFX, ratified by the 2026-07-24 governance vote. Each epoch
is exactly 30 days (2,592,000 seconds) and epochs are contiguous, so the window
runs 180 days.

**The window opens Monday 2026-08-03 at 16:00 UTC** and runs 180 days, closing
on or about 2027-01-30.

Epoch 1 begins at the timestamp of the start block — a marker transaction posted
on-chain by the deployer at that time, whose hash and block number are published
here and in the announcement. The true epoch boundaries are that timestamp plus
multiples of 2,592,000 seconds, so they land within a block or two of the
nominal dates below:

| Epoch | Opens (nominal UTC) | Closes (nominal UTC) | Budget (AFX) |
|-------|---------------------|----------------------|--------------|
| 1 | 2026-08-03 16:00 | 2026-09-02 16:00 | 60,000,000 |
| 2 | 2026-09-02 16:00 | 2026-10-02 16:00 | 55,000,000 |
| 3 | 2026-10-02 16:00 | 2026-11-01 16:00 | 50,000,000 |
| 4 | 2026-11-01 16:00 | 2026-12-01 16:00 | 47,500,000 |
| 5 | 2026-12-01 16:00 | 2026-12-31 16:00 | 45,000,000 |
| 6 | 2026-12-31 16:00 | 2027-01-30 16:00 | 42,500,000 |

Accrual starts the instant the marker block lands, and every eligible position
in range at that moment begins earning together, split by ETH-side value. There
is no advantage to adding liquidity on the day over adding it a week early —
what matters is being in range when the window opens.

Because the project's own two positions are excluded, third-party LPs share the
entire emission among themselves, however few of them there are. So while the
pool is thin, each LP's share per unit of capital is correspondingly large. If
at some moment no eligible position is in range at all, that interval's emission
is simply not distributed (see `undistributed` above).

## Claims

Monthly. After each epoch the engine's output (per-address cumulative totals,
merkle root, proofs) is published in this repo, and the root is posted to the
`MerkleRewardDistributor` (`src/incentives/`). Leaves are OpenZeppelin
StandardMerkleTree `["address","uint256"]` pairs of **cumulative lifetime
total**; a claim pays cumulative-minus-already-claimed, so skipping months
loses nothing. The distributor holds only the current tranche of rewards —
never LP positions.

## Reproducing a root

    offchain/lp-accrual> npm run epoch -- --config config/mainnet.json --epoch N

against any Ethereum archive RPC. Byte-identical `reports/epoch-N.json`
(including the root) is the acceptance test.
