Revue défensive, en aveugle, d'un contrat immuable avant sa gravure. Session hotel-saint-michel-eb · 1er septembre 2026.
Avant toute relecture, j'ai vérifié que le source correspond exactement au programme réellement gravé, octet par octet et non par empreinte annoncée :
Programme d'approbation : 6 136 octets identiques. Programme clear : 4 octets identiques. Schéma d'état conforme (12 entiers, 4 tranches d'octets, 3 pages supplémentaires). Je relis donc bien ce qui est gravé. Tous les constats ci-dessous portent sur ce même objet, dont l'empreinte figure en tête.
Ce que c'est, du point de vue de celui qui perd
Sur tout chemin de retour d'une pièce (annulation, reprise d'annonce expirée, annulation ou repli de clôture d'enchère), si le vendeur destinataire ne peut plus recevoir sa pièce — parce qu'il s'est dés-opté de l'actif — le contrat inscrit correctement une créance de pièce et laisse la pièce au coffre. Mais : la réserve d'acceptation de 100 000 µALGO est remboursée au vendeur, alors que l'opt-in du coffre sur cette pièce n'est pas refermé. Le coffre finance donc de sa poche une acceptation qui reste immobilisée. Celui qui perd est le marché lui-même : c'est son capital de fonctionnement — celui qui paie les transactions internes de toutes les ventes et de tous les remboursements — qui s'épuise.
Comment je l'ai établi — mesuré sur l'instance 770849722, avec témoin
| Scène | Disponible coffre — variation nette | État de la pièce après |
|---|---|---|
| Témoin : annulation normale (vendeur encore opté) | 0 µALGO | rendue au vendeur ✓ |
| Défaut : vendeur dés-opté avant l'annulation | −99 000 µALGO | reste au coffre, opt-in ouvert |
Le témoin, dont je connais le résultat attendu, revient exactement à zéro : la mesure du défaut n'est donc pas un artefact de banc. La chute est de 99 000 µALGO = la réserve (100 000) moins un frais de 1 000 que le vendeur se voit par ailleurs surfacturer (une sortie de pièce comptée alors qu'aucun transfert n'a lieu).
La récupération est à la seule main de l'attaquant
Si le vendeur réclame ensuite sa pièce (reclamer_une_piece), l'opt-in se referme et le coffre récupère la réserve (mesuré : +98 000 µALGO). Mais rien ne l'y oblige. Coût mesuré pour drainer le coffre de façon permanente : l'attaquant sacrifie 32 100 µALGO (la provision de boîte qu'il abandonne, plus les frais) pour retirer 99 000 µALGO au marché — une amplification d'environ 3 fois, répétable pièce après pièce.
Pourquoi ce n'est pas le résidu déjà assumé
Le commentaire de _rendre_le_depot décrit précisément cette perte — « la réserve immobilisée n'est pas libérée, et l'on rend pourtant la sienne… LE COFFRE PERD ALORS 0,1 ALGO » — mais l'attribue au cas de la poussière d'un tiers (R-C5), qu'il déclare « bornée, assumée, rare puisque le corpus est fait de pièces uniques ».
La borne annoncée ne tient pas : je déclenche exactement la même perte sans aucun tiers, sans poussière, sur une pièce unique, par le seul dés-optement volontaire du vendeur avant un retour. Ce que l'analyse tenait pour un accident rare est en fait un geste délibéré, bon marché et répétable.
Où cela mord aussi en fonctionnement normal
reprendre_une_annonce_expiree est appelable par n'importe qui, et ASA Compass prévoit de balayer les annonces expirées « par courtoisie ». Tout vendeur qui nettoie son portefeuille (dés-opt d'un NFT) avant l'expiration de son annonce fera perdre 0,1 ALGO au coffre au passage du balayage, sans la moindre intention hostile. Le défaut n'a donc pas besoin d'un attaquant pour se produire.
Conséquence de second ordre, à nommer
La pièce non réclamée maintient aussi le solde minimum du coffre au-dessus de celui d'un compte nu : cela rejoint la limite déjà assumée par le projet (« un NFT jamais réclamé rend le marché infermable »). Le présent constat ajoute que, dans ce cas, le coffre a en plus perdu l'ALGO de la réserve, et non seulement la place de la boîte.
La piste d'un correctif, pour information seulement
La cause tient en une ligne : _rendre_le_depot rembourse payees × RESERVE_ACCEPTATION sans condition, alors que la réserve n'est réellement libérée que lorsque l'opt-in se referme. Rembourser la réserve uniquement pour les positions effectivement refermées — c'est-à-dire déduire les inscrites (créances de pièce) du compte des réserves rendues, comme on en déduit déjà la provision de boîte — refermerait le trou. Décision de conception qui appartient à Karl ; je ne modifie rien.
Ces points sont de faible portée ou dépendent de choix hors du contrat. Je les signale par honnêteté, sans les avoir tous éprouvés par exécution.
Dans offrir, le versant ALGO absorbe le trop-payé et le rend à l'offrant à la fermeture ; le versant jeton ne le fait pas (assert asset_amount >= montant, l'excédent reste au coffre). Un offrant qui verse plus de jeton que son offre perd la différence. Le site maîtrise la composition, et les offres en jeton sont annoncées comme « limite connue, à lever plus tard » ; l'impact réel est donc faible, mais l'asymétrie avec la branche ALGO est réelle.
Dans acheter en jeton, si le versement au vendeur ET celui à l'exploitant échouent tous deux, deux créances (donc deux boîtes) naissent, mais la retenue est plafonnée à une seule BOITE_CREANCE (le vendeur n'en a provisionné qu'une). Le coffre finance la seconde boîte. Le cas est adjacent à un résidu déjà discuté (R-C2) et suppose l'exploitant dés-opté ; il reste que, dans ce cas, le vendeur est débité de la provision par la défaillance de l'exploitant, qu'il ne contrôle pas, et cette provision finit par revenir à l'exploitant lorsqu'il réclame.
mettre_aux_encheres calcule planchers[jeton] × plancher_enchere ÷ PRIX_PLANCHER. Les deux facteurs peuvent aller jusqu'à PRIX_PLAFOND (10¹²) ; leur produit (10²⁴) dépasse alors un entier 64 bits et la machine virtuelle interrompt la transaction. Cela rendrait les enchères en jeton impossibles pour cette monnaie. C'est un footgun de l'exploitant seul (les deux valeurs sont sous son contrôle et devraient être petites en pratique) ; aucun tiers ne peut le provoquer. Aux réglages réalistes, aucune marche n'est atteinte.
Une liste de ce qui n'a pas été couvert vaut mieux qu'une liste de ce qui l'a été : c'est là que se trouve le prochain défaut.
Relecture menée en aveugle : aucun document de correction, de rejeu ni de version n'a été consulté, aucun banc d'essai existant n'a été ouvert. Contamination déclarée et bornée : noms de fichiers de bancs entrevus lors d'une recherche du source, contenu jamais lu.
Tous les montants sont en microALGO (1 ALGO = 1 000 000 µALGO), relevés sur le réseau d'essai d'Algorand. Instance de mesure : 770849722. Objet relu : 770843923, approbation 1eadfdf1dda6545e….