Skip to content
This repository has been archived by the owner on Mar 8, 2023. It is now read-only.

Latest commit

 

History

History
403 lines (282 loc) · 17.3 KB

writing-score.md

File metadata and controls

403 lines (282 loc) · 17.3 KB
title
Writing SCORE

This document presents how to write a SCORE, smart contract of the ICON network. Through this document, you will learn from setting the workspace to deploying a SCORE.

Technically speaking, SCORE is the platform for ICON's smart contract. But it is also used to refer to the ICON's smart contract itself.

Intended Audience

The intended audience is the developers who have basic Python programming knowledge.

Purpose

After reading this document, you will understand the structure of SCORE and learn the basic syntax of writing SCORE.

Prerequisite

  • SCORE Overview
  • Token & Crowdsale
  • T-Bears CLI Commands

Creating a Workspace

SCORE is written in Python programming language, so you can use any Python programming tools, as long as the required files, as listed below, are included in the SCORE project. These files are all plain text.

  • __init__.py: This file makes Python treat the directory containing the file as a package.
  • project.py: The main module file of the SCORE to be executed at the top level. This file name should be given in the package.json.
  • package.json: This file contains the basic information about the SCORE.

Using T-Bears, you can create a workspace for writing SCORE. T-Bears creates the above-mentioned template files for SCORE and other configuration files for the development environments. You can refer to the T-Bears guide for the details.

T-Bears init command will create a SCORE project.

$ tbears init [project_name] [main_class_name]
$ tbears init hello_world HelloWorld

In the hello_world folder, SCORE template is created.

$ ls -lF
total 4
drwxr-xr-x  6 edward  staff   192  4  4 10:24 hello_world/

$ ls -lF hello_world/
total 16
-rw-r--r--  1 edward  staff    0  4  4 10:20 __init__.py
-rw-r--r--  1 edward  staff  420  4  4 10:20 hello_world.py
-rw-r--r--  1 edward  staff   90  4  4 10:20 package.json
drwxr-xr-x  4 edward  staff  128  4  4 10:23 tests/

Structure of SCORE

SCORE is a collection of codes written in Python. There should be a main class which has member variables and methods. The main class name should be specified in the package.json as the value of main_score field so that ICON nodes determine which class should be loaded and executed as an entry point. The main class must implement three methods (__init__, on_install, on_update) that are invoked on loading, installing, and updating SCORE respectively. In addition, the main class should have more than one external methods which are supposed to be invoked by transactions from EOA or other Smart Contracts. Transactions will result in the internal state update. The main class has member variables to store its states, and those variables should be incorporated with the state database, which will be explained later with VarDB, DictDB, and ArrayDB.

The following is the content of the main class which we have just generated by T-Bears init command.

from iconservice import *

TAG = 'HelloWorld'

class HelloWorld(IconScoreBase):

    def __init__(self, db: IconScoreDatabase) -> None:
        super().__init__(db)

    def on_install(self) -> None:
        super().on_install()

    def on_update(self) -> None:
        super().on_update()

    @external(readonly=True)
    def hello(self) -> str:
        Logger.debug(f'Hello, world!', TAG)
        return "Hello"

IconScoreBase

Every main class of SCORE should be inherited from IconScoreBase. If the main class is not derived from IconScoreBase, it cannot be deployed.

The main class should have 3 methods, __init__, on_install and on_update.

__init__

__init__ is the constructor for the class. This method gets called whenever the class object is instantiated.

Member variables can be declared here, however, declaring member variables which are not managed by state DB is prohibited. In other words, all member variables must be stored in the state database to keep the values non-volatile during the executions of the smart contract.

Here's an example of declaring member variables as state data.

def __init__(self, db: IconScoreDatabase) -> None:
    # Parent’s __init__ method must be called as well
    super().__init__(db)

    self._total_supply = VarDB('total_supply', db, value_type=int)
    self._decimals = VarDB('decimals', db, value_type=int)
    self._balances = DictDB('balances', db, value_type=int)

on_install

This method is called when the smart contract is deployed for the first time, and will not be called again on contract update or deletion afterward. This is the place where you initialize the state database.

on_update

This method is called when the smart contract is updated.

Built-in Properties

These are member variables supported by IconScoreBase, whose values are set by the ICON platform to deliver necessary information to the SCORE. Therefore, they are read-only variables and do not allow to be modified by the contract.

  • msg: Holds information of the account who called the SCORE method.
    • msg.sender: Address of the account who called this method. If it is another contact that called this method, msg.sender points to the caller contract's address.
    • msg.value: Amount of ICX that the sender attempts to transfer to the SCORE.
  • tx: Transaction information
    • tx.origin: The account who created the transaction.
    • tx.index: Transaction index.
    • tx.hash: Transaction hash.
    • tx.timestamp: Transaction creation time.
    • tx.nonce: (optional) an arbitrary number set by the sender.
  • icx: An object used to transfer ICX coins
  • db: An instance used to access the state DB
  • address: Address of the SCORE
  • owner: Address of the account who deployed the contract
  • block_height: Current block height
  • now: Wrapping method of block.timestamp.

