À la fin de ce notebook, vous saurez : 1. Utiliser un LLM réellement invoqué pour générer du code Solidity 2. Appliquer des patterns de prompting efficaces pour les smart contracts 3. Exploiter un LLM pour la détection de vulnérabilités 4. Générer automatiquement la documentation NatSpec 5. Intégrer l’API OpenAI dans un workflow de développement 6. Compiler et tester sur une EVM locale le Solidity généré avant de le considérer comme utilisable 7. Situer un LLM local (Qwen) comme alternative optionnelle
Prérequis
Notebooks SC-1 à SC-4 (fondements Solidity)
Clé OPENAI_API_KEY dans un fichier .env gitignored
Python 3.10+ avec openai, python-dotenv, py-solc-x, web3, eth-tester, py-evm et requests
Durée estimée : 45 minutes
Introduction
Les Large Language Models ont revolutionne le développement de smart contracts. Ils peuvent :
Generer du code a partir de specifications en langage naturel
Auditer des contrats pour detecter des vulnerabilites
Documenter automatiquement avec des commentaires NatSpec
Expliquer des concepts complexes Solidity
Pourquoi utiliser les LLMs pour Solidity ?
Avantage
Description
Rapidite
Prototypage en minutes, pas en heures
Apprentissage
Explications contextuelles du code genere
Securite
Detection precoce de patterns vulnerables
Documentation
NatSpec genere automatiquement
Limitations importantes
Attention : Les LLMs peuvent generer du code contenant des bugs ou vulnerabilites. Toujours auditer, tester et verifier formellement le code genere avant deploiement en production.
Configuration du client OpenAI et du compilateur Solidity. Le fournisseur est choisi explicitement dans la cellule de paramètres ; la présence opportuniste d’une autre clé ne change jamais le chemin exécuté.
@dataclassclass LLMConfig:"""Configuration du fournisseur réellement exécuté.""" provider: str= DEFAULT_PROVIDER model: str= OPENAI_MODEL max_completion_tokens: int=4096 timeout_seconds: float=90.0config = LLMConfig()if config.provider !="openai":raiseValueError(f"Fournisseur non pris en charge dans ce run : {config.provider}")openai_api_key = os.getenv("OPENAI_API_KEY")ifnot openai_api_key:raiseValueError("OPENAI_API_KEY manque. Ajoutez-la à un fichier .env gitignored avant d'exécuter SC-11." )llm_client = OpenAI(api_key=openai_api_key, timeout=config.timeout_seconds)print(f"Client {config.provider} initialisé avec le modèle {config.model}")print("Clé API chargée depuis l'environnement (valeur non affichée)")
Client openai initialisé avec le modèle gpt-5.6-luna
Clé API chargée depuis l'environnement (valeur non affichée)
Interprétation : prérequis d’exécution
Le notebook exige OPENAI_API_KEY avant toute démonstration : si la clé manque, l’exécution s’arrête avec une erreur explicite et ne fabrique aucun résultat de substitution.
Configuration locale : 1. Créez un fichier .env dans le répertoire SmartContracts ; 2. ajoutez OPENAI_API_KEY=<votre-cle> ; 3. gardez ce fichier hors Git — .env est déjà ignoré par le dépôt.
La cellule confirme seulement que la clé est chargée ; elle n’en affiche jamais la valeur ni un préfixe.
2. Prompt Patterns pour Solidity
Le prompting efficace pour Solidity necessite structure et precision. Voici les templates de base.
# Templates de prompts pour SoliditySOLIDITY_PROMPTS = {"contract_generation": """Tu es un expert en développement de smart contracts Solidity.Génère un contrat Solidity complet pour le cas d'usage suivant :{description}Contraintes :- Version Solidity : ^0.8.20- Contrat autonome : aucun import ni dépendance externe- Constructeur sans paramètre : le contrat est déployé sans argument- Nommer exactement les fonctions appelées par le harnais : `store(uint256)` et `retrieve()`- Ajouter les commentaires NatSpec complets- Implémenter les contrôles de sécurité adaptés- Respecter exactement les fonctionnalités demandées- Solc 0.8.20 : pas d'assembly inline accédant à une variable immutable (ex. `owner.slot` non supporté) — écrire les transferts de propriété en Solidity simple- Utiliser uniquement des caractères ASCII dans les chaînes littérales exécutées par le contratRetourne uniquement un bloc de code Solidity compilable, sans explication supplémentaire.""","audit": """Tu es un auditeur de sécurité de smart contracts.Analyse ce contrat Solidity pour les vulnérabilités potentielles :```solidity{code}```Identifie :1. les vulnérabilités connues ;2. les problèmes de gas ;3. les risques de centralisation ;4. les bonnes pratiques non respectées.Pour chaque finding, utilise exactement une ligne `SEVERITY: CRITICAL`, `SEVERITY: HIGH`,`SEVERITY: MEDIUM`, `SEVERITY: LOW` ou `SEVERITY: INFO`, puis donne la fonction concernée,l'explication et une correction. N'ajoute aucun préfixe Markdown sur la ligne `SEVERITY`.Si aucun finding n'existe, retourne exactement `NO_FINDINGS` sur une ligne isolée.Ne prétends pas avoir exécuté un outil d'analyse statique.""","natspec": """Ajoute une documentation NatSpec complète à ce contrat Solidity sans modifier son comportement :```solidity{code}```Inclus uniquement les tags valides pour chaque déclaration : @notice et @dev selon le besoin,@title et @author uniquement au niveau du contrat (solc les rejette sur une fonction ou un événement),@param exactement une fois par paramètre, et @return uniquement pour une fonction qui déclare une valeurde retour. Ne modifie aucune signature, instruction ni chaîne littérale exécutée par le contrat.Retourne uniquement le contrat complet dans un bloc de code Solidity compilable.""","explain": """Explique ce code Solidity de manière pédagogique :```solidity{code}```Présente son objectif, son état, ses fonctions, ses contrôles de sécurité et ses limites.""",}print("Templates de prompts définis :", ", ".join(SOLIDITY_PROMPTS))
Templates de prompts définis : contract_generation, audit, natspec, explain
2.1 Bonnes pratiques de prompting
Pratique
Exemple
Impact
Spécifier la version
« Solidity ^0.8.20 »
Code compatible
Borner les dépendances
« Contrat autonome, sans import »
Compilation reproductible
Demander NatSpec
« Ajoute les commentaires NatSpec »
Documentation automatique
Contraintes de sécurité
« Applique Checks-Effects-Interactions »
Contrôles explicites
Format de sortie
« Retourne uniquement le code »
Extraction fiable
3. Génération et compilation de contrats via API
Le chemin principal invoque réellement OpenAI, extrait le bloc Solidity, puis le compile avec py-solc-x. Une réponse textuelle non compilable est donc rejetée au lieu d’être présentée comme un contrat utilisable.
SOLC_VERSION ="0.8.20"def extract_solidity_code(response_text: str) ->str:"""Extrait un bloc Solidity d'une réponse LLM.""" solidity_blocks = re.findall(r"```solidity\s*\n(.*?)```", response_text, re.DOTALL | re.IGNORECASE )if solidity_blocks:return solidity_blocks[0].strip() generic_blocks = re.findall(r"```\s*\n(.*?)```", response_text, re.DOTALL)if generic_blocks:return generic_blocks[0].strip()return response_text.strip()def call_llm(prompt: str) ->str:"""Invoque le fournisseur configuré et retourne sa réponse textuelle.""" response = llm_client.chat.completions.create( model=config.model, max_completion_tokens=config.max_completion_tokens, # gpt-5.6-luna : max_completion_tokens requis, temperature non supportee messages=[{"role": "user", "content": prompt}], ) content = response.choices[0].message.contentifnot content:raiseRuntimeError("OpenAI a retourné une réponse vide.")return contentdef compile_contract(solidity_code: str) ->dict[str, Any]:"""Compile le Solidity généré et retourne l'ABI et le bytecode.""" installed = {str(version) for version in get_installed_solc_versions()}if SOLC_VERSION notin installed: install_solc(SOLC_VERSION) artifacts = compile_source( solidity_code, output_values=["abi", "bin"], solc_version=SOLC_VERSION, )ifnot artifacts:raiseRuntimeError("solc n'a produit aucun artefact.") contract_id, artifact =next(iter(artifacts.items()))return {"contract_id": contract_id,"abi": artifact["abi"],"bytecode": artifact["bin"], }def test_simple_storage(compilation: dict[str, Any]) ->dict[str, Any]:"""Déploie et teste SimpleStorage sur une EVM PyEVM locale.""" tester = EthereumTester(backend=PyEVMBackend()) web3 = Web3(EthereumTesterProvider(tester)) owner, outsider = web3.eth.accounts[:2] contract_factory = web3.eth.contract( abi=compilation["abi"], bytecode=compilation["bytecode"] ) deployment = contract_factory.constructor().transact({"from": owner}) deployment_receipt = web3.eth.wait_for_transaction_receipt(deployment) contract = web3.eth.contract( address=deployment_receipt.contractAddress, abi=compilation["abi"] ) initial_value = contract.functions.retrieve().call() update = contract.functions.store(42).transact({"from": owner}) update_receipt = web3.eth.wait_for_transaction_receipt(update) stored_value = contract.functions.retrieve().call() outsider_rejected =Falsetry: contract.functions.store(7).transact({"from": outsider})except TransactionFailed: outsider_rejected =Trueif initial_value !=0or stored_value !=42ornot outsider_rejected:raiseAssertionError("Le test EVM de SimpleStorage n'a pas vérifié le comportement attendu." )return {"backend": type(tester.backend).__name__,"deployment_status": deployment_receipt.status,"update_status": update_receipt.status,"initial_value": initial_value,"stored_value": stored_value,"outsider_rejected": outsider_rejected, }def generate_contract(description: str) ->tuple[str, str]:"""Génère un contrat Solidity avec le fournisseur réellement configuré.""" prompt = SOLIDITY_PROMPTS["contract_generation"].format(description=description) full_response = call_llm(prompt) solidity_code = extract_solidity_code(full_response)return solidity_code, full_responseprint("Fonctions LLM, compilation et test EVM définies")
Fonctions LLM, compilation et test EVM définies
Génération d’un contrat Solidity simple à partir d’une description en langage naturel via l’API LLM.
# Exemple : générer, compiler et tester un contrat simpledescription ="""Un contrat SimpleStorage autonome, sans import externe, qui permet :- stocker une valeur uint256 ;- récupérer la valeur stockée ;- autoriser uniquement le propriétaire à modifier la valeur ;- émettre un événement ValueChanged quand la valeur change."""print("Génération d'un contrat de stockage avec OpenAI...")code, response = generate_contract(description)compiled = compile_contract(code)behavior = test_simple_storage(compiled)print("CONTRAT GÉNÉRÉ :")print("="*60)print(code)print("\nCOMPILATION SOLC : SUCCÈS")print(f"Contrat : {compiled['contract_id']}")print(f"Entrées ABI : {len(compiled['abi'])}")print(f"Bytecode : {len(compiled['bytecode']) //2} octets")print("\nTEST EVM : SUCCÈS")print(f"Backend : {behavior['backend']}")print(f"Déploiement : statut {behavior['deployment_status']}")print(f"Écriture propriétaire : statut {behavior['update_status']}")print(f"Lecture initiale : {behavior['initial_value']}")print(f"Lecture après store(42) : {behavior['stored_value']}")print(f"Écriture non-propriétaire rejetée : {behavior['outsider_rejected']}")
Génération d'un contrat de stockage avec OpenAI...
CONTRAT GÉNÉRÉ :
============================================================
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @title SimpleStorage
* @notice Stores a single uint256 value that can only be modified by the owner.
* @dev The owner is permanently set to the deployer's address.
*/
contract SimpleStorage {
/**
* @notice The address authorized to modify the stored value.
*/
address public immutable owner;
/**
* @notice The currently stored value.
*/
uint256 private _value;
/**
* @notice Emitted when the stored value changes.
* @param oldValue The value before the change.
* @param newValue The value after the change.
*/
event ValueChanged(uint256 indexed oldValue, uint256 indexed newValue);
/**
* @notice Raised when a non-owner attempts to modify the stored value.
*/
error Unauthorized();
/**
* @notice Restricts function access to the owner.
*/
modifier onlyOwner() {
if (msg.sender != owner) {
revert Unauthorized();
}
_;
}
/**
* @notice Initializes the contract and assigns ownership to the deployer.
*/
constructor() {
owner = msg.sender;
}
/**
* @notice Stores a new uint256 value.
* @dev Only the owner can call this function.
* @param newValue The value to store.
*/
function store(uint256 newValue) external onlyOwner {
uint256 oldValue = _value;
_value = newValue;
if (oldValue != newValue) {
emit ValueChanged(oldValue, newValue);
}
}
/**
* @notice Returns the currently stored value.
* @return The stored uint256 value.
*/
function retrieve() external view returns (uint256) {
return _value;
}
}
COMPILATION SOLC : SUCCÈS
Contrat : <stdin>:SimpleStorage
Entrées ABI : 6
Bytecode : 779 octets
TEST EVM : SUCCÈS
Backend : PyEVMBackend
Déploiement : statut 1
Écriture propriétaire : statut 1
Lecture initiale : 0
Lecture après store(42) : 42
Écriture non-propriétaire rejetée : True
Interprétation : génération vérifiée
La cellule précédente ne s’arrête pas à la réponse du LLM : elle passe le code extrait à solc 0.8.20, puis déploie l’artefact sur une EVM locale fournie par PyEVMBackend. Le test vérifie l’état initial, l’écriture propriétaire, la lecture de la valeur 42 et le rejet d’une écriture provenant d’un autre compte.
Les nombres d’entrées ABI et d’octets de bytecode proviennent de l’artefact compilé dans ce run ; les statuts de transaction et valeurs lues proviennent de l’exécution EVM. Cette preuve comportementale bornée ne garantit pas la sûreté générale du contrat. L’audit LLM qui suit reste une aide à la revue, pas un substitut aux tests Foundry plus complets, au fuzzing et à l’expertise humaine.
4. Audit de Securite avec LLM
Utilisons les LLMs pour detecter les vulnerabilites dans les smart contracts.
def normalize_audit_report(report: str) ->str:"""Normalise les lignes de sévérité et rejette un rapport non structuré.""" normalized_lines = [] severities = []for line in report.splitlines(): severity_match = re.search(r"(?i)\bSEVERITY\s*:\s*(CRITICAL|HIGH|MEDIUM|LOW|INFO)\b", line, )if severity_match: severity = severity_match.group(1).upper() severities.append(severity) normalized_lines.append(f"SEVERITY: {severity}")continueif re.fullmatch(r"\s*NO_FINDINGS\s*", line, re.IGNORECASE): normalized_lines.append("NO_FINDINGS")continue normalized_lines.append(line.rstrip())if severities: normalized_lines = [line for line in normalized_lines if line !="NO_FINDINGS"]elif"NO_FINDINGS"notin normalized_lines:raiseRuntimeError("Le rapport d'audit ne contient ni ligne SEVERITY structurée ni NO_FINDINGS." )return"\n".join(normalized_lines).strip()def audit_contract(code: str) ->str:"""Demande au fournisseur configuré d'auditer un contrat Solidity.""" prompt = SOLIDITY_PROMPTS["audit"].format(code=code) raw_report = call_llm(prompt)return normalize_audit_report(raw_report)print("Fonctions d'audit et de normalisation définies")
Fonctions d'audit et de normalisation définies
Exemple d’audit automatique d’un contrat volontairement vulnérable. Le rapport est produit par un appel OpenAI réel ; il reste une aide à la revue et non une analyse statique formelle.
# Exemple: Auditer un contrat vulnerablesvulnerable_contract ="""// SPDX-License-Identifier: MITpragma solidity ^0.8.0;contract VulnerableVault { mapping(address => uint256) public balances; function deposit() external payable { balances[msg.sender] += msg.value; } function withdraw() external { uint256 amount = balances[msg.sender]; // VULNERABILITE: appel externe avant mise a jour (bool success, ) = msg.sender.call{value: amount}(""); require(success, "Transfer failed"); balances[msg.sender] = 0; }}"""print("=== AUDIT DE SECURITE ===")print("Contrat analyse: VulnerableVault")print("\n"+"="*60)audit_result = audit_contract(vulnerable_contract)print(audit_result)
=== AUDIT DE SECURITE ===
Contrat analyse: VulnerableVault
============================================================
SEVERITY: CRITICAL
Fonction concernée : `withdraw()`
Vulnérabilité : réentrance, car un appel externe à `msg.sender` est effectué avant la remise à zéro de `balances[msg.sender]`. Un contrat attaquant peut réappeler `withdraw()` depuis sa fonction `receive()` ou `fallback()` et retirer plusieurs fois le même solde, potentiellement jusqu’à vider le coffre.
Correction : appliquer le modèle Checks-Effects-Interactions en mettant `balances[msg.sender] = 0` avant l’appel externe, puis ajouter éventuellement un verrou de réentrance (`nonReentrant`) ; par exemple, utiliser OpenZeppelin `ReentrancyGuard`.
SEVERITY: MEDIUM
Fonction concernée : `withdraw()`
Vulnérabilité : un bénéficiaire contractuel dont `receive()` ou `fallback()` échoue ne peut pas retirer ses fonds tant que cet appel échoue. L’utilisation d’un appel externe arbitraire augmente aussi la surface d’attaque et rend le retrait dépendant du comportement du destinataire.
Correction : conserver la mise à jour d’état avant l’appel, gérer explicitement l’échec du transfert et envisager un mécanisme de retrait permettant au bénéficiaire de récupérer les fonds via une adresse compatible.
SEVERITY: LOW
Fonction concernée : `withdraw()`
Problème de gas : la fonction effectue un appel externe même lorsque `balances[msg.sender] == 0`, ce qui consomme inutilement du gas et peut déclencher du code arbitraire chez un contrat appelant.
Correction : ajouter `require(amount > 0, "No balance")` avant l’appel externe.
SEVERITY: INFO
Fonctions concernées : `deposit()`, `withdraw()`
Bonne pratique non respectée : le contrat ne fournit pas d’événements `Deposit` et `Withdraw`, ce qui réduit la traçabilité et complique le suivi off-chain des mouvements de fonds.
Correction : émettre des événements après chaque dépôt et retrait, par exemple `event Deposit(address indexed user, uint256 amount)` et `event Withdraw(address indexed user, uint256 amount)`.
SEVERITY: INFO
Fonctions concernées : `deposit()`, `withdraw()`
Risque de centralisation : aucun rôle administrateur, propriétaire ou pouvoir privilégié n’est présent dans ce contrat ; il n’existe donc pas de risque de centralisation identifiable.
Correction : aucune correction nécessaire sur ce point ; si une gouvernance est ajoutée ultérieurement, limiter ses privilèges et les protéger par une multisignature ou un timelock.
Interpretation : Audit de VulnerableVault
Le contrat VulnerableVault contient une reentrancy classique, la vulnerabilite la plus citee dans les hacks DeFi.
Sequence d’attaque :
L’attaquant depose 1 ETH via deposit()
Il appelle withdraw() — le contrat envoie 1 ETH via msg.sender.call{value: amount}("")
Le fallback de l’attaquant reappellewithdraw() avant que balances[msg.sender] = 0 ne s’execute
Le solde est encore a 1 ETH → retrait d’un second ETH
La règle est simple : toute mise a jour de l’etat (Effects) doit preceder tout appel externe (Interactions). La variable amount est lue avant la mise a zero (Checks), garantissant que le reappel trouve un solde a zero. Alternative plus robuste : le modifier ReentrancyGuard d’OpenZeppelin centralise cette protection.
5. Generation de Documentation NatSpec
La documentation NatSpec (Ethereum Natural Language Specification) est essentielle pour les smart contracts.
def generate_natspec(code: str, max_attempts: int=2) ->str:"""Génère du NatSpec et exige une recompilation avant de retourner.""" prompt = SOLIDITY_PROMPTS["natspec"].format(code=code) documented_response = call_llm(prompt)for attempt inrange(max_attempts): documented_code = extract_solidity_code(documented_response)try: compile_contract(documented_code)return documented_responseexcept SolcError as error:if attempt == max_attempts -1:raise documented_response = call_llm(f"""Corrige uniquement les commentaires NatSpec invalides du contrat ci-dessous.Rappel : @title et @author ne sont valides qu'au niveau du contrat, jamais sur une fonction.Ne modifie aucune signature, instruction ni chaîne littérale exécutée.Retourne uniquement le contrat Solidity complet dans un bloc de code compilable.Diagnostic solc :{error.message}Contrat documenté :```solidity{documented_code}```""", )raiseRuntimeError("La génération NatSpec n'a produit aucun contrat compilable.")print("Fonction generate_natspec définie avec validation solc bornée")
Fonction generate_natspec définie avec validation solc bornée
Exemple de documentation NatSpec produite par OpenAI puis recompilée avec solc pour vérifier que l’ajout de commentaires n’a pas altéré le contrat.
# Exemple : ajouter NatSpec à un contrat sans documentationundocumented_contract ="""pragma solidity ^0.8.20;contract Token { string public name = "MyToken"; string public symbol = "MTK"; uint8 public decimals = 18; uint256 public totalSupply; mapping(address => uint256) public balanceOf; event Transfer(address indexed from, address indexed to, uint256 value); constructor(uint256 _initialSupply) { totalSupply = _initialSupply * 10 ** uint256(decimals); balanceOf[msg.sender] = totalSupply; } function transfer(address _to, uint256 _value) public returns (bool success) { require(balanceOf[msg.sender] >= _value, "Insufficient balance"); balanceOf[msg.sender] -= _value; balanceOf[_to] += _value; emit Transfer(msg.sender, _to, _value); return true; }}"""print("=== GÉNÉRATION NATSPEC ===")documented_response = generate_natspec(undocumented_contract)documented_code = extract_solidity_code(documented_response)documented_compilation = compile_contract(documented_code)print(documented_code)print("\nRECOMPILATION SOLC : SUCCÈS")print(f"Contrat : {documented_compilation['contract_id']}")print(f"Bytecode : {len(documented_compilation['bytecode']) //2} octets")
=== GÉNÉRATION NATSPEC ===
pragma solidity ^0.8.20;
/// @title MyToken
/// @author Token contract author
/// @notice Implements a basic fungible token with balances and transfers.
/// @dev Token metadata and supply are initialized during deployment.
contract Token {
/// @notice The name of the token.
string public name = "MyToken";
/// @notice The symbol used to identify the token.
string public symbol = "MTK";
/// @notice The number of decimal places used by the token.
uint8 public decimals = 18;
/// @notice The total number of token units in existence.
uint256 public totalSupply;
/// @notice Returns the token balance of an address.
mapping(address => uint256) public balanceOf;
/// @notice Emitted when tokens are transferred between addresses.
/// @param from The address sending the tokens.
/// @param to The address receiving the tokens.
/// @param value The number of token units transferred.
event Transfer(address indexed from, address indexed to, uint256 value);
/// @notice Creates the token and assigns the initial supply to the deployer.
/// @dev The supplied amount is multiplied by the token's decimal scale.
/// @param _initialSupply The initial token supply before applying decimals.
constructor(uint256 _initialSupply) {
totalSupply = _initialSupply * 10 ** uint256(decimals);
balanceOf[msg.sender] = totalSupply;
}
/// @notice Transfers tokens from the caller to another address.
/// @dev The caller must have a sufficient balance for the requested transfer.
/// @param _to The address receiving the tokens.
/// @param _value The number of token units to transfer.
/// @return success True if the transfer succeeds.
function transfer(address _to, uint256 _value) public returns (bool success) {
require(balanceOf[msg.sender] >= _value, "Insufficient balance");
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
emit Transfer(msg.sender, _to, _value);
return true;
}
}
RECOMPILATION SOLC : SUCCÈS
Contrat : <stdin>:Token
Bytecode : 3843 octets
Interprétation : balises NatSpec
La documentation NatSpec generee inclut les tags standards :
Tag
Usage
@title
Nom du contrat
@author
Auteur du code
@notice
Description utilisateur
@dev
Details techniques
@param
Description des paramètres
@return
Description de la valeur de retour
@event
Description des événements
6. Pipeline LLM vérifiable
SolidityLLMAssistant applique le même fournisseur explicite aux trois étapes : génération, audit et documentation. La génération est en outre compilée par solc avant la poursuite du pipeline. Aucun choix ne dépend de la présence opportuniste d’une autre clé et aucun fallback ne fabrique une réponse terminale.
Démonstration du workflow complet : génération, audit et documentation d’un contrat Solidity via l’assistant LLM.
# Démonstration du workflow completprint("=== WORKFLOW COMPLET LLM ===")result = assistant.full_workflow("""Un contrat Counter autonome, sans import externe, qui permet :- incrémenter un compteur ;- décrémenter le compteur sans passer sous zéro ;- lire la valeur actuelle ;- autoriser uniquement le propriétaire à incrémenter ou décrémenter.""")print("CODE GÉNÉRÉ :")print(result["original_code"])print("\nAUDIT LLM :")print(result["audit_report"])print("\nPREUVE DE COMPILATION :")print(f"Contrat : {result['compilation']['contract_id']}")print(f"Bytecode : {len(result['compilation']['bytecode']) //2} octets")
=== WORKFLOW COMPLET LLM ===
CODE GÉNÉRÉ :
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/// @title Counter
/// @notice Stores a counter value that can be read by anyone and modified only by the owner.
/// @dev The contract is deployed with the deployer as its owner.
contract Counter {
/// @notice The address authorized to modify the counter.
address public immutable owner;
/// @notice The current counter value.
uint256 private _counter;
/// @notice Raised when a non-owner attempts to modify the counter.
error NotOwner();
/// @notice Raised when a decrement would make the counter negative.
error DecrementExceedsCounter();
/// @notice Restricts execution to the contract owner.
modifier onlyOwner() {
if (msg.sender != owner) {
revert NotOwner();
}
_;
}
/// @notice Sets the initial owner of the contract.
/// @dev The deployer becomes the immutable owner.
constructor() {
owner = msg.sender;
}
/// @notice Stores a new value in the counter.
/// @dev Only the owner can modify the stored value.
/// @param value The new counter value.
function store(uint256 value) external onlyOwner {
_counter = value;
}
/// @notice Returns the current counter value.
/// @return The current value of the counter.
function retrieve() external view returns (uint256) {
return _counter;
}
/// @notice Increments the counter by a specified amount.
/// @dev Only the owner can increment the counter.
/// @param amount The amount to add to the counter.
function increment(uint256 amount) external onlyOwner {
_counter += amount;
}
/// @notice Decrements the counter by a specified amount.
/// @dev Only the owner can decrement the counter, and the value cannot become negative.
/// @param amount The amount to subtract from the counter.
function decrement(uint256 amount) external onlyOwner {
if (amount > _counter) {
revert DecrementExceedsCounter();
}
_counter -= amount;
}
}
AUDIT LLM :
SEVERITY: MEDIUM
Fonction concernée : `store`, `increment`, `decrement`, `constructor`
Le contrat repose sur un propriétaire unique et permanent. Une compromission de la clé du déployeur permettrait de modifier arbitrairement le compteur ; la perte de cette clé rendrait définitivement les fonctions d’écriture inutilisables. L’absence de mécanisme de transfert empêche également une rotation opérationnelle de l’administration.
Correction : utiliser un multisig, ou hériter d’un module `Ownable2Step` avec transfert d ownership contrôlé, éventuellement complété par une timelock.
SEVERITY: INFO
Fonction concernée : `store`, `increment`, `decrement`
Aucun événement n’est émis lors des modifications du compteur. Cela complique la surveillance, l’indexation et l’audit off-chain des changements d’état, même si cela ne permet pas une exploitation directe.
Correction : ajouter des événements tels que `CounterStored`, `CounterIncremented` et `CounterDecremented`, incluant l’appelant, l’ancienne valeur et la nouvelle valeur.
SEVERITY: INFO
Fonction concernée : `decrement`
Après le contrôle `amount > _counter`, la soustraction ne peut plus provoquer d’underflow. Le contrôle arithmétique automatique de Solidity 0.8.x reste donc potentiellement redondant et peut entraîner un léger coût en gas supplémentaire.
Correction : remplacer la soustraction par `unchecked { _counter -= amount; }` après le contrôle explicite, uniquement si cette invariant est conservée et documentée.
SEVERITY: INFO
Fonction concernée : `increment`
L’addition est protégée contre l’overflow par les vérifications intégrées de Solidity 0.8.x. Elle revert toutefois lorsque le compteur atteint la limite de `uint256`, ce qui constitue un comportement de déni de service pour les futures incrémentations, mais uniquement sous le contrôle du propriétaire.
Correction : conserver ce comportement si le revert est attendu, ou définir explicitement une limite métier et une erreur dédiée ; ne pas utiliser `unchecked` sans garantie qu’un overflow est acceptable.
SEVERITY: INFO
Fonction concernée : `store`
La fonction effectue une écriture de stockage même lorsque `value` est égal à la valeur actuelle. Ces appels inutiles consomment du gas, même si l’impact est limité et contrôlé par le propriétaire.
Correction : ajouter `if (_counter == value) return;` avant l’écriture, si la réduction de gas justifie cette vérification.
PREUVE DE COMPILATION :
Contrat : <stdin>:Counter
Bytecode : 1335 octets
Interprétation : workflow complet
Le pipeline exécute quatre preuves distinctes :
Étape
Entrée
Sortie vérifiable
Génération
Description en langage naturel
Réponse OpenAI et code Solidity extrait
Compilation
Code généré
Identifiant de contrat et bytecode produits par solc
Audit
Code compilable
Rapport de vulnérabilités issu d’un second appel OpenAI
Documentation
Code généré
Code NatSpec issu d’un troisième appel, lui aussi recompilé
La compilation ferme un défaut important : un LLM peut produire un texte plausible mais syntaxiquement invalide. Elle ne remplace pas les tests comportementaux, qui restent l’étape suivante avec Foundry dans SC-12.
7. LLM Local avec Qwen (Optionnel)
Pour les cas ou les API cloud ne sont pas disponibles ou souhaites, on peut utiliser un LLM local via un endpoint.
class LocalLLMAssistant:"""Assistant optionnel utilisant un endpoint Ollama local."""def__init__(self, endpoint: str="http://localhost:11434/api/generate", model: str="qwen2.5-coder:7b", ):self.endpoint = endpointself.model = modelself.available =self._check_availability()def _check_availability(self) ->bool:"""Vérifie si l'API Ollama répond."""try: response = requests.get("http://localhost:11434/api/tags", timeout=2) response.raise_for_status()returnTrueexcept requests.RequestException:returnFalsedef generate(self, prompt: str, temperature: float=0.2) ->str:"""Génère une réponse via Ollama ou propage l'erreur réseau."""ifnotself.available:raiseRuntimeError("L'endpoint Ollama optionnel n'est pas disponible.") response = requests.post(self.endpoint, json={"model": self.model,"prompt": prompt,"options": {"temperature": temperature},"stream": False, }, timeout=90, ) response.raise_for_status() content = response.json().get("response")ifnot content:raiseRuntimeError("Ollama a retourné une réponse vide.")return contentdef generate_contract(self, description: str) ->str:"""Génère et compile un contrat Solidity via Ollama.""" prompt = SOLIDITY_PROMPTS["contract_generation"].format(description=description) code = extract_solidity_code(self.generate(prompt)) compile_contract(code)return codelocal_assistant = LocalLLMAssistant()print(f"Endpoint Ollama optionnel disponible : {local_assistant.available}")print(f"Modèle local configuré : {local_assistant.model}")
Démonstration de l’assistant LLM local avec un modèle hébergé en local (si disponible) pour la génération de contrats.
# Démonstration locale optionnelle, distincte du chemin principal OpenAIif local_assistant.available: local_code = local_assistant.generate_contract("Un contrat HelloWorld autonome qui stocke et retourne un message." )print("Contrat généré par Ollama et compilé avec solc :")print(local_code)else:print("Extension locale non exécutée : endpoint Ollama indisponible.")print("Le chemin principal OpenAI a déjà fourni la preuve d'exécution de ce notebook.")
Contrat généré par Ollama et compilé avec solc :
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @title HelloWorld
* @author Qwen
* @notice A simple contract to store and retrieve a message.
*/
contract HelloWorld {
/**
* @notice The stored message.
*/
string private message;
/**
* @notice Constructor to initialize the contract.
*/
constructor() {
message = "Hello, World!";
}
/**
* @notice Stores a new message.
* @param _message The new message to store.
*/
function store(uint256 _message) public {
message = string(abi.encodePacked(_message));
}
/**
* @notice Retrieves the stored message.
* @return The stored message.
*/
function retrieve() public view returns (string memory) {
return message;
}
}
API cloud et extension locale
Aspect
Chemin principal OpenAI
Extension locale Ollama
Preuve dans ce notebook
Appels réels + compilation
Exécutée seulement si l’endpoint répond
Données
Envoyées au fournisseur
Restent sur la machine locale
Prérequis
Clé API gitignored
Serveur Ollama et modèle installés
Erreur
Propagée, le notebook échoue
Propagée si une génération locale est lancée
Cette table décrit l’architecture, pas une comparaison de qualité ou de performance. Comparer les modèles exigerait un protocole et des mesures dédiés.
Résumé et bonnes pratiques
Ce que nous avons appris
Le chemin principal de ce notebook suit une règle simple : le LLM propose, les outils déterministes vérifient. OpenAI génère le Solidity, tandis que solc établit sa compilabilité et que PyEVM vérifie les comportements explicitement testés. L’audit LLM et la documentation NatSpec restent des aides probabilistes qui exigent une revue humaine.
Checklist avant déploiement
Perspectives
Le pipeline complet — génération, compilation, audit et documentation — illustre une intégration industrielle minimale mais honnête. Il évite les réponses de substitution : une erreur du fournisseur, de compilation ou de test est propagée au lieu d’être transformée en faux succès.
Les exercices suivants permettent de prolonger le notebook en faisant varier les spécifications, en observant les audits et en construisant une boucle de correction bornée. Le notebook suivant, SC-12-Foundry-Testing-Python, approfondit les tests Solidity.
Exemples guidés et exercices
Les deux exemples guidés ci-dessous sont des resolutions d’etudiants des promotions précédentes, conservees comme demonstrations. Chaque exemple est suivi d’un nouvel exercice a faire vous-même. Les exercices 3 et 4 (plus bas) restent a completer.
Exemple guidé 1 : Generer un Token ERC-20
Contribution etudiante : @Tooom123 (tom) — PR #2474. Resolution conservee comme exemple guide.
Utilisation de l’assistant LLM pour generer un token ERC-20 complet (nom, symbole, decimales, fonctions mint/burn owner-only, events Transfer/Approval, documentation NatSpec), puis audit du code genere.
# Exemple guidé 1 - Générer un ERC-20 avec l'assistant LLM# Contribution originale : @Tooom123erc20_description ="""Un token ERC-20 autonome, sans import externe, avec :- nom MyToken, symbole MTK et 18 décimales ;- supply initiale de 1 000 000 tokens allouée au déployeur ;- fonctions transfer, approve et transferFrom conformes à l'interface ERC-20 ;- fonctions mint et burn réservées au propriétaire ;- événements Transfer et Approval ;- documentation NatSpec complète."""erc20_result = assistant.full_workflow(erc20_description)print("Code ERC-20 généré et compilé :")print(erc20_result["original_code"])print("\nRapport d'audit :")print(erc20_result["audit_report"])print("\nCompilation : SUCCÈS")print(f"Bytecode : {len(erc20_result['compilation']['bytecode']) //2} octets")
Code ERC-20 généré et compilé :
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/// @title MyToken
/// @author
/// @notice Autonomous ERC-20 token with owner-controlled minting and burning.
/// @dev This contract implements the ERC-20 standard without external dependencies.
contract MyToken {
/// @notice The name of the token.
string public constant name = "MyToken";
/// @notice The symbol of the token.
string public constant symbol = "MTK";
/// @notice The number of decimals used by the token.
uint8 public constant decimals = 18;
/// @notice The initial token supply in the smallest unit.
uint256 public constant INITIAL_SUPPLY = 1_000_000 * 10 ** 18;
/// @notice The address with permission to mint, burn, and transfer ownership.
address public owner;
/// @notice The total number of tokens currently in existence.
uint256 public totalSupply;
/// @dev Stores the token balance of each account.
mapping(address => uint256) private _balances;
/// @dev Stores the approved spending allowance for each owner and spender.
mapping(address => mapping(address => uint256)) private _allowances;
/// @dev Stores the value used by the store and retrieve functions.
uint256 private _storedValue;
/// @notice Emitted when tokens are transferred between accounts.
/// @param from The address sending the tokens.
/// @param to The address receiving the tokens.
/// @param value The number of tokens transferred.
event Transfer(address indexed from, address indexed to, uint256 value);
/// @notice Emitted when an allowance is set or changed.
/// @param owner The address that owns the tokens.
/// @param spender The address authorized to spend the tokens.
/// @param value The new allowance amount.
event Approval(
address indexed owner,
address indexed spender,
uint256 value
);
/// @notice Emitted when ownership is transferred.
/// @param previousOwner The address of the former owner.
/// @param newOwner The address of the new owner.
event OwnershipTransferred(
address indexed previousOwner,
address indexed newOwner
);
/// @dev Restricts a function to the current owner.
modifier onlyOwner() {
require(msg.sender == owner, "MyToken: caller is not the owner");
_;
}
/// @notice Deploys the token and assigns the initial supply to the deployer.
constructor() {
owner = msg.sender;
totalSupply = INITIAL_SUPPLY;
_balances[msg.sender] = INITIAL_SUPPLY;
emit OwnershipTransferred(address(0), msg.sender);
emit Transfer(address(0), msg.sender, INITIAL_SUPPLY);
}
/// @notice Returns the token balance of an account.
/// @param account The address whose balance is queried.
/// @return The account token balance.
function balanceOf(address account) external view returns (uint256) {
return _balances[account];
}
/// @notice Returns the amount of tokens that a spender is allowed to use.
/// @param tokenOwner The address that owns the tokens.
/// @param spender The address authorized to spend the tokens.
/// @return The remaining allowance.
function allowance(
address tokenOwner,
address spender
) external view returns (uint256) {
return _allowances[tokenOwner][spender];
}
/// @notice Transfers tokens from the caller to another account.
/// @param to The address receiving the tokens.
/// @param amount The number of tokens to transfer.
/// @return True if the transfer succeeds.
function transfer(
address to,
uint256 amount
) external returns (bool) {
_transfer(msg.sender, to, amount);
return true;
}
/// @notice Approves an account to spend tokens on behalf of the caller.
/// @param spender The address authorized to spend the tokens.
/// @param amount The maximum number of tokens the spender may use.
/// @return True if the approval succeeds.
function approve(
address spender,
uint256 amount
) external returns (bool) {
require(spender != address(0), "MyToken: invalid spender");
_allowances[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
/// @notice Transfers tokens using an allowance.
/// @param from The address from which tokens are withdrawn.
/// @param to The address receiving the tokens.
/// @param amount The number of tokens to transfer.
/// @return True if the transfer succeeds.
function transferFrom(
address from,
address to,
uint256 amount
) external returns (bool) {
uint256 currentAllowance = _allowances[from][msg.sender];
require(
currentAllowance >= amount,
"MyToken: insufficient allowance"
);
if (currentAllowance != type(uint256).max) {
unchecked {
_allowances[from][msg.sender] = currentAllowance - amount;
}
emit Approval(from, msg.sender, _allowances[from][msg.sender]);
}
_transfer(from, to, amount);
return true;
}
/// @notice Creates new tokens and assigns them to an account.
/// @dev This function can only be called by the owner.
/// @param to The address receiving the newly created tokens.
/// @param amount The number of tokens to create.
function mint(address to, uint256 amount) external onlyOwner {
require(to != address(0), "MyToken: mint to zero address");
totalSupply += amount;
_balances[to] += amount;
emit Transfer(address(0), to, amount);
}
/// @notice Destroys tokens from the owner's balance.
/// @dev This function can only be called by the owner.
/// @param amount The number of tokens to destroy.
function burn(uint256 amount) external onlyOwner {
require(
_balances[msg.sender] >= amount,
"MyToken: burn amount exceeds balance"
);
unchecked {
_balances[msg.sender] -= amount;
totalSupply -= amount;
}
emit Transfer(msg.sender, address(0), amount);
}
/// @notice Transfers ownership to another address.
/// @dev This function can only be called by the current owner.
/// @param newOwner The address that will become the new owner.
function transferOwnership(address newOwner) external onlyOwner {
require(newOwner != address(0), "MyToken: new owner is zero address");
address previousOwner = owner;
owner = newOwner;
emit OwnershipTransferred(previousOwner, newOwner);
}
/// @notice Renounces ownership of the contract.
/// @dev After renouncing ownership, owner-only functions cannot be called.
function renounceOwnership() external onlyOwner {
address previousOwner = owner;
owner = address(0);
emit OwnershipTransferred(previousOwner, address(0));
}
/// @notice Stores an arbitrary unsigned integer value.
/// @param value The value to store.
function store(uint256 value) external {
_storedValue = value;
}
/// @notice Returns the currently stored unsigned integer value.
/// @return The stored value.
function retrieve() external view returns (uint256) {
return _storedValue;
}
/// @dev Transfers tokens between two non-zero addresses.
/// @param from The address sending the tokens.
/// @param to The address receiving the tokens.
/// @param amount The number of tokens to transfer.
function _transfer(address from, address to, uint256 amount) internal {
require(from != address(0), "MyToken: transfer from zero address");
require(to != address(0), "MyToken: transfer to zero address");
require(
_balances[from] >= amount,
"MyToken: transfer amount exceeds balance"
);
unchecked {
_balances[from] -= amount;
_balances[to] += amount;
}
emit Transfer(from, to, amount);
}
}
Rapport d'audit :
SEVERITY: HIGH
Fonction concernée : `mint`
Le propriétaire peut créer une quantité illimitée de tokens sans plafond ni mécanisme de gouvernance. Une compromission de la clé du propriétaire, ou un propriétaire malveillant, permettrait de diluer massivement les détenteurs et potentiellement de vendre les tokens nouvellement créés.
Correction : imposer un `MAX_SUPPLY`, limiter les émissions, utiliser un multisig et éventuellement un timelock pour les opérations de mint. Documenter clairement ce risque dans le modèle de confiance.
SEVERITY: MEDIUM
Fonction concernée : `approve`
La modification directe d’une allowance non nulle vers une autre valeur non nulle est exposée à la race condition classique ERC-20 : le spender peut consommer l’ancienne allowance avant que la nouvelle soit appliquée, puis bénéficier de la nouvelle allowance.
Correction : exiger que l’ancienne allowance soit d’abord remise à zéro, ou fournir et recommander des fonctions `increaseAllowance` et `decreaseAllowance`.
SEVERITY: MEDIUM
Fonction concernée : `transferOwnership`
Le transfert de propriété est effectué en une seule étape. Une erreur d’adresse ou l’utilisation d’une adresse inaccessible peut transférer définitivement les privilèges administratifs à un compte inutilisable.
Correction : implémenter un mécanisme en deux étapes avec `pendingOwner` et `acceptOwnership`, et utiliser de préférence un multisig pour le propriétaire.
SEVERITY: LOW
Fonction concernée : `store`
La fonction est accessible à n’importe quelle adresse et permet à n’importe quel utilisateur d’écraser `_storedValue`. Si cette valeur est utilisée par d’autres composants ou supposée être contrôlée par une autorité, elle peut être manipulée ou réinitialisée par un tiers.
Correction : supprimer cette fonctionnalité si elle est inutile, ou restreindre son accès avec `onlyOwner`/une gouvernance. Émettre également un événement lors de sa modification si elle est conservée.
SEVERITY: LOW
Fonctions concernées : `mint`, `burn`, `_transfer`, `approve`, `transferFrom`
Les chaînes de caractères utilisées dans `require` augmentent généralement la taille du bytecode et le coût de certaines exécutions. Les fonctions `transferFrom` émettent aussi un événement `Approval` supplémentaire lors de la diminution de l’allowance, ce qui est standard mais coûteux.
Correction : remplacer les messages par des custom errors, par exemple `error InsufficientBalance();`. Conserver l’événement `Approval` pour la compatibilité ERC-20, sauf si les exigences d’intégration permettent explicitement de l’omettre.
SEVERITY: INFO
Fonctions concernées : `burn`, `_transfer`
Les blocs `unchecked` sont sûrs dans l’état actuel si les invariants `balance <= totalSupply` et la cohérence entre balances et supply sont toujours respectés. Ils augmentent toutefois le risque de régression future si une nouvelle fonction modifie ces invariants.
Correction : utiliser l’arithmétique vérifiée par défaut, ou documenter formellement les invariants et ajouter des tests d’invariant avant de conserver `unchecked`.
SEVERITY: INFO
Fonctions concernées : `store`, `retrieve`
La fonctionnalité de stockage arbitraire est sans rapport avec l’ERC-20, ajoute de l’état persistant et augmente la surface de maintenance et d’audit du contrat.
Correction : déplacer cette fonctionnalité dans un contrat distinct ou la supprimer si elle n’est pas nécessaire.
SEVERITY: INFO
Fonctions concernées : `mint`, `burn`, `transferOwnership`, `renounceOwnership`
Les opérations privilégiées ne disposent pas de délai, de multisig, de plafond ou de mécanisme de gouvernance. Le contrat est donc fortement dépendant de la sécurité et de l’honnêteté de la clé `owner`.
Correction : utiliser un multisig, un timelock, une gouvernance ou des limites explicites sur les opérations sensibles.
Compilation : SUCCÈS
Bytecode : 7015 octets
Exercice 1 (a vous) : Generer un contrat de sequestre (Escrow)
En vous inspirant de l’exemple guide ci-dessus (même structure, description différente), utilisez assistant.full_workflow() pour generer un contrat de sequestre (escrow) a deux parties : - un acheteur depose des fonds, - le vendeur ne peut les recuperer qu’après confirmation de l’acheteur, - l’acheteur peut se faire rembourser si la livraison n’a pas lieu.
Affichez le code genere puis le rapport d’audit.
# Exercice 1 (a vous) - Espace de travail# Etape 1 : Decrire un contrat de sequestre (escrow) a deux parties (acheteur / vendeur)# Etape 2 : Appeler assistant.full_workflow(escrow_description)# Etape 3 : Afficher result["original_code"] puis result["audit_report"]# Indice : reprenez la structure de l'exemple guide 1 ci-dessusescrow_description ="""# Votre description du contrat de sequestre (escrow) ici"""# result = assistant.full_workflow(escrow_description)# print(result["original_code"])# print(result["audit_report"])print("Exercice a completer")
Exercice a completer
Exemple guidé 2 : Workflow de Securite (boucle de generation auto-corrigee)
Contribution etudiante : @Tooom123 (tom) — PR #2474. Resolution conservee comme exemple guide.
Une fonction qui : (1) genere un contrat a partir d’une description, (2) audite le code, (3) si des vulnerabilites CRITICAL/HIGH sont detectees, demande au LLM de corriger, puis (4) recommence jusqu’a un audit propre ou max_iterations. Testee ici sur un contrat de vote simple.
# Exemple guidé 2 - Boucle de génération sécurisée# Contribution originale : @Tooom123def blocking_severities(audit_report: str) ->set[str]:"""Extrait les sévérités bloquantes d'un rapport au format imposé.""" all_severities = re.findall(r"(?im)^\s*SEVERITY\s*:\s*(CRITICAL|HIGH|MEDIUM|LOW|INFO)\s*$", audit_report, ) no_findings = re.search(r"(?im)^\s*NO_FINDINGS\s*$", audit_report)ifnot all_severities andnot no_findings:raiseRuntimeError("Le rapport d'audit ne contient ni ligne SEVERITY structurée ni NO_FINDINGS." )return { severity.upper()for severity in all_severitiesif severity.upper() in {"CRITICAL", "HIGH"} }def secure_generation_loop(description: str, max_iterations: int=2) -> Dict[str, Any]:"""Génère, compile, audite et corrige un contrat avec le même fournisseur.""" code, _ = generate_contract(description) compilation = compile_contract(code) audit = audit_contract(code)for _ inrange(max_iterations -1): severities = blocking_severities(audit)ifnot severities:break correction_prompt =f"""Corrige tous les findings CRITICAL et HIGH de l'audit ci-dessous.Retourne uniquement le contrat Solidity complet, autonome et compilable.Conserve uniquement des caractères ASCII dans les chaînes littérales exécutées par le contrat.Contrat :```solidity{code}```Audit :{audit}""" code = extract_solidity_code(call_llm(correction_prompt)) compilation = compile_contract(code) audit = audit_contract(code) final_severities =sorted(blocking_severities(audit))return {"code_final": code,"rapport_audit": audit,"severites_bloquantes": final_severities,"converged": not final_severities,"compilation": compilation, }voting_description ="""Un contrat de vote autonome où chaque adresse ne peut voter qu'une fois,le propriétaire peut ajouter des candidats avant l'ouverture du scrutin,et personne ne peut voter après la clôture."""resultat = secure_generation_loop(voting_description)print("Code final compilé :")print(resultat["code_final"])print("\nRapport d'audit final :")print(resultat["rapport_audit"])print("\nSévérités bloquantes finales :", resultat["severites_bloquantes"])print("Compilation : SUCCÈS")print(f"Bytecode : {len(resultat['compilation']['bytecode']) //2} octets")if resultat["converged"]:print("Verdict de boucle : aucune sévérité CRITICAL/HIGH détectée à la borne.")else:print("Verdict de boucle : borne atteinte avec revue humaine requise avant utilisation.")
Code final compilé :
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/// @title AutonomousVoting
/// @author
/// @notice Provides a controlled voting process with one vote per address.
/// @dev Candidates can only be added before the voting process is opened.
contract AutonomousVoting {
/// @notice Represents the current phase of the voting process.
enum Phase {
Setup,
Open,
Closed
}
/// @notice Represents a candidate and the number of votes received.
struct Candidate {
string name;
uint256 voteCount;
}
/// @notice Address that owns and administers the contract.
address public owner;
/// @notice Current phase of the voting process.
Phase public phase;
/// @notice Number of candidates registered in the contract.
uint256 public candidateCount;
/// @notice Candidate data indexed by candidate identifier.
mapping(uint256 => Candidate) private candidates;
/// @notice Records whether an address has already voted.
mapping(address => bool) public hasVoted;
/// @notice Stores a compatibility value accessible through store and retrieve.
uint256 private storedValue;
/// @notice Emitted when ownership is transferred.
/// @param previousOwner Address of the former owner.
/// @param newOwner Address of the new owner.
event OwnershipTransferred(
address indexed previousOwner,
address indexed newOwner
);
/// @notice Emitted when a candidate is added.
/// @param candidateId Identifier assigned to the candidate.
/// @param name Candidate name.
event CandidateAdded(uint256 indexed candidateId, string name);
/// @notice Emitted when the voting process is opened.
event VotingOpened();
/// @notice Emitted when the voting process is closed.
event VotingClosed();
/// @notice Emitted when a vote is cast.
/// @param voter Address that cast the vote.
/// @param candidateId Identifier of the selected candidate.
event VoteCast(address indexed voter, uint256 indexed candidateId);
/// @notice Raised when a caller is not the owner.
error NotOwner();
/// @notice Raised when the zero address is supplied where it is not allowed.
error ZeroAddress();
/// @notice Raised when an operation is attempted in an invalid phase.
error InvalidPhase();
/// @notice Raised when no candidates exist.
error NoCandidates();
/// @notice Raised when a candidate identifier is invalid.
error InvalidCandidate();
/// @notice Raised when an address attempts to vote more than once.
error AlreadyVoted();
/// @notice Raised when an empty candidate name is supplied.
error EmptyCandidateName();
/// @notice Restricts execution to the owner.
modifier onlyOwner() {
if (msg.sender != owner) {
revert NotOwner();
}
_;
}
/// @notice Creates the voting contract and assigns ownership to the deployer.
constructor() {
owner = msg.sender;
phase = Phase.Setup;
emit OwnershipTransferred(address(0), msg.sender);
}
/// @notice Transfers ownership to another address.
/// @dev Ownership can be transferred only while the voting process is in setup.
/// @param newOwner Address that will become the new owner.
function transferOwnership(address newOwner) external onlyOwner {
if (phase != Phase.Setup) {
revert InvalidPhase();
}
if (newOwner == address(0)) {
revert ZeroAddress();
}
address previousOwner = owner;
owner = newOwner;
emit OwnershipTransferred(previousOwner, newOwner);
}
/// @notice Adds a candidate before the voting process is opened.
/// @param name Name of the candidate.
/// @return candidateId Identifier assigned to the new candidate.
function addCandidate(
string calldata name
) external onlyOwner returns (uint256 candidateId) {
if (phase != Phase.Setup) {
revert InvalidPhase();
}
if (bytes(name).length == 0) {
revert EmptyCandidateName();
}
candidateId = candidateCount;
candidates[candidateId] = Candidate({
name: name,
voteCount: 0
});
candidateCount = candidateCount + 1;
emit CandidateAdded(candidateId, name);
}
/// @notice Opens the voting process.
/// @dev At least one candidate must have been added before opening.
function openVoting() external onlyOwner {
if (phase != Phase.Setup) {
revert InvalidPhase();
}
if (candidateCount == 0) {
revert NoCandidates();
}
phase = Phase.Open;
emit VotingOpened();
}
/// @notice Closes the voting process permanently.
function closeVoting() external onlyOwner {
if (phase != Phase.Open) {
revert InvalidPhase();
}
phase = Phase.Closed;
emit VotingClosed();
}
/// @notice Casts one vote for a candidate.
/// @dev Each address can vote at most once and only while voting is open.
/// @param candidateId Identifier of the selected candidate.
function vote(uint256 candidateId) external {
if (phase != Phase.Open) {
revert InvalidPhase();
}
if (candidateId >= candidateCount) {
revert InvalidCandidate();
}
if (hasVoted[msg.sender]) {
revert AlreadyVoted();
}
hasVoted[msg.sender] = true;
candidates[candidateId].voteCount =
candidates[candidateId].voteCount +
1;
emit VoteCast(msg.sender, candidateId);
}
/// @notice Returns the data of a candidate.
/// @param candidateId Identifier of the candidate.
/// @return name Candidate name.
/// @return voteCount Number of votes received by the candidate.
function getCandidate(
uint256 candidateId
) external view returns (string memory name, uint256 voteCount) {
if (candidateId >= candidateCount) {
revert InvalidCandidate();
}
Candidate storage candidate = candidates[candidateId];
return (candidate.name, candidate.voteCount);
}
/// @notice Returns the candidate with the highest number of votes.
/// @dev In case of a tie, returns the candidate with the lowest identifier.
/// @return candidateId Identifier of the winning candidate.
/// @return name Name of the winning candidate.
/// @return voteCount Number of votes received by the winning candidate.
function winningCandidate()
external
view
returns (
uint256 candidateId,
string memory name,
uint256 voteCount
)
{
if (candidateCount == 0) {
revert NoCandidates();
}
candidateId = 0;
voteCount = candidates[0].voteCount;
for (uint256 index = 1; index < candidateCount; index++) {
if (candidates[index].voteCount > voteCount) {
candidateId = index;
voteCount = candidates[index].voteCount;
}
}
name = candidates[candidateId].name;
}
/// @notice Stores an unsigned integer value.
/// @param value Value to store.
function store(uint256 value) external {
storedValue = value;
}
/// @notice Retrieves the previously stored unsigned integer value.
/// @return value The stored value.
function retrieve() external view returns (uint256 value) {
return storedValue;
}
}
Rapport d'audit final :
SEVERITY: MEDIUM
Fonction : `addCandidate`, `openVoting`, `closeVoting`, `transferOwnership`
Problème : Le propriétaire contrôle entièrement les candidats, le lancement et la clôture du vote. Il peut ajouter des candidats arbitraires, empêcher indéfiniment l’ouverture, fermer le vote immédiatement après son ouverture ou transférer le contrôle à une adresse choisie. Cela constitue un risque de centralisation et exige une confiance totale envers le propriétaire.
Correction : Utiliser une gouvernance multisignature ou décentralisée, définir une durée minimale et maximale de vote, et éventuellement rendre l’ouverture et la clôture automatiques selon des timestamps immuables.
SEVERITY: MEDIUM
Fonction : `winningCandidate`
Problème : La fonction parcourt tous les candidats avec une boucle linéaire. Comme aucun nombre maximal de candidats n’est défini, son coût peut dépasser la limite de gas d’un appel on-chain ou d’un RPC. Tout contrat dépendant de cette fonction pourrait devenir inutilisable.
Correction : Maintenir le gagnant courant lors de chaque vote, limiter `candidateCount`, ou fournir une fonction paginée et effectuer l’agrégation hors chaîne.
SEVERITY: LOW
Fonction : `addCandidate`
Problème : Le nombre de candidats et la longueur de `name` sont illimités. Le stockage de chaînes et l’émission de `CandidateAdded` deviennent coûteux, et un grand nombre de candidats aggrave également les lectures et calculs ultérieurs.
Correction : Imposer une limite maximale au nombre de candidats et à la longueur du nom, par exemple avec `MAX_CANDIDATES` et `MAX_NAME_LENGTH`.
SEVERITY: LOW
Fonction : `transferOwnership`
Problème : Le transfert de propriété est immédiat et ne nécessite aucune acceptation par le nouveau propriétaire. Une erreur d’adresse peut donc rendre le contrôle difficile ou impossible à récupérer, en particulier parce que le transfert n’est possible que pendant `Setup`.
Correction : Implémenter un modèle en deux étapes avec `pendingOwner` et `acceptOwnership`, et éventuellement prévoir une procédure de récupération gouvernée.
SEVERITY: INFO
Fonction : `vote`
Problème : La restriction porte sur une adresse, et non sur une personne ou une identité unique. Un utilisateur peut créer un nombre illimité d’adresses et voter plusieurs fois. Cela respecte la documentation actuelle, mais ne garantit pas réellement « une personne, un vote ».
Correction : Utiliser une liste blanche d’identités vérifiées, des signatures d’un système d’identité ou un mécanisme de preuve anti-Sybil si l’objectif est de limiter le vote à une personne.
SEVERITY: INFO
Fonction : `store`
Problème : Toute adresse peut modifier `storedValue`. Si cette variable est destinée à être une donnée administrée ou fiable, n’importe quel utilisateur peut la remplacer. La documentation actuelle ne précise toutefois pas qu’elle doit être protégée.
Correction : Ajouter `onlyOwner` ou une autorisation dédiée si la valeur doit être contrôlée ; sinon documenter explicitement son caractère public et modifiable par tous.
SEVERITY: INFO
Fonction : `addCandidate`
Problème : Le contrôle `bytes(name).length == 0` interdit uniquement les chaînes vides, mais autorise des noms composés uniquement d’espaces ou contenant des données Unicode ambiguës. Cela peut entraîner une présentation trompeuse des candidats.
Correction : Valider les noms hors chaîne ou implémenter une validation stricte adaptée au format attendu, notamment une longueur maximale et une normalisation.
SEVERITY: INFO
Fonction : `store`
Problème : Aucun événement n’est émis lors de la modification de `storedValue`, ce qui complique le suivi fiable des changements par les indexeurs et les applications clientes.
Correction : Ajouter un événement `ValueStored(uint256 oldValue, uint256 newValue)` émis après chaque modification.
Sévérités bloquantes finales : []
Compilation : SUCCÈS
Bytecode : 5846 octets
Verdict de boucle : aucune sévérité CRITICAL/HIGH détectée à la borne.
Exercice 2 (à vous) : journaliser les itérations de la boucle sécurisée
À partir de l’exemple guidé 2, écrivez une variante secure_generation_loop_journalisee qui enregistre à chaque itération le numéro d’itération et les sévérités bloquantes retournées par blocking_severities(), puis retourne cet historique avec le code final.
Indice : stockez sorted(blocking_severities(audit)) plutôt que de compter toutes les occurrences des mots « HIGH » et « CRITICAL » dans la prose.
# Exercice 2 (à vous) - Espace de travail# Objectif : variante de secure_generation_loop qui journalise chaque itération# Étape 1 : repartir de l'exemple guidé 2 ci-dessus# Étape 2 : ajouter {"iteration": i, "severites": [...]} à une liste# Étape 3 : retourner code_final, rapport_audit et historique# Indice : sorted(blocking_severities(audit))def secure_generation_loop_journalisee( description: str, max_iterations: int=3) -> Dict[str, Any] |None: historique = [] # TODO étudiant : journal des itérations# TODO étudiant : implémenter la variante sans réponse préfabriquéereturnNone# test_description = "Un contrat de tirage au sort autonome"# print(secure_generation_loop_journalisee(test_description))print("Exercice a completer")
Exercice a completer
Exercice 3 : Generer la documentation NatSpec d’un contrat
Reutilisez generate_natspec() (section 5) pour documenter automatiquement un contrat sans commentaires. Comptez les tags @notice dans la sortie.
Indice : generate_natspec retourne le code documente. Comptez "@notice" dedans.
# Exercice 3 - Generer la documentation NatSpec d'un contrat# TODO etudiant : utiliser generate_natspec() pour documenter un contrat sans commentaires# Etape 1 : Definir contract_a_documenter (un contrat Solidity minimal sans NatSpec)# Etape 2 : Appeler generate_natspec(contract_a_documenter)# Etape 3 : Extraire le code et compter les tags @noticecontract_a_documenter ="""pragma solidity ^0.8.20;contract Counter { uint256 public count; function increment() public { count += 1; } function reset() public { count = 0; }}"""# documented = generate_natspec(contract_a_documenter)# code = extract_solidity_code(documented)# print(code)# print("Tags @notice trouves :", code.count("@notice"))print("Exercice a completer")
Exercice a completer
Exercice 4 : comparer deux formulations d’audit
Réutilisez audit_contract() pour auditer le même contrat vulnérable avec deux formulations du code : une version utilisant tx.origin, puis une version corrigée utilisant msg.sender. Comparez les sévérités et les recommandations.
Indice : le fournisseur reste identique ; l’expérience isole l’effet de la correction du contrat plutôt qu’une différence entre deux modèles.
# Exercice 4 - Comparer un contrat vulnérable et sa correction# TODO étudiant : auditer les deux versions avec le même fournisseur# Étape 1 : conserver la version vulnérable utilisant tx.origin# Étape 2 : écrire contrat_corrige en remplaçant ce contrôle par msg.sender# Étape 3 : appeler audit_contract() sur chaque version et comparer les sévérités# Indice : cherchez notamment la mention de phishing dans les rapportscontrat_vulnerable ="""// SPDX-License-Identifier: MITpragma solidity ^0.8.20;contract PhishableWallet { address public owner; constructor() { owner = msg.sender; } function transferTo(address payable dest, uint256 amount) external { require(tx.origin == owner, "Not owner"); dest.transfer(amount); }}"""contrat_corrige ="""// TODO étudiant : recopier le contrat et remplacer le contrôle tx.origin"""# rapport_vulnerable = audit_contract(contrat_vulnerable)# rapport_corrige = audit_contract(contrat_corrige)# print(rapport_vulnerable)# print(rapport_corrige)print("Exercice a completer")
Le pipeline démontré associe une invocation OpenAI réelle à des validations locales indépendantes : génération du Solidity, compilation, déploiement et test comportemental borné sur PyEVM, audit LLM, ajout de NatSpec et recompilation. L’exemple VulnerableVault montre en particulier comment un rapport LLM peut guider la revue d’une réentrance et de son correctif Checks-Effects-Interactions, sans être présenté comme une analyse statique formelle.
Cette combinaison rend visibles les limites de chaque outil : le LLM propose, audite et documente de manière probabiliste ; solc établit que le source produit un artefact EVM valide ; le test PyEVM vérifie quelques comportements explicitement choisis. L’extension Qwen/Ollama reste optionnelle et distincte ; elle ne remplace jamais la preuve du chemin principal OpenAI.
Un test borné ne démontre pas la sûreté générale. Avant tout déploiement, complétez cette première barrière par des tests Foundry plus complets, du fuzzing, une analyse statique et une revue humaine adaptée au niveau de risque. Le notebook suivant, SC-12-Foundry-Testing-Python, introduit cette étape de tests Solidity.