SC-04-Functions-State-Python - Fonctions et État

# Parameters
BATCH_MODE = "true"

<< Solidity Basics | Inheritance >>


Objectifs d’apprentissage

  1. Comprendre les data locations (storage, memory, calldata)
  2. Maîtriser la visibilité des fonctions
  3. Utiliser les modifiers (view, pure, payable)

Prérequis

Durée estimée : 45 minutes


0. Connexion a la blockchain locale

Tous les contrats de ce notebook sont compiles et deployes réellement sur anvil. Lancez anvil dans un terminal avant d’executer les cellules.

# Connection a anvil (blockchain locale Foundry)
# Prerequis: anvil en cours d'execution dans un terminal
try:
    from web3 import Web3
    import solcx
except ImportError as e:
    print(f"Installation requise : pip install web3 py-solc-x")
    print(f"Erreur : {e}")

SOLC_VERSION = "0.8.28"
ANVIL_URL = "http://127.0.0.1:8545"

# Connexion
w3 = Web3(Web3.HTTPProvider(ANVIL_URL))
assert w3.is_connected(), f"Impossible de se connecter a {ANVIL_URL}. Lancez 'anvil' dans un terminal."

# Installer solc si necessaire
installed = [str(v) for v in solcx.get_installed_solc_versions()]
if SOLC_VERSION not in installed:
    solcx.install_solc(SOLC_VERSION)
solcx.set_solc_version(SOLC_VERSION)

deployer = w3.eth.accounts[0]
print(f"Connecte a anvil (chain {w3.eth.chain_id}), deployer: {deployer[:10]}...")


def compile_and_deploy(w3, source_code, deployer, *constructor_args):
    """Compiler et deployer un contrat Solidity."""
    compiled = solcx.compile_source(
        source_code, output_values=["abi", "bin"], solc_version=SOLC_VERSION
    )
    contract_id, contract_interface = compiled.popitem()
    Contract = w3.eth.contract(
        abi=contract_interface["abi"], bytecode=contract_interface["bin"]
    )
    tx_hash = Contract.constructor(*constructor_args).transact({"from": deployer})
    receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
    instance = w3.eth.contract(
        address=receipt.contractAddress, abi=contract_interface["abi"]
    )
    print(f"Deploye: {contract_id.split(':')[-1]} a {receipt.contractAddress}")
    return instance, receipt
Connecte a anvil (chain 31337), deployer: 0xf39Fd6e5...

1. Data Locations

Solidite définit trois emplacements de données :

Location Description Coût gas
storage Blockchain permanente Élevé
memory Temporaire (RAM) Faible
calldata Données d’entrée (read-only) Minimal
# Data locations en Solidity
DATA_LOCATION_EXAMPLE = '''
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

contract DataLocations {
    // STORAGE : stocke sur la blockchain (permanent)
    uint256[] public numbers;  // storage par defaut pour variables d'etat
    mapping(address => uint256) public balances;

    // MEMORY : temporaire, efface apres l'appel
    function processArray(uint256[] memory arr) public pure returns (uint256) {
        uint256 sum = 0;
        for (uint i = 0; i < arr.length; i++) {
            sum += arr[i];
        }
        return sum;
    }

    // CALLDATA : read-only, le moins couteux
    function getElement(uint256[] calldata arr, uint256 index) 
        external pure returns (uint256) 
    {
        return arr[index];
    }

    // Storage reference pour modification
    function updateNumber(uint256 index, uint256 value) public {
        numbers[index] = value;  // Modification directe du storage
    }
}
'''


# Compilation et deploiement reel sur anvil
datalocations, receipt = compile_and_deploy(w3, DATA_LOCATION_EXAMPLE, deployer)
Deploye: DataLocations a 0xf4B146FbA71F41E0592668ffbF264F1D186b2Ca8

1.1 Règles de data location

  • Variables d’état : toujours en storage
  • Arguments de fonction : memory ou calldata pour les types complexes
  • Variables locales : storage (reference) ou memory (valeur copiee)
