SC-09-DAO-Governance-Python - Gouvernance DAO

<< Précédent : DeFi Primitives | Retour au sommaire | Suivant : Account Abstraction >>


Objectifs d’apprentissage

  1. Implementer un système de vote pondere
  2. Créer et executer des propositions
  3. Utiliser un timelock pour la securite

Prerequis

  • SC-3 a SC-8 completes
  • Concepts de gouvernance on-chain et votes
  • Tokens ERC-20 et delégation (SC-5)

Duree estimee : 45 minutes


0. Connexion a la blockchain locale

Tous les contrats de ce notebook sont compiles et deployes reellement 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
except ImportError:
    print("Installation requise : pip install web3")
    raise

try:
    import solcx
except ImportError:
    print("Installation requise : pip install py-solc-x")
    raise

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."

# Reset anvil pour un etat propre (les checkpoints de vote dependent du block.number)
w3.provider.make_request("anvil_reset", [])

# 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}), bloc {w3.eth.block_number}, 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 = None, None
    for cid, ci in compiled.items():
        if ci["bin"]:
            contract_id, contract_interface = cid, ci
    if contract_interface is None:
        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), bloc 0, deployer: 0xf39Fd6e5...

1. Governance Token

Le token de gouvernance donne des droits de vote.

# Governance Token avec checkpoint de votes
GOVERNANCE_TOKEN = '''
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

contract GovernanceToken {
    string public constant name = "Governance Token";
    string public constant symbol = "GOV";
    uint8 public constant decimals = 18;

    uint256 private _totalSupply;
    mapping(address => uint256) private _balances;
    mapping(address => mapping(address => uint256)) private _allowances;

    struct Checkpoint {
        uint32 fromBlock;
        uint224 votes;
    }

    mapping(address => Checkpoint[]) private _checkpoints;
    mapping(address => address) private _delegates;

    event Transfer(address indexed from, address indexed to, uint256 value);
    event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate);

    constructor(uint256 initialSupply) {
        _mint(msg.sender, initialSupply);
    }

    function delegate(address delegatee) public {
        address currentDelegate = delegates(msg.sender);
        uint256 balance = _balances[msg.sender];
        _delegates[msg.sender] = delegatee;
        _moveVotingPower(currentDelegate, delegatee, balance);
        emit DelegateChanged(msg.sender, currentDelegate, delegatee);
    }

    function delegates(address account) public view returns (address) {
        address d = _delegates[account];
        return d == address(0) ? account : d;
    }

    function getVotes(address account) public view returns (uint256) {
        uint256 pos = _checkpoints[account].length;
        return pos > 0 ? _checkpoints[account][pos - 1].votes : 0;
    }

    function getPastVotes(address account, uint256 blockNumber)
        public view returns (uint256)
    {
        require(blockNumber < block.number, "Block not yet mined");
        Checkpoint[] storage ckpts = _checkpoints[account];
        uint256 low = 0;
        uint256 high = ckpts.length;
        while (low < high) {
            uint256 mid = (low + high) / 2;
            if (ckpts[mid].fromBlock > blockNumber) {
                high = mid;
            } else {
                low = mid + 1;
            }
        }
        return high == 0 ? 0 : ckpts[high - 1].votes;
    }

    function totalSupply() public view returns (uint256) { return _totalSupply; }
    function balanceOf(address account) public view returns (uint256) { return _balances[account]; }

    function transfer(address to, uint256 amount) public returns (bool) {
        require(_balances[msg.sender] >= amount, "Insufficient balance");
        _balances[msg.sender] -= amount;
        _balances[to] += amount;
        _moveVotingPower(delegates(msg.sender), delegates(to), amount);
        emit Transfer(msg.sender, to, amount);
        return true;
    }

    function _mint(address to, uint256 amount) internal {
        _totalSupply += amount;
        _balances[to] += amount;
        _moveVotingPower(address(0), delegates(to), amount);
        emit Transfer(address(0), to, amount);
    }

    function _moveVotingPower(address src, address dst, uint256 amount) internal {
        if (src != address(0)) {
            uint256 pos = _checkpoints[src].length;
            uint224 old = pos > 0 ? _checkpoints[src][pos - 1].votes : 0;
            _writeCheckpoint(_checkpoints[src], old - uint224(amount));
        }
        if (dst != address(0)) {
            uint256 pos = _checkpoints[dst].length;
            uint224 old = pos > 0 ? _checkpoints[dst][pos - 1].votes : 0;
            _writeCheckpoint(_checkpoints[dst], old + uint224(amount));
        }
    }

    function _writeCheckpoint(Checkpoint[] storage ckpts, uint224 newVotes) internal {
        uint256 pos = ckpts.length;
        if (pos > 0 && ckpts[pos - 1].fromBlock == block.number) {
            ckpts[pos - 1].votes = newVotes;
        } else {
            ckpts.push(Checkpoint({fromBlock: uint32(block.number), votes: newVotes}));
        }
    }
}
'''

