4. Smart contracts
Limites connues
Cette page recense ce que les contrats et leur outillage ne font pas, ou font imparfaitement. Chaque point a été constaté dans le code du dépôt. Les limites propres au backend, au portefeuille intégré et au cadre juridique sont traitées dans leurs parties respectives, notamment Clés et garde des portefeuilles et Crypto-actifs et MiCA.
#Équité et confiance
| Limite |
Détail |
Piste |
| Aléa prévisible en local |
Le mock calcule keccak256(abi.encode(requestId, 0)), et fulfillRandomWordsWithOverride est appelable par tous : sur la chaîne locale, n'importe qui peut choisir le gagnant |
Ne présenter comme équitable qu'un tirage fait sur Sepolia avec le vrai coordinateur |
Pouvoir du propriétaire ConfirmedOwner |
Hérité de VRFConsumerBaseV2Plus, il peut remplacer le coordinateur par setCoordinator, donc fournir un faux aléa. Les scripts la transfèrent à VRF_OWNER_ADDRESS (le multisig par défaut), mais en deux temps : tant que le multisig n'a pas appelé acceptOwnership, le déployeur la garde |
Faire signer acceptOwnership par le multisig juste après le déploiement (voir Exploitation du contrat) |
| Veto de l'opérateur |
cancelRound est accepté pendant le tirage. Un opérateur qui lit la réponse de l'oracle dans le mempool peut annuler le tour avant qu'elle soit incluse. Personne ne perd d'argent, mais l'issue peut être écartée |
Restreindre l'annulation en Drawing aux tirages plus anciens que drawTimeout |
| Administrateur sans délai |
Sur Sepolia, DEFAULT_ADMIN_ROLE est confié à MAIRIE_ADMIN_ADDRESS, prévu pour un multisig, mais le script ne vérifie pas que l'adresse en est un, et aucun délai (timelock) ne précède un changement de configuration ; en local, l'opérateur reste administrateur additionnel |
Timelock pour les changements de configuration |
| Pas d'audit externe |
Les contrats n'ont été vérifiés que par les tests, solhint et Slither |
Audit avant tout usage avec de vraies valeurs |
#Règles de jeu
| Limite |
Détail |
| Plafond par participant contournable |
maxTicketsPerPlayer (1 au déploiement) s'applique par adresse bénéficiaire. Rien n'empêche une personne de jouer avec plusieurs adresses, ou d'offrir des tickets à des adresses qu'elle contrôle. Le contrat ne connaît pas l'identité des citoyens : seul le backend, qui crée un portefeuille par compte, exige un âge vérifié par la Mairie et refuse un second ticket ou un cadeau à soi-même, limite cet usage, et seulement pour les achats faits par l'application |
| Remboursement au payeur |
En cas d'annulation, les tickets offerts sont remboursés à celui qui les a payés, pas au bénéficiaire. C'est voulu, mais peut surprendre |
| Paiement exact obligatoire |
Un excédent n'est pas rendu, l'achat est refusé. Un client qui lit un prix périmé (après un changement de configuration entre deux tours) voit son achat échouer avec IncorrectPayment |
| Pas de date limite de tirage |
Un tour expiré avec assez de tickets reste Open, sans achat possible, tant que personne n'appelle requestDraw |
| Cagnottes non réclamées |
Elles restent dues indéfiniment. Aucune fonction ne permet de les récupérer ou de les reverser après un délai |
| Configuration VRF immédiate |
Contrairement à la configuration des tours, setVrfConfig s'applique aussitôt, y compris drawTimeout pour un tirage en attente |
| Asymétrie en pause |
checkUpkeep renvoie false en pause, mais performUpkeep peut encore clore un tour expiré sans participants |
| Lecture d'un tour inexistant |
getRound renvoie une structure vide dont le statut vaut Open ; il faut comparer à currentRoundId |
#Coût et passage à l'échelle
| Limite |
Détail |
| Achat linéaire en gaz |
Chaque ticket ajoute une entrée à ticketOwners. Le coût mesuré va de 119 577 gaz à 356 745 gaz pour dix tickets ; cent tickets en une transaction coûteraient plusieurs millions de gaz, payés par la dotation du citoyen |
| Stockage jamais libéré |
Les tableaux ticketOwners des tours passés restent en stockage, ce qui est sans effet sur le coût des tours suivants mais grossit l'état du contrat |
| Pas de mise à jour |
Aucun proxy : corriger un bug impose un redéploiement, avec de nouvelles adresses et une base à réindexer (voir Exploitation du contrat) |
| Ethers forcés perdus |
Des ethers envoyés par selfdestruct d'un autre contrat ne sont comptés nulle part et restent bloqués |
#Badge POAP
| Limite |
Détail |
| Pas de lien on-chain avec la loterie |
BlockLuckyPOAP ne vérifie pas qu'un participant a acheté un ticket. Un titulaire de MINTER_ROLE peut frapper un badge pour n'importe quelle adresse et n'importe quel numéro de tour |
| Métadonnées centralisées et modifiables |
Servies par le backend ; setBaseUri les change pour tous les badges. Si le backend disparaît, les badges n'ont plus d'image |
| Pas d'ERC-5192 |
Le blocage des transferts n'est pas annoncé par l'interface standard ; approve et setApprovalForAll restent appelables sans effet |
| Pas de destruction |
Aucun badge ne peut être retiré, même à la demande de son détenteur |
| Pas d'énumération |
Sans ERC721Enumerable, lister les badges d'un détenteur passe par les événements BadgeMinted |
#Automatisation
| Limite |
Détail |
| Aucun upkeep Chainlink enregistré |
npm run automation:sepolia inscrit l'upkeep, mais le contrat n'est pas encore déployé sur Sepolia : le tirage dépend du keeper du backend ou d'un appel manuel (voir Chainlink Automation) |
| Opérations manuelles non outillées |
Le déploiement répartit les rôles et propose la propriété VRF, mais aucun script ne couvre retryDraw, setRoundConfig, setVrfConfig, acceptOwnership ni la rotation ultérieure des rôles : console Hardhat, Remix, Etherscan ou Transaction Builder du multisig |
#Déploiement et outillage
| Limite |
Détail |
| Aucun déploiement Sepolia consigné |
Les scripts vrf:sepolia, deploy:sepolia, automation:sepolia et verify:sepolia existent, mais ils demandent les clés et les fonds de test de l'équipe : le dépôt ne contient pas de deployments/sepolia.json |
| Bloc de départ possiblement tardif |
deploy-sepolia.ts lit deploymentBlock après le déploiement de BlockLucky ; si un bloc a été produit entre-temps, CHAIN_START_BLOCK saute l'événement RoundOpened du tour 1 (voir Déploiement sur Sepolia) |
| Adresses locales fragiles |
Les adresses déterministes supposent que le compte 0 n'a envoyé aucune transaction avant le script, et que l'ordre du script ne change pas |
| Chaîne locale sans persistance |
Un redémarrage du conteneur blockchain efface la chaîne ; la base du backend doit être réinitialisée |
| Triage Slither lié aux lignes |
Les constats acceptés citent des numéros de ligne ; un déplacement de code peut les faire réapparaître et bloquer la CI |
| Couverture des branches non mesurée |
Le seuil de 100 % porte sur les lignes. Certaines conditions de validation (maxTickets nul, keyHash nulle, drawTimeout nul…) ne sont pas testées isolément |
| Scripts Sepolia non testés |
La répartition des rôles de lib/roles.ts est testée (test/roles.test.ts), mais setup-vrf-sepolia.ts, deploy-sepolia.ts, register-automation-sepolia.ts et verify-sepolia.ts ne sont exécutés par aucun test ni par la CI |
| Fuzzing sans état |
Les tests de fuzzing rejouent des scénarios fixes avec des paramètres aléatoires ; il n'y a pas de test d'invariants sur des séquences d'appels quelconques |
#Écarts entre le contrat et le backend
| Écart |
Conséquence |
Le reversement de commission du backend appelle setTreasury avec le compte opérateur |
setTreasury exige DEFAULT_ADMIN_ROLE. En local, l'opérateur le garde pour cette raison ; sur Sepolia, il ne l'a pas et le reversement ne fonctionne que vers MAIRIE_TREASURY_ADDRESS (voir Exploitation du contrat) |
| Le montant demandé au reversement est ignoré en mode on-chain |
withdrawFees verse toujours la totalité de accruedFees |