# Storage vs Memory : demonstration de la difference critique
STORAGE_MEMORY_EXAMPLE = '''
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

contract StorageVsMemory {
    struct User {
        string name;
        uint256 balance;
    }

    User[] public users;

    function addUser(string memory name, uint256 balance) public {
        users.push(User(name, balance));
    }

    // STORAGE reference : modifie directement le tableau
    function updateBalanceGood(uint256 index, uint256 amount) public {
        User storage user = users[index];
        user.balance = amount;  // Modification permanente
    }

    // MEMORY copy : ne modifie PAS le storage (bug courant!)
    function updateBalanceBad(uint256 index, uint256 amount) public {
        User memory user = users[index];
        user.balance = amount;  // Modification locale uniquement!
    }

    function getBalance(uint256 index) public view returns (uint256) {
        return users[index].balance;
    }
}
'''

print("--- Storage vs Memory : difference critique ---")
sm_contract, receipt = compile_and_deploy(w3, STORAGE_MEMORY_EXAMPLE, deployer)
print(f"  Deploye : {sm_contract.address}")

# Ajouter un utilisateur
sm_contract.functions.addUser("Alice", 1000).transact({'from': deployer})
print(f"  Alice balance initiale : {sm_contract.functions.getBalance(0).call()}")

# Mettre a jour via storage (correct)
sm_contract.functions.updateBalanceGood(0, 2000).transact({'from': deployer})
print(f"  Apres updateBalanceGood(2000) : {sm_contract.functions.getBalance(0).call()} (attendu: 2000)")

# Mettre a jour via memory (bug - ne change rien)
sm_contract.functions.updateBalanceBad(0, 9999).transact({'from': deployer})
print(f"  Apres updateBalanceBad(9999) : {sm_contract.functions.getBalance(0).call()} (attendu: 2000, pas 9999!)")
--- Storage vs Memory : difference critique ---
Deploye: StorageVsMemory a 0x172076E0166D1F9Cc711C77Adf8488051744980C
  Deploye : 0x172076E0166D1F9Cc711C77Adf8488051744980C
  Alice balance initiale : 1000
  Apres updateBalanceGood(2000) : 2000 (attendu: 2000)
  Apres updateBalanceBad(9999) : 2000 (attendu: 2000, pas 9999!)

Interpretation : Storage vs Memory - Différence critique

Résultat obtenu : Une reference storage modifie l’état, une copie memory ne le fait pas.

Fonction Type de variable Comportement Résultat
updateBalanceGood() User storage Reference directe Modifie le storage (2000)
updateBalanceBad() User memory Copie locale Ne modifie PAS le storage (reste 2000)

Points clés : - User storage user = users[index] créé une REFERENCE vers le storage : les modifications sont permanentes - User memory user = users[index] créé une COPIE locale : les modifications sont perdues a la fin de la fonction - C’est un bug classique en Solidity : penser modifier une variable alors qu’on ne modifie qu’une copie - Règle pratique : toujours utiliser storage pour modifier des structures complexes, memory pour la lecture seule - Le coût gas est plus élevé pour storage mais nécessaire pour les modifications


2. Visibilite des fonctions

Visibilite Exterieur Contrat enfant Même contrat
public Oui Oui Oui
external Oui Non Non
internal Non Oui Oui
private Non Non Oui
# Visibilite des fonctions - demonstration
VISIBILITY_EXAMPLE = '''
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

contract VisibilityExample {
    uint256 private privateVar = 1;
    uint256 internal internalVar = 2;
    uint256 public publicVar = 3;

    function publicFunction() public pure returns (string memory) {
        return "Je suis publique!";
    }

    function externalFunction() external pure returns (string memory) {
        return "Je suis externe!";
    }

    function internalFunction() internal pure returns (string memory) {
        return "Je suis interne!";
    }

    function privateFunction() private pure returns (string memory) {
        return "Je suis prive!";
    }

    function callInternal() public pure returns (string memory) {
        return internalFunction();
    }

    function getPrivateVar() public view returns (uint256) {
        return privateVar;
    }

    function getAllVars() public view returns (uint256, uint256, uint256) {
        return (privateVar, internalVar, publicVar);
    }
}
'''

