SC-18-Vyper-Python - Smart Contracts en Python-like

<< E2E Verifiable Voting | Ripple XRP >>


Objectifs d’apprentissage

  1. Comprendre la philosophie de conception de Vyper et ses différences avec Solidity
  2. Maitriser la syntaxe Vyper pour les types de base, fonctions et decorateurs
  3. Ecrire et compiler un contrat de stockage et un contrat de token en Vyper
  4. Deployer un contrat Vyper sur une blockchain locale via web3.py
  5. Comparer les approches Vyper et Solidity sur des exemples equivalents

Prerequis

  • Python 3.10+ avec web3.py
  • Compilateur vyper (optionnel, le notebook fonctionne en mode degrade sans)
  • anvil (Foundry) pour le deploiement local

Duree estimee : 45 minutes


1. Introduction a Vyper

Vyper est un langage de smart contracts pour l’EVM (Ethereum Virtual Machine) dont la syntaxe s’inspire directement de Python. Contrairement a Solidity, Vyper a ete concu avec une philosophie radicalement minimaliste : tout ce qui complique l’audit ou introduit des vecteurs d’attaque est deliberement exclu.

Philosophie de conception

  • Simplicite : le code doit etre lisible et previsible
  • Securite : reduire la surface d’attaque en limitant les fonctionnalites
  • Auditabilite : un lecteur humain doit pouvoir comprendre le contrat rapidement

Ce que Vyper n’a PAS (volontairement)

Fonctionnalite exclue Raison
Heritage de classes Rend le flux de contrôle difficile a suivre
Surcharge d’opérateurs Masque le comportement reel du code
Boucles infinies (while True) Risque de gas infini, DOS
Assembleur inline Empeche l’analyse statique et l’audit
Appels recursifs Vulnerabilite de reentrancy (cf. hack DAO 2016)
Modificateurs (modifier) Logique cachee, flux non lineaire

Comparaison Vyper vs Solidity

Critere Solidity Vyper
Syntaxe C-like / JavaScript Python-like
Heritage Oui (multiple) Non
Boucles for, while illimites for avec borne fixe uniquement
Surcharge fonctions Oui Non
Assembleur inline Oui (assembly {}) Non
Modificateurs Oui (modifier) Non (decorateurs natifs)
Reentrancy guard Manuel ou via OpenZeppelin Protection integree (@nonreentrant)
Maturite ecosysteme Très large Plus restreint, en croissance
Utilisation notable Uniswap V2/V3, AAVE Curve Finance, Lido

Vyper est utilise en production par des protocoles DeFi majeurs comme Curve Finance, qui gerent des milliards de dollars en valeur verrouillee (TVL).

# Verification de l'environnement
import sys
print(f"Python {sys.version}")

# Verifier si vyper est installe
try:
    import vyper
    print(f"Vyper {vyper.__version__} : installe")
    VYPER_AVAILABLE = True
except ImportError:
    print("Vyper : non installe (mode demonstration)")
    print("  -> pip install vyper  (pour compiler les contrats)")
    VYPER_AVAILABLE = False

# Verifier web3.py
try:
    from web3 import Web3
    print(f"web3.py : installe")
    WEB3_AVAILABLE = True
except ImportError:
    print("web3.py : non installe")
    print("  -> pip install web3")
    WEB3_AVAILABLE = False
Python 3.13.12 | packaged by Anaconda, Inc. | (main, Feb 24 2026, 16:05:56) [MSC v.1942 64 bit (AMD64)]
Vyper 0.4.3 : installe
web3.py : installe

Observation

Le notebook fonctionne en deux modes : - Mode complet : si vyper et web3.py sont installes, les contrats sont compiles et deployes - Mode demonstration : sinon, le code Vyper est affiche et explique comme chaînes de caractères

Les deux modes sont pedagogiquement equivalents pour comprendre la syntaxe Vyper.


2. Syntaxe Vyper

La syntaxe Vyper est proche de Python, avec quelques specificites liees au contexte blockchain. Les éléments principaux sont :

  • Variables d’etat : declarees au niveau du module (pas dans une classe)
  • Types : uint256, int128, address, bool, String, Bytes, HashMap
  • Decorateurs : @external, @internal, @view, @pure, @payable, @nonreentrant
  • Pragmas : #pragma version pour specifier la version du compilateur

Types de base Vyper

Type Description Equivalent Python
uint256 Entier non signe 256 bits int (positif)
int128 Entier signe 128 bits int
bool Booléen bool
address Adresse Ethereum (20 octets) str hex
String[N] Chaîne de taille max N str
Bytes[N] Octets bruts de taille max N bytes
HashMap[K, V] Table de hachage dict
DynArray[T, N] Tableau dynamique borne list
# Contrat Vyper : stockage simple
# Equivalent du "Hello World" des smart contracts

