This document presents how to write a SCORE, smart contract of the ICON network. Through the series of documents, 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. It is highly recommended to look up the reference Token & Crowdsale implementation along with this document to grasp the SCORE programming syntax.
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: 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.
init command will create a SCORE project.
$ tbears init [project_name] [main_class_name] $ tbears init hello_world HelloWorld
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 (
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
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__ 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.senderpoints 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
- This object provides two methods, see Transferring ICX for details.
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
Users can implement methods which are supposed to be invoked from outside the blockchain.
These methods can be decorated with
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 (
Address (newly defined type for SCORE codes).
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
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
It is recommended to declare a method without an implementation body. Even if the method has an implementation body, it won't be executed.
When declaring a method, Python type hints must be specified. Without type hinting, SCORE loading will fail.
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.
# 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')
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
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
If you query the SCORE API list, you will receive
fallback method only if it is decorated with
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.
key can be numbers or characters, and
value_type can be
key does not exist, the wrapping classes return the zero value of
value_type, which is 0 for
int, "" for
str, and None for
VarDB can be used to store simple key-value state, and DictDB behaves more like Python
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.
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.
pop operations, i.e., the data can be accessed in a stack-like fashion. It does not support
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 ## ok # test_array = 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.
varDB = VarDB(...) varDB.set(100) # right varDB = 100 # wrong arrayDB = ArrayDB(...) arrayDB = 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.
@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.
# 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,
icx.transfer(addr_to: Address, amount: int) -> None
- Transfers designated amount of ICX coins 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
- Basic behavior is the same as
icx.transfer, but the raised exception will be caught inside the method.
Truewhen the coin transfer succeeded,
Falsewhen 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
dict type parameters are not supported for method parameters.
Return types can be
When you handle exceptions in your contract, it is recommended to use
revert function rather than using an
There are certain SCORE functions that ICON Tracker calls to display the SCORE information -
decimals. 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
decimals of SCORE. Please read the Audit Checklist for detailed coding guideline.
name, symbol, decimals
For IRC-2 token contract, you should not change
decimalsof the token once deployed.
For every other SCORE, you should not change
nameonce deployed. Note that unlike IRC tokens,
nameis not a mandatory function to implement. If you didn't implement
namefunction in your first deploy, you cannot add
namefunction on a subsequent update, because it is considered as changing the name.
Updated 8 months ago