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
Ejemplo n.º 2
0
    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
Ejemplo n.º 3
0
    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
Ejemplo n.º 4
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
Ejemplo n.º 5
0
    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
Ejemplo n.º 6
0
    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)
Ejemplo n.º 7
0
    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
Ejemplo n.º 8
0
    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)
Ejemplo n.º 9
0
    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
Ejemplo n.º 10
0
    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
Ejemplo n.º 11
0
    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 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
Ejemplo n.º 14
0
    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
Ejemplo n.º 15
0
    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)
Ejemplo n.º 16
0
    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
Ejemplo n.º 18
0
    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 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
Ejemplo n.º 22
0
    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
Ejemplo n.º 23
0
    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
Ejemplo n.º 24
0
    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
Ejemplo n.º 25
0
    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
Ejemplo n.º 26
0
    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