1import smartpy as sp
2
3
4@sp.module
5def main():
6 # Internal administration action type specification
7 InternalAdminAction: type = sp.variant(
8 addSigners=sp.list[sp.address],
9 changeQuorum=sp.nat,
10 removeSigners=sp.list[sp.address],
11 )
12
13 class MultisigAction(sp.Contract):
14 """A contract that can be used by multiple signers to administrate other
15 contracts. The administrated contracts implement an interface that make it
16 possible to explicit the administration process to non expert users.
17
18 Signers vote for proposals. A proposal is a list of a target with a list of
19 action. An action is a simple byte but it is intended to be a pack value of
20 a variant. This simple pattern make it possible to build a UX interface
21 that shows the content of a proposal or build one.
22 """
23
24 def __init__(self, quorum, signers):
25 self.data.inactiveBefore = 0
26 self.data.nextId = 0
27 self.data.proposals = sp.cast(
28 sp.big_map(),
29 sp.big_map[
30 sp.nat,
31 sp.list[sp.record(target=sp.address, actions=sp.list[sp.bytes])],
32 ],
33 )
34 self.data.quorum = sp.cast(quorum, sp.nat)
35 self.data.signers = sp.cast(signers, sp.set[sp.address])
36 self.data.votes = sp.cast(
37 sp.big_map(), sp.big_map[sp.nat, sp.set[sp.address]]
38 )
39
40 @sp.entrypoint
41 def send_proposal(self, proposal):
42 """Signer-only. Submit a proposal to the vote.
43
44 Args:
45 proposal (sp.list of sp.record of target address and action): List\
46 of target and associated administration actions.
47 """
48 assert self.data.signers.contains(sp.sender), "Only signers can propose"
49 self.data.proposals[self.data.nextId] = proposal
50 self.data.votes[self.data.nextId] = set()
51 self.data.nextId += 1
52
53 @sp.entrypoint
54 def vote(self, pId):
55 """Vote for one or more proposals
56
57 Args:
58 pId (sp.nat): Id of the proposal.
59 """
60 assert self.data.signers.contains(sp.sender), "Only signers can vote"
61 assert self.data.votes.contains(pId), "Proposal unknown"
62 assert pId >= self.data.inactiveBefore, "The proposal is inactive"
63 self.data.votes[pId].add(sp.sender)
64
65 if sp.len(self.data.votes.get(pId, default=set())) >= self.data.quorum:
66 self._onApproved(pId)
67
68 @sp.private(with_storage="read-write", with_operations=True)
69 def _onApproved(self, pId):
70 """Inlined function. Logic applied when a proposal has been approved."""
71 proposal = self.data.proposals.get(pId, default=[])
72 for p_item in proposal:
73 contract = sp.contract(sp.list[sp.bytes], p_item.target)
74 sp.transfer(
75 p_item.actions,
76 sp.tez(0),
77 contract.unwrap_some(error="InvalidTarget"),
78 )
79 # Inactivate all proposals that have been already submitted.
80 self.data.inactiveBefore = self.data.nextId
81
82 @sp.entrypoint
83 def administrate(self, actions):
84 """Self-call only. Administrate this contract.
85
86 This entrypoint must be called through the proposal system.
87
88 Args:
89 actions (sp.list of sp.bytes): List of packed variant of \
90 `InternalAdminAction` (`addSigners`, `changeQuorum`, `removeSigners`).
91 """
92 assert (
93 sp.sender == sp.self_address()
94 ), "This entrypoint must be called through the proposal system."
95
96 for packed_actions in actions:
97 action = sp.unpack(packed_actions, InternalAdminAction).unwrap_some(
98 error="Bad actions format"
99 )
100 with sp.match(action):
101 with sp.case.changeQuorum as quorum:
102 self.data.quorum = quorum
103 with sp.case.addSigners as added:
104 for signer in added:
105 self.data.signers.add(signer)
106 with sp.case.removeSigners as removed:
107 for address in removed:
108 self.data.signers.remove(address)
109 # Ensure that the contract never requires more quorum than the total of signers.
110 assert self.data.quorum <= sp.len(
111 self.data.signers
112 ), "More quorum than signers."
113
114
115if "main" in __name__:
116
117 @sp.add_test()
118 def test():
119 signer1 = sp.test_account("signer1")
120 signer2 = sp.test_account("signer2")
121 signer3 = sp.test_account("signer3")
122
123 s = sp.test_scenario("Basic scenario", main)
124 s.h1("Basic scenario")
125
126 s.h2("Origination")
127 c1 = main.MultisigAction(
128 quorum=2,
129 signers=sp.set([signer1.address, signer2.address]),
130 )
131 s += c1
132
133 s.h2("Proposal for adding a new signer")
134 target = sp.to_address(
135 sp.contract(sp.list[sp.bytes], c1.address, "administrate").unwrap_some()
136 )
137 action = sp.pack(
138 sp.set_type_expr(
139 sp.variant.addSigners([signer3.address]), main.InternalAdminAction
140 )
141 )
142 c1.send_proposal([sp.record(target=target, actions=[action])], _sender=signer1)
143
144 s.h2("Signer 1 votes for the proposal")
145 c1.vote(0, _sender=signer1)
146 s.h2("Signer 2 votes for the proposal")
147 c1.vote(0, _sender=signer2)
148
149 s.verify(c1.data.signers.contains(signer3.address))