# mutuala - subcurrency

# We want to issue a currency that reduces in value as you store it through negative interest.
# That negative interest would be stored in a commons account.  It's like the p2p version of a
# capital tax

# the same things goes for transactions - you pay as you use the currency.  However, the more
# you pay, the more you get to say about what the tax is used for

# each participant can propose a recipient for a payout to be made out of the commons account,
# others can vote on it by awarding it tax_credits.

# TODO should proposal have expiration timestamp?, after which the tax_credits are refunded
# TODO multiple proposals can take more credits that available in the Commons, how to handle this
# TODO how to handle lost accounts, after which no longer possible to get 2/3 majority

shared:
    COMMONS = 42
    ADMIN = 666
    CAPITAL_TAX_PER_DAY = 7305 # 5% per year
    PAYMENT_TAX = 20 # 5%

    ACCOUNT_LIST_OFFSET = 2^160
    ACCOUNT_MAP_OFFSET = 2^161
    PROPOSAL_LIST_OFFSET = 2^162
    PROPOSAL_MAP_OFFSET = 2^163

init:
    contract.storage[ADMIN] = msg.sender
    contract.storage[ACCOUNT_LIST_OFFSET - 1] = 1
    contract.storage[ACCOUNT_LIST_OFFSET] = msg.sender
    contract.storage[ACCOUNT_MAP_OFFSET + msg.sender] = 10^12
    contract.storage[ACCOUNT_MAP_OFFSET + msg.sender + 1] = block.timestamp

# contract.storage[COMMONS] = balance commons

# contract.storage[ACCOUNT_LIST_OFFSET - 1] = number of accounts
# contract.storage[ACCOUNT_LIST_OFFSET + n] = account n

# contract.storage[PROPOSAL_LIST_OFFSET - 1] contains the number of proposals
# contract.storage[PROPOSAL_LIST_OFFSET + n] = proposal n

# per account:
# contract.storage[ACCOUNT_MAP_OFFSET + account] = balance
# contract.storage[ACCOUNT_MAP_OFFSET + account+1] = timestamp_last_transaction
# contract.storage[ACCOUNT_MAP_OFFSET + account+2] = tax_credits

# per proposal:
# contract.storage[PROPOSAL_MAP_OFFSET + proposal_id] = recipient
# contract.storage[PROPOSAL_MAP_OFFSET + proposal_id+1] = amount
# contract.storage[PROPOSAL_MAP_OFFSET + proposal_id+2] = total vote credits

