Skip to content
On this page

SmartPy Tutorial

SmartPy is a language designed for writing smart contracts on the Tezos blockchain. If you're not entirely sure what a smart contract is, don't worry. This tutorial is the ideal place to learn about them. It will guide you through the development of smart contracts using SmartPy's intuitive Python-like syntax. You'll be quickly on your way to testing and deploying your first smart contract on the Tezos blockchain!

There are no strict prerequisites for this tutorial, as it begins from the very basics. However, basic familiarity with programming, preferably in Python, would be beneficial.

First steps

The anatomy of a smart contract

Let's start by examining a simple example of a SmartPy contract:

SmartPy
class Adder(sp.Contract):
    def __init__(self):
        self.data.sum = 0

    @sp.entrypoint
    def add(self, x):
        self.data.sum += x

To start with, we can see that a smart contract is implemented in SmartPy as a Python class inheriting from sp.Contract. Furthermore, the class has an __init__ method, and another method called add.

The __init__ method initialises the field self.data.sum to 0. This is the initial value that the field will hold when the contract is deployed to the Tezos blockchain.

The add method, marked as an entrypoint using the decorator sp.entrypoint, can be invoked from outside the contract. This interaction is typically initiated by a human interacting with the blockchain (e.g. through a "wallet") or by another contract. In our example, the entry point merely adds its parameter x to the sum maintained in self.data.sum.

Simulating contracts in SmartPy

Before deploying our contract to the Tezos blockchain, let's test it. In general, this step is crucial in smart contract development, as the code of a contract becomes unchangeable once it's deployed.

Here is the previous example included in a SmartPy module, along with a test:

python
import smartpy as sp

@sp.module
def main():
    class Adder(sp.Contract):
        def __init__(self):
            self.data.sum = 0

        @sp.entrypoint
        def add(self, x):
            self.data.sum += x

@sp.add_test()
def test():
    s = sp.test_scenario("my first test", main)
    a = main.Adder()
    s += a

    a.add(2)
    a.add(3)

Let's dissect this example step by step:

  • Firstly, we import the smartpy library, which is conventionally abbreviated as sp.

  • A SmartPy module, main, is then defined using the decorator sp.module. This indicates that the following code is written in SmartPy, as opposed to ordinary Python. Among other things, this means that the code contained in this block will be type-checked. More on this later.

  • A module can contain one or several definitions. In this instance, the sole definition is that of the contract class Adder, which we've already discussed in the previous section.

  • Next, we add a test, designated as such by the decorator sp.add_test. Inside this test, s = sp.test_scenario(main) creates a scenario. Then a = main.Adder() instantiates the contract, and s += a adds it to the scenario. Note that we write main.Adder because Adder has been defined inside the main module. We also have to give main as an argument to sp.test_scenario.

  • Finally, we interact with the adder contract: firstly, we call the entrypoint add with the argument 2, then a second time with the argument 3.

Simulation results

SmartPy provides a convenient online IDE (integrated development environment) that enables you to write smart contracts and test them directly in your browser. It presents the simulation results in a user-friendly manner.

🚀 Try it out!

Head over to smartpy.io/ide and paste the above code snippet into the code editor. Then click the button. The result should resemble something like this:

The simulation results show the following elements:

  • The first one corresponds to the line s += a and looks like this: This simply records the instantiation of a contract.

  • Next, the first entrypoint call a.add(2) yields: It shows that the argument was 2 and that the new contract storage now contains a new value for sum (this corresponds to self.data.sum in the source code), namely 2.

  • The second call a.add(3) then results in: As expected, sum now is 5.

INFO

SmartPy compiles to Michelson, the native, low-level language of Tezos. To view the compiled code, click on Deploy Michelson Contract and then Code on the right-hand side of the browser window. You should see something like this:

Knowledge of Michelson is not necessary to work with SmartPy.

Summary

Before moving on to the next part, let's summarise what we've learned so far:

  • In SmartPy, a contract is represented as a Python class inheriting from sp.Contract.

  • A contract can have one or several entrypoints, each marked as sp.entrypoint. Once a contract has been deployed, its entrypoints cannot be altered. The blockchain ensures that the contract evolves only according to the rules specified in its code.

  • A smart contract's storage is contained in fields of self.data, such as self.data.sum.

A simple token

Now that you are familiar with the basics, let's write a slightly more interesting contract. In this part, we're going to make a new currency, which we'll call the "Ducat".

WARNING

The smart contracts presented in this section are meant to illustrate basic concepts and we do not recommend using them in production. For real-world applications, please have a look at FA2 lib.

The contract

A common way to implement a new currency is by using a smart contract that remembers the number of tokens (here Ducats) owned by each participant. This is akin to a bank maintaining balances (number of Ducats) for current accounts.

To start, we'll create a module with a contract and its constructor:

python
import smartpy as sp

@sp.module
def main():
    class Ducat(sp.Contract):
        def __init__(self, admin):
            self.data.balances = {}
            self.data.admin = admin

Here balances is a map, which associates a balance to each address. Initially it is empty, indicating that nobody owns any Ducats. Then there is an admin address that has special powers, namely it is allowed to make new Ducats, i.e. mint them.

Before we write the transfer and mint entrypoints, we need two auxiliary methods. The first one is for increasing the balance in a given account x by amount:

SmartPy
@sp.private(with_storage="read-write")
def increase_balance(self, x, amount):
    if self.data.balances.contains(x):
        self.data.balances[x] += amount
    else:
        self.data.balances[x] = amount

The code first checks whether x already has an entry in the ledger. If so, amount is simply added to the balance in the entry. If no entry exists yet, a new one is added and initialised to amount.

