Module admin_multisig

Expand source code
import smartpy as sp

"""
    A Multisig Contract used to administrate other contracts.
    THIS CONTRACT IS FOR ILLUSTRATIVE PURPOSE.
    IT HAS NOT BEEN AUDITED.
"""


@sp.module
def MS_TYPES():
    # Internal administration action type specification
    InternalAdminAction: type = sp.variant(
        changeSigners=sp.variant(
            removed=sp.set[sp.address],
            added=sp.list[sp.record(address=sp.address, publicKey=sp.key)],
        ),
        changeQuorum=sp.nat,
        changeMetadata=sp.pair[sp.string, sp.option[sp.bytes]],
    )

    # External administration action type specification
    ExternalAdminAction: type = sp.record(target=sp.address, actions=sp.bytes)

    # Proposal action type specification
    ProposalAction: type = sp.variant(
        internal=sp.list[InternalAdminAction], external=sp.list[ExternalAdminAction]
    )

    # Proposal type specification
    Proposal: type = sp.record(
        startedAt=sp.timestamp,
        initiator=sp.address,
        endorsements=sp.set[sp.address],
        actions=ProposalAction,
    )

    AggregatedProposalParams: type = sp.record(
        signatures=sp.list[sp.record(signerAddress=sp.address, signature=sp.signature)],
        proposalId=sp.nat,
        actions=ProposalAction,
    )

    AggregatedEndorsementParams: type = sp.list[
        sp.record(
            signatures=sp.list[
                sp.record(signerAddress=sp.address, signature=sp.signature)
            ],
            proposalId=sp.nat,
        )
    ]


@sp.module
def ERR():
    Badsig = "MULTISIG_Badsig"
    ProposalUnknown = "MULTISIG_ProposalUnknown"
    NotInitiator = "MULTISIG_NotInitiator"
    SignerUnknown = "MULTISIG_SignerUnknown"
    InvalidTarget = "MULTISIG_InvalidTarget"
    MoreQuorumThanSigners = "MULTISIG_MoreQuorumThanSigners"
    InvalidProposalId = "MULTISIG_InvalidProposalId"


METADATA = {
    "name": "Generic Multisig Administrator",
    "version": "1",
    "description": "Generic Multisig Administrator",
    "source": {"tools": ["SmartPy"]},
    "interfaces": ["TZIP-016"],
}