# Deployer le GovernanceToken avec 10M tokens
initial_supply = 10_000_000 * 10**18
gov_token, _ = compile_and_deploy(w3, GOVERNANCE_TOKEN, deployer, initial_supply)

# Deleguer le voting power a soi-meme
tx = gov_token.functions.delegate(deployer).transact({"from": deployer})
w3.eth.wait_for_transaction_receipt(tx)

# Transferer des tokens et deleguer pour un second votant
voter2 = w3.eth.accounts[1]
tx = gov_token.functions.transfer(voter2, 2_000_000 * 10**18).transact({"from": deployer})
w3.eth.wait_for_transaction_receipt(tx)
tx = gov_token.functions.delegate(voter2).transact({"from": voter2})
w3.eth.wait_for_transaction_receipt(tx)

# Miner des blocs pour ancrer les checkpoints (getPastVotes regarde block.number - 1)
for _ in range(3):
    w3.provider.make_request("evm_mine", [])

print(f"Governance Token:")
print(f"  Total supply   : {gov_token.functions.totalSupply().call() / 10**18:,.0f} GOV")
print(f"  Votes deployer : {gov_token.functions.getVotes(deployer).call() / 10**18:,.0f} GOV")
print(f"  Votes voter2   : {gov_token.functions.getVotes(voter2).call() / 10**18:,.0f} GOV")
Deploye: GovernanceToken a 0x5FbDB2315678afecb367f032d93F642f64180aa3
Governance Token:
  Total supply   : 10,000,000 GOV
  Votes deployer : 8,000,000 GOV
  Votes voter2   : 2,000,000 GOV

Exemple guide : Calcul du quorum

Cette section montre comment verifier si une proposition de gouvernance atteint le quorum, c’est-a-dire la participation minimale requise pour qu’un vote soit valide. La fonction check_quorum (cellule suivante) est entierement resolue : elle calcule, a partir des voix pour / contre / abstention et de la supply totale de tokens, si le seuil de participation est franchi, puis la repartition des voix exprimees.

Logique implementee :

  • total_votes = for + against + abstain
  • quorum_threshold = total_supply * quorum_percent / 100 (4 % par defaut, même idee que la constante QUORUM = 4_000_000 du SimpleGovernor deploye plus loin)
  • quorum_reached = total_votes >= quorum_threshold
  • participation_percent, for_percent, against_percent : parts relatives a la supply (participation) et aux votes exprimes (pour / contre)

Test resolu : avec 3,5 M de votes pour, 1 M contre et 0,5 M d’abstention sur une supply de 100 M de tokens, la participation s’eleve a 5,00 % (seuil de quorum 4 M franchi), soit 70,0 % de voix pour et 20,0 % contre - le quorum est atteint.

La delegation de votes - transferer son pouvoir de vote vers un representant - est enseignee dans l’interpretation précédente : le contrat GovernanceToken deploye implemente delegate(address), les checkpoints par bloc, ainsi que getVotes et getPastVotes. Les trois exercices a completer qui suivent (pouvoir de vote a un bloc passe, vote quadratique, statut d’une transaction timelockee) construisent sur ces mécanismes de gouvernance.

