def do_approve(self, storage: StorageAPI, t_owner, t_spender, amount): if not CheckWitness(t_owner): print("Incorrect permission") return False if amount < 0: print("Negative amount") return False from_balance = storage.get(t_owner) # cannot approve an amount that is # currently greater than the from balance if from_balance >= amount: approval_key = concat(t_owner, t_spender) if len(approval_key) != 40: return False if amount == 0: storage.delete(approval_key) else: storage.put(approval_key, amount) OnApprove(t_owner, t_spender, amount) return True return False
def add_to_circulation(self, amount: int, storage: StorageAPI): """ Adds an amount of token to circulation :param amount: int the amount to add to circulation :param storage: StorageAPI A StorageAPI object for storage interaction """ current_supply = storage.get(self.in_circulation_key) current_supply += amount storage.put(self.in_circulation_key, current_supply)
def start_public_sale(self, token: Token): storage = StorageAPI() owner = storage.get(token.owner_key) if not CheckWitness(owner): return False pub_sale_start_block = storage.get(self.pub_sale_start_block_key) if pub_sale_start_block: print("can't start the public sale twice") return False height = GetHeight() storage.put(self.pub_sale_start_block_key, height) return True
def pause_sale(token: Token): """ Pause the sale :param token: Token The token of the sale to pause :return: bool: Whether the operation was successful """ storage = StorageAPI() owner = storage.get(token.owner_key) if not CheckWitness(owner): print("Must be owner to pause sale") return False # mark the sale as paused storage.put(token.sale_paused_key, True) return True
def kyc_register(self, args, token: Token): """ :param args:list a list of addresses to register :param token: Token A token object with your ICO settings :return: int: The number of addresses registered for KYC """ ok_count = 0 storage = StorageAPI() owner = storage.get(token.owner_key) if CheckWitness(owner): for addresses in args: # bl: allowing multiple addresses to be encoded into a single parameter. this works around # the limitation of list arguments only supporting at most 16 elements # can be passed in as follows: # testinvoke {script_hash} crowdsale_register [bytearray(b'\x015\x829\x8cm6f\xb3\xac\xcc\xcas\x1dw\x06\xbc\xd2\x9co#\xba\'\x03\xc52c\xe8\xd6\xe5"\xdc2\x2039\xdc\xd8\xee\xe9')] # note that neo-python doesn't like spaces in the strings, so convert any spaces to the hex equivalent: '\x20' addr_length = len(addresses) # addresses are 20 bytes, so the length must be a multiple of 20 or else it's invalid! if (addr_length % 20) != 0: continue addr_count = addr_length / 20 i = 0 while i < addr_count: start = i * 20 address = substr(addresses, start, 20) kyc_storage_key = concat(self.kyc_key, address) storage.put(kyc_storage_key, True) OnKYCRegister(address) ok_count += 1 i += 1 return ok_count
def exchange(self, token: Token): """ Make a token sale contribution to exchange NEO for NRVE :param token: Token The token object with NEP5/sale settings :return: bool: Whether the exchange was successful """ attachments = get_asset_attachments() # type: Attachments storage = StorageAPI() # this looks up whether the exchange can proceed tokens = self.check_and_calculate_tokens(token, attachments, storage, False) if tokens <= 0: print("Cannot exchange value") # This should only happen in the case that there are a lot of TX on the final # block before the total amount is reached. An amount of TX will get through # the verification phase because the total amount cannot be updated during that phase # because of this, there should be a process in place to manually refund tokens if attachments.neo_attached > 0: OnRefund(attachments.sender_addr, attachments.neo_attached) return False self.mint_tokens(token, attachments.receiver_addr, attachments.sender_addr, tokens, storage) # update the total sold during the public sale public_sale_sold = storage.get(token.public_sale_sold_key) public_sale_sold += tokens storage.put(token.public_sale_sold_key, public_sale_sold) # track contributions as a separate event for token sale account page transaction updates OnContribution(attachments.sender_addr, attachments.neo_attached, tokens) return True
def deploy(token: Token): """ :param token: Token The token to deploy :return: bool: Whether the operation was successful """ if not CheckWitness(token.original_owner): print("Must be original_owner to deploy") return False storage = StorageAPI() # can only deploy once, so if we already have an owner, no-op if storage.get(token.owner_key): return False # mark the current owner, which can be changed later storage.put(token.owner_key, token.original_owner) return True
def do_transfer(self, storage: StorageAPI, t_from, t_to, amount): if amount <= 0: return False if len(t_to) != 20: return False if CheckWitness(t_from): if t_from == t_to: print("transfer to self!") return True from_val = storage.get(t_from) if from_val < amount: print("insufficient funds") return False if from_val == amount: storage.delete(t_from) else: difference = from_val - amount storage.put(t_from, difference) to_value = storage.get(t_to) to_total = to_value + amount storage.put(t_to, to_total) OnTransfer(t_from, t_to, amount) return True else: print("from address is not the tx sender") return False
def mint_tokens(self, token: Token, from_address, to_address, tokens, storage: StorageAPI): """ Mint tokens for an address :param token: the token being minted :param from_address: the address from which the tokens are being minted (should always be the contract address) :param to_address: the address to transfer the minted tokens to :param tokens: the number of tokens to mint :param storage: StorageAPI """ # lookup the current balance of the address current_balance = storage.get(to_address) # add it to the exchanged tokens and persist in storage new_total = tokens + current_balance storage.put(to_address, new_total) # update the in circulation amount token.add_to_circulation(tokens, storage) # dispatch transfer event OnTransfer(from_address, to_address, tokens)
def change_owner(token: Token, new_owner): """ Record a transfer request to a new owner. The new owner must accept the request via accept_owner :param token: Token The token to change the owner for :param new_owner: the new owner of the contract :return: bool: Whether the operation was successful """ storage = StorageAPI() owner = storage.get(token.owner_key) if not owner: print("Must deploy before changing owner") return False if not CheckWitness(owner): print("Must be owner to change owner") return False # setup the new owner. will need to be accepted by the new owner in order to finalize storage.put(token.new_owner_key, new_owner) return True
def accept_owner(token: Token): """ Change the owner of this smart contract who will be able to perform protected operations :param token: Token The token to change the owner for :return: bool: Whether the operation was successful """ storage = StorageAPI() new_owner = storage.get(token.new_owner_key) if not new_owner: print("Must call change_owner before accept_owner") return False if not CheckWitness(new_owner): print("Must be new_owner to accept owner") return False # setup the new owner. storage.put(token.owner_key, new_owner) # now that it's accepted, make the change official by removing the pending new_owner storage.delete(token.new_owner_key) return True
def do_transfer_from(self, storage: StorageAPI, t_from, t_to, amount): if amount <= 0: return False available_key = concat(t_from, t_to) if len(available_key) != 40: return False available_to_to_addr = storage.get(available_key) if available_to_to_addr < amount: print("Insufficient funds approved") return False from_balance = storage.get(t_from) if from_balance < amount: print("Insufficient tokens in from balance") return False to_balance = storage.get(t_to) new_from_balance = from_balance - amount new_to_balance = to_balance + amount storage.put(t_to, new_to_balance) storage.put(t_from, new_from_balance) print("transfer complete") new_allowance = available_to_to_addr - amount if new_allowance == 0: print("removing all balance") storage.delete(available_key) else: print("updating allowance to new allowance") storage.put(available_key, new_allowance) OnTransfer(t_from, t_to, amount) return True
def mint_rewards_tokens(self, token: Token, args): """ Mint tokens for the rewards pool :param token: the token being minted for the rewards pool :param args: the address and number of tokens to mint :return: True if successful """ storage = StorageAPI() owner = storage.get(token.owner_key) if not CheckWitness(owner): return False if len(args) != 2: return False address = args[0] tokens = args[1] if len(address) != 20: return False if tokens <= 0: return False now = get_now() # no minting rewards tokens until after the token sale ends if now < self.sale_end: print("can't mint_rewards_tokens before sale ends") return False rewards_fund_tokens_distributed = storage.get( self.rewards_fund_token_distribution_key) rewards_fund_tokens_distributed += tokens # don't allow more than the max tokens to be distributed if rewards_fund_tokens_distributed > self.rewards_fund_tokens_max: print("can't exceed mint_rewards_tokens limit") return False storage.put(self.rewards_fund_token_distribution_key, rewards_fund_tokens_distributed) attachments = get_asset_attachments() # type: Attachments #self.mint_tokens(token, attachments.receiver_addr, address, tokens, storage) from_address = attachments.receiver_addr to_address = address # bl: the following is an exact copy of the mint_tokens function. invoking self.mint_tokens will break the # execution of this method due to a neo-boa compiler issue. this results in a lot of code duplication, # but it's preferable to the alternative of a broken smart contract. refer: https://github.com/CityOfZion/neo-boa/issues/40 # lookup the current balance of the address current_balance = storage.get(to_address) # add it to the exchanged tokens and persist in storage new_total = tokens + current_balance storage.put(to_address, new_total) # update the in circulation amount token.add_to_circulation(tokens, storage) # dispatch transfer event OnTransfer(from_address, to_address, tokens) return True
def transfer_company_tokens(self, token: Token, args): """ Transfer company tokens to a wallet address according to the 2-year company token vesting schedule :param token: the token being minted for the company :param args: the address and number of tokens to mint :return: True if successful """ storage = StorageAPI() owner = storage.get(token.owner_key) if not CheckWitness(owner): return False if len(args) != 2: return False address = args[0] tokens = args[1] if len(address) != 20: return False if tokens <= 0: return False now = get_now() seconds_in_year = 31536000 # no company token distribution until after the ICO ends if now < self.sale_end: print("can't transfer_company_tokens before sale ends") return False # in the first year, allow 50% token distribution if now < (self.sale_end + seconds_in_year): max_token_distribution = self.company_tokens_max * 5 / 10 # in the second year, allow 75% total token distribution elif now < (self.sale_end + (2 * seconds_in_year)): max_token_distribution = self.company_tokens_max * 75 / 100 # beyond the second year, allow 100% total token distribution else: max_token_distribution = self.company_tokens_max company_tokens_distributed = storage.get( self.company_token_distribution_key) company_tokens_distributed += tokens # don't allow more than the max tokens to be distributed if company_tokens_distributed > max_token_distribution: print("can't exceed transfer_company_tokens vesting limit") return False storage.put(self.company_token_distribution_key, company_tokens_distributed) attachments = get_asset_attachments() # type: Attachments #self.mint_tokens(token, attachments.receiver_addr, address, tokens, storage) from_address = attachments.receiver_addr to_address = address # bl: the following is an exact copy of the mint_tokens function. invoking self.mint_tokens will break the # execution of this method due to a neo-boa compiler issue. this results in a lot of code duplication, # but it's preferable to the alternative of a broken smart contract. refer: https://github.com/CityOfZion/neo-boa/issues/40 # lookup the current balance of the address current_balance = storage.get(to_address) # add it to the exchanged tokens and persist in storage new_total = tokens + current_balance storage.put(to_address, new_total) # update the in circulation amount token.add_to_circulation(tokens, storage) # dispatch transfer event OnTransfer(from_address, to_address, tokens) return True
def transfer_presale_tokens(self, token: Token, args): """ Transfer pre-sale tokens to a wallet address according to the 800 NEO minimum and 3,000 NEO maximum individual limits :param token: the token being minted for the team :param args: the address and number of neo for the contribution :return: True if successful """ storage = StorageAPI() owner = storage.get(token.owner_key) if not CheckWitness(owner): return False if len(args) != 2: return False address = args[0] neo = args[1] if len(address) != 20: return False if neo <= 0: return False presale_minted = storage.get(token.presale_minted_key) max_neo_remaining = (self.presale_token_limit - presale_minted) / self.presale_tokens_per_neo # calculate the number of tokens based on the neo value supplied tokens = neo * self.presale_tokens_per_neo new_presale_minted = presale_minted + tokens # don't allow more than the presale token limit to be distributed if new_presale_minted > self.presale_token_limit: print("transfer would exceed presale token limit") return False # protect against scenarios where we could deadlock the contract by making # a mistake in our manual distribution. allow amount smaller than 800 NEO # if we're down to fewer than 800 NEO remaining to close the pre-sale if neo < self.presale_minimum and self.presale_minimum < max_neo_remaining: print("insufficient presale contribution") return False # check if they have already exchanged in the limited round phase_key = concat(self.presale_phase_key, address) total_amount_contributed = storage.get(phase_key) # add on the amount of the new contribution total_amount_contributed += neo if total_amount_contributed > self.presale_individual_limit: print("transfer would exceed presale individual limit") return False storage.put(phase_key, total_amount_contributed) attachments = get_asset_attachments() # type: Attachments #self.mint_tokens(token, attachments.receiver_addr, address, tokens, storage) from_address = attachments.receiver_addr to_address = address # bl: the following is an exact copy of the mint_tokens function (except for the OnPreSaleMint). invoking self.mint_tokens will break the # execution of this method due to a neo-boa compiler issue. this results in a lot of code duplication, # but it's preferable to the alternative of a broken smart contract. refer: https://github.com/CityOfZion/neo-boa/issues/40 # lookup the current balance of the address current_balance = storage.get(to_address) # add it to the exchanged tokens and persist in storage new_total = tokens + current_balance storage.put(to_address, new_total) # update the in circulation amount token.add_to_circulation(tokens, storage) # update the total pre-sale tokens that have been minted storage.put(token.presale_minted_key, new_presale_minted) # track pre-sale mint as a separate event for easier tracking OnPreSaleMint(to_address, neo, tokens) # dispatch transfer event OnTransfer(from_address, to_address, tokens) return True
def calculate_tokens(self, token: Token, neo_attached: int, address, verify_only: bool): """ Perform custom token exchange calculations here. :param token: Token The token settings for the sale :param neo_attached: int Number of NEO to convert to tokens :param address: bytearray The address to mint the tokens to :param verify_only: boolean to indicate whether we are only verifying the tx. when verifying, we will skip any put side effects. :return: int: Total amount of tokens to distribute, or 0 if this isn't a valid contribution """ height = GetHeight() storage = StorageAPI() pub_sale_start_block = storage.get(self.pub_sale_start_block_key) if not pub_sale_start_block: print("main sale not started") return 0 elif height > (pub_sale_start_block + self.sale_blocks): print("crowdsale ended") return 0 elif height > (pub_sale_start_block + (2 * self.blocks_per_day)): # if we are in main sale, post-day 2, then any contribution is allowed phase_key_prefix = None individual_limit = -1 tokens_per_neo = self.sale_tokens_per_neo elif height > (pub_sale_start_block + self.blocks_per_day): phase_key_prefix = self.day2_phase_key individual_limit = self.day2_individual_limit tokens_per_neo = self.day2_tokens_per_neo else: phase_key_prefix = self.day1_phase_key individual_limit = self.day1_individual_limit tokens_per_neo = self.day1_tokens_per_neo # this value will always be an int value, but is converted to float by the division. cast back to int, which should always be safe. # note that the neo_attached has a value mirroring GAS. so, even though NEO technically doesn't have any decimals of precision, # the value still needs to be divided to get down to the whole NEO unit tokens = neo_attached / 100000000 * tokens_per_neo public_sale_sold = storage.get(token.public_sale_sold_key) new_public_sale_sold = public_sale_sold + tokens if new_public_sale_sold > token.public_sale_token_limit: print("purchase would exceed token sale limit") return 0 # in the main sale, all contributions are allowed, up to the tokens in circulation limit defined above if individual_limit <= 0: # note that we do not need to store the contribution at this point since there is no limit return tokens if neo_attached <= individual_limit: # check if they have already exchanged in the limited round phase_key = concat(phase_key_prefix, address) total_amount_contributed = storage.get(phase_key) # add on the amount of the new contribution total_amount_contributed += neo_attached # if the total amount is less than the individual limit, they're good! if total_amount_contributed <= individual_limit: # note that this method can be invoked during the Verification trigger, so we have the # verify_only param to avoid the Storage.Put during the read-only Verification trigger. # this works around a "method Neo.Storage.Put not found in ->" error in InteropService.py # since Verification is read-only and thus uses a StateReader, not a StateMachine if not verify_only: storage.put(phase_key, total_amount_contributed) return tokens print("contribution limit exceeded in round") return 0 print("too much for limited round") return 0