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.
The intended audience is the developers who have basic Python programming knowledge.
After reading this document, you will understand the structure of SCORE and learn the basic syntax of writing SCORE.
- SCORE Overview
- Token & Crowdsale
- T-Bears CLI Commands
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/
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"
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__
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)
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.
This method is called when the smart contract is updated.
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 informationtx.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- This object provides two methods, see Transferring ICX for details.
db
: An instance used to access the state DBaddress
: Address of the SCOREowner
: Address of the account who deployed the contractblock_height
: Current block heightnow
: Wrapping method ofblock.timestamp
.
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.
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.
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.
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
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.
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
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
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)
...
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.
- Transfers designated amount of ICX coins to
-
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.
- Sends the designated amount of ICX coins to
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
.
When you handle exceptions in your contract, it is recommended to use revert
function
rather than using an IconScoreException
.
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.
- iconservice API references
- Token & Crowdsale
- T-Bears CLI Reference
- Audit Checklist