Skip to content
On this page

Contracts

A SmartPy contract is a Python class that inherits from sp.Contract. They can contain any number of entrypoints, views, and auxiliary functions.

Storage

Smart contracts can have any number of storage fields. Only the execution of the contract's entrypoints and __init__() method can change the value of these fields.

Most contracts use an __init__() method to initialise the contract storage. All storage fields must be initialised in this function; entrypoints can modify the storage but cannot add storage fields that were not initialised.

To initalise the contract storage, assign fields to the variable self.data, as in this example:

smartpy
class A(sp.Contract):
    def __init__(self):
        self.data.x = 0

    @sp.entrypoint
    def set_x(self, x):
        self.data.x = x

To make the contents of the contract storage clear, you can cast the storage parameters to a type, as in this example:

smartpy
@sp.module
def main():

    storage: type = sp.record(
        nat_value = sp.nat,
        int_value = sp.int,
        string_value = sp.string,
    )

    class B(sp.Contract):
        def __init__(self, param):
            self.data.nat_value = 0
            self.data.int_value = param.int_value
            self.data.string_value = param.string_value
            sp.cast(self.data, storage)

This __init__() method becomes the constructor to create an instance of the contract. In this way, you can use the method to pass storage values when you create an instance of the contract or deploy the contract, as in this example:

smartpy
@sp.module
def main():

    class A(sp.Contract):
        def __init__(self, int_value, string_value):
            self.data.int_value = sp.cast(int_value, sp.int)
            self.data.string_value = sp.cast(string_value, sp.string)

@sp.add_test()
def test():
    scenario = sp.test_scenario("A", main)
    contract = main.A(12, "Hello!")
    scenario += contract

    scenario.verify(contract.data.int_value == 12)
    scenario.verify(contract.data.string_value == "Hello!")

The __init__() method can be declared with an effects specification using the @sp.init_effects decorator.

Entrypoints and views can access values from storage with the self object, which is their first parameter. This object has two fields:

  • self.data: A variable that provides access to the values in the contract storage. You can set initial values in the __init__() method and access and change them in entrypoints, as in this example:

    smartpy
    import smartpy as sp
    
    @sp.module
    def main():
    
        class MyContract(sp.Contract):
            def __init__(self, int_value, string_value):
                self.data.int_value = sp.cast(int_value, sp.int)
                self.data.string_value = sp.cast(string_value, sp.string)
    
            @sp.entrypoint
            def changeValues(self, newInt, newString):
                self.data.int_value = newInt
                self.data.string_value = newString
  • self.private: A variable that provides access to constants and private lambdas.

    Constants behave like the storage values in self.data but only the __init__() method can set them. They are read-only to all other code.

Metadata

Contracts can have metadata that provides descriptive information about them to wallets, explorers, dApps, and other off-chain applications. Contract metadata is stored off-chain and therefore on-chain applications including smart contracts cannot access it. To store data off-chain in a decentralized way, many Tezos developers use IPFS.

The primary Tezos standard for metadata is TZIP-016 (Tezos Metadata Standard).

Contracts store a link to their metadata in a big map of type sp.big_map[sp.string, sp.bytes]. This big map is stored in a variable named metadata in the contract storage. This big map always contains the empty key "" and its value is an encoded URI that points to a JSON document.

SmartPy includes tools to help you create standard-compliant metadata and store it in IPFS. You create and publish contract metadata in a test scenario; see Creating and publishing metadata.

Inheritance

Contracts can inherit from each other as a superclass using the ordinary Python syntax:

smartpy
class A(sp.Contract):
    def __init__(self, x):
        self.data.x = x

class B(A):
    def __init__(self, x, y):
        A.__init__(self, x)
        self.data.y = y

Inheritance order

Attributes are first searched for in the current class. If not found, the search moves to parent classes. This is left-to-right, depth-first.

Initialization order

In SmartPy you must call the superclass's __init__() method explicitly.

The order of initialization in SmartPy follows the order in which the __init__() methods are called and the sequence in which fields are set. If a field is assigned multiple times during initialization, the last assignment is what determines the field's final value.

Passing parameters

Just like ordinary Python functions, you can define any number of parameters on SmartPy functions. However, the way callers pass those parameters is different depending on the number of parameters.

If a function accepts a single parameter, you can pass a literal value. For example, this contract has an entrypoint that accepts a single number as a parameter and sets its storage to the square of that number:

smartpy
@sp.module
def main():

    class Calculator(sp.Contract):

        def __init__(self):
            self.data.value = 0

        @sp.entrypoint
        def setSquare(self, a):
            self.data.value = a * a

