templates.multisig_action

  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))