STORAGE_CONTRACT = '''
# @version ^0.4.0

# Variable d'etat : stockee sur la blockchain
stored_value: public(uint256)

# Variable d'etat : proprietaire du contrat
owner: public(address)

@deploy
def __init__(initial_value: uint256):
    """Constructeur : appele une seule fois au deploiement."""
    self.stored_value = initial_value
    self.owner = msg.sender

@external
def set_value(new_value: uint256):
    """Modifier la valeur stockee (seul le proprietaire peut le faire)."""
    assert msg.sender == self.owner, "Seul le proprietaire peut modifier"
    self.stored_value = new_value

@view
@external
def get_value() -> uint256:
    """Lire la valeur stockee (gratuit, pas de transaction)."""
    return self.stored_value
'''

print("CONTRAT VYPER : STOCKAGE SIMPLE")
print("=" * 60)
print(STORAGE_CONTRACT)
print()
print("Points cles :")
print("  - @deploy / __init__ : constructeur (anciennement @external __init__)")
print("  - @external : fonction appelable depuis l'exterieur")
print("  - @view : lecture seule, ne modifie pas l'etat")
print("  - public(uint256) : genere automatiquement un getter")
print("  - assert : verification avec message d'erreur")
print("  - self.variable : acces aux variables d'etat")
print("  - msg.sender : adresse de l'appelant")
CONTRAT VYPER : STOCKAGE SIMPLE
============================================================

# @version ^0.4.0

# Variable d'etat : stockee sur la blockchain
stored_value: public(uint256)

# Variable d'etat : proprietaire du contrat
owner: public(address)

@deploy
def __init__(initial_value: uint256):
    """Constructeur : appele une seule fois au deploiement."""
    self.stored_value = initial_value
    self.owner = msg.sender

@external
def set_value(new_value: uint256):
    """Modifier la valeur stockee (seul le proprietaire peut le faire)."""
    assert msg.sender == self.owner, "Seul le proprietaire peut modifier"
    self.stored_value = new_value

@view
@external
def get_value() -> uint256:
    """Lire la valeur stockee (gratuit, pas de transaction)."""
    return self.stored_value


Points cles :
  - @deploy / __init__ : constructeur (anciennement @external __init__)
  - @external : fonction appelable depuis l'exterieur
  - @view : lecture seule, ne modifie pas l'etat
  - public(uint256) : genere automatiquement un getter
  - assert : verification avec message d'erreur
  - self.variable : acces aux variables d'etat
  - msg.sender : adresse de l'appelant

Interpretation : Contrat de stockage

Ce contrat illustre les éléments fondamentaux de Vyper :

Élément Syntaxe Vyper Equivalent Solidity
Variable d’etat stored_value: public(uint256) uint256 public stored_value
Constructeur @deploy def __init__(...) constructor(...)
Visibilite @external external
Lecture seule @view + @external function ... view external
Verification assert condition, "msg" require(condition, "msg")
Appelant msg.sender msg.sender (identique)

Points cles : - public(uint256) genere automatiquement un getter (comme en Solidity) - @deploy marque le constructeur de facon plus explicite que constructor() - Le pattern owner-only est implemente par assert msg.sender == self.owner sans modifier

Implémentation d’un token ERC-20 simplifié en Vyper pour illustrer la gestion des balances et des transferts.

# Contrat Vyper : token ERC-20 simplifie

TOKEN_CONTRACT = '''
# @version ^0.4.0

# Evenements (logs sur la blockchain)
event Transfer:
    sender: indexed(address)
    receiver: indexed(address)
    amount: uint256

event Approval:
    owner: indexed(address)
    spender: indexed(address)
    amount: uint256

# Variables d'etat
name: public(String[32])
symbol: public(String[8])
decimals: public(uint8)
total_supply: public(uint256)

balances: HashMap[address, uint256]
allowances: HashMap[address, HashMap[address, uint256]]

@deploy
def __init__(name_: String[32], symbol_: String[8], supply: uint256):
    self.name = name_
    self.symbol = symbol_
    self.decimals = 18
    self.total_supply = supply
    self.balances[msg.sender] = supply
    log Transfer(empty(address), msg.sender, supply)

@view
@external
def balanceOf(account: address) -> uint256:
    return self.balances[account]

@external
def transfer(to: address, amount: uint256) -> bool:
    assert self.balances[msg.sender] >= amount, "Solde insuffisant"
    self.balances[msg.sender] -= amount
    self.balances[to] += amount
    log Transfer(msg.sender, to, amount)
    return True

@external
def approve(spender: address, amount: uint256) -> bool:
    self.allowances[msg.sender][spender] = amount
    log Approval(msg.sender, spender, amount)
    return True

@external
def transferFrom(owner: address, to: address, amount: uint256) -> bool:
    assert self.allowances[owner][msg.sender] >= amount, "Allowance insuffisante"
    assert self.balances[owner] >= amount, "Solde insuffisant"
    self.allowances[owner][msg.sender] -= amount
    self.balances[owner] -= amount
    self.balances[to] += amount
    log Transfer(owner, to, amount)
    return True
'''

