Relecture de sécurité indépendante
du contrat de marché ASA Compass

Revue défensive, en aveugle, d'un contrat immuable avant sa gravure. Session hotel-saint-michel-eb · 1er septembre 2026.

Objet relu — application testnet 770843923
source : contrat/marche/contrat.py
sha256 source : 208bd5ba20032571c6b9aed6ef53c1083fb7454a6e84670892abbe9250e2c535
sha256 approbation compilée : 1eadfdf1dda6545e893c0927983f460ddd34ff84618581de17e64f28319e9811
taille : 6 136 octets · schéma : 12 entiers / 4 tranches / 3 pages
instance d'essai déployée pour cette revue : 770849722 (même bytecode)

1. Conformité du code à ce qui est gravé établi par exécution

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 :

  1. copie du source, empreinte relevée : 208bd5ba…c535 ;
  2. compilation avec le puyapy du projet vers TEAL ;
  3. assemblage du TEAL par l'algod public ;
  4. relevé du approval-program de l'application 770843923 sur la chaîne ;
  5. comparaison des 6 136 octets un à un.

Résultat

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.

2. Constat principal établi par mesure

Le coffre perd sa réserve d'acceptation (0,1 ALGO) quand une pièce revient à un vendeur dés-opté

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èneDisponible coffre — variation netteÉtat de la pièce après
Témoin : annulation normale (vendeur encore opté)0 µALGOrendue au vendeur ✓
Défaut : vendeur dés-opté avant l'annulation−99 000 µALGOreste 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.

3. Observations mineures établies par lecture

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.

a. Trop-payé perdu sur une offre en jeton

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.

b. Le coffre paie une seconde boîte de créance quand vendeur ET exploitant sont dés-optés

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.

c. Dépassement d'entier possible sur le plancher d'enchère en jeton

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.

4. Ce que je n'ai pas regardé, et pourquoi

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….