@sp.module
def main():
    @sp.effects(with_storage="read-only")
    def failIfNotSigner(address):
        sp.cast(
            self.data.signers,
            sp.map[
                sp.address,
                sp.record(publicKey=sp.key, lastProposalId=sp.option[sp.nat]),
            ],
        )
        assert self.data.signers.contains(address), ERR.SignerUnknown

    class MultisigAdmin(sp.Contract):
        def __init__(self, quorum, signers, metadata):

            # Metadata helper
            # self.init_metadata("metadata", METADATA)

            self.data.quorum = quorum
            self.data.lastProposalId = 0
            self.data.signers = signers
            self.data.proposals = sp.big_map()
            self.data.activeProposals = sp.set()
            self.data.metadata = metadata

            sp.cast(
                self.data,
                sp.record(
                    quorum=sp.nat,
                    lastProposalId=sp.nat,
                    signers=sp.map[
                        sp.address,
                        sp.record(publicKey=sp.key, lastProposalId=sp.option[sp.nat]),
                    ],
                    proposals=sp.big_map[sp.nat, MS_TYPES.Proposal],
                    activeProposals=sp.set[sp.nat],
                    metadata=sp.big_map[sp.string, sp.bytes],
                ),
            )

        @sp.entrypoint
        def proposal(self, actions):
            """
            Each user can have at most one proposal active at a time.
            Submitting a new proposal overrides the previous one.
            """
            # Proposals can only be submitted by registered signers
            failIfNotSigner(sp.sender)

            # If the proposal initiator has an active proposal,
            # then replace that proposal with the new one
            signerLastProposalId = self.data.signers[sp.sender].lastProposalId
            with sp.match(signerLastProposalId):
                with sp.case.Some as id:
                    self.data.activeProposals.remove(id)

            # Increment proposal counter
            self.data.lastProposalId += 1
            proposalId = self.data.lastProposalId
            # Store new proposal
            self.data.activeProposals.add(proposalId)
            self.data.proposals[proposalId] = sp.record(
                startedAt=sp.now,
                initiator=sp.sender,
                endorsements=sp.set(sp.sender),
                actions=actions,
            )
            # Update signer's last proposal
            self.data.signers[sp.sender].lastProposalId = sp.Some(proposalId)

            # Approve the proposal if quorum only requires 1 vote
            if self.data.quorum < 2:
                self.onApproved(
                    sp.record(
                        proposalId=proposalId,
                        actions=actions,
                    )
                )

        @sp.entrypoint
        def endorsement(self, endorsements):
            """
            Entrypoint used to submit endorsements to single/multiple proposals.
            """
            # Endorsements can only be submitted by registered signers
            failIfNotSigner(sp.sender)

            # Iterate over every endorsement
            for pId in endorsements:
                self.registerEndorsement(
                    sp.record(proposalId=pId, signerAddress=sp.sender)
                )

                # Approve the proposal if quorum was reached
                proposal = self.data.proposals[pId]
                if sp.len(proposal.endorsements) >= self.data.quorum:
                    self.onApproved(
                        sp.record(
                            proposalId=pId,
                            actions=proposal.actions,
                        )
                    )

        @sp.entrypoint
        def aggregated_proposal(self, params):
            """
            Users can send aggregated proposal, which are signed offchain and validated onchain.
            """
            sp.cast(params, MS_TYPES.AggregatedProposalParams)
            failIfNotSigner(sp.sender)

            self.data.lastProposalId += 1
            assert self.data.lastProposalId == params.proposalId, ERR.InvalidProposalId

            proposal = sp.record(
                startedAt=sp.now,
                initiator=sp.sender,
                endorsements=sp.set(sp.sender),
                actions=params.actions,
            )

            # If the proposal initiator has an active proposal,
            # then replace that proposal with the new one
            proposerLastProposalId = self.data.signers[sp.sender].lastProposalId
            with sp.match(proposerLastProposalId):
                with sp.case.Some as id:
                    self.data.activeProposals.remove(id)
            self.data.signers[sp.sender].lastProposalId = sp.Some(params.proposalId)

            self.data.activeProposals.add(params.proposalId)
            self.data.proposals[params.proposalId] = proposal

            preSignature = sp.pack(
                sp.record(
                    actions=params.actions,
                    # (contractAddress + proposalId) protect against replay attacks
                    proposalId=params.proposalId,
                    contractAddress=sp.self_address(),
                )
            )

            # Validate and apply endorsements
            for signature in params.signatures:
                failIfNotSigner(signature.signerAddress)

                publicKey = self.data.signers[signature.signerAddress].publicKey
                assert sp.check_signature(
                    publicKey, signature.signature, preSignature
                ), ERR.Badsig

                proposal.endorsements.add(signature.signerAddress)

            # Check quorum
            if sp.len(proposal.endorsements) >= self.data.quorum:
                self.onApproved(
                    sp.record(
                        proposalId=params.proposalId,
                        actions=proposal.actions,
                    )
                )

        @sp.entrypoint
        def aggregated_endorsement(self, endorsements):
            """
            Users can send aggregated votes, which are signed offchain and validated onchain.
            """
            sp.cast(endorsements, MS_TYPES.AggregatedEndorsementParams)

            for endorsement in endorsements:
                for signature in endorsement.signatures:
                    failIfNotSigner(signature.signerAddress)
                    preSignature = sp.pack(
                        sp.record(
                            # (contractAddress + proposalId) protect against replay attacks
                            contractAddress=sp.self_address(),
                            proposalId=endorsement.proposalId,
                        )
                    )
                    publicKey = self.data.signers[signature.signerAddress].publicKey
                    assert sp.check_signature(
                        publicKey, signature.signature, preSignature
                    ), ERR.Badsig
                    self.registerEndorsement(
                        sp.record(
                            proposalId=endorsement.proposalId,
                            signerAddress=signature.signerAddress,
                        )
                    )
                proposal = self.data.proposals[endorsement.proposalId]
                if sp.len(proposal.endorsements) >= self.data.quorum:
                    self.onApproved(
                        sp.record(
                            proposalId=endorsement.proposalId,
                            actions=proposal.actions,
                        )
                    )

        @sp.entrypoint
        def cancel_proposal(self, proposalId):
            failIfNotSigner(sp.sender)

            # Signers can only cancel their own proposals
            assert (
                self.data.proposals[proposalId].initiator == sp.sender
            ), ERR.NotInitiator
            self.data.activeProposals.remove(proposalId)

        @sp.private(with_storage="read-write")
        def registerEndorsement(self, params):
            assert self.data.activeProposals.contains(
                params.proposalId
            ), ERR.ProposalUnknown
            # Add endorsement to proposal
            self.data.proposals[params.proposalId].endorsements.add(
                params.signerAddress
            )

        @sp.private(with_storage="read-write", with_operations=True)
        def onApproved(self, params):
            with sp.match(params.actions):
                # Internal actions are applied to the multisig contract
                with sp.case.internal as internalActions:
                    for action in internalActions:
                        with sp.match(action):
                            with sp.case.changeQuorum as quorum:
                                self.data.quorum = quorum
                            with sp.case.changeMetadata as metadata:
                                (k, v) = metadata
                                if v.is_some():
                                    self.data.metadata[k] = v.unwrap_some()
                                else:
                                    del self.data.metadata[k]

                            with sp.case.changeSigners as changeSigners:
                                with sp.match(changeSigners):
                                    with sp.case.removed as removeSet:
                                        for address in removeSet.elements():
                                            if self.data.signers.contains(address):
                                                # Remove signer
                                                del self.data.signers[address]
                                                # We don't remove signer[address].lastProposalId
                                                # because we remove all activeProposals after it.
                                    with sp.case.added as addList:
                                        for signer in addList:
                                            self.data.signers[
                                                signer.address
                                            ] = sp.record(
                                                publicKey=signer.publicKey,
                                                lastProposalId=None,
                                            )
                        # Ensure that the contract never requires more quorum than the total of signers.
                        assert self.data.quorum <= sp.len(
                            self.data.signers
                        ), ERR.MoreQuorumThanSigners
                    # Removes all active proposals after an administrative change.
                    self.data.activeProposals = sp.set()
                # External actions are applied to other contracts
                with sp.case.external as externalActions:
                    for action in externalActions:
                        target = sp.contract(sp.bytes, action.target).unwrap_some(
                            error=ERR.InvalidTarget
                        )
                        sp.transfer(action.actions, sp.tez(0), target)

            self.data.activeProposals.remove(params.proposalId)


