Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions benchmarks/apps/04-addendum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# `/apps` — Addendum aux décisions ouvertes

**Date : 9 août 2026.** Ce document tranche les trois points laissés ouverts dans
`00-EXECUTION-BRIEF.md` et corrige un passage approximatif sur le budget de recompute.

---

## Décision 1 — Renommage de la colonne de classement

`net_real_revenue` est un abus de langage dès qu'on y inclut des buybacks : ce n'est pas
du revenue au sens comptable. Le terme induit le même péché de fusion que DefiLlama.

**Décision** : la colonne de classement s'appelle `net_value_capture_usd`, libellé UI
**« Valeur nette captée »**. La formule est affichée **en permanence** sous l'en-tête de
colonne (pas au survol) :

```
Valeur nette captée
= trésor + holders + burn − émissions
```

La barre segmentée décompose visuellement les trois termes positifs. Le chiffre scalaire
reste disponible pour le tri mais n'est jamais présenté sans sa formule.

**Cohérence avec P4** : `with_holders` est le défaut. `treasury_only` est disponible en
toggle discret. Les deux variantes sont calculées et stockées dans `fee_facts_windowed`
sous `variant IN ('with_holders', 'treasury_only')`.

---

## Décision 2 — Budget de recompute : critère chiffré

La formulation « c'est un simple GROUP BY, ça tient en 30 minutes » était paresseuse. Le
vrai goulot d'étranglement est la jointure de pricing sur `(token, hour)`.

**Analyse** :

- Seules les paires `(token, heure)` où un event a eu lieu sont pricées : la table de
prix est **creuse et pilotée par la demande**, pas dense.
- Les prix sont une observation (pas un artefact de méthodologie) : un recompute de
formule ne les invalide pas. Seul un changement de méthode de pricing le fait, et c'est
rare.
- Le long tail Solana (memecoins) tombe en `pool_ratio`, dérivé du swap lui-même. Ce sont
les cas les moins chers : aucun appel externe. Seuls ~50 tokens majeurs touchent Pyth.

**Critère d'acceptation chiffré (Phase 1)** :

```
Benchmark sur 20M d'events synthétiques, 3 000 tokens distincts :
- Recompute complet < 30 minutes sur le VPS (4 cores, 15GB RAM)
- EXPLAIN ANALYZE ne montre aucun Seq Scan sur fee_events ou fee_facts
- Si ces deux conditions ne sont pas satisfaites simultanément :
partitionner fee_events par mois (PARTITION BY RANGE sur ts)
et fee_facts par (bucket_start, methodology_version)
```

Le partitionnement n'est pas activé par défaut : il complique le schéma sans bénéfice
prouvé à l'échelle MVP. Le benchmark tranche, pas le raisonnement.

---

## Décision 3 — Aerodrome en T1, phase 4a

**Constat** : indexer les swaps Aerodrome en direct getLogs est intenable (milliers de
pools × millions de swaps). Mais on n'a pas besoin des swaps.

Les fees d'Aerodrome atterrissent dans les contrats `FeesVotingReward` (un par gauge),
via `notifyRewardAmount()` appelé par le Voter. L'event émis est :

```solidity
// IReward.sol — vérifié sur aerodrome-finance/contracts main
event NotifyReward(
address indexed from, // Voter
address indexed reward, // token de fee (USDC, WETH, etc.)
uint256 indexed epoch, // epochStart en secondes
uint256 amount // montant en unités natives
);

// topic0 vérifié :
// 0x52977ea98a2220a03ee9ba5cb003ada08d394ea10155483c95dc2dc77a7eb24b
```

Volume réel : O(epoch × gauges actives × tokens) — quelques centaines d'events par
semaine, pas un par swap. **Aerodrome est indexable en T1 sans Envio.**

**Prix à payer documenté** : granularité epoch (hebdomadaire). La fenêtre 24h d'Aerodrome
est de résolution dégradée — l'émission AERO est répartie uniformément sur 7 jours
(cf. `aerodrome.yaml : spread_over_epoch: true`) mais les fees sont discrets. L'UI affiche
un badge « résolution hebdomadaire » sur la ligne Aerodrome pour les fenêtres 24h et 7j.
La fenêtre 30j est pleine résolution.

**Comment découvrir les FeesVotingReward** : via `Voter.gaugeToFees(gaugeAddress)`.
Énumérer les gauges actifs depuis `Voter`, résoudre leur contrat de fees à l'init,
surveiller les nouveaux gauges à chaque epoch.

**Aerodrome passe de Phase 4 à Phase 4a** (parallèle à Uniswap, avant Raydium qui reste
le plus complexe).

---

## Correction — passage approximatif dans 01-architecture.md

Le §7 (séquencement) listait Raydium avant Uniswap. Ordre corrigé :

