บทเรียนที่ 1

Introduction and Multisig Contracts

Multisignature (multisig) contracts, also known as "M-of-N" contracts, are a crucial mechanism used to increase the security and flexibility of transactions in the blockchain environment. These contracts change the way control is exerted over assets by requiring approval from multiple parties before transactions can be executed. The term "M-of-N" refers to the requirement that M out of N total parties must approve the transaction for it to be valid.

Theory of Multisig Contracts

Multisig contracts provide a means of creating shared control over assets. The typical use cases involve escrow services, corporate account management, co-signing financial agreements, and more. These contracts are exceptionally beneficial for organizations or groups where collective decision-making is necessary.

By design, multisig contracts are tamper-resistant and prevent single points of failure. Even if one party’s keys are compromised, the attacker cannot execute transactions without approval from the other parties. This adds an extra layer of security.

Multisig contracts can be thought of as a digital equivalent of a safe deposit box that requires multiple keys to open. The total number of keys (N) and the minimum number of keys required to open the box (M) are agreed upon when the contract is created.

Multisig contracts can have many different configurations depending on the values of M and N:

  • 1-of-N: A single party out of the total can approve the transaction. This configuration is the same as a regular transaction without multisig. It might be used where several keys exist for convenience, but any one of them can approve transactions.
  • N-of-N: All parties must approve the transaction. This configuration provides the highest level of security but can become problematic if a party loses their key or refuses to approve transactions.
  • M-of-N (where M < N): A subset of the total parties must approve the transaction. This configuration is often used in practice because it balances security with flexibility.

Multisig Contracts in Blockchain

In the context of blockchain, multisig contracts are used widely to enhance transaction security, support complex governance mechanisms, or maintain flexible control over blockchain assets. Here are some examples:

  • Wallets: Multisig wallets are used to secure assets. They require multiple parties to sign off on transactions, thus providing added security against theft, external hacks, and insider threats.
  • Decentralized Autonomous Organizations (DAOs): DAOs often use multisig contracts to enforce their governance rules. Votes on proposals are implemented as multisig transactions, with members of the DAO acting as the signatories. The proposal is executed only if it receives enough votes.
  • Cross-chain operations: In cross-chain operations, multisig contracts can act as custodians of assets. When assets are moved from one blockchain to another, a multisig contract on the originating chain can ensure that the assets are locked up safely until the operation on the other chain is confirmed.
    While the implementation of multisig contracts can vary from one blockchain to another, the core concept remains the same - the need for multiple parties to approve a transaction before it’s executed. This added layer of security makes multisig contracts an essential tool in the blockchain and crypto space.

Coding Example: Writing and Deploying Multisig Contracts with SmartPy

As for our code examples, we will be looking at three different multisignature contract implementations:

The Lambda Contract

It is quite versatile and allows for a broad range of uses. It requires multiple signatures to execute arbitrary lambda functions.

Python
import smartpy as sp


@sp.module
def main():
    operation_lambda: type = sp.lambda_(sp.unit, sp.unit, with_operations=True)

    class MultisigLambda(sp.Contract):
        """Multiple members vote for executing lambdas.

        This contract can be originated with a list of addresses and a number of
        required votes. Any member can submit as much lambdas as he wants and vote
        for active proposals. When a lambda reaches the required votes, its code is
        called and the output operations are executed. This allows this contract to
        do anything that a contract can do: transferring tokens, managing assets,
        administrating another contract...

        When a lambda is applied, all submitted lambdas until now are inactivated.
        The members can still submit new lambdas.
        """

        def __init__(self, members, required_votes):
            """Constructor

            Args:
                members (sp.set of sp.address): people who can submit and vote
                    for lambda.
                required_votes (sp.nat): number of votes required
            """
            assert required_votes <= sp.len(
                members
            ), "required_votes must be <= len(members)"
            self.data.lambdas = sp.cast(
                sp.big_map(), sp.big_map[sp.nat, operation_lambda]
            )
            self.data.votes = sp.cast(
                sp.big_map(), sp.big_map[sp.nat, sp.set[sp.address]]
            )
            self.data.nextId = 0
            self.data.inactiveBefore = 0
            self.data.members = sp.cast(members, sp.set[sp.address])
            self.data.required_votes = sp.cast(required_votes, sp.nat)

        @sp.entrypoint
        def submit_lambda(self, lambda_):
            """Submit a new lambda to the vote.

            Submitting a proposal does not imply casting a vote in favour of it.

            Args:
                lambda_(sp.lambda with operations): lambda proposed to vote.
            Raises:
                `You are not a member`
            """
            assert self.data.members.contains(sp.sender), "You are not a member"
            self.data.lambdas[self.data.nextId] = lambda_
            self.data.votes[self.data.nextId] = sp.set()
            self.data.nextId += 1

        @sp.entrypoint
        def vote_lambda(self, id):
            """Vote for a lambda.

            Args:
                id(sp.nat): id of the lambda to vote for.
            Raises:
                `You are not a member`, `The lambda is inactive`, `Lambda not found`

            There is no vote against or pass. If someone disagrees with a lambda
            they can avoid to vote.
            """
            assert self.data.members.contains(sp.sender), "You are not a member"
            assert id >= self.data.inactiveBefore, "The lambda is inactive"
            assert self.data.lambdas.contains(id), "Lambda not found"
            self.data.votes[id].add(sp.sender)
            if sp.len(self.data.votes[id]) >= self.data.required_votes:
                self.data.lambdas[id]()
                self.data.inactiveBefore = self.data.nextId

        @sp.onchain_view()
        def get_lambda(self, id):
            """Return the corresponding lambda.

            Args:
                id (sp.nat): id of the lambda to get.

            Return:
                pair of the lambda and a boolean showing if the lambda is active.
            """
            return (self.data.lambdas[id], id >= self.data.inactiveBefore)