@sp.module
def helpers():
    AdministrationType: type = sp.variant(changeAdmin=sp.address, changeActive=sp.bool)

    class Administrated(sp.Contract):
        """
        This contract is a sample
        It shows how a contract can be administrated
        through the multisig administration contract
        """

        def __init__(self, admin, active):
            self.data.admin = admin
            self.data.active = active

        @sp.entrypoint
        def administrate(self, actionsBytes):
            assert sp.sender == self.data.admin, "NOT ADMIN"

            # actionsBytes is packed and must be unpacked
            actions = sp.unpack(actionsBytes, sp.list[AdministrationType]).unwrap_some(
                error="Actions are invalid"
            )

            for action in actions:
                with sp.match(action):
                    with sp.case.changeActive as active:
                        self.data.active = active
                    with sp.case.changeAdmin as admin:
                        self.data.admin = admin

        @sp.entrypoint
        def verifyActive(self):
            assert self.data.active, "NOT ACTIVE"


if "templates" not in __name__:
    #########
    # Helpers

    class InternalHelper:
        def variant(content):
            return sp.variant("internal", content)

        def changeQuorum(quorum):
            return sp.variant("changeQuorum", quorum)

        def removeSigners(l):
            return sp.variant("changeSigners", sp.variant("removed", sp.set(l)))

        def addSigners(l):
            added_list = []
            for added_info in l:
                addr, publicKey = added_info
                added_list.append(sp.record(address=addr, publicKey=publicKey))
            return sp.variant("changeSigners", sp.variant("added", sp.list(added_list)))

    class ExternalHelper:
        def variant(content):
            return sp.variant("external", content)

        def changeActive(active):
            return sp.variant("changeActive", active)

        def changeAdmin(address):
            return sp.variant("changeAdmin", address)

    def sign(account, contract):
        message = sp.pack(
            sp.record(
                contractAddress=contract.address,
                proposalId=contract.data.lastProposalId,
            )
        )
        signature = sp.make_signature(account.secret_key, message, message_format="Raw")
        vote = sp.record(signerAddress=account.address, signature=signature)
        return vote

    def packActions(actions):
        actions = sp.set_type_expr(actions, sp.TList(helpers.AdministrationType))
        return sp.pack(actions)

    def add_test(internal_tests, is_default=True):
        name = (
            "Internal Administration tests"
            if internal_tests
            else "External Administration tests"
        )

        @sp.add_test(name=name, is_default=is_default)
        def test():
            sc = sp.test_scenario([MS_TYPES, ERR, main, helpers])
            sc.h1(name)

            admin = sp.test_account("admin")
            signer1 = sp.test_account("signer1")
            signer2 = sp.test_account("signer2")
            signer3 = sp.test_account("signer3")
            signer4 = sp.test_account("signer4")

            if internal_tests:

                sc.h3("Originate Multisig Admin")
                multisigAdmin = main.MultisigAdmin(
                    quorum=1,
                    signers=sp.map(
                        {
                            signer1.address: sp.record(
                                publicKey=signer1.public_key, lastProposalId=sp.none
                            ),
                            signer2.address: sp.record(
                                publicKey=signer2.public_key, lastProposalId=sp.none
                            ),
                        }
                    ),
                    metadata=sp.utils.metadata_of_url("ipfs://"),
                )
                sc += multisigAdmin

                ##########################
                # Auto-accepted proposal #
                ##########################
                sc.h2("Auto-accepted proposal when quorum is 1")
                sc.h3("signer1 propose to change quorum to 2")
                sc.verify(multisigAdmin.data.quorum == 1)
                changeQuorum = InternalHelper.changeQuorum(2)
                multisigAdmin.proposal(InternalHelper.variant([changeQuorum])).run(
                    sender=signer1
                )
                sc.verify(multisigAdmin.data.quorum == 2)

                ####################
                # Add a 3rd signer #
                ####################
                sc.h2("Adding a 3rd signer")
                sc.h3("signer2 new proposal to include signer3")
                sc.verify(sp.len(multisigAdmin.data.signers) == 2)
                sc.verify(~multisigAdmin.data.signers.contains(signer3.address))
                changeSigners = InternalHelper.addSigners(
                    [(signer3.address, signer3.public_key)]
                )
                multisigAdmin.proposal(InternalHelper.variant([changeSigners])).run(
                    sender=signer2
                )
                sc.h3("signer1 votes the proposal")
                multisigAdmin.endorsement([multisigAdmin.data.lastProposalId]).run(
                    sender=signer1
                )
                sc.verify(multisigAdmin.data.signers.contains(signer3.address))
                sc.verify(sp.len(multisigAdmin.data.signers) == 3)

                ############################################
                # New proposal (change Quorum from 2 to 3) #
                ############################################
                sc.h2("New proposal (change Quorum from 2 to 3)")
                sc.h3("signer1 new proposal to change quorum to 3")
                changeQuorum = InternalHelper.changeQuorum(3)
                multisigAdmin.proposal(InternalHelper.variant([changeQuorum])).run(
                    sender=signer1
                )
                # Proposal has not been validated yet
                sc.verify(multisigAdmin.data.quorum == 2)
                sc.h3("signer2 votes the proposal (2/2)")
                multisigAdmin.endorsement([multisigAdmin.data.lastProposalId]).run(
                    sender=signer2
                )
                sc.verify(multisigAdmin.data.quorum == 3)

                ###########################################
                # Newly included signer starts a proposal #
                ###########################################
                sc.h2("Newly included signer starts a proposal")
                sc.h3("New proposal by signer 3 to decrease quorum to 2")
                changeQuorum = InternalHelper.changeQuorum(2)
                multisigAdmin.proposal(InternalHelper.variant([changeQuorum])).run(
                    sender=signer3
                )
                sc.h3("signer1 votes the proposal")
                multisigAdmin.endorsement([multisigAdmin.data.lastProposalId]).run(
                    sender=signer1
                )
                sc.verify(multisigAdmin.data.quorum == 3)
                sc.h3("signer2 votes the proposal")
                multisigAdmin.endorsement([multisigAdmin.data.lastProposalId]).run(
                    sender=signer2
                )
                sc.verify(multisigAdmin.data.quorum == 2)

                ##########
                # Cancel #
                ##########
                sc.h2("Proposal cancellation")
                sc.h3("New proposal by signer 1")
                changeTimeout = InternalHelper.changeQuorum(3)
                multisigAdmin.proposal(InternalHelper.variant([changeTimeout])).run(
                    sender=signer1
                )
                sc.verify(sp.len(multisigAdmin.data.activeProposals) == 1)
                sc.h3(
                    "Signer 2 tries to cancel the proposal (must fail, only the initiator can cancel)"
                )
                multisigAdmin.cancel_proposal(multisigAdmin.data.lastProposalId).run(
                    sender=signer2, valid=False
                )
                sc.h3("Signer 1 cancels the proposal")
                multisigAdmin.cancel_proposal(multisigAdmin.data.lastProposalId).run(
                    sender=signer1
                )
                sc.verify(sp.len(multisigAdmin.data.activeProposals) == 0)
                sc.h3("Signer 2 tries to vote the canceled proposal")
                multisigAdmin.endorsement([multisigAdmin.data.lastProposalId]).run(
                    sender=signer2, valid=False
                )
                sc.verify(multisigAdmin.data.quorum != 3)

                ######################
                # 2 actions proposal #
                ######################
                sc.h2("2 actions proposal")
                sc.h3("Signer 1 new proposal: change quorum to 2 and add signer 4")
                sc.verify(~multisigAdmin.data.signers.contains(signer4.address))
                changeQuorum = InternalHelper.changeQuorum(3)
                changeSigners = InternalHelper.addSigners(
                    [(signer4.address, signer4.public_key)]
                )
                multisigAdmin.proposal(
                    InternalHelper.variant([changeQuorum, changeSigners])
                ).run(sender=signer1)
                sc.h3("Signer 2 votes the proposal")
                multisigAdmin.endorsement([multisigAdmin.data.lastProposalId]).run(
                    sender=signer2
                )
                sc.verify(multisigAdmin.data.quorum == 3)
                sc.verify(multisigAdmin.data.signers.contains(signer4.address))

                #########################################
                # 2 Internal proposals at the same time #
                #########################################
                sc.h3("Signer 1 new proposal: change quorum to 2 and remove signer 4")
                changeQuorum = InternalHelper.changeQuorum(2)
                multisigAdmin.proposal(InternalHelper.variant([changeQuorum])).run(
                    sender=signer1
                )
                changeSigners = InternalHelper.removeSigners([signer4.address])
                multisigAdmin.proposal(InternalHelper.variant([changeSigners])).run(
                    sender=signer2
                )
                sc.verify(sp.len(multisigAdmin.data.activeProposals) == 2)
                sc.h3("Signer 3 votes on quorum proposal")
                multisigAdmin.endorsement(
                    [sp.as_nat(multisigAdmin.data.lastProposalId - 1)]
                ).run(sender=signer3)
                sc.h3("Signer 4 votes on signers proposal")
                multisigAdmin.endorsement([multisigAdmin.data.lastProposalId]).run(
                    sender=signer4
                )
                sc.h3("Confirm that nothing has changed")
                sc.verify(multisigAdmin.data.quorum == 3)
                sc.verify(multisigAdmin.data.signers.contains(signer4.address))
                sc.h3("Signer 4 votes on quorum proposal")
                multisigAdmin.endorsement(
                    [sp.as_nat(multisigAdmin.data.lastProposalId - 1)]
                ).run(sender=signer4)
                sc.h3(
                    "Confirm that quorum was updated and signers proposal was canceled"
                )
                sc.verify(sp.len(multisigAdmin.data.activeProposals) == 0)
                sc.verify(multisigAdmin.data.quorum == 2)
                sc.verify(multisigAdmin.data.signers.contains(signer4.address))

                #########################
                # Multisig endorsements #
                #########################
                sc.h2("Multi vote in one call")
                sc.h3("Signer 1 new proposal")
                changeQuorum = InternalHelper.changeQuorum(3)
                multisigAdmin.proposal(InternalHelper.variant([changeQuorum])).run(
                    sender=signer1
                )
                sc.h3("Signer 2 and Signer 3 votes are pushed by Signer 1")
                signer2_endorsement = sign(signer2, contract=multisigAdmin)
                signer3_endorsement = sign(signer3, contract=multisigAdmin)
                proposalEndorsements = sp.record(
                    proposalId=multisigAdmin.data.lastProposalId,
                    signatures=[signer2_endorsement, signer3_endorsement],
                )
                multisigAdmin.aggregated_endorsement([proposalEndorsements]).run(
                    sender=signer1
                )
                sc.verify(multisigAdmin.data.quorum == 3)

                #####################
                # Multisig proposal #
                #####################
                sc.h2("Multi vote in one call")
                sc.h3("Signer 1 new proposal")
                changeQuorum = InternalHelper.changeQuorum(3)
                multisigAdmin.proposal(InternalHelper.variant([changeQuorum])).run(
                    sender=signer1
                )
                sc.h3("Signer 2 and Signer 3 votes are pushed by Signer 1")
                signer2_vote = sign(signer2, contract=multisigAdmin)
                signer3_vote = sign(signer3, contract=multisigAdmin)
                proposalVotes = sp.record(
                    proposalId=multisigAdmin.data.lastProposalId,
                    signatures=[signer2_vote, signer3_vote],
                )
                multisigAdmin.aggregated_endorsement([proposalVotes]).run(
                    sender=signer1
                )
                sc.verify(multisigAdmin.data.quorum == 3)

            ##########################################

            else:

                sc.h3("Originate Multisig Admin")
                multisigAdmin = main.MultisigAdmin(
                    quorum=3,
                    signers=sp.map(
                        {
                            signer1.address: sp.record(
                                publicKey=signer1.public_key, lastProposalId=sp.none
                            ),
                            signer2.address: sp.record(
                                publicKey=signer2.public_key, lastProposalId=sp.none
                            ),
                            signer3.address: sp.record(
                                publicKey=signer3.public_key, lastProposalId=sp.none
                            ),
                        }
                    ),
                    metadata=sp.utils.metadata_of_url("ipfs://"),
                )
                sc += multisigAdmin

                sc.h3("Originate administrated contract")
                administrated = helpers.Administrated(admin.address, False)
                sc += administrated
                administrated_entrypoint = sp.contract(
                    sp.TBytes, administrated.address, entrypoint="administrate"
                ).open_some()

                sc.h2("Set multisig as admin of administrated contract")
                sc.verify(administrated.data.active == False)
                sc.verify(administrated.data.admin == admin.address)
                actions = packActions(
                    [ExternalHelper.changeAdmin(multisigAdmin.address)]
                )
                administrated.administrate(actions).run(sender=admin)
                sc.verify(administrated.data.active == False)
                sc.verify(administrated.data.admin == multisigAdmin.address)

                sc.h2("Activate the administrated contract")
                sc.h3("Signer 1 new proposal: changeActive")
                actions = packActions([ExternalHelper.changeActive(True)])
                multisigAdmin.proposal(
                    ExternalHelper.variant(
                        [
                            sp.record(
                                target=sp.to_address(administrated_entrypoint),
                                actions=actions,
                            )
                        ]
                    )
                ).run(sender=signer1)
                sc.verify(administrated.data.active == False)
                sc.h3("Signer 2 votes")
                multisigAdmin.endorsement([multisigAdmin.data.lastProposalId]).run(
                    sender=signer2
                )
                sc.verify(administrated.data.active == False)
                sc.h3("Signer 3 votes")
                multisigAdmin.endorsement([multisigAdmin.data.lastProposalId]).run(
                    sender=signer3
                )
                sc.verify(administrated.data.active == True)

                sc.h2("Use Multisig vote to deactivate the administrated contract")
                sc.h3("Signer 1 new proposal: changeActive")
                actions = packActions([ExternalHelper.changeActive(False)])
                multisigAdmin.proposal(
                    ExternalHelper.variant(
                        [
                            sp.record(
                                target=sp.to_address(administrated_entrypoint),
                                actions=actions,
                            )
                        ]
                    )
                ).run(sender=signer1)
                sc.verify(administrated.data.active == True)
                sc.h3("Signer 2 and Signer 3 votes are pushed by Signer 1")
                signer2_vote = sign(signer2, contract=multisigAdmin)
                signer3_vote = sign(signer3, contract=multisigAdmin)
                proposalVotes = sp.record(
                    proposalId=multisigAdmin.data.lastProposalId,
                    signatures=[signer2_vote, signer3_vote],
                )
                multisigAdmin.aggregated_endorsement([proposalVotes]).run(
                    sender=signer1
                )
                sc.verify(administrated.data.active == False)

    add_test(internal_tests=True)
    add_test(internal_tests=False)

