Module admin_hand_over

Expand source code
import smartpy as sp


@sp.module
def main():
    class AdminHandOver(sp.Contract):
        """This contract implements a mechanism to switch administrator (an address
        that with special permission) in a way that prevents the administrator from
        handing over to an address with a typo or a contract without an interaction
        code.

        The problem: An administrator hands the contract over to a new administrator
        and then discovers that they made a typing error in the address.
        In this case, the contract can no longer be administered.

        The solution: The administrator hand over is a two step process:
        1. the current administrator adds a candidate admin.
        2. the new administrator removes the old one.
        """

        def __init__(self, admins):
            """
            Args:
                admins (sp.set of sp.address): The contract's administrators.
            """
            sp.cast(admins, sp.set[sp.address])
            self.data.admins = admins

        @sp.entrypoint
        def add_admins(self, admins):
            """Add new administrators.

            Args:
                admins (sp.list of sp.address): The new admins.
            Raises:
                `Only an admin can call this entrypoint.`
            """
            assert self.data.admins.contains(
                sp.sender
            ), "Only an admin can call this entrypoint."

            for admin in admins:
                self.data.admins.add(admin)

        @sp.entrypoint
        def remove_admins(self, admins):
            """Remove administrators.

            An admin cannot remove themselves.

            Args:
                admins (sp.list of sp.address): The admins to remove.
            Raises:
                `Only an admin can call this entrypoint.`
                `An admin cannot remove themselves.`
            """
            assert self.data.admins.contains(
                sp.sender
            ), "Only an admin can call this entrypoint."

            for admin in admins:
                assert admin != sp.sender, "An admin cannot remove themselves."
                self.data.admins.remove(admin)

        @sp.entrypoint
        def protected(self):
            """Example of entrypoint reserved to administrators.

            Raises:
                `Only an admin can call this entrypoint.`
            """
            assert self.data.admins.contains(
                sp.sender
            ), "Only an admin can call this entrypoint."


# This prevent tests from being executed on importation.
if "templates" not in __name__:

    first_admin = sp.test_account("first_admin")
    new_admin = sp.test_account("new_admin")
    non_admin = sp.test_account("non_admin")

    @sp.add_test(name="Admin hand over basic scenario", is_default=True)
    def basic_scenario():
        """Test:
        - Origination
        - An admin adds a new admin.
        - The new admin removes the older admin.
        - The new admin calls the protected entrypoint.
        """
        sc = sp.test_scenario(main)
        sc.h1("Basic scenario.")

        sc.h2("Origination.")
        c1 = main.AdminHandOver(admins=sp.set([first_admin.address]))
        sc += c1

        sc.h2("An admin adds a new admin.")
        c1.add_admins([new_admin.address]).run(sender=first_admin)

        sc.h2("The new admin removes the older admin.")
        c1.remove_admins([first_admin.address]).run(sender=new_admin)

        sc.h2("The new admin calls the protected entrypoint")
        c1.protected().run(sender=new_admin)

    @sp.add_test(name="Full")
    def test():
        sc = sp.test_scenario(main)
        sc.h1("Full test")
        sc.h2("Origination")
        c1 = main.AdminHandOver(admins=sp.set([first_admin.address]))
        sc += c1

        sc.h2("add_admins")
        sc.h3("(Failure) Non admin fails to a new admin.")
        c1.add_admins([new_admin.address]).run(
            sender=non_admin,
            valid=False,
            exception="Only an admin can call this entrypoint.",
        )
        sc.h3("An admin adds a new admin.")
        c1.add_admins([new_admin.address]).run(sender=first_admin)

        sc.h2("remove_admins")
        sc.h3("(Failure) Non admin fails to remove the older admin.")
        c1.remove_admins([first_admin.address]).run(
            sender=non_admin,
            valid=False,
            exception="Only an admin can call this entrypoint.",
        )
        sc.h3("(Failure) Admin tries to remove themselves.")
        c1.remove_admins([first_admin.address]).run(
            sender=first_admin,
            valid=False,
            exception="An admin cannot remove themselves.",
        )
        sc.h3("The new admin removes the older admin.")
        c1.remove_admins([first_admin.address]).run(sender=new_admin)
        sc.verify(~c1.data.admins.contains(first_admin.address))

        sc.h2("protected")
        sc.h3("The new admin calls the protected entrypoint")
        c1.protected().run(sender=new_admin)
        sc.h3("(Failure) Non admin calls the protected entrypoint")
        c1.protected().run(
            sender=non_admin,
            valid=False,
            exception="Only an admin can call this entrypoint.",
        )

    # @sp.add_test(name="Mutation", is_default=False)
    # def test():
    #     s = sp.test_scenario()
    #     with s.mutation_test() as mt:
    #         mt.add_scenario("Full")