ASA Compass · relecture indépendante
Le site annonce que l'échange n'ouvrira qu'une fois le contrat relu par quelqu'un d'autre que son auteur. Voici cette relecture : une tentative de casser le contrat, et ce qu'elle a trouvé.
Le chantier a corrigé les quatre défauts A à D. La relecture a vérifié dans le code — pas sur parole — que chaque correctif est réellement présent et sain, et que la nouvelle version compile :
Le re-test sur la chaîne de ces correctifs, et la couverture des angles non vérifiés (vente à prix fixe en jeton, lots, enchères, signature matérielle), restent à faire sur une version stable, une fois le compte de test réapprovisionné.
Ci-dessous, les défauts tels qu'ils ont été trouvés dans la version auditée (empreinte f33e994b). Voir le bandeau « Suite » pour leur correction.
| Gravité | Défaut | Effet |
|---|---|---|
| Critique | Une offre ne peut jamais être acceptée | Le mécanisme d'amorçage — la fonctionnalité n°1 — est inopérant. |
| Moyen | Réserve d'édition captable par un tiers | Un attaquant empoche les 0,1 ALGO de réserve d'un autre vendeur. |
| Moyen | Offre en jeton financée par le coffre | Le coffre immobilise son propre ALGO ; griefing possible. |
| Moyen | Marché infermable après une monnaie | 0,1093 ALGO piégés par monnaie, sans issue. |
| Faible | Frais d'acceptation à surpayer | Le provisionnement de l'offrant n'entre pas dans la réserve de frais. |
| Faible | Bytecode déployé périmé | L'app testnet en service ne correspond pas à la source actuelle. |
Le contrat porte la décision de conception la plus structurante du projet : le marché s'amorce par les offres. Un collectionneur dépose une offre en ALGO sur n'importe quelle pièce ; le détenteur la découvre et l'accepte. Sans cela, « un catalogue reste un musée ». Cette voie ne fonctionne pas.
accepter_l_offre exige que le vendeur envoie sa pièce au coffre (asset_receiver == current_application_address), puis le coffre la fait suivre à l'acheteur par transaction interne. Or, sur Algorand, un compte doit avoir accepté (opt-in) un actif pour pouvoir le recevoir. lister et mettre_aux_encheres optent bien le coffre dans les pièces qu'on y dépose ; accepter_un_jeton l'opte dans les monnaies de paiement. Mais aucune méthode n'opte le coffre dans la pièce d'une offre — et seul le contrat le pourrait, l'opt-in étant une transaction interne que l'utilisateur ne peut pas signer à sa place.
python essai_D_offre_algo.py
ok offre en ALGO de 0,1 ALGO créée (offre 3) RATÉ acceptation à frais minimaux → motif : receiver error: must optin, asset 770778845 missing from [coffre] RATÉ acceptation à frais suffisants → motif : receiver error: must optin, asset 770778845 missing from [coffre]
Testé dans les deux ordres de groupe possibles (pièce d'abord, appel d'abord) : même échec, avant même que la logique du contrat ne tourne. Le protocole rejette le transfert de la pièce vers un coffre non opté.
python essai_E_controle_positif.py
ok pièce mise en vente (annonce 2) ok l'acheteur achète ok la pièce est arrivée chez l'acheteur ok le vendeur a reçu le prix (net du remboursement de dépôt)
La vente à prix fixe en ALGO fonctionne de bout en bout, parce que lister opte le coffre au moment du dépôt. Le défaut est donc ciblé sur l'acceptation d'offre, pas généralisé.
Une édition limitée (tirage > 1, forme massivement présente dans le corpus de 2021) peut être détenue par plusieurs comptes. Le coffre n'accepte l'actif qu'une seule fois, au premier dépôt, et lister fait payer cette réserve de 0,1 ALGO au premier vendeur. Mais la réserve n'est libérée qu'au dernier exemplaire qui sort du coffre (asset_close_to). Celui qui referme en dernier l'empoche, qu'il l'ait payée ou non.
python essai_A_reserve_edition.py
Le montant est borné à 0,1 ALGO par collision, mais c'est une perte de fonds réelle d'un utilisateur vers un autre, déclenchable à volonté sur toute édition dont l'attaquant détient un exemplaire.
Quand une offre est libellée dans un jeton, offrir stocke depot_boite = 0 : la boîte de l'offre (0,0349 ALGO) est immobilisée sur le solde disponible du coffre, l'offrant ne versant aucun ALGO. Le contrat le reconnaît lui-même comme « une limite connue ».
python essai_BC_jeton_fermeture.py
Conséquence : le coffre subventionne chaque offre en jeton sur son propre solde. Un attaquant peut répéter l'opération pour épuiser l'ALGO disponible du coffre et le rendre incapable d'émettre ses transactions internes — un griefing gratuit, l'offrant ne risquant rien. La porte est ouverte dès qu'une monnaie de paiement est acceptée.
fermer_le_marche existe précisément pour éviter que le solde immobilisé reste « prisonnier pour toujours ». Elle exige que le solde minimum du coffre soit retombé à celui d'un compte nu. Mais une fois accepter_un_jeton appelé, le coffre garde pour toujours un opt-in d'actif (0,1 ALGO) et une boîte de plancher (0,0093 ALGO) qu'aucune méthode ne peut défaire. La condition de fermeture devient inatteignable.
python essai_BC_jeton_fermeture.py
C'est exactement le défaut « prisonnier pour toujours » que la méthode prétend éviter. La note de tête promet « l'exploitant ne peut que fermer une caisse déjà vide » ; en pratique, la caisse ne peut plus jamais être vidée de ses opt-in de monnaies.
Les commentaires disent que l'offrant provisionne « les deux transactions qui la refermeront ». En réalité, ces frais provisionnés sont retenus par le coffre et n'entrent pas dans la réserve de frais du groupe (le fee pooling). L'acceptant doit donc gonfler lui-même le frais de son appel pour couvrir les transactions internes à fee=0.
RATÉ acceptation à frais minimaux → motif : group fee too small (needs 1mA more)
Sans gravité pour la sécurité, et une intégration soignée du site le gère en fixant un frais d'appel suffisant. À noter tout de même : le provisionnement décrit ne joue pas le rôle que les commentaires lui prêtent, et le petit surplus retenu par le coffre à chaque offre s'y accumule sans issue (même famille que le défaut 4).
La compilation de la version figée est identique octet pour octet aux artefacts du dépôt (marche/artefacts/), mais différente du bytecode réellement déployé sur l'app de test partagée.
Tout essai lancé contre l'app partagée teste donc du code plus ancien que la source. Il faut redéployer avant de conclure quoi que ce soit sur cette app. La relecture a contourné le problème en déployant sa propre instance depuis la version figée.
Un audit qui ne dit pas ce qu'il n'a pas regardé ne vaut rien. Voici les angles restés dans l'ombre.
La version auditée a été compilée depuis la source figée (empreinte f33e994b…), puis déployée en propre sur le testnet (app 770778378). Chaque attaque a été jouée en transactions réelles signées et confirmées sur la chaîne, et non simulée. Les essais reproductibles vivent dans audit-contrat/ : essai_A_reserve_edition.py, essai_BC_jeton_fermeture.py, essai_D_offre_algo.py, essai_E_controle_positif.py. Ils tournent avec le même banc d'essai que les scénarios du chantier (contrat/commun.py), pointé sur l'instance d'audit.
Conclusion : un défaut critique empêche aujourd'hui la fonctionnalité qui devait amorcer le marché. Le chemin à prix fixe, lui, tient. Les trois défauts moyens touchent des fonds réels mais bornés, ou la capacité à retirer le contrat. L'échange ne devrait pas ouvrir avant correction du défaut 1 au minimum, et avant que les angles non vérifiés — les enchères et la signature matérielle en tête — aient été éprouvés.
Relecture indépendante · ASA Compass · testnet uniquement · 31 août 2026
Version auditée figée à l'empreinte f33e994b, compilée et déployée par la relecture.