# Exercice 1 : Calcul du quorum
def check_quorum(for_votes, against_votes, abstain_votes, total_supply, quorum_percent=4):
    """
    Verifie si le quorum est atteint
    
    Quorum = (for + against + abstain) >= quorum_percent * total_supply / 100
    
    Args:
        for_votes: Nombre de votes pour
        against_votes: Nombre de votes contre
        abstain_votes: Nombre d abstentions
        total_supply: Supply totale de tokens
        quorum_percent: Pourcentage requis pour le quorum
    
    Returns:
        dict avec:
        - quorum_reached (bool)
        - total_votes
        - quorum_threshold
        - participation_percent
        - for_percent
        - against_percent
    """
    # Etape: Calculer le total des votes
    total_votes = for_votes + against_votes + abstain_votes
    # Etape: Calculer le seuil du quorum (total_supply * quorum_percent / 100)
    quorum_threshold = total_supply * quorum_percent / 100
    # Etape: Determiner si le quorum est atteint
    quorum_reached = total_votes >= quorum_threshold
    # Etape: Calculer le pourcentage de participation
    participation_percent = (total_votes / total_supply * 100) if total_supply > 0 else 0
    # Etape: Calculer les pourcentages pour/contre
    for_percent = (for_votes / total_votes * 100) if total_votes > 0 else 0
    against_percent = (against_votes / total_votes * 100) if total_votes > 0 else 0
    # Etape: Retourner le dictionnaire avec tous les resultats
    return {
        "quorum_reached": quorum_reached,
        "total_votes": total_votes,
        "quorum_threshold": quorum_threshold,
        "participation_percent": participation_percent,
        "for_percent": for_percent,
        "against_percent": against_percent,
    }

# Test (decommenter apres implementation)
total_supply = 100_000_000 * 1e18  # 100M tokens
result = check_quorum(
    for_votes=3_500_000 * 1e18,
    against_votes=1_000_000 * 1e18,
    abstain_votes=500_000 * 1e18,
    total_supply=total_supply
)

print(f"Quorum atteint: {result['quorum_reached']}")
print(f"Participation: {result['participation_percent']:.2f}%")
print(f"Pour: {result['for_percent']:.1f}%")
print(f"Contre: {result['against_percent']:.1f}%")
Quorum atteint: True
Participation: 5.00%
Pour: 70.0%
Contre: 20.0%

Interpretation : GovernanceToken — checkpoints et delegation

Résultat obtenu : Le token GOV est deploye avec 10M de supply initiale. Après delegation et transfer, le deployer detient 8M de votes et voter2 en detient 2M. Des blocs sont mines pour ancrer les checkpoints dans le passe.

Architecture des checkpoints :

Concept Implementation Intérêt
Delegation delegate(address) Les tokens ne donnent pas de votes par defaut — il faut deleguer
Checkpoints Checkpoint[] par adresse Historique du pouvoir de vote par bloc
getVotes(account) Dernier checkpoint Pouvoir de vote actuel
getPastVotes(account, block) Recherche binaire Pouvoir de vote a un bloc passe

Points cles : - Sans delegation explicite, un token holder n’a aucun pouvoir de vote — c’est une protection contre les attaques par flash loan (emprunter, voter, rembourser dans le même bloc) - L’appel anvil_reset au debut du notebook assure un block.number = 0 propre — les checkpoints de vote dependent du numéro de bloc, pas du timestamp - Le mining de 3 blocs supplementaires (evm_mine) est necessaire pour que getPastVotes fonctionne : elle exige blockNumber < block.number - En production (Uniswap, Compound), les token holders deleguent souvent a des “vote delegates” specialises


2. Governor Contract

Le governor gere le cycle de vie des propositions.