print("--- Visibilite : public / external / internal / private ---")
vis_contract, receipt = compile_and_deploy(w3, VISIBILITY_EXAMPLE, deployer)
print(f"  Deploye : {vis_contract.address}")
print(f"  publicFunction() = '{vis_contract.functions.publicFunction().call()}'")
print(f"  callInternal() = '{vis_contract.functions.callInternal().call()}'")
print(f"  publicVar = {vis_contract.functions.publicVar().call()}")
priv, intern, pub = vis_contract.functions.getAllVars().call()
print(f"  getAllVars() : private={priv}, internal={intern}, public={pub}")
--- Visibilite : public / external / internal / private ---
Deploye: VisibilityExample a 0x2B0d36FACD61B71CC05ab8F3D2355ec3631C0dd5
  Deploye : 0x2B0d36FACD61B71CC05ab8F3D2355ec3631C0dd5
  publicFunction() = 'Je suis publique!'
  callInternal() = 'Je suis interne!'
  publicVar = 3
  getAllVars() : private=1, internal=2, public=3

Interpretation : Visibilite des fonctions

Résultat obtenu : Les quatre niveaux de visibilité controlent l’accès aux fonctions.

Visibilite Externe Enfants Interne Usage
public Oui Oui Oui Fonctions générales
external Oui Non Non Optimisation gas
internal Non Oui Oui Helpers internes
private Non Non Oui Implementation privee

Points clés : - publicFunction() et publicVar sont accessibles de partout - externalFunction() est optimise pour les appels externes (pas d’appel interne possible) - callInternal() appelle internalFunction() (autorise car même contrat) - privateVar est accessible via un getter public getPrivateVar() mais pas directement - La visibilité limite l’accessite mais pas la sécurité (tout est visible sur la blockchain)

Comparaison entre les modificateurs external et public pour illustrer l’impact sur la consommation de gas.

# External vs Public : optimisation gas
EXTERNAL_OPTIMIZATION = '''
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

contract GasOptimization {
    // EXTERNAL est moins couteux pour les tableaux
    // car les donnees restent en calldata (pas de copie)

    // Moins efficace (copie en memory)
    function sumPublic(uint256[] memory arr) public pure returns (uint256) {
        uint256 total = 0;
        for (uint i = 0; i < arr.length; i++) {
            total += arr[i];
        }
        return total;
    }

    // Plus efficace (reste en calldata)
    function sumExternal(uint256[] calldata arr) external pure returns (uint256) {
        uint256 total = 0;
        for (uint i = 0; i < arr.length; i++) {
            total += arr[i];
        }
        return total;
    }
}
'''

print("--- External vs Public : optimisation gas ---")
gas_contract, receipt = compile_and_deploy(w3, EXTERNAL_OPTIMIZATION, deployer)
print(f"  Deploye : {gas_contract.address}")

# Tableau de test : [1, 2, 3, 4, 5]
arr = [1, 2, 3, 4, 5]
result_public = gas_contract.functions.sumPublic(arr).call()
result_external = gas_contract.functions.sumExternal(arr).call()
print(f"  sumPublic([1..5])   = {result_public}  (memory copy)")
print(f"  sumExternal([1..5]) = {result_external}  (calldata, plus efficace)")
print("  Resultat identique, mais external consomme moins de gas pour les grands tableaux")
--- External vs Public : optimisation gas ---
Deploye: GasOptimization a 0xfbC22278A96299D91d41C453234d97b4F5Eb9B2d
  Deploye : 0xfbC22278A96299D91d41C453234d97b4F5Eb9B2d
  sumPublic([1..5])   = 15  (memory copy)
  sumExternal([1..5]) = 15  (calldata, plus efficace)
  Resultat identique, mais external consomme moins de gas pour les grands tableaux

Interpretation : External vs Public - Optimisation gas

Résultat obtenu : Les fonctions external sont plus efficaces que public pour les tableaux.

Fonction Type de paramètre Copie mémoire ? Coût gas
sumPublic() memory Oui Plus élevé
sumExternal() calldata Non Moins élevé

Points clés : - public copie les tableaux de calldata vers memory (allocation et copie couteuse) - external garde les données en calldata (read-only, pas de copie) - La différence de gas devient significative pour les grands tableaux - external ne peut pas être appele depuis l’interieur du contrat (pas d’appel interne) - Règle pratique : utiliser external pour les fonctions appeles de l’exterieur avec des paramètres complexes (tableaux, strings)


3. Modificateurs de fonction

3.1 view et pure

