BlockLucky Documentation officielle GitHub

Exploitation du contrat

Cette page s'adresse à l'équipe qui exploite BlockLucky une fois déployé : la Mairie (administrateur) et le compte opérateur du backend. Elle décrit chaque opération réservée à un rôle, quand l'utiliser, comment l'exécuter et ce qu'elle ne permet pas. Les procédures d'incident pas à pas sont dans Runbooks blockchain.

Outils

Trois moyens d'appeler les fonctions d'administration :

Moyen Usage
Interface d'administration Mairie, via l'API du backend Pause, annulation du tour, versement de la commission : signés par le compte opérateur (voir Administration Mairie)
Console Hardhat Toute opération, signée par le compte configuré pour le réseau (SEPOLIA_PRIVATE_KEY sur Sepolia, compte 0 en local)
Etherscan (« Write Contract ») ou Remix (« At Address ») Toute opération, signée par MetaMask, une fois le code vérifié

Le backend n'appelle ni retryDraw, ni setRoundConfig, ni setVrfConfig, ni setCoordinator, ni grantRole : ces opérations se font toujours à la main.

Les exemples de cette page utilisent la console Hardhat, ouverte depuis smart-contract/ :

Terminal
npx hardhat console --network sepolia
JavaScript
const { ethers } = await network.create();
const lottery = await ethers.getContractAt("BlockLucky", "<adresse BlockLucky>");
const poap = await ethers.getContractAt("BlockLuckyPOAP", "<adresse BlockLuckyPOAP>");

Remplacer sepolia par localhost et les adresses par celles de la blockchain locale pour s'entraîner sans risque. Chaque écriture renvoie une transaction ; await (await lottery.pause()).wait() attend sa confirmation.

Rôles

