diff --git a/hatch.py b/hatch.py new file mode 100644 index 0000000..a85984f --- /dev/null +++ b/hatch.py @@ -0,0 +1,96 @@ +from typing import List, Tuple +from abcurve import AugmentedBondingCurve +from datetime import datetime + +def unlocked_fraction(day: int, cliff_days: int, halflife_days: float) -> float: + return 1 - 0.5**((day - cliff_days)/halflife_days) + +def convert_80p_to_cliff_and_halflife(days: int, v_ratio: int = 2) -> Tuple[float, float]: + """ + For user's convenience, we ask him after how many days he would like 80% of his tokens to be unlocked. + This needs to be converted into a half life (unit days). + 2.321928094887362 is log(base0.5) 0.2, or log 0.2 / log 0.5. + v_ratio is cliff / halflife, and its default is determined by Commons Stack + """ + halflife = days / (2.321928094887362 + v_ratio) + cliff = v_ratio * halflife + return cliff, halflife + +def hatch_raise_split_pools(total_hatch_raise, funding_pool_fraction): + """Splits the hatch raise between the funding / collateral pool based on the fraction.""" + funding_pool = funding_pool_fraction * total_hatch_raise + collateral_pool = total_hatch_raise * (1-funding_pool_fraction) + return funding_pool, collateral_pool + +def contributions_to_token_batches(hatcher_contributions: List[int], initial_token_supply: int, vesting_80p_unlocked: int) -> List[float]: + """ + hatcher_contributions: a list of hatcher contributions + initial_token_supply: NOT denominated in millions + vesting_80p_unlocked: vesting parameter - the number of days after which 80% of tokens will be unlocked, including the cliff period + """ + total_hatch_raise = sum(hatcher_contributions) + + # In the hatch, everyone buys in at the same time, with the same price. So just split the token supply amongst the hatchers proportionally to their contributions + tokens_per_hatcher = [(x / total_hatch_raise) * initial_token_supply for x in hatcher_contributions] + + cliff_days, halflife_days = convert_80p_to_cliff_and_halflife(vesting_80p_unlocked) + + token_batches = [TokenBatch(x, cliff_days, halflife_days, hatch=True) for x in tokens_per_hatcher] + return token_batches + +class TokenBatch: + def __init__(self, value: float, cliff_days: int, halflife_days: int, hatch = False): + self.hatch_tokens = hatch + self.value = value + self.creation_date = datetime.today() + self.cliff_days = cliff_days + self.halflife_days = halflife_days + def __repr__(self): + o = "TokenBatch {} {}, Unlocked: {}".format("Hatch" if self.hatch_tokens else "", self.value, self.unlocked(datetime.today())) + return o + + def unlocked(self, day: datetime = datetime.today()) -> float: + if self.hatch_tokens: + days_delta = day - self.creation_date + u = unlocked_fraction(days_delta.days, self.cliff_days, self.halflife_days) + return u if u > 0 else 0 + else: + return 1.0 + +class Organization: + def __init__(self, total_hatch_raise, funding_pool_fraction, token_supply_millions, exit_tribute=0): + # a fledgling organization starts out in the hatching phase. After the hatch phase ends, money from new investors will only go into the collateral pool. + # Essentials + self.funding_pool_fraction = funding_pool_fraction + self._collateral_pool = (1-funding_pool_fraction) * total_hatch_raise # (1-0.35) -> 0.65 * total_hatch_raise = 65% collateral, 35% funding + self._funding_pool = funding_pool_fraction * total_hatch_raise # 0.35 * total_hatch_raise = 35% + self._token_supply = token_supply_millions + self._hatch_tokens = token_supply_millions # hatch_tokens keeps track of the number of tokens that were created when hatching, so we can calculate the unlocking of those + self.bonding_curve = AugmentedBondingCurve(self._collateral_pool, token_supply_millions) + + # Options + self.exit_tribute = exit_tribute + + def deposit(self, dai_millions): + """ + Deposit DAI after the hatch phase. This means all the incoming deposit goes to the collateral pool. + """ + tokens, realized_price = self.bonding_curve.deposit(dai_millions, self._collateral_pool, self._token_supply) + self._token_supply += tokens + self._collateral_pool += dai_millions + return tokens, realized_price + + def burn(self, tokens_millions): + """ + Burn tokens, with/without an exit tribute. + """ + dai_millions, realized_price = self.bonding_curve.burn(tokens_millions, self._collateral_pool, self._token_supply) + self._token_supply -= tokens_millions + self._collateral_pool -= dai_millions + money_returned = dai_millions + + if self.exit_tribute: + self._funding_pool += organization.exit_tribute * dai_millions + money_returned = (1-organization.exit_tribute) * dai_millions + + return money_returned, realized_price \ No newline at end of file diff --git a/hatch_test.py b/hatch_test.py new file mode 100644 index 0000000..edee876 --- /dev/null +++ b/hatch_test.py @@ -0,0 +1,25 @@ +from hatch import * +import unittest +from datetime import datetime, timedelta + +class TestHatch(unittest.TestCase): + def test_unlocked_fraction(self): + self.assertEqual(unlocked_fraction(90, 90, 90), 0) # At Day 90, the cliff has just ended and the vesting curve has begun at 0 + self.assertEqual(unlocked_fraction(180, 90, 90), 0.5) # At Day 180, the cliff has ended and we are in the vesting curve, whose half-life is 90 as well, so at 180 we should get 0.5. + self.assertEqual(unlocked_fraction(270, 90, 90), 0.75) # At Day 270, 2 half lives of the vesting curve have passed - 0.75 of tokens should be unlocked. + self.assertLess(unlocked_fraction(89, 90, 90), 0) # At Day 270, 2 half lives of the vesting curve have passed - 0.75 of tokens should be unlocked. + def test_convert_80p_to_halflife(self): + self.assertEqual(convert_80p_to_cliff_and_halflife(90), (41.64807836875666, 20.82403918437833)) + +class TestSystem(unittest.TestCase): + def test_system(self): + # 3 contributors contribute equally to the foundation of an organization for 6 million tokens. + token_supply_initial = 6e6 # TODO: millions + hatcher_contributions = [3e3, 3e3, 3e3] + token_batches = contributions_to_token_batches(hatcher_contributions, token_supply_initial, 90) + + o = Organization(sum(hatcher_contributions), 0.3, token_supply_initial) + + print(o._funding_pool) + print(o.deposit(5e5)) + print(o._funding_pool) \ No newline at end of file