print("CONTRAT VYPER : TOKEN ERC-20 SIMPLIFIE")
print("=" * 60)
print(TOKEN_CONTRACT)
print()
print("Points cles :")
print("  - event : declaration d'evenements (logs on-chain)")
print("  - indexed(address) : permet le filtrage dans les logs")
print("  - HashMap[K, V] : equivalent de mapping() en Solidity")
print("  - log Event(...) : emettre un evenement (emit en Solidity)")
print("  - empty(address) : adresse zero (0x000...000)")
print("  - uint8 : entier non signe 8 bits (0-255)")
CONTRAT VYPER : TOKEN ERC-20 SIMPLIFIE
============================================================

# @version ^0.4.0

# Evenements (logs sur la blockchain)
event Transfer:
    sender: indexed(address)
    receiver: indexed(address)
    amount: uint256

event Approval:
    owner: indexed(address)
    spender: indexed(address)
    amount: uint256

# Variables d'etat
name: public(String[32])
symbol: public(String[8])
decimals: public(uint8)
total_supply: public(uint256)

balances: HashMap[address, uint256]
allowances: HashMap[address, HashMap[address, uint256]]

@deploy
def __init__(name_: String[32], symbol_: String[8], supply: uint256):
    self.name = name_
    self.symbol = symbol_
    self.decimals = 18
    self.total_supply = supply
    self.balances[msg.sender] = supply
    log Transfer(empty(address), msg.sender, supply)

@view
@external
def balanceOf(account: address) -> uint256:
    return self.balances[account]

@external
def transfer(to: address, amount: uint256) -> bool:
    assert self.balances[msg.sender] >= amount, "Solde insuffisant"
    self.balances[msg.sender] -= amount
    self.balances[to] += amount
    log Transfer(msg.sender, to, amount)
    return True

@external
def approve(spender: address, amount: uint256) -> bool:
    self.allowances[msg.sender][spender] = amount
    log Approval(msg.sender, spender, amount)
    return True

@external
def transferFrom(owner: address, to: address, amount: uint256) -> bool:
    assert self.allowances[owner][msg.sender] >= amount, "Allowance insuffisante"
    assert self.balances[owner] >= amount, "Solde insuffisant"
    self.allowances[owner][msg.sender] -= amount
    self.balances[owner] -= amount
    self.balances[to] += amount
    log Transfer(owner, to, amount)
    return True


Points cles :
  - event : declaration d'evenements (logs on-chain)
  - indexed(address) : permet le filtrage dans les logs
  - HashMap[K, V] : equivalent de mapping() en Solidity
  - log Event(...) : emettre un evenement (emit en Solidity)
  - empty(address) : adresse zero (0x000...000)
  - uint8 : entier non signe 8 bits (0-255)

Interpretation

En comparant les deux contrats ci-dessus avec leurs equivalents Solidity :

Aspect Solidity Vyper
Constructeur constructor() @deploy def __init__()
Visibilite public, private, internal, external Decorateurs @external, @internal
Lecture seule view dans la signature @view decorateur
Événements event Transfer(...) + emit Transfer(...) event Transfer: + log Transfer(...)
Assertion require(condition, "msg") assert condition, "msg"
Mapping mapping(address => uint256) HashMap[address, uint256]

La syntaxe Vyper est systematiquement plus proche de Python, ce qui la rend accessible aux developpeurs Python sans expérience blockchain.


2b. Exercice : Traduire un contrat Solidity en Vyper

Objectif

Convertissez le contrat Solidity suivant en code Vyper equivalent. Le contrat est un système de vote simple ou les electeurs peuvent voter pour une proposition et le résultat est publiquement consultable.

Contrat Solidity a traduire

contract SimpleVote {
    mapping(address => bool) public has_voted;
    uint256 public yes_votes;
    uint256 public no_votes;

    function vote(bool support) external {
        require(!has_voted[msg.sender], "Déjà vote");
        has_voted[msg.sender] = true;
        if (support) {
            yes_votes += 1;
        } else {
            no_votes += 1;
        }
    }

    function get_results() external view returns (uint256, uint256) {
        return (yes_votes, no_votes);
    }
}

Indice :

  • mapping(address => bool) devient HashMap[address, bool]
  • require(condition, "msg") devient assert condition, "msg"
  • Pas de if/else avec blocs dans le même style, mais Vyper les supporte
  • Les variables d’etat sont declarees au niveau du module, pas dans un contract {}