@sp.add_test()
def test():
    scenario = sp.test_scenario("Calculator", main)
    contract = main.Calculator()
    scenario += contract

    # There is one parameter, so pass a single value
    contract.square(4)
    scenario.verify(contract.data.value == 16)

If a function accepts multiple parameters, you pass them as a record. For example, this contract has an entrypoint that has two named parameters. To call it, the test code passes a record with two fields, one for each named parameter:

smartpy
@sp.module
def main():

    class Calculator(sp.Contract):

        def __init__(self):
            self.data.value = 0

        @sp.entrypoint
        def multiply(self, a, b):
            self.data.value = a * b

@sp.add_test()
def test():
    scenario = sp.test_scenario("Calculator", main)
    contract = main.Calculator()
    scenario += contract

    # There are multiple parameters, so pass a record
    contract.multiply(a=5, b=6)
    scenario.verify(contract.data.value == 30)

If the function is within a SmartPy module, such as an auxiliary function, you must explicitly pass a record, as in this example:

smartpy
@sp.module
def main():

    def addInts(a, b):
        sp.cast(a, sp.int)
        return a + b

    class Calculator(sp.Contract):

        def __init__(self):
            self.data.value = 0

        @sp.entrypoint
        def add(self, a, b):
            self.data.value = addInts(sp.record(a = a, b = b))

These rules apply to all functions in a SmartPy module, including auxiliary functions, entrypoints, and views.

Auxiliary functions

Modules can contain auxiliary functions that you can use in contracts, as in this example:

smartpy
@sp.module
def main():
    def multiply(a, b):
        return a * b

    class C(sp.Contract):
        @sp.entrypoint
        def ep(self):
            assert multiply(a=4, b=5) == 20

Auxiliary functions can be declared with an effects specification using the @sp.effects decorator.

Entrypoints

Contracts can have any number of entrypoints, each decorated as sp.entrypoint. Each entrypoint receives the self variable as the first parameter, which provides access to the storage in the self.data and self.private records. Just like ordinary Python functions, you can define any number of other parameters on entrypoints.

Entrypoints can change values in the contract's storage but they cannot return a value.

An entrypoint may run logic based on:

  • The contract storage
  • The parameters that senders pass
  • Transaction context values such as sp.balance and sp.sender
  • The table of constants

Entrypoints cannot access information outside of Tezos, such as calling external APIs. If an entrypoint needs information from outside Tezos it must use oracles; see Oracles on docs.tezos.com and Using and trusting Oracles on opentezos.com.

Entrypoints have default effects for allowing changes to the contract storage, raising exceptions, permitting mutez calculations that may overflow or underflow and emitting new operations that are run after the entrypoint completes.

The default effect values can be changed by changing the appropriate fields in the @sp.entrypoint decorator.

An entrypoint can call other entrypoints in its contract or entrypoints in other contracts.

For example, this contract has a single entrypoint that sets the value of a field in the contract storage:

smartpy
class A(sp.Contract):
    def __init__(self):
        self.data.x = 0

    @sp.entrypoint
    def set_x(self, x):
        self.data.x = x

Views

Views are a way for contracts to expose information to other contracts and to off-chain consumers.

A view is similar to an entrypoint, with a few differences:

  • Views return a value.
  • Calls to views are synchronous, which means that contracts can call views and use the returned values immediately. In other words, calling a view doesn't produce a new operation. The call to the view runs immediately and the return value can be used in the next instruction.
  • Calling a view doesn't have any effect other than returning that value or raising an exception. In particular, it doesn't modify the storage of its contract and doesn't generate any operations.
  • Views do not include the transfer of any tez and calling them does not require any fees.

There are two kinds of views:

  • On-chain views have code in the smart contract itself
  • Off-chain views have code in an off-chain metadata file

Views have default effects for reading the contract storage, raising exceptions, permitting mutez calculations that may overflow or underflow. Views are not allowed to write to storage or emit operations.

The default effect values can be changed by changing the appropriate fields in the @sp.onchain_view or @sp.offchain_view decorators.

Creating views

The @sp.onchain_view annotation creates an on-chain view. For example, this contract has a view that returns a value from a big map in storage:

smartpy
@sp.module
def main():

    storage_type: type = sp.big_map[sp.address, sp.nat]

    class MyContract(sp.Contract):
        def __init__(self):
            # Start with an empty big map
            self.data = sp.big_map()
            sp.cast(self.data, storage_type)

        @sp.entrypoint
        def add(self, addr, value):
            # Add or update an element in the big map
            currentVal = self.data.get(addr, default=0)
            self.data[addr] = currentVal + value

        @sp.onchain_view
        def getValue(self, addr):
            # Get a value from the big map
            return self.data.get(addr, default=0)

