Module multisig_action
Expand source code
import smartpy as sp
# Internal administration action type specification
InternalAdminAction = sp.TVariant(
addSigners=sp.TList(sp.TAddress),
changeQuorum=sp.TNat,
removeSigners=sp.TList(sp.TAddress),
)
class MultisigAction(sp.Contract):
"""A contract that can be used by multiple signers to administrate other
contracts. The administrated contracts implement an interface that make it
possible to explicit the administration process to non expert users.
Signers vote for proposals. A proposal is a list of a target with a list of
action. An action is a simple byte but it is intended to be a pack value of
a variant. This simple pattern make it possible to build a UX interface
that shows the content of a proposal or build one.
"""
def __init__(self, quorum, signers):
self.init(
inactiveBefore=0,
nextId=0,
proposals=sp.big_map(
tkey=sp.TNat,
tvalue=sp.TList(
sp.TRecord(target=sp.TAddress, actions=sp.TList(sp.TBytes))
),
),
quorum=quorum,
signers=sp.set(signers),
votes=sp.big_map(tkey=sp.TNat, tvalue=sp.TSet(sp.TAddress)),
)
@sp.entrypoint(new_syntax=True)
def send_proposal(self, proposal):
"""Signer-only. Submit a proposal to the vote.
Args:
proposal (sp.TList of sp.TRecord of target address and action): List\
of target and associated administration actions.
"""
assert self.data.signers.contains(sp.sender), "Only signers can propose"
self.data.proposals[self.data.nextId] = proposal
self.data.votes[self.data.nextId] = sp.set()
self.data.nextId += 1
@sp.entrypoint(new_syntax=True)
def vote(self, pId):
"""Vote for one or more proposals
Args:
pId (sp.TNat): Id of the proposal.
"""
assert self.data.signers.contains(sp.sender), "Only signers can vote"
assert self.data.votes.contains(pId), "Proposal unknown"
assert pId >= self.data.inactiveBefore, "The proposal is 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_lambda(with_storage="read-write", with_operations=True)
def _onApproved(self, pId):
"""Inlined function. Logic applied when a proposal has been approved."""
proposal = self.data.proposals.get(pId, [])
with sp.for_("p_item", proposal) as p_item:
contract = sp.contract(sp.TList(sp.TBytes), p_item.target)
sp.transfer(p_item.actions, sp.tez(0), contract.open_some("InvalidTarget"))
# Inactivate all proposals that have been already submitted.
self.data.inactiveBefore = self.data.nextId
@sp.entrypoint
def administrate(self, actions):
"""Self-call only. Administrate this contract.
This entrypoint must be called through the proposal system.
Args:
actions (sp.TList of sp.TBytes): List of packed variant of \
`InternalAdminAction` (`addSigners`, `changeQuorum`, `removeSigners`).
"""
sp.verify(
sp.sender == sp.self_address,
"This entrypoint must be called through the proposal system.",
)
with sp.for_("action", actions) as action:
action = sp.unpack(action, InternalAdminAction).open_some("Bad actions format")
with (action).match_cases() as arg:
with arg.match("changeQuorum") as quorum:
self.data.quorum = quorum
with arg.match("addSigners") as added:
with sp.for_("signer", added) as signer:
self.data.signers.add(signer)
with arg.match("removeSigners") as removed:
with sp.for_("address", removed) as address:
self.data.signers.remove(address)
# Ensure that the contract never requires more quorum than the total of signers.
sp.verify(
self.data.quorum <= sp.len(self.data.signers),
message="More quorum than signers.",
)
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()
s.h1("Basic scenario")
s.h2("Origination")
c1 = MultisigAction(
quorum=2,
signers=[signer1.address, signer2.address],
)
s += c1
s.h2("Proposal for adding a new signer")
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]), InternalAdminAction)
)
c1.send_proposal([sp.record(target=target, actions=[action])]).run(
sender=signer1
)
s.h2("Signer 1 votes for the proposal")
c1.vote(0).run(sender=signer1)
s.h2("Signer 2 votes for the proposal")
c1.vote(0).run(sender=signer2)
s.verify(c1.data.signers.contains(signer3.address))
Classes
class MultisigAction (quorum, signers)
-
A contract that can be used by multiple signers to administrate other contracts. The administrated contracts implement an interface that make it possible to explicit the administration process to non expert users.
Signers vote for proposals. A proposal is a list of a target with a list of action. An action is a simple byte but it is intended to be a pack value of a variant. This simple pattern make it possible to build a UX interface that shows the content of a proposal or build one.
Expand source code
class MultisigAction(sp.Contract): """A contract that can be used by multiple signers to administrate other contracts. The administrated contracts implement an interface that make it possible to explicit the administration process to non expert users. Signers vote for proposals. A proposal is a list of a target with a list of action. An action is a simple byte but it is intended to be a pack value of a variant. This simple pattern make it possible to build a UX interface that shows the content of a proposal or build one. """ def __init__(self, quorum, signers): self.init( inactiveBefore=0, nextId=0, proposals=sp.big_map( tkey=sp.TNat, tvalue=sp.TList( sp.TRecord(target=sp.TAddress, actions=sp.TList(sp.TBytes)) ), ), quorum=quorum, signers=sp.set(signers), votes=sp.big_map(tkey=sp.TNat, tvalue=sp.TSet(sp.TAddress)), ) @sp.entrypoint(new_syntax=True) def send_proposal(self, proposal): """Signer-only. Submit a proposal to the vote. Args: proposal (sp.TList of sp.TRecord of target address and action): List\ of target and associated administration actions. """ assert self.data.signers.contains(sp.sender), "Only signers can propose" self.data.proposals[self.data.nextId] = proposal self.data.votes[self.data.nextId] = sp.set() self.data.nextId += 1 @sp.entrypoint(new_syntax=True) def vote(self, pId): """Vote for one or more proposals Args: pId (sp.TNat): Id of the proposal. """ assert self.data.signers.contains(sp.sender), "Only signers can vote" assert self.data.votes.contains(pId), "Proposal unknown" assert pId >= self.data.inactiveBefore, "The proposal is 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_lambda(with_storage="read-write", with_operations=True) def _onApproved(self, pId): """Inlined function. Logic applied when a proposal has been approved.""" proposal = self.data.proposals.get(pId, []) with sp.for_("p_item", proposal) as p_item: contract = sp.contract(sp.TList(sp.TBytes), p_item.target) sp.transfer(p_item.actions, sp.tez(0), contract.open_some("InvalidTarget")) # Inactivate all proposals that have been already submitted. self.data.inactiveBefore = self.data.nextId @sp.entrypoint def administrate(self, actions): """Self-call only. Administrate this contract. This entrypoint must be called through the proposal system. Args: actions (sp.TList of sp.TBytes): List of packed variant of \ `InternalAdminAction` (`addSigners`, `changeQuorum`, `removeSigners`). """ sp.verify( sp.sender == sp.self_address, "This entrypoint must be called through the proposal system.", ) with sp.for_("action", actions) as action: action = sp.unpack(action, InternalAdminAction).open_some("Bad actions format") with (action).match_cases() as arg: with arg.match("changeQuorum") as quorum: self.data.quorum = quorum with arg.match("addSigners") as added: with sp.for_("signer", added) as signer: self.data.signers.add(signer) with arg.match("removeSigners") as removed: with sp.for_("address", removed) as address: self.data.signers.remove(address) # Ensure that the contract never requires more quorum than the total of signers. sp.verify( self.data.quorum <= sp.len(self.data.signers), message="More quorum than signers.", )
Ancestors
- smartpy.Contract
Methods
def send_proposal(self, proposal)
-
Entrypoint. Signer-only. Submit a proposal to the vote.
Args
proposal
:sp.TList
ofsp.TRecord
oftarget address and action
- List of target and associated administration actions.
Expand source code
@sp.entrypoint(new_syntax=True) def send_proposal(self, proposal): """Signer-only. Submit a proposal to the vote. Args: proposal (sp.TList of sp.TRecord of target address and action): List\ of target and associated administration actions. """ assert self.data.signers.contains(sp.sender), "Only signers can propose" self.data.proposals[self.data.nextId] = proposal self.data.votes[self.data.nextId] = sp.set() self.data.nextId += 1
def vote(self, pId)
-
Entrypoint. Vote for one or more proposals
Args
pId
:sp.TNat
- Id of the proposal.
Expand source code
@sp.entrypoint(new_syntax=True) def vote(self, pId): """Vote for one or more proposals Args: pId (sp.TNat): Id of the proposal. """ assert self.data.signers.contains(sp.sender), "Only signers can vote" assert self.data.votes.contains(pId), "Proposal unknown" assert pId >= self.data.inactiveBefore, "The proposal is 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)
def administrate(self, actions)
-
Entrypoint. Self-call only. Administrate this contract.
This entrypoint must be called through the proposal system.
Args
actions
:sp.TList
ofsp.TBytes
- List of packed variant of
InternalAdminAction
(addSigners
,changeQuorum
,removeSigners
).
Expand source code
@sp.entrypoint def administrate(self, actions): """Self-call only. Administrate this contract. This entrypoint must be called through the proposal system. Args: actions (sp.TList of sp.TBytes): List of packed variant of \ `InternalAdminAction` (`addSigners`, `changeQuorum`, `removeSigners`). """ sp.verify( sp.sender == sp.self_address, "This entrypoint must be called through the proposal system.", ) with sp.for_("action", actions) as action: action = sp.unpack(action, InternalAdminAction).open_some("Bad actions format") with (action).match_cases() as arg: with arg.match("changeQuorum") as quorum: self.data.quorum = quorum with arg.match("addSigners") as added: with sp.for_("signer", added) as signer: self.data.signers.add(signer) with arg.match("removeSigners") as removed: with sp.for_("address", removed) as address: self.data.signers.remove(address) # Ensure that the contract never requires more quorum than the total of signers. sp.verify( self.data.quorum <= sp.len(self.data.signers), message="More quorum than signers.", )