# Governor simplifie
GOVERNOR_CONTRACT = '''
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

interface IGovernanceToken {
    function getVotes(address account) external view returns (uint256);
    function getPastVotes(address account, uint256 blockNumber) external view returns (uint256);
}

contract SimpleGovernor {
    IGovernanceToken public immutable token;

    uint256 public constant VOTING_DELAY = 1;
    uint256 public constant VOTING_PERIOD = 50400;
    uint256 public constant PROPOSAL_THRESHOLD = 100 * 1e18;
    uint256 public constant QUORUM = 4_000_000 * 1e18;

    enum ProposalState { Pending, Active, Canceled, Defeated, Succeeded, Queued, Expired, Executed }

    struct Proposal {
        uint256 id;
        address proposer;
        address[] targets;
        uint256[] values;
        bytes[] calldatas;
        string description;
        uint256 voteStart;
        uint256 voteEnd;
        uint256 forVotes;
        uint256 againstVotes;
        uint256 abstainVotes;
        bool executed;
    }

    mapping(uint256 => Proposal) public proposals;
    mapping(uint256 => mapping(address => bool)) public hasVoted;
    uint256 private _proposalCount;

    event ProposalCreated(uint256 indexed id, address indexed proposer, address[] targets, uint256[] values, bytes[] calldatas, string description);
    event VoteCast(address indexed voter, uint256 indexed proposalId, uint8 support, uint256 weight);

    constructor(address token_) { token = IGovernanceToken(token_); }

    function propose(address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description) public returns (uint256) {
        require(token.getPastVotes(msg.sender, block.number - 1) >= PROPOSAL_THRESHOLD, "Below proposal threshold");
        uint256 proposalId = ++_proposalCount;
        proposals[proposalId] = Proposal({
            id: proposalId, proposer: msg.sender, targets: targets, values: values,
            calldatas: calldatas, description: description,
            voteStart: block.number + VOTING_DELAY, voteEnd: block.number + VOTING_DELAY + VOTING_PERIOD,
            forVotes: 0, againstVotes: 0, abstainVotes: 0, executed: false
        });
        emit ProposalCreated(proposalId, msg.sender, targets, values, calldatas, description);
        return proposalId;
    }

    function castVote(uint256 proposalId, uint8 support) public {
        require(state(proposalId) == ProposalState.Active, "Voting not active");
        require(!hasVoted[proposalId][msg.sender], "Already voted");
        Proposal storage proposal = proposals[proposalId];
        uint256 weight = token.getPastVotes(msg.sender, proposal.voteStart);
        hasVoted[proposalId][msg.sender] = true;
        if (support == 0) proposal.againstVotes += weight;
        else if (support == 1) proposal.forVotes += weight;
        else proposal.abstainVotes += weight;
        emit VoteCast(msg.sender, proposalId, support, weight);
    }

    function state(uint256 proposalId) public view returns (ProposalState) {
        Proposal storage p = proposals[proposalId];
        if (p.executed) return ProposalState.Executed;
        if (block.number < p.voteStart) return ProposalState.Pending;
        if (block.number < p.voteEnd) return ProposalState.Active;
        uint256 total = p.forVotes + p.againstVotes + p.abstainVotes;
        if (total < QUORUM || p.againstVotes >= p.forVotes) return ProposalState.Defeated;
        return ProposalState.Succeeded;
    }
}
'''

# Deployer le Governor
governor, _ = compile_and_deploy(w3, GOVERNOR_CONTRACT, deployer, gov_token.address)

# Miner des blocs pour que les checkpoints de delegation soient bien dans le passe
for _ in range(3):
    w3.provider.make_request("evm_mine", [])

# Creer une proposition
tx = governor.functions.propose(
    [deployer], [0], [b""],
    "Proposition #1: Augmenter le budget R&D de 10%"
).transact({"from": deployer})
receipt = w3.eth.wait_for_transaction_receipt(tx)
proposal_events = governor.events.ProposalCreated().process_receipt(receipt)
proposal_id = proposal_events[0]["args"]["id"]

state_names = ["Pending", "Active", "Canceled", "Defeated", "Succeeded", "Queued", "Expired", "Executed"]
print(f"\nGovernor:")
print(f"  Proposal ID : {proposal_id}")
print(f"  Etat        : {state_names[governor.functions.state(proposal_id).call()]}")

# Avancer de 2 blocs : 1 pour VOTING_DELAY, 1 pour que voteStart soit dans le passe
# (castVote appelle getPastVotes(voter, voteStart) qui exige voteStart < block.number)
w3.provider.make_request("evm_mine", [])
w3.provider.make_request("evm_mine", [])
print(f"  Etat        : {state_names[governor.functions.state(proposal_id).call()]}")

# Voter
tx = governor.functions.castVote(proposal_id, 1).transact({"from": deployer})
w3.eth.wait_for_transaction_receipt(tx)
tx = governor.functions.castVote(proposal_id, 1).transact({"from": voter2})
w3.eth.wait_for_transaction_receipt(tx)

# Resultats
proposal_data = governor.functions.proposals(proposal_id).call()
print(f"\nVotes:")
print(f"  Pour    : {proposal_data[5] / 10**18:,.0f} GOV")
print(f"  Contre  : {proposal_data[6] / 10**18:,.0f} GOV")
print(f"  Abstain : {proposal_data[7] / 10**18:,.0f} GOV")
Deploye: SimpleGovernor a 0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9