Functions

def sign(account, contract)
Expand source code
def sign(account, contract):
    message = sp.pack(
        sp.record(
            contractAddress=contract.address,
            proposalId=contract.data.lastProposalId,
        )
    )
    signature = sp.make_signature(account.secret_key, message, message_format="Raw")
    vote = sp.record(signerAddress=account.address, signature=signature)
    return vote
def packActions(actions)
Expand source code
def packActions(actions):
    actions = sp.set_type_expr(actions, sp.TList(helpers.AdministrationType))
    return sp.pack(actions)
def add_test(internal_tests, is_default=True)
Expand source code
def add_test(internal_tests, is_default=True):
    name = (
        "Internal Administration tests"
        if internal_tests
        else "External Administration tests"
    )

    @sp.add_test(name=name, is_default=is_default)
    def test():
        sc = sp.test_scenario([MS_TYPES, ERR, main, helpers])
        sc.h1(name)

        admin = sp.test_account("admin")
        signer1 = sp.test_account("signer1")
        signer2 = sp.test_account("signer2")
        signer3 = sp.test_account("signer3")
        signer4 = sp.test_account("signer4")

        if internal_tests:

            sc.h3("Originate Multisig Admin")
            multisigAdmin = main.MultisigAdmin(
                quorum=1,
                signers=sp.map(
                    {
                        signer1.address: sp.record(
                            publicKey=signer1.public_key, lastProposalId=sp.none
                        ),
                        signer2.address: sp.record(
                            publicKey=signer2.public_key, lastProposalId=sp.none
                        ),
                    }
                ),
                metadata=sp.utils.metadata_of_url("ipfs://"),
            )
            sc += multisigAdmin

            ##########################
            # Auto-accepted proposal #
            ##########################
            sc.h2("Auto-accepted proposal when quorum is 1")
            sc.h3("signer1 propose to change quorum to 2")
            sc.verify(multisigAdmin.data.quorum == 1)
            changeQuorum = InternalHelper.changeQuorum(2)
            multisigAdmin.proposal(InternalHelper.variant([changeQuorum])).run(
                sender=signer1
            )
            sc.verify(multisigAdmin.data.quorum == 2)

            ####################
            # Add a 3rd signer #
            ####################
            sc.h2("Adding a 3rd signer")
            sc.h3("signer2 new proposal to include signer3")
            sc.verify(sp.len(multisigAdmin.data.signers) == 2)
            sc.verify(~multisigAdmin.data.signers.contains(signer3.address))
            changeSigners = InternalHelper.addSigners(
                [(signer3.address, signer3.public_key)]
            )
            multisigAdmin.proposal(InternalHelper.variant([changeSigners])).run(
                sender=signer2
            )
            sc.h3("signer1 votes the proposal")
            multisigAdmin.endorsement([multisigAdmin.data.lastProposalId]).run(
                sender=signer1
            )
            sc.verify(multisigAdmin.data.signers.contains(signer3.address))
            sc.verify(sp.len(multisigAdmin.data.signers) == 3)

            ############################################
            # New proposal (change Quorum from 2 to 3) #
            ############################################
            sc.h2("New proposal (change Quorum from 2 to 3)")
            sc.h3("signer1 new proposal to change quorum to 3")
            changeQuorum = InternalHelper.changeQuorum(3)
            multisigAdmin.proposal(InternalHelper.variant([changeQuorum])).run(
                sender=signer1
            )
            # Proposal has not been validated yet
            sc.verify(multisigAdmin.data.quorum == 2)
            sc.h3("signer2 votes the proposal (2/2)")
            multisigAdmin.endorsement([multisigAdmin.data.lastProposalId]).run(
                sender=signer2
            )
            sc.verify(multisigAdmin.data.quorum == 3)

            ###########################################
            # Newly included signer starts a proposal #
            ###########################################
            sc.h2("Newly included signer starts a proposal")
            sc.h3("New proposal by signer 3 to decrease quorum to 2")
            changeQuorum = InternalHelper.changeQuorum(2)
            multisigAdmin.proposal(InternalHelper.variant([changeQuorum])).run(
                sender=signer3
            )
            sc.h3("signer1 votes the proposal")
            multisigAdmin.endorsement([multisigAdmin.data.lastProposalId]).run(
                sender=signer1
            )
            sc.verify(multisigAdmin.data.quorum == 3)
            sc.h3("signer2 votes the proposal")
            multisigAdmin.endorsement([multisigAdmin.data.lastProposalId]).run(
                sender=signer2
            )
            sc.verify(multisigAdmin.data.quorum == 2)

            ##########
            # Cancel #
            ##########
            sc.h2("Proposal cancellation")
            sc.h3("New proposal by signer 1")
            changeTimeout = InternalHelper.changeQuorum(3)
            multisigAdmin.proposal(InternalHelper.variant([changeTimeout])).run(
                sender=signer1
            )
            sc.verify(sp.len(multisigAdmin.data.activeProposals) == 1)
            sc.h3(
                "Signer 2 tries to cancel the proposal (must fail, only the initiator can cancel)"
            )
            multisigAdmin.cancel_proposal(multisigAdmin.data.lastProposalId).run(
                sender=signer2, valid=False
            )
            sc.h3("Signer 1 cancels the proposal")
            multisigAdmin.cancel_proposal(multisigAdmin.data.lastProposalId).run(
                sender=signer1
            )
            sc.verify(sp.len(multisigAdmin.data.activeProposals) == 0)
            sc.h3("Signer 2 tries to vote the canceled proposal")
            multisigAdmin.endorsement([multisigAdmin.data.lastProposalId]).run(
                sender=signer2, valid=False
            )
            sc.verify(multisigAdmin.data.quorum != 3)

            ######################
            # 2 actions proposal #
            ######################
            sc.h2("2 actions proposal")
            sc.h3("Signer 1 new proposal: change quorum to 2 and add signer 4")
            sc.verify(~multisigAdmin.data.signers.contains(signer4.address))
            changeQuorum = InternalHelper.changeQuorum(3)
            changeSigners = InternalHelper.addSigners(
                [(signer4.address, signer4.public_key)]
            )
            multisigAdmin.proposal(
                InternalHelper.variant([changeQuorum, changeSigners])
            ).run(sender=signer1)
            sc.h3("Signer 2 votes the proposal")
            multisigAdmin.endorsement([multisigAdmin.data.lastProposalId]).run(
                sender=signer2
            )
            sc.verify(multisigAdmin.data.quorum == 3)
            sc.verify(multisigAdmin.data.signers.contains(signer4.address))

            #########################################
            # 2 Internal proposals at the same time #
            #########################################
            sc.h3("Signer 1 new proposal: change quorum to 2 and remove signer 4")
            changeQuorum = InternalHelper.changeQuorum(2)
            multisigAdmin.proposal(InternalHelper.variant([changeQuorum])).run(
                sender=signer1
            )
            changeSigners = InternalHelper.removeSigners([signer4.address])
            multisigAdmin.proposal(InternalHelper.variant([changeSigners])).run(
                sender=signer2
            )
            sc.verify(sp.len(multisigAdmin.data.activeProposals) == 2)
            sc.h3("Signer 3 votes on quorum proposal")
            multisigAdmin.endorsement(
                [sp.as_nat(multisigAdmin.data.lastProposalId - 1)]
            ).run(sender=signer3)
            sc.h3("Signer 4 votes on signers proposal")
            multisigAdmin.endorsement([multisigAdmin.data.lastProposalId]).run(
                sender=signer4
            )
            sc.h3("Confirm that nothing has changed")
            sc.verify(multisigAdmin.data.quorum == 3)
            sc.verify(multisigAdmin.data.signers.contains(signer4.address))
            sc.h3("Signer 4 votes on quorum proposal")
            multisigAdmin.endorsement(
                [sp.as_nat(multisigAdmin.data.lastProposalId - 1)]
            ).run(sender=signer4)
            sc.h3(
                "Confirm that quorum was updated and signers proposal was canceled"
            )
            sc.verify(sp.len(multisigAdmin.data.activeProposals) == 0)
            sc.verify(multisigAdmin.data.quorum == 2)
            sc.verify(multisigAdmin.data.signers.contains(signer4.address))

            #########################
            # Multisig endorsements #
            #########################
            sc.h2("Multi vote in one call")
            sc.h3("Signer 1 new proposal")
            changeQuorum = InternalHelper.changeQuorum(3)
            multisigAdmin.proposal(InternalHelper.variant([changeQuorum])).run(
                sender=signer1
            )
            sc.h3("Signer 2 and Signer 3 votes are pushed by Signer 1")
            signer2_endorsement = sign(signer2, contract=multisigAdmin)
            signer3_endorsement = sign(signer3, contract=multisigAdmin)
            proposalEndorsements = sp.record(
                proposalId=multisigAdmin.data.lastProposalId,
                signatures=[signer2_endorsement, signer3_endorsement],
            )
            multisigAdmin.aggregated_endorsement([proposalEndorsements]).run(
                sender=signer1
            )
            sc.verify(multisigAdmin.data.quorum == 3)

            #####################
            # Multisig proposal #
            #####################
            sc.h2("Multi vote in one call")
            sc.h3("Signer 1 new proposal")
            changeQuorum = InternalHelper.changeQuorum(3)
            multisigAdmin.proposal(InternalHelper.variant([changeQuorum])).run(
                sender=signer1
            )
            sc.h3("Signer 2 and Signer 3 votes are pushed by Signer 1")
            signer2_vote = sign(signer2, contract=multisigAdmin)
            signer3_vote = sign(signer3, contract=multisigAdmin)
            proposalVotes = sp.record(
                proposalId=multisigAdmin.data.lastProposalId,
                signatures=[signer2_vote, signer3_vote],
            )
            multisigAdmin.aggregated_endorsement([proposalVotes]).run(
                sender=signer1
            )
            sc.verify(multisigAdmin.data.quorum == 3)

        ##########################################

        else:

            sc.h3("Originate Multisig Admin")
            multisigAdmin = main.MultisigAdmin(
                quorum=3,
                signers=sp.map(
                    {
                        signer1.address: sp.record(
                            publicKey=signer1.public_key, lastProposalId=sp.none
                        ),
                        signer2.address: sp.record(
                            publicKey=signer2.public_key, lastProposalId=sp.none
                        ),
                        signer3.address: sp.record(
                            publicKey=signer3.public_key, lastProposalId=sp.none
                        ),
                    }
                ),
                metadata=sp.utils.metadata_of_url("ipfs://"),
            )
            sc += multisigAdmin

            sc.h3("Originate administrated contract")
            administrated = helpers.Administrated(admin.address, False)
            sc += administrated
            administrated_entrypoint = sp.contract(
                sp.TBytes, administrated.address, entrypoint="administrate"
            ).open_some()

            sc.h2("Set multisig as admin of administrated contract")
            sc.verify(administrated.data.active == False)
            sc.verify(administrated.data.admin == admin.address)
            actions = packActions(
                [ExternalHelper.changeAdmin(multisigAdmin.address)]
            )
            administrated.administrate(actions).run(sender=admin)
            sc.verify(administrated.data.active == False)
            sc.verify(administrated.data.admin == multisigAdmin.address)

            sc.h2("Activate the administrated contract")
            sc.h3("Signer 1 new proposal: changeActive")
            actions = packActions([ExternalHelper.changeActive(True)])
            multisigAdmin.proposal(
                ExternalHelper.variant(
                    [
                        sp.record(
                            target=sp.to_address(administrated_entrypoint),
                            actions=actions,
                        )
                    ]
                )
            ).run(sender=signer1)
            sc.verify(administrated.data.active == False)
            sc.h3("Signer 2 votes")
            multisigAdmin.endorsement([multisigAdmin.data.lastProposalId]).run(
                sender=signer2
            )
            sc.verify(administrated.data.active == False)
            sc.h3("Signer 3 votes")
            multisigAdmin.endorsement([multisigAdmin.data.lastProposalId]).run(
                sender=signer3
            )
            sc.verify(administrated.data.active == True)

            sc.h2("Use Multisig vote to deactivate the administrated contract")
            sc.h3("Signer 1 new proposal: changeActive")
            actions = packActions([ExternalHelper.changeActive(False)])
            multisigAdmin.proposal(
                ExternalHelper.variant(
                    [
                        sp.record(
                            target=sp.to_address(administrated_entrypoint),
                            actions=actions,
                        )
                    ]
                )
            ).run(sender=signer1)
            sc.verify(administrated.data.active == True)
            sc.h3("Signer 2 and Signer 3 votes are pushed by Signer 1")
            signer2_vote = sign(signer2, contract=multisigAdmin)
            signer3_vote = sign(signer3, contract=multisigAdmin)
            proposalVotes = sp.record(
                proposalId=multisigAdmin.data.lastProposalId,
                signatures=[signer2_vote, signer3_vote],
            )
            multisigAdmin.aggregated_endorsement([proposalVotes]).run(
                sender=signer1
            )
            sc.verify(administrated.data.active == False)

