ASA Compass · contrat de règlement · relecture adverse
Relecture statique, ligne à ligne, conduite sans lire aucun rapport d'audit antérieur ni la liste des défauts connus. Chaque conclusion a été éprouvée sur le testnet Algorand, sur une instance déployée pour cette relecture.
J'ai compilé la source gelée dans mon propre dossier et obtenu un TEAL identique bit pour bit à celui des artefacts. Puis j'ai lu le programme d'approbation de trois endroits et comparé leurs empreintes :
app 770793296 (celle des autres sessions) 4522 o sha256 67fd7a8736810d52dd08c751 app 770797552 (la mienne) 4522 o sha256 67fd7a8736810d52dd08c751 ma compilation locale 4522 o sha256 67fd7a8736810d52dd08c751
Ce que j'ai mesuré sur mon instance vaut donc exactement pour l'application déployée.
Ce contrat ne doit pas être déployé sur le réseau principal en l'état.
J'ai trouvé quinze défauts, et voici où ils tombent. Deux font sortir du coffre plus d'argent qu'il n'y est entré. Un fait passer l'argent d'un acheteur au vendeur sans contrepartie. Quatre rendent un bien ou une somme définitivement inaccessibles, sans qu'aucune méthode du contrat puisse les en sortir, et un cinquième blocage dépend du bon vouloir de l'exploitant. Quatre coûtent de l'argent sans le détruire. Un fausse ce que le contrat publie comme historique. Un dernier ne se voit qu'en cas d'incident : le seul pouvoir d'urgence de l'exploitant ne ferme pas la porte par laquelle le coffre se vide. Le quinzième n'est pas un défaut du contrat mais de sa documentation, qui décrit une procédure que le code refuse.
Aucun de ces quinze n'est une garde qui refuserait mal : ce sont toutes des gardes qui n'existent pas. C'est pourquoi aucun ne pouvait apparaître dans un compteur de contrôles verts.
Le plus grave se ramène à une seule phrase, répétée cinq fois dans le fichier : le contrat compte les transactions du groupe, mais ne les consomme jamais. Une même transaction sert donc autant d'appels qu'on en place dans le groupe.
Classé par ce que ça coûte, pas par l'ordre où je l'ai trouvé. Chaque défaut porte sa reproduction : le numéro de tour permet de retrouver la transaction sur un explorateur du testnet.
Cinq méthodes lisent le groupe atomique avec la même boucle
for i in urange(Global.group_size) pour constater qu'une somme est arrivée
ou qu'une pièce est passée. Aucune ne marque la transaction constatée. Le protocole
Algorand n'offre aucun marquage implicite : deux appels de la même application dans un
même groupe voient donc rigoureusement la même chose.
À l'inverse, les paramètres déclarés de type transaction en ARC-4
(le depot de lister, d'offrir,
d'encherir, de mettre_aux_encheres et
d'accepter_un_jeton) sont résolus par le compilateur en
GroupIndex moins 1, ce que j'ai lu dans le TEAL. Cette position est imposée
par le protocole et ne se falsifie pas. Le principe du dépôt n'est donc pas en cause :
c'est la lecture par balayage qui l'est.
acheter, boucle ligne 936, garde absente après la ligne 956
acheter ne reçoit pas le paiement en argument : il l'additionne en
parcourant le groupe, puis vérifie recu >= prix + commission. Deux appels
placés dans le même groupe additionnent le même paiement et se croient chacun payé.
Le contrat verse alors deux fois le prix au vendeur et livre deux fois les pièces,
pour un seul encaissement. La différence sort du coffre, c'est-à-dire de l'argent que
d'autres utilisateurs y ont immobilisé : mises d'enchères en cours, offres en attente,
dépôts de vendeurs.
deux annonces à 0,2 ALGO, groupe de trois transactions : [ paiement 0,202 ALGO , acheter(0) , acheter(1) ] le groupe est accepté acheteur -0,205000 ALGO et il repart avec LES DEUX pièces vendeur +0,679400 ALGO coffre -0,489400 ALGO pièces reçues : 770797563 et 770797565, valeur affichée 0,4 ALGO
Un groupe Algorand porte seize transactions : un paiement peut donc financer jusqu'à quinze achats. L'attaque se répète tant que le coffre a de quoi payer, et s'arrête quand il est vide, c'est-à-dire quand plus aucun retrait légitime n'est possible. Aucun réglage de l'exploitant ne l'arrête, voir le défaut 10.
PIECES_PAR_ENCHERE_MAXIMUM ligne 108, PIECES_PAR_LOT_MAXIMUM ligne 107
Les deux maxima ont été mesurés, la note du fichier le dit, et le raisonnement est juste. Mais il a été mené sur une vente en ALGO. Un appel d'application porte au plus huit références. La clôture d'une enchère en nomme : les N pièces, le vendeur, le gagnant, l'exploitant et la boîte, soit N + 4, donc N au plus 4. Une vente libellée en JETON exige une référence de plus, celle du jeton, que le coffre doit nommer pour émettre le versement et la commission. Le compte devient N + 5, donc N au plus 3.
clôture, 4 pièces sans jeton = 8 réf. acceptée en forme clôture, 4 pièces + le jeton = 9 réf. tx references exceed MaxAppTotalTxnReferences = 8 clôture, 3 pièces + le jeton = 8 réf. acceptée en forme
Une enchère de quatre pièces en jeton se met donc en vente sans obstacle. Sa
réserve une fois atteinte, la transaction de clôture n'est pas constructible, et
annuler_l_enchere la refuse par la garde
reserve atteinte, vente due, que j'ai fait jouer séparément. Les quatre
pièces et la mise du gagnant sont alors perdues pour toujours.
C'est exactement le blocage que la note de PIECES_PAR_LOT_MAXIMUM dit
vouloir éviter, revenu par la porte de la monnaie de paiement. Le même décalage vaut
pour l'achat : un lot de cinq pièces en jeton est déposable sans être achetable, mais
là le vendeur peut au moins annuler.
lister ligne 747 et mettre_aux_encheres ligne 1399, même boucle
Les deux méthodes construisent leur liste de pièces en balayant le groupe. Rien n'interdit qu'elles cohabitent, et aucune ne marque le transfert. Un seul NFT déposé ouvre donc à la fois une annonce à prix fixe et une enchère.
groupe de cinq transactions, un seul transfert de pièce : [ dépôt , lister , dépôt , mettre_aux_encheres , transfert ] accepté : annonce 2 et enchère 0 promettent toutes deux 770797699 un enchérisseur mise 0,13 ALGO de bonne foi, la pièce est au coffre le vendeur vend l'annonce : la pièce sort, le coffre en détient 0 l'enchère promet toujours cette pièce et retient 0,13 ALGO le vendeur tente d'annuler pour rendre l'argent : REFUSÉ, garde « reserve atteinte, vente due » (pc 3058)
L'enchérisseur n'a rien fait d'anormal. Il a misé sur une pièce réellement présente au coffre. Son argent est immobilisé dans une enchère que personne ne peut plus ni clore ni annuler. Ce n'est pas un vol au profit de l'attaquant, l'argent restant au coffre : c'est une destruction, et elle coûte à peine des frais de réseau à celui qui la commet.
lister ligne 747 et mettre_aux_encheres ligne 1399, garde absente
La note de tête du contrat s'appuie sur un fait mesuré hors chaîne : « les NFT de 2021 visés par ce projet ont tous une clawback vide, vérifié sur la chaîne ». C'est une constante qui décrit une réalité, et aucune ligne du contrat ne la vérifie au dépôt. Or le marché accepte n'importe quel actif, pas seulement ce corpus.
actif 770798271, clawback = l'émetteur le vendeur le détient de bonne foi et le met en vente annonce 7 ouverte, le contrat n'objecte rien l'émetteur reprend la pièce DANS LE COFFRE, sans rien signer du marché le coffre détient 0 exemplaire, l'annonce en promet toujours 1 le vendeur tente d'annuler pour récupérer son dépôt : ÉCHEC : inner tx 0 failed: underflow on subtracting 1 from sender amount 0 garde du contrat ayant joué : AUCUNE
Le dépôt du vendeur est prisonnier, et aucune méthode ne peut plus fermer cette annonce. Sur une enchère dont la réserve est atteinte, le même geste détruit la mise du gagnant, puisque l'annulation est alors interdite. Ni le vendeur ni le gagnant n'ont commis d'erreur : c'est l'émetteur de la collection qui décide.
La promesse publique du projet est ici en jeu. Écrire que le NFT « ne peut aller qu'à un acheteur payant ou revenir au vendeur » est faux pour tout actif dont la clawback n'est pas vide, et le contrat ne fait rien pour restreindre ce qu'il accepte.
accepter_l_offre, boucle ligne 1271
La méthode constate le passage de la pièce en balayant le groupe et pose
trouvee = 1. Deux offres du même acheteur sur la même pièce se règlent donc
avec un seul transfert. Un acheteur qui surenchérit sur lui-même sans retirer sa
première offre, ce que rien n'empêche et que l'interface peut encourager, paie deux
fois et ne reçoit qu'une pièce.
deux offres de 0,15 ALGO du même acheteur sur la pièce 770797807 groupe : [ accepter_l_offre(0) , accepter_l_offre(1) , transfert ] le groupe est accepté vendeur +0,289000 ALGO pour UNE seule pièce vendue acheteur : 1 pièce reçue, 0,3 ALGO immobilisés
offrir, boucle ligne 1129
Une offre libellée en jeton exige un second dépôt en ALGO pour payer sa boîte, et
ce dépôt est cherché par balayage. Chaque offre du groupe inscrit alors
depot = 36 900 dans sa boîte, alors qu'une seule l'a financé. Le
remboursement, lui, est bien versé à chacune.
groupe : [ transfert jeton , offrir , transfert jeton , offrir ,
paiement ALGO 0,0369 ]
accepté
versé par l'offrant 0,041900 ALGO (un seul dépôt, plus les frais)
rendu après retrait 0,063800 ALGO
bilan de l'aller-retour +0,021900 ALGO au détriment du coffre
accepter_un_jeton ligne 1801, garde absente ; commission émise ligne 981
accepter_un_jeton fait accepter la monnaie au COFFRE. Rien ne vérifie
que l'EXPLOITANT l'a acceptée, alors que c'est vers lui que part la commission. Une
monnaie ouverte sans cela rend toute vente impossible dans cette monnaie, et le refus
ne vient d'aucune garde du contrat : c'est le protocole qui rejette la transaction
interne.
monnaie 770797992 ouverte au marché par l'exploitant l'exploitant l'a-t-il acceptée pour lui-même ? NON l'acheteur tente d'acheter en payant dans cette monnaie : ÉCHEC : must optin, asset 770797992 missing from AQ2GHD… garde du contrat ayant joué : AUCUNE l'exploitant accepte la monnaie, RIEN D'AUTRE ne change : le même achat passe, tour 66866626
Cela contredit frontalement la quatrième mesure de sécurité, inscrite dans le constructeur : « Le SEUL pouvoir de l'exploitant est de suspendre les NOUVEAUX dépôts. Il ne doit exister aucun chemin de code permettant d'empêcher un retrait. » Se dés-opter d'une monnaie est un tel chemin, et il ne passe par aucune méthode du contrat, donc aucune relecture du seul fichier ne le montre. Sur une enchère en jeton dont la réserve est atteinte, le blocage est définitif, l'annulation étant interdite.
fermer_une_monnaie ligne 1861, garde trop grossière
La garde refuse dès que le coffre détient la moindre unité de la monnaie. Son intention est juste, mais elle ne distingue pas un dépôt légitime d'un résidu. Or n'importe qui peut envoyer une unité au coffre par un transfert ordinaire, hors du contrat, et le contrat n'a aucune méthode capable de balayer ce résidu.
après une vente propre, le coffre détient 0 unité de 770797992
un tiers quelconque envoie UNE unité au coffre
l'exploitant tente de fermer la monnaie :
REFUSÉ, garde « le coffre detient encore cette monnaie »
La conséquence porte plus loin que la monnaie. fermer_le_marche exige
que le solde minimum du coffre soit retombé à celui d'un compte nu, ce qui suppose
qu'aucun actif ne reste accepté. Une monnaie qu'on ne peut plus fermer rend donc la
fermeture du marché impossible, définitivement, et avec elle la récupération du solde
immobilisé du créateur. Le surplus d'un acheteur qui paie trop, jamais rendu (défaut
12), produit le même effet sans aucune malveillance.
_rendre_le_depot ligne 566, et _rendre_les_pieces ligne 614
Sur un actif tiré à plusieurs exemplaires, le premier vendeur paie la réserve
d'acceptation et le second n'en paie aucune. Le champ reserves borne
correctement le remboursement par ce que chacun a versé, et empêche bien le second de
voler le premier : cette correction fonctionne. Mais elle ne traite qu'une moitié du
problème. Si le premier sort AVANT le second, il ne referme rien, le coffre détenant
encore un exemplaire, donc il ne récupère pas sa réserve ; et quand le second referme,
il n'a rien payé, donc il ne reçoit rien non plus.
actif 770798077 tiré à deux exemplaires, deux annonces du même vendeur
versé au total 0,197400 ALGO
annulation de l'annonce qui a PAYÉ la réserve, puis de l'autre
position refermée, réserve libérée AU COFFRE
récupéré 0,075400 ALGO
perte nette sur un aller-retour sans vente : 0,122000 ALGO
Les 0,1 ALGO restent au coffre sans propriétaire, et aucune méthode ne les réattribue. Le corpus visé étant surtout fait de pièces uniques, le cas est rare, mais il se produit dès qu'une collection comporte des éditions à plusieurs exemplaires.
trois gardes de suspension seulement, lignes 701, 1044 et 1356
suspendu est vérifié dans lister, offrir et
mettre_aux_encheres. Ni acheter ni encherir ne
la portent. Pour acheter c'est défendable, un achat étant un règlement et
non un dépôt. Deux conséquences le sont moins.
encherir immobilise de l'argent neuf et n'est pas suspendable, ce
qui contredit la formule « suspendre les NOUVEAUX dépôts ».acheter. En cas d'incident,
l'exploitant n'a aucun moyen d'arrêter l'hémorragie : le seul pouvoir dont il dispose
laisse ouverte exactement la porte par laquelle le coffre se vide. Le contrat étant
immuable, il n'existe alors aucune réponse.lister ligne 719, et la même phrase dans mettre_aux_encheres
Le commentaire affirme : « L'acceptation étant une transaction que le vendeur signe
lui-même, le site la place dans le même groupe : une seule signature suffit. » Or la
boucle ne FILTRE pas les transferts d'actif, elle les EXIGE tous conformes, par des
assert. Une acceptation est un transfert de zéro unité vers soi-même :
elle échoue donc sur asset_receiver == app et sur
asset_amount == 1.
[ dépôt , lister , transfert de la pièce , ACCEPTATION de la monnaie ] REFUSÉ, garde « piece mal adressee » (pc 638) le même groupe SANS l'acceptation accepté, tour 66866578
Un site qui suivrait cette documentation composerait un groupe qui échoue toujours, précisément dans le cas normal du premier vendeur en XDB. La procédure décrite est impossible : il faut une signature séparée.
acheter ligne 956, offrir ligne 1121
La vérification est recu >= prix + commission, et seuls
prix et commission ressortent. La différence reste au coffre
sans qu'aucune méthode puisse la rendre. En ALGO c'est une perte pour l'acheteur ; en
jeton, ce résidu déclenche en plus le blocage du défaut 08.
mettre_aux_encheres ligne 1365, fixer_le_seuil_d_exoneration ligne 1968
jeton == 0.
Une enchère libellée en jeton n'est tenue que par le plancher de la monnaie, qui peut
être bien plus bas. Or ce plancher existe, la note le dit, pour que la part retenue
sur un gagnant défaillant ait de quoi mordre. En jeton, elle peut redevenir
symbolique, et le sabotage gratuit.designer_le_jeton_exonere est à usage unique, pour qu'on ne puisse
pas modifier les frais après coup. Mais fixer_le_seuil_d_exoneration
n'est borné par rien et reste modifiable à volonté : fixer un seuil inatteignable
annule l'exonération. La promesse tenue par une porte est reprise par l'autre._rendre_l_argent ligne 1151, et le paramètre sorties de _rendre_le_depot
_rendre_l_argent émet ses transactions avec fee=0 : elles
sont donc payées par la mise en commun des frais du groupe, c'est-à-dire par celui qui
appelle. Mais sorties les compte quand même, et
_rendre_le_depot retient sorties * FRAIS_RESEAU sur le dépôt.
L'appelant paie donc une première fois de sa poche, et une seconde fois par retenue.
une offre de 0,15 ALGO, dépôt de boîte 36 900, retirée par son auteur frais d'appel réglés au strict nécessaire : 2 000 (l'appel + la sortie à fee 0) variation de l'offrant +182 900 µALGO variation du coffre -185 900 µALGO attendu si le compte juste +183 900 µALGO écart -1 000 µALGO le coffre a émis DEUX transactions internes, dont UNE SEULE à ses frais, et il a pourtant retenu 2 000 µALGO sur le dépôt.
Le même écart vaut pour reprendre_une_offre_expiree et
accepter_l_offre, et il monte à 2 000 dans la clôture d'une enchère
conclue, où sorties compte le versement au vendeur et la commission, tous
deux à frais nuls. À l'inverse, acheter, annuler,
reprendre_une_annonce_expiree et annuler_l_enchere comptent
juste : leurs sorties portent toutes un frais explicite, ou bien celles à frais nuls ne
sont pas comptées. Le défaut est donc précisément localisé, et il n'est pas de
conception : c'est un décompte.
acheter ligne 910 et encherir ligne 1463, aucune comparaison des parties
Le contrat émet des événements que Karl a voulus pour une raison précise, écrite dans le fichier : rendre « l'historique exact et indexable en une requête ». Aucune méthode ne compare pourtant l'acheteur au vendeur, ni l'enchérisseur au vendeur. Un détenteur peut donc acheter sa propre annonce, ou faire monter sa propre enchère, et le contrat inscrit cette vente dans l'historique public exactement comme une vraie.
le vendeur achète sa propre annonce 3, affichée 0,2 ALGO accepté, il récupère sa pièce et son dépôt l'événement VenteConclue réellement publié, décodé depuis le bloc : sélecteur e04d391d annonce 3 vendeur XVPHZ3Z5FNXCZ7W5YVB3ZN75MXK3OU5LUHTVR4TXPRE34C6GPJY46Z4VVM acheteur XVPHZ3Z5FNXCZ7W5YVB3ZN75MXK3OU5LUHTVR4TXPRE34C6GPJY46Z4VVM prix 200 000 les deux adresses sont identiques
Le coût de l'opération est la commission, soit un pour cent du prix qu'on souhaite afficher, plus quelques frais de réseau. Tout le reste revient à son auteur. Un catalogue peut donc se voir doter de prix de référence entièrement fabriqués, et l'acheteur suivant paiera d'après eux.
Le contrat ne peut pas empêcher cela complètement, deux comptes complices suffisant à contourner toute comparaison d'adresses. Mais refuser le cas identique coûte une ligne, et il fait la différence entre une manipulation qui demande deux comptes et une somme immobilisée, et une qui n'en demande aucun. Surtout, l'événement devrait porter de quoi distinguer les deux : ce qu'il n'inscrit pas est perdu pour toujours, comme le dit sa propre note.
Une relecture ne vaut que si elle dit aussi ce qu'elle a examiné sans rien trouver. Ces points-là ont été lus ligne à ligne, et pour plusieurs d'entre eux éprouvés sur la chaîne.
Les arguments transaction ARC-4 ne se falsifient pas.
Le TEAL les résout en txn GroupIndex ; 1 ; -, avec un contrôle de type. Un
second appel ne peut pas viser le dépôt du premier : il lirait le premier appel, dont le
type n'est pas pay. Le dépôt d'offrir, celui
d'encherir et celui d'accepter_un_jeton sont donc corrects.
La fermeture de position ne livre jamais la pièce d'un autre.
_rendre_les_pieces ne pose asset_close_to que si le coffre ne
détient plus que l'exemplaire qui sort. Mesuré à l'essai des éditions multiples : la
première sortie laisse la position ouverte, la seconde la referme. Le raisonnement de la
note est exact.
Un vendeur ne peut pas empocher la réserve d'un autre.
Le champ reserves borne le remboursement par ce que ce vendeur a
effectivement versé. La correction décrite dans les commentaires fonctionne. Le défaut
09 est son revers, pas son échec.
L'exploitant ne peut pas partir avec la caisse.
fermer_le_marche ne se contente pas d'une condition déclarative : elle lit
le solde minimum réel imposé par le protocole au coffre et exige qu'il soit retombé à
celui d'un compte nu. Une seule boîte, un seul actif accepté suffit à la bloquer. C'est
la meilleure garde du fichier.
La part retenue sur un gagnant défaillant ne peut pas devenir une
confiscation. regler_les_encheres borne le taux à
PENALITE_POINTS_PLAFOND, soit vingt pour cent, et cette borne est une
constante que l'exploitant ne peut pas déplacer. La pénalité est en outre plafonnée en
montant absolu.
Le gel d'enchère par surenchère sur soi-même est bien fermé.
Les deux gardes se complètent comme la note l'annonce : on ne prolonge que sur une mise
venue d'un autre compte, ce qui ferme le cas de l'attaquant seul, et
fin_maximum est fixée à l'ouverture et jamais modifiée, ce qui ferme celui
de deux complices. J'ai vérifié dans la boîte que fin_maximum vaut bien
fin + 86 400 et qu'aucun chemin ne la réécrit.
Sans réserve déclarée, le prix de départ en tient lieu.
_reserve_atteinte ferme le trou par lequel un vendeur sans réserve aurait
pu annuler à volonté après avoir vu les mises. Mesuré : mon enchère à réserve nulle et
départ 0,13 est bien réputée atteinte à 0,13, et l'annulation a été refusée.
Le repli de clôture quand le gagnant s'est dés-opté est le bon choix. Exiger l'acceptation au moment de la mise n'empêcherait rien, puisque rien n'oblige à rester accepté. Le repli, lui, garantit que les pièces reviennent au vendeur au lieu de rester au coffre. Ce raisonnement, écrit dans le commentaire, tient.
Les méthodes ouvertes à tous ne sont pas détournables.
reprendre_une_annonce_expiree, reprendre_une_offre_expiree et
clore_l_enchere lisent leurs destinataires dans la boîte, jamais dans les
arguments ni dans Txn.sender. Le pire qu'un inconnu puisse faire est de
payer les frais à la place de l'ayant droit.
Aucun dépassement d'entier trouvé.
prix * COMMISSION_POINTS plafonne à 1014 pour un prix au plafond,
loin des 64 bits. Les sommes de dépôt, les réserves et les calculs de temps restent dans
les mêmes ordres. La sentinelle AUCUNE_EXONERATION est correctement choisie :
un identifiant d'actif ne peut pas valoir le maximum d'un entier 64 bits, et zéro aurait
désigné l'ALGO.
Aucune méthode réservée à l'exploitant ne déplace l'avoir d'un
tiers. Vérifié méthode par méthode : sur les huit méthodes gardées par
Txn.sender == self.exploitant, six n'émettent aucune transaction interne, et
les deux qui en émettent, fermer_une_monnaie et fermer_le_marche,
sont l'une et l'autre conditionnées à ce que le coffre ne détienne plus rien d'autrui. Le
pouvoir de blocage du défaut 07 ne passe justement par aucune méthode : il s'exerce en
dehors du contrat.
Le programme d'effacement est inatteignable, et c'est correct.
Il approuve tout, ce qui inquiète à la lecture. Mais le contrat déclare un schéma d'état
local vide, vérifié sur l'application déployée comme sur la mienne
(local {num-uint: 0, num-byte-slice: 0}) : personne ne peut donc s'opter à
l'application, et il n'existe aucun chemin pour atteindre ce programme.
C'est cette section qui dit ce que vaut le reste. Chaque ligne porte son motif, et aucune n'est là par prudence de façade.
| Non vérifié | Motif |
|---|---|
| Le scénario complet du défaut 02, joué de bout en bout | Chaque maillon est mesuré séparément, la chaîne entière ne l'est pas. Il faut une heure d'attente par enchère, la durée minimum étant de 3 600 secondes. |
| Le blocage définitif d'une enchère par un exploitant qui se dés-opte | Déduit de deux mesures réelles, l'échec de la commission et le refus d'annulation, mais non joué en une seule chaîne, pour la même raison de durée. |
| Le budget de calcul, 700 unités par appel | Non instrumenté. Les chemins les plus chargés, clôture d'une enchère de quatre pièces et achat d'un lot de cinq, n'ont pas été mesurés à la limite. La note du fichier dit l'avoir fait pour la mise en vente. |
Les actifs gelés, par freeze ou default_frozen |
Non éprouvé. Par lecture, le mécanisme est le même que celui du défaut 04, sans qu'il soit besoin d'une reprise : un gel suffit à faire échouer toute sortie. |
Le comportement en cas de rekey d'un compte |
Non examiné. Les adresses sont stockées dans les boîtes, un rekey ne les change pas, mais je n'ai pas cherché les conséquences. |
| Les enchères libellées en jeton, de bout en bout | Seuls l'ouverture et le comptage de références ont été mesurés. La clôture réelle d'une enchère en jeton n'a pas été jouée. |
| Les événements ARC-28 et leur exactitude | Hors de mon angle. Je n'ai pas vérifié que les champs émis correspondent à ce qui s'est réellement passé. |
| Le service, le site et la composition réelle des groupes | Hors périmètre : ma relecture porte sur le contrat seul. Plusieurs défauts ci-dessus dépendent pourtant de ce que le site compose, notamment le 11. |
| Le comportement sous charge, et les courses entre transactions | Non éprouvé. Toutes mes mesures sont séquentielles. |
Une limite de méthode qu'il faut connaître. J'ai cherché ce qui MANQUE, et une garde absente ne laisse aucune trace : ni test rouge, ni message d'erreur, ni compteur de programme. Ce que je rapporte est donc ce que j'ai su imaginer, pas la liste complète de ce qui manque. Les quinze défauts ci-dessus sont réels et reproduits ; leur nombre ne dit rien de ce qui reste.
Lu seulement maintenant, après que tout ce qui précède a été écrit. C'était la condition de l'exercice, et elle a servi : sur les quinze défauts ci-dessus, onze ne figurent nulle part dans ce rapport.
Une précaution d'honnêteté avant de le juger. Ce rapport n'est plus dans l'état où je l'aurais trouvé si j'avais commencé par lui. J'ai signalé mon premier défaut à la session de coordination dès que je l'ai mesuré, sans attendre ce document, et le rapport porte maintenant un bandeau de retrait qui le mentionne, ainsi qu'une correction datée du 1er septembre. Je juge donc ci-dessous deux choses distinctes : ce que le rapport affirmait avant mon signalement, et ce qu'il en dit après.
Sa méthode vaut mieux que ses trouvailles, et c'est un compliment. Exiger de tout contrôle qu'il nomme la garde attendue et vérifie que le refus vient bien d'elle, en lisant le message et le compteur de programme, est la meilleure idée de tout ce dossier. Je l'ai employée pour chacun de mes refus mesurés. Sans elle, mon défaut 11 serait passé pour une simple erreur de composition de groupe au lieu d'une garde nommée qui refuse ce que la documentation prescrit.
L'enchère perpétuelle est réellement fermée. Le rapport l'a
trouvée seul, l'a mesurée, et a vu que la seule correction sur la durée ne suffisait pas.
La version que je relis porte les deux gardes qu'il réclamait, la concurrence et
fin_maximum. J'ai relu ce chemin en cherchant une troisième voie et je n'en ai
pas trouvé.
Son raisonnement sur la pénalité impossible au prix plancher est juste, et il a produit le bon réglage. Il a établi qu'au plancher des annonces aucune part retenue sur la mise ne pouvait couvrir la perte du vendeur, et en a tiré la nécessité d'un plancher propre aux enchères. Ce plancher existe aujourd'hui.
Il a nommé le bon principe pour le maximum de pièces. Sa mesure donnait sept avec un actif répété, et il a écrit lui-même : « Le sept mesuré est un plafond optimiste, pas une garantie : la mesure est à refaire avec des actifs distincts. » Cette prudence était exacte, et elle a produit les maxima actuels.
C'est le titre d'une de ses sections « sain », et il repose sur une seule épreuve :
le marché a été suspendu, un vendeur a retiré sa pièce pendant la suspension. Cette
épreuve est valide, mais elle ne prouve que ce qu'elle teste, à savoir que
suspendre ne ferme pas annuler.
Trois blocages que j'ai mesurés la contredisent, et aucun ne passe par la suspension : un actif dont l'émetteur a gardé la reprise fait échouer toute sortie sans qu'aucune garde ne joue (défaut 04) ; une enchère de quatre pièces en jeton n'est ni clôturable ni annulable (défaut 02) ; un exploitant qui n'a pas accepté une monnaie bloque toutes les ventes dans cette monnaie (défaut 07). Le point commun des trois : le blocage ne vient d'aucune méthode du contrat, donc aucune épreuve de garde ne pouvait le rencontrer.
Le rapport avait conclu que lister, mettre_aux_encheres et
offrir étaient protégées parce qu'elles reçoivent un argument transaction
déclaré. Il l'a lui-même rectifié : l'argument protège le dépôt, tandis que la boucle
ramasse autre chose à côté, les pièces dans deux cas et l'ALGO de la boîte dans le
troisième.
Je confirme la rectification, et j'ajoute la moitié qui manque encore : la protection
par argument déclaré est réelle et elle est solide. Je l'ai vérifiée dans le TEAL
(txn GroupIndex ; 1 ; - avec contrôle de type) et elle interdit bien à un
second appel de viser le dépôt du premier. Le diagnostic exact n'est donc pas que la
protection est illusoire, c'est qu'elle ne couvre pas ce qui est ramassé par balayage à
côté d'elle. Cette nuance décide du remède : il ne faut pas remplacer le mécanisme des
arguments, il faut l'étendre à ce qui est aujourd'hui balayé.
La démonstration est bonne et je la confirme : la garde par le solde minimum réel tient. Mais le rapport en déduit implicitement que l'exploitant est sans pouvoir sur les biens, ce que le contrat affirme aussi dans son constructeur. C'est inexact : en ne s'optant pas à une monnaie qu'il ouvre, ou en s'en dés-optant, il bloque toutes les ventes dans cette monnaie (défaut 07). Le rapport ne pouvait pas le voir en éprouvant les méthodes une à une, puisque ce pouvoir ne s'exerce par aucune méthode.
Symétriquement, il a constaté qu'une instance antérieure est « définitivement infermable » et l'a consigné comme un résultat, ce qui est juste. Il n'a pas vu que le même état revient sur la version corrigée : une seule unité envoyée au coffre par n'importe qui suffit à rendre une monnaie infermable, donc le marché aussi (défaut 08).
Le rapport consigne honnêtement deux fausses alertes, ce qui est à son crédit. La seconde portait sur un manque de 0,006 ALGO à la reprise d'une offre expirée, et il a conclu que « le contrat avait raison et le seuil de l'audit avait tort », au motif que « l'offrant paie les frais de ses trois transactions d'entrée et le contrat retient ceux des deux de sortie, soit exactement six frais ».
Le raisonnement est juste sur ce que le contrat DOCUMENTE, et faux sur ce qu'il
DÉPENSE. Le contrat retient bien deux frais de sortie, mais il n'en paie qu'un : la
seconde transaction porte fee=0 et retombe sur le pool du groupe, donc sur
l'appelant. C'est mon défaut 14, mesuré à 1 000 µALGO près sur une transaction encadrée.
L'audit avait donc vu quelque chose de réel et l'a classé en fausse alerte après avoir
vérifié le commentaire du contrat plutôt que ses transactions internes.
Cela n'enlève rien à sa leçon de méthode, qui reste bonne : une mesure de solde doit encadrer la seule transaction qu'elle juge. C'est en l'appliquant que j'ai pu isoler ces mille microALGO.
Onze de mes quinze défauts sont absents de ce rapport : les numéros 02, 04, 07, 08, 09, 10, 11, 12, 13, 14 et 15. Ce n'est pas un reproche de négligence, et la raison est écrite dans le rapport lui-même, dans la phrase la plus juste qu'il contient : « aucune garde ne les portait, donc aucune épreuve ne pouvait les voir ».
Un audit qui éprouve les gardes mesure la qualité de ce qui a été écrit. Il ne peut rien dire de ce qui ne l'a pas été. Les cinquante-six gardes qu'il a fait refuser refusent effectivement, je n'en ai pris aucune en défaut, et pourtant quinze chemins font perdre un bien ou de l'argent. Les deux constats sont parfaitement compatibles, et c'est cela qu'il faut retenir de la comparaison entre nos deux lectures.
Deux manques méritent une mention particulière, parce qu'ils prolongent directement une mise en garde que le rapport avait lui-même formulée.
if jeton == UInt64(0). En
jeton, le trou qu'il avait mesuré est intégralement rouvert (défaut 13).Le rapport recommande de « refuser qu'un groupe contienne plus d'un appel à cette application ». C'est le bon remède pour le défaut de fond : il ferme les cinq balayages d'un coup, il est simple à écrire, et il se vérifie d'une ligne. Deux réserves, la seconde plus importante que la première.
Le rapport existant et cette relecture ne se contredisent presque nulle part : ils regardent deux objets différents. Lui a mesuré que tout ce qui est écrit fonctionne, et c'est vrai. J'ai cherché ce qui n'est pas écrit, et il en manque beaucoup. Un contrat immuable a besoin des deux, et l'ordre a son importance : la lecture statique aurait dû venir en premier, parce qu'un compteur de gardes vertes donne une confiance que rien dans le dispositif ne vient contredire.
Le rapport le formule mieux que moi dans son bandeau de retrait, et cette phrase mérite de survivre à la correction du contrat : le chiffre ne disait pas ce qu'on croyait qu'il disait.