Governor:
  Proposal ID : 1
  Etat        : Pending
  Etat        : Active

Votes:
  Pour    : 10,000,000 GOV
  Contre  : 0 GOV
  Abstain : 0 GOV

Exercice 2 : Calcul du pouvoir de vote a un bloc passe

Les systèmes de gouvernance utilisent des checkpoints pour connaitre le pouvoir de vote a un bloc précis. Implementez une fonction Python qui, etant donne une liste de checkpoints (bloc, votes), retourne le nombre de votes a un bloc donne en utilisant la recherche binaire.

Indice : Les checkpoints sont tries par numéro de bloc croissant. Trouvez le dernier checkpoint dont fromBlock <= block_number par recherche binaire. - Si block_number < checkpoints[0].fromBlock : retourner 0 - Sinon : retourner checkpoints[index].votes ou index est le plus grand index tel que checkpoints[index].fromBlock <= block_number

# Exercice 2 : Calcul du pouvoir de vote a un bloc passe
def get_past_votes(checkpoints, block_number):
    """
    Retourne le pouvoir de vote a un bloc donne (recherche binaire).

    Args:
        checkpoints: Liste de tuples (fromBlock, votes) tries par fromBlock croissant
        block_number: Numero de bloc auquel on veut connaitre les votes

    Returns:
        int: Nombre de votes a ce bloc, ou 0 si aucun checkpoint anterieur
    """
    # TODO etudiant : si la liste est vide ou block_number < premier checkpoint -> 0
    # TODO etudiant : recherche binaire pour trouver le dernier checkpoint <= block_number
    # Indice : utiliser low/high comme dans le contrat GovernanceToken
    return 0


# Test apres implementation
checkpoints = [
    (10, 1000),
    (25, 3000),
    (40, 2500),
    (60, 5000),
]

print(f"Votes au bloc 5  : {get_past_votes(checkpoints, 5)}")   # Aucun checkpoint -> 0
print(f"Votes au bloc 10 : {get_past_votes(checkpoints, 10)}")  # Exact checkpoint -> 1000
print(f"Votes au bloc 30 : {get_past_votes(checkpoints, 30)}")  # Apres bloc 25 -> 3000
print(f"Votes au bloc 55 : {get_past_votes(checkpoints, 55)}")  # Apres bloc 40 -> 2500
print(f"Votes au bloc 70 : {get_past_votes(checkpoints, 70)}")  # Apres bloc 60 -> 5000")
Votes au bloc 5  : 0
Votes au bloc 10 : 0
Votes au bloc 30 : 0
Votes au bloc 55 : 0
Votes au bloc 70 : 0

Interpretation : Gouverneur — cycle de proposition et vote

Résultat obtenu : Le gouverneur SimpleGovernor gere le cycle complet d’une proposition — creation (etat Pending), transition vers Active après 2 blocs, puis vote unanime des 10M GOV (8M deployer + 2M voter2).

Progression d’etat de la proposition :

Étape Blocs mines Etat Description
Creation 0 Pending Proposition créée, vote pas encore ouvert
+2 blocs 2 Active block.number >= voteStart, vote ouvert
Votes 2 Active 10M GOV pour, 0 contre, 0 abstain

Paramètres de gouvernance :

Paramètre Valeur Rôle
VOTING_DELAY 1 bloc Delai avant ouverture du vote
VOTING_PERIOD 50 400 blocs (~1 semaine) Duree du vote
PROPOSAL_THRESHOLD 100 GOV Tokens min pour proposer
QUORUM 4 000 000 GOV Participation min pour valider

Points cles : - getPastVotes(voter, voteStart) utilise les checkpoints du GovernanceToken pour connaitre le pouvoir de vote a un bloc précis — evite les attaques type “acheter des tokens pour voter” - Le quorum de 4M GOV (40% de la supply) est atteint facilement ici avec 10M de votes - Une proposition est Defeated si le quorum n’est pas atteint OU si les votes contre >= votes pour

Exercice 3 : Vote quadratique

Dans un système de vote quadratique, le cout pour un votant est proportionnel au carre de son nombre de votes. Si un votant veut attribuer n votes a une proposition, il paie n^2 credits. Cela privilegie l’intensite des préférences plutot que la seule quantite de tokens.