Implementing SCORE External Methods

Users can implement methods which are supposed to be invoked from outside the blockchain. These methods can be decorated with @external and/or @payable. Additionally, users can declare methods decorated with @eventlog which can be used to leave custom event logs in the TxResult.

External decorator (@external)

Methods decorated with @external can be called from outside the contract. These methods are registered on the external API list and users can query the API list through icx_getScoreApi JSON RPC call.

Any attempt to call a non-external method from outside the contract will fail.

If a method is decorated with a readonly parameter, i.e., @external(readonly=True), the method will have read-only access to the state database.

If a read-only external method is also decorated with @payable, the method call will fail.

Duplicate declaration of @external will raise an exception on class loading time.

Possible data types for external method parameters are Python primitive types (int, str, bytes, bool) and Address (newly defined type for SCORE codes). list, dict and None types are not supported as method parameters. Method parameters can have default values.

Payable decorator (@payable)

Only the methods with @payable decorator are permitted to receive incoming ICX coins. Transferring zero ICX is acceptable if it is decorated with @payable. If ICX coins (self.msg.value) are passed to a non-payable method, that transaction will fail.

Eventlog decorator (@eventlog)

Methods decorated with @eventlog can be called within SCORE codes during the execution of a transaction to include custom event logs in its TxResult as eventlogs. It is recommended to declare a method without an implementation body. Even if the method has an implementation body, it does not be executed. When declaring a method, Python type hints must be specified. Without type hinting, SCORE loading will fail.

If an indexed parameter is set in the decorator, the designated number of parameters in the order of declaration will be indexed and included in the Bloom filter. At most 3 parameters can be indexed, and the index cannot exceed the number of parameters, otherwise, an error will be raised. Indexed parameters and non-indexed parameters are separately stored in TxResult.

Example)

# Declaration
@eventlog
def FundTransfer1(self, _backer: Address, _amount: int, _isContribution: bool): pass

@eventlog(indexed=1) # The first parameter (_backer) will be indexed
def FundTransfer2(self, _backer: Address, _amount: int, _isContribution: bool): pass

# Execution
self.FundTransfer1(self.msg.sender, amount, True)
self.FundTransfer2(self.msg.sender, amount, True)

fallback

fallback is a special method that is executed whenever the smart contract receives plain ICX coins without data.

@payable
def fallback(self):
    """
    Called when anyone sends ICX to the SCORE.
    """
    if something_wrong:
        revert('something wrong')

If fallback method is not decorated with @payable, the ICX coin transfers to the contract will fail even for the zero ICX transfer.

fallback method cannot be decorated with @external (i.e., fallback method is not allowed to be called explicitly by other contract or user). An exception will be raised and the SCORE will be rejected during its deployment if fallback has @external decorator.

If you query the SCORE API list, you will receive fallback method only if it is decorated with @payable decorator.

Storing State Data

The state of SCORE should be stored in the state database in the blockchain. The state of SCORE simply means the values of member variables in the smart contract which are declared as VarDB, DictDB, and ArrayDB. The state of SCORE is changed only by the execution of transactions. All nodes execute transactions and change the state of the smart contract in their own state database independently. To be confirmed, the changed state should be agreed between the ⅔ of nodes after each execution of transactions, and this process is called consensus in the blockchain.

In general, the state database is a key-value database, and can be accessed using VarDB, DictDB and ArrayDB classes.

VarDB, DictDB, and ArrayDB are utility classes wrapping the state database. A key can be numbers or characters, and value_type can be int, str, bytes and Address. If the key does not exist, the wrapping classes return the zero value of value_type, which is 0 for int, "" for str, and None for bytes or Address.

VarDB can be used to store simple key-value state, and DictDB behaves more like Python dict. DictDB does not maintain order, whereas ArrayDB, which supports length and iterator, maintains order.

VarDB(‘key’, ‘target db’, ‘return type’)

Example) Setting a value "icon" for the key "name" on the state DB:

VarDB('name', db, value_type=str).set('icon')

Example) Getting value by the key "name":

name = VarDB('name', db, value_type=str).get()
print(name) ## 'icon'

DictDB(‘key’, ‘target db’, ‘return type’, ‘dict depth (default is 1)’)

Example 1) One-depth dict

test_dict1 = DictDB('test_dict1', db, value_type=int)
test_dict1['key'] = 1 ## set
print(test_dict1['key']) ## get 1

print(test_dict1['nonexistence_key']) # prints 0 (key does not exist and value_type=int)

Example 2) Two-depth dict

test_dict2 = DictDB('test_dict2', db, value_type=str, depth=2)
test_dict2['key1']['key2'] = 'a' ## set
print(test_dict2['key1']['key2']) ## get 'a'

print(test_dict2['key1']['nonexistent_key']) # prints "" (key does not exist and value_type=str)