# view et pure
VIEW_PURE_EXAMPLE = '''
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

contract ViewPureExample {
    uint256 public value = 42;

    // VIEW : lit le storage mais ne modifie pas
    function getValue() public view returns (uint256) {
        return value;
    }

    // PURE : ni lecture ni ecriture du storage
    function add(uint256 a, uint256 b) public pure returns (uint256) {
        return a + b;
    }

    // Fonction normale : peut lire et ecrire
    function increment() public returns (uint256) {
        value += 1;
        return value;
    }
}
'''

print("--- view = lecture seule, pure = sans acces storage ---")
vp_contract, receipt = compile_and_deploy(w3, VIEW_PURE_EXAMPLE, deployer)
print(f"  Deploye : {vp_contract.address}")
print(f"  getValue() = {vp_contract.functions.getValue().call()}  (view : lit value=42)")
print(f"  add(10, 32) = {vp_contract.functions.add(10, 32).call()}  (pure : calcul sans storage)")
vp_contract.functions.increment().transact({'from': deployer})
print(f"  Apres increment() : getValue() = {vp_contract.functions.getValue().call()}  (attendu: 43)")
--- view = lecture seule, pure = sans acces storage ---
Deploye: ViewPureExample a 0x46b142DD1E924FAb83eCc3c08e4D46E82f005e0E
  Deploye : 0x46b142DD1E924FAb83eCc3c08e4D46E82f005e0E
  getValue() = 42  (view : lit value=42)
  add(10, 32) = 42  (pure : calcul sans storage)
  Apres increment() : getValue() = 43  (attendu: 43)

Interpretation : View et Pure - Optimisation gas

Résultat obtenu : Les modificateurs view et pure indiquent si une fonction accede au storage.

Modificateur Lit le storage ? Modifie le storage ? Coût gas
(aucun) Possible Possible Élevé
view Oui Non Faible
pure Non Non Minimal

Points clés : - getValue() est view : lit value mais ne la modifie pas - add(10, 32) est pure : ni lecture ni ecriture du storage (calcul local) - increment() est normale : modifie value (ecriture dans le storage) - Les fonctions view et pure peuvent être executees hors-chaîne (sans gas) par un client ETH - Le compilateur vérifie que les modificateurs sont corrects (erreur si une fonction pure lit une variable d’état)

3.2 payable

# payable pour recevoir des ETH
PAYABLE_EXAMPLE = '''
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

contract PayableExample {
    address public owner;
    mapping(address => uint256) public balances;

    constructor() {
        owner = msg.sender;
    }

    // Fonction payable : peut recevoir des ETH
    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    // Retirer des ETH
    function withdraw(uint256 amount) public {
        require(balances[msg.sender] >= amount, "Fonds insuffisants");
        balances[msg.sender] -= amount;
        (bool success, ) = payable(msg.sender).call{value: amount}("");
        require(success, "Echec du transfert");
    }

    // Consulter le solde du contrat
    function getContractBalance() public view returns (uint256) {
        return address(this).balance;
    }

    // Receive : appele lors d'un transfert ETH simple
    receive() external payable {
        balances[msg.sender] += msg.value;
    }
}
'''

print("--- payable : recevoir et retirer des ETH ---")
pay_contract, receipt = compile_and_deploy(w3, PAYABLE_EXAMPLE, deployer)
print(f"  Deploye : {pay_contract.address}")
print(f"  Balance initiale du contrat : {pay_contract.functions.getContractBalance().call()} wei")

# Deposer 1 ETH (1e18 wei)
one_eth = w3.to_wei(1, 'ether')
pay_contract.functions.deposit().transact({'from': deployer, 'value': one_eth})
print(f"  Apres deposit(1 ETH) : contrat = {w3.from_wei(pay_contract.functions.getContractBalance().call(), 'ether')} ETH")
print(f"  Solde deployer dans contrat : {w3.from_wei(pay_contract.functions.balances(deployer).call(), 'ether')} ETH")