# if "templates" not in __name__:


@sp.module
def test():
    class Administrated(sp.Contract):
        def __init__(self, admin):
            self.data.admin = admin
            self.data.value = sp.int(0)

        @sp.entrypoint
        def set_value(self, value):
            assert sp.sender == self.data.admin
            self.data.value = value


@sp.add_test(name="MultisigLambda basic scenario", is_default=True)
def basic_scenario():
    """Use the multisigLambda as an administrator of an example contract.

    Tests:
    - Origination
    - Lambda submission
    - Lambda vote
    """
    sc = sp.test_scenario([main, test])
    sc.h1("Basic scenario.")

    member1 = sp.test_account("member1")
    member2 = sp.test_account("member2")
    member3 = sp.test_account("member3")
    members = sp.set([member1.address, member2.address, member3.address])

    sc.h2("MultisigLambda: origination")
    c1 = main.MultisigLambda(members, 2)
    sc += c1

    sc.h2("Administrated: origination")
    c2 = test.Administrated(c1.address)
    sc += c2

    sc.h2("MultisigLambda: submit_lambda")

    def set_42(params):
        administrated = sp.contract(sp.TInt, c2.address, entrypoint="set_value")
        sp.transfer(sp.int(42), sp.tez(0), administrated.open_some())

    lambda_ = sp.build_lambda(set_42, with_operations=True)
    c1.submit_lambda(lambda_).run(sender=member1)

    sc.h2("MultisigLambda: vote_lambda")
    c1.vote_lambda(0).run(sender=member1)
    c1.vote_lambda(0).run(sender=member2)

    # We can check that the administrated contract received the transfer.
    sc.verify(c2.data.value == 42)

The MultisigAction Contract

It introduces the concept of voting for proposals. In this contract, signers can vote for certain actions to be taken, and if a quorum is reached, the proposed actions are executed.

Python
import smartpy as sp


