templates.price_feed_multisign_admin

  1import smartpy as sp
  2
  3"""
  4    Multisign Admin is a multisign administration contract.
  5
  6    Multiple signers can propose and vote for a list of batch of actions
  7    on the contract itself or the target.
  8
  9    The contract is configurable at origination time to adapt to target needed.
 10
 11    The contract supports gathering off-chain signed votes and pushing them in one call.
 12
 13    Voting system:
 14        When the quorum is reached on a proposal,
 15        If the number of yay > nay: the proposal is acceped
 16        Else: the proposal is rejected
 17
 18        Once a proposal is accepted the batchs of actions are executed,
 19        the other proposals are automatically canceled.
 20
 21    Vocabulary:
 22        Action: something to do on the MultisignAdmin or Target
 23        Batch of Actions: multiple actions to do on the same internal call
 24        List of Batchs: multiple internal calls containing batch of actions
 25
 26    A complexe proposal can include for example 2 batchs of actions:
 27    1) Self admin: change quorum + change signers
 28    2) Target admin: change parameter + change active
 29
 30    Antipattern:
 31        Instead of doing 2 batchs of 2 actions
 32        some could be tempted to use 4 batchs of 1 action
 33        In this case the params will be bigger and there will be 2 internal calls.
 34        Advice: Use less batchs possible and most actions per batch
 35"""
 36
 37
 38@sp.module
 39def m2():
 40    # TODO: activate when metadata is available
 41    # AGGREGATOR_METADATA = {
 42    #     "name"          : "Multi-sign Administrator",
 43    #     "version"       : "1",
 44    #     "description"   : "Multi-sign administrator for the price feed aggregator",
 45    #     "source"        : {
 46    #         "tools": [ "SmartPy" ]
 47    #     },
 48    #     "interfaces"    : [ "TZIP-016" ],
 49    # }
 50
 51    MultisignAdmin_Badsig = "MultisignAdmin_Badsig"
 52    MultisignAdmin_ProposalClosed = "MultisignAdmin_ProposalClosed"
 53    MultisignAdmin_ProposalUnknown = "MultisignAdmin_ProposalUnknown"
 54    MultisignAdmin_AlreadyVoted = "MultisignAdmin_AlreadyVoted"
 55    MultisignAdmin_VoterUnknown = "MultisignAdmin_VoterUnknown"
 56    MultisignAdmin_VoterAlreadyknown = "MultisignAdmin_VoterAlreadyknown"
 57    MultisignAdmin_ProposalTimedout = "MultisignAdmin_ProposalTimedout"
 58    MultisignAdmin_TargetUnkown = "MultisignAdmin_TargetUnkown"
 59    MultisignAdmin_MoreQuorumThanVoters = "MultisignAdmin_MoreQuorumThanVoters"
 60    MultisignAdmin_VotersLessThan1 = "MultisignAdmin_VotersLessThan1"
 61    MultisignAdmin_TargetNotSet = "MultisignAdmin_TargetNotSet"
 62    MultisignAdmin_WrongCallbackInterface = "MultisignAdmin_WrongCallbackInterface"
 63
 64    t_selfAdminAction: type = sp.variant(
 65        changeVoters=sp.record(
 66            removed=sp.set[sp.address],
 67            added=sp.list[sp.record(addr=sp.address, publicKey=sp.key)],
 68        ),
 69        changeTarget=sp.address,
 70        changeQuorum=sp.nat,
 71        changeTimeout=sp.int,
 72    )
 73
 74    t_targetAdminAction: type = sp.variant(
 75        changeOracles=sp.record(
 76            removed=sp.list[sp.address],
 77            added=sp.list[
 78                sp.pair[
 79                    sp.address,
 80                    sp.record(
 81                        startingRound=sp.nat,
 82                        endingRound=sp.option[sp.nat],
 83                        adminAddress=sp.address,
 84                    ),
 85                ]
 86            ],
 87        ),
 88        changeActive=sp.bool,
 89        changeAdmin=sp.address,
 90        updateFutureRounds=sp.record(
 91            minSubmissions=sp.nat,
 92            maxSubmissions=sp.nat,
 93            restartDelay=sp.nat,
 94            timeout=sp.nat,
 95            oraclePayment=sp.nat,
 96        ),
 97    )
 98    t_ProposalBatchs: type = sp.variant(
 99        selfAdmin=sp.list[t_selfAdminAction], targetAdmin=sp.list[t_targetAdminAction]
100    )
101    t_Proposal: type = sp.record(
102        startedAt=sp.timestamp,
103        id=sp.nat,
104        yay=sp.set[sp.nat],
105        nay=sp.set[sp.nat],
106        batchs=sp.list[t_ProposalBatchs],
107        canceled=sp.bool,
108    )
109
110    t_votes: type = sp.list[
111        sp.record(voterId=sp.nat, signature=sp.signature, yay=sp.bool)
112    ]
113
114    t_proposalsVotes: type = sp.list[
115        sp.record(initiatorId=sp.nat, proposalId=sp.nat, votes=t_votes)
116    ]
117
118    t_activeProposal: type = sp.record(
119        startedAt=sp.timestamp,
120        initiatorId=sp.nat,
121        initiatorProposalId=sp.nat,
122        yay=sp.set[sp.nat],
123        nay=sp.set[sp.nat],
124        batchs=sp.list[t_ProposalBatchs],
125    )
126
127    class MultisignAdmin(sp.Contract):
128        """
129        Each user can have at most one proposal
130        """
131
132        def __init__(
133            self,
134            quorum,
135            timeout,
136            addrVoterId,
137            keyVoterId,
138            voters,
139            lastVoterId,
140            metadata,
141        ):
142            # TODO: activate when metadata is available
143            # self.init_metadata("metadata", {
144            #     **AGGREGATOR_METADATA,
145            #     "views": [self.listActiveProposals]
146            # })
147
148            self.data.target = None
149            self.data.quorum = quorum
150            self.data.timeout = timeout
151            self.data.lastVoterId = lastVoterId
152            self.data.lastVoteTimestamp = sp.timestamp(0)
153            self.data.nbVoters = sp.len(voters)
154            self.data.voters = sp.big_map()
155            self.data.addrVoterId = addrVoterId
156            self.data.keyVoterId = keyVoterId
157            self.data.proposals = sp.big_map()
158            self.data.metadata = metadata
159
160            for voter in voters.items():
161                self.data.voters[voter.key] = voter.value
162
163            sp.cast(
164                self.data,
165                sp.record(
166                    target=sp.option[sp.address],
167                    quorum=sp.nat,
168                    timeout=sp.int,
169                    lastVoterId=sp.nat,
170                    lastVoteTimestamp=sp.timestamp,
171                    nbVoters=sp.nat,
172                    voters=sp.big_map[
173                        sp.nat,
174                        sp.record(
175                            publicKey=sp.key, addr=sp.address, lastProposalId=sp.nat
176                        ),
177                    ],
178                    addrVoterId=sp.big_map[sp.address, sp.nat],
179                    keyVoterId=sp.big_map[sp.key, sp.nat],
180                    proposals=sp.big_map[sp.nat, t_Proposal],
181                    metadata=sp.big_map[sp.string, sp.bytes],
182                ),
183            )
184
185        # ----------------
186        # Public entrypoints
187
188        @sp.entrypoint
189        def newProposal(self, batchs):
190            voterId = self.data.addrVoterId.get(
191                sp.sender, error=MultisignAdmin_VoterUnknown
192            )
193            self.data.voters[voterId].lastProposalId += 1
194            self.data.proposals[voterId] = sp.record(
195                startedAt=sp.now,
196                id=self.data.voters[voterId].lastProposalId,
197                yay={voterId},
198                nay=set(),
199                batchs=batchs,
200                canceled=False,
201            )
202            if self.data.quorum < 2:
203                self.onVoted(self.data.proposals[voterId])
204
205        @sp.entrypoint
206        def vote(self, votes):
207            voterId = self.data.addrVoterId.get(
208                sp.sender, error=MultisignAdmin_VoterUnknown
209            )
210            for vote in votes:
211                self.registerVote(
212                    sp.record(
213                        initiatorId=vote.initiatorId,
214                        proposalId=vote.proposalId,
215                        voterId=voterId,
216                        yay=vote.yay,
217                    )
218                )
219                proposal = self.data.proposals[vote.initiatorId]
220                if sp.len(proposal.nay) + sp.len(proposal.yay) >= self.data.quorum:
221                    if sp.len(proposal.nay) >= sp.len(proposal.yay):
222                        self.data.proposals[vote.initiatorId].canceled = True
223                    else:
224                        self.onVoted(proposal)
225
226        @sp.entrypoint
227        def multiVote(self, proposalsVotes):
228            """
229            Signed: Pair(ADDRESS multisignAdmin, Pair(initiatorId, proposalId))
230            """
231            sp.cast(proposalsVotes, t_proposalsVotes)
232            for proposalVotes in proposalsVotes:
233                initiatorId = proposalVotes.initiatorId
234                proposalId = proposalVotes.proposalId
235                for vote in proposalVotes.votes:
236                    signed = sp.pack((sp.self_address(), (initiatorId, proposalId)))
237                    publicKey = self.data.voters.get(
238                        vote.voterId, error=MultisignAdmin_VoterUnknown
239                    ).publicKey
240                    assert sp.check_signature(
241                        publicKey, vote.signature, signed
242                    ), MultisignAdmin_Badsig
243                    self.registerVote(
244                        sp.record(
245                            initiatorId=initiatorId,
246                            proposalId=proposalId,
247                            voterId=vote.voterId,
248                            yay=vote.yay,
249                        )
250                    )
251                proposal = self.data.proposals[initiatorId]
252                if sp.len(proposal.nay) + sp.len(proposal.yay) >= self.data.quorum:
253                    if sp.len(proposal.nay) >= sp.len(proposal.yay):
254                        self.data.proposals[initiatorId].canceled = True
255                    else:
256                        self.onVoted(proposal)
257
258        @sp.entrypoint
259        def cancelProposal(self, proposalId):
260            voterId = self.data.addrVoterId.get(
261                sp.sender, error=MultisignAdmin_VoterUnknown
262            )
263            proposal = self.data.proposals.get(
264                voterId, error=MultisignAdmin_ProposalUnknown
265            )
266            assert proposal.id == proposalId, MultisignAdmin_ProposalUnknown
267            self.data.proposals[voterId].canceled = True
268
269        # ----------------
270        # Public views
271        @sp.entrypoint
272        def getParams(self, callback):
273            params = sp.record(
274                target=self.data.target,
275                quorum=self.data.quorum,
276                timeout=self.data.timeout,
277            )
278            sp.transfer(params, sp.tez(0), callback)
279
280        @sp.entrypoint
281        def getLastProposal(self, params):
282            proposal = self.data.proposals[sp.fst(params)]
283            callback = sp.contract(t_Proposal, sp.snd(params))
284            sp.transfer(
285                proposal,
286                sp.tez(0),
287                callback.unwrap_some(error=MultisignAdmin_WrongCallbackInterface),
288            )
289
290        # ----------------
291        # Offchain views
292
293        @sp.offchain_view
294        def listActiveProposals(self):
295            """List active proposals"""
296            proposals = self.data.proposals
297            response = sp.cast([], sp.list[t_activeProposal])
298
299            for voterId in range(0, self.data.nbVoters, 1):
300                if proposals.contains(voterId):
301                    proposal = proposals[voterId]
302                    if (
303                        # not canceled
304                        not proposal.canceled
305                        # not timedout
306                        and sp.now
307                        < sp.add_seconds(proposal.startedAt, self.data.timeout * 60)
308                        # not already voted
309                        and (sp.len(proposal.nay) + sp.len(proposal.yay))
310                        < self.data.quorum
311                        # not closed by other vote
312                        and proposal.startedAt > self.data.lastVoteTimestamp
313                    ):
314                        response.push(
315                            sp.record(
316                                startedAt=proposal.startedAt,
317                                initiatorId=voterId,
318                                initiatorProposalId=proposal.id,
319                                yay=proposal.yay,
320                                nay=proposal.nay,
321                                batchs=proposal.batchs,
322                            )
323                        )
324
325            return response
326
327        # ----------------
328        # Private functions
329
330        @sp.private(with_storage="read-write")
331        def registerVote(self, params):
332            proposal = self.data.proposals.get(
333                params.initiatorId, error=MultisignAdmin_ProposalUnknown
334            )
335            assert proposal.id == params.proposalId, MultisignAdmin_ProposalUnknown
336            assert (
337                proposal.startedAt > self.data.lastVoteTimestamp
338                and not proposal.canceled
339            ), MultisignAdmin_ProposalClosed
340            assert sp.now < sp.add_seconds(
341                proposal.startedAt, self.data.timeout * 60
342            ), MultisignAdmin_ProposalTimedout
343            assert not proposal.yay.contains(
344                params.voterId
345            ) and not proposal.nay.contains(params.voterId), MultisignAdmin_AlreadyVoted
346            if params.yay:
347                proposal.yay.add(params.voterId)
348            else:
349                proposal.nay.add(params.voterId)
350            self.data.proposals[params.initiatorId] = proposal
351
352        @sp.private(with_storage="read-write", with_operations=True)
353        def onVoted(self, proposal):
354            self.data.lastVoteTimestamp = sp.now
355            for batch in proposal.batchs:
356                with sp.match(batch):
357                    with sp.case.selfAdmin as selfAdminActions:
358                        for action in selfAdminActions:
359                            with sp.match(action):
360                                with sp.case.changeQuorum as quorum:
361                                    assert (
362                                        quorum <= self.data.nbVoters
363                                    ), MultisignAdmin_MoreQuorumThanVoters
364                                    self.data.quorum = quorum
365                                with sp.case.changeTarget as target:
366                                    self.data.target = sp.Some(target)
367                                with sp.case.changeTimeout as timeout:
368                                    self.data.timeout = timeout
369                                with sp.case.changeVoters as changeVoters:
370                                    for voterAddress in changeVoters.removed.elements():
371                                        voterId = self.data.addrVoterId.get(
372                                            voterAddress,
373                                            error=MultisignAdmin_VoterUnknown,
374                                        )
375                                        del self.data.addrVoterId[voterAddress]
376                                        del self.data.keyVoterId[
377                                            self.data.voters[voterId].publicKey
378                                        ]
379                                        del self.data.proposals[voterId]
380                                        del self.data.voters[voterId]
381                                    self.data.nbVoters = sp.as_nat(
382                                        self.data.nbVoters
383                                        - sp.len(changeVoters.removed)
384                                    )
385                                    for voter in changeVoters.added:
386                                        assert not self.data.addrVoterId.contains(
387                                            voter.addr
388                                        ) and not self.data.keyVoterId.contains(
389                                            voter.publicKey
390                                        ), MultisignAdmin_VoterAlreadyknown
391                                        self.data.lastVoterId += 1
392                                        voterId = self.data.lastVoterId
393                                        self.data.voters[voterId] = sp.record(
394                                            publicKey=voter.publicKey,
395                                            addr=voter.addr,
396                                            lastProposalId=0,
397                                        )
398                                        self.data.addrVoterId[voter.addr] = voterId
399                                        self.data.keyVoterId[voter.publicKey] = voterId
400                                    self.data.nbVoters += sp.len(changeVoters.added)
401                                    assert (
402                                        self.data.nbVoters > 0
403                                    ), MultisignAdmin_VotersLessThan1
404                                    assert (
405                                        self.data.quorum <= self.data.nbVoters
406                                    ), MultisignAdmin_MoreQuorumThanVoters
407                    with sp.case.targetAdmin as targetActions:
408                        target = self.data.target.unwrap_some(
409                            error=MultisignAdmin_TargetNotSet
410                        )
411                        target_contract = sp.contract(
412                            sp.list[t_targetAdminAction], target
413                        ).unwrap_some(error=MultisignAdmin_TargetUnkown)
414                        sp.transfer(targetActions, sp.tez(0), target_contract)
415
416    ################
417    # Test contract
418
419    class Administrated(sp.Contract):
420        """
421        This contract is a sample
422        It shouws how a contract can be administrated
423        through the multisign administration contract
424        """
425
426        def __init__(self, admin, active):
427            self.data.admin = admin
428            self.data.active = active
429            self.data.value = None
430
431            sp.cast(
432                self.data,
433                sp.record(
434                    admin=sp.address,
435                    active=sp.bool,
436                    value=sp.option[sp.int],
437                ),
438            )
439
440        @sp.entrypoint
441        def administrate(self, actions):
442            assert sp.sender == self.data.admin, "NOT ADMIN"
443            sp.cast(actions, sp.list[t_targetAdminAction])
444            for action in actions:
445                with sp.match(action):
446                    with sp.case.changeActive as active:
447                        self.data.active = active
448                    with sp.case.changeAdmin as admin:
449                        self.data.admin = admin
450
451        @sp.entrypoint
452        def setValue(self, value):
453            assert self.data.active, "NOT ACTIVE"
454            self.data.value = value
455
456
457#########
458# Helpers
459
460
461class SelfHelper:
462    def variant(content):
463        return sp.variant.selfAdmin(content)
464
465    def changeQuorum(quorum):
466        return SelfHelper.variant([sp.variant.changeQuorum(quorum)])
467
468    def changeAdmin(admin):
469        return SelfHelper.variant([sp.variant.changeAdmin(admin)])
470
471    def changeTarget(target):
472        return SelfHelper.variant([sp.variant.changeTarget(target)])
473
474    def changeTimeout(timeout):
475        return SelfHelper.variant([sp.variant.changeTimeout(timeout)])
476
477    def changeVoters(removed=[], added=[]):
478        added_list = []
479        for added_info in added:
480            addr, publicKey = added_info
481            added_list.append(sp.record(addr=addr, publicKey=publicKey))
482        return SelfHelper.variant(
483            [
484                sp.variant(
485                    "changeVoters",
486                    sp.record(removed=sp.set(removed), added=sp.list(added_list)),
487                )
488            ]
489        )
490
491
492def sign(account, contract, voterId, initiatorId, proposalId, yay):
493    message = sp.pack(sp.pair(contract.address, sp.pair(initiatorId, proposalId)))
494    signature = sp.make_signature(account.secret_key, message, message_format="Raw")
495    vote = sp.record(voterId=voterId, signature=signature, yay=yay)
496    return vote
497
498
499def vote(contract, signer, yay):
500    """
501    Only use for test as you can't know if the proposalId correspond to
502    the one you want to vote
503    """
504    voterId = contract.data.addrVoterId[signer.address]
505    proposal = contract.data.proposals[voterId]
506    return sp.record(proposalId=proposal.id, initiatorId=voterId, yay=yay)
507
508
509################
510# Tests
511
512
513def add_test(name):
514    @sp.add_test()
515    def test():
516        sc = sp.test_scenario(name, m2)
517        sc.h1(name)
518
519        admin = sp.test_account("admin")
520        signer1 = sp.test_account("signer1")
521        signer2 = sp.test_account("signer2")
522        signer3 = sp.test_account("signer3")
523        signer4 = sp.test_account("signer4")
524
525        sc.h2("Init contracts")
526
527        sc.h3("Administrated")
528        administrated = m2.Administrated(admin.address, False)
529        sc += administrated
530        administrated_entrypoint = sp.to_address(
531            sp.contract(
532                m2.t_targetAdminAction, administrated.address, entrypoint="administrate"
533            ).unwrap_some()
534        )
535
536        sc.h3("multisignAdmin")
537        multisignAdmin = m2.MultisignAdmin(
538            quorum=1,
539            timeout=5,
540            addrVoterId=sp.big_map({signer1.address: 0, signer2.address: 1}),
541            keyVoterId=sp.big_map({signer1.public_key: 0, signer2.public_key: 1}),
542            voters={
543                0: sp.record(
544                    addr=signer1.address, publicKey=signer1.public_key, lastProposalId=0
545                ),
546                1: sp.record(
547                    addr=signer2.address, publicKey=signer2.public_key, lastProposalId=0
548                ),
549            },
550            lastVoterId=1,
551            metadata=sp.scenario_utils.metadata_of_url(
552                "ipfs://QmYnU2jwq4XU4jwX1Rmu1EGfmyhMhpDCpPpHF5j9jF9y1w"
553            ),
554        )
555        sc += multisignAdmin
556        now = sp.timestamp(1)
557
558        if name == "Self Administration tests":
559            ##########################
560            # Auto-accepted proposal #
561            ##########################
562            sc.h2("Auto-accepted proposal when quorum is 1")
563            sc.h3("signer1 propose to change quorum to 2")
564            sc.verify(multisignAdmin.data.quorum == 1)
565            changeQuorum = SelfHelper.changeQuorum(2)
566            multisignAdmin.newProposal([changeQuorum], _sender=signer1, _now=now)
567            now = now.add_seconds(1)
568            sc.verify(multisignAdmin.data.quorum == 2)
569
570            ##################
571            # Invalid quorum #
572            ##################
573            sc.h2("Invalid quorum proposal")
574            sc.h3("signer1 new proposal to change quorum to 3")
575            sc.verify(multisignAdmin.data.quorum != 3)
576            changeQuorum = SelfHelper.changeQuorum(3)
577            multisignAdmin.newProposal([changeQuorum], _sender=signer1, _now=now)
578            # Proposal has not been validated yet
579            sc.verify(multisignAdmin.data.quorum == 2)
580            sc.h3("signer2 votes the proposal")
581            sc.p("proposal is rejected because nbSigners < proposed quorum")
582            multisignAdmin.vote(
583                [vote(multisignAdmin, signer1, yay=True)], _sender=signer2, _valid=False
584            )
585            sc.verify(multisignAdmin.data.quorum != 3)
586
587            ##############
588            # Add signer #
589            ##############
590            sc.h2("Adding new voters")
591            sc.h3("signer2 new proposal to include signer3")
592            sc.verify(~multisignAdmin.data.voters.contains(2))
593            changeVoters = SelfHelper.changeVoters(
594                [], added=[(signer3.address, signer3.public_key)]
595            )
596            multisignAdmin.newProposal([changeVoters], _sender=signer2, _now=now)
597            sc.h3("signer1 votes the proposal")
598            multisignAdmin.vote(
599                [vote(multisignAdmin, signer2, yay=True)], _sender=signer1
600            )
601            sc.verify(multisignAdmin.data.voters.contains(2))
602            now = now.add_seconds(1)
603
604            #########################
605            # Newly included signer #
606            #########################
607            sc.h2("Newly included signer starts a proposal")
608            sc.h3("New proposal by signer 3 to increase quorum to 3")
609            sc.verify(multisignAdmin.data.quorum != 3)
610            changeQuorum = SelfHelper.changeQuorum(3)
611            multisignAdmin.newProposal([changeQuorum], _sender=signer3, _now=now)
612            sc.h3("signer1 votes the proposal")
613            multisignAdmin.vote(
614                [vote(multisignAdmin, signer3, yay=True)], _sender=signer1
615            )
616            sc.verify(multisignAdmin.data.quorum == 3)
617            now = now.add_seconds(1)
618
619            ##########
620            # Cancel #
621            ##########
622            sc.h2("Proposal cancellation")
623            sc.h3("New proposal")
624            sc.verify(multisignAdmin.data.timeout != 10)
625            changeTimeout = SelfHelper.changeTimeout(10)
626            multisignAdmin.newProposal([changeTimeout], _sender=signer1, _now=now)
627            sc.h3("Signer 2 tries to cancel the proposal")
628            voterId = multisignAdmin.data.addrVoterId[signer1.address]
629            proposalId = multisignAdmin.data.proposals[voterId].id
630            multisignAdmin.cancelProposal(proposalId, _sender=signer2, _valid=False)
631            sc.h3("Signer 1 cancels the proposal")
632            multisignAdmin.cancelProposal(proposalId, _sender=signer1)
633            sc.h3("Signer 2 tries to vote the canceled proposal")
634            multisignAdmin.vote(
635                [vote(multisignAdmin, signer1, yay=True)], _sender=signer2, _valid=False
636            )
637            sc.verify(multisignAdmin.data.timeout != 10)
638
639            ############
640            # Rejected #
641            ############
642            sc.h2("Proposal rejection")
643            sc.h3("New proposal")
644            sc.verify(multisignAdmin.data.timeout != 10)
645            changeTimeout = SelfHelper.changeTimeout(10)
646            multisignAdmin.newProposal([changeTimeout], _sender=signer1, _now=now)
647            sc.h3("Signer 2 votes against the proposal")
648            multisignAdmin.vote(
649                [vote(multisignAdmin, signer1, yay=False)], _sender=signer2
650            )
651            sc.h3("Signer 3 votes against the proposal")
652            sc.verify(multisignAdmin.data.proposals[voterId].canceled == False)
653            multisignAdmin.vote(
654                [vote(multisignAdmin, signer1, yay=False)], _sender=signer3
655            )
656            voterId = multisignAdmin.data.addrVoterId[signer1.address]
657            sc.verify(multisignAdmin.data.proposals[voterId].canceled == True)
658            sc.verify(multisignAdmin.data.timeout != 10)
659
660            ######################
661            # Remove signer fail #
662            ######################
663            sc.h2("Invalid Removed signer proposal")
664            sc.h3("Signer 1 new proposal: remove signer 3")
665            changeVoters = SelfHelper.changeVoters(removed=[signer3.address])
666            multisignAdmin.newProposal([changeVoters], _sender=signer1, _now=now)
667            sc.h3("Signer 2 votes the remove proposal")
668            multisignAdmin.vote(
669                [vote(multisignAdmin, signer1, yay=True)], _sender=signer2
670            )
671            sc.h3("Signer 3 tries to vote the remove proposal")
672            sc.p("Fails because quorum would be > number of signers")
673            multisignAdmin.vote(
674                [vote(multisignAdmin, signer1, yay=True)], _sender=signer3, _valid=False
675            )
676            sc.verify(multisignAdmin.data.voters.contains(2))
677
678            ######################
679            # 2 actions proposal #
680            ######################
681            sc.h2("2 actions proposal")
682            sc.h3("Signer 1 new proposal: change quorum to 2 and remove signer 3")
683            sc.verify(multisignAdmin.data.quorum == 3)
684            sc.verify(multisignAdmin.data.voters.contains(2))
685            changeQuorum = SelfHelper.changeQuorum(2)
686            changeVoters = SelfHelper.changeVoters(removed=[signer3.address])
687            multisignAdmin.newProposal(
688                [changeQuorum, changeVoters], _sender=signer1, _now=now
689            )
690            sc.h3("Signer 2 votes the proposal")
691            multisignAdmin.vote(
692                [vote(multisignAdmin, signer1, yay=True)], _sender=signer2
693            )
694            sc.h3("Signer 3 votes the proposal")
695            multisignAdmin.vote(
696                [vote(multisignAdmin, signer1, yay=True)], _sender=signer3
697            )
698            now = now.add_seconds(1)
699            sc.verify(multisignAdmin.data.quorum == 2)
700            sc.verify(~multisignAdmin.data.voters.contains(2))
701
702            ###########################
703            # Votes for past proposal #
704            ###########################
705            sc.h2("Vote for past proposal")
706            sc.h3("Signer 1 new proposal: change timeout to 2")
707            changeTimeout = SelfHelper.changeTimeout(2)
708            multisignAdmin.newProposal([changeTimeout], _sender=signer1, _now=now)
709            sc.h3("Signer 2 new proposal: add new signer")
710            sc.verify(multisignAdmin.data.timeout != 3)
711            changeVoters = SelfHelper.changeVoters(
712                [], added=[(signer4.address, signer4.public_key)]
713            )
714            multisignAdmin.newProposal([changeVoters], _sender=signer2)
715            now = now.add_seconds(1)
716            sc.h3("Signer 2 tries to vote signer1's proposal after timedout")
717            multisignAdmin.vote(
718                [vote(multisignAdmin, signer1, yay=True)],
719                _sender=signer2,
720                _now=now.add_minutes(100),
721                _valid=False,
722            )
723            sc.h3("Signer 1 votes for signer2's proposal")
724            multisignAdmin.vote(
725                [vote(multisignAdmin, signer2, yay=True)], _sender=signer1, _now=now
726            )
727            sc.h3(
728                "Signer 2 tries to vote signer1's proposal while a more recent one was accepted"
729            )
730            multisignAdmin.vote(
731                [vote(multisignAdmin, signer1, yay=True)],
732                _sender=signer2,
733                _now=now.add_seconds(1),
734                _valid=False,
735            )
736            now = now.add_seconds(1)
737
738            ##############
739            # Multivotes #
740            ##############
741            sc.h2("Multi vote in one call")
742            sc.h3("Signer 1 new proposal")
743            changeTimeout = SelfHelper.changeTimeout(2)
744            multisignAdmin.newProposal([changeTimeout], _sender=signer1, _now=now)
745            now = now.add_seconds(1)
746            sc.h3("Signer 2 and Signer 3 votes are pushed by Signer 1")
747            signer2_vote = sign(
748                signer2,
749                voterId=1,
750                contract=multisignAdmin,
751                initiatorId=0,
752                proposalId=proposalId,
753                yay=True,
754            )
755            signer4_vote = sign(
756                signer4,
757                voterId=3,
758                contract=multisignAdmin,
759                initiatorId=0,
760                proposalId=proposalId,
761                yay=True,
762            )
763            proposalVotes = sp.record(
764                initiatorId=0, proposalId=proposalId, votes=[signer2_vote, signer4_vote]
765            )
766            multisignAdmin.multiVote([proposalVotes], _sender=signer1, _now=now)
767            sc.verify(multisignAdmin.data.timeout == 2)
768            now = now.add_seconds(1)
769
770        ##########################################
771
772        else:
773            #########################
774            # Target Administration #
775            #########################
776            now = sp.timestamp(2)
777            sc.h2("Set multisignAdmin as administrated's admin")
778            administrated.administrate(
779                [sp.variant.changeAdmin(multisignAdmin.address)],
780                _sender=admin,
781                _now=now,
782            )
783
784            sc.h2("Use multisignadmin to set a target")
785            sc.h3("Signer 1 new proposal: changeTarget")
786            now = now.add_seconds(10)
787            targetAddress = sp.contract(
788                sp.list[m2.t_targetAdminAction],
789                administrated.address,
790                entrypoint="administrate",
791            ).unwrap_some()
792            multisignAdmin.newProposal(
793                [SelfHelper.changeTarget(sp.to_address(targetAddress))],
794                _sender=signer1,
795                _now=now,
796            )
797
798            sc.h2("Use multisignadmin to administrate target")
799            sc.h3("Signer 1 new proposal: changeActive = True on target")
800            sc.verify(~administrated.data.active)
801            now = now.add_seconds(12)
802            changeTargetActive = sp.variant(
803                "targetAdmin", [sp.variant.changeActive(True)]
804            )
805            multisignAdmin.newProposal([changeTargetActive], _sender=signer1, _now=now)
806            sc.verify(administrated.data.active)
807
808
809if "main" in __name__:
810    add_test("Self Administration tests")
811    add_test("Target Administration tests")
class SelfHelper:
462class SelfHelper:
463    def variant(content):
464        return sp.variant.selfAdmin(content)
465
466    def changeQuorum(quorum):
467        return SelfHelper.variant([sp.variant.changeQuorum(quorum)])
468
469    def changeAdmin(admin):
470        return SelfHelper.variant([sp.variant.changeAdmin(admin)])
471
472    def changeTarget(target):
473        return SelfHelper.variant([sp.variant.changeTarget(target)])
474
475    def changeTimeout(timeout):
476        return SelfHelper.variant([sp.variant.changeTimeout(timeout)])
477
478    def changeVoters(removed=[], added=[]):
479        added_list = []
480        for added_info in added:
481            addr, publicKey = added_info
482            added_list.append(sp.record(addr=addr, publicKey=publicKey))
483        return SelfHelper.variant(
484            [
485                sp.variant(
486                    "changeVoters",
487                    sp.record(removed=sp.set(removed), added=sp.list(added_list)),
488                )
489            ]
490        )
def variant(content):
463    def variant(content):
464        return sp.variant.selfAdmin(content)
def changeQuorum(quorum):
466    def changeQuorum(quorum):
467        return SelfHelper.variant([sp.variant.changeQuorum(quorum)])
def changeAdmin(admin):
469    def changeAdmin(admin):
470        return SelfHelper.variant([sp.variant.changeAdmin(admin)])
def changeTarget(target):
472    def changeTarget(target):
473        return SelfHelper.variant([sp.variant.changeTarget(target)])
def changeTimeout(timeout):
475    def changeTimeout(timeout):
476        return SelfHelper.variant([sp.variant.changeTimeout(timeout)])
def changeVoters(removed=[], added=[]):
478    def changeVoters(removed=[], added=[]):
479        added_list = []
480        for added_info in added:
481            addr, publicKey = added_info
482            added_list.append(sp.record(addr=addr, publicKey=publicKey))
483        return SelfHelper.variant(
484            [
485                sp.variant(
486                    "changeVoters",
487                    sp.record(removed=sp.set(removed), added=sp.list(added_list)),
488                )
489            ]
490        )
def sign(account, contract, voterId, initiatorId, proposalId, yay):
493def sign(account, contract, voterId, initiatorId, proposalId, yay):
494    message = sp.pack(sp.pair(contract.address, sp.pair(initiatorId, proposalId)))
495    signature = sp.make_signature(account.secret_key, message, message_format="Raw")
496    vote = sp.record(voterId=voterId, signature=signature, yay=yay)
497    return vote
def vote(contract, signer, yay):
500def vote(contract, signer, yay):
501    """
502    Only use for test as you can't know if the proposalId correspond to
503    the one you want to vote
504    """
505    voterId = contract.data.addrVoterId[signer.address]
506    proposal = contract.data.proposals[voterId]
507    return sp.record(proposalId=proposal.id, initiatorId=voterId, yay=yay)