# Retirer 0.5 ETH
half_eth = w3.to_wei(0.5, 'ether')
pay_contract.functions.withdraw(half_eth).transact({'from': deployer})
print(f"  Apres withdraw(0.5 ETH) : contrat = {w3.from_wei(pay_contract.functions.getContractBalance().call(), 'ether')} ETH")
--- payable : recevoir et retirer des ETH ---
Deploye: PayableExample a 0x1c85638e118b37167e9298c2268758e058DdfDA0
  Deploye : 0x1c85638e118b37167e9298c2268758e058DdfDA0
  Balance initiale du contrat : 0 wei
  Apres deposit(1 ETH) : contrat = 1 ETH
  Solde deployer dans contrat : 1 ETH
  Apres withdraw(0.5 ETH) : contrat = 0.5 ETH

Interpretation : Payable - Gestion des ETH

Résultat obtenu : Le mot-clé payable permet aux fonctions de recevoir des ETH.

Élément Valeur Signification
Balance initiale 0 ETH Contrat vide au déploiement
Après deposit(1 ETH) 1 ETH Le contrat a recu 1 ETH
Après withdraw(0.5 ETH) 0.5 ETH Retrait de 0.5 ETH vers le deployer

Points clés : - deposit() est payable : peut recevoir de l’ETH via msg.value - balances[msg.sender] suit les depots de chaque utilisateur - withdraw() vérifie les fonds (require), debite le solde, et transfere l’ETH - Le pattern payable(addr).call{value: amount}("") est la méthode moderne de transfert (preferable a transfer() ou send()) - receive() est une fonction speciale appelee automatiquement lors d’un transfert ETH simple (sans data)


4. Custom Modifiers

Les modifiers permettent de reutiliser des conditions prealables.

# Custom modifiers
MODIFIER_EXAMPLE = '''
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

contract ModifierExample {
    address public owner;
    bool public paused = false;
    uint256 public counter = 0;

    constructor() {
        owner = msg.sender;
    }

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

    modifier whenNotPaused() {
        require(!paused, "Contrat en pause");
        _;
    }

    function pause() public onlyOwner {
        paused = true;
    }

    function unpause() public onlyOwner {
        paused = false;
    }

    function increment() public whenNotPaused {
        counter += 1;
    }

    function criticalOperation() public onlyOwner whenNotPaused returns (uint256) {
        counter += 10;
        return counter;
    }
}
'''

print("--- Custom modifiers : onlyOwner + whenNotPaused ---")
mod_contract, receipt = compile_and_deploy(w3, MODIFIER_EXAMPLE, deployer)
print(f"  Deploye : {mod_contract.address}")
print(f"  paused = {mod_contract.functions.paused().call()}, counter = {mod_contract.functions.counter().call()}")

# Incrementer (fonctionne car pas en pause)
mod_contract.functions.increment().transact({'from': deployer})
print(f"  Apres increment() : counter = {mod_contract.functions.counter().call()}")

# Mettre en pause
mod_contract.functions.pause().transact({'from': deployer})
print(f"  Apres pause() : paused = {mod_contract.functions.paused().call()}")

# increment() doit revert
try:
    mod_contract.functions.increment().transact({'from': deployer})
    print("  ERREUR : increment() aurait du revert!")
except Exception as e:
    print(f"  increment() reverte (attendu) : Contrat en pause")

# Reprendre et faire criticalOperation
mod_contract.functions.unpause().transact({'from': deployer})
result = mod_contract.functions.criticalOperation().transact({'from': deployer})
print(f"  Apres unpause() + criticalOperation() : counter = {mod_contract.functions.counter().call()}")
--- Custom modifiers : onlyOwner + whenNotPaused ---
Deploye: ModifierExample a 0x7A9Ec1d04904907De0ED7b6839CcdD59c3716AC9
  Deploye : 0x7A9Ec1d04904907De0ED7b6839CcdD59c3716AC9
  paused = False, counter = 0
  Apres increment() : counter = 1
  Apres pause() : paused = True
  increment() reverte (attendu) : Contrat en pause
  Apres unpause() + criticalOperation() : counter = 11

Interpretation : Custom Modifiers - Reutilisation de logique

Résultat obtenu : Les modifiers encapsulent des conditions prealables reutilisables.

Modifier Condition Effet
onlyOwner msg.sender == owner Restreint au propriétaire
whenNotPaused !paused Bloque si le contrat est en pause
onlyOwner whenNotPaused Les deux conditions Combine les restrictions