@sp.module
def main():
    # Internal administration action type specification
    InternalAdminAction: type = sp.variant(
        addSigners=sp.list[sp.address],
        changeQuorum=sp.nat,
        removeSigners=sp.list[sp.address],
    )

    class MultisigAction(sp.Contract):
        """A contract that can be used by multiple signers to administrate other
        contracts. The administrated contracts implement an interface that make it
        possible to explicit the administration process to non expert users.

        Signers vote for proposals. A proposal is a list of a target with a list of
        action. An action is a simple byte but it is intended to be a pack value of
        a variant. This simple pattern make it possible to build a UX interface
        that shows the content of a proposal or build one.
        """

        def __init__(self, quorum, signers):
            self.data.inactiveBefore = 0
            self.data.nextId = 0
            self.data.proposals = sp.cast(
                sp.big_map(),
                sp.big_map[
                    sp.nat,
                    sp.list[sp.record(target=sp.address, actions=sp.list[sp.bytes])],
                ],
            )
            self.data.quorum = sp.cast(quorum, sp.nat)
            self.data.signers = sp.cast(signers, sp.set[sp.address])
            self.data.votes = sp.cast(
                sp.big_map(), sp.big_map[sp.nat, sp.set[sp.address]]
            )

        @sp.entrypoint
        def send_proposal(self, proposal):
            """Signer-only. Submit a proposal to the vote.

            Args:
                proposal (sp.list of sp.record of target address and action): List\
                    of target and associated administration actions.
            """
            assert self.data.signers.contains(sp.sender), "Only signers can propose"
            self.data.proposals[self.data.nextId] = proposal
            self.data.votes[self.data.nextId] = sp.set()
            self.data.nextId += 1

        @sp.entrypoint
        def vote(self, pId):
            """Vote for one or more proposals

            Args:
                pId (sp.nat): Id of the proposal.
            """
            assert self.data.signers.contains(sp.sender), "Only signers can vote"
            assert self.data.votes.contains(pId), "Proposal unknown"
            assert pId >= self.data.inactiveBefore, "The proposal is inactive"
            self.data.votes[pId].add(sp.sender)

            if sp.len(self.data.votes.get(pId, default=sp.set())) >= self.data.quorum:
                self._onApproved(pId)

        @sp.private(with_storage="read-write", with_operations=True)
        def _onApproved(self, pId):
            """Inlined function. Logic applied when a proposal has been approved."""
            proposal = self.data.proposals.get(pId, default=[])
            for p_item in proposal:
                contract = sp.contract(sp.list[sp.bytes], p_item.target)
                sp.transfer(
                    p_item.actions,
                    sp.tez(0),
                    contract.unwrap_some(error="InvalidTarget"),
                )
            # Inactivate all proposals that have been already submitted.
            self.data.inactiveBefore = self.data.nextId

        @sp.entrypoint
        def administrate(self, actions):
            """Self-call only. Administrate this contract.

            This entrypoint must be called through the proposal system.

            Args:
                actions (sp.list of sp.bytes): List of packed variant of \
                    `InternalAdminAction` (`addSigners`, `changeQuorum`, `removeSigners`).
            """
            assert (
                sp.sender == sp.self_address()
            ), "This entrypoint must be called through the proposal system."

            for packed_actions in actions:
                action = sp.unpack(packed_actions, InternalAdminAction).unwrap_some(
                    error="Bad actions format"
                )
                with sp.match(action):
                    with sp.case.changeQuorum as quorum:
                        self.data.quorum = quorum
                    with sp.case.addSigners as added:
                        for signer in added:
                            self.data.signers.add(signer)
                    with sp.case.removeSigners as removed:
                        for address in removed:
                            self.data.signers.remove(address)
                # Ensure that the contract never requires more quorum than the total of signers.
                assert self.data.quorum <= sp.len(
                    self.data.signers
                ), "More quorum than signers."


if "templates" not in __name__:

    @sp.add_test(name="Basic scenario", is_default=True)
    def test():
        signer1 = sp.test_account("signer1")
        signer2 = sp.test_account("signer2")
        signer3 = sp.test_account("signer3")

        s = sp.test_scenario(main)
        s.h1("Basic scenario")

        s.h2("Origination")
        c1 = main.MultisigAction(
            quorum=2,
            signers=sp.set([signer1.address, signer2.address]),
        )
        s += c1

        s.h2("Proposal for adding a new signer")
        target = sp.to_address(
            sp.contract(sp.TList(sp.TBytes), c1.address, "administrate").open_some()
        )
        action = sp.pack(
            sp.set_type_expr(
                sp.variant("addSigners", [signer3.address]), main.InternalAdminAction
            )
        )
        c1.send_proposal([sp.record(target=target, actions=[action])]).run(
            sender=signer1
        )

        s.h2("Signer 1 votes for the proposal")
        c1.vote(0).run(sender=signer1)
        s.h2("Signer 2 votes for the proposal")
        c1.vote(0).run(sender=signer2)

        s.verify(c1.data.signers.contains(signer3.address))

The MultisigView Contract

It also utilizes a voting mechanism. This contract allows members to submit and vote for arbitrary bytes. Once a proposal achieves the required number of votes, its status can be confirmed via a view.

Python
import smartpy as sp


