def kyc_deregister(self, args, token: Token): """ :param args:list a list of addresses to deregister :param token: Token A token object with your ICO settings :return: int: The number of addresses deregistered from KYC """ ok_count = 0 storage = StorageAPI() owner = storage.get(token.owner_key) if CheckWitness(owner): for address in args: if len(address) == 20: kyc_storage_key = concat(self.kyc_key, address) storage.delete(kyc_storage_key) OnKYCDeregister(address) ok_count += 1 return ok_count
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 crowdsale_available_amount(self): """ :return: int The amount of tokens left for sale in the crowdsale """ storage = StorageAPI() public_sale_sold = storage.get(self.public_sale_sold_key) # bl: the total amount of tokens available is now based off of how many tokens have been sold during the public sale available = self.public_sale_token_limit - public_sale_sold if available < 0: return 0 return available
def resume_sale(token: Token): """ Resume the sale :param token: Token The token of the sale to resume :return: bool: Whether the operation was successful """ storage = StorageAPI() owner = storage.get(token.owner_key) if not CheckWitness(owner): print("Must be owner to resume sale") return False # mark the sale as active storage.delete(token.sale_paused_key) 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 get_circulation(self, storage: StorageAPI): """ Get the total amount of tokens in circulation :param storage: StorageAPI A StorageAPI object for storage interaction :return: int: Total amount in circulation """ return storage.get(self.in_circulation_key)
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 do_allowance(self, storage: StorageAPI, t_owner, t_spender): allowance_key = concat(t_owner, t_spender) if len(allowance_key) != 40: return 0 amount = storage.get(allowance_key) return amount
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 cancel_change_owner(token: Token): """ Cancel a pending ownership transfer request :param token: Token The token to cancel the ownership transfer for :return: bool: Whether the operation was successful """ storage = StorageAPI() new_owner = storage.get(token.new_owner_key) if not new_owner: print( "Can't cancel_change_owner unless an owner change is already pending" ) return False owner = storage.get(token.owner_key) if not CheckWitness(owner): print("Must be owner to cancel change_owner") return False # delete the new owner to cancel the transfer. storage.delete(token.new_owner_key) return True
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 kyc_status(self, args): """ Gets the KYC Status of an address :param args:list a list of arguments :return: bool: Returns the kyc status of an address """ storage = StorageAPI() if len(args) > 0: addr = args[0] return self.get_kyc_status(addr, storage) return False
def get_kyc_status(self, address, storage: StorageAPI): """ Looks up the KYC status of an address :param address:bytearray The address to lookup :param storage: StorageAPI A StorageAPI object for storage interaction :return: bool: KYC Status of address """ if len(address) == 20: kyc_storage_key = concat(self.kyc_key, address) if storage.get(kyc_storage_key): return True return False
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 check_and_calculate_tokens(self, token: Token, attachments: Attachments, storage: StorageAPI, verify_only: bool): """ Determines if the contract invocation meets all requirements for the ICO exchange of neo into NEP5 Tokens. Note: This method can be called via both the Verification portion of an SC or the Application portion When called in the Verification portion of an SC, it can be used to reject TX that do not qualify for exchange, thereby reducing the need for manual NEO refunds considerably :param token: Token A token object with your ICO settings :param attachments: Attachments An attachments object with information about attached NEO/Gas assets :param storage: StorageAPI A StorageAPI object for storage interaction :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 """ # don't allow any contributions if the sale is paused if storage.get(token.sale_paused_key): return 0 if attachments.neo_attached == 0: print("no neo attached") return 0 # the following looks up whether an address has been # registered with the contract for KYC regulations is_kyc_approved = self.get_kyc_status(attachments.sender_addr, storage) if not is_kyc_approved: print("not KYC approved") return 0 # it turns out this is REQUIRED to work around a neo-boa python compiler issue: https://github.com/CityOfZion/neo-boa/issues/29 # else: # print("KYC approved") j = 0 return self.calculate_tokens(token, attachments.neo_attached, attachments.sender_addr, verify_only)
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 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 Main(operation, args): """ :param operation: str The name of the operation to perform :param args: list A list of arguments along with the operation :return: bytearray: The result of the operation """ trigger = GetTrigger() token = Token() # This is used in the Verification portion of the contract # To determine whether a transfer of system assets (NEO/Gas) involving # This contract's address can proceed if trigger == Verification: storage = StorageAPI() owner = storage.get(token.owner_key) if owner: # If the invoker is the owner of this contract, proceed if CheckWitness(owner): return True else: # check original_owner if not deployed yet (i.e. no owner in storage) if CheckWitness(token.original_owner): return True # Otherwise, we need to lookup the assets and determine # If attachments of assets is ok attachments = get_asset_attachments() # type:Attachments crowdsale = Crowdsale() # the exchange will be allowed if the number of tokens to convert to is greater than zero. # zero indicates that there is a reason this contribution will not be allowed return crowdsale.check_and_calculate_tokens(token, attachments, storage, True) > 0 elif trigger == Application: if operation != None: nep = NEP5Handler() for op in nep.get_methods(): if operation == op: return nep.handle_nep51(operation, args, token) if operation == 'deploy': return deploy(token) if operation == 'circulation': storage = StorageAPI() return token.get_circulation(storage) sale = Crowdsale() if operation == 'mintTokens': return sale.exchange(token) if operation == 'crowdsale_register': return sale.kyc_register(args, token) if operation == 'crowdsale_deregister': return sale.kyc_deregister(args, token) if operation == 'crowdsale_status': return sale.kyc_status(args) if operation == 'crowdsale_available': return token.crowdsale_available_amount() if operation == 'transfer_presale_tokens': return sale.transfer_presale_tokens(token, args) if operation == 'transfer_team_tokens': return sale.transfer_team_tokens(token, args) if operation == 'transfer_company_tokens': return sale.transfer_company_tokens(token, args) if operation == 'mint_rewards_tokens': return sale.mint_rewards_tokens(token, args) if operation == 'change_owner': new_owner = args[0] return change_owner(token, new_owner) if operation == 'accept_owner': return accept_owner(token) if operation == 'cancel_change_owner': return cancel_change_owner(token) if operation == 'start_public_sale': return sale.start_public_sale(token) if operation == 'pause_sale': return pause_sale(token) if operation == 'resume_sale': return resume_sale(token) return 'unknown operation' return False
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
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 handle_nep51(self, operation, args, token: Token): # these first 3 don't require get ctx if operation == 'name': return token.name elif operation == 'decimals': return token.decimals elif operation == 'symbol': return token.symbol storage = StorageAPI() arg_error = 'Incorrect Arg Length' if operation == 'totalSupply': return token.get_circulation(storage) elif operation == 'balanceOf': if len(args) == 1: account = args[0] if len(account) != 20: return 0 return storage.get(account) return arg_error elif operation == 'transfer': if len(args) == 3: t_from = args[0] t_to = args[1] t_amount = args[2] return self.do_transfer(storage, t_from, t_to, t_amount) return arg_error elif operation == 'transferFrom': if len(args) == 3: t_from = args[0] t_to = args[1] t_amount = args[2] return self.do_transfer_from(storage, t_from, t_to, t_amount) return arg_error elif operation == 'approve': if len(args) == 3: t_owner = args[0] t_spender = args[1] t_amount = args[2] return self.do_approve(storage, t_owner, t_spender, t_amount) return arg_error elif operation == 'allowance': if len(args) == 2: t_owner = args[0] t_spender = args[1] return self.do_allowance(storage, t_owner, t_spender) return arg_error return False