code:
    if msg.data[0] == "suicide" and msg.sender == contract.storage[ADMIN]:
        suicide(msg.sender)

    elif msg.data[0] == "balance":
        addr = msg.data[1]
        return(contract.storage[ACCOUNT_MAP_OFFSET + addr])

    elif msg.data[0] == "pay":
        from = msg.sender
        fromvalue = contract.storage[ACCOUNT_MAP_OFFSET + from]
        to = msg.data[1]
        if to == 0 or to >= 2^160:
            return([0, "invalid address"], 2)
        value = msg.data[2]
        tax = value / PAYMENT_TAX

        if fromvalue >= value + tax:
            contract.storage[ACCOUNT_MAP_OFFSET + from] = fromvalue - (value + tax)
            contract.storage[ACCOUNT_MAP_OFFSET + to] += value
            # tax
            contract.storage[COMMONS] += tax
            contract.storage[ACCOUNT_MAP_OFFSET + from + 2] += tax

            # check timestamp field to see if target account exists
            if contract.storage[ACCOUNT_MAP_OFFSET + to + 1] == 0:
                # register new account
                nr_accounts = contract.storage[ACCOUNT_LIST_OFFSET - 1]
                contract.storage[ACCOUNT_LIST_OFFSET + nr_accounts] = to
                contract.storage[ACCOUNT_LIST_OFFSET - 1] += 1
                contract.storage[ACCOUNT_MAP_OFFSET + to + 1] = block.timestamp

            return(1)
        else:
            return([0, "insufficient balance"], 2)

    elif msg.data[0] == "hash":
        proposal_id = sha3(msg.data[1])
        return(proposal_id)

    elif msg.data[0] == "propose":
        from = msg.sender
        # check if sender has an account and has tax credits
        if contract.storage[ACCOUNT_MAP_OFFSET + from + 2] == 0:
            return([0, "sender has no tax credits"], 2)

        proposal_id = sha3(msg.data[1])
        # check if proposal doesn't already exist
        if contract.storage[PROPOSAL_MAP_OFFSET + proposal_id]:
            return([0, "proposal already exists"])

        to = msg.data[2]
        # check if recipient is a valid address and has an account (with timestamp)
        if to == 0 or to >= 2^160:
            return([0, "invalid address"], 2)
        if contract.storage[ACCOUNT_MAP_OFFSET + to + 1] == 0:
            return([0, "invalid to account"], 2)

        value = msg.data[3]
        # check if there is enough money in the commons account
        if value > contract.storage[COMMONS]:
            return([0, "not enough credits in commons"], 2)

        # record proposal in list
        nr_proposals = contract.storage[PROPOSAL_LIST_OFFSET - 1]
        contract.storage[PROPOSAL_LIST_OFFSET + nr_proposals] = proposal_id
        contract.storage[PROPOSAL_LIST_OFFSET - 1] += 1

        # record proposal in map
        contract.storage[PROPOSAL_MAP_OFFSET + proposal_id] = to
        contract.storage[PROPOSAL_MAP_OFFSET + proposal_id + 1] = value

        return(proposal_id)

    elif msg.data[0] == "vote":
        from = msg.sender
        proposal_id = sha3(msg.data[1])
        value = msg.data[2]
        # check if sender has an account and has tax credits
        if value < contract.storage[ACCOUNT_MAP_OFFSET + from + 2]:
            return([0, "sender doesn't have enough tax credits"], 2)

        # check if proposal exist
        if contract.storage[PROPOSAL_MAP_OFFSET + proposal_id] == 0:
            return([0, "proposal doesn't exist"], 2)

        # increase votes
        contract.storage[PROPOSAL_MAP_OFFSET + proposal_id + 2] += value
        # withdraw tax credits
        contract.storage[ACCOUNT_MAP_OFFSET + from + 2] -= value

        # did we reach 2/3 threshold?
        if contract.storage[PROPOSAL_MAP_OFFSET + proposal_id + 2] >= contract.storage[COMMONS] * 2 / 3:
            # got majority
            to = contract.storage[PROPOSAL_MAP_OFFSET + proposal_id]
            amount = contract.storage[PROPOSAL_MAP_OFFSET + proposal_id + 1]

            # adjust balances
            contract.storage[ACCOUNT_MAP_OFFSET + to] += amount
            contract.storage[COMMONS] -= amount

            # reset proposal
            contract.storage[PROPOSAL_MAP_OFFSET + proposal_id] = 0
            contract.storage[PROPOSAL_MAP_OFFSET + proposal_id + 1] = 0
            contract.storage[PROPOSAL_MAP_OFFSET + proposal_id + 2] = 0
            return(1)

        return(proposal_id)

    elif msg.data[0] == "tick":
        nr_accounts = contract.storage[ACCOUNT_LIST_OFFSET - 1]
        account_idx = 0
        tax_paid = 0
        # process all accounts and see if they have to pay their daily capital tax
        while account_idx < nr_accounts:
            cur_account = contract.storage[ACCOUNT_LIST_OFFSET + account_idx]
            last_timestamp = contract.storage[ACCOUNT_MAP_OFFSET + cur_account + 1]
            time_diff = block.timestamp - last_timestamp
            if time_diff >= 86400:
                tax_days = time_diff / 86400
                balance = contract.storage[ACCOUNT_MAP_OFFSET + cur_account]
                tax = tax_days * (balance / CAPITAL_TAX_PER_DAY)
                if tax > 0:
                    # charge capital tax, but give tax credits in return
                    contract.storage[ACCOUNT_MAP_OFFSET + cur_account] -= tax
                    contract.storage[ACCOUNT_MAP_OFFSET + cur_account + 1] += tax_days * 86400
                    contract.storage[ACCOUNT_MAP_OFFSET + cur_account + 2] += tax

                    contract.storage[COMMONS] += tax
                    tax_paid += 1
            account_idx += 1
        return(tax_paid) # how many accounts did we charge tax on

    else:
        return([0, "unknown command"], 2)