@sp.module
def main():
    class MultisigView(sp.Contract):
        """Multiple members vote for arbitrary bytes.

        This contract can be originated with a list of addresses and a number of
        required votes. Any member can submit as many bytes as they want and vote
        for active proposals.

        Any bytes that reached the required votes can be confirmed via a view.
        """

        def __init__(self, members, required_votes):
            """Constructor

            Args:
                members (sp.set of sp.address): people who can submit and vote for
                    lambda.
                required_votes (sp.nat): number of votes required
            """
            assert required_votes <= sp.len(
                members
            ), "required_votes must be <= len(members)"
            self.data.proposals = sp.cast(sp.big_map(), sp.big_map[sp.bytes, sp.bool])
            self.data.votes = sp.cast(
                sp.big_map(), sp.big_map[sp.bytes, sp.set[sp.address]]
            )
            self.data.members = sp.cast(members, sp.set[sp.address])
            self.data.required_votes = sp.cast(required_votes, sp.nat)

        @sp.entrypoint
        def submit_proposal(self, bytes):
            """Submit a new proposal to the vote.

            Submitting a proposal does not imply casting a vote in favour of it.

            Args:
                bytes(sp.bytes): bytes proposed to vote.
            Raises:
                `You are not a member`
            """
            assert self.data.members.contains(sp.sender), "You are not a member"
            self.data.proposals[bytes] = False
            self.data.votes[bytes] = sp.set()

        @sp.entrypoint
        def vote_proposal(self, bytes):
            """Vote for a proposal.

            There is no vote against or pass. If one disagrees with a proposal they
            can avoid to vote. Warning: old non-voted proposals never become
            obsolete.

            Args:
                id(sp.bytes): bytes of the proposal.
            Raises:
                `You are not a member`, `Proposal not found`
            """
            assert self.data.members.contains(sp.sender), "You are not a member"
            assert self.data.proposals.contains(bytes), "Proposal not found"
            self.data.votes[bytes].add(sp.sender)
            if sp.len(self.data.votes[bytes]) >= self.data.required_votes:
                self.data.proposals[bytes] = True

        @sp.onchain_view()
        def is_voted(self, id):
            """Returns a boolean indicating whether the proposal has been voted on.

            Args:
                id (sp.bytes): bytes of the proposal
            Return:
                (sp.bool): True if the proposal has been voted, False otherwise.
            """
            return self.data.proposals.get(id, error="Proposal not found")


if "templates" not in __name__:

    @sp.add_test(name="MultisigView basic scenario", is_default=True)
    def basic_scenario():
        """A scenario with a vote on the multisigView contract.

        Tests:
        - Origination
        - Proposal submission
        - Proposal vote
        """
        sc = sp.test_scenario(main)
        sc.h1("Basic scenario.")

        member1 = sp.test_account("member1")
        member2 = sp.test_account("member2")
        member3 = sp.test_account("member3")
        members = sp.set([member1.address, member2.address, member3.address])

        sc.h2("Origination")
        c1 = main.MultisigView(members, 2)
        sc += c1

        sc.h2("submit_proposal")
        c1.submit_proposal(sp.bytes("0x42")).run(sender=member1)

        sc.h2("vote_proposal")
        c1.vote_proposal(sp.bytes("0x42")).run(sender=member1)
        c1.vote_proposal(sp.bytes("0x42")).run(sender=member2)

        # We can check that the proposal has been validated.
        sc.verify(c1.is_voted(sp.bytes("0x42")))

Each contract provides a different mechanism for achieving multi-signature control, offering flexibility depending on the specific needs of your blockchain use case.

Step-by-step Guide to Try the Multisig Contract on SmartPy Online

To try the multisig contracts we’ve written in SmartPy, you can follow these steps:

  1. Go to the SmartPy IDE at https://smartpy.io/ide.

  2. Paste the contract code into the editor. You can replace the existing code.

  3. To execute the contract, click on the “Run” button located on the top panel.

  4. After running the contract, you can view the scenario execution in the “Output” panel on the right. Here, you can see details of each action, including proposals, votes, and approvals.

  5. To deploy your contract on the Tezos network, you first need to compile it. Click the “Compile” button on the top panel.

  6. After compiling, you can deploy the contract onto the testnet by clicking “Deploy Michelson Contract”. You’ll need to provide a Secret Key for a Tezos account with enough funds to pay for the gas costs of deployment.

  7. Once the contract is deployed, you’ll be provided with the contract’s address on the blockchain. You can use this address to interact with the contract via transactions.

  8. To submit proposals or vote in the contracts, you can use the entrypoints defined in the contract code, such as submit_proposal or vote_proposal. These can be called directly from transactions that you create.

Remember, while the SmartPy IDE allows you to test your contract on a simulated blockchain, deploying the contract onto the actual Tezos network will incur gas costs, which must be paid in XTZ, the native cryptocurrency of the Tezos network.

ข้อจำกัดความรับผิด
* การลงทุนคริปโตมีความเสี่ยงสูง โปรดดำเนินการด้วยความระมัดระวัง หลักสูตรนี้ไม่ได้มีไว้เพื่อเป็นคำแนะนำในการลงทุน
* หลักสูตรนี้สร้างขึ้นโดยผู้เขียนที่ได้เข้าร่วม Gate Learn ความคิดเห็นของผู้เขียนไม่ได้มาจาก Gate Learn
แคตตาล็อก
บทเรียนที่ 1