def translate_vote_to_vyper():
    """Traduire le contrat SimpleVote de Solidity en Vyper.

    TODO etudiant : convertissez le contrat Solidity en syntaxe Vyper.
    Remplacez chaque element Solidity par son equivalent Vyper :
    - mapping -> HashMap
    - require -> assert
    - function ... external -> @external
    - function ... view -> @view @external
    - Pas de wrapper contract {}

    Retourner le code Vyper sous forme de chaine.
    """
    # TODO etudiant : ecrivez le code Vyper equivalent
    vyper_code = """
# @version ^0.4.0

# TODO : traduisez les variables d'etat

# TODO : traduisez la fonction vote

# TODO : traduisez la fonction get_results
"""
    return vyper_code


print("Exercice a completer : traduisez le contrat SimpleVote en Vyper")
Exercice a completer : traduisez le contrat SimpleVote en Vyper

3. Compilation et deploiement

Le workflow de deploiement d’un contrat Vyper est similaire a Solidity :

  1. Compilation : le code source Vyper est compile en bytecode EVM + ABI
  2. Deploiement : le bytecode est envoye comme transaction sur la blockchain
  3. Interaction : les fonctions sont appelees via l’ABI (comme tout contrat EVM)

La compilation peut se faire via : - Le compilateur vyper en ligne de commande - La bibliotheque Python vyper (ce que nous utilisons ici) - L’outil ape (Apeworx, equivalent de Foundry pour Vyper)

# Compilation du contrat de stockage

STORAGE_SOURCE = '''
# @version ^0.4.0

stored_value: public(uint256)
owner: public(address)

@deploy
def __init__(initial_value: uint256):
    self.stored_value = initial_value
    self.owner = msg.sender

@external
def set_value(new_value: uint256):
    assert msg.sender == self.owner, "Seul le proprietaire peut modifier"
    self.stored_value = new_value

@view
@external
def get_value() -> uint256:
    return self.stored_value
'''

if VYPER_AVAILABLE:
    from vyper.compiler import compile_code
    # Compiler en bytecode + ABI
    compiled = compile_code(
        STORAGE_SOURCE,
        output_formats=["abi", "bytecode"]
    )
    abi = compiled["abi"]
    bytecode = compiled["bytecode"]
    print("COMPILATION VYPER REUSSIE")
    print("=" * 60)
    print(f"Bytecode : {bytecode[:80]}...")
    print(f"Taille bytecode : {len(bytecode)//2 - 1} octets")
    print(f"ABI : {len(abi)} fonctions/evenements")
    for item in abi:
        kind = item.get("type", "?")
        name = item.get("name", "constructor")
        print(f"  [{kind}] {name}")
else:
    print("COMPILATION VYPER (mode demonstration)")
    print("=" * 60)
    print("Le compilateur vyper n'est pas installe.")
    print("Voici ce que la compilation produirait :")
    print()
    print("1. BYTECODE : code machine EVM (identique a un contrat Solidity compile)")
    print("2. ABI (Application Binary Interface) :")
    print("   - constructor(uint256 initial_value)")
    print("   - set_value(uint256 new_value) [external]")
    print("   - get_value() -> uint256 [view, external]")
    print("   - stored_value() -> uint256 [view, auto-generated]")
    print("   - owner() -> address [view, auto-generated]")
    print()
    print("-> Le bytecode EVM est le meme format que Solidity.")
    print("   Un contrat Vyper deploye est indistinguable d'un contrat Solidity")
    print("   du point de vue de l'EVM.")
    # ABI manuelle pour la suite
    abi = [
        {"type": "constructor", "inputs": [{"name": "initial_value", "type": "uint256"}], "stateMutability": "nonpayable"},
        {"type": "function", "name": "set_value", "inputs": [{"name": "new_value", "type": "uint256"}], "outputs": [], "stateMutability": "nonpayable"},
        {"type": "function", "name": "get_value", "inputs": [], "outputs": [{"name": "", "type": "uint256"}], "stateMutability": "view"},
        {"type": "function", "name": "stored_value", "inputs": [], "outputs": [{"name": "", "type": "uint256"}], "stateMutability": "view"},
        {"type": "function", "name": "owner", "inputs": [], "outputs": [{"name": "", "type": "address"}], "stateMutability": "view"},
    ]
    bytecode = None
COMPILATION VYPER REUSSIE
============================================================
Bytecode : 0x346100255760206101965f395f515f553360015561013761002961000039610137610000f35b5f...
Taille bytecode : 406 octets
ABI : 5 fonctions/evenements
  [function] set_value
  [function] get_value
  [function] stored_value
  [function] owner
  [constructor] constructor

Interpretation : Compilation Vyper

Le compilateur Vyper produit les mêmes artefacts que solc (Solidity) :

Artefact Rôle Format
Bytecode Code machine execute par l’EVM Hexadecimal, identique Solidity
ABI Interface de communication JSON, decrit fonctions et événements

Fonctions generees automatiquement : - stored_value() et owner() sont des getters auto-genere par public() - Le compilateur créé ces accesseurs sans code supplementaire

