templates.admin_multisig

  1import smartpy as sp
  2
  3"""
  4    A Multisig Contract used to administrate other contracts.
  5    THIS CONTRACT IS FOR ILLUSTRATIVE PURPOSE.
  6    IT HAS NOT BEEN AUDITED.
  7"""
  8
  9
 10@sp.module
 11def MS_TYPES():
 12    # Internal administration action type specification
 13    InternalAdminAction: type = sp.variant(
 14        changeSigners=sp.variant(
 15            removed=sp.set[sp.address],
 16            added=sp.list[sp.record(address=sp.address, publicKey=sp.key)],
 17        ),
 18        changeQuorum=sp.nat,
 19        changeMetadata=sp.pair[sp.string, sp.option[sp.bytes]],
 20    )
 21
 22    # External administration action type specification
 23    ExternalAdminAction: type = sp.record(target=sp.address, actions=sp.bytes)
 24
 25    # Proposal action type specification
 26    ProposalAction: type = sp.variant(
 27        internal=sp.list[InternalAdminAction], external=sp.list[ExternalAdminAction]
 28    )
 29
 30    # Proposal type specification
 31    Proposal: type = sp.record(
 32        startedAt=sp.timestamp,
 33        initiator=sp.address,
 34        endorsements=sp.set[sp.address],
 35        actions=ProposalAction,
 36    )
 37
 38    AggregatedProposalParams: type = sp.record(
 39        signatures=sp.list[sp.record(signerAddress=sp.address, signature=sp.signature)],
 40        proposalId=sp.nat,
 41        actions=ProposalAction,
 42    )
 43
 44    AggregatedEndorsementParams: type = sp.list[
 45        sp.record(
 46            signatures=sp.list[
 47                sp.record(signerAddress=sp.address, signature=sp.signature)
 48            ],
 49            proposalId=sp.nat,
 50        )
 51    ]
 52
 53
 54@sp.module
 55def ERR():
 56    Badsig = "MULTISIG_Badsig"
 57    ProposalUnknown = "MULTISIG_ProposalUnknown"
 58    NotInitiator = "MULTISIG_NotInitiator"
 59    SignerUnknown = "MULTISIG_SignerUnknown"
 60    InvalidTarget = "MULTISIG_InvalidTarget"
 61    MoreQuorumThanSigners = "MULTISIG_MoreQuorumThanSigners"
 62    InvalidProposalId = "MULTISIG_InvalidProposalId"
 63
 64
 65METADATA = {
 66    "name": "Generic Multisig Administrator",
 67    "version": "1",
 68    "description": "Generic Multisig Administrator",
 69    "source": {"tools": ["SmartPy"]},
 70    "interfaces": ["TZIP-016"],
 71}
 72
 73
 74@sp.module
 75def main():
 76    @sp.effects(with_storage="read-only")
 77    def failIfNotSigner(address):
 78        sp.cast(
 79            self.data.signers,
 80            sp.map[
 81                sp.address,
 82                sp.record(publicKey=sp.key, lastProposalId=sp.option[sp.nat]),
 83            ],
 84        )
 85        assert self.data.signers.contains(address), ERR.SignerUnknown
 86
 87    class MultisigAdmin(sp.Contract):
 88        def __init__(self, quorum, signers, metadata):
 89            # Metadata helper
 90            # self.init_metadata("metadata", METADATA)
 91
 92            self.data.quorum = quorum
 93            self.data.lastProposalId = 0
 94            self.data.signers = signers
 95            self.data.proposals = sp.big_map()
 96            self.data.activeProposals = set()
 97            self.data.metadata = metadata
 98
 99            sp.cast(
100                self.data,
101                sp.record(
102                    quorum=sp.nat,
103                    lastProposalId=sp.nat,
104                    signers=sp.map[
105                        sp.address,
106                        sp.record(publicKey=sp.key, lastProposalId=sp.option[sp.nat]),
107                    ],
108                    proposals=sp.big_map[sp.nat, MS_TYPES.Proposal],
109                    activeProposals=sp.set[sp.nat],
110                    metadata=sp.big_map[sp.string, sp.bytes],
111                ),
112            )
113
114        @sp.entrypoint
115        def proposal(self, actions):
116            """
117            Each user can have at most one proposal active at a time.
118            Submitting a new proposal overrides the previous one.
119            """
120            # Proposals can only be submitted by registered signers
121            failIfNotSigner(sp.sender)
122
123            # If the proposal initiator has an active proposal,
124            # then replace that proposal with the new one
125            signerLastProposalId = self.data.signers[sp.sender].lastProposalId
126            with sp.match(signerLastProposalId):
127                with sp.case.Some as id:
128                    self.data.activeProposals.remove(id)
129
130            # Increment proposal counter
131            self.data.lastProposalId += 1
132            proposalId = self.data.lastProposalId
133            # Store new proposal
134            self.data.activeProposals.add(proposalId)
135            self.data.proposals[proposalId] = sp.record(
136                startedAt=sp.now,
137                initiator=sp.sender,
138                endorsements={sp.sender},
139                actions=actions,
140            )
141            # Update signer's last proposal
142            self.data.signers[sp.sender].lastProposalId = sp.Some(proposalId)
143
144            # Approve the proposal if quorum only requires 1 vote
145            if self.data.quorum < 2:
146                self.onApproved(
147                    sp.record(
148                        proposalId=proposalId,
149                        actions=actions,
150                    )
151                )
152
153        @sp.entrypoint
154        def endorsement(self, endorsements):
155            """
156            Entrypoint used to submit endorsements to single/multiple proposals.
157            """
158            # Endorsements can only be submitted by registered signers
159            failIfNotSigner(sp.sender)
160
161            # Iterate over every endorsement
162            for pId in endorsements:
163                self.registerEndorsement(
164                    sp.record(proposalId=pId, signerAddress=sp.sender)
165                )
166
167                # Approve the proposal if quorum was reached
168                proposal = self.data.proposals[pId]
169                if sp.len(proposal.endorsements) >= self.data.quorum:
170                    self.onApproved(
171                        sp.record(
172                            proposalId=pId,
173                            actions=proposal.actions,
174                        )
175                    )
176
177        @sp.entrypoint
178        def aggregated_proposal(self, params):
179            """
180            Users can send aggregated proposal, which are signed offchain and validated onchain.
181            """
182            sp.cast(params, MS_TYPES.AggregatedProposalParams)
183            failIfNotSigner(sp.sender)
184
185            self.data.lastProposalId += 1
186            assert self.data.lastProposalId == params.proposalId, ERR.InvalidProposalId
187
188            proposal = sp.record(
189                startedAt=sp.now,
190                initiator=sp.sender,
191                endorsements={sp.sender},
192                actions=params.actions,
193            )
194
195            # If the proposal initiator has an active proposal,
196            # then replace that proposal with the new one
197            proposerLastProposalId = self.data.signers[sp.sender].lastProposalId
198            with sp.match(proposerLastProposalId):
199                with sp.case.Some as id:
200                    self.data.activeProposals.remove(id)
201            self.data.signers[sp.sender].lastProposalId = sp.Some(params.proposalId)
202
203            self.data.activeProposals.add(params.proposalId)
204            self.data.proposals[params.proposalId] = proposal
205
206            preSignature = sp.pack(
207                sp.record(
208                    actions=params.actions,
209                    # (contractAddress + proposalId) protect against replay attacks
210                    proposalId=params.proposalId,
211                    contractAddress=sp.self_address(),
212                )
213            )
214
215            # Validate and apply endorsements
216            for signature in params.signatures:
217                failIfNotSigner(signature.signerAddress)
218
219                publicKey = self.data.signers[signature.signerAddress].publicKey
220                assert sp.check_signature(
221                    publicKey, signature.signature, preSignature
222                ), ERR.Badsig
223
224                proposal.endorsements.add(signature.signerAddress)
225
226            # Check quorum
227            if sp.len(proposal.endorsements) >= self.data.quorum:
228                self.onApproved(
229                    sp.record(
230                        proposalId=params.proposalId,
231                        actions=proposal.actions,
232                    )
233                )
234
235        @sp.entrypoint
236        def aggregated_endorsement(self, endorsements):
237            """
238            Users can send aggregated votes, which are signed offchain and validated onchain.
239            """
240            sp.cast(endorsements, MS_TYPES.AggregatedEndorsementParams)
241
242            for endorsement in endorsements:
243                for signature in endorsement.signatures:
244                    failIfNotSigner(signature.signerAddress)
245                    preSignature = sp.pack(
246                        sp.record(
247                            # (contractAddress + proposalId) protect against replay attacks
248                            contractAddress=sp.self_address(),
249                            proposalId=endorsement.proposalId,
250                        )
251                    )
252                    publicKey = self.data.signers[signature.signerAddress].publicKey
253                    assert sp.check_signature(
254                        publicKey, signature.signature, preSignature
255                    ), ERR.Badsig
256                    self.registerEndorsement(
257                        sp.record(
258                            proposalId=endorsement.proposalId,
259                            signerAddress=signature.signerAddress,
260                        )
261                    )
262                proposal = self.data.proposals[endorsement.proposalId]
263                if sp.len(proposal.endorsements) >= self.data.quorum:
264                    self.onApproved(
265                        sp.record(
266                            proposalId=endorsement.proposalId,
267                            actions=proposal.actions,
268                        )
269                    )
270
271        @sp.entrypoint
272        def cancel_proposal(self, proposalId):
273            failIfNotSigner(sp.sender)
274
275            # Signers can only cancel their own proposals
276            assert (
277                self.data.proposals[proposalId].initiator == sp.sender
278            ), ERR.NotInitiator
279            self.data.activeProposals.remove(proposalId)
280
281        @sp.private(with_storage="read-write")
282        def registerEndorsement(self, params):
283            assert self.data.activeProposals.contains(
284                params.proposalId
285            ), ERR.ProposalUnknown
286            # Add endorsement to proposal
287            self.data.proposals[params.proposalId].endorsements.add(
288                params.signerAddress
289            )
290
291        @sp.private(with_storage="read-write", with_operations=True)
292        def onApproved(self, params):
293            with sp.match(params.actions):
294                # Internal actions are applied to the multisig contract
295                with sp.case.internal as internalActions:
296                    for action in internalActions:
297                        with sp.match(action):
298                            with sp.case.changeQuorum as quorum:
299                                self.data.quorum = quorum
300                            with sp.case.changeMetadata as metadata:
301                                (k, v) = metadata
302                                if v.is_some():
303                                    self.data.metadata[k] = v.unwrap_some()
304                                else:
305                                    del self.data.metadata[k]
306
307                            with sp.case.changeSigners as changeSigners:
308                                with sp.match(changeSigners):
309                                    with sp.case.removed as removeSet:
310                                        for address in removeSet.elements():
311                                            if self.data.signers.contains(address):
312                                                # Remove signer
313                                                del self.data.signers[address]
314                                                # We don't remove signer[address].lastProposalId
315                                                # because we remove all activeProposals after it.
316                                    with sp.case.added as addList:
317                                        for signer in addList:
318                                            self.data.signers[
319                                                signer.address
320                                            ] = sp.record(
321                                                publicKey=signer.publicKey,
322                                                lastProposalId=None,
323                                            )
324                        # Ensure that the contract never requires more quorum than the total of signers.
325                        assert self.data.quorum <= sp.len(
326                            self.data.signers
327                        ), ERR.MoreQuorumThanSigners
328                    # Removes all active proposals after an administrative change.
329                    self.data.activeProposals = set()
330                # External actions are applied to other contracts
331                with sp.case.external as externalActions:
332                    for action in externalActions:
333                        target = sp.contract(sp.bytes, action.target).unwrap_some(
334                            error=ERR.InvalidTarget
335                        )
336                        sp.transfer(action.actions, sp.tez(0), target)
337
338            self.data.activeProposals.remove(params.proposalId)
339
340
341@sp.module
342def helpers():
343    AdministrationType: type = sp.variant(changeAdmin=sp.address, changeActive=sp.bool)
344
345    class Administrated(sp.Contract):
346        """
347        This contract is a sample
348        It shows how a contract can be administrated
349        through the multisig administration contract
350        """
351
352        def __init__(self, admin, active):
353            self.data.admin = admin
354            self.data.active = active
355
356        @sp.entrypoint
357        def administrate(self, actionsBytes):
358            assert sp.sender == self.data.admin, "NOT ADMIN"
359
360            # actionsBytes is packed and must be unpacked
361            actions = sp.unpack(actionsBytes, sp.list[AdministrationType]).unwrap_some(
362                error="Actions are invalid"
363            )
364
365            for action in actions:
366                with sp.match(action):
367                    with sp.case.changeActive as active:
368                        self.data.active = active
369                    with sp.case.changeAdmin as admin:
370                        self.data.admin = admin
371
372        @sp.entrypoint
373        def verifyActive(self):
374            assert self.data.active, "NOT ACTIVE"
375
376
377if "main" in __name__:
378    #########
379    # Helpers
380
381    class InternalHelper:
382        def variant(content):
383            return sp.variant.internal(content)
384
385        def changeQuorum(quorum):
386            return sp.variant.changeQuorum(quorum)
387
388        def removeSigners(l):
389            return sp.variant.changeSigners(sp.variant.removed(sp.set(l)))
390
391        def addSigners(l):
392            added_list = []
393            for added_info in l:
394                addr, publicKey = added_info
395                added_list.append(sp.record(address=addr, publicKey=publicKey))
396            return sp.variant.changeSigners(sp.variant.added(sp.list(added_list)))
397
398    class ExternalHelper:
399        def variant(content):
400            return sp.variant.external(content)
401
402        def changeActive(active):
403            return sp.variant.changeActive(active)
404
405        def changeAdmin(address):
406            return sp.variant.changeAdmin(address)
407
408    def sign(account, contract):
409        message = sp.pack(
410            sp.record(
411                contractAddress=contract.address,
412                proposalId=contract.data.lastProposalId,
413            )
414        )
415        signature = sp.make_signature(account.secret_key, message, message_format="Raw")
416        vote = sp.record(signerAddress=account.address, signature=signature)
417        return vote
418
419    def packActions(actions):
420        actions = sp.set_type_expr(actions, sp.list[helpers.AdministrationType])
421        return sp.pack(actions)
422
423    def add_test(internal_tests):
424        name = (
425            "Internal Administration tests"
426            if internal_tests
427            else "External Administration tests"
428        )
429
430        @sp.add_test()
431        def test():
432            sc = sp.test_scenario(name, [MS_TYPES, ERR, main, helpers])
433            sc.h1(name)
434
435            admin = sp.test_account("admin")
436            signer1 = sp.test_account("signer1")
437            signer2 = sp.test_account("signer2")
438            signer3 = sp.test_account("signer3")
439            signer4 = sp.test_account("signer4")
440
441            if internal_tests:
442                sc.h3("Originate Multisig Admin")
443                multisigAdmin = main.MultisigAdmin(
444                    quorum=1,
445                    signers=sp.map(
446                        {
447                            signer1.address: sp.record(
448                                publicKey=signer1.public_key, lastProposalId=None
449                            ),
450                            signer2.address: sp.record(
451                                publicKey=signer2.public_key, lastProposalId=None
452                            ),
453                        }
454                    ),
455                    metadata=sp.scenario_utils.metadata_of_url("ipfs://"),
456                )
457                sc += multisigAdmin
458
459                ##########################
460                # Auto-accepted proposal #
461                ##########################
462                sc.h2("Auto-accepted proposal when quorum is 1")
463                sc.h3("signer1 propose to change quorum to 2")
464                sc.verify(multisigAdmin.data.quorum == 1)
465                changeQuorum = InternalHelper.changeQuorum(2)
466                multisigAdmin.proposal(
467                    InternalHelper.variant([changeQuorum]), _sender=signer1
468                )
469                sc.verify(multisigAdmin.data.quorum == 2)
470
471                ####################
472                # Add a 3rd signer #
473                ####################
474                sc.h2("Adding a 3rd signer")
475                sc.h3("signer2 new proposal to include signer3")
476                sc.verify(sp.len(multisigAdmin.data.signers) == 2)
477                sc.verify(~multisigAdmin.data.signers.contains(signer3.address))
478                changeSigners = InternalHelper.addSigners(
479                    [(signer3.address, signer3.public_key)]
480                )
481                multisigAdmin.proposal(
482                    InternalHelper.variant([changeSigners]), _sender=signer2
483                )
484                sc.h3("signer1 votes the proposal")
485                multisigAdmin.endorsement(
486                    [multisigAdmin.data.lastProposalId], _sender=signer1
487                )
488                sc.verify(multisigAdmin.data.signers.contains(signer3.address))
489                sc.verify(sp.len(multisigAdmin.data.signers) == 3)
490
491                ############################################
492                # New proposal (change Quorum from 2 to 3) #
493                ############################################
494                sc.h2("New proposal (change Quorum from 2 to 3)")
495                sc.h3("signer1 new proposal to change quorum to 3")
496                changeQuorum = InternalHelper.changeQuorum(3)
497                multisigAdmin.proposal(
498                    InternalHelper.variant([changeQuorum]), _sender=signer1
499                )
500                # Proposal has not been validated yet
501                sc.verify(multisigAdmin.data.quorum == 2)
502                sc.h3("signer2 votes the proposal (2/2)")
503                multisigAdmin.endorsement(
504                    [multisigAdmin.data.lastProposalId], _sender=signer2
505                )
506                sc.verify(multisigAdmin.data.quorum == 3)
507
508                ###########################################
509                # Newly included signer starts a proposal #
510                ###########################################
511                sc.h2("Newly included signer starts a proposal")
512                sc.h3("New proposal by signer 3 to decrease quorum to 2")
513                changeQuorum = InternalHelper.changeQuorum(2)
514                multisigAdmin.proposal(
515                    InternalHelper.variant([changeQuorum]), _sender=signer3
516                )
517                sc.h3("signer1 votes the proposal")
518                multisigAdmin.endorsement(
519                    [multisigAdmin.data.lastProposalId], _sender=signer1
520                )
521                sc.verify(multisigAdmin.data.quorum == 3)
522                sc.h3("signer2 votes the proposal")
523                multisigAdmin.endorsement(
524                    [multisigAdmin.data.lastProposalId], _sender=signer2
525                )
526                sc.verify(multisigAdmin.data.quorum == 2)
527
528                ##########
529                # Cancel #
530                ##########
531                sc.h2("Proposal cancellation")
532                sc.h3("New proposal by signer 1")
533                changeTimeout = InternalHelper.changeQuorum(3)
534                multisigAdmin.proposal(
535                    InternalHelper.variant([changeTimeout]), _sender=signer1
536                )
537                sc.verify(sp.len(multisigAdmin.data.activeProposals) == 1)
538                sc.h3(
539                    "Signer 2 tries to cancel the proposal (must fail, only the initiator can cancel)"
540                )
541                multisigAdmin.cancel_proposal(
542                    multisigAdmin.data.lastProposalId, _sender=signer2, _valid=False
543                )
544                sc.h3("Signer 1 cancels the proposal")
545                multisigAdmin.cancel_proposal(
546                    multisigAdmin.data.lastProposalId, _sender=signer1
547                )
548                sc.verify(sp.len(multisigAdmin.data.activeProposals) == 0)
549                sc.h3("Signer 2 tries to vote the canceled proposal")
550                multisigAdmin.endorsement(
551                    [multisigAdmin.data.lastProposalId], _sender=signer2, _valid=False
552                )
553                sc.verify(multisigAdmin.data.quorum != 3)
554
555                ######################
556                # 2 actions proposal #
557                ######################
558                sc.h2("2 actions proposal")
559                sc.h3("Signer 1 new proposal: change quorum to 2 and add signer 4")
560                sc.verify(~multisigAdmin.data.signers.contains(signer4.address))
561                changeQuorum = InternalHelper.changeQuorum(3)
562                changeSigners = InternalHelper.addSigners(
563                    [(signer4.address, signer4.public_key)]
564                )
565                multisigAdmin.proposal(
566                    InternalHelper.variant([changeQuorum, changeSigners]),
567                    _sender=signer1,
568                )
569                sc.h3("Signer 2 votes the proposal")
570                multisigAdmin.endorsement(
571                    [multisigAdmin.data.lastProposalId], _sender=signer2
572                )
573                sc.verify(multisigAdmin.data.quorum == 3)
574                sc.verify(multisigAdmin.data.signers.contains(signer4.address))
575
576                #########################################
577                # 2 Internal proposals at the same time #
578                #########################################
579                sc.h3("Signer 1 new proposal: change quorum to 2 and remove signer 4")
580                changeQuorum = InternalHelper.changeQuorum(2)
581                multisigAdmin.proposal(
582                    InternalHelper.variant([changeQuorum]), _sender=signer1
583                )
584                changeSigners = InternalHelper.removeSigners([signer4.address])
585                multisigAdmin.proposal(
586                    InternalHelper.variant([changeSigners]), _sender=signer2
587                )
588                sc.verify(sp.len(multisigAdmin.data.activeProposals) == 2)
589                sc.h3("Signer 3 votes on quorum proposal")
590                multisigAdmin.endorsement(
591                    [sp.as_nat(multisigAdmin.data.lastProposalId - 1)], _sender=signer3
592                )
593                sc.h3("Signer 4 votes on signers proposal")
594                multisigAdmin.endorsement(
595                    [multisigAdmin.data.lastProposalId], _sender=signer4
596                )
597                sc.h3("Confirm that nothing has changed")
598                sc.verify(multisigAdmin.data.quorum == 3)
599                sc.verify(multisigAdmin.data.signers.contains(signer4.address))
600                sc.h3("Signer 4 votes on quorum proposal")
601                multisigAdmin.endorsement(
602                    [sp.as_nat(multisigAdmin.data.lastProposalId - 1)], _sender=signer4
603                )
604                sc.h3(
605                    "Confirm that quorum was updated and signers proposal was canceled"
606                )
607                sc.verify(sp.len(multisigAdmin.data.activeProposals) == 0)
608                sc.verify(multisigAdmin.data.quorum == 2)
609                sc.verify(multisigAdmin.data.signers.contains(signer4.address))
610
611                #########################
612                # Multisig endorsements #
613                #########################
614                sc.h2("Multi vote in one call")
615                sc.h3("Signer 1 new proposal")
616                changeQuorum = InternalHelper.changeQuorum(3)
617                multisigAdmin.proposal(
618                    InternalHelper.variant([changeQuorum]), _sender=signer1
619                )
620                sc.h3("Signer 2 and Signer 3 votes are pushed by Signer 1")
621                signer2_endorsement = sign(signer2, contract=multisigAdmin)
622                signer3_endorsement = sign(signer3, contract=multisigAdmin)
623                proposalEndorsements = sp.record(
624                    proposalId=multisigAdmin.data.lastProposalId,
625                    signatures=[signer2_endorsement, signer3_endorsement],
626                )
627                multisigAdmin.aggregated_endorsement(
628                    [proposalEndorsements], _sender=signer1
629                )
630                sc.verify(multisigAdmin.data.quorum == 3)
631
632                #####################
633                # Multisig proposal #
634                #####################
635                sc.h2("Multi vote in one call")
636                sc.h3("Signer 1 new proposal")
637                changeQuorum = InternalHelper.changeQuorum(3)
638                multisigAdmin.proposal(
639                    InternalHelper.variant([changeQuorum]), _sender=signer1
640                )
641                sc.h3("Signer 2 and Signer 3 votes are pushed by Signer 1")
642                signer2_vote = sign(signer2, contract=multisigAdmin)
643                signer3_vote = sign(signer3, contract=multisigAdmin)
644                proposalVotes = sp.record(
645                    proposalId=multisigAdmin.data.lastProposalId,
646                    signatures=[signer2_vote, signer3_vote],
647                )
648                multisigAdmin.aggregated_endorsement([proposalVotes], _sender=signer1)
649                sc.verify(multisigAdmin.data.quorum == 3)
650
651            ##########################################
652
653            else:
654                sc.h3("Originate Multisig Admin")
655                multisigAdmin = main.MultisigAdmin(
656                    quorum=3,
657                    signers=sp.map(
658                        {
659                            signer1.address: sp.record(
660                                publicKey=signer1.public_key, lastProposalId=None
661                            ),
662                            signer2.address: sp.record(
663                                publicKey=signer2.public_key, lastProposalId=None
664                            ),
665                            signer3.address: sp.record(
666                                publicKey=signer3.public_key, lastProposalId=None
667                            ),
668                        }
669                    ),
670                    metadata=sp.scenario_utils.metadata_of_url("ipfs://"),
671                )
672                sc += multisigAdmin
673
674                sc.h3("Originate administrated contract")
675                administrated = helpers.Administrated(admin.address, False)
676                sc += administrated
677                administrated_entrypoint = sp.contract(
678                    sp.bytes, administrated.address, entrypoint="administrate"
679                ).unwrap_some()
680
681                sc.h2("Set multisig as admin of administrated contract")
682                sc.verify(administrated.data.active == False)
683                sc.verify(administrated.data.admin == admin.address)
684                actions = packActions(
685                    [ExternalHelper.changeAdmin(multisigAdmin.address)]
686                )
687                administrated.administrate(actions, _sender=admin)
688                sc.verify(administrated.data.active == False)
689                sc.verify(administrated.data.admin == multisigAdmin.address)
690
691                sc.h2("Activate the administrated contract")
692                sc.h3("Signer 1 new proposal: changeActive")
693                actions = packActions([ExternalHelper.changeActive(True)])
694                multisigAdmin.proposal(
695                    ExternalHelper.variant(
696                        [
697                            sp.record(
698                                target=sp.to_address(administrated_entrypoint),
699                                actions=actions,
700                            )
701                        ]
702                    ),
703                    _sender=signer1,
704                )
705                sc.verify(administrated.data.active == False)
706                sc.h3("Signer 2 votes")
707                multisigAdmin.endorsement(
708                    [multisigAdmin.data.lastProposalId], _sender=signer2
709                )
710                sc.verify(administrated.data.active == False)
711                sc.h3("Signer 3 votes")
712                multisigAdmin.endorsement(
713                    [multisigAdmin.data.lastProposalId], _sender=signer3
714                )
715                sc.verify(administrated.data.active == True)
716
717                sc.h2("Use Multisig vote to deactivate the administrated contract")
718                sc.h3("Signer 1 new proposal: changeActive")
719                actions = packActions([ExternalHelper.changeActive(False)])
720                multisigAdmin.proposal(
721                    ExternalHelper.variant(
722                        [
723                            sp.record(
724                                target=sp.to_address(administrated_entrypoint),
725                                actions=actions,
726                            )
727                        ]
728                    ),
729                    _sender=signer1,
730                )
731                sc.verify(administrated.data.active == True)
732                sc.h3("Signer 2 and Signer 3 votes are pushed by Signer 1")
733                signer2_vote = sign(signer2, contract=multisigAdmin)
734                signer3_vote = sign(signer3, contract=multisigAdmin)
735                proposalVotes = sp.record(
736                    proposalId=multisigAdmin.data.lastProposalId,
737                    signatures=[signer2_vote, signer3_vote],
738                )
739                multisigAdmin.aggregated_endorsement([proposalVotes], _sender=signer1)
740                sc.verify(administrated.data.active == False)
741
742    add_test(internal_tests=True)
743    add_test(internal_tests=False)
METADATA = {'name': 'Generic Multisig Administrator', 'version': '1', 'description': 'Generic Multisig Administrator', 'source': {'tools': ['SmartPy']}, 'interfaces': ['TZIP-016']}