Introduction and Multisig Contracts

Multisignature (multisig) contracts, also known as "M-of-N" contracts, are a crucial mechanism used to increase the security and flexibility of transactions in the blockchain environment. These contracts change the way control is exerted over assets by requiring approval from multiple parties before transactions can be executed. The term "M-of-N" refers to the requirement that M out of N total parties must approve the transaction for it to be valid.

Theory of Multisig Contracts

Multisig contracts provide a means of creating shared control over assets. The typical use cases involve escrow services, corporate account management, co-signing financial agreements, and more. These contracts are exceptionally beneficial for organizations or groups where collective decision-making is necessary.

By design, multisig contracts are tamper-resistant and prevent single points of failure. Even if one party’s keys are compromised, the attacker cannot execute transactions without approval from the other parties. This adds an extra layer of security.

Multisig contracts can be thought of as a digital equivalent of a safe deposit box that requires multiple keys to open. The total number of keys (N) and the minimum number of keys required to open the box (M) are agreed upon when the contract is created.

Multisig contracts can have many different configurations depending on the values of M and N:

  • 1-of-N: A single party out of the total can approve the transaction. This configuration is the same as a regular transaction without multisig. It might be used where several keys exist for convenience, but any one of them can approve transactions.
  • N-of-N: All parties must approve the transaction. This configuration provides the highest level of security but can become problematic if a party loses their key or refuses to approve transactions.
  • M-of-N (where M < N): A subset of the total parties must approve the transaction. This configuration is often used in practice because it balances security with flexibility.

Multisig Contracts in Blockchain

In the context of blockchain, multisig contracts are used widely to enhance transaction security, support complex governance mechanisms, or maintain flexible control over blockchain assets. Here are some examples:

  • Wallets: Multisig wallets are used to secure assets. They require multiple parties to sign off on transactions, thus providing added security against theft, external hacks, and insider threats.
  • Decentralized Autonomous Organizations (DAOs): DAOs often use multisig contracts to enforce their governance rules. Votes on proposals are implemented as multisig transactions, with members of the DAO acting as the signatories. The proposal is executed only if it receives enough votes.
  • Cross-chain operations: In cross-chain operations, multisig contracts can act as custodians of assets. When assets are moved from one blockchain to another, a multisig contract on the originating chain can ensure that the assets are locked up safely until the operation on the other chain is confirmed.
    While the implementation of multisig contracts can vary from one blockchain to another, the core concept remains the same - the need for multiple parties to approve a transaction before it’s executed. This added layer of security makes multisig contracts an essential tool in the blockchain and crypto space.

Coding Example: Writing and Deploying Multisig Contracts with SmartPy

As for our code examples, we will be looking at three different multisignature contract implementations:

The Lambda Contract

It is quite versatile and allows for a broad range of uses. It requires multiple signatures to execute arbitrary lambda functions.

Python
import smartpy as sp


@sp.module
def main():
    operation_lambda: type = sp.lambda_(sp.unit, sp.unit, with_operations=True)

    class MultisigLambda(sp.Contract):
        """Multiple members vote for executing lambdas.

        This contract can be originated with a list of addresses and a number of
        required votes. Any member can submit as much lambdas as he wants and vote
        for active proposals. When a lambda reaches the required votes, its code is
        called and the output operations are executed. This allows this contract to
        do anything that a contract can do: transferring tokens, managing assets,
        administrating another contract...

        When a lambda is applied, all submitted lambdas until now are inactivated.
        The members can still submit new lambdas.
        """

        def __init__(self, members, required_votes):
            """Constructor

            Args:
                members (sp.set of sp.address): people who can submit and vote
                    for lambda.
                required_votes (sp.nat): number of votes required
            """
            assert required_votes <= sp.len(
                members
            ), "required_votes must be <= len(members)"
            self.data.lambdas = sp.cast(
                sp.big_map(), sp.big_map[sp.nat, operation_lambda]
            )
            self.data.votes = sp.cast(
                sp.big_map(), sp.big_map[sp.nat, sp.set[sp.address]]
            )
            self.data.nextId = 0
            self.data.inactiveBefore = 0
            self.data.members = sp.cast(members, sp.set[sp.address])
            self.data.required_votes = sp.cast(required_votes, sp.nat)

        @sp.entrypoint
        def submit_lambda(self, lambda_):
            """Submit a new lambda to the vote.

            Submitting a proposal does not imply casting a vote in favour of it.

            Args:
                lambda_(sp.lambda with operations): lambda proposed to vote.
            Raises:
                `You are not a member`
            """
            assert self.data.members.contains(sp.sender), "You are not a member"
            self.data.lambdas[self.data.nextId] = lambda_
            self.data.votes[self.data.nextId] = sp.set()
            self.data.nextId += 1

        @sp.entrypoint
        def vote_lambda(self, id):
            """Vote for a lambda.

            Args:
                id(sp.nat): id of the lambda to vote for.
            Raises:
                `You are not a member`, `The lambda is inactive`, `Lambda not found`

            There is no vote against or pass. If someone disagrees with a lambda
            they can avoid to vote.
            """
            assert self.data.members.contains(sp.sender), "You are not a member"
            assert id >= self.data.inactiveBefore, "The lambda is inactive"
            assert self.data.lambdas.contains(id), "Lambda not found"
            self.data.votes[id].add(sp.sender)
            if sp.len(self.data.votes[id]) >= self.data.required_votes:
                self.data.lambdas[id]()
                self.data.inactiveBefore = self.data.nextId

        @sp.onchain_view()
        def get_lambda(self, id):
            """Return the corresponding lambda.

            Args:
                id (sp.nat): id of the lambda to get.

            Return:
                pair of the lambda and a boolean showing if the lambda is active.
            """
            return (self.data.lambdas[id], id >= self.data.inactiveBefore)