Point cle : l’EVM ne distingue pas un contrat Vyper d’un contrat Solidity. Le bytecode est le format natif ; le langage source n’est utilise qu’avant compilation.

Déploiement du contrat de stockage Vyper sur une blockchain locale (Anvil) et interaction avec ses fonctions.

# Deploiement sur blockchain locale (anvil)

if WEB3_AVAILABLE and VYPER_AVAILABLE and bytecode:
    from web3 import Web3

    # Connexion a anvil (blockchain locale Foundry)
    w3 = Web3(Web3.HTTPProvider("http://127.0.0.1:8545"))

    if w3.is_connected():
        print("DEPLOIEMENT SUR ANVIL")
        print("=" * 60)

        # Utiliser le premier compte de test
        deployer = w3.eth.accounts[0]
        print(f"Deployer : {deployer}")

        # Creer le contrat
        contract = w3.eth.contract(abi=abi, bytecode=bytecode)

        # Deployer avec valeur initiale = 42
        tx_hash = contract.constructor(42).transact({"from": deployer})
        tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
        contract_address = tx_receipt.contractAddress

        print(f"Contrat deploye : {contract_address}")
        print(f"Gas utilise : {tx_receipt.gasUsed:,}")
        print()

        # Interagir avec le contrat
        deployed = w3.eth.contract(address=contract_address, abi=abi)

        # Lire la valeur (appel view, gratuit)
        value = deployed.functions.get_value().call()
        print(f"Valeur initiale : {value}")

        # Modifier la valeur (transaction, coute du gas)
        tx = deployed.functions.set_value(100).transact({"from": deployer})
        w3.eth.wait_for_transaction_receipt(tx)

        value = deployed.functions.get_value().call()
        print(f"Apres set_value(100) : {value}")

        owner = deployed.functions.owner().call()
        print(f"Owner : {owner}")
        print(f"Owner == deployer : {owner == deployer}")
    else:
        print("Anvil non disponible. Lancez : anvil")
        print("Le deploiement sera simule.")
else:
    print("DEPLOIEMENT (mode demonstration)")
    print("=" * 60)
    print("Sans vyper et/ou anvil, voici le deroulement du deploiement :")
    print()
    print("1. Compilation : vyper source -> bytecode EVM + ABI")
    print("2. Transaction de deploiement :")
    print("   - from: adresse du deployer")
    print("   - data: bytecode + arguments du constructeur encodes")
    print("   - to: None (creation de contrat)")
    print("3. Le contrat recoit une adresse unique")
    print("4. Interaction via ABI :")
    print("   - call() pour les fonctions @view (gratuit)")
    print("   - transact() pour les fonctions qui modifient l'etat (coute du gas)")
    print()
    print("-> Le processus est identique a Solidity car l'EVM ne connait")
    print("   que le bytecode, pas le langage source.")
DEPLOIEMENT SUR ANVIL
============================================================
Deployer : 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Contrat deploye : 0x5FbDB2315678afecb367f032d93F642f64180aa3
Gas utilise : 165,655

Valeur initiale : 42
Apres set_value(100) : 100
Owner : 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Owner == deployer : True

Interpretation

Le deploiement d’un contrat Vyper est identique a celui d’un contrat Solidity : l’EVM ne fait pas de différence entre les deux langages. Seul le bytecode compte.

Étape Solidity Vyper
Compilation solc ou forge build vyper ou ape compile
Output ABI + bytecode ABI + bytecode (même format)
Deploiement web3.py / ethers.js web3.py / ethers.js (identique)
Interaction Via ABI Via ABI (identique)

Un contrat Vyper deploye est indistinguable d’un contrat Solidity pour les utilisateurs et les autres contrats.


3b. Exercice : Contrat de compteur avec plafond

Objectif

Ecrivez en Vyper un contrat CappedCounter qui fonctionne comme un compteur avec un plafond maximum. Le compteur ne peut jamais depasser cette valeur.

Specifications

  • Le constructeur prend un paramètre max_value: uint256
  • La fonction increment() ajoute 1 au compteur, uniquement si le plafond n’est pas atteint
  • La fonction decrement() soustrait 1 au compteur, uniquement si le compteur est > 0
  • La fonction get_count() retourne la valeur actuelle (lecture seule)
  • Seul le proprietaire (owner) peut appeler increment() et decrement()

Indice :

Inspirez-vous du contrat de stockage (section 2) et du compteur compare (section 4). Utilisez assert pour les verifications de plafond et de propriete. Retournez le code Vyper sous forme de chaîne de caractères.