| Phase | Contenu |
|---|---|
| 1 | Socle + dYdX |
| 2 | Hyperliquid + GMX V2 |
| 3 | Page `/apps` + API publique (3 protocoles) |
| 4a | Aerodrome (T1 via NotifyReward) + Uniswap |
| 4b | Raydium (le plus complexe : accrual Solana) |
| 5 | Changelog, exports CSV, coverage page |
111 changes: 111 additions & 0 deletions benchmarks/apps/_taxonomy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Taxonomie partagée par toutes les specs /apps.
# Un fee_event ne peut porter qu'un `component` et qu'un `beneficiary` de ces listes.
# Toute modification ici est un changement de méthodologie : bump obligatoire de
# methodology_version sur les specs affectées + recompute complet.

schema_version: 1

components:
taker_fee:
included_in_gross: true
label: "Frais preneur"
maker_fee:
included_in_gross: true
label: "Frais offreur"
maker_rebate:
included_in_gross: false
label: "Rabais offreur"
note: "Montant stocké positif, beneficiary=lp. Sortie de trésorerie, pas un fee."
swap_fee:
included_in_gross: true
label: "Frais de swap AMM"
borrow_fee:
included_in_gross: true
label: "Frais d'emprunt"
note: "Payé au pool, pas à une contrepartie. Revenue réel du système."
liquidation_fee:
included_in_gross: true
label: "Frais de liquidation"
note: "Uniquement la part retenue par le protocole ou le pool. Le collatéral saisi est une perte du trader, jamais un fee."
pool_creation_fee:
included_in_gross: true
label: "Frais de création de pool"
bonding_curve_fee:
included_in_gross: true
label: "Frais de bonding curve"

# --- Collectés mais exclus des agrégats par défaut ---
funding_payment:
included_in_gross: false
toggleable: true
label: "Paiements de funding"
rationale: >-
Transfert de somme nulle entre longs et shorts. Le protocole n'en retient rien.
L'inclure gonfle mécaniquement les perps face aux DEX spot et rend le classement
inter-catégories inutilisable.
interface_fee:
included_in_gross: true
beneficiary_forced: third_party
is_app_layer: true
excluded_from_ranking: true
rationale: >-
Réel, mais Uniswap Labs a coupé ses frontend fees lors d'UNIfication. Les compter
chez les autres fausserait la comparaison. Mesuré, affiché, hors classement.
external_bribe:
included_in_gross: false
label: "Incentives externes"
rationale: "Payé par des tiers aux voters, pas un revenue généré par le protocole."
sequencer_revenue:
included_in_gross: false
counted_in: non_trading_holder_value
rationale: >-
Cas Unichain : entre dans le même mécanisme de burn que les protocol fees, mais
n'est pas un frais de trading. Additionné séparément et visiblement.