Objectifs : 1. Implementer une fonction quadraticVote(proposalId, numVotes) qui consomme numVotes^2 credits 2. Verifier que le votant a suffisamment de credits 3. Suivre les credits depenses dans un mapping creditsSpent

Indices : - Un mapping mapping(address => uint256) public creditsSpent tracke les credits utilises par adresse - Les credits disponibles d’un votant = getVotes(voter) - creditsSpent[voter] - Le cout d’un vote quadratique = numVotes * numVotes (attention a l’overflow) - Utilisez require(numVotes > 0) pour eviter les votes nuls

# Exercice 3 : Vote quadratique
def quadratic_vote(voter_credits, proposal_votes, voter, proposal_id, num_votes):
    """
    Simule un vote quadratique : le cout est proportionnel au carre des votes.

    Args:
        voter_credits: dict {address: credits_disponibles}
        proposal_votes: dict {proposal_id: total_votes_cast}
        voter: adresse du votant
        proposal_id: identifiant de la proposition
        num_votes: nombre de votes a attribuer

    Returns:
        tuple: (voter_credits_mis_a_jour, proposal_votes_mis_a_jour, cost)
        ou None si le vote echoue (credits insuffisants ou votes nuls)

    TODO etudiant : implementez quadratic_vote.
    Etape 1 : verifier que num_votes > 0
    Etape 2 : calculer le cout = num_votes * num_votes
    Etape 3 : verifier que voter_credits[voter] - credits_spent >= cost
    Etape 4 : deduire les credits et ajouter les votes a la proposition
    """
    # TODO etudiant : implementez quadratic_vote
    return None


# Test apres implementation
credits = {
    "Alice": 1_000_000,
    "Bob": 500_000,
    "Charlie": 200_000,
}
proposals = {1: 0, 2: 0}

# Alice vote 100 votes sur la proposition 1 (cout = 10 000)
result = quadratic_vote(credits, proposals, "Alice", 1, 100)
if result:
    credits, proposals, cost = result
    print(f"Alice vote 100 sur prop 1 : cout = {cost}, votes prop 1 = {proposals[1]}")
    print(f"  Credits restants Alice : {credits['Alice']}")
else:
    print("Vote echoue (a implementer)")

# Bob vote 800 votes sur la proposition 1 (cout = 640 000 > 500 000)
result = quadratic_vote(credits, proposals, "Bob", 1, 800)
if result:
    print("Bob : vote accepte (inespere)")
else:
    print("Bob : credits insuffisants pour 800 votes (cout=640000 > 500000)")

# Charlie vote 400 votes (cout = 160 000 < 200 000)
result = quadratic_vote(credits, proposals, "Charlie", 1, 400)
if result:
    credits, proposals, cost = result
    print(f"Charlie vote 400 sur prop 1 : cout = {cost}, votes prop 1 = {proposals[1]}")
else:
    print("Vote echoue (a implementer)")
print("Exercice a completer : quadratic_vote")
Vote echoue (a implementer)
Bob : credits insuffisants pour 800 votes (cout=640000 > 500000)
Vote echoue (a implementer)
Exercice a completer : quadratic_vote

3. Timelock Controller

Le timelock ajoute un delai avant l’exécution pour la securite.