def write_capped_counter():
    """Generer le code Vyper d'un compteur avec plafond.

    TODO etudiant : ecrivez le contrat Vyper avec :
    - Variables d'etat : count, max_value, owner (toutes public)
    - Constructeur @deploy __init__(max_value_: uint256)
    - increment() : +1 si count < max_value et msg.sender == owner
    - decrement() : -1 si count > 0 et msg.sender == owner
    - get_count() -> uint256 : lecture seule

    Retourner le code Vyper sous forme de chaine.
    """
    # TODO etudiant : remplacez par votre code Vyper
    contract_code = """
# @version ^0.4.0

# TODO : declarez les variables d'etat

# TODO : implementez le constructeur

# TODO : implementez increment()

# TODO : implementez decrement()

# TODO : implementez get_count()
"""
    return contract_code


print("Exercice a completer : implementez write_capped_counter")
Exercice a completer : implementez write_capped_counter

4. Comparaison Vyper vs Solidity

Pour mieux comprendre les différences, comparons des implementations equivalentes dans les deux langages. Chaque exemple montre le même contrat ecrit en Solidity puis en Vyper.

# Comparaison 1 : Variable d'etat et getter

solidity_example_1 = '''
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Counter {
    uint256 public count;

    constructor(uint256 _initial) {
        count = _initial;
    }

    function increment() external {
        count += 1;
    }

    function decrement() external {
        require(count > 0, "Underflow");
        count -= 1;
    }
}
'''

vyper_example_1 = '''
# @version ^0.4.0

count: public(uint256)

@deploy
def __init__(initial: uint256):
    self.count = initial

@external
def increment():
    self.count += 1

@external
def decrement():
    assert self.count > 0, "Underflow"
    self.count -= 1
'''

print("COMPARAISON 1 : COMPTEUR")
print("=" * 60)
print("--- Solidity ---")
print(solidity_example_1)
print("--- Vyper ---")
print(vyper_example_1)
print()

# Comptage des lignes de code (hors lignes vides et commentaires)
def count_loc(source):
    return sum(1 for line in source.strip().split("\n")
              if line.strip() and not line.strip().startswith("//") and not line.strip().startswith("#"))

sol_loc = count_loc(solidity_example_1)
vyp_loc = count_loc(vyper_example_1)
print(f"Lignes de code : Solidity={sol_loc}, Vyper={vyp_loc}")
print(f"Reduction : {(1 - vyp_loc/sol_loc)*100:.0f}% moins de code en Vyper")
COMPARAISON 1 : COMPTEUR
============================================================
--- Solidity ---

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Counter {
    uint256 public count;

    constructor(uint256 _initial) {
        count = _initial;
    }

    function increment() external {
        count += 1;
    }

    function decrement() external {
        require(count > 0, "Underflow");
        count -= 1;
    }
}

--- Vyper ---

# @version ^0.4.0

count: public(uint256)

@deploy
def __init__(initial: uint256):
    self.count = initial

@external
def increment():
    self.count += 1

@external
def decrement():
    assert self.count > 0, "Underflow"
    self.count -= 1


Lignes de code : Solidity=14, Vyper=11
Reduction : 21% moins de code en Vyper

Interpretation : Compteur Vyper vs Solidity

Metrique Solidity Vyper
Lignes de code 14 11
Reduction - 21%
Mot-cle constructeur constructor() @deploy def __init__()
Assertion require(condition, "msg") assert condition, "msg"
Wrapper contract Counter { ... } Aucun (module direct)

Points cles : - Vyper elimine le wrapper contract : le module entier est le contrat - @deploy remplace constructor() et rend le rôle de la fonction explicite - assert (Python-like) remplace require (C-like) avec la même sémantique - Le gain de lignes (21%) est representatif de la concision générale de Vyper

Deuxième comparaison illustrant la gestion du contrôle d’accès (Ownable) en Solidity et Vyper.

# Comparaison 2 : Controle d'acces (Ownable)

solidity_example_2 = '''
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Ownable {
    address public owner;

    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
        _;
    }

    constructor() {
        owner = msg.sender;
    }

    function transferOwnership(address newOwner) external onlyOwner {
        require(newOwner != address(0), "Zero address");
        owner = newOwner;
    }

    function doSomething() external onlyOwner {
        // logique protegee
    }
}
'''

vyper_example_2 = '''
# @version ^0.4.0

owner: public(address)

@deploy
def __init__():
    self.owner = msg.sender

@external
def transfer_ownership(new_owner: address):
    assert msg.sender == self.owner, "Not owner"
    assert new_owner != empty(address), "Zero address"
    self.owner = new_owner

@external
def do_something():
    assert msg.sender == self.owner, "Not owner"
    # logique protegee
    pass
'''