Classes

class InternalHelper
Expand source code
class InternalHelper:
    def variant(content):
        return sp.variant("internal", content)

    def changeQuorum(quorum):
        return sp.variant("changeQuorum", quorum)

    def removeSigners(l):
        return sp.variant("changeSigners", sp.variant("removed", sp.set(l)))

    def addSigners(l):
        added_list = []
        for added_info in l:
            addr, publicKey = added_info
            added_list.append(sp.record(address=addr, publicKey=publicKey))
        return sp.variant("changeSigners", sp.variant("added", sp.list(added_list)))

Methods

def variant(content)
Expand source code
def variant(content):
    return sp.variant("internal", content)
def changeQuorum(quorum)
Expand source code
def changeQuorum(quorum):
    return sp.variant("changeQuorum", quorum)
def removeSigners(l)
Expand source code
def removeSigners(l):
    return sp.variant("changeSigners", sp.variant("removed", sp.set(l)))
def addSigners(l)
Expand source code
def addSigners(l):
    added_list = []
    for added_info in l:
        addr, publicKey = added_info
        added_list.append(sp.record(address=addr, publicKey=publicKey))
    return sp.variant("changeSigners", sp.variant("added", sp.list(added_list)))
class ExternalHelper
Expand source code
class ExternalHelper:
    def variant(content):
        return sp.variant("external", content)

    def changeActive(active):
        return sp.variant("changeActive", active)

    def changeAdmin(address):
        return sp.variant("changeAdmin", address)

Methods

def variant(content)
Expand source code
def variant(content):
    return sp.variant("external", content)
def changeActive(active)
Expand source code
def changeActive(active):
    return sp.variant("changeActive", active)
def changeAdmin(address)
Expand source code
def changeAdmin(address):
    return sp.variant("changeAdmin", address)