Points clés : - Le symbol _; est l’endroit ou le code de la fonction est exécuté (après les checks du modifier) - pause() et unpause() ne peuvent être appeles que par le owner (grace a onlyOwner) - increment() echoue quand le contrat est en pause (revert avec “Contrat en pause”) - criticalOperation() combine deux modifiers : vérification owner + état pause - Les modifiers reduisent la duplication de code et augmentent la lisibilite


5. Fonctions speciales

# Constructor, receive, fallback
SPECIAL_FUNCTIONS = '''
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

contract SpecialFunctions {
    address public owner;
    string public name;
    uint256 public receivedTotal = 0;

    event Received(address from, uint256 amount);

    // CONSTRUCTOR : execute une seule fois au deploiement
    constructor(string memory _name) {
        owner = msg.sender;
        name = _name;
    }

    // RECEIVE : appele lors d'un transfert ETH simple (sans data)
    receive() external payable {
        receivedTotal += msg.value;
        emit Received(msg.sender, msg.value);
    }

    function getBalance() public view returns (uint256) {
        return address(this).balance;
    }
}
'''

print("--- Fonctions speciales : constructor, receive ---")
# Constructor prend un argument string "_name"
special, receipt = compile_and_deploy(w3, SPECIAL_FUNCTIONS, deployer, "MonContrat")
print(f"  Deploye : {special.address}")
print(f"  name = '{special.functions.name().call()}'  (initialise par constructor)")
print(f"  owner = {special.functions.owner().call()[:10]}...")
print(f"  receivedTotal initial = {special.functions.receivedTotal().call()} wei")

# Envoyer des ETH via receive() (transfert simple sans data)
w3.eth.send_transaction({'from': deployer, 'to': special.address, 'value': w3.to_wei(0.1, 'ether')})
print(f"  Apres transfert 0.1 ETH : receivedTotal = {w3.from_wei(special.functions.receivedTotal().call(), 'ether')} ETH")
print(f"  Balance contrat : {w3.from_wei(special.functions.getBalance().call(), 'ether')} ETH")
--- Fonctions speciales : constructor, receive ---
Deploye: SpecialFunctions a 0xf953b3A269d80e3eB0F2947630Da976B896A8C5b
  Deploye : 0xf953b3A269d80e3eB0F2947630Da976B896A8C5b
  name = 'MonContrat'  (initialise par constructor)
  owner = 0xf39Fd6e5...
  receivedTotal initial = 0 wei
  Apres transfert 0.1 ETH : receivedTotal = 0.1 ETH
  Balance contrat : 0.1 ETH

Interpretation : Fonctions speciales - Constructor et Receive

Résultat obtenu : Le constructor initialise le contrat UNE SEULE FOIS, receive est appele automatiquement lors d’un transfert ETH.

Fonction Quand est-elle appelee ? Usage
constructor Une seule fois au déploiement Initialisation (owner, name)
receive() Transfert ETH simple (sans data) Reception automatique d’ETH
fallback() Appel de fonction inexistante Gestion d’erreur

Points clés : - Le constructor peut prendre des arguments (_name dans l’exemple) - receive() est external payable : ne peut pas être appele directement, seulement via transfert ETH - L’event Received est logge lors de la reception d’ETH - receivedTotal compte le total des ETH recus via receive()


6. Exemples guidés

Exemple guide 1 : Contrat Bank

Créez un contrat qui permet de deposer et retirer des ETH avec un modifier onlyOwner.

# Exercice 1 : Contrat Bank
EXERCICE_BANK = '''
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

contract Bank {
    address public owner;
    mapping(address => uint256) public balances;

    constructor() {
        owner = msg.sender;
    }

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

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw(uint256 amount) public {
        require(balances[msg.sender] >= amount, "Insufficient funds");
        balances[msg.sender] -= amount;
        (bool success, ) = payable(msg.sender).call{value: amount}("");
        require(success, "Transfer failed");
    }

    function emergencyWithdraw() public onlyOwner {
        (bool success, ) = payable(owner).call{value: address(this).balance}("");
        require(success, "Transfer failed");
    }
}
'''

# Compilation et deploiement reel sur anvil
bank, receipt = compile_and_deploy(w3, EXERCICE_BANK, deployer)
client = w3.eth.accounts[1]