Rôle Contrat Titulaires prévus Fonctions
DEFAULT_ADMIN_ROLE Les deux Multisig de la Mairie (MAIRIE_ADMIN_ADDRESS) ; en local, compte n° 2 et opérateur Configuration, trésorerie, métadonnées, gestion des rôles
OPERATOR_ROLE BlockLucky Opérateur retryDraw, cancelRound, withdrawFees
PAUSER_ROLE BlockLucky PAUSER_ADDRESS, l'opérateur par défaut pause, unpause
MINTER_ROLE BlockLuckyPOAP Opérateur mintBadge
Propriétaire ConfirmedOwner BlockLucky VRF_OWNER_ADDRESS (l'administrateur par défaut), après acceptOwnership setCoordinator, transferOwnership

Ces titulaires sont mis en place par les scripts de déploiement, qui retirent ensuite tout rôle au compte déployeur (voir Déploiement sur Sepolia). Sur Sepolia, les opérations d'administration ci-dessous sont donc des transactions du multisig, depuis l'onglet « Write Contract » d'Etherscan ou le Transaction Builder du Safe ; la console Hardhat ne sert qu'aux comptes dont la clé est dans le keystore.

Accorder, retirer, vérifier

JavaScript
const OPERATOR_ROLE = await lottery.OPERATOR_ROLE();
const PAUSER_ROLE = await lottery.PAUSER_ROLE();
const MINTER_ROLE = await poap.MINTER_ROLE();
const operator = "<adresse opérateur>";

await (await lottery.grantRole(OPERATOR_ROLE, operator)).wait();
await (await lottery.grantRole(PAUSER_ROLE, operator)).wait();
await (await poap.grantRole(MINTER_ROLE, operator)).wait();

await lottery.hasRole(OPERATOR_ROLE, operator);

revokeRole(role, compte) retire un rôle ; renounceRole(role, son_adresse) permet à un compte d'abandonner le sien. Chaque changement émet RoleGranted ou RoleRevoked.

Danger

Ne jamais retirer DEFAULT_ADMIN_ROLE au dernier compte qui le détient : plus personne ne pourrait configurer le contrat ni gérer les rôles, et il n'existe aucun moyen de le récupérer. Accorder d'abord le rôle au nouveau compte, vérifier avec hasRole, puis seulement retirer l'ancien.

Pause

Le disjoncteur se déclenche en cas d'incident : faille suspectée, comportement anormal du backend, demande juridique.

JavaScript
await (await lottery.pause()).wait();
await lottery.paused();
await (await lottery.unpause()).wait();
Bloqué en pause Toujours possible en pause
buyTickets, buyTicketsFor claimPrize, claimRefund, withdrawFees
requestDraw (et donc performUpkeep pour un tirage) closeExpiredRound, cancelRound, retryDraw
checkUpkeep renvoie false Réponse VRF d'un tirage déjà demandé
Fonctions d'administration

La pause ne gèle donc jamais les fonds : un gagnant peut toujours retirer sa cagnotte, et l'opérateur peut annuler le tour pour rendre les mises. Le keeper du backend ne fait rien tant que paused() est vrai. pause sur un contrat déjà en pause échoue avec EnforcedPause, unpause sur un contrat actif avec ExpectedPause.

Côté backend, l'interrupteur d'urgence de l'administration appelle pause ou unpause avec le compte opérateur : voir Disjoncteur.

Tirage bloqué : retryDraw

Symptôme : le tour est en Drawing depuis longtemps et aucun WinnerPicked n'est arrivé.

  1. Diagnostiquer : solde de l'abonnement VRF, contrat bien listé comme consommateur, état du réseau, journal de l'oracle local en développement.
  2. Corriger la cause : réapprovisionner l'abonnement, ou relever callbackGasLimit par setVrfConfig si le callback a manqué de gaz.
  3. Vérifier que le délai est écoulé : drawRequestedAt + drawTimeout doit être dans le passé.
  4. Relancer.
JavaScript
const id = await lottery.currentRoundId();
const round = await lottery.getRound(id);
const { drawTimeout } = await lottery.vrfConfig();
console.log(Number(round.status), new Date(Number(round.drawRequestedAt + drawTimeout) * 1000));

await lottery.retryDraw.staticCall();
await (await lottery.retryDraw()).wait();

staticCall simule l'appel : il renvoie DrawNotStale si le délai n'est pas écoulé, DrawNotReady si aucun tirage n'est en cours. La relance émet un nouveau DrawRequested ; une réponse tardive à l'ancienne requête sera ignorée.

Annulation : cancelRound

JavaScript
await (await lottery.cancelRound()).wait();

Annule le tour en cours, qu'il soit ouvert ou en tirage, et ouvre le suivant. Les payeurs récupèrent l'intégralité de leurs mises par claimRefund ; le backend les réclame pour eux par sa file d'actions on-chain. Aucune commission n'est prélevée.

À réserver aux cas où le tour ne peut pas se conclure normalement : oracle durablement indisponible, incident de sécurité, décision de la Mairie. Annuler un tour en tirage revient à écarter un résultat à venir ; cet usage doit rester exceptionnel et tracé (voir Sécurité du contrat). Un tour expiré sans assez de participants n'a pas besoin de cancelRound : closeExpiredRound, appelable par tous, suffit.

Commission : withdrawFees et setTreasury

JavaScript
await lottery.accruedFees();
await lottery.treasury();
await (await lottery.withdrawFees()).wait();

withdrawFees verse la totalité de accruedFees à l'adresse treasury, jamais ailleurs, et émet FeesWithdrawn. Il échoue avec NothingToWithdraw si rien n'est dû, et avec TransferFailed si la trésorerie refuse les ethers (le montant reste alors dû).

Changer la trésorerie est réservé à l'administrateur :

JavaScript
await (await lottery.setTreasury("<nouvelle adresse>")).wait();

Attention

Le service de reversement du backend (withdrawFees de backend/src/blockchain/lottery.gateway.ts) appelle setTreasury avec le compte opérateur quand l'adresse demandée diffère de la trésorerie actuelle. setTreasury exige DEFAULT_ADMIN_ROLE. En local, deploy-local.ts le laisse à l'opérateur (sauf LOCAL_STRICT_ROLES=true) et le reversement fonctionne vers n'importe quelle adresse. Sur Sepolia, deploy-sepolia.ts le réserve au multisig : le reversement vers une nouvelle adresse échoue avec AccessControlUnauthorizedAccount et ne fonctionne que vers MAIRIE_TREASURY_ADDRESS, déjà configurée. Le montant demandé n'est pas non plus pris en compte en mode on-chain : tout accruedFees est versé.

Configuration des tours : setRoundConfig

JavaScript
await (await lottery.setRoundConfig({
  ticketPrice: ethers.parseEther("0.01"),
  maxTickets: 10000,
  minTicketsToDraw: 2,
  maxTicketsPerPlayer: 1,
  duration: 48 * 60 * 60,
  feeBps: 1000,
})).wait();
  • La structure entière est remplacée : tous les champs sont obligatoires. Partir de await lottery.roundConfig() pour ne changer qu'un champ.
  • La nouvelle configuration s'applique au tour suivant. Le tour en cours garde son prix, ses plafonds, sa durée et sa commission, copiés à son ouverture. Pour l'appliquer tout de suite, il faudrait annuler le tour en cours, ce qui rembourse tout le monde.
  • Les règles de validation sont celles de Contrat BlockLucky ; une valeur invalide échoue avec InvalidConfiguration.
  • L'événement RoundConfigUpdated ne porte que le prix, le plafond, la durée et la commission.

Configuration VRF : setVrfConfig

JavaScript
const current = await lottery.vrfConfig();
await (await lottery.setVrfConfig({
  keyHash: current.keyHash,
  subscriptionId: current.subscriptionId,
  callbackGasLimit: 700000,
  requestConfirmations: current.requestConfirmations,
  nativePayment: current.nativePayment,
  drawTimeout: current.drawTimeout,
})).wait();

Contrairement à setRoundConfig, cette configuration s'applique immédiatement : à la prochaine requête (requestDraw ou retryDraw), et pour drawTimeout, dès le prochain contrôle de retryDraw, y compris sur un tirage en attente.

Cas d'usage Champ à changer Préalable
Changer d'abonnement subscriptionId, éventuellement nativePayment Ajouter le contrat comme consommateur du nouvel abonnement avant le prochain tirage
Callback à court de gaz callbackGasLimit Aucun ; puis retryDraw
Changer de voie de prix du gaz keyHash Vérifier que la key hash existe sur le coordinateur
Passer du LINK à l'ETH nativePayment Abonnement financé dans la nouvelle monnaie

setVrfConfig ne change pas le coordinateur : c'est setCoordinator, réservé au propriétaire.

Métadonnées des badges : setBaseUri

JavaScript
await (await poap.setBaseUri("https://api.etherbay.tech/api/v1/lottery/poap/")).wait();

Le préfixe change pour tous les badges, passés et futurs. Il doit se terminer par /, car le contrat y concatène directement le numéro du tour et .json.

Rotation des clés

Compte opérateur

À faire en cas de fuite suspectée de RELAYER_PRIVATE_KEY, ou périodiquement :

  1. Créer un nouveau compte et l'approvisionner en ETH Sepolia.
  2. Avec l'administrateur, lui accorder OPERATOR_ROLE, PAUSER_ROLE et MINTER_ROLE.
  3. Mettre la nouvelle clé dans RELAYER_PRIVATE_KEY et redémarrer le backend.
  4. Avec l'administrateur, retirer les trois rôles à l'ancien compte, puis vérifier avec hasRole.
  5. Transférer le solde restant de l'ancien compte s'il est encore maîtrisé.

En cas de compromission avérée, inverser les étapes 2 et 4 : retirer d'abord les rôles, quitte à interrompre le service. Un opérateur compromis peut au pire annuler des tours, mettre en pause et frapper des badges ; il ne peut pas prendre les mises.

Administrateur

  1. Accorder DEFAULT_ADMIN_ROLE au nouveau compte sur les deux contrats (et OPERATOR_ROLE, PAUSER_ROLE, MINTER_ROLE s'il doit les garder).
  2. Vérifier avec hasRole.
  3. Avec le nouveau compte, retirer les rôles de l'ancien.

Propriétaire ConfirmedOwner

Le transfert se fait en deux temps, pour éviter de céder la propriété à une adresse erronée :

JavaScript
await (await lottery.transferOwnership("<nouvel administrateur>")).wait();

Puis, avec le nouveau compte :

JavaScript
await (await lottery.acceptOwnership()).wait();
await lottery.owner();

Les scripts de déploiement font la première étape eux-mêmes : deploy-local.ts enchaîne aussi acceptOwnership avec le compte n° 2, tandis que sur Sepolia le multisig (VRF_OWNER_ADDRESS) doit l'appeler. Tant qu'il ne l'a pas fait, owner() renvoie encore le déployeur et deployments/sepolia.json indique vrfOwnershipPending: true.

Redéploiement

Un contrat déployé est immuable : il n'y a ni proxy ni mécanisme de mise à jour. Corriger un bug ou changer une règle impose un nouveau déploiement.

  1. Clore l'ancien contrat proprement. Mettre en pause pour arrêter les achats. Laisser le tirage en cours aboutir ou annuler le tour. Les fonds dus (cagnottes, remboursements, commission) restent retirables sur l'ancien contrat, sans limite de temps : ne pas le détruire et continuer à servir les retraits.
  2. Déployer les nouveaux contrats (voir Déploiement sur Sepolia), puis faire accepter la propriété VRF par le multisig, inscrire un nouvel upkeep (npm run automation:sepolia, le nouveau sepolia.json ne contenant plus l'ancien) et vérifier le code (npm run verify:sepolia). Les numéros de tour repartent de 1.
  3. Ajouter le nouveau contrat comme consommateur VRF et, si besoin, retirer l'ancien de l'abonnement une fois ses tirages terminés.
  4. Mettre à jour les ABI si les interfaces ont changé : npm run export:abi, puis vérifier avec node scripts/export-abi.mjs --check.
  5. Reconfigurer le backend : LOTTERY_CONTRACT_ADDRESS, POAP_CONTRACT_ADDRESS, CHAIN_START_BLOCK. La base indexe des numéros de tour du contrat (contractRoundId) qui repartent à 1 : la projection doit être réinitialisée ou migrée (voir Reconstruire depuis la chaîne).
  6. Communiquer la nouvelle adresse aux citoyens, avec le lien Etherscan du code vérifié.

Si l'ancien contrat a des retraits en attente, le backend configuré sur le nouveau contrat ne les réclamera plus pour les citoyens : il faut les traiter à part, avant de basculer.