exclusions:
# Jamais collectés. Documentés pour que la page méthodologie soit complète.
- id: price_impact
reason: "Capté par l'inventaire du LP, non identifiable comme fee dans les events."
- id: mev
reason: "Va au searcher et au validateur, pas au protocole."
- id: gas_and_priority_fees
reason: "Coût réseau, pas revenue protocole."
- id: seized_collateral
reason: "Perte du trader, pas un frais payé au système."
- id: treasury_one_offs
reason: >-
Burns rétroactifs et distributions exceptionnelles (ex. les 100M UNI du trésor
lors d'UNIfication). Ce n'est pas du revenue récurrent ; l'inclure produirait des
pics absurdes.

beneficiaries:
lp: { label: "Fournisseurs de liquidité", in_protocol_cut: false, in_holder_value: false }
token_holder: { label: "Détenteurs du token", in_protocol_cut: false, in_holder_value: true }
staker: { label: "Stakers / validateurs", in_protocol_cut: false, in_holder_value: true }
burn: { label: "Brûlé", in_protocol_cut: false, in_holder_value: true }
treasury: { label: "Trésor", in_protocol_cut: true, in_holder_value: false }
insurance_fund: { label: "Fonds d'assurance", in_protocol_cut: true, in_holder_value: false }
third_party: { label: "Tiers (frontends, affiliés)", in_protocol_cut: false, in_holder_value: false }
creator: { label: "Créateur du pool / token", in_protocol_cut: false, in_holder_value: false }

# Métriques dérivées. Ce sont des VUES sur la partition ci-dessus, pas des colonnes
# indépendantes. Aucune ne peut compter deux fois le même event.
derived:
gross_fees: "Σ components[included_in_gross]"
lp_revenue: "Σ beneficiary == lp"
protocol_cut: "Σ beneficiary.in_protocol_cut"
token_holder_value: "Σ beneficiary.in_holder_value"
emissions_cost: "Σ_jours tokens_émis(j) × TWAP_usd(j)"
net_real_revenue:
with_holders: "protocol_cut + token_holder_value + non_trading_holder_value − emissions_cost"
treasury_only: "protocol_cut − emissions_cost"
default: with_holders
note: >-
`treasury_only` correspond à la définition littérale du brief mais classe Hyperliquid
comme quasi nul, ce qui contredit l'intention. `with_holders` produit le classement
décrit. Les deux sont calculées ; à trancher avant le lancement public.

invariants:
unallocated_tolerance_bps: 50
witness_divergence_warn_bps: 200
witness_divergence_quarantine_bps: 500
132 changes: 132 additions & 0 deletions benchmarks/apps/aerodrome.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
schema_version: 1
slug: aerodrome
name: Aerodrome
category: dex
methodology_version: 1
status: live
verifiability: tier_2

# Le protocole qui justifie la colonne net_real_revenue.
# Résultat attendu : NÉGATIF. Ce n'est pas une anomalie à corriger.

deployments:
- id: aerodrome:base
chain: base
effective_from: "2023-08-28T00:00:00Z"

finality:
mode: l1_confirmed
lag_seconds: 900

source:
kind: envio_hyperindex
canonical: true
repo: https://github.com/velodrome-finance/indexer
note: >-
T2 : dépendance à un indexer tiers. Migration vers evm_logs directs prévue
en phase 5. L'architecture ve(3,3) rend cette dépendance structurellement
risquée à long terme.

witnesses:
- kind: defillama
endpoint: https://api.llama.fi/summary/fees/aerodrome
expected_divergence: high
note: >-
DefiLlama ne soustrait pas les émissions. La divergence est attendue et
constitue la démonstration de la thèse du produit — ne pas la traiter
comme une erreur à réconcilier.

allocation:
rules:
- source: pool_fees
splits:
- component: swap_fee
beneficiary: token_holder # 100% aux voters veAERO
share_source: onchain_field
field: fee_amount
- source: slipstream_unstaked
splits:
- component: swap_fee
beneficiary: lp
share_source: onchain_field
field: unstaked_fee
note: "Positions CL non-stakées : traitement distinct, ne pas confondre."
- source: external_bribes
splits:
- component: external_bribe # exclu de gross_fees par la taxonomie
beneficiary: token_holder
share_source: onchain_field
note: "Payé par des tiers. L'inclure gonflerait artificiellement Aerodrome."

# Exemple de constante correctement encodée : bornée dans le temps, sourcée.
# Le CI échoue si source_url ou effective_from manque, ou si deux intervalles
# se chevauchent. C'est le garde-fou anti-Lido.
constants:
- name: treasury_share
value: 0.0
effective_from: "2023-08-28T00:00:00Z"
effective_to: null
source_url: https://aerodrome.finance/docs
review_due: "2026-11-01"

pricing:
primary: pyth_hermes
witness: chainlink
granularity: hourly_twap
fallback: pool_ratio
fallback_confidence: low
note: "Milliers de tokens sur Base. La couverture Pyth sera partielle."

emissions:
scope: incentives_only
source:
kind: evm_logs
contract: minter
events: [Mint]
read: distributions_to_gauges
note: >-
On mesure les AERO effectivement distribués aux gauges, epoch par epoch.
PAS un taux d'inflation annualisé appliqué à la supply : le "~10.9%/an"
couramment cité est un taux d'inflation, pas un coût d'incentive, et
l'approximation ne résiste pas à un examen sérieux.
token: AERO
epoch: weekly
spread_over_epoch: true
spread_note: >-
Émissions discrètes (hebdomadaires) réparties uniformément sur 7 jours pour les
fenêtres 24h et 7j. Sans ça, le classement 24h dépendrait du jour de l'epoch.
Choix documenté et affiché sur la page méthodologie.
excluded:
- id: ve_rebase
reason: >-
Anti-dilution des lockers, pas une incentive d'acquisition de volume.
Inclus uniquement sous scope=all_inflation.

validation:
checks:
- id: minter_conservation
kind: onchain_conservation
description: "Émissions du Minter par epoch == Σ distributions aux gauges"
tolerance_bps: 1
note: "Doit correspondre exactement. Un écart signale un gauge manqué."
- id: voter_fee_conservation
description: "Fees distribués aux voters == Σ fees des pools"
tolerance_bps: 100

ui:
# Sans cette note, la ligne "lp_revenue = 0" se lit comme une donnée manquante.
annotations:
- target: lp_revenue
text: >-
Les LPs Aerodrome ne perçoivent pas de fees : ils reçoivent des émissions AERO
à la place. La valeur leur revenant est réelle mais dilutive, et apparaît en
coût d'émission plutôt qu'en revenue.
- target: net_real_revenue
text: >-
Négatif : le protocole distribue plus de valeur en émissions qu'il ne capte
en frais. C'est un résultat mesuré, pas une erreur de données.

metadata:
chains: [base]
is_router: false
nests_within: null
Loading
Loading