If the depth is 2 or greater, test_dictN['key'] returns new DictDB with the depth N-1. Attempting to set a value to the wrong depth in the DictDB will raise an exception.

Example 3)

test_dict3 = DictDB('test_dict3', db, value_type=int, depth=3)
test_dict3['key1']['key2']['key3'] = 1 ## ok
test_dict3['key1']['key2'] = 1 ## raise mismatch exception

test_dict2 = test_dict3['key']['key2']
test_dict2['key1'] = 1 ## ok

ArrayDB(‘key’, ‘target db’, ‘return type’)

ArrayDB supports one-dimensional array only. ArrayDB supports put, get and pop operations, i.e., the data can be accessed in a stack-like fashion. It does not support insert or delete operation (adding or removing elements in the middle of the array).

test_array = ArrayDB('test_array', db, value_type=int)
test_array.put(0)
test_array.put(1)
test_array.put(2)
test_array.put(3)
print(len(test_array)) ## prints 4
print(test_array.pop()) ## prints 3
test_array[0] = 0 ## ok
# test_array[100] = 1 ## error
for e in test_array: ## ok
    print(e)
print(test_array[-1]) ## ok
# print(test_array[-100]) ## error

Cautions about VarDB, DictDB and ArrayDB

The state should be stored in persistent storage (state database). Variables which reside in memory are volatile and will be reset when the nodes are restarted. Thus SCORE developers need to use the wrapping classes of the state database, VarDB, ArrayDB, and DictDB, to store the state permanently. Please be careful that you use these classes correctly, otherwise, you may have unexpected results.

Example)

varDB = VarDB(...)
varDB.set(100)       # right
varDB = 100          # wrong

arrayDB = ArrayDB(...)
arrayDB[0] = 100     # right
arrayDB = 100        # wrong

dictDB = DictDB(...)
dictDB['key0'] = 100 # right
dictDB = 100         # wrong

Invoking Other SCORE Methods

InterfaceScore

InterfaceScore is an interface class that is used to invoke other SCORE’s method. This interface should be used instead of the legacy call method. Usage syntax is as follows.

class TokenInterface(InterfaceScore):
    @interface
    def transfer(self, _to: Address, _value: int, _data: bytes=None):
        pass

If another SCORE has the method whose signature is the same as defined here with @interface decorator, the method can be invoked via InterfaceScore class object. Like @eventlog decorator, it is recommended to declare a method without an implementation body. If there is an implementation body, it will be simply ignored.

You need to get an InterfaceScore object by using IconScoreBase’s built-in API, create_interface_score('score address', 'interface class'). Using the object, you can invoke other SCORE’s external method as if it is a local method call.

Example)

# excerpt from Crowdsale SCORE
@payable
def fallback(self):
    ...
    data = b'called from Crowdsale'

    # gets an interface object of the token SCORE
    token_score = self.create_interface_score(self._addr_token_score.get(), TokenInterface)
    # transfers tokens to the contributor as a reward
    token_score.transfer(self.msg.sender, value, data)
    ...

Transferring ICX

Users can transfer ICX coins using icx object and it offers 2 methods, send and transfer.

  • icx.transfer(addr_to: Aaddress, amount: int) -> None

    • Transfers designated amount of ICX coins to addr_to.
    • If an exception occurs during execution, the exception will be escalated to the user.
  • icx.send(addr_to: Address, amount: int) -> bool

    • Sends the designated amount of ICX coins to addr_to.
    • Basic behavior is the same as icx.transfer, but the raised exception will be caught inside the method.
    • Returns True when the coin transfer succeeded, False when it failed.

Type Hints

Type hinting is highly recommended for the input parameters and return value. When clients want to query the list of SCORE's API, the API specification is generated based on its type hints. If type hints are not given to external methods, just method names will return.

@external(readonly=True)
def func1(arg1: int, arg2: str) -> int:
    return 100

Possible data types for method parameters are int, str, bytes, bool and Address. list and dict type parameters are not supported for method parameters. Return types can be int, str, bytes, bool, Address, list and dict.

Exception Handling

When you handle exceptions in your contract, it is recommended to use revert function rather than using an IconScoreException.

Restrictions

There are certain SCORE functions that ICON Tracker calls to display the SCORE information - name, symbol, and decimal. ICON Tracker loads this information once on the initial SCORE deploy, and will never update. This is to prevent any attempt to fraud, and not to confuse end-users. Therefore, ICON prevents changing name, symbol, and decimal of SCORE. Please read the Audit Checklist for detailed coding guideline.

name, decimal, symbol

For IRC-2 token contract, you should not change name, decimal, and symbol of the token once deployed. For every other SCORE, you should not change name once deployed. Note that unlike IRC tokens, name is not a mandatory function to implement. If you didn't implement name function in your first deploy, you cannot add name function on a subsequent update, because it is considered as changing the name.

Reference

  • iconservice API references
  • Token & Crowdsale
  • T-Bears CLI Reference
  • Audit Checklist