print("COMPARAISON 2 : CONTROLE D'ACCES (OWNABLE)")
print("=" * 60)
print("--- Solidity (avec modifier) ---")
print(solidity_example_2)
print("--- Vyper (assertions explicites) ---")
print(vyper_example_2)
print()
print("Analyse :")
print("  Solidity utilise un 'modifier' pour factoriser la verification.")
print("  Le modifier cache le flux de controle (le '_;' est remplace par le corps).")
print()
print("  Vyper exige de repeter l'assertion dans chaque fonction.")
print("  C'est plus verbeux mais le flux est 100% lineaire et auditable.")
print("  -> Chaque fonction se lit de haut en bas, sans indirection.")
COMPARAISON 2 : CONTROLE D'ACCES (OWNABLE)
============================================================
--- Solidity (avec modifier) ---

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Ownable {
    address public owner;

    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
        _;
    }

    constructor() {
        owner = msg.sender;
    }

    function transferOwnership(address newOwner) external onlyOwner {
        require(newOwner != address(0), "Zero address");
        owner = newOwner;
    }

    function doSomething() external onlyOwner {
        // logique protegee
    }
}

--- Vyper (assertions explicites) ---

# @version ^0.4.0

owner: public(address)

@deploy
def __init__():
    self.owner = msg.sender

@external
def transfer_ownership(new_owner: address):
    assert msg.sender == self.owner, "Not owner"
    assert new_owner != empty(address), "Zero address"
    self.owner = new_owner

@external
def do_something():
    assert msg.sender == self.owner, "Not owner"
    # logique protegee
    pass


Analyse :
  Solidity utilise un 'modifier' pour factoriser la verification.
  Le modifier cache le flux de controle (le '_;' est remplace par le corps).

  Vyper exige de repeter l'assertion dans chaque fonction.
  C'est plus verbeux mais le flux est 100% lineaire et auditable.
  -> Chaque fonction se lit de haut en bas, sans indirection.

Interpretation : Contrôle d’acces

Aspect Solidity (modifier) Vyper (assert)
Declaration modifier onlyOwner() { ...; _; } Aucune, assertions inline
Application external onlyOwner (mot-cle) assert msg.sender == self.owner (chaque fonction)
Flux de contrôle Non-lineaire (_; = insertion du corps) Lineaire, haut en bas
Zero address address(0) empty(address)

Points cles : - Le modifier Solidite est un sucre syntaxique qui rend le code plus DRY mais moins lisible - L’assertion Vyper est repetitive mais chaque fonction est autonome et comprehensible sans indirection - La version Vyper facilite l’audit formel car le flux est explicite

Troisième comparaison portant sur la gestion des boucles, un point de divergence important entre Solidity et Vyper.

# Comparaison 3 : Boucles

solidity_example_3 = '''
// Solidity : boucle sans limite fixe (dangereux)
function sumAll(uint256[] memory values) external pure returns (uint256) {
    uint256 total = 0;
    for (uint256 i = 0; i < values.length; i++) {
        total += values[i];
    }
    return total;
}
// -> Si values.length = 10 millions, la transaction echoue (out of gas)
// -> Le compilateur ne previent pas de ce risque
'''

vyper_example_3 = '''
# Vyper : boucle avec borne obligatoire
MAX_VALUES: constant(uint256) = 100

@view
@external
def sum_all(values: DynArray[uint256, MAX_VALUES]) -> uint256:
    total: uint256 = 0
    for v: uint256 in values:
        total += v
    return total
# -> DynArray borne a 100 elements : impossible de depasser
# -> Le compilateur garantit que la boucle termine
'''

print("COMPARAISON 3 : BOUCLES")
print("=" * 60)
print("--- Solidity ---")
print(solidity_example_3)
print("--- Vyper ---")
print(vyper_example_3)
print()
print("Analyse :")
print("  En Solidity, une boucle peut iterer sur un tableau de taille arbitraire.")
print("  Si le tableau est trop grand, la transaction echoue par manque de gas.")
print("  C'est un vecteur classique d'attaque (DOS par gas limit).")
print()
print("  Vyper force une borne maximale (DynArray[T, N] ou range(N)).")
print("  Le compilateur peut prouver que la boucle termine.")
print("  -> Pas de boucle infinie possible, par construction.")
COMPARAISON 3 : BOUCLES
============================================================
--- Solidity ---

// Solidity : boucle sans limite fixe (dangereux)
function sumAll(uint256[] memory values) external pure returns (uint256) {
    uint256 total = 0;
    for (uint256 i = 0; i < values.length; i++) {
        total += values[i];
    }
    return total;
}
// -> Si values.length = 10 millions, la transaction echoue (out of gas)
// -> Le compilateur ne previent pas de ce risque

--- Vyper ---

# Vyper : boucle avec borne obligatoire
MAX_VALUES: constant(uint256) = 100

@view
@external
def sum_all(values: DynArray[uint256, MAX_VALUES]) -> uint256:
    total: uint256 = 0
    for v: uint256 in values:
        total += v
    return total
# -> DynArray borne a 100 elements : impossible de depasser
# -> Le compilateur garantit que la boucle termine