# Timelock simplifie
TIMELOCK_CONTRACT = '''
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

contract SimpleTimelock {
    uint256 public constant MIN_DELAY = 1 days;
    uint256 public constant MAX_DELAY = 30 days;
    uint256 public constant GRACE_PERIOD = 14 days;

    address public admin;
    uint256 public delay;

    mapping(bytes32 => bool) public queuedTransactions;

    event QueueTransaction(
        bytes32 indexed txHash,
        address indexed target,
        uint256 value,
        bytes data,
        uint256 eta
    );
    event ExecuteTransaction(
        bytes32 indexed txHash,
        address indexed target,
        uint256 value,
        bytes data
    );
    event CancelTransaction(bytes32 indexed txHash);

    constructor(uint256 delay_) {
        require(delay_ >= MIN_DELAY, "Delay too short");
        require(delay_ <= MAX_DELAY, "Delay too long");
        admin = msg.sender;
        delay = delay_;
    }

    // Hash unique pour chaque transaction
    function getTxHash(
        address target,
        uint256 value,
        bytes calldata data,
        uint256 eta
    ) public pure returns (bytes32) {
        return keccak256(abi.encode(target, value, data, eta));
    }

    // Mettre en file d'attente
    function queue(
        address target,
        uint256 value,
        bytes calldata data,
        uint256 eta
    ) public returns (bytes32) {
        require(msg.sender == admin, "Not admin");
        require(eta >= block.timestamp + delay, "ETA too soon");

        bytes32 txHash = getTxHash(target, value, data, eta);
        require(!queuedTransactions[txHash], "Already queued");

        queuedTransactions[txHash] = true;
        emit QueueTransaction(txHash, target, value, data, eta);

        return txHash;
    }

    // Executer apres le delai
    function execute(
        address target,
        uint256 value,
        bytes calldata data,
        uint256 eta
    ) public payable {
        require(msg.sender == admin, "Not admin");

        bytes32 txHash = getTxHash(target, value, data, eta);
        require(queuedTransactions[txHash], "Not queued");
        require(block.timestamp >= eta, "Too early");
        require(block.timestamp <= eta + GRACE_PERIOD, "Expired");

        queuedTransactions[txHash] = false;

        (bool success, ) = target.call{value: value}(data);
        require(success, "Execution failed");

        emit ExecuteTransaction(txHash, target, value, data);
    }

    // Annuler
    function cancel(bytes32 txHash) public {
        require(msg.sender == admin, "Not admin");
        require(queuedTransactions[txHash], "Not queued");

        queuedTransactions[txHash] = false;
        emit CancelTransaction(txHash);
    }
}
'''

# Deployer le Timelock avec un delai de 1 jour (86400 secondes)
timelock, _ = compile_and_deploy(w3, TIMELOCK_CONTRACT, deployer, 86400)

# --- Demonstration: mettre une transaction en file d'attente ---
print(f"\nTimelock Controller:")
print(f"  Admin     : {timelock.functions.admin().call()[:10]}...")
print(f"  Delay     : {timelock.functions.delay().call()} secondes ({timelock.functions.delay().call() // 3600}h)")
print(f"  Min delay : {timelock.functions.MIN_DELAY().call()} secondes")
print(f"  Max delay : {timelock.functions.MAX_DELAY().call()} secondes")

# Calculer l'ETA (timestamp actuel + delay + marge)
current_block = w3.eth.get_block("latest")
current_timestamp = current_block["timestamp"]
eta = current_timestamp + 86400 + 60  # delay + 1 minute de marge

# Mettre en queue une transaction fictive (envoyer 0 ETH a deployer)
tx = timelock.functions.queue(
    deployer,  # target
    0,         # value
    b"",       # data (vide)
    eta        # eta
).transact({"from": deployer})
receipt = w3.eth.wait_for_transaction_receipt(tx)

# Verifier le hash
tx_hash = timelock.functions.getTxHash(deployer, 0, b"", eta).call()
is_queued = timelock.functions.queuedTransactions(tx_hash).call()
print(f"\n  Transaction en queue: {is_queued}")
print(f"  TX hash   : {tx_hash.hex()[:20]}...")
print(f"  ETA       : timestamp {eta} (dans ~24h)")

# Annuler la transaction
tx = timelock.functions.cancel(tx_hash).transact({"from": deployer})
w3.eth.wait_for_transaction_receipt(tx)
is_queued_after = timelock.functions.queuedTransactions(tx_hash).call()
print(f"  Apres cancel: queued={is_queued_after}")
Deploye: SimpleTimelock a 0x0165878A594ca255338adfa4d48449f69242Eb8F

Timelock Controller:
  Admin     : 0xf39Fd6e5...
  Delay     : 86400 secondes (24h)
  Min delay : 86400 secondes
  Max delay : 2592000 secondes

  Transaction en queue: True
  TX hash   : f2d0714cd2efeaaf296c...
  ETA       : timestamp 1786977144 (dans ~24h)
  Apres cancel: queued=False

Exercice 4 : Verifier la validite d’une transaction timelocked

Ecrivez une fonction Python qui simule la verification d’une transaction dans un timelock. Etant donne le hash de la transaction, l’ETA, le delai minimum et le timestamp actuel, determinez si la transaction peut etre executee, est en attente ou a expire.

Indice : Une transaction est executable si timestamp >= eta ET timestamp <= eta + grace_period. Si timestamp < eta, elle est en attente. Si timestamp > eta + grace_period, elle a expire.

