Les contrats multisig permettent de créer un contrôle partagé sur les actifs. Les cas d'utilisation typiques concernent les services de dépôt fiduciaire, la gestion des comptes d'entreprise, la cosignature d'accords financiers, etc. Ces contrats sont particulièrement avantageux pour les organisations ou les groupes pour lesquels une prise de décision collective est nécessaire.
De par leur conception, les contrats multisig sont inviolables et empêchent les points de défaillance uniques. Même si les clés d'une partie sont compromises, l'attaquant ne peut pas exécuter de transactions sans l'approbation des autres parties. Cela ajoute une couche supplémentaire de sécurité.
Les contrats multisig peuvent être considérés comme l'équivalent numérique d'un coffre-fort dont l'ouverture nécessite plusieurs clés. Le nombre total de clés (N) et le nombre minimum de clés nécessaires pour ouvrir la boîte (M) sont convenus lors de la création du contrat.
Les contrats multisig peuvent avoir de nombreuses configurations différentes en fonction des valeurs de M et N :
Dans le contexte de la blockchain, les contrats multisig sont largement utilisés pour renforcer la sécurité des transactions, soutenir des mécanismes de gouvernance complexes ou maintenir un contrôle flexible sur les actifs de la blockchain. Voici quelques exemples :
En ce qui concerne nos exemples de code, nous examinerons trois implémentations différentes de contrats multi-signatures :
Il est assez polyvalent et permet un large éventail d'utilisations. Il nécessite des signatures multiples pour exécuter des fonctions lambda arbitraires.
Python
import smartpy as sp
@sp.module
def main() :
operation_lambda : type = sp.lambda_(sp.unit, sp.unit, with_operations=True)
classe MultisigLambda(sp.Contract) :
"""Vote des membres multiples pour l'exécution des lambdas.
Ce contrat peut être établi à partir d'une liste d'adresses et d'un nombre de votes requis
. Chaque membre peut soumettre autant de lambdas qu'il le souhaite et voter
pour les propositions actives. Lorsqu'un lambda atteint les votes requis, son code est appelé sur
et les opérations de sortie sont exécutées. Cela permet à ce contrat de
faire tout ce qu'un contrat peut faire : transférer des jetons, gérer des actifs,
administrer un autre contrat...
Lorsqu'un lambda est appliqué, tous les lambdas soumis jusqu'à présent sont inactivés.
Les membres peuvent toujours soumettre de nouveaux lambdas.
"""
def __init__(self, members, required_votes) : Constructeur Args : membres (sp.set of sp.address) : personnes qui peuvent soumettre et voter pour lambda.
"""
required_votes (sp.nat) : nombre de votes requis
" " "
assert required_votes <= sp.len(
members
), "required_votes must be <= len(members)"
self.data.lambdas = sp.cast(
sp.big_map(), sp.big_map[sp.nat, operation_lambda]
)
self.data.votes = sp.cast(
sp.big_map(), sp.big_map[sp.nat, sp.set[sp.address]]
)
self.data.nextId = 0
self.data.inactiveBefore = 0
self.data.members = sp.cast(members, sp.set[sp.address])
self.data.required_votes = sp.cast(required_votes, sp.nat)
@sp.entrypoint
def submit_lambda(self, lambda_) :
"""Soumettre un nouveau lambda au vote.
La soumission d'une proposition n'implique pas un vote en sa faveur.
Args :
lambda_(sp.lambda with operations) : lambda proposé au vote.
Raise :
`Vous n'êtes pas membre`
" " "
assert self.data.members.contains(sp.sender), "Vous n'êtes pas membre"
self.data.lambdas[self.data.nextId] = lambda_
self.data.votes[self.data.nextId] = sp.set()
self.data.nextId += 1
@sp.entrypoint
def vote_lambda(self, id) :
"""Vote pour un lambda.
Args :
id(sp.nat) : l'identifiant du lambda pour lequel voter.
Souleve :
`Vous n'êtes pas membre`, `Le lambda est inactif`, `Lambda introuvable`
Il n'y a pas de vote contre ou de vote positif. Si quelqu'un n'est pas d'accord avec un lambda
, il peut s'abstenir de voter.
"""
assert self.data.members.contains(sp.sender), "Vous n'êtes pas membre"
assert id >= self.data.inactiveBefore, "Le lambda est inactif"
assert self.data.lambdas.contains(id), "Lambda non trouvé"
self.data.votes[id].add(sp.sender)
if sp.len(self.data.votes[id]) >= self.data.required_votes :
self.data.lambdas[id]()
self.data.inactiveBefore = self.data.nextId
@sp.onchain_view()
def get_lambda(self, id) :
"""Retourne le lambda correspondant.
Args :
id (sp.nat) : id du lambda à obtenir.
Retour :
paire du lambda et un booléen indiquant si le lambda est actif.
"""
return (self.data.lambdas[id], id >= self.data.inactiveBefore)
# if "templates" not in __name__:
@sp.module
def test() :
class Administrated(sp.Contract) :
def __init__(self, admin) :
self.data.admin = admin
self.data.value = sp.int(0)
@sp.entrypoint
def set_value(self, value) :
assert sp.sender == self.data.admin
self.data.value = value
@sp.add_test(name="MultisigLambda basic scenario", is_default=True)
def basic_scenario() :
"""Utilisez le multisigLambda en tant qu'administrateur d'un exemple de contrat.
Tests :
- Origination
- Soumission lambda
- Vote lambda
" " "
sc = sp.test_scenario([main, test])
sc.h1("Scénario de base.")
membre1 = sp.test_account("membre1")
member2 = sp.test_account("member2")
membre3 = sp.test_account("membre3")
membres = sp.set([membre1.adresse, membre2.adresse, membre3.adresse])
sc.h2("MultisigLambda : origination")
c1 = main.MultisigLambda(members, 2)
sc += c1
sc.h2("Administré : origination")
c2 = test.Administré(c1.address)
sc += c2
sc.h2("MultisigLambda : submit_lambda")
def set_42(params) :
administrated = sp.contract(sp.TInt, c2.address, entrypoint="set_value")
sp.transfer(sp.int(42), sp.tez(0), administrated.open_some())
lambda_ = sp.build_lambda(set_42, with_operations=True)
c1.submit_lambda(lambda_).run(sender=member1)
sc.h2("MultisigLambda : vote_lambda")
c1.vote_lambda(0).run(sender=member1)
c1.vote_lambda(0).run(sender=member2)
# Nous pouvons vérifier que le contrat administré a bien reçu le transfert.
sc.verify(c2.data.value == 42)
Il introduit le concept de vote pour les propositions. Dans ce contrat, les signataires peuvent voter pour certaines actions à entreprendre, et si le quorum est atteint, les actions proposées sont exécutées.
Python
import smartpy as sp
@sp.module
def main() :
# Spécification du type d'action d'administration interne
InternalAdminAction : type = sp.variant(
addSigners=sp.list[sp.address],
changeQuorum=sp.nat,
removeSigners=sp.list[sp.address],
)
class MultisigAction(sp.Contract) :
"""Un contrat qui peut être utilisé par plusieurs signataires pour administrer d'autres contrats
. Les contrats administrés mettent en œuvre une interface qui permet
d'expliciter le processus d'administration à des utilisateurs non experts.
Les signataires votent pour les propositions. Une proposition est une liste d'objectifs assortie d'une liste d'actions
. Une action est un simple octet mais elle est destinée à être une valeur de paquet de
une variante. Ce modèle simple permet de construire une interface UX
qui montre le contenu d'une proposition ou d'en construire une.
"""
def __init__(self, quorum, signers) : self.data.inactiveBefore = 0
self.data.nextId = 0
self.data.proposals = sp.cast(
sp.big_map(),
sp.big_map[
sp.nat,
sp.list[sp.record(target=sp.address, actions=sp.list[sp.bytes])],
],
)
self.data.quorum = sp.cast(quorum, sp.nat)
self.data.signers = sp.cast(signers, sp.set[sp.address])
self.data.votes = sp.cast(
sp.big_map(), sp.big_map[sp.nat, sp.set[sp.address]]
)
@sp.entrypoint
def send_proposal(self, proposal) :
"""Signer uniquement. Soumettre une proposition au vote.
Args :
proposition (sp.list of sp.record of target address and action) : Liste
de la cible et des actions administratives associées.
"""
assert self.data.signers.contains(sp.sender), "Seuls les signataires peuvent proposer"
self.data.proposals[self.data.nextId] = proposition
self.data.votes[self.data.nextId] = sp.set()
self.data.nextId += 1
@sp.entrypoint
def vote(self, pId) :
"""Vote pour une ou plusieurs propositions
Args :
pId (sp.nat) : Id de la proposition.
"""
assert self.data.signers.contains(sp.sender), "Seuls les signataires peuvent voter"
assert self.data.votes.contains(pId), "Proposition inconnue"
assert pId >= self.data.inactiveBefore, "La proposition est inactive"
self.data.votes[pId].add(sp.sender)
if sp.len(self.data.votes.get(pId, default=sp.set())) >= self.data.quorum :
self._onApproved(pId)
@sp.private(with_storage="read-write", with_operations=True)
def _onApproved(self, pId) :
"""Fonction intégrée. Logique appliquée lorsqu'une proposition a été approuvée."""
proposition = self.data.proposals.get(pId, default=[])
for p_item in proposal :
contract = sp.contract(sp.list[sp.bytes], p_item.target)
sp.transfer(
p_item.actions,
sp.tez(0),
contract.unwrap_some(error="InvalidTarget"),
)
# Inactivez toutes les propositions qui ont déjà été soumises.
self.data.inactiveBefore = self.data.nextId
@sp.entrypoint
def administrate(self, actions) :
"""Auto-appel uniquement. Gérer ce contrat.
Ce point d'entrée doit être appelé par le système de proposition.
Args :
actions (sp.list of sp.bytes) : Liste des variantes de \
`InternalAdminAction` (`addSigners`, `changeQuorum`, `removeSigners`).
"""
assert ( sp.sender == sp.self_address() ), Ce point d'entrée doit être appelé via le système de proposition. for packed_actions in actions : action = sp.unpack(packed_actions,
"" InternalAdminAction).unwrap_some(
error="Bad actions format"
)
with sp.match(action) :
with sp.case.changeQuorum comme quorum :
self.data.quorum = quorum
avec sp.case.addSigners comme ajouté :
pour signataire dans ajouté :
self.data.signers.add(signataire)
avec sp.case.removeSigners as removed :
for address in removed :
self.data.signers.remove(address)
# Veillez à ce que le contrat n'exige jamais un quorum supérieur au nombre total de signataires.
assert self.data.quorum <= sp.len(
self.data.signers
), "Plus de quorum que de signataires."
if "templates" not in __name__:
@sp.add_test(name="Basic scenario", is_default=True)
def test() :
signer1 = sp.test_account("signer1")
signer2 = sp.test_account("signer2")
signer3 = sp.test_account("signer3")
s = sp.test_scenario(main)
s.h1("Scénario de base")
s.h2("Origination")
c1 = main.MultisigAction(
quorum=2,
signers=sp.set([signer1.address, signer2.adresse]),
)
s += c1
s.h2("Proposition d'ajout d'un nouveau signataire")
target = sp.to_address(
sp.contract(sp.TList(sp.TBytes), c1.address, "administrate").open_some()
)
action = sp.pack(
sp.set_type_expr(
sp.variant("addSigners", [signer3.address]), main.InternalAdminAction
)
)
c1.send_proposal([sp.record(target=target, actions=[action])]).run(
sender=signer1
)
s.h2("Le signataire 1 vote pour la proposition")
c1.vote(0).run(sender=signer1)
s.h2("Le signataire 2 vote pour la proposition")
c1.vote(0).run(sender=signer2)
s.verify(c1.data.signers.contains(signer3.address))
Il utilise également un mécanisme de vote. Ce contrat permet aux membres de soumettre et de voter pour des octets arbitraires. Lorsqu'une proposition atteint le nombre de votes requis, son statut peut être confirmé par une vue.
Python
import smartpy as sp
@sp.module
def main() :
class MultisigView(sp.Contract) :
"""Plusieurs membres votent pour des octets arbitraires.
Ce contrat peut être établi à partir d'une liste d'adresses et d'un nombre de votes requis
. Chaque membre peut soumettre autant d'octets qu'il le souhaite et voter
pour les propositions actives.
Tout octet ayant atteint le nombre de votes requis peut être confirmé par une vue.
"""
def __init__(self, members, required_votes) : Constructeur Args : members (sp.set of sp.address) : personnes qui peuvent soumettre et voter pour lambda.
"""
required_votes (sp.nat) : nombre de votes requis
" " "
assert required_votes <= sp.len(
members
), "required_votes must be <= len(members)"
self.data.proposals = sp.cast(sp.big_map(), sp.big_map[sp.bytes, sp.bool])
self.data.votes = sp.cast(
sp.big_map(), sp.big_map[sp.bytes, sp.set[sp.address]]
)
self.data.members = sp.cast(members, sp.set[sp.address])
self.data.required_votes = sp.cast(required_votes, sp.nat)
@sp.entrypoint
def submit_proposal(self, bytes) :
"""Soumettre une nouvelle proposition au vote.
La soumission d'une proposition n'implique pas un vote en sa faveur.
Args :
bytes(sp.bytes) : octets proposés au vote.
Raise :
`Vous n'êtes pas membre`
" " "
assert self.data.members.contains(sp.sender), "Vous n'êtes pas membre"
self.data.proposals[bytes] = False
self.data.votes[bytes] = sp.set()
@sp.entrypoint
def vote_proposal(self, bytes) :
"""Vote pour une proposition.
Il n'y a pas de vote contre ou de vote pour. Si une personne n'est pas d'accord avec une proposition, elle peut s'abstenir de voter (
). Attention : les anciennes propositions non votées ne deviennent jamais
obsolètes.
Args :
id(sp.bytes) : octets de la proposition.
Rises :
`Vous n'êtes pas membre`, `Proposition introuvable`
" " "
assert self.data.members.contains(sp.sender), "Vous n'êtes pas membre"
assert self.data.proposals.contains(bytes), "Proposition non trouvée"
self.data.votes[bytes].add(sp.sender)
if sp.len(self.data.votes[bytes]) >= self.data.required_votes :
self.data.proposals[bytes] = True
@sp.onchain_view()
def is_voted(self, id) :
"""Retourne un booléen indiquant si la proposition a été votée.
Args :
id (sp.bytes) : bytes de la proposition
Return :
(sp.bool) : Vrai si la proposition a été votée, Faux sinon.
"""
return self.data.proposals.get(id, error="Proposition introuvable")
if "templates" not in __name__:
@sp.add_test(name="MultisigView basic scenario", is_default=True)
def basic_scenario() :
"""Un scénario avec un vote sur le contrat multisigView.
Tests :
- Origine
- Soumission de la proposition
- Vote de la proposition
" " "
sc = sp.test_scenario(main)
sc.h1("Scénario de base.")
membre1 = sp.test_account("membre1")
member2 = sp.test_account("member2")
membre3 = sp.test_account("membre3")
membres = sp.set([membre1.adresse, membre2.adresse, membre3.adresse])
sc.h2("Origination")
c1 = main.MultisigView(members, 2)
sc += c1
sc.h2("submit_proposal")
c1.submit_proposal(sp.bytes("0x42")).run(sender=member1)
sc.h2("vote_proposal")
c1.vote_proposal(sp.bytes("0x42")).run(sender=member1)
c1.vote_proposal(sp.bytes("0x42")).run(sender=member2)
# Nous pouvons vérifier que la proposition a été validée.
sc.verify(c1.is_voted(sp.bytes("0x42")))
Chaque contrat fournit un mécanisme différent pour obtenir un contrôle multi-signature, offrant une flexibilité en fonction des besoins spécifiques de votre cas d'utilisation de la blockchain.
Pour essayer les contrats multisig que nous avons écrits dans SmartPy, vous pouvez suivre les étapes suivantes :
Allez sur l'IDE SmartPy à l'adresse https://smartpy.io/ide.
Collez le code du contrat dans l'éditeur. Vous pouvez remplacer le code existant.
Pour exécuter le contrat, cliquez sur le bouton "Exécuter" situé dans le panneau supérieur.
Après avoir exécuté le contrat, vous pouvez visualiser l'exécution du scénario dans le panneau "Output" à droite. Vous y trouverez les détails de chaque action, y compris les propositions, les votes et les approbations.
Pour déployer votre contrat sur le réseau Tezos, vous devez d'abord le compiler. Cliquez sur le bouton "Compiler" dans le panneau supérieur.
Après la compilation, vous pouvez déployer le contrat sur le réseau de test en cliquant sur "Deploy Michelson Contract". Vous devrez fournir une clé secrète pour un compte Tezos avec suffisamment de fonds pour payer les frais d'essence du déploiement.
Une fois le contrat déployé, vous recevrez l'adresse du contrat sur la blockchain. Vous pouvez utiliser cette adresse pour interagir avec le contrat par le biais de transactions.
Pour soumettre des propositions ou voter dans les contrats, vous pouvez utiliser les points d'entrée définis dans le code du contrat, tels que submit_proposal
ou vote_proposal
. Elles peuvent être appelées directement à partir des transactions que vous créez.
N'oubliez pas que si l'IDE SmartPy vous permet de tester votre contrat sur une blockchain simulée, le déploiement du contrat sur le réseau Tezos réel entraînera des frais de gaz, qui doivent être payés en XTZ, la crypto-monnaie native du réseau Tezos.
Les contrats multisig permettent de créer un contrôle partagé sur les actifs. Les cas d'utilisation typiques concernent les services de dépôt fiduciaire, la gestion des comptes d'entreprise, la cosignature d'accords financiers, etc. Ces contrats sont particulièrement avantageux pour les organisations ou les groupes pour lesquels une prise de décision collective est nécessaire.
De par leur conception, les contrats multisig sont inviolables et empêchent les points de défaillance uniques. Même si les clés d'une partie sont compromises, l'attaquant ne peut pas exécuter de transactions sans l'approbation des autres parties. Cela ajoute une couche supplémentaire de sécurité.
Les contrats multisig peuvent être considérés comme l'équivalent numérique d'un coffre-fort dont l'ouverture nécessite plusieurs clés. Le nombre total de clés (N) et le nombre minimum de clés nécessaires pour ouvrir la boîte (M) sont convenus lors de la création du contrat.
Les contrats multisig peuvent avoir de nombreuses configurations différentes en fonction des valeurs de M et N :
Dans le contexte de la blockchain, les contrats multisig sont largement utilisés pour renforcer la sécurité des transactions, soutenir des mécanismes de gouvernance complexes ou maintenir un contrôle flexible sur les actifs de la blockchain. Voici quelques exemples :
En ce qui concerne nos exemples de code, nous examinerons trois implémentations différentes de contrats multi-signatures :
Il est assez polyvalent et permet un large éventail d'utilisations. Il nécessite des signatures multiples pour exécuter des fonctions lambda arbitraires.
Python
import smartpy as sp
@sp.module
def main() :
operation_lambda : type = sp.lambda_(sp.unit, sp.unit, with_operations=True)
classe MultisigLambda(sp.Contract) :
"""Vote des membres multiples pour l'exécution des lambdas.
Ce contrat peut être établi à partir d'une liste d'adresses et d'un nombre de votes requis
. Chaque membre peut soumettre autant de lambdas qu'il le souhaite et voter
pour les propositions actives. Lorsqu'un lambda atteint les votes requis, son code est appelé sur
et les opérations de sortie sont exécutées. Cela permet à ce contrat de
faire tout ce qu'un contrat peut faire : transférer des jetons, gérer des actifs,
administrer un autre contrat...
Lorsqu'un lambda est appliqué, tous les lambdas soumis jusqu'à présent sont inactivés.
Les membres peuvent toujours soumettre de nouveaux lambdas.
"""
def __init__(self, members, required_votes) : Constructeur Args : membres (sp.set of sp.address) : personnes qui peuvent soumettre et voter pour lambda.
"""
required_votes (sp.nat) : nombre de votes requis
" " "
assert required_votes <= sp.len(
members
), "required_votes must be <= len(members)"
self.data.lambdas = sp.cast(
sp.big_map(), sp.big_map[sp.nat, operation_lambda]
)
self.data.votes = sp.cast(
sp.big_map(), sp.big_map[sp.nat, sp.set[sp.address]]
)
self.data.nextId = 0
self.data.inactiveBefore = 0
self.data.members = sp.cast(members, sp.set[sp.address])
self.data.required_votes = sp.cast(required_votes, sp.nat)
@sp.entrypoint
def submit_lambda(self, lambda_) :
"""Soumettre un nouveau lambda au vote.
La soumission d'une proposition n'implique pas un vote en sa faveur.
Args :
lambda_(sp.lambda with operations) : lambda proposé au vote.
Raise :
`Vous n'êtes pas membre`
" " "
assert self.data.members.contains(sp.sender), "Vous n'êtes pas membre"
self.data.lambdas[self.data.nextId] = lambda_
self.data.votes[self.data.nextId] = sp.set()
self.data.nextId += 1
@sp.entrypoint
def vote_lambda(self, id) :
"""Vote pour un lambda.
Args :
id(sp.nat) : l'identifiant du lambda pour lequel voter.
Souleve :
`Vous n'êtes pas membre`, `Le lambda est inactif`, `Lambda introuvable`
Il n'y a pas de vote contre ou de vote positif. Si quelqu'un n'est pas d'accord avec un lambda
, il peut s'abstenir de voter.
"""
assert self.data.members.contains(sp.sender), "Vous n'êtes pas membre"
assert id >= self.data.inactiveBefore, "Le lambda est inactif"
assert self.data.lambdas.contains(id), "Lambda non trouvé"
self.data.votes[id].add(sp.sender)
if sp.len(self.data.votes[id]) >= self.data.required_votes :
self.data.lambdas[id]()
self.data.inactiveBefore = self.data.nextId
@sp.onchain_view()
def get_lambda(self, id) :
"""Retourne le lambda correspondant.
Args :
id (sp.nat) : id du lambda à obtenir.
Retour :
paire du lambda et un booléen indiquant si le lambda est actif.
"""
return (self.data.lambdas[id], id >= self.data.inactiveBefore)
# if "templates" not in __name__:
@sp.module
def test() :
class Administrated(sp.Contract) :
def __init__(self, admin) :
self.data.admin = admin
self.data.value = sp.int(0)
@sp.entrypoint
def set_value(self, value) :
assert sp.sender == self.data.admin
self.data.value = value
@sp.add_test(name="MultisigLambda basic scenario", is_default=True)
def basic_scenario() :
"""Utilisez le multisigLambda en tant qu'administrateur d'un exemple de contrat.
Tests :
- Origination
- Soumission lambda
- Vote lambda
" " "
sc = sp.test_scenario([main, test])
sc.h1("Scénario de base.")
membre1 = sp.test_account("membre1")
member2 = sp.test_account("member2")
membre3 = sp.test_account("membre3")
membres = sp.set([membre1.adresse, membre2.adresse, membre3.adresse])
sc.h2("MultisigLambda : origination")
c1 = main.MultisigLambda(members, 2)
sc += c1
sc.h2("Administré : origination")
c2 = test.Administré(c1.address)
sc += c2
sc.h2("MultisigLambda : submit_lambda")
def set_42(params) :
administrated = sp.contract(sp.TInt, c2.address, entrypoint="set_value")
sp.transfer(sp.int(42), sp.tez(0), administrated.open_some())
lambda_ = sp.build_lambda(set_42, with_operations=True)
c1.submit_lambda(lambda_).run(sender=member1)
sc.h2("MultisigLambda : vote_lambda")
c1.vote_lambda(0).run(sender=member1)
c1.vote_lambda(0).run(sender=member2)
# Nous pouvons vérifier que le contrat administré a bien reçu le transfert.
sc.verify(c2.data.value == 42)
Il introduit le concept de vote pour les propositions. Dans ce contrat, les signataires peuvent voter pour certaines actions à entreprendre, et si le quorum est atteint, les actions proposées sont exécutées.
Python
import smartpy as sp
@sp.module
def main() :
# Spécification du type d'action d'administration interne
InternalAdminAction : type = sp.variant(
addSigners=sp.list[sp.address],
changeQuorum=sp.nat,
removeSigners=sp.list[sp.address],
)
class MultisigAction(sp.Contract) :
"""Un contrat qui peut être utilisé par plusieurs signataires pour administrer d'autres contrats
. Les contrats administrés mettent en œuvre une interface qui permet
d'expliciter le processus d'administration à des utilisateurs non experts.
Les signataires votent pour les propositions. Une proposition est une liste d'objectifs assortie d'une liste d'actions
. Une action est un simple octet mais elle est destinée à être une valeur de paquet de
une variante. Ce modèle simple permet de construire une interface UX
qui montre le contenu d'une proposition ou d'en construire une.
"""
def __init__(self, quorum, signers) : self.data.inactiveBefore = 0
self.data.nextId = 0
self.data.proposals = sp.cast(
sp.big_map(),
sp.big_map[
sp.nat,
sp.list[sp.record(target=sp.address, actions=sp.list[sp.bytes])],
],
)
self.data.quorum = sp.cast(quorum, sp.nat)
self.data.signers = sp.cast(signers, sp.set[sp.address])
self.data.votes = sp.cast(
sp.big_map(), sp.big_map[sp.nat, sp.set[sp.address]]
)
@sp.entrypoint
def send_proposal(self, proposal) :
"""Signer uniquement. Soumettre une proposition au vote.
Args :
proposition (sp.list of sp.record of target address and action) : Liste
de la cible et des actions administratives associées.
"""
assert self.data.signers.contains(sp.sender), "Seuls les signataires peuvent proposer"
self.data.proposals[self.data.nextId] = proposition
self.data.votes[self.data.nextId] = sp.set()
self.data.nextId += 1
@sp.entrypoint
def vote(self, pId) :
"""Vote pour une ou plusieurs propositions
Args :
pId (sp.nat) : Id de la proposition.
"""
assert self.data.signers.contains(sp.sender), "Seuls les signataires peuvent voter"
assert self.data.votes.contains(pId), "Proposition inconnue"
assert pId >= self.data.inactiveBefore, "La proposition est inactive"
self.data.votes[pId].add(sp.sender)
if sp.len(self.data.votes.get(pId, default=sp.set())) >= self.data.quorum :
self._onApproved(pId)
@sp.private(with_storage="read-write", with_operations=True)
def _onApproved(self, pId) :
"""Fonction intégrée. Logique appliquée lorsqu'une proposition a été approuvée."""
proposition = self.data.proposals.get(pId, default=[])
for p_item in proposal :
contract = sp.contract(sp.list[sp.bytes], p_item.target)
sp.transfer(
p_item.actions,
sp.tez(0),
contract.unwrap_some(error="InvalidTarget"),
)
# Inactivez toutes les propositions qui ont déjà été soumises.
self.data.inactiveBefore = self.data.nextId
@sp.entrypoint
def administrate(self, actions) :
"""Auto-appel uniquement. Gérer ce contrat.
Ce point d'entrée doit être appelé par le système de proposition.
Args :
actions (sp.list of sp.bytes) : Liste des variantes de \
`InternalAdminAction` (`addSigners`, `changeQuorum`, `removeSigners`).
"""
assert ( sp.sender == sp.self_address() ), Ce point d'entrée doit être appelé via le système de proposition. for packed_actions in actions : action = sp.unpack(packed_actions,
"" InternalAdminAction).unwrap_some(
error="Bad actions format"
)
with sp.match(action) :
with sp.case.changeQuorum comme quorum :
self.data.quorum = quorum
avec sp.case.addSigners comme ajouté :
pour signataire dans ajouté :
self.data.signers.add(signataire)
avec sp.case.removeSigners as removed :
for address in removed :
self.data.signers.remove(address)
# Veillez à ce que le contrat n'exige jamais un quorum supérieur au nombre total de signataires.
assert self.data.quorum <= sp.len(
self.data.signers
), "Plus de quorum que de signataires."
if "templates" not in __name__:
@sp.add_test(name="Basic scenario", is_default=True)
def test() :
signer1 = sp.test_account("signer1")
signer2 = sp.test_account("signer2")
signer3 = sp.test_account("signer3")
s = sp.test_scenario(main)
s.h1("Scénario de base")
s.h2("Origination")
c1 = main.MultisigAction(
quorum=2,
signers=sp.set([signer1.address, signer2.adresse]),
)
s += c1
s.h2("Proposition d'ajout d'un nouveau signataire")
target = sp.to_address(
sp.contract(sp.TList(sp.TBytes), c1.address, "administrate").open_some()
)
action = sp.pack(
sp.set_type_expr(
sp.variant("addSigners", [signer3.address]), main.InternalAdminAction
)
)
c1.send_proposal([sp.record(target=target, actions=[action])]).run(
sender=signer1
)
s.h2("Le signataire 1 vote pour la proposition")
c1.vote(0).run(sender=signer1)
s.h2("Le signataire 2 vote pour la proposition")
c1.vote(0).run(sender=signer2)
s.verify(c1.data.signers.contains(signer3.address))
Il utilise également un mécanisme de vote. Ce contrat permet aux membres de soumettre et de voter pour des octets arbitraires. Lorsqu'une proposition atteint le nombre de votes requis, son statut peut être confirmé par une vue.
Python
import smartpy as sp
@sp.module
def main() :
class MultisigView(sp.Contract) :
"""Plusieurs membres votent pour des octets arbitraires.
Ce contrat peut être établi à partir d'une liste d'adresses et d'un nombre de votes requis
. Chaque membre peut soumettre autant d'octets qu'il le souhaite et voter
pour les propositions actives.
Tout octet ayant atteint le nombre de votes requis peut être confirmé par une vue.
"""
def __init__(self, members, required_votes) : Constructeur Args : members (sp.set of sp.address) : personnes qui peuvent soumettre et voter pour lambda.
"""
required_votes (sp.nat) : nombre de votes requis
" " "
assert required_votes <= sp.len(
members
), "required_votes must be <= len(members)"
self.data.proposals = sp.cast(sp.big_map(), sp.big_map[sp.bytes, sp.bool])
self.data.votes = sp.cast(
sp.big_map(), sp.big_map[sp.bytes, sp.set[sp.address]]
)
self.data.members = sp.cast(members, sp.set[sp.address])
self.data.required_votes = sp.cast(required_votes, sp.nat)
@sp.entrypoint
def submit_proposal(self, bytes) :
"""Soumettre une nouvelle proposition au vote.
La soumission d'une proposition n'implique pas un vote en sa faveur.
Args :
bytes(sp.bytes) : octets proposés au vote.
Raise :
`Vous n'êtes pas membre`
" " "
assert self.data.members.contains(sp.sender), "Vous n'êtes pas membre"
self.data.proposals[bytes] = False
self.data.votes[bytes] = sp.set()
@sp.entrypoint
def vote_proposal(self, bytes) :
"""Vote pour une proposition.
Il n'y a pas de vote contre ou de vote pour. Si une personne n'est pas d'accord avec une proposition, elle peut s'abstenir de voter (
). Attention : les anciennes propositions non votées ne deviennent jamais
obsolètes.
Args :
id(sp.bytes) : octets de la proposition.
Rises :
`Vous n'êtes pas membre`, `Proposition introuvable`
" " "
assert self.data.members.contains(sp.sender), "Vous n'êtes pas membre"
assert self.data.proposals.contains(bytes), "Proposition non trouvée"
self.data.votes[bytes].add(sp.sender)
if sp.len(self.data.votes[bytes]) >= self.data.required_votes :
self.data.proposals[bytes] = True
@sp.onchain_view()
def is_voted(self, id) :
"""Retourne un booléen indiquant si la proposition a été votée.
Args :
id (sp.bytes) : bytes de la proposition
Return :
(sp.bool) : Vrai si la proposition a été votée, Faux sinon.
"""
return self.data.proposals.get(id, error="Proposition introuvable")
if "templates" not in __name__:
@sp.add_test(name="MultisigView basic scenario", is_default=True)
def basic_scenario() :
"""Un scénario avec un vote sur le contrat multisigView.
Tests :
- Origine
- Soumission de la proposition
- Vote de la proposition
" " "
sc = sp.test_scenario(main)
sc.h1("Scénario de base.")
membre1 = sp.test_account("membre1")
member2 = sp.test_account("member2")
membre3 = sp.test_account("membre3")
membres = sp.set([membre1.adresse, membre2.adresse, membre3.adresse])
sc.h2("Origination")
c1 = main.MultisigView(members, 2)
sc += c1
sc.h2("submit_proposal")
c1.submit_proposal(sp.bytes("0x42")).run(sender=member1)
sc.h2("vote_proposal")
c1.vote_proposal(sp.bytes("0x42")).run(sender=member1)
c1.vote_proposal(sp.bytes("0x42")).run(sender=member2)
# Nous pouvons vérifier que la proposition a été validée.
sc.verify(c1.is_voted(sp.bytes("0x42")))
Chaque contrat fournit un mécanisme différent pour obtenir un contrôle multi-signature, offrant une flexibilité en fonction des besoins spécifiques de votre cas d'utilisation de la blockchain.
Pour essayer les contrats multisig que nous avons écrits dans SmartPy, vous pouvez suivre les étapes suivantes :
Allez sur l'IDE SmartPy à l'adresse https://smartpy.io/ide.
Collez le code du contrat dans l'éditeur. Vous pouvez remplacer le code existant.
Pour exécuter le contrat, cliquez sur le bouton "Exécuter" situé dans le panneau supérieur.
Après avoir exécuté le contrat, vous pouvez visualiser l'exécution du scénario dans le panneau "Output" à droite. Vous y trouverez les détails de chaque action, y compris les propositions, les votes et les approbations.
Pour déployer votre contrat sur le réseau Tezos, vous devez d'abord le compiler. Cliquez sur le bouton "Compiler" dans le panneau supérieur.
Après la compilation, vous pouvez déployer le contrat sur le réseau de test en cliquant sur "Deploy Michelson Contract". Vous devrez fournir une clé secrète pour un compte Tezos avec suffisamment de fonds pour payer les frais d'essence du déploiement.
Une fois le contrat déployé, vous recevrez l'adresse du contrat sur la blockchain. Vous pouvez utiliser cette adresse pour interagir avec le contrat par le biais de transactions.
Pour soumettre des propositions ou voter dans les contrats, vous pouvez utiliser les points d'entrée définis dans le code du contrat, tels que submit_proposal
ou vote_proposal
. Elles peuvent être appelées directement à partir des transactions que vous créez.
N'oubliez pas que si l'IDE SmartPy vous permet de tester votre contrat sur une blockchain simulée, le déploiement du contrat sur le réseau Tezos réel entraînera des frais de gaz, qui doivent être payés en XTZ, la crypto-monnaie native du réseau Tezos.