Analyse :
  En Solidity, une boucle peut iterer sur un tableau de taille arbitraire.
  Si le tableau est trop grand, la transaction echoue par manque de gas.
  C'est un vecteur classique d'attaque (DOS par gas limit).

  Vyper force une borne maximale (DynArray[T, N] ou range(N)).
  Le compilateur peut prouver que la boucle termine.
  -> Pas de boucle infinie possible, par construction.

Interpretation

Les trois comparaisons illustrent la philosophie de Vyper :

Aspect Approche Solidity Approche Vyper Compromis
Factorisation modifier (indirection) assert repete (explicite) Lisibilite vs DRY
Boucles Taille arbitraire Borne obligatoire Flexibilite vs securite
Heritage Multiple possible Interdit Reutilisation vs simplicite

Points cles : 1. Vyper sacrifie volontairement la flexibilite pour la securite 2. Chaque restriction de Vyper repond a une vulnerabilite connue en Solidity 3. Les deux langages compilent vers le même bytecode EVM 4. Le choix depend du contexte : Vyper pour la DeFi critique, Solidity pour l’ecosysteme large


5. Exercice : Contrat d’enchere en Vyper

Implementez un contrat d’enchere (auction) en Vyper. Le contrat doit :

  • Avoir un proprietaire (beneficiaire de l’enchere)
  • Accepter des mises (bid) superieures a la mise actuelle
  • Rembourser automatiquement le précédent meilleur encherisseur
  • Permettre au beneficiaire de retirer les fonds a la fin

Contraintes Vyper : - Pas de boucle infinie : utiliser un nombre maximum d’encherisseurs - Protection reentrancy : @nonreentrant - Assertions explicites pour chaque verification

# Exercice : Contrat d'enchere en Vyper
# Completez le code Vyper ci-dessous

def write_auction_contract():
    """Generer le code Vyper d'un contrat d'enchere.

    Le contrat doit inclure :
    - Variables : beneficiary, highest_bidder, highest_bid, ended
    - HashMap pour les remboursements en attente (pending_returns)
    - Fonction bid() : @payable, @nonreentrant
    - Fonction withdraw() : retirer les fonds non gagnes
    - Fonction end_auction() : seul le beneficiaire peut terminer

    Retourner le code Vyper sous forme de chaine.
    """
    # TODO: Implementez le contrat
    pass  # TODO: Completez cet exercice
print("Exercice a completer")
Exercice a completer

6. Resume

Aspect Detail
Langage Vyper : syntaxe Python-like pour l’EVM
Philosophie Simplicite, securite, auditabilite
Exclusions volontaires Heritage, surcharge, boucles infinies, assembleur inline, recursion
Types principaux uint256, address, bool, HashMap, DynArray, String
Decorateurs @external, @internal, @view, @pure, @payable, @nonreentrant
Compilation vyper source.vy produit bytecode EVM + ABI (même format que Solidity)
Ecosysteme Curve Finance, Lido, Yearn Finance
Outils compilateur vyper, Apeworx (ape), Titanoboa, web3.py

Points cles

  • Vyper est un choix pertinent pour les contrats critiques en securite (DeFi, tresoreries)
  • La syntaxe Python rend le langage accessible aux developpeurs non-blockchain
  • Les restrictions de Vyper ne sont pas des limitations mais des garanties de securite
  • Un contrat Vyper deploye est indistinguable d’un contrat Solidity pour l’EVM
  • L’ecosysteme Vyper est plus restreint mais soutenu par des protocoles majeurs

Notebook suivant : SC-19-Ripple-XRP-Python - Contrats sur le reseau Ripple

Resume et perspectives

Ce notebook a explore Vyper, un langage de smart contracts dont la syntaxe Python-like et la philosophie minimaliste le distinguent de Solidity. Nous avons ecrit et compile des contrats (stockage, token ERC-20), deploi sur une blockchain locale via web3.py, et compare systematiquement les deux langages sur des exemples equivalents (compteur, contrôle d’acces, boucles). Les restrictions volontaires de Vyper – pas d’heritage, pas de modifier, pas de boucles infinies, pas d’assembleur inline – ne sont pas des limitations mais des garanties de securite qui facilitent l’audit formel.

L’absence de modifier (remplace par des assertions explicites) et l’obligation de borner les boucles (DynArray[T, N]) illustrent un compromis fondamental : sacrifier la concision du code pour la previsibilite de l’exécution. Cette approche est particulierement adaptee aux protocoles DeFi critiques comme Curve Finance ou Lido, ou la lisibilite du code et la certitude de terminaison primordent sur l’expressivite. Le bytecode compile identique a celui de Solidity rappelle que l’EVM est agnostique au langage source.

Le prochain notebook quitte l’ecosysteme Ethereum pour decouvrir le protocole Ripple/XRP, une architecture blockchain concue pour les paiements transfrontaliers avec un consensus original (UNL) et des primitives financieres natives : SC-19-Ripple-XRP-Python.


<< E2E Verifiable Voting | Ripple XRP >>

Retour au sommet