The @sp.offchain_view annotation creates an off-chain view. The code of an off-chain view can be the same as an on-chain view, but to enable it you must publish its code to an external source such as IPFS. See Creating and publishing metadata.

Views can't return values before the end of their code; they must return values as their last command. For example, this code is not valid because it could return from more than one place in the code, even though the return statements are close to each other:

smartpy
if a > b:
    return a # Error: 'return' in non-terminal position.
return b

Instead, the compiler needs a single end block that returns a value, as in the previous example.

Calling views

sp.view(view_name, address: sp.address, arg: t, return_type: type) → sp.option[return_type]

Calls the view view_name on address giving the argument arg and expected return type return_type. Returns None if no view exists for the given elements and sp.Some(return_type) value.

The view_name parameter must be a constant string.

For example:

smartpy
x = sp.view(
    "get_larger", sp.self_address(), sp.record(a=a, b=b), sp.int
).unwrap_some()

The method that calls a view will be required to have

(with_exceptions=True, with_mutez_overflow=True, with_mutez_underflow=True).

Private functions

Contracts can contain private functions that can be used in entrypoints and views of the same contract, as in this example:

smartpy
@sp.module
def main():
    class C(sp.Contract):
        @sp.private
        def multiply(self, a, b):
            return a * b

        @sp.entrypoint
        def ep(self):
            assert self.multiply(a=4, b=5) == 20

Each private method has access to the self object and can be declared with an effects specification using the @sp.effects decorator.

Order of operations

When entrypoints call auxiliary functions or views, those calls run synchronously; the code of the entrypoint pauses, waits for the response, and continues.

However, if an entrypoint creates operations, such as a transfer of tez or a call to another entrypoint, even an entrypoint in the same contract, those operations run only after the entrypoint code completes.

For more information, see Operations and Operations on docs.tezos.com.

The self object

Each method inside a contract receives the self object as its first parameter. This object has two fields:

  • self.data: A record that provides access to the contract storage. You can set initial values in the __init__() method and access and change them in entrypoints, as in this example:

    smartpy
    import smartpy as sp
    
    @sp.module
    def main():
    
        class MyContract(sp.Contract):
            def __init__(self, intValue, stringValue):
                self.data.intValue = sp.cast(intValue, sp.int)
                self.data.stringValue = sp.cast(stringValue, sp.string)
    
            @sp.entrypoint
            def changeValues(self, newInt, newString):
                self.data.intValue = newInt
                self.data.stringValue = newString
  • self.private: A record that provides access to constants and private lambdas.

    Constants behave like the storage fields in self.data but only the __init__() method can set them. They are read-only to all other methods.

Deploying contracts

SmartPy doesn't include a built-in way to deploy (originate) contracts to Tezos. You can deploy contracts in the SmartPy IDE or use other tools that can deploy Tezos contracts.

One popular tool is the Octez client, which is a command-line client that runs many different kinds of Tezos operations. For installation instructions, see Installing the Octez client on docs.tezos.com.

Here are general steps for deploying a SmartPy contract with the Octez client:

  1. Install SmartPy locally as described in Installation.

  2. Install the Octez client and configure it for the target Tezos network. For example, to configure the client for the Ghostnet test network, get the URL of a Ghostnet node from https://teztnets.com and use it in the octez-client config update command, as in this example:

    bash
    octez-client --endpoint https://rpc.ghostnet.teztnets.com config update

    If you are using a testnet, Octez shows a warning that you are not using Mainnet. You can hide this message by setting the TEZOS_CLIENT_UNSAFE_DISABLE_DISCLAIMER environment variable to "YES".

  3. In the Octez client, create or import a wallet as described in Creating accounts.

  4. Fund the wallet with tez tokens. If you are using a testnet, you can use the network's faucet to get free tez. Testnet faucets are listed on https://teztnets.com.

  5. Compile the contract with the python command. SmartPy writes the output to a folder with the same name as the test scenario.

  6. In the output folder, find the compiled contract file, which ends in contract.tz. For example, if you downloaded and compiled the Welcome contract described in Installation, the compiled contract is in the file Welcome/step_002_cont_0_contract.tz.

  7. Optional: In the output folder, find the compiled initial storage value, which ends in storage.tz. For example, if you downloaded and compiled the Welcome contract described in Installation, the compiled contract storage value is in the file Welcome/step_002_cont_0_storage.tz.

  8. Use the octez-client originate contract command to deploy the contract. For example, this command deploys the Welcome contract and sets its initial storage:

    bash
    octez-client originate contract welcome transferring 0 from my_account \
      running Welcome/step_002_cont_0_contract.tz \
      --init "`cat Welcome/step_002_cont_0_storage.tz`" --burn-cap 1

Now you can call the contract from a block explorer, a dApp, or the Octez client.