# if "templates" not in __name__:


@sp.module
def test():
    class Administrated(sp.Contract):
        def __init__(self, admin):
            self.data.admin = admin
            self.data.value = sp.int(0)

        @sp.entrypoint
        def set_value(self, value):
            assert sp.sender == self.data.admin
            self.data.value = value


@sp.add_test(name="MultisigLambda basic scenario", is_default=True)
def basic_scenario():
    """Use the multisigLambda as an administrator of an example contract.

    Tests:
    - Origination
    - Lambda submission
    - Lambda vote
    """
    sc = sp.test_scenario([main, test])
    sc.h1("Basic scenario.")

    member1 = sp.test_account("member1")
    member2 = sp.test_account("member2")
    member3 = sp.test_account("member3")
    members = sp.set([member1.address, member2.address, member3.address])

    sc.h2("MultisigLambda: origination")
    c1 = main.MultisigLambda(members, 2)
    sc += c1

    sc.h2("Administrated: origination")
    c2 = test.Administrated(c1.address)
    sc += c2

    sc.h2("MultisigLambda: submit_lambda")

    def set_42(params):
        administrated = sp.contract(sp.TInt, c2.address, entrypoint="set_value")
        sp.transfer(sp.int(42), sp.tez(0), administrated.open_some())

    lambda_ = sp.build_lambda(set_42, with_operations=True)
    c1.submit_lambda(lambda_).run(sender=member1)

    sc.h2("MultisigLambda: vote_lambda")
    c1.vote_lambda(0).run(sender=member1)
    c1.vote_lambda(0).run(sender=member2)

    # We can check that the administrated contract received the transfer.
    sc.verify(c2.data.value == 42)

The MultisigAction Contract

It introduces the concept of voting for proposals. In this contract, signers can vote for certain actions to be taken, and if a quorum is reached, the proposed actions are executed.

Python
import smartpy as sp


