diff --git a/README.md b/README.md index 8d864d2..3ba0504 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ Documents that merged into the master branch are published on the developers por - [account-management](./howto/account-management.md) - [how-to-change-network-in-iconex-chrome](./howto/how-to-change-network-in-iconex-chrome.md) - [how-to-generate-a-transaction-signature](./howto/how-to-generate-a-transaction-signature.md) + - [how-to-write-score-integration-test](./howto/how-to-write-score-integration-test.md) + - [how-to-write-score-unit-test](./howto/how-to-write-score-unit-test.md) - [icon-development-network-on-aws-marketplace](./howto/icon-development-network-on-aws-marketplace.md) - icon-key-concepts - [accounts](./icon-key-concepts/accounts.md) @@ -30,6 +32,9 @@ Documents that merged into the master branch are published on the developers por - [swift-api-reference](./references/swift-api-reference.md) - [swift-sdk](./references/swift-sdk.md) - [t-bears-reference](./references/t-bears-reference.md) +- sample-scores + - [multi-signature-wallet](./sample-scores/multi-signature-wallet.md) + - [token-crowdsale](./sample-scores/token-crowdsale.md) - score - [audit-checklist](./score/audit-checklist.md) - [score-audit](./score/score-audit.md) diff --git a/howto/how-to-write-score-integration-test.md b/howto/how-to-write-score-integration-test.md new file mode 100644 index 0000000..2c5e2f1 --- /dev/null +++ b/howto/how-to-write-score-integration-test.md @@ -0,0 +1,192 @@ +--- +title: "How to write SCORE integration test" +--- + +This document explains how to write SCORE integration test using `IconIntegrateTestBase`. + +## Purpose + +Understand how to write SCORE integration test + +## Prerequisite + +* [SCORE Overview](score-overview) +* [T-Bears Overview](tbears-overview) +* [ICON Python SDK](python-sdk) + +## How to Write SCORE Integration Test Code + +The SCORE integration test code works as follows. + +1. Deploy the SCORE to be tested +2. Create an ICON JSON-RPC API request for the SCORE API you want to test +3. If necessary, sign an ICON JSON-RPC API request +4. Invoke an ICON JSON-RPC API request and get the result +5. Check the result + +### Packages and modules + +#### ICON Python SDK +You can create and sign an ICON JSON-RPC API request using the ICON Python SDK + +```python +# create key wallet +self._test = KeyWallet.create() + +# Generates an instance of transaction for deploying SCORE. +transaction = DeployTransactionBuilder() \ + .from_(self._test.get_address()) \ + .to(to) \ + .step_limit(100_000_000_000) \ + .nid(3) \ + .nonce(100) \ + .content_type("application/zip") \ + .content(gen_deploy_data_content(self.SCORE_PROJECT)) \ + .build() + +# Returns the signed transaction object having a signature +signed_transaction = SignedTransaction(transaction, self._test) +``` + + + +#### IconIntegrateTestBase in T-Bears + +Every SCORE integration test class must inherit `IconIntegrateTestBase`. + +IconIntegrateTestBase class provides three functions + +1. Support Python unittest + 1. You can write and run the test method with prefix 'test_' + 2. You can initialize and finalize the test by override setUp and tearDown method + +2. Emulate ICON service for test + 1. Initialize ICON service and confirm genesis block + 2. Create accounts for test + 1. self._test1 : Account with 1,000,000 ICX + 2. self._wallet_array[] : 10 empty accounts in list + +3. Provide API for SCORE integration test + 1. process_transaction() + Invoke transaction and return transaction result + 2. process_call() + Calls SCORE's external function which is read-only and returns result + +### examples + +You can get the source code with `tbears init score_test ScoreTest` command. + +#### score_test.py + +```python +from iconservice import * + +TAG = 'ScoreTest' + +class ScoreTest(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" +``` + +#### score_tests/test_score_test.py + +```python +import os + +from iconsdk.builder.transaction_builder import DeployTransactionBuilder +from iconsdk.builder.call_builder import CallBuilder +from iconsdk.libs.in_memory_zip import gen_deploy_data_content +from iconsdk.signed_transaction import SignedTransaction + +from tbears.libs.icon_integrate_test import IconIntegrateTestBase, SCORE_INSTALL_ADDRESS + +DIR_PATH = os.path.abspath(os.path.dirname(__file__)) + + +class TestScoreTest(IconIntegrateTestBase): + TEST_HTTP_ENDPOINT_URI_V3 = "http://127.0.0.1:9000/api/v3" + SCORE_PROJECT= os.path.abspath(os.path.join(DIR_PATH, '..')) + + def setUp(self): + super().setUp() + + self.icon_service = None + # If you want to send request to network, uncomment next line and set self.TEST_HTTP_ENDPOINT_URI_V3 + # self.icon_service = IconService(HTTPProvider(self.TEST_HTTP_ENDPOINT_URI_V3)) + + # deploy SCORE + self._score_address = self._deploy_score()['scoreAddress'] + + def _deploy_score(self, to: str = SCORE_INSTALL_ADDRESS) -> dict: + # Generates an instance of transaction for deploying SCORE. + transaction = DeployTransactionBuilder() \ + .from_(self._test1.get_address()) \ + .to(to) \ + .step_limit(100_000_000_000) \ + .nid(3) \ + .nonce(100) \ + .content_type("application/zip") \ + .content(gen_deploy_data_content(self.SCORE_PROJECT)) \ + .build() + + # Returns the signed transaction object having a signature + signed_transaction = SignedTransaction(transaction, self._test1) + + # process the transaction in local + tx_result = self._process_transaction(signed_transaction) + + # check transaction result + self.assertTrue('status' in tx_result) + self.assertEqual(1, tx_result['status']) + self.assertTrue('scoreAddress' in tx_result) + + return tx_result + + def test_score_update(self): + # update SCORE + tx_result = self._deploy_score(self._score_address) + + self.assertEqual(self._score_address, tx_result['scoreAddress']) + + def test_call_hello(self): + # Generates a call instance using the CallBuilder + call = CallBuilder().from_(self._test1.get_address()) \ + .to(self._score_address) \ + .method("hello") \ + .build() + + # Sends the call request + response = self._process_call(call, self.icon_service) + + # check call result + self.assertEqual("Hello", response) +``` + +#### Run test code + +```bash +$ tbears test score_test +.. +---------------------------------------------------------------------- +Ran 2 tests in 0.172s + +OK +``` + +## References + +* [ICON Python SDK](python-sdk) +* [ICON SCORE samples](sample-scores) + diff --git a/howto/how-to-write-score-unit-test.md b/howto/how-to-write-score-unit-test.md new file mode 100644 index 0000000..b2a15a2 --- /dev/null +++ b/howto/how-to-write-score-unit-test.md @@ -0,0 +1,368 @@ +--- +title: "How to write SCORE unit test" +--- + +This document explains how to write SCORE unit-test using T-Bears unit-test framework. + +## Purpose + +Understand how to write SCORE unit-test + +## Prerequisite + +* [SCORE Overview](score-overview) +* [T-Bears Overview](doc:tbears-overview) +* [ICON Python SDK](python-sdk) + + +## How to Write SCORE Unit Test Code + +SCORE unittest should inherit `ScoreTestCase`. The SCORE unit test code works as follows + +- Get SCORE instance to be tested +- Call SCORE method +- Check the result + +### Functions Provided by ScoreTestCase + +- Instantiate SCORE + - Instantiate SCORE. So, you can access attributes and methods of SCORE like a general object. +- Set `property` in SCORE + - Provide the ability to set properties used inside SCORE methods. +- Mock state DB + - Store changed state caused by SCORE method invocation. The state is stored in memory, not in the file system. +- Mock event log + - It is sufficient to check that the event log has been called +- Mock internalCall(call external function in SCORE). + - The operation on the other SCORE is considered to be reliable. so, what you need is InternalCall called with specified arguments. + +### Methods + +`ScoreTestCase` has 11 main methods. Inside `setUp` method and `tearDown` method, `ScoreTestCase` sets the environment for SCORE unit-test and clears them. +So, if you want to override `setUp` or `tearDown`, you should call `super()` as a first statement in the overridden method. + +#### Getting SCORE instance +- get_score_instance(score_class, owner, on_install_params) + - Get an instance of the SCORE class passed as an `score_class` argument + - **parameters** + - **score_class** : SCORE to instantiate + - **owner** : Address to set as owner of SCORE + - **on_install_params** : parameters of on_install_method + - Refer to `setUp` method in [simple_score2/tests/test_unit_simple_score2.py] +- update_score(prev_score_address, score_class, on_update_params) + - Update SCORE at `prev_score_address` with `score_class` instance and get updated SCORE + - **parameters** + - **Prev_score_address** : address of SCORE to update + - **score_class** : SCORE class to update + - **on_update_params** : parameters of on_update method + - Refer to `test_update` method in [simple_score2/tests/test_unit_simple_score2.py] + +#### Setting SCORE properties +- set_msg(sender, value) + - Set msg property in SCORE + - **parameters** + - **sender** : Set sender attribute of msg to given sender argument + - **value** : Set value attribute of msg to given sender argument + - Refer to `test_msg` method in [simple_score2/tests/test_unit_simple_score2.py] +- set_tx(origin, timestamp, _hash, index, nonce) + - Set tx property in SCORE + - **parameters** + - **origin** : Set origin attribute of tx to given origin argument + - **timestamp** : Set timestamp attribute of tx to given timestamp argument + - **_hash** : Set hash attribute of tx to given _hash argument + - **index** : Set index attribute of tx to given index argument + - **nonce** : Set nonce attribute of tx to given nonce argument + - Refer to `test_tx` method in [simple_score2/tests/test_unit_simple_score2.py] +- set_block(height, timestamp) + - Set the block property inside SCORE. + If you pass only height, the value of block.timestamp is set to height * 2 seconds. + When this method is called, the block_height inside the SCORE associated with the block is set to height, and the return value of the now () method is set to timestamp. + It should be called if you use the value associated with the block information in the SCORE method you are calling. + - **parameters** + - **height** : Set height attribute of block to given height argument + - **timestamp** : Set timestamp attribute of block to given timestamp argument + - Refer to `test_block` method in [simple_score2/tests/test_unit_simple_score2.py] + +#### Patching InternalCall & asserting InternalCall +- register_interface_score(internal_score_address) + - This method should be called before testing the internal_call that calls the SCORE method with an `internal_score_address` address. + If you call this method, you can use the `assert_internal_call` method to evaluate whether internal_call is called properly with specified arguments. + - **parameters** + - **internal_score_address** : address of interface SCORE + - Refer to `test_internal2` method in [simple_score2/tests/test_unit_simple_score2.py] +- patch_internal_method(score_address, method, new_method) + - You will use this method for patching query method to set return value. + Since this function internally calls `register_interface_score`, you don't need to call `register_interface_score` when calling this function. + The third argument, the new method, must be a function with the same number of arguments as the actual method. + - **parameters** + - **internal_score_address** : address of the SCORE having method to be called + - **method** : method to be patched + - **new_method** : method to patch + - Refer to `test_interanl` method in [simple_score2/tests/test_unit_simple_score2.py] +- assert_internal_call(internal_score_address, method, *params) + - assert that internal call(mock) was called with the specified arguments. Raises an AssertionError if the params passed in are different to the last call to the mock. + - **parameters** + - **internal_score_address** : address of internal call SCORE + - **method** : method to check + - **params** : params to check + - Refer to `test_internal` method in [simple_score2/tests/test_unit_simple_score2.py] + +#### Utils +- transfer(_from, to, amount) + - Transfer icx to given 'to' address. If you pass a SCORE address to the `to` argument, this method calls the SCORE fallback method. + - **parameters** + - **_from** : address of sender + - **to** : address of receiver + - **amount** : amount to transfer + - Refer to `test_transfer` method in [simple_score2/tests/test_unit_simple_score2.py] +- get_balance(address) + - Query icx balance of given address. + - **parameters** + - **address** : address to query for icx balance + - Refer to `test_get_balance` method in [simple_score2/tests/test_unit_simple_score2.py] +- initialize_accounts(accounts_info) + - Initialize accounts using given dictionary info. + - **parameters** + - **accounts_info** : dictionary with address as key and balance as value + - Refer to `setUp` method in [simple_score2/tests/test_unit_simple_score2.py] + +### Examples + +In this example, we'll use two simple SCOREs only have getter and setter. (the first SCORE has getter and setter and another SCORE has internalCall, getter, and setter) + +#### simple_score/simple_score.py + +```python +from iconservice import * + +class SimpleScore(IconScoreBase): + + def __init__(self, db: IconScoreDatabase) -> None: + super().__init__(db) + self.value = VarDB("value", db, value_type=str) + + @eventlog() + def SetValue(self, value: str): pass + + def on_install(self) -> None: + super().on_install() + + def on_update(self) -> None: + super().on_update() + self.value.set("updated value") + + @external(readonly=True) + def hello(self) -> str: + return "Hello" + + @external + def setValue(self, value: str): + self.value.set(value) + + self.SetValue(value) + + @external(readonly=True) + def getValue(self) -> str: + return self.value.get() + +``` + +#### simple_score2/simple_score2.py + +```python +from iconservice import * + + +class SimpleScoreInterface(InterfaceScore): + @interface + def setValue(self, value): pass + + @interface + def getValue(self)->str: pass + + +class SimpleScore2(IconScoreBase): + + def __init__(self, db: IconScoreDatabase) -> None: + super().__init__(db) + self.value = VarDB("value1", db, value_type=str) + self.score_address = VarDB("score_address", db, value_type=Address) + + @eventlog(indexed=0) + def SetValue(self, value: str): pass + + @eventlog(indexed=1) + def SetSCOREValue(self, value: str): pass + + def on_install(self, score_address: 'Address') -> None: + super().on_install() + self.score_address.set(score_address) + + def on_update(self, value: str) -> None: + super().on_update() + self.value.set(value) + + @external(readonly=True) + def getValue(self) -> str: + return self.value.get() + + @external + def setValue(self, value: str): + self.value.set(value) + + self.SetValue(value) + + @external + def setSCOREValue(self, value: str): + score = self.create_interface_score(self.score_address.get(), SimpleScoreInterface) + score.setValue(value) + + self.SetSCOREValue(value) + + @external(readonly=True) + def getSCOREValue(self) ->str: + score = self.create_interface_score(self.score_address.get(), SimpleScoreInterface) + + return score.getValue() + + @external(readonly=True) + def write_on_readonly(self) ->str: + self.value.set('3') + return 'd' + + # This method is for understanding the ScoreTestCase.set_msg method. + def t_msg(self): + assert self.msg.sender == Address.from_string(f"hx{'1234'*10}") + assert self.msg.value == 3 + + # This method is for understanding the ScoreTestCase.set_tx method. + def t_tx(self): + assert self.tx.origin == Address.from_string(f"hx{'1234'*10}") + + # This method is for understanding the ScoreTestCase.set_block method. + def t_block(self): + assert self.block.height == 3 + assert self.block.timestamp == 30 + assert self.block_height ==3 + assert self.now() == 30 + +``` + +#### simple_score2/tests/test_unit_simple_score2.py + +```python +from iconservice import Address +from iconservice.base.exception import DatabaseException +from tbears.libs.scoretest.score_unit_test_base import ScoreTestCase +from simple_score2 import SimpleScore2 + +class TestSimple(ScoreTestCase): + def setUp(self): + super().setUp() + self.mock_score_address = Address.from_string(f"cx{'1234'*10}") + self.score2 = self.get_score_instance(SimpleScore2, self.test_account1, + on_install_params={'score_address': self.mock_score_address}) + + self.test_account3 = Address.from_string(f"hx{'12345'*8}") + self.test_account4 = Address.from_string(f"hx{'1234'*10}") + account_info = { + self.test_account3: 10 ** 21, + self.test_account4: 10 ** 21} + self.initialize_accounts(account_info) + + def test_set_value(self): + str_value = 'string_value' + self.score2.setValue(str_value) + # assert event log called with specified arguments + self.score2.SetValue.assert_called_with(str_value) + + self.assertEqual(self.score2.getValue(), str_value) + + def test_get_value_and_set_value(self): + # at first, value is empty string + self.assertEqual(self.score2.getValue(), '') + + str_value = 'strValue' + self.score2.setValue(str_value) + + self.assertEqual(self.score2.getValue(), str_value) + + # try writing value inside readonly method + def test_write_on_readonly(self): + self.assertRaises(DatabaseException, self.score2.write_on_readonly) + + # internal call + def test_internal_call(self): + self.patch_internal_method(self.mock_score_address, 'getValue', lambda: 150) # Patch the getValue function of SCORE at self.mock_score_address address with a function that takes no argument and returns 150 + value = self.score2.getSCOREValue() + self.assertEqual(value, 150) + self.assert_internal_call(self.mock_score_address, 'getValue') # assert getValue in self.mock_score_address is called. + + self.score2.setSCOREValue('asdf') + self.assert_internal_call(self.mock_score_address, 'setValue', 'asdf') # assert setValue in self.mock_score_address is called with 'asdf' + + # internal call + def test_internal_call2(self): + # To determine whether a method is called properly with specified arguments, calling register_interface_score method is enough + self.register_interface_score(self.mock_score_address) + self.score2.setSCOREValue('asdf') + self.assert_internal_call(self.mock_score_address, 'setValue', 'asdf') + + def test_msg(self): + self.set_msg(Address.from_string(f"hx{'1234'*10}"), 3) + self.score2.t_msg() # On the upper line, set the msg property to pass the assert statement so that no exception is raised. + + self.set_msg(Address.from_string(f"hx{'12'*20}"), 3) + self.assertRaises(AssertionError, self.score2.t_msg) # On the upper line, set the msg property not to pass the assert statement, and raise an exception. + + def test_tx(self): + self.set_tx(Address.from_string(f"hx{'1234'*10}")) + self.score2.t_tx() # On the upper line, set the tx property to pass the assert statement so that no exception is raised. + + self.set_tx(Address.from_string(f"hx{'12'*20}")) + self.assertRaises(AssertionError, self.score2.t_tx) # On the upper line, set the tx property not to pass the assert statement, and raise an exception. + + def test_block(self): + self.set_block(3, 30) + self.score2.t_block() # On the upper line, set the block property to pass the assert statement so that no exception is raised. + + self.set_block(3) + self.assertRaises(AssertionError, self.score2.t_block) # On the upper line, set the block property not to pass the assert statement, and raise an exception. + + def test_update(self): + self.score2 = self.update_score(self.score2.address, SimpleScore2, on_update_params={"value": "updated_value"}) + self.assertEqual(self.score2.value.get(), "updated_value") # In the on_update method of SimpleScore2, set the value of the value to "updated_value". + + def test_get_balance(self): + balance = self.get_balance(self.test_account3) + self.assertEqual(balance, 10**21) + + def test_transfer(self): + # before calling transfer method, check balance of test_account3 and test_account4 + amount = 10**21 + balance_3 = self.get_balance(self.test_account3) + self.assertEqual(balance_3, amount) + balance_4 = self.get_balance(self.test_account4) + self.assertEqual(balance_4, amount) + + self.transfer(self.test_account3, self.test_account4, amount) + # after calling transfer method, check balance of test_account3 and test_account4 + balance_3 = self.get_balance(self.test_account3) + self.assertEqual(balance_3, 0) + balance_4 = self.get_balance(self.test_account4) + self.assertEqual(balance_4, amount*2) +``` + +#### Run test code + +```bash +$ tbears test simple_score2 +........ +---------------------------------------------------------------------- +Ran 11 tests in 0.027s + +OK +``` + +[simple_score2/tests/test_unit_simple_score2.py]:#section-simple_score2-tests-test_unit_simple_score2-py + diff --git a/sample-scores/multi-signature-wallet-1.png b/sample-scores/multi-signature-wallet-1.png new file mode 100755 index 0000000..96d7238 Binary files /dev/null and b/sample-scores/multi-signature-wallet-1.png differ diff --git a/sample-scores/multi-signature-wallet.md b/sample-scores/multi-signature-wallet.md new file mode 100644 index 0000000..a121a18 --- /dev/null +++ b/sample-scores/multi-signature-wallet.md @@ -0,0 +1,904 @@ +--- +title: "Multi signature Wallet tutorial" +--- + +This document describes the inner workings of a Multi Signature Wallet SCORE and provides guidelines and APIs about how to use this service. + +## Intended Audience + +- Users who want to use and manage multi signature wallet service +- Developers who want to know the basic logic of multi-signature wallet and extend for their own needs + +## Purpose + +This page covers several things about multi signature wallet. Below is available to learn from this tutorial. + +- Basic understanding of multi signature wallet +- how to use multi signature wallet + +## Prerequisite + +Below is the list of links that helps you understand multi signature wallet and proceed with the tutorial. + +- [Definition of multi signature wallet](https://en.bitcoin.it/wiki/Multisignature) +- SCORE Overview +- SCORE by Example + +## What is Multi Signature Wallet + +A Multi Signature Wallet is a SCORE that enables more than one user to manage their ICON funds safely. Such wallet can prevent one person from running off with the stored ICX or tokens and reduce the risk in case of one person is incapacitated or loses their keys. We adopted the multi signature wallet mechanism inspired by [**gnosis**](https://github.com/gnosis/MultiSigWallet). + +## Definition + +### Wallet + +SCORE in which ICX and tokens are stored in. Stored ICX and tokens can be used (transferred) only when the wallet conditions declared internally are satisfied. + +### Wallet owner + +Addresses who have participation rights of the Wallet SCORE. + +### Transaction + +Initiated by a wallet owner, a transaction changes the wallet state (e.g. transfer tokens or ICX stored in the wallet, add a new wallet owner, change requirement of confirmations (2 to 3 -> 3 to 3), etc). + +### Requirement + +The number of approvals from the wallet owners required for the transaction to be executed. + +## How To Use + +The first step is to deploy a multi signature wallet SCORE. At the time of deployment, you can set wallet owners and a requirement value. For example, if you want to use a wallet which sets three wallet owners and needs two confirmations for executing a transaction (2 to 3), you have to input two parameters: 1) fill the `_walletOwners` field with three wallet addresses in a comma-separated string, 2) fill the `_required` field with '2' when deploying the wallet. + +```json +{ + "_walletOwners": "hx7f39710d3718e7d1f307d7c71755fbbe76be3c71,hx07a2731037cfe59dbf76579d8ba1fbfc02616135,hxed36008ce6be8c8deb9acdfc05d1a06f5576a2db", + "_required": "2" +} +``` + +After deploying the wallet, wallet owners can deposit ICX and tokens to this wallet as usual and manage it. + +If you want to use funds (e.g. send ICX or token) or change the internally set conditions (e.g. add owner, remove owner, change requirement), use the `submitTransaction` method. For example, if you want to send 10 ICX to a specific address, call `submitTransaction` with below parameters. + +```json +{ + "_destination": "hx7f39710d3718e7d1f307d7c71755fbbe76be3c71", + "_description": "send 10 icx to owner1", + "_value": "0x8ac7230489e80000" +} +``` +After the transaction is registered, other wallet owners can confirm this transaction using the `confirmTransaction` method, and only if the number of confirmations meets the 'requirement' value then this transaction is executed. All transactions' information are saved in the wallet eternally. + +## Specification + +### Methods (Read-only) + +Below is the list of read-only methods. By calling these methods, you can get information from the wallet. + +#### getRequirement + +Returns the requirement value. + +```python +@external(readonly=True) +def getRequirement(self) -> int: +``` + +**Example** + +```json +{ + "jsonrpc": "2.0", + "method": "icx_call", + "id": 1, + "params": { + "from": "hx5dd0b2a161bc16194d38b050744c7cd623626661", + "to": "cx30d7fcf580135d9f9eb491292555a5b29d9314cb", + "dataType": "call", + "data": { + "method": "getRequirement", + "params": {} + } + } +} +``` + +**Call result** + +```json +{ + "jsonrpc": "2.0", + "result": "0x2", + "id": 1 +} +``` + +#### getTransactionInfo + +Returns the transaction data for each ID. + +```python +@external(readonly=True) +def getTransactionInfo(self, _transactionId: int) -> dict: +``` + +**Example** + +```json +{ + "jsonrpc": "2.0", + "method": "icx_call", + "id": 1, + "params": { + "from": "hx5dd0b2a161bc16194d38b050744c7cd623626661", + "to": "cx30d7fcf580135d9f9eb491292555a5b29d9314cb", + "dataType": "call", + "data": { + "method": "getTransactionInfo", + "params": { + "_transactionId": "0x00" + } + } + } +} +``` + +**Call result** + +```json +{ + "jsonrpc": "2.0", + "result": { + "_executed": "0x1", + "_destination": "cx30d7fcf580135d9f9eb491292555a5b29d9314cb", + "_value": "0x0", + "_method": "addWalletOwner", + "_params": "[{\"name\":\"_walletOwner\",\"type\":\"Address\",\"value\":\"hx1262526a4da004550021b5f9d249b9c7d98b5892\"}]", + "_description": "add owner4 in wallet", + "_transaction_id": "0x0" + }, + "id": 1 +} +``` + +#### getTransactionsExecuted + +Returns a boolean which shows whether the transaction is executed or not. + +```python +@external(readonly=True) +def getTransactionsExecuted(self, _transactionId: int) -> bool: +``` + +**Example** + +```json +{ + "jsonrpc": "2.0", + "method": "icx_call", + "id": 1, + "params": { + "from": "hx5dd0b2a161bc16194d38b050744c7cd623626661", + "to": "cx30d7fcf580135d9f9eb491292555a5b29d9314cb", + "dataType": "call", + "data": { + "method": "getTransactionsExecuted", + "params": { + "_transactionId": "0x00" + } + } + } +} +``` + +**Call result** + +```json +{ + "jsonrpc": "2.0", + "result": "0x1", + "id": 1 +} +``` + +#### checkIfWalletOwner + +Returns a boolean which shows whether a given address is a wallet owner or not. + +```python +@external(readonly=True) +def checkIfWalletOwner(self, _walletOwner: Address) -> bool: +``` + +**Example** + +```json +{ + "jsonrpc": "2.0", + "method": "icx_call", + "id": 1, + "params": { + "from": "hx5dd0b2a161bc16194d38b050744c7cd623626661", + "to": "cx30d7fcf580135d9f9eb491292555a5b29d9314cb", + "dataType": "call", + "data": { + "method": "checkIfWalletOwner", + "params": { + "_walletOwner": "hx07a2731037cfe59dbf76579d8ba1fbfc02616135" + } + } + } +} +``` + +**Call result** + +```json +{ + "jsonrpc": "2.0", + "result": "0x0", + "id": 1 +} +``` + +#### getWalletOwnerCount + +Returns the total number of wallet owners. + +```python +@external(readonly=True) +def getWalletOwnerCount(self) -> int: +``` + +**Example** + +```json +{ + "jsonrpc": "2.0", + "method": "icx_call", + "id": 1, + "params": { + "from": "hx5dd0b2a161bc16194d38b050744c7cd623626661", + "to": "cx30d7fcf580135d9f9eb491292555a5b29d9314cb", + "dataType": "call", + "data": { + "method": "getWalletOwnerCount", + "params": {} + } + } +} +``` + +**Call result** + +```json +{ + "jsonrpc": "2.0", + "result": "0x3", + "id": 1 +} +``` + +#### getWalletOwners + +Returns a list of wallet owners. + +```python +@external(readonly=True) +def getWalletOwners(self, _offset: int, _count: int) -> list: +``` + +**Example** + +```json +{ + "jsonrpc": "2.0", + "method": "icx_call", + "id": 1, + "params": { + "from": "hx5dd0b2a161bc16194d38b050744c7cd623626661", + "to": "cx30d7fcf580135d9f9eb491292555a5b29d9314cb", + "dataType": "call", + "data": { + "method": "getWalletOwners", + "params": { + "_offset": "0x00", + "_count": "0x0A" + } + } + } +} +``` + +**Call result** + +```json +{ + "jsonrpc": "2.0", + "result": [ + "hx5dd0b2a161bc16194d38b050744c7cd623626661", + "hxd980b07d43d1df399392f8871d6ec7c975f3e832", + "hx4873b94352c8c1f3b2f09aaeccea31ce9e90bd31", + "hx1262526a4da004550021b5f9d249b9c7d98b5892" + ], + "id": 1 +} +``` + +#### getConfirmationCount + +Returns a transaction confirmation count given a transaction ID. + +```python +@external(readonly=True) +def getConfirmationCount(self, _transactionId: int) -> int: +``` + +**Example** + +```json +{ + "jsonrpc": "2.0", + "method": "icx_call", + "id": 1, + "params": { + "from": "hx5dd0b2a161bc16194d38b050744c7cd623626661", + "to": "cx30d7fcf580135d9f9eb491292555a5b29d9314cb", + "dataType": "call", + "data": { + "method": "getConfirmationCount", + "params": { + "_transactionId": "0x00" + } + } + } +} +``` + +**Call result** + +```json +{ + "jsonrpc": "2.0", + "result": "0x2", + "id": 1 +} +``` + +#### getConfirmations + +Returns a list of wallet owners who have been confirmed by a given transaction. + +```python +@external(readonly=True) +def getConfirmations(self, _offset: int, _count: int, _transactionId: int) -> list: +``` +**Example** + +```json +{ + "jsonrpc": "2.0", + "method": "icx_call", + "id": 1, + "params": { + "from": "hx5dd0b2a161bc16194d38b050744c7cd623626661", + "to": "cx30d7fcf580135d9f9eb491292555a5b29d9314cb", + "dataType": "call", + "data": { + "method": "getConfirmations", + "params": { + "_offset": "0x00", + "_count": "0x0A", + "_transactionId": "0x00" + } + } + } +} +``` + +**Call result** + +```json +{ + "jsonrpc": "2.0", + "result": [ + "hx5dd0b2a161bc16194d38b050744c7cd623626661", + "hxd980b07d43d1df399392f8871d6ec7c975f3e832" + ], + "id": 1 +} +``` + +#### getTransactionCount + +Returns the total number of transactions which is submitted in the wallet. + +```python +@external(readonly=True) +def getTransactionCount(self, _pending: bool=True, _executed: bool=True) -> int: +``` +**Example** + +```json +{ + "jsonrpc": "2.0", + "method": "icx_call", + "id": 1, + "params": { + "from": "hx5dd0b2a161bc16194d38b050744c7cd623626661", + "to": "cx30d7fcf580135d9f9eb491292555a5b29d9314cb", + "dataType": "call", + "data": { + "method": "getTransactionCount", + "params": { + "_pending": "0x00", + "_executed": "0x01" + } + } + } +} +``` + +**Call result** + +```json +{ + "jsonrpc": "2.0", + "result": "0x1", + "id": 1 +} +``` + +#### getTransactionList + +Returns a list of transactions. + +```python +@external(readonly=True) +def getTransactionList(self, _offset: int, _count: int, _pending: bool=True, _executed: bool=True) -> list: +``` + +**Example** + +```json +{ + "jsonrpc": "2.0", + "method": "icx_call", + "id": 1, + "params": { + "from": "hx5dd0b2a161bc16194d38b050744c7cd623626661", + "to": "cx30d7fcf580135d9f9eb491292555a5b29d9314cb", + "dataType": "call", + "data": { + "method": "getTransactionList", + "params": { + "_offset": "0x00", + "_count": "0x0A", + "_pending": "0x01", + "_executed": "0x01" + } + } + } +} +``` + +**Call result** + +```json +{ + "jsonrpc": "2.0", + "result": [ + { + "_executed": "0x1", + "_destination": "cx30d7fcf580135d9f9eb491292555a5b29d9314cb", + "_value": "0x0", + "_method": "addWalletOwner", + "_params": "[{\"name\":\"_walletOwner\",\"type\":\"Address\",\"value\":\"hx1262526a4da004550021b5f9d249b9c7d98b5892\"}]", + "_description": "add owner4 in wallet", + "_transaction_id": "0x0" + }, + { + "_executed": "0x0", + "_destination": "cx30d7fcf580135d9f9eb491292555a5b29d9314cb", + "_value": "0x0", + "_method": "addWalletOwner", + "_params": "[{\"name\":\"_walletOwner\",\"type\":\"Address\",\"value\":\"hxbedeeadea922dc7f196e22eaa763fb01aab0b64c\"}]", + "_description": "add owner5 in wallet", + "_transaction_id": "0x1" + } + ], + "id": 1 +} +``` + +### Methods + +Below is a list of the methods that the wallet owners can call. + +#### submitTransaction + +Submits a transaction which is to be executed when the number of confirmations meets the 'requirement' value. Only wallet owners can call this method. The wallet owner who has called this method is confirmed as soon as the transaction is submitted successfully. + +```python +@external +def submitTransaction(self, _destination: Address, _method: str="", _params: str="", _value: int=0, _description: str=""): +``` + +`_destination` is the SCORE address where `_method` is defined. + +`_description` is a supplementary explanation of the transaction. (optional parameter) + +`_value` is amount of ICX coin in loop (1 ICX == 1 ^ 18 loop). This parameter is used when transferring ICX coin or calling 'payable' method. (optional parameter) + +`_method` is the name of the method that is to be executed when the number of confirmations meets the 'requirement' value. In the case of transferring ICX coin, do not have to specify this parameter. (optional parameter) + +`_params` is a stringified JSON data. This data is used as the arguments of the `_method` when it is executed. Below is the format. **name** is parameter's name, **type** is parameter's type (supported types are `int`, `str`, `bool`, `Address` and `bytes`), **value** is the actual data. In the case of transferring ICX coin, do not have to specify this parameter. (optional parameter) + + +![parameter_list_in_json_format](multi-signature-wallet-1.png) + +Below is an example of a `addWalletOwner` method call. After writing the request in the JSON format, you have to stringify it. +```json +[ + {"name": "_walletOwner", "type": "Address", "value": "hx1262526a4da004550021b5f9d249b9c7d98b5892"}, +] +``` +**Example** + +```json +{ + "jsonrpc": "2.0", + "method": "icx_sendTransaction", + "params": { + "version": "0x3", + "from": "hx5dd0b2a161bc16194d38b050744c7cd623626661", + "value": "0x0", + "stepLimit": "0x3000000", + "nid": "0x3", + "nonce": "0x1", + "to": "cx30d7fcf580135d9f9eb491292555a5b29d9314cb", + "dataType": "call", + "data": { + "method": "submitTransaction", + "params": { + "_destination": "cx30d7fcf580135d9f9eb491292555a5b29d9314cb", + "_method": "addWalletOwner", + "_params": "[{\"name\":\"_walletOwner\",\"type\":\"Address\",\"value\":\"hx1262526a4da004550021b5f9d249b9c7d98b5892\"}]", + "_description": "add owner4 in wallet" + } + } + }, + "id": 1 +} +``` + +**Sendtx result** + +```json +{ + "jsonrpc": "2.0", + "result": { + "txHash": "0xca72bef2d0f3e77a6621dc20bf9f47d34e87f30b4f4717be9edfa2e2f15d24fa", + "blockHeight": "0x4", + "blockHash": "0xfc3ec6b4777b108f2c7fad4ee703a7f712d68b654a9ccb042ffc8614795f09a8", + "txIndex": "0x0", + "to": "cx30d7fcf580135d9f9eb491292555a5b29d9314cb", + "stepUsed": "0x10bad0", + "stepPrice": "0x0", + "cumulativeStepUsed": "0x10bad0", + "eventLogs": [ + { + "scoreAddress": "cx30d7fcf580135d9f9eb491292555a5b29d9314cb", + "indexed": [ + "Submission(int)", + "0x0" + ], + "data": [] + }, + { + "scoreAddress": "cx30d7fcf580135d9f9eb491292555a5b29d9314cb", + "indexed": [ + "Confirmation(Address,int)", + "hx5dd0b2a161bc16194d38b050744c7cd623626661", + "0x0" + ], + "data": [] + } + ], + "logsBloom": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000060000000000011000000000000000000000000000000000000000000000000000a0000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002800000000004000000000000000000000000000000000000000000000000000100000000003200000000000000000000000000000000000000000000000000000000000000000000000000000", + "status": "0x1" + }, + "id": 1 +} +``` + +#### confirmTransaction + +Confirms a transaction corresponding to the `_transactionId`. As soon as a transaction confirmation count meets the 'requirement' value (should not exceed), the transaction is executed. Only wallet owners can call this method. + +```python +@external +def confirmTransaction(self, _transactionId: int): +``` + +**Example** + +```json +{ + "jsonrpc": "2.0", + "method": "icx_sendTransaction", + "params": { + "version": "0x3", + "from": "hxd980b07d43d1df399392f8871d6ec7c975f3e832", + "value": "0x0", + "stepLimit": "0x30000000", + "nid": "0x3", + "nonce": "0x1", + "to": "cx30d7fcf580135d9f9eb491292555a5b29d9314cb", + "dataType": "call", + "data": { + "method": "confirmTransaction", + "params": { + "_transactionId": "0x00" + } + } + }, + "id": 1 +} +``` + +**Sendtx result** + +```json +{ + "jsonrpc": "2.0", + "result": { + "txHash": "0x07f407914ae8a37183d588d75e9a9cfead3ce2ebc29af4c809e0cff493e7baaa", + "blockHeight": "0x5", + "blockHash": "0x4d28315fd2de5095ef6a8da3f39644be126ae29fef7ee89be52837acf50c4be6", + "txIndex": "0x0", + "to": "cx30d7fcf580135d9f9eb491292555a5b29d9314cb", + "stepUsed": "0x1026c4", + "stepPrice": "0x0", + "cumulativeStepUsed": "0x1026c4", + "eventLogs": [ + { + "scoreAddress": "cx30d7fcf580135d9f9eb491292555a5b29d9314cb", + "indexed": [ + "Confirmation(Address,int)", + "hxd980b07d43d1df399392f8871d6ec7c975f3e832", + "0x0" + ], + "data": [] + }, + { + "scoreAddress": "cx30d7fcf580135d9f9eb491292555a5b29d9314cb", + "indexed": [ + "WalletOwnerAddition(Address)", + "hx1262526a4da004550021b5f9d249b9c7d98b5892" + ], + "data": [] + }, + { + "scoreAddress": "cx30d7fcf580135d9f9eb491292555a5b29d9314cb", + "indexed": [ + "Execution(int)", + "0x0" + ], + "data": [] + } + ], + "logsBloom": "0x00000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000011000000000000000000000000000000000000000000000000000080000000000820000000000000000000000000000000000000000000000000002000000000221000000000000000000000000000000000000000000000000002800000000004000000000000000000000000000000000000000000000000000100000000002000000000000000000000000000000000000000000000000000040000000000020000000000000", + "status": "0x1" + }, + "id": 1 +} +``` + +#### revokeTransaction + +Revokes confirmation of a transaction corresponding to the `_transactionId`. Only already confirmed wallet owners can revoke their own confirmation of a transaction. Wallet owners can't revoke others' confirmation. This method is only valid for pending transaction. + +```python +@external +def revokeTransaction(self, _transactionId: int): +``` + +**Example** + +```json +{ + "jsonrpc": "2.0", + "method": "icx_sendTransaction", + "params": { + "version": "0x3", + "from": "hxef73db5d0ad02eb1fadb37d0041be96bfa56d4e6", + "value": "0x0", + "stepLimit": "0x3000000", + "timestamp": "0x573117f1d6568", + "nid": "0x3", + "nonce": "0x1", + "to": "cx4d5a79f329adcf00a3daa99539f0eeea2d43d239", + "dataType": "call", + "data": { + "method": "revokeTransaction", + "params": { + "_transactionId": "0x01" + } + } + }, + "id": 1 +} +``` + +**Sendtx result** + +```json +{ + "jsonrpc": "2.0", + "result": { + "txHash": "0x70a5c03cd41d205b5b93abf57e53160d3f58679b1f3c254a10db9d220ecfac21", + "blockHeight": "0x7", + "blockHash": "0x92558d91cc52f0ff75686d8b5691bf5cdcbd586eff625b9c93750b510d324887", + "txIndex": "0x0", + "to": "cx30d7fcf580135d9f9eb491292555a5b29d9314cb", + "stepUsed": "0xf9c4a", + "stepPrice": "0x0", + "cumulativeStepUsed": "0xf9c4a", + "eventLogs": [ + { + "scoreAddress": "cx30d7fcf580135d9f9eb491292555a5b29d9314cb", + "indexed": [ + "Revocation(Address,int)", + "hx5dd0b2a161bc16194d38b050744c7cd623626661", + "0x1" + ], + "data": [] + } + ], + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000400000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000020000000001300000000000000000000000000000000000000000000000000000000000000001000000000000", + "status": "0x1" + }, + "id": 1 +} +``` + +### Methods (only callable by wallet) + +These methods only can be called by the multisig wallet SCORE itself. **In short, you can not call those methods directly (or it will fail)**. If you want to execute these methods, call `submitTransaction` with the method's information as a parameter. + +#### addWalletOwner + +Adds a new wallet owner. + +```python +@external +def addWalletOwner(self, _walletOwner: Address): +``` +#### replaceWalletOwner + +Replaces an existing wallet owner by a new wallet owner. + +```python +@external +def replaceWalletOwner(self, _walletOwner: Address, _newWalletOwner: Address): +``` +#### removeWalletOwner + +Removes an existing wallet owner. + +```python +@external +def removeWalletOwner(self, _walletOwner: Address): +``` +#### changeRequirement + +Changes the requirement value. `_required` can't exceed the number of wallet owners. + +```python +@external +def changeRequirement(self, _required: int): +``` + +### Eventlogs + +#### Confirmation + +Must trigger on any successful confirmation. + +```python +@eventlog(indexed=2) +def Confirmation(self, _sender: Address, _transactionId: int): + pass +``` +#### Revocation + +Must trigger on any revoked confirmation. + +```python +@eventlog(indexed=2) +def Revocation(self, _sender: Address, _transactionId: int): + pass +``` +#### Submission + +Must trigger on any submitted transaction. + +```python +@eventlog(indexed=1) +def Submission(self, _transactionId: int): + pass +``` +#### Execution + +Must trigger on the transaction being executed successfully. + +```python +@eventlog(indexed=1) +def Execution(self, _transactionId: int): + pass +``` +#### ExecutionFailure + +Must trigger on a failure during the transaction execution. + +```python +@eventlog(indexed=1) +def ExecutionFailure(self, _transactionId: int): + pass +``` +#### Deposit + +Must trigger on a ICX deposit event to a MultiSig Wallet SCORE. + +```python +@eventlog(indexed=1) +def Deposit(self, _sender: Address, _value: int): + pass +``` +#### DepositToken + +Must trigger on a token deposit event to a MultiSig Wallet SCORE. + +```python +@eventlog(indexed=1) +def DepositToken(self, _sender: Address, _value: int, _data: bytes): + pass +``` +#### WalletOwnerAddition + +Must trigger on adding a new wallet owner. + +```python +@eventlog(indexed=1) +def WalletOwnerAddition(self, _walletOwner: Address): + pass +``` +#### WalletOwnerRemoval + +Must trigger on removing a wallet owner. + +```python +@eventlog(indexed=1) +def WalletOwnerRemoval(self, _walletOwner: Address): + pass +``` +#### Requirement + +Must trigger on changing the requirement value. + +```python +@eventlog +def RequirementChange(self, _required: int): + pass +``` + +## Summary + +So far, we have learned the basic understanding of multi signature wallet and how to use it. Multi signature wallet can be used in various ways such as exchange, managing business funds and managing simple membership fee among users. You can use it according to your use. + +## Tips or FAQs + +## References +- [https://github.com/gnosis/MultiSigWallet](https://github.com/gnosis/MultiSigWallet) diff --git a/sample-scores/token-crowdsale.md b/sample-scores/token-crowdsale.md new file mode 100644 index 0000000..eb5da19 --- /dev/null +++ b/sample-scores/token-crowdsale.md @@ -0,0 +1,369 @@ +--- +title: "Token & Crowdsale" +--- + +This document will explain how to write SCOREs with T-Bears framework. +Let’s start by creating a simple token contract. You can create an empty project using init command. Suppose your project name is ‘sample_token’ and the main class name is ‘SampleToken’. + +```console +$ tbears init sample_token SampleToken +``` + +Above command will create a project folder, sample_token, and generate __init__.py, sample_token.py, and package.json files in the folder. sample_token.py has the main class declaration whose name is SampleToken. You need to implement SampleToken class. + +IRC-2 standard defines the common behavior of tokens running on ICON. IRC-2 compliant token must implement following methods. The specification is here, [IRC-2](https://github.com/icon-project/IIPs/blob/master/IIPS/iip-2.md). + +```python +@external(readonly=True) +def name(self) -> str: + +@external(readonly=True) +def symbol(self) -> str: + +@external(readonly=True) +def decimals(self) -> int: + +@external(readonly=True) +def totalSupply(self) -> int: + +@external(readonly=True) +def balanceOf(self, _owner: Address) -> int: + +@external +def transfer(self, _to: Address, _value: int, _data: bytes=None): +``` + +Below is a complete token implementation. You can copy and paste it to fill your sample_token.py. Note that TokenFallbackInterface is declared in the beginning to interact with SampleCrowdsale contract defined later. (In fact, tbears samples command will generate the two sample projects, standard_token and standard_crowdsale, with the complete source code provided. However, we used init command here to illustrate how to create a new project.) + +When you deploy the contract, on_install method is called. You can pass the amount of initial tokens to the parameter initialSupply, and, in this example, 100% of initial tokens go to the contract owner. + +```python +from iconservice import * + +TAG = 'SampleToken' + + +# An interface of ICON Token Standard, IRC-2 +class TokenStandard(ABC): + @abstractmethod + def name(self) -> str: + pass + + @abstractmethod + def symbol(self) -> str: + pass + + @abstractmethod + def decimals(self) -> int: + pass + + @abstractmethod + def totalSupply(self) -> int: + pass + + @abstractmethod + def balanceOf(self, _owner: Address) -> int: + pass + + @abstractmethod + def transfer(self, _to: Address, _value: int, _data: bytes = None): + pass + +# An interface of tokenFallback. +# Receiving SCORE that has implemented this interface can handle +# the receiving or further routine. +class TokenFallbackInterface(InterfaceScore): + @interface + def tokenFallback(self, _from: Address, _value: int, _data: bytes): + pass + + +class SampleToken(IconScoreBase, TokenStandard): + + _BALANCES = 'balances' + _TOTAL_SUPPLY = 'total_supply' + _DECIMALS = 'decimals' + + @eventlog(indexed=3) + def Transfer(self, _from: Address, _to: Address, _value: int, _data: bytes): + pass + + def __init__(self, db: IconScoreDatabase) -> None: + super().__init__(db) + self._total_supply = VarDB(self._TOTAL_SUPPLY, db, value_type=int) + self._decimals = VarDB(self._DECIMALS, db, value_type=int) + self._balances = DictDB(self._BALANCES, db, value_type=int) + + def on_install(self, _initialSupply: int, _decimals: int) -> None: + super().on_install() + + if _initialSupply < 0: + revert("Initial supply cannot be less than zero") + + if _decimals < 0: + revert("Decimals cannot be less than zero") + + total_supply = _initialSupply * 10 ** _decimals + Logger.debug(f'on_install: total_supply={total_supply}', TAG) + + self._total_supply.set(total_supply) + self._decimals.set(_decimals) + self._balances[self.msg.sender] = total_supply + + def on_update(self) -> None: + super().on_update() + + @external(readonly=True) + def name(self) -> str: + return "SampleToken" + + @external(readonly=True) + def symbol(self) -> str: + return "ST" + + @external(readonly=True) + def decimals(self) -> int: + return self._decimals.get() + + @external(readonly=True) + def totalSupply(self) -> int: + return self._total_supply.get() + + @external(readonly=True) + def balanceOf(self, _owner: Address) -> int: + return self._balances[_owner] + + @external + def transfer(self, _to: Address, _value: int, _data: bytes = None): + if _data is None: + _data = b'None' + self._transfer(self.msg.sender, _to, _value, _data) + + def _transfer(self, _from: Address, _to: Address, _value: int, _data: bytes): + + # Checks the sending value and balance. + if _value < 0: + revert("Transferring value cannot be less than zero") + if self._balances[_from] < _value: + revert("Out of balance") + + self._balances[_from] = self._balances[_from] - _value + self._balances[_to] = self._balances[_to] + _value + + if _to.is_contract: + # If the recipient is SCORE, + # then calls `tokenFallback` to hand over control. + recipient_score = self.create_interface_score(_to, TokenFallbackInterface) + recipient_score.tokenFallback(_from, _value, _data) + + # Emits an event log `Transfer` + self.Transfer(_from, _to, _value, _data) + Logger.debug(f'Transfer({_from}, {_to}, {_value}, {_data})', TAG) +``` + +Now, we are going to write a crowdsale contract using above token. Let’s create a new project for the crowdsale contract. + +```console +$ tbears init sample_crowdsale SampleCrowdsale +``` +Our crowdsale contract will do the following. + +- Exchange ratio to ICX is 1:1. Crowdsale target, token contract address, and its duration are set when the contract is first deployed. +- total_joiner_count function returns the number of contributors, and check_goal_reached function tests if the crowdsale target has been met. +- After the crowdsale finished, safe_withdrawal function transfers the fund to the beneficiary, contract owner in this example, if the sales target has been met. If sales target failed, each contributors can withdraw their contributions back. + +Again, complete source is given below. Note that crowdsale duration is given in number of blocks, because SCORE logic must be deterministic across nodes, thus it must not rely on clock time. + +```python +from iconservice import * + +TAG = 'SampleCrowdsale' + + +# An interface of token to give a reward to anyone who contributes +class TokenInterface(InterfaceScore): + @interface + def transfer(self, _to: Address, _value: int, _data: bytes=None): + pass + + +class SampleCrowdsale(IconScoreBase): + + _ADDR_BENEFICIARY = 'addr_beneficiary' + _ADDR_TOKEN_SCORE = 'addr_token_score' + _FUNDING_GOAL = 'funding_goal' + _AMOUNT_RAISED = 'amount_raised' + _DEAD_LINE = 'dead_line' + _PRICE = 'price' + _BALANCES = 'balances' + _JOINER_LIST = 'joiner_list' + _FUNDING_GOAL_REACHED = 'funding_goal_reached' + _CROWDSALE_CLOSED = 'crowdsale_closed' + + @eventlog(indexed=3) + def FundTransfer(self, backer: Address, amount: int, is_contribution: bool): + pass + + @eventlog(indexed=2) + def GoalReached(self, recipient: Address, total_amount_raised: int): + pass + + def __init__(self, db: IconScoreDatabase) -> None: + super().__init__(db) + + self._addr_beneficiary = VarDB(self._ADDR_BENEFICIARY, db, value_type=Address) + self._addr_token_score = VarDB(self._ADDR_TOKEN_SCORE, db, value_type=Address) + self._funding_goal = VarDB(self._FUNDING_GOAL, db, value_type=int) + self._amount_raised = VarDB(self._AMOUNT_RAISED, db, value_type=int) + self._dead_line = VarDB(self._DEAD_LINE, db, value_type=int) + self._price = VarDB(self._PRICE, db, value_type=int) + self._balances = DictDB(self._BALANCES, db, value_type=int) + self._joiner_list = ArrayDB(self._JOINER_LIST, db, value_type=Address) + self._funding_goal_reached = VarDB(self._FUNDING_GOAL_REACHED, db, value_type=bool) + self._crowdsale_closed = VarDB(self._CROWDSALE_CLOSED, db, value_type=bool) + + def on_install(self, _fundingGoalInIcx: int, _tokenScore: Address, _durationInBlocks: int) -> None: + """ + Called when this SCORE first deployed. + + :param _fundingGoalInIcx: The funding goal of this crowdsale, in ICX + :param _tokenScore: SCORE address of token that will be used for the rewards + :param _durationInBlocks: the sale duration is given in number of blocks + """ + super().on_install() + + Logger.debug(f'on_install: fundingGoalInIcx={_fundingGoalInIcx}', TAG) + Logger.debug(f'on_install: tokenScore={_tokenScore}', TAG) + Logger.debug(f'on_install: durationInBlocks={_durationInBlocks}', TAG) + + if _fundingGoalInIcx < 0: + revert("Funding goal cannot be less than zero") + + if _durationInBlocks < 0: + revert("Duration cannot be less than zero") + + # The exchange ratio to ICX is 1:1 + icx_cost_of_each_token = 1 + + self._addr_beneficiary.set(self.msg.sender) + self._addr_token_score.set(_tokenScore) + self._funding_goal.set(_fundingGoalInIcx) + self._dead_line.set(self.block.height + _durationInBlocks) + price = int(icx_cost_of_each_token) + self._price.set(price) + + self._funding_goal_reached.set(False) + self._crowdsale_closed.set(True) # Crowdsale closed by default + + def on_update(self) -> None: + super().on_update() + + @external + def tokenFallback(self, _from: Address, _value: int, _data: bytes): + """ + Implements `tokenFallback` in order for the SCORE + to receive initial tokens to reward to the contributors + """ + + # Checks if the caller is a Token SCORE address that this SCORE is interested in. + if self.msg.sender != self._addr_token_score.get(): + revert("Unknown token address") + + # Depositing tokens can only be done by owner + if _from != self.owner: + revert("Invalid sender") + + if _value < 0: + revert("Depositing value cannot be less than zero") + + # start Crowdsale hereafter + self._crowdsale_closed.set(False) + Logger.debug(f'tokenFallback: token supply = "{_value}"', TAG) + + @payable + def fallback(self): + """ + Called when anyone sends funds to the SCORE. + This SCORE regards it as a contribution. + """ + if self._crowdsale_closed.get(): + revert('Crowdsale is closed.') + + # Accepts the contribution + amount = self.msg.value + self._balances[self.msg.sender] = self._balances[self.msg.sender] + amount + self._amount_raised.set(self._amount_raised.get() + amount) + value = int(amount / self._price.get()) + data = b'called from Crowdsale' + + # Gives tokens to the contributor as a reward + token_score = self.create_interface_score(self._addr_token_score.get(), TokenInterface) + token_score.transfer(self.msg.sender, value, data) + + if self.msg.sender not in self._joiner_list: + self._joiner_list.put(self.msg.sender) + + self.FundTransfer(self.msg.sender, amount, True) + Logger.debug(f'FundTransfer({self.msg.sender}, {amount}, True)', TAG) + + @external(readonly=True) + def totalJoinerCount(self) -> int: + """ + Returns the number of contributors. + + :return: the number of contributors + """ + return len(self._joiner_list) + + def _after_dead_line(self) -> bool: + # Checks if it has been reached to the deadline block + Logger.debug(f'after_dead_line: block.height = {self.block.height}', TAG) + Logger.debug(f'after_dead_line: dead_line() = {self._dead_line.get()}', TAG) + return self.block.height >= self._dead_line.get() + + @external + def checkGoalReached(self): + """ + Checks if the goal has been reached and ends the campaign. + """ + if self._after_dead_line(): + if self._amount_raised.get() >= self._funding_goal.get(): + self._funding_goal_reached.set(True) + self.GoalReached(self._addr_beneficiary.get(), self._amount_raised.get()) + Logger.debug(f'Goal reached!', TAG) + self._crowdsale_closed.set(True) + + @external + def safeWithdrawal(self): + """ + Withdraws the funds. + + If the funding goal has been reached, sends the entire amount to the beneficiary. + If the goal was not reached, each contributor can withdraw the amount they contributed. + """ + if self._after_dead_line(): + # each contributor can withdraw the amount they contributed if the goal was not reached + if not self._funding_goal_reached.get(): + amount = self._balances[self.msg.sender] + self._balances[self.msg.sender] = 0 + if amount > 0: + if self.icx.send(self.msg.sender, amount): + self.FundTransfer(self.msg.sender, amount, False) + Logger.debug(f'FundTransfer({self.msg.sender}, {amount}, False)', TAG) + else: + self._balances[self.msg.sender] = amount + + # The sales target has been met. Owner can withdraw the contribution. + if self._funding_goal_reached.get() and self._addr_beneficiary.get() == self.msg.sender: + if self.icx.send(self._addr_beneficiary.get(), self._amount_raised.get()): + self.FundTransfer(self._addr_beneficiary.get(), self._amount_raised.get(), False) + Logger.debug(f'FundTransfer({self._addr_beneficiary.get()},' + f'{self._amount_raised.get()}, False)', TAG) + else: + # if the transfer to beneficiary fails, unlock contributors balance + Logger.debug(f'Failed to send to beneficiary!', TAG) + self._funding_goal_reached.set(False) +``` + + +