w3.eth.wait_for_transaction_receipt(
    bank.functions.deposit().transact({
        'from': client,
        'value': w3.to_wei(2, 'ether'),
    })
)
solde_client = bank.functions.balances(client).call()
print("Solde apres depot :", w3.from_wei(solde_client, 'ether'), "ETH")
assert solde_client == w3.to_wei(2, 'ether')

w3.eth.wait_for_transaction_receipt(
    bank.functions.withdraw(w3.to_wei(0.75, 'ether')).transact({'from': client})
)
solde_client = bank.functions.balances(client).call()
print("Solde apres retrait :", w3.from_wei(solde_client, 'ether'), "ETH")
assert solde_client == w3.to_wei(1.25, 'ether')

print("ETH encore dans la banque :", w3.from_wei(w3.eth.get_balance(bank.address), 'ether'), "ETH")
w3.eth.wait_for_transaction_receipt(
    bank.functions.emergencyWithdraw().transact({'from': deployer})
)
print("Apres emergencyWithdraw :", w3.from_wei(w3.eth.get_balance(bank.address), 'ether'), "ETH")
assert w3.eth.get_balance(bank.address) == 0
Deploye: Bank a 0x5c74c94173F05dA1720953407cbb920F3DF9f887
Solde apres depot : 2 ETH
Solde apres retrait : 1.25 ETH
ETH encore dans la banque : 1.25 ETH
Apres emergencyWithdraw : 0 ETH

Indice : Pour onlyOwner, utilisez require(msg.sender == owner, "Not owner"); _;. Pour deposit, faites balances[msg.sender] += msg.value. Pour withdraw, vérifiez les fonds avec require, debitez le solde, et utilisez payable(msg.sender).call{value: amount}("").

Exemple guide 2 : Contrat Pausable

Créez un contrat avec des fonctions qui peuvent être mises en pause.

# Exercice 2 : Contrat Pausable
EXERCICE_PAUSABLE = '''
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

contract Pausable {
    address public owner;
    bool public paused = false;
    uint256 public counter = 0;

    constructor() {
        owner = msg.sender;
    }

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

    modifier whenNotPaused() {
        require(!paused, "Paused");
        _;
    }

    function pause() public onlyOwner {
        paused = true;
    }

    function unpause() public onlyOwner {
        paused = false;
    }

    function increment() public whenNotPaused {
        counter += 1;
    }

    function reset() public onlyOwner whenNotPaused {
        counter = 0;
    }
}
'''

# Compilation et deploiement reel sur anvil
pausable, receipt = compile_and_deploy(w3, EXERCICE_PAUSABLE, deployer)
print("Depart :", "paused =", pausable.functions.paused().call(), ", counter =", pausable.functions.counter().call())

w3.eth.wait_for_transaction_receipt(
    pausable.functions.increment().transact({'from': deployer})
)
print("Apres increment :", pausable.functions.counter().call())
assert pausable.functions.counter().call() == 1

w3.eth.wait_for_transaction_receipt(
    pausable.functions.pause().transact({'from': deployer})
)
print("Contrat en pause :", pausable.functions.paused().call())

increment_bloque = False
try:
    pausable.functions.increment().transact({'from': deployer})
except Exception:
    increment_bloque = True
print("Increment pendant la pause :", "bloque" if increment_bloque else "autorise")
assert increment_bloque

w3.eth.wait_for_transaction_receipt(
    pausable.functions.unpause().transact({'from': deployer})
)
w3.eth.wait_for_transaction_receipt(
    pausable.functions.reset().transact({'from': deployer})
)
print("Retour a zero :", "paused =", pausable.functions.paused().call(), ", counter =", pausable.functions.counter().call())
assert pausable.functions.paused().call() is False
assert pausable.functions.counter().call() == 0
Deploye: Pausable a 0xe8D2A1E88c91DCd5433208d4152Cc4F399a7e91d
Depart : paused = False , counter = 0
Apres increment : 1
Contrat en pause : True
Increment pendant la pause : bloque
Retour a zero : paused = False , counter = 0

Indice : Initialisez owner dans le constructor. Pour les modifiers, utilisez require suivi de _;. Les fonctions pause et unpause doivent simplement modifier paused. increment et reset sont proteges par whenNotPaused.