@sp.module
def main():
    # Internal administration action type specification
    InternalAdminAction: type = sp.variant(
        addSigners=sp.list[sp.address],
        changeQuorum=sp.nat,
        removeSigners=sp.list[sp.address],
    )

    class MultisigAction(sp.Contract):
        """A contract that can be used by multiple signers to administrate other
        contracts. The administrated contracts implement an interface that make it
        possible to explicit the administration process to non expert users.

        Signers vote for proposals. A proposal is a list of a target with a list of
        action. An action is a simple byte but it is intended to be a pack value of
        a variant. This simple pattern make it possible to build a UX interface
        that shows the content of a proposal or build one.
        """

        def __init__(self, quorum, signers):
            self.data.inactiveBefore = 0
            self.data.nextId = 0
            self.data.proposals = sp.cast(
                sp.big_map(),
                sp.big_map[
                    sp.nat,
                    sp.list[sp.record(target=sp.address, actions=sp.list[sp.bytes])],
                ],
            )
            self.data.quorum = sp.cast(quorum, sp.nat)
            self.data.signers = sp.cast(signers, sp.set[sp.address])
            self.data.votes = sp.cast(
                sp.big_map(), sp.big_map[sp.nat, sp.set[sp.address]]
            )

        @sp.entrypoint
        def send_proposal(self, proposal):
            """Signer-only. Submit a proposal to the vote.

            Args:
                proposal (sp.list of sp.record of target address and action): List\
                    of target and associated administration actions.
            """
            assert self.data.signers.contains(sp.sender), "Only signers can propose"
            self.data.proposals[self.data.nextId] = proposal
            self.data.votes[self.data.nextId] = sp.set()
            self.data.nextId += 1

        @sp.entrypoint
        def vote(self, pId):
            """Vote for one or more proposals

            Args:
                pId (sp.nat): Id of the proposal.
            """
            assert self.data.signers.contains(sp.sender), "Only signers can vote"
            assert self.data.votes.contains(pId), "Proposal unknown"
            assert pId >= self.data.inactiveBefore, "The proposal is inactive"
            self.data.votes[pId].add(sp.sender)

            if sp.len(self.data.votes.get(pId, default=sp.set())) >= self.data.quorum:
                self._onApproved(pId)

        @sp.private(with_storage="read-write", with_operations=True)
        def _onApproved(self, pId):
            """Inlined function. Logic applied when a proposal has been approved."""
            proposal = self.data.proposals.get(pId, default=[])
            for p_item in proposal:
                contract = sp.contract(sp.list[sp.bytes], p_item.target)
                sp.transfer(
                    p_item.actions,
                    sp.tez(0),
                    contract.unwrap_some(error="InvalidTarget"),
                )
            # Inactivate all proposals that have been already submitted.
            self.data.inactiveBefore = self.data.nextId

        @sp.entrypoint
        def administrate(self, actions):
            """Self-call only. Administrate this contract.

            This entrypoint must be called through the proposal system.

            Args:
                actions (sp.list of sp.bytes): List of packed variant of \
                    `InternalAdminAction` (`addSigners`, `changeQuorum`, `removeSigners`).
            """
            assert (
                sp.sender == sp.self_address()
            ), "This entrypoint must be called through the proposal system."

            for packed_actions in actions:
                action = sp.unpack(packed_actions, InternalAdminAction).unwrap_some(
                    error="Bad actions format"
                )
                with sp.match(action):
                    with sp.case.changeQuorum as quorum:
                        self.data.quorum = quorum
                    with sp.case.addSigners as added:
                        for signer in added:
                            self.data.signers.add(signer)
                    with sp.case.removeSigners as removed:
                        for address in removed:
                            self.data.signers.remove(address)
                # Ensure that the contract never requires more quorum than the total of signers.
                assert self.data.quorum <= sp.len(
                    self.data.signers
                ), "More quorum than signers."


if "templates" not in __name__:

    @sp.add_test(name="Basic scenario", is_default=True)
    def test():
        signer1 = sp.test_account("signer1")
        signer2 = sp.test_account("signer2")
        signer3 = sp.test_account("signer3")

        s = sp.test_scenario(main)
        s.h1("Basic scenario")

        s.h2("Origination")
        c1 = main.MultisigAction(
            quorum=2,
            signers=sp.set([signer1.address, signer2.address]),
        )
        s += c1

        s.h2("Proposal for adding a new signer")
        target = sp.to_address(
            sp.contract(sp.TList(sp.TBytes), c1.address, "administrate").open_some()
        )
        action = sp.pack(
            sp.set_type_expr(
                sp.variant("addSigners", [signer3.address]), main.InternalAdminAction
            )
        )
        c1.send_proposal([sp.record(target=target, actions=[action])]).run(
            sender=signer1
        )

        s.h2("Signer 1 votes for the proposal")
        c1.vote(0).run(sender=signer1)
        s.h2("Signer 2 votes for the proposal")
        c1.vote(0).run(sender=signer2)

        s.verify(c1.data.signers.contains(signer3.address))

The MultisigView Contract

It also utilizes a voting mechanism. This contract allows members to submit and vote for arbitrary bytes. Once a proposal achieves the required number of votes, its status can be confirmed via a view.

Python
import smartpy as sp