Only use for test as you can't know if the proposalId correspond to the one you want to vote

def add_test(name):
514def add_test(name):
515    @sp.add_test()
516    def test():
517        sc = sp.test_scenario(name, m2)
518        sc.h1(name)
519
520        admin = sp.test_account("admin")
521        signer1 = sp.test_account("signer1")
522        signer2 = sp.test_account("signer2")
523        signer3 = sp.test_account("signer3")
524        signer4 = sp.test_account("signer4")
525
526        sc.h2("Init contracts")
527
528        sc.h3("Administrated")
529        administrated = m2.Administrated(admin.address, False)
530        sc += administrated
531        administrated_entrypoint = sp.to_address(
532            sp.contract(
533                m2.t_targetAdminAction, administrated.address, entrypoint="administrate"
534            ).unwrap_some()
535        )
536
537        sc.h3("multisignAdmin")
538        multisignAdmin = m2.MultisignAdmin(
539            quorum=1,
540            timeout=5,
541            addrVoterId=sp.big_map({signer1.address: 0, signer2.address: 1}),
542            keyVoterId=sp.big_map({signer1.public_key: 0, signer2.public_key: 1}),
543            voters={
544                0: sp.record(
545                    addr=signer1.address, publicKey=signer1.public_key, lastProposalId=0
546                ),
547                1: sp.record(
548                    addr=signer2.address, publicKey=signer2.public_key, lastProposalId=0
549                ),
550            },
551            lastVoterId=1,
552            metadata=sp.scenario_utils.metadata_of_url(
553                "ipfs://QmYnU2jwq4XU4jwX1Rmu1EGfmyhMhpDCpPpHF5j9jF9y1w"
554            ),
555        )
556        sc += multisignAdmin
557        now = sp.timestamp(1)
558
559        if name == "Self Administration tests":
560            ##########################
561            # Auto-accepted proposal #
562            ##########################
563            sc.h2("Auto-accepted proposal when quorum is 1")
564            sc.h3("signer1 propose to change quorum to 2")
565            sc.verify(multisignAdmin.data.quorum == 1)
566            changeQuorum = SelfHelper.changeQuorum(2)
567            multisignAdmin.newProposal([changeQuorum], _sender=signer1, _now=now)
568            now = now.add_seconds(1)
569            sc.verify(multisignAdmin.data.quorum == 2)
570
571            ##################
572            # Invalid quorum #
573            ##################
574            sc.h2("Invalid quorum proposal")
575            sc.h3("signer1 new proposal to change quorum to 3")
576            sc.verify(multisignAdmin.data.quorum != 3)
577            changeQuorum = SelfHelper.changeQuorum(3)
578            multisignAdmin.newProposal([changeQuorum], _sender=signer1, _now=now)
579            # Proposal has not been validated yet
580            sc.verify(multisignAdmin.data.quorum == 2)
581            sc.h3("signer2 votes the proposal")
582            sc.p("proposal is rejected because nbSigners < proposed quorum")
583            multisignAdmin.vote(
584                [vote(multisignAdmin, signer1, yay=True)], _sender=signer2, _valid=False
585            )
586            sc.verify(multisignAdmin.data.quorum != 3)
587
588            ##############
589            # Add signer #
590            ##############
591            sc.h2("Adding new voters")
592            sc.h3("signer2 new proposal to include signer3")
593            sc.verify(~multisignAdmin.data.voters.contains(2))
594            changeVoters = SelfHelper.changeVoters(
595                [], added=[(signer3.address, signer3.public_key)]
596            )
597            multisignAdmin.newProposal([changeVoters], _sender=signer2, _now=now)
598            sc.h3("signer1 votes the proposal")
599            multisignAdmin.vote(
600                [vote(multisignAdmin, signer2, yay=True)], _sender=signer1
601            )
602            sc.verify(multisignAdmin.data.voters.contains(2))
603            now = now.add_seconds(1)
604
605            #########################
606            # Newly included signer #
607            #########################
608            sc.h2("Newly included signer starts a proposal")
609            sc.h3("New proposal by signer 3 to increase quorum to 3")
610            sc.verify(multisignAdmin.data.quorum != 3)
611            changeQuorum = SelfHelper.changeQuorum(3)
612            multisignAdmin.newProposal([changeQuorum], _sender=signer3, _now=now)
613            sc.h3("signer1 votes the proposal")
614            multisignAdmin.vote(
615                [vote(multisignAdmin, signer3, yay=True)], _sender=signer1
616            )
617            sc.verify(multisignAdmin.data.quorum == 3)
618            now = now.add_seconds(1)
619
620            ##########
621            # Cancel #
622            ##########
623            sc.h2("Proposal cancellation")
624            sc.h3("New proposal")
625            sc.verify(multisignAdmin.data.timeout != 10)
626            changeTimeout = SelfHelper.changeTimeout(10)
627            multisignAdmin.newProposal([changeTimeout], _sender=signer1, _now=now)
628            sc.h3("Signer 2 tries to cancel the proposal")
629            voterId = multisignAdmin.data.addrVoterId[signer1.address]
630            proposalId = multisignAdmin.data.proposals[voterId].id
631            multisignAdmin.cancelProposal(proposalId, _sender=signer2, _valid=False)
632            sc.h3("Signer 1 cancels the proposal")
633            multisignAdmin.cancelProposal(proposalId, _sender=signer1)
634            sc.h3("Signer 2 tries to vote the canceled proposal")
635            multisignAdmin.vote(
636                [vote(multisignAdmin, signer1, yay=True)], _sender=signer2, _valid=False
637            )
638            sc.verify(multisignAdmin.data.timeout != 10)
639
640            ############
641            # Rejected #
642            ############
643            sc.h2("Proposal rejection")
644            sc.h3("New proposal")
645            sc.verify(multisignAdmin.data.timeout != 10)
646            changeTimeout = SelfHelper.changeTimeout(10)
647            multisignAdmin.newProposal([changeTimeout], _sender=signer1, _now=now)
648            sc.h3("Signer 2 votes against the proposal")
649            multisignAdmin.vote(
650                [vote(multisignAdmin, signer1, yay=False)], _sender=signer2
651            )
652            sc.h3("Signer 3 votes against the proposal")
653            sc.verify(multisignAdmin.data.proposals[voterId].canceled == False)
654            multisignAdmin.vote(
655                [vote(multisignAdmin, signer1, yay=False)], _sender=signer3
656            )
657            voterId = multisignAdmin.data.addrVoterId[signer1.address]
658            sc.verify(multisignAdmin.data.proposals[voterId].canceled == True)
659            sc.verify(multisignAdmin.data.timeout != 10)
660
661            ######################
662            # Remove signer fail #
663            ######################
664            sc.h2("Invalid Removed signer proposal")
665            sc.h3("Signer 1 new proposal: remove signer 3")
666            changeVoters = SelfHelper.changeVoters(removed=[signer3.address])
667            multisignAdmin.newProposal([changeVoters], _sender=signer1, _now=now)
668            sc.h3("Signer 2 votes the remove proposal")
669            multisignAdmin.vote(
670                [vote(multisignAdmin, signer1, yay=True)], _sender=signer2
671            )
672            sc.h3("Signer 3 tries to vote the remove proposal")
673            sc.p("Fails because quorum would be > number of signers")
674            multisignAdmin.vote(
675                [vote(multisignAdmin, signer1, yay=True)], _sender=signer3, _valid=False
676            )
677            sc.verify(multisignAdmin.data.voters.contains(2))
678
679            ######################
680            # 2 actions proposal #
681            ######################
682            sc.h2("2 actions proposal")
683            sc.h3("Signer 1 new proposal: change quorum to 2 and remove signer 3")
684            sc.verify(multisignAdmin.data.quorum == 3)
685            sc.verify(multisignAdmin.data.voters.contains(2))
686            changeQuorum = SelfHelper.changeQuorum(2)
687            changeVoters = SelfHelper.changeVoters(removed=[signer3.address])
688            multisignAdmin.newProposal(
689                [changeQuorum, changeVoters], _sender=signer1, _now=now
690            )
691            sc.h3("Signer 2 votes the proposal")
692            multisignAdmin.vote(
693                [vote(multisignAdmin, signer1, yay=True)], _sender=signer2
694            )
695            sc.h3("Signer 3 votes the proposal")
696            multisignAdmin.vote(
697                [vote(multisignAdmin, signer1, yay=True)], _sender=signer3
698            )
699            now = now.add_seconds(1)
700            sc.verify(multisignAdmin.data.quorum == 2)
701            sc.verify(~multisignAdmin.data.voters.contains(2))
702
703            ###########################
704            # Votes for past proposal #
705            ###########################
706            sc.h2("Vote for past proposal")
707            sc.h3("Signer 1 new proposal: change timeout to 2")
708            changeTimeout = SelfHelper.changeTimeout(2)
709            multisignAdmin.newProposal([changeTimeout], _sender=signer1, _now=now)
710            sc.h3("Signer 2 new proposal: add new signer")
711            sc.verify(multisignAdmin.data.timeout != 3)
712            changeVoters = SelfHelper.changeVoters(
713                [], added=[(signer4.address, signer4.public_key)]
714            )
715            multisignAdmin.newProposal([changeVoters], _sender=signer2)
716            now = now.add_seconds(1)
717            sc.h3("Signer 2 tries to vote signer1's proposal after timedout")
718            multisignAdmin.vote(
719                [vote(multisignAdmin, signer1, yay=True)],
720                _sender=signer2,
721                _now=now.add_minutes(100),
722                _valid=False,
723            )
724            sc.h3("Signer 1 votes for signer2's proposal")
725            multisignAdmin.vote(
726                [vote(multisignAdmin, signer2, yay=True)], _sender=signer1, _now=now
727            )
728            sc.h3(
729                "Signer 2 tries to vote signer1's proposal while a more recent one was accepted"
730            )
731            multisignAdmin.vote(
732                [vote(multisignAdmin, signer1, yay=True)],
733                _sender=signer2,
734                _now=now.add_seconds(1),
735                _valid=False,
736            )
737            now = now.add_seconds(1)
738
739            ##############
740            # Multivotes #
741            ##############
742            sc.h2("Multi vote in one call")
743            sc.h3("Signer 1 new proposal")
744            changeTimeout = SelfHelper.changeTimeout(2)
745            multisignAdmin.newProposal([changeTimeout], _sender=signer1, _now=now)
746            now = now.add_seconds(1)
747            sc.h3("Signer 2 and Signer 3 votes are pushed by Signer 1")
748            signer2_vote = sign(
749                signer2,
750                voterId=1,
751                contract=multisignAdmin,
752                initiatorId=0,
753                proposalId=proposalId,
754                yay=True,
755            )
756            signer4_vote = sign(
757                signer4,
758                voterId=3,
759                contract=multisignAdmin,
760                initiatorId=0,
761                proposalId=proposalId,
762                yay=True,
763            )
764            proposalVotes = sp.record(
765                initiatorId=0, proposalId=proposalId, votes=[signer2_vote, signer4_vote]
766            )
767            multisignAdmin.multiVote([proposalVotes], _sender=signer1, _now=now)
768            sc.verify(multisignAdmin.data.timeout == 2)
769            now = now.add_seconds(1)
770
771        ##########################################
772
773        else:
774            #########################
775            # Target Administration #
776            #########################
777            now = sp.timestamp(2)
778            sc.h2("Set multisignAdmin as administrated's admin")
779            administrated.administrate(
780                [sp.variant.changeAdmin(multisignAdmin.address)],
781                _sender=admin,
782                _now=now,
783            )
784
785            sc.h2("Use multisignadmin to set a target")
786            sc.h3("Signer 1 new proposal: changeTarget")
787            now = now.add_seconds(10)
788            targetAddress = sp.contract(
789                sp.list[m2.t_targetAdminAction],
790                administrated.address,
791                entrypoint="administrate",
792            ).unwrap_some()
793            multisignAdmin.newProposal(
794                [SelfHelper.changeTarget(sp.to_address(targetAddress))],
795                _sender=signer1,
796                _now=now,
797            )
798
799            sc.h2("Use multisignadmin to administrate target")
800            sc.h3("Signer 1 new proposal: changeActive = True on target")
801            sc.verify(~administrated.data.active)
802            now = now.add_seconds(12)
803            changeTargetActive = sp.variant(
804                "targetAdmin", [sp.variant.changeActive(True)]
805            )
806            multisignAdmin.newProposal([changeTargetActive], _sender=signer1, _now=now)
807            sc.verify(administrated.data.active)