<!-- file: docs/reports/FEE_EVENT_AMOUNTS_MODEL_NOTE_0_7_56.md -->

# Note technique — `k_sol_fee_event_amounts` et policy fees — `0.7.56`

## Objectif

La table `k_sol_fee_events` était suffisante pour un fee mono-montant, mais insuffisante pour les cas multi-mint, multi-destination ou multi-composant. `0.7.56` ajoute donc `k_sol_fee_event_amounts` comme table de legs rattachée au parent fee.

## Modèle

```text
k_sol_fee_events
  id
  transaction_id
  decoded_event_id
  fee_token_mint      nullable / vide si multi-leg
  fee_amount_raw      nullable / vide si multi-leg
  payload_json

k_sol_fee_event_amounts
  id
  fee_event_id
  transaction_id
  decoded_event_id
  leg_index
  fee_component_kind
  token_mint
  amount_raw
  source_account
  destination_account
  amount_source
  payload_json
```

## Règles invariantes

1. Un parent fee scalaire avec `fee_token_mint + fee_amount_raw` doit toujours avoir un leg `0`.
2. Un parent multi-leg ou multi-mint ne doit pas agréger artificiellement ses montants dans le parent.
3. Les legs doivent pointer vers le même `transaction_id` et `decoded_event_id` que le parent.
4. Les legs doivent être supprimés/remplacés lors du replay/cleanup parent.
5. Les transactions failed ne doivent pas créer de parent ou leg fee métier.
6. Les bornes d'instruction (`maxAmount`, `minAmount`, `u64::MAX`) ne sont pas des montants exécutés.

## Sources de montant acceptées

| `amount_source` | Usage |
|---|---|
| `parent_fee_event_amount` | Leg généré automatiquement depuis un parent scalaire déjà fiable. |
| `fee_event_amounts` | Source explicite reconstruite par un matérialisateur spécialisé. |
| `inner_spl_transfer` | CPI SPL vérifié dans un matérialisateur spécialisé. |
| `lamport_balance_delta` | Delta lamports prouvé et contextualisé. |
| `allowlisted_inner_spl_transfer` | Recovery générique mais strictement allowlistée pour event kinds déjà validés. |

## Recovery allowlistée

La recovery `allowlisted_inner_spl_transfer` n'est pas une règle globale. Elle s'applique seulement aux event kinds explicitement autorisés dans le code. Cette restriction protège les futurs décodeurs : une nouvelle surface ne doit jamais créer des legs fee à partir de transferts internes tant que la sémantique n'a pas été inspectée.

Résultats de validation croisée en `0.7.56` :

| Base / surface | Effet observé |
|---|---|
| `meteora_dbc` | Aucun usage de l'allowlist générique ; chemins spécialisés DBC conservés. |
| `raydium_launchpad` | `212` parents enrichis en legs : `claim_creator_fee`, `claim_platform_fee`, `claim_platform_fee_from_vault`, `collect_fee`. |
| `raydium_cpmm` | `collect_creator_fee` enrichi ; `collect_fund_fee` / `collect_protocol_fee` explicités sans transfert exploitable. |
| `pump_swap` | `collect_coin_creator_fee` et un `transfer_creator_fees_to_pump_v2` enrichis ; autres cas zero/no-transfer explicités. |
| `pump_fees` | `crank_donation_fee_pda` et `sweep_buyback` enrichis ; events déjà scalaires conservés. |

## Contrôles SQL obligatoires

### Parent scalaire sans leg

```sql
SELECT
    de.protocol_name,
    de.event_kind,
    tx.signature,
    fee.id AS fee_event_id,
    fee.fee_token_mint,
    fee.fee_amount_raw,
    fee.payload_json
FROM k_sol_fee_events fee
JOIN k_sol_dex_decoded_events de
    ON de.id = fee.decoded_event_id
JOIN k_sol_chain_transactions tx
    ON tx.id = fee.transaction_id
LEFT JOIN k_sol_fee_event_amounts fea
    ON fea.fee_event_id = fee.id
WHERE COALESCE(TRIM(fee.fee_token_mint), '') <> ''
  AND COALESCE(TRIM(fee.fee_amount_raw), '') <> ''
  AND fea.id IS NULL
ORDER BY
    de.protocol_name,
    de.event_kind,
    tx.signature
LIMIT 100;
```

Attendu : vide.

### Legs orphelins

```sql
SELECT
    fea.id,
    fea.fee_event_id,
    fea.transaction_id,
    fea.decoded_event_id
FROM k_sol_fee_event_amounts fea
LEFT JOIN k_sol_fee_events fee
    ON fee.id = fea.fee_event_id
WHERE fee.id IS NULL;
```

Attendu : vide.

### Résumé parent/legs par event

```sql
SELECT
    de.protocol_name,
    de.event_kind,
    COUNT(DISTINCT fee.id) AS fee_parent_count,
    COUNT(DISTINCT CASE
        WHEN COALESCE(TRIM(fee.fee_token_mint), '') <> ''
         AND COALESCE(TRIM(fee.fee_amount_raw), '') <> ''
        THEN fee.id
        ELSE NULL
    END) AS parent_with_scalar_amount_count,
    COUNT(DISTINCT fea.id) AS fee_amount_leg_count,
    MIN(tx.signature) AS sample_signature
FROM k_sol_fee_events fee
JOIN k_sol_dex_decoded_events de
    ON de.id = fee.decoded_event_id
JOIN k_sol_chain_transactions tx
    ON tx.id = fee.transaction_id
LEFT JOIN k_sol_fee_event_amounts fea
    ON fea.fee_event_id = fee.id
GROUP BY
    de.protocol_name,
    de.event_kind
ORDER BY
    de.protocol_name,
    de.event_kind;
```

## Règles pour `0.7.57 meteora_dlmm`

- Les instructions `claim_fee`, `claim_fee2`, `withdraw_protocol_fee`, `zap_protocol_fee`, `CompositionFee`, `ClaimFee`, `ClaimFee2` doivent utiliser `k_sol_fee_event_amounts` dès qu'un montant/mint fiable est disponible.
- Les rewards (`claim_reward*`, `fund_reward`, `withdraw_ineligible_reward`) vont vers `k_sol_reward_events`, pas vers fees, sauf sémantique contraire prouvée.
- Les `composition_fee` de swap/liquidity ne doivent pas être double-comptés si déjà inclus dans le trade/liquidity effectif.
- Toute recovery générique doit être déclarée par policy explicite et tests synthétiques ; ne pas ajouter DLMM à l'allowlist sans audit par event kind.