@sp.module
def main():
    class MultisigView(sp.Contract):
        """Multiple members vote for arbitrary bytes.

        This contract can be originated with a list of addresses and a number of
        required votes. Any member can submit as many bytes as they want and vote
        for active proposals.

        Any bytes that reached the required votes can be confirmed via a view.
        """

        def __init__(self, members, required_votes):
            """Constructor

            Args:
                members (sp.set of sp.address): people who can submit and vote for
                    lambda.
                required_votes (sp.nat): number of votes required
            """
            assert required_votes <= sp.len(
                members
            ), "required_votes must be <= len(members)"
            self.data.proposals = sp.cast(sp.big_map(), sp.big_map[sp.bytes, sp.bool])
            self.data.votes = sp.cast(
                sp.big_map(), sp.big_map[sp.bytes, sp.set[sp.address]]
            )
            self.data.members = sp.cast(members, sp.set[sp.address])
            self.data.required_votes = sp.cast(required_votes, sp.nat)

        @sp.entrypoint
        def submit_proposal(self, bytes):
            """Submit a new proposal to the vote.

            Submitting a proposal does not imply casting a vote in favour of it.

            Args:
                bytes(sp.bytes): bytes proposed to vote.
            Raises:
                `You are not a member`
            """
            assert self.data.members.contains(sp.sender), "You are not a member"
            self.data.proposals[bytes] = False
            self.data.votes[bytes] = sp.set()

        @sp.entrypoint
        def vote_proposal(self, bytes):
            """Vote for a proposal.

            There is no vote against or pass. If one disagrees with a proposal they
            can avoid to vote. Warning: old non-voted proposals never become
            obsolete.

            Args:
                id(sp.bytes): bytes of the proposal.
            Raises:
                `You are not a member`, `Proposal not found`
            """
            assert self.data.members.contains(sp.sender), "You are not a member"
            assert self.data.proposals.contains(bytes), "Proposal not found"
            self.data.votes[bytes].add(sp.sender)
            if sp.len(self.data.votes[bytes]) >= self.data.required_votes:
                self.data.proposals[bytes] = True

        @sp.onchain_view()
        def is_voted(self, id):
            """Returns a boolean indicating whether the proposal has been voted on.

            Args:
                id (sp.bytes): bytes of the proposal
            Return:
                (sp.bool): True if the proposal has been voted, False otherwise.
            """
            return self.data.proposals.get(id, error="Proposal not found")


if "templates" not in __name__:

    @sp.add_test(name="MultisigView basic scenario", is_default=True)
    def basic_scenario():
        """A scenario with a vote on the multisigView contract.

        Tests:
        - Origination
        - Proposal submission
        - Proposal vote
        """
        sc = sp.test_scenario(main)
        sc.h1("Basic scenario.")

        member1 = sp.test_account("member1")
        member2 = sp.test_account("member2")
        member3 = sp.test_account("member3")
        members = sp.set([member1.address, member2.address, member3.address])

        sc.h2("Origination")
        c1 = main.MultisigView(members, 2)
        sc += c1

        sc.h2("submit_proposal")
        c1.submit_proposal(sp.bytes("0x42")).run(sender=member1)

        sc.h2("vote_proposal")
        c1.vote_proposal(sp.bytes("0x42")).run(sender=member1)
        c1.vote_proposal(sp.bytes("0x42")).run(sender=member2)

        # We can check that the proposal has been validated.
        sc.verify(c1.is_voted(sp.bytes("0x42")))

Each contract provides a different mechanism for achieving multi-signature control, offering flexibility depending on the specific needs of your blockchain use case.

Step-by-step Guide to Try the Multisig Contract on SmartPy Online

To try the multisig contracts we’ve written in SmartPy, you can follow these steps:

  1. Go to the SmartPy IDE at https://smartpy.io/ide.

  2. Paste the contract code into the editor. You can replace the existing code.

  3. To execute the contract, click on the “Run” button located on the top panel.

  4. After running the contract, you can view the scenario execution in the “Output” panel on the right. Here, you can see details of each action, including proposals, votes, and approvals.

  5. To deploy your contract on the Tezos network, you first need to compile it. Click the “Compile” button on the top panel.

  6. After compiling, you can deploy the contract onto the testnet by clicking “Deploy Michelson Contract”. You’ll need to provide a Secret Key for a Tezos account with enough funds to pay for the gas costs of deployment.

  7. Once the contract is deployed, you’ll be provided with the contract’s address on the blockchain. You can use this address to interact with the contract via transactions.

  8. To submit proposals or vote in the contracts, you can use the entrypoints defined in the contract code, such as submit_proposal or vote_proposal. These can be called directly from transactions that you create.

Remember, while the SmartPy IDE allows you to test your contract on a simulated blockchain, deploying the contract onto the actual Tezos network will incur gas costs, which must be paid in XTZ, the native cryptocurrency of the Tezos network.

ข้อจำกัดความรับผิด
* การลงทุนคริปโตมีความเสี่ยงสูง โปรดดำเนินการด้วยความระมัดระวัง หลักสูตรนี้ไม่ได้มีไว้เพื่อเป็นคำแนะนำในการลงทุน
* หลักสูตรนี้สร้างขึ้นโดยผู้เขียนที่ได้เข้าร่วม Gate Learn ความคิดเห็นของผู้เขียนไม่ได้มาจาก Gate Learn