In this chapter, we will use SmartPy to develop a smart contract based Raffle and cover the most important aspects of the framework. We will use this opportunity to introduce new notions as they appear. For a complete reference of SmartPy, please refer to the Reference Manual.
SmartPy is a Python library. SmartPy scripts are regular Python scripts that use SmartPy constructions. This mechanism is useful because it brings very powerful meta-programming capabilities.
Meta-programming is when we can write a program that writes a program, i.e., constructs a contract. Indeed, the functions of the SmartPy Library are used to construct a smart contract.
Smart contracts are executed once they are deployed in the Tezos blockchain (although they can be simulated).
Like most languages, SmartPy has expressions. For example:
self.data.xrepresents the contract storage field
2represents the number `2
self.data.x + 2represents their sum
Inside a contract, when we write
y as an alias the SmartPy expression
self.data.x + 2.
Note that the actual addition is not carried out until the contract has been deployed and the entrypoint is called.
As you will see throughout this tutorial, SmartPy is a library that will be imported in the following way:
And the functions of SmartPy will be called with the prefix
sp.. For example:
sp.verify()checks that the field
x is larger than
2 and raises an error if it is not. This is performed at run time, i.e., in the blockchain, once translated into Michelson.
Since Python does not allow its control statements to be overloaded, certain language constructs are desugared by a pre-processor:
sp.while are SmartPy commands.
For example, we will use:
If we would have used the
if native to Python it could not be interpreted and compiled in Michelson.
A raffle is a game of chance that distributes a winning prize.
The organizer is in charge of defining a jackpot and selling tickets that will either be winners or losers. In the case of our example, we will only have one winning ticket.
Fig.3 represents our smart contract.
FIGURE 3: Raffle contract
Three entrypoints allow interaction with the contract:
- open_raffle can only be called by the administrator. During this call, he sends the tez amount of the jackpot to the contract, defines a closing date, indicates the number/identity of the winning ticket (in an encrypted way), and declares the raffle open.
- buy_ticket allows anyone to buy a ticket for 1 tez and take part in the raffle.
- close_raffle can only be called by the administrator. It closes the raffle and sends the jackpot to the winner.
Note that this is a simplified conception of what a raffle is. Here the jackpot is fixed by the administrator, but it is possible to make a contract where the jackpot depends on the number of sold tickets.
This section illustrates the coding of the smart contract in the online editor proposed by SmartPy. You can however also use your favourite IDE instead, as described previously.
To start, create a new contract in the online editor and name it Raffle Contract.
FIGURE 4: Online Editor Create Contract
Copy/paste the template below to get started:
A SmartPy contract is a class definition that inherits from the
A class is a code template for creating objects. Objects have member variables and have a behaviour associated with them. In python a class is created by the keyword
Inheritance allows us to define a class that can inherit all the methods and properties of another class.
The SmartPy storage is defined into the constructor
__init__which makes a call to
self.init()that initializes the fields and sets up the storage.
Entrypoints are a method of a contract class that can be called on from the outside. Entrypoints need to be marked with the
Decorators are functions that modify the functionality of other functions. They are introduced by
@and are placed before the function.
Test Scenarios are good tools to make sure our smart contracts are working correctly.
- A new test is a method marked with the
- A new scenario is instantiated by
- Scenarios describe a sequence of actions: originating contracts, computing expressions or calling entry points, etc.
- In the online editor of SmartPy.io, the scenario is computed and then displayed as an HTML document on the output panel.
Note that there is a difference between Test Case which is a set of actions executed to verify particular features or functionality and Test Scenario which includes an end to end functionality to be tested.
We will explain in more details the use of all these concepts in the next sections.
Our code doesn't do much for now, but it can already be compiled by pressing the run button. If there is no error, you should be able to visualize the generated Michelson code in the Deploy Michelson Contract tab.
open_raffle is the entrypoint that only the administrator can call. If the invocation is successful, then the raffle will open, and the smart contract's storage will be updated with the chosen amount and the hash of the winning ticket number.
Here is the first version of this contract. We will go through its different parts one at a time.
The definition of the storage is done in the constructor
__init__ and the different fields of the storage are stated as follows:
self.init( field1=value1, field2=value2, field3=value3)
field3are the names of the variables and are accessible via
value3are initial values or variables passed as constructors like
__init__(self, value1)as we did above for the
SmartPy types are all of the form
sp.T<TypeName>. Check out Typing.
Types are usually automatically inferred and not explicitly needed. However, it is still possible to add constraints on types, e.g. check out Setting a type of constraint in SmartPy.
They are then compiled into their corresponding Michelson type.
For the storage of the raffle contract, we have defined five fields for the moment:
- admin is the only authorized
addressto call the two entrypoints open_raffle and close_raffle.
- close_date is a
timestampto indicate the closing date of the raffle. The raffle must remain open for at least seven days.
- jackpot is the amount in
tezthat will be distributed to the winner.
- raffle_is_open is a
booleanto indicate if the raffle is open or not.
- hash_winning_ticket is the hash of the winning ticket indicated by the admin. It is of type
It's not possible to generate a truly random number from a smart contract, so an easy alternative is to use a hash that the admin will reveal the value later. This example is for educational purposes and is not intended to be deployed on the real Tezos network.
An entrypoint is a method of the contract class and is always preceded by the keyword
@sp.entry_point. It can take several parameters. In our case, the first entrypoint we use, is called
open_raffle and does the following:
sp.verify_equal(), we check that a statement is true or if it returns an error message (more info at Checking a Condition). Here we check four statements :
The address that calls the entrypoint must be the administrator one indicated in the storage. We compare here
sp.senderis the address that calls the current entrypoint.
sp.sourceis the address that initiates the current transaction. It may or may not be equal to
sp.sender, but in our case, it is.
No raffle must be open. For this, we use the boolean
raffle_is_opendefined in the storage.
~is the symbol used for logical negation.
sp.amountsent to the contract by the administrator during the transaction must be at least greater than the value specified in the
The closing date
close_datepassed as a parameter must be at least seven days in the future (more info on Timestamps).
Once all the conditions are passed we update the storage as follows:
In a scenario, we simulate the origination and a number of calls to entry point, that can be made from different accounts. The execution of the test generates html code that can help visualize it.
The purpose of the test scenario is to ensure that the smart contract functions properly by triggering the conditions and checking the changes made to the storage.
On SmartPy, a test is a method of the contract class, preceded by
Inside this method, you need to instantiate your contract class and your scenarios, to which you will add the contract instance and all the related calls that you want to test. For instance:
Note that you can also organize your scenarios by adding titles with
scenario.h2("My subtitle"), etc.
An interesting capacity is to define test accounts for our scenarios:
Test accounts can be defined through calling
sp.test_account(seed), where seed is a string.
A test account contains a few fields:
You can then simulate the calls to the entrypoints by specifying the different arguments as follows:
The run method accepts optional parameters that can help to setup a relevant context for the entrypoint call. You can specify the
source of the transaction, the
amount of tez sent, the transaction date using
Note that the option
valid=Falseallows you to indicate that the transaction is expected to fail here because Alice is not the administrator.
The result is displayed in an HTML document in the output panel of the online editor.
Let's run our code:
FIGURE 4: Online Editor Contract Summary
You can see a summary of our smart contract with the following information:
- Address of the contract
- Balance in tez
- Entry points
By clicking on the Types tab, we have access to the types of the storage elements and the parameters of the entrypoints.
FIGURE 5: Online Editor Types
As with Python, most of the time, it is not necessary to specify the type of an object in SmartPy.
But because the target language of SmartPy, Michelson, requires types.
Each SmartPy expression, however, needs a type. This is why SmartPy uses type inference to determine the type of each expression.
See doc Typing.
By clicking on the Deploy Michelson Contract tab, we have access to the codes compiled in Michelson for the storage (Storage tab) and the smart contract (Code tab).
The michelson code of our smart contract is for now, the following:
By scrolling down a little, we have access to the results of the test scenario, with within each step a summary of the contract.
FIGURE 4: Online Editor Scenario Output
buy_ticket is an entrypoint that can be called on by everyone who wants to participate in the raffle.
If the invocation is successful, the address of the sender will be added to the storage, and the player will be eligible to win the jackpot
Here is the second version of this contract with the addition of a new entrypoint. We will go through the additonal parts one at a time.
With the addition of this entrypoint we have defined two new fields in the storage:
- players, is a
Setthat receives the addresses of each new player who bought a raffle ticket.
- sold_tickets, is a
Mapthat associates each player's address with a ticket number.
- Sets are unordered collections of values of the same type, unlike lists, which are ordered collections.
- Sets in SmartPy are of type
sp.TSet(element). It will be then compiled into the corresponding type in Michelson which is
- For SmartPy expressions, we must use
sp.set([e1, e2, …, en])to define a set.
- Map is a data structure which associates a value to a key, thus creating a key-value binding. All keys have the same type and all values have the same type. An additional requirement is that the type of the keys must be comparable.
- Maps in SmartPy are of type sp.TMap(key, value). It will be then compiled into the corresponding type in Michelson which is
- For SmartPy expressions, we can define a map as follows:
my_map = sp.map(l = …, tkey = …, tvalue = …).
- To add or replace an element in a map, we use:
my_map[key] = value
Mapsload their entries into the environment, which is fine for small maps, but for maps holding millions of entries, the cost of loading them would be too expensive. For this we use
BigMaps. Their syntax is the same as for regular maps.
Set and a
Map are used here to store the players. But in case there would be a very large number of players this can block the contract.
The solution would be to use only a
BigMap. Indeed, a
Map uses more storage but costs less gas, whereas a
BigMap consumes less storage but has higher gas costs during the Smart Contract's execution.
Three assertions are tested for this entrypoint to work:
- The raffle must be open.
- The amount of tez sent to the contract during the transaction must be equal to the ticket price (
- Each player is allowed to buy only one ticket.
If the conditions are met, then the storage is updated:
- The address of the player is added to the set
- The ticket identification (id) is associated with the player's address in the map
ticket_id = abs(sp.len(self.data.players) - 1), the ticket id is incremented for each new participant. The
abs()function, which designates the absolute value, is used to ensure that the
ticket_idis of type
Only the administrator can call on the entrypoint
close_raffle. If the invocation is successful, the raffle is closed, the jackpot amount is sent to the winner, and the storage is reset to a default value.
Here is the code in its final form with the implementation of the last entrypoint.
The storage definition has not been modified by the addition of this entrypoint, so we can directly explain its implementation.
Four assertions are checked in this entrypoint:
- The caller must be the admin of the contract.
- The raffle must be open.
- The closing date must be greater than or equal to the closing date indicated in the storage.
- The hash of the ticket used as a parameter, must be equal to the hash of the ticket indicated in the storage.
The administrator provides as parameter a
sp.nat()which must correspond to the number of the winning ticket, afterwards this natural integer is converted into
byteand hashed using the
If the conditions are met, then:
- The jackpot is sent to the winner's address.
- The storage is reset to the default values.
Here are some precisions about the
sp.TBytes type and its functionality which are used here in the close_raffle entrypoint.
- Bytes are sequences of byte, such as
0x12e4in hexadecimal notation.
- Bytes in SmartPy are of type
sp.TBytes. It will be then compiled into the corresponding type in Michelson which is
- For SmartPy expressions, we must use
sp.bytes('Ox...')to define bytes.
- We use
sp.pack(x)to serialize a piece of data x to its optimized binary representation. It then returns an object of type
- The function
sp.TBytesvalue and return the corresponding hash as a new
We are getting to the end of our smart contract. Run it one last time and explore the result. Don't hesitate to read the test scenario, to make sure your smart contract is working correctly. You can, of course, modify the scenarios or create new ones.
Check out the final Michelson code generated by SmartPy for this smart contract. Note that you can use this Michelson code to create additional tests with PyTezos as described in the LIGO Module.
SmartPy is meant for smart contract development and it always yields Michelson code. The method for developing such smart contracts is pretty much always the same.
- the smart contract is a class definition that inherits from
- the storage is defined in the constructor of this class.
- the entrypoints are defined as a method of the contract class and are marked with the
There is no need for a main function like LIGO which dispatches the actions of the smart contract. The code can be compiled directly.
SmartPy was designed to help developers build smart contracts by providing them with a syntax familiar to them and a powerful analysis tool.
You can explore contract examples and train yourself on the online editor.
You can go to Cryptocodeschool.in which is a fun platform that teaches how to code decentralized apps on the Tezos blockchain using SmartPy, and more.