# Hedged zero-supply dollar implementation
# Uses SchellingCoin as price-determining backend
#
# Stored variables:
#
# 0: Schelling coin contract
# 1: Last epoch
# 2: Genesis block of contract
# 3: USD exposure
# 4: ETH exposure
# 5: Cached price
# 6: Last interest rate
# 2^160 + k: interest rate accumulator at k epochs
# 2^161 + ADDR * 3: eth-balance of a particular address
# 2^161 + ADDR * 3 + 1: usd-balance of a particular address
# 2^161 + ADDR * 3 + 1: last accessed epoch of a particular address
#
# Transaction types:
#
# [1, to, val]: send ETH
# [2, to, val]: send USD
# [3, wei_amount]: convert ETH to USD
# [4, usd_amount]: converts USD to ETH
# [5]: deposit
# [6, amount]: withdraw
# [7]: my balance query
# [7, acct]: balance query for any acct
# [8]: global state query
# [9]: liquidation test any account
#
# The purpose of the contract is to serve as a sort of cryptographic
# bank account where users can store both ETH and USD. ETH must be
# stored in zero or positive quantities, but USD balances can be
# positive or negative. If the USD balance is negative, the invariant
# usdbal * 10 >= ethbal * 9 must be satisfied; if any account falls
# below this value, then that account's balances are zeroed. Note
# that there is a 2% bounty to ping the app if an account does go
# below zero; one weakness is that if no one does ping then it is
# quite possible for accounts to go negative-net-worth, then zero
# themselves out, draining the reserves of the "bank" and potentially
# bankrupting it. A 0.1% fee on ETH <-> USD trade is charged to
# minimize this risk. Additionally, the bank itself will inevitably
# end up with positive or negative USD exposure; to mitigate this,
# it automatically updates interest rates on USD to keep exposure
# near zero.

data schelling_coin
data last_epoch
data starting_block
data usd_exposure
data eth_exposure
data price
data last_interest_rate
data interest_rate_accum[2^50]
data accounts[2^160](eth, usd, last_epoch)

extern sc: [submit_hash, submit_value, request_balance, request_output]

def init():
    self.schelling_coin = create('schellingcoin.se')
    self.price = self.schelling_coin.request_output()
    self.interest_rate_accum[0] = 10^18
    self.starting_block = block.number

def any():
    sender = msg.sender
    epoch = (block.number - self.starting_block) / 100
    last_epoch = self.last_epoch
    usdprice = self.price

    # Update contract epochs
    if epoch > last_epoch:
        delta = epoch - last_epoch
        last_interest_rate = self.last_interest_rate
        usd_exposure - self.usd_exposure
        last_accum = self.interest_rate_accum[last_epoch]

        if usd_exposure < 0:
            self.last_interest_rate = last_interest_rate - 10000 * delta
        elif usd_exposure > 0:
            self.last_interest_rate = last_interest_rate + 10000 * delta
        
        self.interest_rate_accum[epoch] = last_accum + last_accum * last_interest_rate * delta / 10^9

        # Proceeds go to support the SchellingCoin feeding it price data, ultimately providing the depositors
        # of the SchellingCoin an interest rate
        bal = max(self.balance - self.eth_exposure, 0) / 10000
        usdprice = self.schelling_coin.request_output()
        self.price = usdprice
        self.last_epoch = epoch

    ethbal = self.accounts[msg.sender].eth
    usdbal = self.accounts[msg.sender].usd

    # Apply interest rates to sender and liquidation-test self
    if msg.sender != self:
        self.ping(self)
    
def send_eth(to, value):
    if value > 0 and value <= ethbal and usdbal * usdprice * 2 + (ethbal - value) >= 0:
        self.accounts[msg.sender].eth = ethbal - value
        self.ping(to)
        self.accounts[to].eth += value
        return(1)

def send_usd(to, value):
    if value > 0 and value <= usdbal and (usdbal - value) * usdprice * 2 + ethbal >= 0:
        self.accounts[msg.sender].usd = usdbal - value
        self.ping(to)
        self.accounts[to].usd += value
        return(1)

def convert_to_eth(usdvalue):
    ethplus = usdvalue * usdprice * 999 / 1000
    if usdvalue > 0 and (usdbal - usdvalue) * usdprice * 2 + (ethbal + ethplus) >= 0:
        self.accounts[msg.sender].eth = ethbal + ethplus
        self.accounts[msg.sender].usd = usdbal - usdvalue
        self.eth_exposure += ethplus
        self.usd_exposure -= usdvalue
        return([ethbal + ethplus, usdbal - usdvalue], 2)

def convert_to_usd(ethvalue):
    usdplus = ethvalue / usdprice * 999 / 1000
    if ethvalue > 0 and (usdbal + usdplus) * usdprice * 2 + (ethbal - ethvalue) >= 0:
        self.accounts[msg.sender].eth = ethbal - ethvalue
        self.accounts[msg.sender].usd = usdbal + usdplus
        self.eth_exposure -= ethvalue
        self.usd_exposure += usdplus
        return([ethbal - ethvalue, usdbal + usdplus], 2)

def deposit():
    self.accounts[msg.sender].eth = ethbal + msg.value
    self.eth_exposure += msg.value
    return(ethbal + msg.value)

def withdraw(value):
    if value > 0 and value <= ethbal and usdbal * usdprice * 2 + (ethbal - value) >= 0:
        self.accounts[msg.sender].eth -= value
        self.eth_exposure -= value
        return(ethbal - value)

def balance(acct):
    self.ping(acct)
    return([self.accounts[acct].eth, self.accounts[acct].usd], 2)

def global_state_query(acct):
    interest = self.last_interest_rate
    usd_exposure = self.usd_exposure
    eth_exposure = self.eth_exposure
    eth_balance = self.balance
    return([epoch, usdprice, interest, usd_exposure, eth_exposure, eth_balance], 6)

def ping(acct):
    account_last_epoch = self.accounts[acct].last_epoch
    if account_last_epoch != epoch:
        cur_usd_balance = self.accounts[acct].usd
        new_usd_balance = cur_usd_balance * self.interest_rate_accum[epoch] / self.interest_rate_accum[account_last_epoch]
        self.accounts[acct].usd = new_usd_balance
        self.accounts[acct].last_epoch = epoch
        self.usd_exposure += new_usd_balance - cur_usd_balance
        
        ethbal = self.accounts[acct].eth

        if new_usd_balance * usdval * 10 + ethbal * 9 < 0:
            self.accounts[acct].eth = 0
            self.accounts[acct].usd = 0
            self.accounts[msg.sender].eth += ethbal / 50
            self.eth_exposure += -ethbal + ethbal / 50
            self.usd_exposure += new_usd_balance
            return(1)
        return(0)