The second auxiliary function decreases the balance:

SmartPy
@sp.private(with_storage="read-write")
def decrease_balance(self, x, amount):
    b = self.data.balances[x] - amount
    assert b >= 0
    if b == 0:
        del self.data.balances[x]
    else:
        self.data.balances[x] = b

Here two points are noteworthy:

  • If the balance in the account is insufficient the assert b >= 0 statement will throw an error.

  • Special care is taken to remove the account from the ledger if its balance reaches 0.

We can now get to the entrypoints of our Ducat contract. First, there is a transfer entrypoint, which allows transferring Ducats from one account to another:

SmartPy
@sp.entrypoint
def transfer(self, params):
    assert params.amount >= 0
    self.decrease_balance(sp.record(x=sp.sender, amount=params.amount))
    self.increase_balance(sp.record(x=params.dest, amount=params.amount))

This uses the two previously defined auxiliary functions. A few things are noteworthy here:

  • The sender here is taken from sp.sender, which is defined as the account calling the entrypoint.

  • If the sending account has an insufficient balance, decreaseBalance fails. On Tezos if any part of an operation fails, the entire transaction is aborted. This means that even calling increase_balance before decrease_balance would not yield inconsistent results in this case.

  • We use record syntax to call auxiliary functions with several parameters.

Finally, as mentioned previously, the admin is allowed to mint new coins. This is implemented by the following entrypoint:

SmartPy
@sp.entrypoint
def mint(self, params):
    assert sp.sender == self.data.admin
    self.increase_balance(sp.record(x=params.dest, amount=params.amount))

The assert statement ensures that the operation fails if anyone other than the admin attempts to call this entrypoint. The newly minted amount is assigned to the account given as a parameter.

A test scenario

Now that we have written the smart contract for Ducats, let us add a test scenario. To begin with, we simply define three accounts admin, alice, and bob:

python
@sp.add_test()
def test():
    admin = sp.test_account("Admin").address
    alice = sp.test_account("Alice").address
    bob = sp.test_account("Bob").address

Then, as previously, we define a test scenario and instantiate our contract:

python
s = sp.test_scenario("Ducat test", main)
c = main.Ducat(admin)
s += c

To start off, we have the admin mint 5 ducats for each of Alice and Bob.

python
c.mint(dest=alice, amount=5, _sender=admin)
c.mint(dest=bob  , amount=5, _sender=admin)

Adding _sender=admin to an entrypoint call informs SmartPy that the call should be executed from the admin account.

For testing purposes, we verify that the ledger indeed contains these sums:

python
s.verify(c.data.balances[alice] == 5)
s.verify(c.data.balances[bob] == 5)

Next, we let Bob transfer 3 tokens to Alice. Again, we verify the resulting balances:

python
c.transfer(dest=alice, amount=3, _sender=bob)
s.verify(c.data.balances[alice] == 8)
s.verify(c.data.balances[bob] == 2)

Finally, let's see what happens if we do something unauthorised. Here Bob is trying to send 3 ducats to Alice, even though there are only 2 ducats in his account. To indicate that the operation is expected to fail we supply _valid=False to the entrypoint call:

python
c.transfer(dest=alice, amount=3, _sender=bob, _valid=False)
python
c.mint(dest=bob, amount=100, _sender=bob, _valid=False)

For reference, here is the complete code of the Ducat smart contract and the test scenario:

python
import smartpy as sp

@sp.module
def main():
    class Ducat(sp.Contract):
        def __init__(self, admin):
            self.data.balances = {}
            self.data.admin = admin

        @sp.private(with_storage="read-write")
        def increase_balance(self, x, amount):
            if self.data.balances.contains(x):
                self.data.balances[x] += amount
            else:
                self.data.balances[x] = amount

        @sp.private(with_storage="read-write")
        def decrease_balance(self, x, amount):
            b = self.data.balances[x] - amount
            assert b >= 0
            if b == 0:
                del self.data.balances[x]
            else:
                self.data.balances[x] = b

        @sp.entrypoint
        def transfer(self, params):
            self.decrease_balance(sp.record(x=sp.sender, amount=params.amount))
            self.increase_balance(sp.record(x=params.dest, amount=params.amount))

        @sp.entrypoint
        def mint(self, params):
            assert sp.sender == self.data.admin
            self.increase_balance(sp.record(x=params.dest, amount=params.amount))


@sp.add_test()
def test():
    admin = sp.test_account("Admin").address
    alice = sp.test_account("Alice").address
    bob = sp.test_account("Bob").address
    s = sp.test_scenario("Ducat test", main)
    c = main.Ducat(admin)
    s += c

    c.mint(dest=alice, amount=5, _sender=admin)
    c.mint(dest=bob  , amount=5, _sender=admin)
    s.verify(c.data.balances[alice] == 5)
    s.verify(c.data.balances[bob] == 5)

    c.transfer(dest=alice, amount=3, _sender=bob)
    s.verify(c.data.balances[alice] == 8)
    s.verify(c.data.balances[bob] == 2)

    c.transfer(dest=alice, amount=3, _sender=bob, _valid=False)
    c.mint(dest=bob, amount=100, _sender=bob, _valid=False)

🚀 Try it out!

As before, you can go to smartpy.io/ide and paste the above code snippet into the code editor. Then click the button and examine the results.

You can also remove the valid=False from the last two entrypoints and see what happens.

Further resources

Congratulations on concluding this tutorial! As you have seen, SmartPy makes writing and testing smart contracts a breeze. However, we've barely scratched the surface of what is possible. Here are some further resources to accompany you on your SmartPy journey: