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'.
$ 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 the following methods. The specification is here, IRC-2.
@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:​@externaldef 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.
When you deploy the contract, on_install method is called. You can pass the number of initial tokens to the parameter initialSupply, and, in this example, 100% of initial tokens go to the contract owner.
from iconservice import *​TAG = 'SampleToken'​​# An interface of ICON Token Standard, IRC-2class TokenStandard(ABC):@abstractmethoddef name(self) -> str:pass​@abstractmethoddef symbol(self) -> str:pass​@abstractmethoddef decimals(self) -> int:pass​@abstractmethoddef totalSupply(self) -> int:pass​@abstractmethoddef balanceOf(self, _owner: Address) -> int:pass​@abstractmethoddef 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):@interfacedef 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 ** _decimalsLogger.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]​@externaldef 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] - _valueself._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 the above token. Let's create a new project for the crowdsale contract.
$ 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 contributor can withdraw their contributions back.
Again, the 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.
from iconservice import *​TAG = 'SampleCrowdsale'​​# An interface of token to give a reward to anyone who contributesclass TokenInterface(InterfaceScore):@interfacedef 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:1icx_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()​@externaldef tokenFallback(self, _from: Address, _value: int, _data: bytes):"""Implements `tokenFallback` in order for the SCOREto 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 ownerif _from != self.owner:revert("Invalid sender")​if _value < 0:revert("Depositing value cannot be less than zero")​# start Crowdsale hereafterself._crowdsale_closed.set(False)Logger.debug(f'tokenFallback: token supply = "{_value}"', TAG)​@payabledef 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 contributionamount = self.msg.valueself._balances[self.msg.sender] = self._balances[self.msg.sender] + amountself._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 rewardtoken_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 blockLogger.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()​@externaldef 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)​@externaldef 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 reachedif not self._funding_goal_reached.get():amount = self._balances[self.msg.sender]self._balances[self.msg.sender] = 0if 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 balanceLogger.debug(f'Failed to send to beneficiary!', TAG)self._funding_goal_reached.set(False)