Exercice 3 : Contrat Vault avec modifier onlyOwner

Mettez en pratique les custom modifiers (section 4) et payable (section 3.2). Créez un coffre-fort ou seul le propriétaire peut retirer les fonds.

Indice : require(msg.sender == owner, "Not owner"); _; pour le modifier.

# Exercice 3 : Contrat Vault avec modifier onlyOwner
# TODO etudiant : implementer un coffre-fort owner-only
# Etape 1 : Definir address public owner, capture msg.sender dans le constructor
# Etape 2 : Definir modifier onlyOwner() avec require(msg.sender == owner); _;
# Etape 3 : Implementer deposit() public payable
# Etape 4 : Implementer withdrawAll() public onlyOwner
EXERCICE_3_VAULT = """
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

contract Vault {
    // TODO etudiant : owner, modifier onlyOwner, deposit, withdrawAll
}
"""
# vault, receipt = compile_and_deploy(w3, EXERCICE_3_VAULT, deployer)
# vault.functions.deposit().transact({'from': deployer, 'value': w3.to_wei(1, 'ether')})
# vault.functions.withdrawAll().transact({'from': deployer})
print("Exercice a completer")
Exercice a completer

7. Résumé

Concept Description
storage Données permanentes sur blockchain
memory Données temporaires en RAM
calldata Données d’entrée read-only
public Accessible partout
external Accessible uniquement de l’exterieur
internal Ce contrat et enfants
private Uniquement ce contrat
view Lecture seule du storage
pure Aucun accès au storage
payable Peut recevoir des ETH

Notebook suivant : SC-05-Inheritance-Python


<< Solidity Basics | Inheritance >>

Exercice 4 : Contrat Calculator (view vs pure)

Distinguez les fonctions view et pure (section 3.1). Une calculatrice qui montre la différence entre lecture seule et aucun accès storage.

Indice : pure = ni lecture ni ecriture storage. view = lecture seule.

# Exercice 4 : Contrat Calculator (view vs pure)
# TODO etudiant : implementer une calculatrice distinguant pure / view / ecriture
# Etape 1 : Definir uint256 public lastResult
# Etape 2 : Implementer add(uint256 a, uint256 b) public pure returns (uint256)
# Etape 3 : Implementer computeAndStore(uint256 a, uint256 b) public returns (uint256)
# Etape 4 : Implementer getLast() public view returns (uint256)
EXERCICE_4_CALC = """
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

contract Calculator {
    // TODO etudiant : lastResult, add (pure), computeAndStore, getLast (view)
}
"""
# calc, receipt = compile_and_deploy(w3, EXERCICE_4_CALC, deployer)
# print("add(2, 3) =", calc.functions.add(2, 3).call())
# calc.functions.computeAndStore(10, 5).transact({'from': deployer})
# print("getLast() =", calc.functions.getLast().call())
print("Exercice a completer")
Exercice a completer

Exercice 5 : Contrat Registry (data locations storage vs memory)

Mettez en pratique les data locations (section 1). Le piege : User memory u créé une copie (modifications perdues), User storage u modifie réellement le tableau.

Indice : User storage u = users[index]; pour une vraie reference storage.

# Exercice 5 : Contrat Registry (data locations storage vs memory)
# TODO etudiant : implementer un registre ou bumpScore modifie le storage (pas une copie memory)
# Etape 1 : Definir struct User { string name; uint256 score; } et User[] public users
# Etape 2 : Implementer addUser(string memory name)
# Etape 3 : Implementer bumpScore(uint256 index) avec User storage u = users[index];
# Etape 4 : Implementer getScore(uint256 index) public view returns (uint256)
EXERCICE_5_REGISTRY = """
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

contract Registry {
    // TODO etudiant : struct User, tableau users, addUser, bumpScore (storage ref), getScore
}
"""
# registry, receipt = compile_and_deploy(w3, EXERCICE_5_REGISTRY, deployer)
# registry.functions.addUser("Alice").transact({'from': deployer})
# registry.functions.bumpScore(0).transact({'from': deployer})
# registry.functions.bumpScore(0).transact({'from': deployer})
# print("getScore(0) =", registry.functions.getScore(0).call())
print("Exercice a completer")
Exercice a completer
Retour au sommet