# Exercice 4 : Verifier la validite d'une transaction timelocked
def check_timelock_status(eta, current_timestamp, grace_period=14 * 24 * 3600):
    """
    Determine le statut d'une transaction dans le timelock.

    Args:
        eta: Timestamp d'execution prevu (epoch secondes)
        current_timestamp: Timestamp actuel
        grace_period: Periode de grace apres ETA (default 14 jours)

    Returns:
        str: "EXECUTABLE", "PENDING" (en attente), ou "EXPIRED" (expiree)
    """
    # TODO etudiant : verifier si current_timestamp < eta -> PENDING
    # TODO etudiant : verifier si current_timestamp > eta + grace_period -> EXPIRED
    # TODO etudiant : sinon -> EXECUTABLE
    return "PENDING"


# Test apres implementation
import time

now = 1_700_000_000
eta_future = now + 86400          # demain
eta_past = now - 86400            # hier
eta_expired = now - 16 * 86400    # il y a 16 jours

print(f"Future  (eta+1j) : {check_timelock_status(eta_future, now)}")
print(f"Passe   (eta-1j) : {check_timelock_status(eta_past, now)}")
print(f"Expired (eta-16j): {check_timelock_status(eta_expired, now)}")
Future  (eta+1j) : PENDING
Passe   (eta-1j) : PENDING
Expired (eta-16j): PENDING

Interpretation : Timelock — file d’attente et annulation

Résultat obtenu : Le contrat SimpleTimelock deploye avec un delai de 86 400 secondes (24h). Une transaction est mise en file d’attente, verifiee comme queued=True, puis annulee avec succes (queued=False).

Cycle de vie d’une transaction timelocked :

Etat Action Condition
Inexistante queue(target, value, data, eta) eta >= now + delay
En file (queued=True) Attendre block.timestamp >= eta Delai de 24h minimum
Executable execute(target, value, data, eta) eta <= now <= eta + GRACE_PERIOD
Expiree automatique now > eta + 14 jours

Points cles : - Le hash unique keccak256(abi.encode(target, value, data, eta)) identifie chaque transaction — un changement d’un seul paramètre genere un hash différent - Le delai de 24h est le minimum configurable (MIN_DELAY = 1 day), le maximum est 30 jours - La grace period de 14 jours après l’ETA evite que des transactions restent en file indefiniment - Le timelock est le dernier rempart de securite dans une DAO : même si un vote malveillant passe, la communaute a 24h pour reagir (exit, fork, contre-proposition)


4. Resume

Composant Rôle
GovernanceToken Token avec voting power
Governor Gestion des propositions et votes
Timelock Delai de securite avant exécution

Paramètres cles

  • Voting Delay : Blocs avant debut du vote
  • Voting Period : Duree du vote
  • Quorum : Participation minimum requise
  • Proposal Threshold : Tokens min pour proposer

Notebook suivant : SC-10-Account-Abstraction-Python

Resume et perspectives

Ce notebook a constitue une plongee complete dans la gouvernance decentralisee on-chain. Nous avons implemente un GovernanceToken avec checkpoints de votes et delegation, un mécanisme essentiel pour se proteger des attaques par flash loan (emprunter, voter, rembourser dans le même bloc). Le SimpleGovernor a demontre le cycle de vie complet d’une proposition : creation (avec seuil de tokens requis), transition Pending vers Active après le voting delay, vote pondere via getPastVotes, et resolution basee sur le quorum et la majorite.

Le SimpleTimelock a complete l’architecture de gouvernance en ajoutant un delai obligatoire entre l’approbation d’une proposition et son exécution. Ce mécanisme de 24 heures (minimum configurable) offre a la communaute une dernière chance de reagir face a une decision malveillante, que ce soit par un exit collectif, un fork ou une contre-proposition. Le hachage keccak256(abi.encode(target, value, data, eta)) garantit l’unicite et l’integrite de chaque transaction en file d’attente.

Le notebook suivant, SC-10-Account-Abstraction-Python, aborde ERC-4337 et l’abstraction de compte, une evolution qui transforme les wallets en contrats intelligents programmables avec recuperation sociale, sponsoring de gas et logique personnalisee.


<< Précédent : DeFi Primitives | Retour au sommaire | Suivant : Account Abstraction >>

Retour au sommet