From 1cc033377630c8705cbf1ac459331e5b6749d722 Mon Sep 17 00:00:00 2001 From: Mc01 Date: Sat, 20 Dec 2025 00:53:14 +0100 Subject: [PATCH 01/15] Visualize mechanics with math. WIP. --- .gitignore | 0 math/test_yield_model.py | 122 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 .gitignore create mode 100644 math/test_yield_model.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/math/test_yield_model.py b/math/test_yield_model.py new file mode 100644 index 0000000..6336453 --- /dev/null +++ b/math/test_yield_model.py @@ -0,0 +1,122 @@ +from decimal import Decimal as D +from typing import Dict + +# const +K = D(1_000) +M = D(1_000_000) +B = D(1_000_000_000) + +# cap +CAP = 1 * B + +class LP: + balance_usd: D = D(0) + balance_token = CAP + price: D = D(1) + # TODO: track users liquidity + +lp = LP() + +class CompoundingSnapshot: + value: D + snapshot_of_compounding_index: D + + def __init__(self, value: D, snapshot: D): + self.value = value + self.snapshot_of_compounding_index = snapshot + +class Vault: + apy: D = D(5) / D(100) + balance_usd: D = D(0) + compounding_index: D = D(1.0) + user_compounding_snapshots: Dict[str, CompoundingSnapshot] = {} + compounds: int = 0 + + # local compounding performed on user interaction + # TODO: convert to single snapshot per LP + def add(self, user_name: str, value: D): + user_snapshot = self.user_compounding_snapshots.get(user_name, None) + if user_snapshot is None: + self.user_compounding_snapshots[user_name] = CompoundingSnapshot( + value, self.compounding_index + ) + self.balance_usd += value + else: + raise NotImplementedError("Handle case with local compound of user snapshot") + + # local compounding performed on user interaction + def remove(self, user_name: str, value: D): + pass + + # global compounding performed daily + def compound(self, days: int): + # run compounding daily + for _ in range(0, days): + self.compounding_index *= D(1) + (self.apy / D(365)) + + # track compounds number + self.compounds += days + +vault = Vault() + +class User: + name: str + balance_usd: D = D(0) + balance_token: D = D(0) + + def __init__(self, name: str, usd: D, token: D = D(0)): + self.name = name + self.balance_usd = usd + self.balance_token = token + +user_a = User("aaron", 1 * K) + +def buy(user: User, amount: D): + # take usd + user.balance_usd -= amount + lp.balance_usd += amount + + # compute out amount (token) + # 1_000 USD / 1 USD price -> 1_000 tokens + # 2_000 USD / 1 USD price -> 2_000 tokens + # 1_500 USD / 1.5 USD price -> 1_000 tokens + out_amount = amount / lp.price + + # give token + lp.balance_token -= out_amount + user.balance_token += out_amount + + # bump price + # in pool: 0 USD ; amount: 10_000 USD -> 0.1 UP (higher amount > pool) + # in pool: 100 USD ; amount: 100 USD -> 0.01 UP (equal amount to pool) + # in pool: 1000 USD ; amount: 100 USD -> 0.001 UP (smaller amount < pool) + # TODO: convert to price discovery (bonding curve) + lp.price += D(0.1) + + # rehypo + rehypo(user) + +def rehypo(user: User): + vault.add(user.name, lp.balance_usd) + lp.balance_usd = D(0) + +def sell(user: User, amount: D): + # take token + user.balance_token -= amount + lp.balance_token += amount + + # compute amount in (usd) + in_amount = amount * lp.price + + # dehypo + dehypo(user, in_amount) + + lp.balance_usd -= in_amount + user.balance_usd += in_amount + + # deflate price + # TODO: convert to price discovery (bonding curve) + lp.price -= D(0.1) + +def dehypo(user: User, amount: D): + pass From ae6ba1e1418658d251c88d9c3c04d5c52112980d Mon Sep 17 00:00:00 2001 From: Mc01 Date: Sun, 21 Dec 2025 00:10:45 +0100 Subject: [PATCH 02/15] Single user. No fees. Simple pool. Simple vault. --- math/test_yield_model.py | 269 +++++++++++++++++++++++++++++---------- 1 file changed, 203 insertions(+), 66 deletions(-) diff --git a/math/test_yield_model.py b/math/test_yield_model.py index 6336453..de065a9 100644 --- a/math/test_yield_model.py +++ b/math/test_yield_model.py @@ -1,5 +1,5 @@ from decimal import Decimal as D -from typing import Dict +from typing import Dict, Optional # const K = D(1_000) @@ -9,13 +9,15 @@ # cap CAP = 1 * B -class LP: +class User: + name: str balance_usd: D = D(0) - balance_token = CAP - price: D = D(1) - # TODO: track users liquidity + balance_token: D = D(0) -lp = LP() + def __init__(self, name: str, usd: D, token: D = D(0)): + self.name = name + self.balance_usd = usd + self.balance_token = token class CompoundingSnapshot: value: D @@ -29,26 +31,48 @@ class Vault: apy: D = D(5) / D(100) balance_usd: D = D(0) compounding_index: D = D(1.0) - user_compounding_snapshots: Dict[str, CompoundingSnapshot] = {} + lp_compounding_snapshot: Optional[CompoundingSnapshot] = None compounds: int = 0 - # local compounding performed on user interaction - # TODO: convert to single snapshot per LP - def add(self, user_name: str, value: D): - user_snapshot = self.user_compounding_snapshots.get(user_name, None) - if user_snapshot is None: - self.user_compounding_snapshots[user_name] = CompoundingSnapshot( - value, self.compounding_index + def balance_of(self) -> D: + if self.lp_compounding_snapshot is None: + return self.balance_usd + else: + return self.lp_compounding_snapshot.value * ( + self.compounding_index / self.lp_compounding_snapshot.snapshot_of_compounding_index + ) + + def add(self, value: D): + if self.lp_compounding_snapshot is None: + # store value as snapshot + self.lp_compounding_snapshot = CompoundingSnapshot( + value, + self.compounding_index + ) + else: + # we assume that compound has been already run + # store deposit + last deposit with rewards as snapshot + self.lp_compounding_snapshot = CompoundingSnapshot( + value + self.balance_of(), + self.compounding_index ) - self.balance_usd += value + + # set balance in usd as deposit + last deposit with rewards + self.balance_usd = self.balance_of() + + def remove(self, value: D): + if self.lp_compounding_snapshot is None: + raise Exception("Nothing staked!") else: - raise NotImplementedError("Handle case with local compound of user snapshot") + # store last deposit with rewards - withdrawal as snapshot + self.lp_compounding_snapshot = CompoundingSnapshot( + self.balance_of() - value, + self.compounding_index + ) - # local compounding performed on user interaction - def remove(self, user_name: str, value: D): - pass + # set balance in usd as last deposit with rewards - withdrawal + self.balance_usd = self.balance_of() - # global compounding performed daily def compound(self, days: int): # run compounding daily for _ in range(0, days): @@ -57,66 +81,179 @@ def compound(self, days: int): # track compounds number self.compounds += days + # increase usd value + vault = Vault() -class User: - name: str +class UserSnapshot: + compounds: int + snapshot_of_compounding_index: D + + def __init__(self, compounds: int, snapshot: D): + self.compounds = compounds + self.snapshot_of_compounding_index = snapshot + +class LP: balance_usd: D = D(0) - balance_token: D = D(0) + balance_token = D(0) + price: D = D(1) + minted: D = D(0) + liquidity: Dict[str, D] = {} + total_liquidity: D = D(0) + user_snapshot: Dict[str, UserSnapshot] = {} - def __init__(self, name: str, usd: D, token: D = D(0)): - self.name = name - self.balance_usd = usd - self.balance_token = token + # use token to perform mint (in case of buy or inflation) + def mint(self, amount: D): + if self.minted + amount > CAP: + raise Exception("Cannot mint over cap") + self.balance_token += amount + self.minted += amount -user_a = User("aaron", 1 * K) + def add_liquidity(self, user: User, token_amount: D, usd_amount: D): + # take tokens from user + user.balance_token -= token_amount + user.balance_usd -= usd_amount + + # push tokens to pool + self.balance_token += token_amount + self.balance_usd += usd_amount -def buy(user: User, amount: D): - # take usd - user.balance_usd -= amount - lp.balance_usd += amount + # put usdc on vault for yield generation + self.rehypo(user) - # compute out amount (token) - # 1_000 USD / 1 USD price -> 1_000 tokens - # 2_000 USD / 1 USD price -> 2_000 tokens - # 1_500 USD / 1.5 USD price -> 1_000 tokens - out_amount = amount / lp.price + # store compound day on user + self.user_snapshot[user.name] = UserSnapshot( + vault.compounds, + vault.compounding_index + ) + + # compute liquidity + user_liquidity = self.liquidity.get(user.name) + if user_liquidity is None: + self.liquidity[user.name] = token_amount + usd_amount + else: + self.liquidity[user.name] += token_amount + usd_amount + self.total_liquidity += token_amount + usd_amount + + def remove_liquidity(self, user: User, liquidity_amount: D): + # translate liquidity to token & usdc + compound_delta = vault.compounding_index / self.user_snapshot[user.name].snapshot_of_compounding_index + + usd_deposit = liquidity_amount / 2 + usd_yield = usd_deposit * (compound_delta - D(1)) * 2 + usd_amount = usd_deposit + usd_yield - # give token - lp.balance_token -= out_amount - user.balance_token += out_amount + token_deposit = liquidity_amount / 2 + token_yield = token_deposit * (compound_delta - D(1)) + token_amount = token_deposit + token_yield - # bump price - # in pool: 0 USD ; amount: 10_000 USD -> 0.1 UP (higher amount > pool) - # in pool: 100 USD ; amount: 100 USD -> 0.01 UP (equal amount to pool) - # in pool: 1000 USD ; amount: 100 USD -> 0.001 UP (smaller amount < pool) - # TODO: convert to price discovery (bonding curve) - lp.price += D(0.1) + # mint inflation yield on tokens + self.mint(token_yield) - # rehypo - rehypo(user) + # remove user usdc deposit & rewards from vault + self.dehypo(user, usd_amount) + + # remove funds from lp + self.balance_token -= token_amount + self.balance_usd -= usd_amount + + # send funds to user + user.balance_token += token_amount + user.balance_usd += usd_amount + + # update liquidity + self.liquidity[user.name] -= liquidity_amount + self.total_liquidity -= liquidity_amount + + def buy(self, user: User, amount: D): + # take usd + user.balance_usd -= amount + self.balance_usd += amount + + # compute out amount (token) + # 1_000 USD / 1 USD price -> 1_000 tokens + # 2_000 USD / 1 USD price -> 2_000 tokens + # 1_500 USD / 1.5 USD price -> 1_000 tokens + out_amount = amount / self.price + + # mint as much token as needed + self.mint(out_amount - self.balance_token) + + # give token + self.balance_token -= out_amount + user.balance_token += out_amount -def rehypo(user: User): - vault.add(user.name, lp.balance_usd) - lp.balance_usd = D(0) + # bump price + # in pool: 0 USD ; amount: 10_000 USD -> 0.1 UP (higher amount > pool) + # in pool: 100 USD ; amount: 100 USD -> 0.01 UP (equal amount to pool) + # in pool: 1000 USD ; amount: 100 USD -> 0.001 UP (smaller amount < pool) + # TODO: convert to price discovery (bonding curve) + self.price += D(0.1) -def sell(user: User, amount: D): - # take token - user.balance_token -= amount - lp.balance_token += amount + # rehypo + self.rehypo(user) - # compute amount in (usd) - in_amount = amount * lp.price + def rehypo(self, user: User): + # add funds to vault + vault.add(self.balance_usd) + + # remove funds from lp + self.balance_usd = D(0) + + # save user information + + def sell(self, user: User, amount: D): + # take token + user.balance_token -= amount + self.balance_token += amount + + # compute amount in (usd) + in_amount = amount * self.price + + # dehypo + self.dehypo(user, in_amount) + + self.balance_usd -= in_amount + user.balance_usd += in_amount + + # deflate price + # TODO: convert to price discovery (bonding curve) + self.price -= D(0.1) + + def dehypo(self, user: User, amount: D): + # remove from vault + vault.remove(amount) + + # add to lp + self.balance_usd += D(amount) + + # update user information + +lp = LP() + +user_a = User("aaron", 1 * K) - # dehypo - dehypo(user, in_amount) +# buy tokens for 500 usd +lp.buy(user_a, D(500)) +assert lp.balance_usd == D(0) +assert vault.balance_usd == D(500) +assert vault.balance_of() == D(500) - lp.balance_usd -= in_amount - user.balance_usd += in_amount +lp.add_liquidity(user_a, D(500), D(500)) +assert lp.balance_token == D(500) +assert lp.balance_usd == D(0) +assert vault.balance_usd == D(1_000) +assert vault.balance_of() == D(1_000) - # deflate price - # TODO: convert to price discovery (bonding curve) - lp.price -= D(0.1) +# compound for 100 days +vault.compound(100) +print(f"[100 days] Vault balance: {vault.balance_of()}") -def dehypo(user: User, amount: D): - pass +# remove liquidity +lp.remove_liquidity(user_a, lp.liquidity[user_a.name]) +print(f"[Liquidity removal] User USDC: {user_a.balance_usd}") +print(f"[Liquidity removal] User tokens: {user_a.balance_token}") +print(f"[Liquidity removal] LP tokens: {lp.balance_token}") +print(f"[Liquidity removal] LP USDC: {lp.balance_usd}") +print(f"[Liquidity removal] Vault balance of: {vault.balance_of()}") +print(f"[Liquidity removal] Vault USDC: {vault.balance_usd}") From 469f873e98d03891a1756ec8d37646ad19f08e49 Mon Sep 17 00:00:00 2001 From: Mc01 Date: Sun, 21 Dec 2025 00:42:39 +0100 Subject: [PATCH 03/15] Encapsulate into scenarios. --- math/test_yield_model.py | 133 ++++++++++++++++++++++++--------------- 1 file changed, 81 insertions(+), 52 deletions(-) diff --git a/math/test_yield_model.py b/math/test_yield_model.py index de065a9..dfff784 100644 --- a/math/test_yield_model.py +++ b/math/test_yield_model.py @@ -11,10 +11,10 @@ class User: name: str - balance_usd: D = D(0) - balance_token: D = D(0) + balance_usd: D + balance_token: D - def __init__(self, name: str, usd: D, token: D = D(0)): + def __init__(self, name: str, usd: D = D(0), token: D = D(0)): self.name = name self.balance_usd = usd self.balance_token = token @@ -28,11 +28,18 @@ def __init__(self, value: D, snapshot: D): self.snapshot_of_compounding_index = snapshot class Vault: - apy: D = D(5) / D(100) - balance_usd: D = D(0) - compounding_index: D = D(1.0) - lp_compounding_snapshot: Optional[CompoundingSnapshot] = None - compounds: int = 0 + apy: D + balance_usd: D + compounding_index: D + lp_compounding_snapshot: Optional[CompoundingSnapshot] + compounds: int + + def __init__(self, ): + self.apy = D(5) / D(100) + self.balance_usd = D(0) + self.compounding_index = D(1.0) + self.lp_compounding_snapshot = None + self.compounds = 0 def balance_of(self) -> D: if self.lp_compounding_snapshot is None: @@ -81,10 +88,6 @@ def compound(self, days: int): # track compounds number self.compounds += days - # increase usd value - -vault = Vault() - class UserSnapshot: compounds: int snapshot_of_compounding_index: D @@ -94,13 +97,24 @@ def __init__(self, compounds: int, snapshot: D): self.snapshot_of_compounding_index = snapshot class LP: - balance_usd: D = D(0) - balance_token = D(0) - price: D = D(1) - minted: D = D(0) - liquidity: Dict[str, D] = {} - total_liquidity: D = D(0) - user_snapshot: Dict[str, UserSnapshot] = {} + balance_usd: D + balance_token: D + price: D + minted: D + liquidity: Dict[str, D] + total_liquidity: D + user_snapshot: Dict[str, UserSnapshot] + vault: Vault + + def __init__(self, vault: Vault): + self.balance_usd = D(0) + self.balance_token = D(0) + self.price = D(1) + self.minted = D(0) + self.liquidity = {} + self.total_liquidity = D(0) + self.user_snapshot = {} + self.vault = vault # use token to perform mint (in case of buy or inflation) def mint(self, amount: D): @@ -123,8 +137,8 @@ def add_liquidity(self, user: User, token_amount: D, usd_amount: D): # store compound day on user self.user_snapshot[user.name] = UserSnapshot( - vault.compounds, - vault.compounding_index + self.vault.compounds, + self.vault.compounding_index ) # compute liquidity @@ -137,7 +151,7 @@ def add_liquidity(self, user: User, token_amount: D, usd_amount: D): def remove_liquidity(self, user: User, liquidity_amount: D): # translate liquidity to token & usdc - compound_delta = vault.compounding_index / self.user_snapshot[user.name].snapshot_of_compounding_index + compound_delta = self.vault.compounding_index / self.user_snapshot[user.name].snapshot_of_compounding_index usd_deposit = liquidity_amount / 2 usd_yield = usd_deposit * (compound_delta - D(1)) * 2 @@ -195,7 +209,7 @@ def buy(self, user: User, amount: D): def rehypo(self, user: User): # add funds to vault - vault.add(self.balance_usd) + self.vault.add(self.balance_usd) # remove funds from lp self.balance_usd = D(0) @@ -222,38 +236,53 @@ def sell(self, user: User, amount: D): def dehypo(self, user: User, amount: D): # remove from vault - vault.remove(amount) + self.vault.remove(amount) # add to lp self.balance_usd += D(amount) # update user information -lp = LP() - -user_a = User("aaron", 1 * K) - -# buy tokens for 500 usd -lp.buy(user_a, D(500)) -assert lp.balance_usd == D(0) -assert vault.balance_usd == D(500) -assert vault.balance_of() == D(500) - -lp.add_liquidity(user_a, D(500), D(500)) -assert lp.balance_token == D(500) -assert lp.balance_usd == D(0) -assert vault.balance_usd == D(1_000) -assert vault.balance_of() == D(1_000) - -# compound for 100 days -vault.compound(100) -print(f"[100 days] Vault balance: {vault.balance_of()}") - -# remove liquidity -lp.remove_liquidity(user_a, lp.liquidity[user_a.name]) -print(f"[Liquidity removal] User USDC: {user_a.balance_usd}") -print(f"[Liquidity removal] User tokens: {user_a.balance_token}") -print(f"[Liquidity removal] LP tokens: {lp.balance_token}") -print(f"[Liquidity removal] LP USDC: {lp.balance_usd}") -print(f"[Liquidity removal] Vault balance of: {vault.balance_of()}") -print(f"[Liquidity removal] Vault USDC: {vault.balance_usd}") +def single_user_scenario( + user_initial_usd: D = 1 * K, + user_buy_token_usd: D = D(500), + user_add_liquidity_token: D = D(500), + user_add_liquidity_usd: D = D(500), + compound_days: int = 100, +): + vault = Vault() + lp = LP(vault) + user = User("aaron", user_initial_usd) + print(f"[Initial] User USDC: {user.balance_usd}") + + # buy tokens for 500 usd + lp.buy(user, user_buy_token_usd) + assert lp.balance_usd == D(0) + assert vault.balance_usd == user_buy_token_usd + assert vault.balance_of() == user_buy_token_usd + print(f"[Buy] User USDC: {user_buy_token_usd}") + print(f"[Buy] User tokens: {user.balance_token}") + + lp.add_liquidity(user, user_add_liquidity_token, user_add_liquidity_usd) + assert lp.balance_token == user_add_liquidity_token + assert lp.balance_usd == D(0) + assert vault.balance_usd == user_add_liquidity_usd + user_buy_token_usd + assert vault.balance_of() == user_add_liquidity_usd + user_buy_token_usd + print(f"[Liquidity add] User USDC: {user_add_liquidity_usd}") + print(f"[Liquidity add] User tokens: {user_add_liquidity_token}") + print(f"[Liquidity add] User liquidity: {lp.liquidity[user.name]}") + + # compound for 100 days + vault.compound(compound_days) + print(f"[{compound_days} days] Vault balance: {vault.balance_of()}") + + # remove liquidity + lp.remove_liquidity(user, lp.liquidity[user.name]) + print(f"[Liquidity removal] User USDC: {user.balance_usd}") + print(f"[Liquidity removal] User tokens: {user.balance_token}") + print(f"[Liquidity removal] LP tokens: {lp.balance_token}") + print(f"[Liquidity removal] LP USDC: {lp.balance_usd}") + print(f"[Liquidity removal] Vault balance of: {vault.balance_of()}") + print(f"[Liquidity removal] Vault USDC: {vault.balance_usd}") + +single_user_scenario() From 61eda30245d94d1672158f417ad1937d245d1862 Mon Sep 17 00:00:00 2001 From: Mc01 Date: Mon, 22 Dec 2025 00:00:16 +0100 Subject: [PATCH 04/15] Lp price curve. --- math/test_yield_model.py | 157 +++++++++++++++++++++++++++++++++------ 1 file changed, 135 insertions(+), 22 deletions(-) diff --git a/math/test_yield_model.py b/math/test_yield_model.py index dfff784..1d1993a 100644 --- a/math/test_yield_model.py +++ b/math/test_yield_model.py @@ -6,6 +6,9 @@ M = D(1_000_000) B = D(1_000_000_000) +# price movement amplification +EXPOSURE_FACTOR = 100 * K + # cap CAP = 1 * B @@ -99,22 +102,106 @@ def __init__(self, compounds: int, snapshot: D): class LP: balance_usd: D balance_token: D - price: D minted: D liquidity: Dict[str, D] total_liquidity: D user_snapshot: Dict[str, UserSnapshot] vault: Vault + k: Optional[D] + buy_usdc: D # USDC from buy operations (affects bonding curve) + lp_usdc: D # USDC from LP operations (yield only) + virtual_liquidity: D # Bootstrap virtual USDC for bonding curve def __init__(self, vault: Vault): self.balance_usd = D(0) self.balance_token = D(0) - self.price = D(1) self.minted = D(0) self.liquidity = {} self.total_liquidity = D(0) self.user_snapshot = {} self.vault = vault + self.k = None + self.buy_usdc = D(0) + self.lp_usdc = D(0) + self.virtual_liquidity = CAP / EXPOSURE_FACTOR # Bootstrap virtual liquidity + + def get_buy_usdc_with_yield(self) -> D: + """ + Get current buy_usdc value including compounded yield. + Buy USDC grows proportionally with total vault balance. + """ + if self.buy_usdc == 0 and self.lp_usdc == 0: + return D(0) + + total_principal = self.buy_usdc + self.lp_usdc + if total_principal == 0: + return D(0) + + # Buy USDC gets its share of vault yield + compound_ratio = self.vault.balance_of() / total_principal + return self.buy_usdc * compound_ratio + + @property + def price(self) -> D: + """Current token price: price = buy_usdc_with_yield / minted_tokens + Only buy USDC affects price, not LP USDC.""" + if self.minted == 0: + return D(1) # default price before any mints + return self.get_buy_usdc_with_yield() / self.minted + + def get_exposure(self) -> D: + """ + Dynamic exposure that decreases as more tokens are minted. + Reaches 0 at 1M tokens minted. + """ + # Amplify minting effect by 1000x to hit 0 at 1M tokens + effective_minted = min(self.minted * D(1000), CAP) + exposure = EXPOSURE_FACTOR * (D(1) - effective_minted / CAP) + return max(D(0), exposure) + + def _update_k(self): + """ + Update constant product invariant using virtual reserves with dynamic exposure: + k = (token_reserve) * (buy_usdc + virtual_liquidity) + Only buy_usdc affects bonding curve, not lp_usdc. + """ + exposure = self.get_exposure() + token_reserve = (CAP - self.minted) / exposure if exposure > 0 else CAP - self.minted + usdc_reserve = self.buy_usdc + self.virtual_liquidity + self.k = token_reserve * usdc_reserve + + def _get_out_amount(self, sold_amount: D, selling_token: bool) -> D: + """ + Calculate output using constant product with virtual reserves: + (token_reserve) * (buy_usdc + virtual_liquidity) = k + Uses dynamic exposure that decreases as more tokens are minted. + Only buy_usdc affects bonding curve. + """ + exposure = self.get_exposure() + + if self.k is None: + # First buy: initialize k with virtual liquidity + self.k = ((CAP - self.minted) / exposure if exposure > 0 else CAP - self.minted) * self.virtual_liquidity + + token_reserve = (CAP - self.minted) / exposure if exposure > 0 else CAP - self.minted + usdc_reserve = self.buy_usdc + self.virtual_liquidity + + if selling_token: + # Selling tokens, getting USDC (sell operation) + # User adds tokens back, removes USDC from buy pool + # (token_reserve + token_in) * (usdc_reserve - usdc_out) = k + new_token_reserve = token_reserve + sold_amount + new_usdc_reserve = self.k / new_token_reserve + usdc_out = usdc_reserve - new_usdc_reserve + return usdc_out + else: + # Buying tokens with USDC + # User adds USDC to buy pool, mints tokens + # (token_reserve - token_out) * (usdc_reserve + usdc_in) = k + new_usdc_reserve = usdc_reserve + sold_amount + new_token_reserve = self.k / new_usdc_reserve + token_out = token_reserve - new_token_reserve + return token_out # use token to perform mint (in case of buy or inflation) def mint(self, amount: D): @@ -132,9 +219,16 @@ def add_liquidity(self, user: User, token_amount: D, usd_amount: D): self.balance_token += token_amount self.balance_usd += usd_amount + # track LP USDC (does NOT affect bonding curve) + self.lp_usdc += usd_amount + # put usdc on vault for yield generation self.rehypo(user) + # initialize or update k on first liquidity add + if self.k is None: + self._update_k() + # store compound day on user self.user_snapshot[user.name] = UserSnapshot( self.vault.compounds, @@ -152,7 +246,7 @@ def add_liquidity(self, user: User, token_amount: D, usd_amount: D): def remove_liquidity(self, user: User, liquidity_amount: D): # translate liquidity to token & usdc compound_delta = self.vault.compounding_index / self.user_snapshot[user.name].snapshot_of_compounding_index - + usd_deposit = liquidity_amount / 2 usd_yield = usd_deposit * (compound_delta - D(1)) * 2 usd_amount = usd_deposit + usd_yield @@ -167,6 +261,9 @@ def remove_liquidity(self, user: User, liquidity_amount: D): # remove user usdc deposit & rewards from vault self.dehypo(user, usd_amount) + # reduce lp_usdc by the original LP principal + self.lp_usdc -= usd_deposit + # remove funds from lp self.balance_token -= token_amount self.balance_usd -= usd_amount @@ -184,29 +281,25 @@ def buy(self, user: User, amount: D): user.balance_usd -= amount self.balance_usd += amount - # compute out amount (token) - # 1_000 USD / 1 USD price -> 1_000 tokens - # 2_000 USD / 1 USD price -> 2_000 tokens - # 1_500 USD / 1.5 USD price -> 1_000 tokens - out_amount = amount / self.price + # compute out amount (token) using x*y=k + out_amount = self._get_out_amount(amount, selling_token=False) # mint as much token as needed - self.mint(out_amount - self.balance_token) - + self.mint(max(D(0), out_amount - self.balance_token)) + # give token self.balance_token -= out_amount user.balance_token += out_amount - # bump price - # in pool: 0 USD ; amount: 10_000 USD -> 0.1 UP (higher amount > pool) - # in pool: 100 USD ; amount: 100 USD -> 0.01 UP (equal amount to pool) - # in pool: 1000 USD ; amount: 100 USD -> 0.001 UP (smaller amount < pool) - # TODO: convert to price discovery (bonding curve) - self.price += D(0.1) + # track buy USDC (affects bonding curve) + self.buy_usdc += amount - # rehypo + # rehypo (deposits all USDC to vault) self.rehypo(user) + # update invariant + self._update_k() + def rehypo(self, user: User): # add funds to vault self.vault.add(self.balance_usd) @@ -221,18 +314,21 @@ def sell(self, user: User, amount: D): user.balance_token -= amount self.balance_token += amount - # compute amount in (usd) - in_amount = amount * self.price + # compute amount in (usd) using x*y=k + in_amount = self._get_out_amount(amount, selling_token=True) + + # update buy_usdc (reduces bonding curve reserve) + self.buy_usdc -= in_amount # dehypo self.dehypo(user, in_amount) + # give usd self.balance_usd -= in_amount user.balance_usd += in_amount - # deflate price - # TODO: convert to price discovery (bonding curve) - self.price -= D(0.1) + # update invariant after swap + self._update_k() def dehypo(self, user: User, amount: D): # remove from vault @@ -263,6 +359,12 @@ def single_user_scenario( print(f"[Buy] User USDC: {user_buy_token_usd}") print(f"[Buy] User tokens: {user.balance_token}") + # assert price after buy + assert lp.price > D(1), f"Price should be > 1, got {lp.price}" + print(f"[Buy] Token price: {lp.price}") + print(f"[Buy] Pool invariant k: {lp.k}") + + # add liquidity lp.add_liquidity(user, user_add_liquidity_token, user_add_liquidity_usd) assert lp.balance_token == user_add_liquidity_token assert lp.balance_usd == D(0) @@ -272,10 +374,19 @@ def single_user_scenario( print(f"[Liquidity add] User tokens: {user_add_liquidity_token}") print(f"[Liquidity add] User liquidity: {lp.liquidity[user.name]}") + price_after_add_liquidity = lp.price + print(f"[Liquidity add] Token price: {lp.price}") + print(f"[Liquidity add] Pool invariant k: {lp.k}") + # compound for 100 days vault.compound(compound_days) print(f"[{compound_days} days] Vault balance: {vault.balance_of()}") + # Price changes as vault balance grows (more USDC per token) + assert lp.price > price_after_add_liquidity, f"Price should increase as vault compounds, got {lp.price} vs {price_after_add_liquidity}" + print(f"[After compound] Token price: {lp.price}") + print(f"[After compound] Pool invariant k: {lp.k}") + # remove liquidity lp.remove_liquidity(user, lp.liquidity[user.name]) print(f"[Liquidity removal] User USDC: {user.balance_usd}") @@ -284,5 +395,7 @@ def single_user_scenario( print(f"[Liquidity removal] LP USDC: {lp.balance_usd}") print(f"[Liquidity removal] Vault balance of: {vault.balance_of()}") print(f"[Liquidity removal] Vault USDC: {vault.balance_usd}") + print(f"[Liquidity removal] Token price: {lp.price}") + print(f"[Liquidity removal] Pool invariant k: {lp.k}") single_user_scenario() From 4fc177b7b9a4bfd6a71809f1d6957fad286c2bec Mon Sep 17 00:00:00 2001 From: Mc01 Date: Mon, 22 Dec 2025 00:44:31 +0100 Subject: [PATCH 05/15] Cleanup. --- math/test_yield_model.py | 122 +++++++++++++++------------------------ 1 file changed, 48 insertions(+), 74 deletions(-) diff --git a/math/test_yield_model.py b/math/test_yield_model.py index 1d1993a..7ca34c8 100644 --- a/math/test_yield_model.py +++ b/math/test_yield_model.py @@ -3,7 +3,6 @@ # const K = D(1_000) -M = D(1_000_000) B = D(1_000_000_000) # price movement amplification @@ -24,87 +23,66 @@ def __init__(self, name: str, usd: D = D(0), token: D = D(0)): class CompoundingSnapshot: value: D - snapshot_of_compounding_index: D + index: D - def __init__(self, value: D, snapshot: D): + def __init__(self, value: D, index: D): self.value = value - self.snapshot_of_compounding_index = snapshot + self.index = index class Vault: apy: D balance_usd: D compounding_index: D - lp_compounding_snapshot: Optional[CompoundingSnapshot] + snapshot: Optional[CompoundingSnapshot] compounds: int - def __init__(self, ): + def __init__(self): self.apy = D(5) / D(100) self.balance_usd = D(0) self.compounding_index = D(1.0) - self.lp_compounding_snapshot = None + self.snapshot = None self.compounds = 0 def balance_of(self) -> D: - if self.lp_compounding_snapshot is None: + if self.snapshot is None: return self.balance_usd - else: - return self.lp_compounding_snapshot.value * ( - self.compounding_index / self.lp_compounding_snapshot.snapshot_of_compounding_index - ) + return self.snapshot.value * (self.compounding_index / self.snapshot.index) def add(self, value: D): - if self.lp_compounding_snapshot is None: - # store value as snapshot - self.lp_compounding_snapshot = CompoundingSnapshot( - value, - self.compounding_index - ) - else: - # we assume that compound has been already run - # store deposit + last deposit with rewards as snapshot - self.lp_compounding_snapshot = CompoundingSnapshot( - value + self.balance_of(), - self.compounding_index - ) - - # set balance in usd as deposit + last deposit with rewards + self.snapshot = CompoundingSnapshot( + value + self.balance_of(), + self.compounding_index + ) self.balance_usd = self.balance_of() def remove(self, value: D): - if self.lp_compounding_snapshot is None: + if self.snapshot is None: raise Exception("Nothing staked!") - else: - # store last deposit with rewards - withdrawal as snapshot - self.lp_compounding_snapshot = CompoundingSnapshot( - self.balance_of() - value, - self.compounding_index - ) - - # set balance in usd as last deposit with rewards - withdrawal + self.snapshot = CompoundingSnapshot( + self.balance_of() - value, + self.compounding_index + ) self.balance_usd = self.balance_of() def compound(self, days: int): # run compounding daily - for _ in range(0, days): + for _ in range(days): self.compounding_index *= D(1) + (self.apy / D(365)) - + # track compounds number self.compounds += days class UserSnapshot: - compounds: int - snapshot_of_compounding_index: D + index: D - def __init__(self, compounds: int, snapshot: D): - self.compounds = compounds - self.snapshot_of_compounding_index = snapshot + def __init__(self, index: D): + self.index = index class LP: balance_usd: D balance_token: D minted: D liquidity: Dict[str, D] - total_liquidity: D user_snapshot: Dict[str, UserSnapshot] vault: Vault k: Optional[D] @@ -117,7 +95,6 @@ def __init__(self, vault: Vault): self.balance_token = D(0) self.minted = D(0) self.liquidity = {} - self.total_liquidity = D(0) self.user_snapshot = {} self.vault = vault self.k = None @@ -155,20 +132,26 @@ def get_exposure(self) -> D: Reaches 0 at 1M tokens minted. """ # Amplify minting effect by 1000x to hit 0 at 1M tokens - effective_minted = min(self.minted * D(1000), CAP) - exposure = EXPOSURE_FACTOR * (D(1) - effective_minted / CAP) + effective = min(self.minted * D(1000), CAP) + exposure = EXPOSURE_FACTOR * (D(1) - effective / CAP) return max(D(0), exposure) + def _get_token_reserve(self) -> D: + """Virtual token reserve = (CAP - minted) / exposure""" + exposure = self.get_exposure() + return (CAP - self.minted) / exposure if exposure > 0 else CAP - self.minted + + def _get_usdc_reserve(self) -> D: + """Virtual USDC reserve = buy_usdc + virtual_liquidity""" + return self.buy_usdc + self.virtual_liquidity + def _update_k(self): """ Update constant product invariant using virtual reserves with dynamic exposure: k = (token_reserve) * (buy_usdc + virtual_liquidity) Only buy_usdc affects bonding curve, not lp_usdc. """ - exposure = self.get_exposure() - token_reserve = (CAP - self.minted) / exposure if exposure > 0 else CAP - self.minted - usdc_reserve = self.buy_usdc + self.virtual_liquidity - self.k = token_reserve * usdc_reserve + self.k = self._get_token_reserve() * self._get_usdc_reserve() def _get_out_amount(self, sold_amount: D, selling_token: bool) -> D: """ @@ -177,14 +160,12 @@ def _get_out_amount(self, sold_amount: D, selling_token: bool) -> D: Uses dynamic exposure that decreases as more tokens are minted. Only buy_usdc affects bonding curve. """ - exposure = self.get_exposure() - if self.k is None: # First buy: initialize k with virtual liquidity - self.k = ((CAP - self.minted) / exposure if exposure > 0 else CAP - self.minted) * self.virtual_liquidity + self.k = self._get_token_reserve() * self.virtual_liquidity - token_reserve = (CAP - self.minted) / exposure if exposure > 0 else CAP - self.minted - usdc_reserve = self.buy_usdc + self.virtual_liquidity + token_reserve = self._get_token_reserve() + usdc_reserve = self._get_usdc_reserve() if selling_token: # Selling tokens, getting USDC (sell operation) @@ -223,15 +204,14 @@ def add_liquidity(self, user: User, token_amount: D, usd_amount: D): self.lp_usdc += usd_amount # put usdc on vault for yield generation - self.rehypo(user) + self.rehypo() # initialize or update k on first liquidity add if self.k is None: self._update_k() - # store compound day on user + # snapshot compounding index self.user_snapshot[user.name] = UserSnapshot( - self.vault.compounds, self.vault.compounding_index ) @@ -241,25 +221,24 @@ def add_liquidity(self, user: User, token_amount: D, usd_amount: D): self.liquidity[user.name] = token_amount + usd_amount else: self.liquidity[user.name] += token_amount + usd_amount - self.total_liquidity += token_amount + usd_amount def remove_liquidity(self, user: User, liquidity_amount: D): # translate liquidity to token & usdc - compound_delta = self.vault.compounding_index / self.user_snapshot[user.name].snapshot_of_compounding_index + delta = self.vault.compounding_index / self.user_snapshot[user.name].index usd_deposit = liquidity_amount / 2 - usd_yield = usd_deposit * (compound_delta - D(1)) * 2 + usd_yield = usd_deposit * (delta - D(1)) * 2 usd_amount = usd_deposit + usd_yield token_deposit = liquidity_amount / 2 - token_yield = token_deposit * (compound_delta - D(1)) + token_yield = token_deposit * (delta - D(1)) token_amount = token_deposit + token_yield # mint inflation yield on tokens self.mint(token_yield) # remove user usdc deposit & rewards from vault - self.dehypo(user, usd_amount) + self.dehypo(usd_amount) # reduce lp_usdc by the original LP principal self.lp_usdc -= usd_deposit @@ -274,7 +253,6 @@ def remove_liquidity(self, user: User, liquidity_amount: D): # update liquidity self.liquidity[user.name] -= liquidity_amount - self.total_liquidity -= liquidity_amount def buy(self, user: User, amount: D): # take usd @@ -295,20 +273,18 @@ def buy(self, user: User, amount: D): self.buy_usdc += amount # rehypo (deposits all USDC to vault) - self.rehypo(user) + self.rehypo() # update invariant self._update_k() - def rehypo(self, user: User): + def rehypo(self): # add funds to vault self.vault.add(self.balance_usd) # remove funds from lp self.balance_usd = D(0) - # save user information - def sell(self, user: User, amount: D): # take token user.balance_token -= amount @@ -321,7 +297,7 @@ def sell(self, user: User, amount: D): self.buy_usdc -= in_amount # dehypo - self.dehypo(user, in_amount) + self.dehypo(in_amount) # give usd self.balance_usd -= in_amount @@ -330,14 +306,12 @@ def sell(self, user: User, amount: D): # update invariant after swap self._update_k() - def dehypo(self, user: User, amount: D): + def dehypo(self, amount: D): # remove from vault self.vault.remove(amount) # add to lp - self.balance_usd += D(amount) - - # update user information + self.balance_usd += amount def single_user_scenario( user_initial_usd: D = 1 * K, From 43bd5dee94775d15172f5d31bb1f2533eddc5e27 Mon Sep 17 00:00:00 2001 From: Mc01 Date: Mon, 22 Dec 2025 01:00:54 +0100 Subject: [PATCH 06/15] Fix scenario. --- math/test_yield_model.py | 44 ++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/math/test_yield_model.py b/math/test_yield_model.py index 7ca34c8..6d474dc 100644 --- a/math/test_yield_model.py +++ b/math/test_yield_model.py @@ -82,7 +82,8 @@ class LP: balance_usd: D balance_token: D minted: D - liquidity: Dict[str, D] + liquidity_token: Dict[str, D] + liquidity_usd: Dict[str, D] user_snapshot: Dict[str, UserSnapshot] vault: Vault k: Optional[D] @@ -94,7 +95,8 @@ def __init__(self, vault: Vault): self.balance_usd = D(0) self.balance_token = D(0) self.minted = D(0) - self.liquidity = {} + self.liquidity_token = {} + self.liquidity_usd = {} self.user_snapshot = {} self.vault = vault self.k = None @@ -216,21 +218,20 @@ def add_liquidity(self, user: User, token_amount: D, usd_amount: D): ) # compute liquidity - user_liquidity = self.liquidity.get(user.name) - if user_liquidity is None: - self.liquidity[user.name] = token_amount + usd_amount - else: - self.liquidity[user.name] += token_amount + usd_amount + self.liquidity_token[user.name] = self.liquidity_token.get(user.name, D(0)) + token_amount + self.liquidity_usd[user.name] = self.liquidity_usd.get(user.name, D(0)) + usd_amount + + def remove_liquidity(self, user: User): + # get user's deposited amounts + token_deposit = self.liquidity_token[user.name] + usd_deposit = self.liquidity_usd[user.name] - def remove_liquidity(self, user: User, liquidity_amount: D): - # translate liquidity to token & usdc + # calculate yield based on compounding delta = self.vault.compounding_index / self.user_snapshot[user.name].index - usd_deposit = liquidity_amount / 2 - usd_yield = usd_deposit * (delta - D(1)) * 2 + usd_yield = usd_deposit * (delta - D(1)) usd_amount = usd_deposit + usd_yield - token_deposit = liquidity_amount / 2 token_yield = token_deposit * (delta - D(1)) token_amount = token_deposit + token_yield @@ -251,8 +252,9 @@ def remove_liquidity(self, user: User, liquidity_amount: D): user.balance_token += token_amount user.balance_usd += usd_amount - # update liquidity - self.liquidity[user.name] -= liquidity_amount + # clear user liquidity + del self.liquidity_token[user.name] + del self.liquidity_usd[user.name] def buy(self, user: User, amount: D): # take usd @@ -316,8 +318,6 @@ def dehypo(self, amount: D): def single_user_scenario( user_initial_usd: D = 1 * K, user_buy_token_usd: D = D(500), - user_add_liquidity_token: D = D(500), - user_add_liquidity_usd: D = D(500), compound_days: int = 100, ): vault = Vault() @@ -338,7 +338,9 @@ def single_user_scenario( print(f"[Buy] Token price: {lp.price}") print(f"[Buy] Pool invariant k: {lp.k}") - # add liquidity + # add liquidity symmetrically: match token value at current price + user_add_liquidity_token = user.balance_token + user_add_liquidity_usd = user_add_liquidity_token * lp.price lp.add_liquidity(user, user_add_liquidity_token, user_add_liquidity_usd) assert lp.balance_token == user_add_liquidity_token assert lp.balance_usd == D(0) @@ -346,8 +348,10 @@ def single_user_scenario( assert vault.balance_of() == user_add_liquidity_usd + user_buy_token_usd print(f"[Liquidity add] User USDC: {user_add_liquidity_usd}") print(f"[Liquidity add] User tokens: {user_add_liquidity_token}") - print(f"[Liquidity add] User liquidity: {lp.liquidity[user.name]}") + print(f"[Liquidity add] User liquidity tokens: {lp.liquidity_token[user.name]}") + print(f"[Liquidity add] User liquidity USDC: {lp.liquidity_usd[user.name]}") + # snapshot price after adding liquidity price_after_add_liquidity = lp.price print(f"[Liquidity add] Token price: {lp.price}") print(f"[Liquidity add] Pool invariant k: {lp.k}") @@ -356,13 +360,13 @@ def single_user_scenario( vault.compound(compound_days) print(f"[{compound_days} days] Vault balance: {vault.balance_of()}") - # Price changes as vault balance grows (more USDC per token) + # price changes as vault balance grows (more USDC per token) assert lp.price > price_after_add_liquidity, f"Price should increase as vault compounds, got {lp.price} vs {price_after_add_liquidity}" print(f"[After compound] Token price: {lp.price}") print(f"[After compound] Pool invariant k: {lp.k}") # remove liquidity - lp.remove_liquidity(user, lp.liquidity[user.name]) + lp.remove_liquidity(user) print(f"[Liquidity removal] User USDC: {user.balance_usd}") print(f"[Liquidity removal] User tokens: {user.balance_token}") print(f"[Liquidity removal] LP tokens: {lp.balance_token}") From 3632cdad18da480fbfd1f30de350a17e5879e1d4 Mon Sep 17 00:00:00 2001 From: Mc01 Date: Mon, 22 Dec 2025 01:17:40 +0100 Subject: [PATCH 07/15] Fix yield math. --- math/test_yield_model.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/math/test_yield_model.py b/math/test_yield_model.py index 6d474dc..7f97049 100644 --- a/math/test_yield_model.py +++ b/math/test_yield_model.py @@ -84,6 +84,7 @@ class LP: minted: D liquidity_token: Dict[str, D] liquidity_usd: Dict[str, D] + user_buy_usdc: Dict[str, D] user_snapshot: Dict[str, UserSnapshot] vault: Vault k: Optional[D] @@ -97,6 +98,7 @@ def __init__(self, vault: Vault): self.minted = D(0) self.liquidity_token = {} self.liquidity_usd = {} + self.user_buy_usdc = {} self.user_snapshot = {} self.vault = vault self.k = None @@ -225,36 +227,48 @@ def remove_liquidity(self, user: User): # get user's deposited amounts token_deposit = self.liquidity_token[user.name] usd_deposit = self.liquidity_usd[user.name] + buy_usdc_principal = self.user_buy_usdc.get(user.name, D(0)) # calculate yield based on compounding delta = self.vault.compounding_index / self.user_snapshot[user.name].index + # LP USDC yield (5% APY) usd_yield = usd_deposit * (delta - D(1)) usd_amount = usd_deposit + usd_yield + # LP token inflation (5% APY) token_yield = token_deposit * (delta - D(1)) token_amount = token_deposit + token_yield + # Buy USDC yield (5% APY) + buy_usdc_yield = buy_usdc_principal * (delta - D(1)) + total_usdc = usd_amount + buy_usdc_yield + # mint inflation yield on tokens self.mint(token_yield) - # remove user usdc deposit & rewards from vault - self.dehypo(usd_amount) + # remove LP USDC + buy USDC yield from vault + self.dehypo(total_usdc) # reduce lp_usdc by the original LP principal self.lp_usdc -= usd_deposit + # reduce buy_usdc by the yield (principal stays for bonding curve) + self.buy_usdc -= buy_usdc_yield + # remove funds from lp self.balance_token -= token_amount - self.balance_usd -= usd_amount + self.balance_usd -= total_usdc # send funds to user user.balance_token += token_amount - user.balance_usd += usd_amount + user.balance_usd += total_usdc # clear user liquidity del self.liquidity_token[user.name] del self.liquidity_usd[user.name] + if user.name in self.user_buy_usdc: + del self.user_buy_usdc[user.name] def buy(self, user: User, amount: D): # take usd @@ -274,6 +288,9 @@ def buy(self, user: User, amount: D): # track buy USDC (affects bonding curve) self.buy_usdc += amount + # track USDC used to buy tokens + self.user_buy_usdc[user.name] = self.user_buy_usdc.get(user.name, D(0)) + amount + # rehypo (deposits all USDC to vault) self.rehypo() From cd0828f12c346fbaa062e8afc1e269997accbb39 Mon Sep 17 00:00:00 2001 From: Mc01 Date: Mon, 22 Dec 2025 01:46:52 +0100 Subject: [PATCH 08/15] Burn & test sell. --- math/test_yield_model.py | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/math/test_yield_model.py b/math/test_yield_model.py index 7f97049..7b26f61 100644 --- a/math/test_yield_model.py +++ b/math/test_yield_model.py @@ -307,20 +307,22 @@ def rehypo(self): def sell(self, user: User, amount: D): # take token user.balance_token -= amount - self.balance_token += amount - # compute amount in (usd) using x*y=k - in_amount = self._get_out_amount(amount, selling_token=True) + # burn tokens + self.minted -= amount + + # compute amount out (usd) using x*y=k + out_amount = self._get_out_amount(amount, selling_token=True) # update buy_usdc (reduces bonding curve reserve) - self.buy_usdc -= in_amount + self.buy_usdc -= out_amount # dehypo - self.dehypo(in_amount) + self.dehypo(out_amount) # give usd - self.balance_usd -= in_amount - user.balance_usd += in_amount + self.balance_usd -= out_amount + user.balance_usd += out_amount # update invariant after swap self._update_k() @@ -393,4 +395,17 @@ def single_user_scenario( print(f"[Liquidity removal] Token price: {lp.price}") print(f"[Liquidity removal] Pool invariant k: {lp.k}") + # sell all tokens + user_tokens = user.balance_token + lp.sell(user, user_tokens) + print(f"[Sell] User sold tokens: {user_tokens}") + print(f"[Sell] User USDC: {user.balance_usd}") + print(f"[Sell] User tokens: {user.balance_token}") + print(f"[Sell] LP tokens: {lp.balance_token}") + print(f"[Sell] LP USDC: {lp.balance_usd}") + print(f"[Sell] Vault balance of: {vault.balance_of()}") + print(f"[Sell] Vault USDC: {vault.balance_usd}") + print(f"[Sell] Token price: {lp.price}") + print(f"[Sell] Pool invariant k: {lp.k}") + single_user_scenario() From e5819f31fb26393d576e688a131da9a7bcb078fd Mon Sep 17 00:00:00 2001 From: Mc01 Date: Mon, 22 Dec 2025 15:31:32 +0100 Subject: [PATCH 09/15] Use deflationary virtual liquidity for USDC. --- math/test_yield_model.py | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/math/test_yield_model.py b/math/test_yield_model.py index 7b26f61..bc77bad 100644 --- a/math/test_yield_model.py +++ b/math/test_yield_model.py @@ -11,6 +11,9 @@ # cap CAP = 1 * B +# max USDC before virtual liquidity vanishes +VIRTUAL_LIMIT = 100 * K + class User: name: str balance_usd: D @@ -124,10 +127,17 @@ def get_buy_usdc_with_yield(self) -> D: @property def price(self) -> D: - """Current token price: price = buy_usdc_with_yield / minted_tokens + """Current token price calculated from bonding curve reserves. + When tokens exist: price = buy_usdc_with_yield / minted + When no tokens: price = usdc_reserve / token_reserve (marginal price) Only buy USDC affects price, not LP USDC.""" if self.minted == 0: - return D(1) # default price before any mints + # Calculate marginal price from bonding curve + token_reserve = self._get_token_reserve() + usdc_reserve = self._get_usdc_reserve() + if token_reserve == 0: + return D(1) # fallback if no reserves + return usdc_reserve / token_reserve return self.get_buy_usdc_with_yield() / self.minted def get_exposure(self) -> D: @@ -140,14 +150,24 @@ def get_exposure(self) -> D: exposure = EXPOSURE_FACTOR * (D(1) - effective / CAP) return max(D(0), exposure) + def get_virtual_liquidity(self) -> D: + """ + Dynamic virtual liquidity that decreases as more USDC is added. + Reaches 0 at 100K USDC. + """ + base = CAP / EXPOSURE_FACTOR # 10,000 + effective = min(self.buy_usdc, VIRTUAL_LIMIT) + liquidity = base * (D(1) - effective / VIRTUAL_LIMIT) + return max(D(0), liquidity) + def _get_token_reserve(self) -> D: """Virtual token reserve = (CAP - minted) / exposure""" exposure = self.get_exposure() return (CAP - self.minted) / exposure if exposure > 0 else CAP - self.minted def _get_usdc_reserve(self) -> D: - """Virtual USDC reserve = buy_usdc + virtual_liquidity""" - return self.buy_usdc + self.virtual_liquidity + """Virtual USDC reserve = buy_usdc + dynamic virtual_liquidity""" + return self.buy_usdc + self.get_virtual_liquidity() def _update_k(self): """ @@ -165,8 +185,8 @@ def _get_out_amount(self, sold_amount: D, selling_token: bool) -> D: Only buy_usdc affects bonding curve. """ if self.k is None: - # First buy: initialize k with virtual liquidity - self.k = self._get_token_reserve() * self.virtual_liquidity + # First buy: initialize k with dynamic virtual liquidity + self.k = self._get_token_reserve() * self.get_virtual_liquidity() token_reserve = self._get_token_reserve() usdc_reserve = self._get_usdc_reserve() From 6c77450eacc4e403f18a552a0247fa758e390995 Mon Sep 17 00:00:00 2001 From: Mc01 Date: Mon, 22 Dec 2025 17:22:46 +0100 Subject: [PATCH 10/15] Add multi-user scenario & fix price calculation. --- math/test_yield_model.py | 168 +++++++++++++++++++++++++++++++++++---- 1 file changed, 153 insertions(+), 15 deletions(-) diff --git a/math/test_yield_model.py b/math/test_yield_model.py index bc77bad..f5e566d 100644 --- a/math/test_yield_model.py +++ b/math/test_yield_model.py @@ -127,18 +127,15 @@ def get_buy_usdc_with_yield(self) -> D: @property def price(self) -> D: - """Current token price calculated from bonding curve reserves. - When tokens exist: price = buy_usdc_with_yield / minted - When no tokens: price = usdc_reserve / token_reserve (marginal price) + """Current token price calculated from bonding curve marginal price. + price = usdc_reserve / token_reserve + This is the instantaneous price for the next token based on x*y=k curve. Only buy USDC affects price, not LP USDC.""" - if self.minted == 0: - # Calculate marginal price from bonding curve - token_reserve = self._get_token_reserve() - usdc_reserve = self._get_usdc_reserve() - if token_reserve == 0: - return D(1) # fallback if no reserves - return usdc_reserve / token_reserve - return self.get_buy_usdc_with_yield() / self.minted + token_reserve = self._get_token_reserve() + usdc_reserve = self._get_usdc_reserve() + if token_reserve == 0: + return D(1) # fallback if no reserves + return usdc_reserve / token_reserve def get_exposure(self) -> D: """ @@ -166,8 +163,9 @@ def _get_token_reserve(self) -> D: return (CAP - self.minted) / exposure if exposure > 0 else CAP - self.minted def _get_usdc_reserve(self) -> D: - """Virtual USDC reserve = buy_usdc + dynamic virtual_liquidity""" - return self.buy_usdc + self.get_virtual_liquidity() + """Virtual USDC reserve = buy_usdc_with_yield + dynamic virtual_liquidity + Includes compounded yield so price increases as vault compounds.""" + return self.get_buy_usdc_with_yield() + self.get_virtual_liquidity() def _update_k(self): """ @@ -298,8 +296,8 @@ def buy(self, user: User, amount: D): # compute out amount (token) using x*y=k out_amount = self._get_out_amount(amount, selling_token=False) - # mint as much token as needed - self.mint(max(D(0), out_amount - self.balance_token)) + # always mint new tokens for buy operations (don't use LP tokens) + self.mint(out_amount) # give token self.balance_token -= out_amount @@ -428,4 +426,144 @@ def single_user_scenario( print(f"[Sell] Token price: {lp.price}") print(f"[Sell] Pool invariant k: {lp.k}") +def multi_user_scenario( + aaron_buy_usd: D = D(500), + bob_buy_usd: D = D(400), + carl_buy_usd: D = D(300), + dennis_buy_usd: D = D(600), + compound_interval: int = 50, +): + vault = Vault() + lp = LP(vault) + aaron = User("aaron", 2 * K) + bob = User("bob", 2 * K) + carl = User("carl", 2 * K) + dennis = User("dennis", 2 * K) + print(f"\n=== MULTI-USER SCENARIO ===") + + print(f"[Initial] Aaron USDC: {aaron.balance_usd}") + print(f"[Initial] Bob USDC: {bob.balance_usd}") + print(f"[Initial] Carl USDC: {carl.balance_usd}") + print(f"[Initial] Dennis USDC: {dennis.balance_usd}") + + # aaron buys tokens for 500 usd + lp.buy(aaron, aaron_buy_usd) + assert vault.balance_of() == aaron_buy_usd + print(f"[Aaron Buy] Aaron tokens: {aaron.balance_token}") + print(f"[Aaron Buy] Token price: {lp.price}") + print(f"[Aaron Buy] Vault balance: {vault.balance_of()}") + + # aaron adds liquidity symmetrically + aaron_add_liquidity_token = aaron.balance_token + aaron_add_liquidity_usd = aaron_add_liquidity_token * lp.price + lp.add_liquidity(aaron, aaron_add_liquidity_token, aaron_add_liquidity_usd) + assert lp.liquidity_token[aaron.name] == aaron_add_liquidity_token + print(f"[Aaron LP] Aaron liquidity tokens: {lp.liquidity_token[aaron.name]}") + print(f"[Aaron LP] Aaron liquidity USDC: {lp.liquidity_usd[aaron.name]}") + print(f"[Aaron LP] Vault balance: {vault.balance_of()}") + + # bob buys tokens for 400 usd + lp.buy(bob, bob_buy_usd) + print(f"[Bob Buy] Bob tokens: {bob.balance_token}") + print(f"[Bob Buy] Token price: {lp.price}") + print(f"[Bob Buy] Vault balance: {vault.balance_of()}") + + # bob adds liquidity symmetrically + bob_add_liquidity_token = bob.balance_token + bob_add_liquidity_usd = bob_add_liquidity_token * lp.price + lp.add_liquidity(bob, bob_add_liquidity_token, bob_add_liquidity_usd) + assert lp.liquidity_token[bob.name] == bob_add_liquidity_token + print(f"[Bob LP] Bob liquidity tokens: {lp.liquidity_token[bob.name]}") + print(f"[Bob LP] Bob liquidity USDC: {lp.liquidity_usd[bob.name]}") + print(f"[Bob LP] Vault balance: {vault.balance_of()}") + + # carl buys tokens for 300 usd + lp.buy(carl, carl_buy_usd) + print(f"[Carl Buy] Carl tokens: {carl.balance_token}") + print(f"[Carl Buy] Token price: {lp.price}") + print(f"[Carl Buy] Vault balance: {vault.balance_of()}") + + # carl adds liquidity symmetrically + carl_add_liquidity_token = carl.balance_token + carl_add_liquidity_usd = carl_add_liquidity_token * lp.price + lp.add_liquidity(carl, carl_add_liquidity_token, carl_add_liquidity_usd) + assert lp.liquidity_token[carl.name] == carl_add_liquidity_token + print(f"[Carl LP] Carl liquidity tokens: {lp.liquidity_token[carl.name]}") + print(f"[Carl LP] Carl liquidity USDC: {lp.liquidity_usd[carl.name]}") + print(f"[Carl LP] Vault balance: {vault.balance_of()}") + + # dennis buys tokens for 600 usd + lp.buy(dennis, dennis_buy_usd) + print(f"[Dennis Buy] Dennis tokens: {dennis.balance_token}") + print(f"[Dennis Buy] Token price: {lp.price}") + print(f"[Dennis Buy] Vault balance: {vault.balance_of()}") + + # dennis adds liquidity symmetrically + dennis_add_liquidity_token = dennis.balance_token + dennis_add_liquidity_usd = dennis_add_liquidity_token * lp.price + lp.add_liquidity(dennis, dennis_add_liquidity_token, dennis_add_liquidity_usd) + assert lp.liquidity_token[dennis.name] == dennis_add_liquidity_token + print(f"[Dennis LP] Dennis liquidity tokens: {lp.liquidity_token[dennis.name]}") + print(f"[Dennis LP] Dennis liquidity USDC: {lp.liquidity_usd[dennis.name]}") + print(f"[Dennis LP] Vault balance: {vault.balance_of()}") + print(f"[Dennis LP] Pool tokens: {lp.balance_token}") + print(f"[Dennis LP] Minted tokens: {lp.minted}") + + # compound for 50 days + vault.compound(compound_interval) + print(f"[{compound_interval} days] Vault balance: {vault.balance_of()}") + print(f"[{compound_interval} days] Token price: {lp.price}") + + # aaron removes liquidity (staked 50 days) + aaron_usdc_before = aaron.balance_usd + lp.remove_liquidity(aaron) + print(f"[Aaron removal] Aaron USDC: {aaron.balance_usd}") + print(f"[Aaron removal] Aaron USDC gain: {aaron.balance_usd - aaron_usdc_before}") + print(f"[Aaron removal] Aaron tokens: {aaron.balance_token}") + print(f"[Aaron removal] Vault balance: {vault.balance_of()}") + print(f"[Aaron removal] Token price: {lp.price}") + + # compound for another 50 days + vault.compound(compound_interval) + print(f"[{compound_interval*2} days] Vault balance: {vault.balance_of()}") + print(f"[{compound_interval*2} days] Token price: {lp.price}") + + # bob removes liquidity (staked 100 days) + bob_usdc_before = bob.balance_usd + lp.remove_liquidity(bob) + print(f"[Bob removal] Bob USDC: {bob.balance_usd}") + print(f"[Bob removal] Bob USDC gain: {bob.balance_usd - bob_usdc_before}") + print(f"[Bob removal] Bob tokens: {bob.balance_token}") + print(f"[Bob removal] Vault balance: {vault.balance_of()}") + print(f"[Bob removal] Token price: {lp.price}") + + # compound for another 50 days + vault.compound(compound_interval) + print(f"[{compound_interval*3} days] Vault balance: {vault.balance_of()}") + print(f"[{compound_interval*3} days] Token price: {lp.price}") + + # carl removes liquidity (staked 150 days) + carl_usdc_before = carl.balance_usd + lp.remove_liquidity(carl) + print(f"[Carl removal] Carl USDC: {carl.balance_usd}") + print(f"[Carl removal] Carl USDC gain: {carl.balance_usd - carl_usdc_before}") + print(f"[Carl removal] Carl tokens: {carl.balance_token}") + print(f"[Carl removal] Vault balance: {vault.balance_of()}") + print(f"[Carl removal] Token price: {lp.price}") + + # compound for another 50 days + vault.compound(compound_interval) + print(f"[{compound_interval*4} days] Vault balance: {vault.balance_of()}") + print(f"[{compound_interval*4} days] Token price: {lp.price}") + + # dennis removes liquidity (staked 200 days - longest) + dennis_usdc_before = dennis.balance_usd + lp.remove_liquidity(dennis) + print(f"[Dennis removal] Dennis USDC: {dennis.balance_usd}") + print(f"[Dennis removal] Dennis USDC gain: {dennis.balance_usd - dennis_usdc_before}") + print(f"[Dennis removal] Dennis tokens: {dennis.balance_token}") + print(f"[Dennis removal] Vault balance: {vault.balance_of()}") + print(f"[Dennis removal] Token price: {lp.price}") + single_user_scenario() +multi_user_scenario() From 512f3118bf09563a59b6420fd0bec27e83b364ed Mon Sep 17 00:00:00 2001 From: Mc01 Date: Mon, 22 Dec 2025 21:15:26 +0100 Subject: [PATCH 11/15] Fine tuning & pretty printing. --- math/test_yield_model.py | 495 ++++++++++++++++++++++++++++++++------- 1 file changed, 414 insertions(+), 81 deletions(-) diff --git a/math/test_yield_model.py b/math/test_yield_model.py index f5e566d..27c0ae0 100644 --- a/math/test_yield_model.py +++ b/math/test_yield_model.py @@ -14,6 +14,20 @@ # max USDC before virtual liquidity vanishes VIRTUAL_LIMIT = 100 * K +# ANSI color codes +class Color: + HEADER = '\033[95m' # Magenta + BLUE = '\033[94m' # Blue + CYAN = '\033[96m' # Cyan + GREEN = '\033[92m' # Green + YELLOW = '\033[93m' # Yellow + RED = '\033[91m' # Red + BOLD = '\033[1m' # Bold + UNDERLINE = '\033[4m' # Underline + DIM = '\033[2m' # Dim/faint + STATS = '\033[90m' # Gray (for technical stats) + END = '\033[0m' # Reset + class User: name: str balance_usd: D @@ -150,12 +164,18 @@ def get_exposure(self) -> D: def get_virtual_liquidity(self) -> D: """ Dynamic virtual liquidity that decreases as more USDC is added. - Reaches 0 at 100K USDC. + Floor at 0 USDC, ceiling at 100K USDC. """ base = CAP / EXPOSURE_FACTOR # 10,000 effective = min(self.buy_usdc, VIRTUAL_LIMIT) liquidity = base * (D(1) - effective / VIRTUAL_LIMIT) - return max(D(0), liquidity) + + # Floor >= 1: buy_usdc + virtual_liquidity >= token_reserve + token_reserve = self._get_token_reserve() + floor_liquidity = token_reserve - self.buy_usdc + + # Use the higher of liquidity or floor requirement + return max(D(0), liquidity, floor_liquidity) def _get_token_reserve(self) -> D: """Virtual token reserve = (CAP - minted) / exposure""" @@ -175,12 +195,58 @@ def _update_k(self): """ self.k = self._get_token_reserve() * self._get_usdc_reserve() + def _apply_fair_share_cap(self, requested_amount: D, user_fraction: D) -> D: + """ + Apply fair share cap to prevent bank runs. + Returns capped amount based on user's fraction of vault. + + Args: + requested_amount: Amount user would get from bonding curve + user_fraction: User's fraction of total pool (0 to 1) + + Returns: + Capped amount: min(requested_amount, fair_share, vault_available) + """ + vault_available = self.vault.balance_of() + fair_share = user_fraction * vault_available + return min(requested_amount, fair_share, vault_available) + + def _get_fair_share_scaling(self, requested_total_usdc: D, user_principal: D, total_principal: D) -> D: + """ + Calculate fair share scaling factor for withdrawals. + Returns a scaling factor between 0 and 1 to apply proportionally to USDC and tokens. + + Args: + requested_total_usdc: Total USDC user would get with full yield + user_principal: User's principal (LP USDC + buy USDC) + total_principal: Total principal in pool (all users' LP USDC + buy USDC) + + Returns: + Scaling factor (0 to 1) to apply to both USDC and token withdrawals + """ + vault_available = self.vault.balance_of() + + if total_principal > 0 and requested_total_usdc > 0: + fraction = user_principal / total_principal + fair_share = fraction * vault_available + # scale down if either fair_share or vault_available is insufficient + scaling_factor = min(D(1), fair_share / requested_total_usdc, vault_available / requested_total_usdc) + elif requested_total_usdc > 0: + # no principal tracked, just cap at vault available + scaling_factor = min(D(1), vault_available / requested_total_usdc) + else: + # no USDC requested, no scaling needed + scaling_factor = D(1) + + return scaling_factor + def _get_out_amount(self, sold_amount: D, selling_token: bool) -> D: """ Calculate output using constant product with virtual reserves: (token_reserve) * (buy_usdc + virtual_liquidity) = k Uses dynamic exposure that decreases as more tokens are minted. - Only buy_usdc affects bonding curve. + Only buy_usdc affects bonding curve, not lp_usdc. + Applies quadratic vault scaling on sells to prevent depletion. """ if self.k is None: # First buy: initialize k with dynamic virtual liquidity @@ -195,8 +261,16 @@ def _get_out_amount(self, sold_amount: D, selling_token: bool) -> D: # (token_reserve + token_in) * (usdc_reserve - usdc_out) = k new_token_reserve = token_reserve + sold_amount new_usdc_reserve = self.k / new_token_reserve - usdc_out = usdc_reserve - new_usdc_reserve - return usdc_out + usdc_out_curve = usdc_reserve - new_usdc_reserve + + if self.minted == 0: + # All tokens sold - use bonding curve with virtual reserves, no fair share scaling + vault_available = self.vault.balance_of() + return min(usdc_out_curve, vault_available) + + # apply fair share cap to prevent bank run + user_fraction = sold_amount / self.minted + return self._apply_fair_share_cap(usdc_out_curve, user_fraction) else: # Buying tokens with USDC # User adds USDC to buy pool, mints tokens @@ -206,7 +280,6 @@ def _get_out_amount(self, sold_amount: D, selling_token: bool) -> D: token_out = token_reserve - new_token_reserve return token_out - # use token to perform mint (in case of buy or inflation) def mint(self, amount: D): if self.minted + amount > CAP: raise Exception("Cannot mint over cap") @@ -252,27 +325,37 @@ def remove_liquidity(self, user: User): # LP USDC yield (5% APY) usd_yield = usd_deposit * (delta - D(1)) - usd_amount = usd_deposit + usd_yield + usd_amount_full = usd_deposit + usd_yield # LP token inflation (5% APY) - token_yield = token_deposit * (delta - D(1)) - token_amount = token_deposit + token_yield + token_yield_full = token_deposit * (delta - D(1)) # Buy USDC yield (5% APY) - buy_usdc_yield = buy_usdc_principal * (delta - D(1)) - total_usdc = usd_amount + buy_usdc_yield + buy_usdc_yield_full = buy_usdc_principal * (delta - D(1)) + total_usdc_full = usd_amount_full + buy_usdc_yield_full + + # calculate fair share scaling factor + principal = usd_deposit + buy_usdc_principal + total_principal = self.lp_usdc + self.buy_usdc + scaling_factor = self._get_fair_share_scaling(total_usdc_full, principal, total_principal) - # mint inflation yield on tokens + # apply scaling to both USDC and tokens proportionally + total_usdc = total_usdc_full * scaling_factor + token_yield = token_yield_full * scaling_factor + token_amount = token_deposit + token_yield + + # mint scaled inflation yield on tokens self.mint(token_yield) - # remove LP USDC + buy USDC yield from vault + # remove scaled USDC from vault self.dehypo(total_usdc) # reduce lp_usdc by the original LP principal self.lp_usdc -= usd_deposit - # reduce buy_usdc by the yield (principal stays for bonding curve) - self.buy_usdc -= buy_usdc_yield + # reduce buy_usdc by scaled buy yield + buy_usdc_yield_actual = buy_usdc_yield_full * scaling_factor + self.buy_usdc -= buy_usdc_yield_actual # remove funds from lp self.balance_token -= token_amount @@ -352,6 +435,48 @@ def dehypo(self, amount: D): # add to lp self.balance_usd += amount + def print_stats(self, label: str = "Stats"): + """Print detailed mathematical statistics for nerds""" + # Use CYAN which is visible on both light and dark backgrounds + print(f"\n{Color.CYAN} โ”Œโ”€ ๐Ÿ“Š {label} (Math Under the Hood) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€{Color.END}") + + # Virtual reserves + token_reserve = self._get_token_reserve() + usdc_reserve = self._get_usdc_reserve() + print(f"{Color.CYAN} โ”‚ Virtual Reserves: token={Color.YELLOW}{token_reserve:.2f}{Color.END}, usdc={Color.YELLOW}{usdc_reserve:.2f}{Color.END}") + + # Bonding curve constant + k_value = f"{self.k:.2f}" if self.k else "None" + print(f"{Color.CYAN} โ”‚ Bonding Curve k = {Color.YELLOW}{k_value}{Color.END}") + + # Dynamic factors + exposure = self.get_exposure() + virtual_liq = self.get_virtual_liquidity() + buy_usdc_with_yield = self.get_buy_usdc_with_yield() + print(f"{Color.CYAN} โ”‚ Exposure Factor: {Color.YELLOW}{exposure:.2f}{Color.END} (decreases as tokens mint)") + print(f"{Color.CYAN} โ”‚ Virtual Liquidity: {Color.YELLOW}{virtual_liq:.2f}{Color.END} (decreases as USDC added)") + + # USDC tracking + total_principal = self.buy_usdc + self.lp_usdc + buy_ratio = (self.buy_usdc / total_principal * 100) if total_principal > 0 else D(0) + lp_ratio = (self.lp_usdc / total_principal * 100) if total_principal > 0 else D(0) + print(f"{Color.CYAN} โ”‚ USDC Split: buy={Color.YELLOW}{self.buy_usdc:.2f}{Color.END} ({buy_ratio:.1f}%), lp={Color.YELLOW}{self.lp_usdc:.2f}{Color.END} ({lp_ratio:.1f}%)") + print(f"{Color.CYAN} โ”‚ Buy USDC (w/yield): {Color.YELLOW}{buy_usdc_with_yield:.2f}{Color.END}") + + # Vault & compounding + print(f"{Color.CYAN} โ”‚ Vault Balance: {Color.YELLOW}{self.vault.balance_of():.2f}{Color.END}") + print(f"{Color.CYAN} โ”‚ Vault Index: {Color.YELLOW}{self.vault.compounding_index:.6f}{Color.END} ({self.vault.compounds} days)") + + # Price calculation breakdown + if token_reserve > 0: + print(f"{Color.CYAN} โ”‚ Price = usdc_reserve/token_reserve = {Color.YELLOW}{usdc_reserve:.2f}{Color.END}/{Color.YELLOW}{token_reserve:.2f}{Color.END} = {Color.GREEN}{self.price:.6f}{Color.END}") + + # Minted vs Cap + mint_pct = (self.minted / CAP * 100) if CAP > 0 else D(0) + print(f"{Color.CYAN} โ”‚ Minted: {Color.YELLOW}{self.minted:.2f}{Color.END} / {CAP} ({mint_pct:.4f}%)") + + print(f"{Color.CYAN} โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€{Color.END}\n") + def single_user_scenario( user_initial_usd: D = 1 * K, user_buy_token_usd: D = D(500), @@ -360,22 +485,32 @@ def single_user_scenario( vault = Vault() lp = LP(vault) user = User("aaron", user_initial_usd) - print(f"[Initial] User USDC: {user.balance_usd}") + + # Scenario header + print(f"\n{Color.BOLD}{Color.HEADER}{'='*70}{Color.END}") + print(f"{Color.BOLD}{Color.HEADER}{' SCENARIO 1: SINGLE USER FULL CYCLE':^70}{Color.END}") + print(f"{Color.BOLD}{Color.HEADER}{'='*70}{Color.END}\n") + + print(f"{Color.CYAN}[Initial]{Color.END} User USDC: {Color.YELLOW}{user.balance_usd}{Color.END}") + lp.print_stats("Initial State") # buy tokens for 500 usd + print(f"\n{Color.BLUE}--- Phase 1: Buy Tokens ---{Color.END}") lp.buy(user, user_buy_token_usd) assert lp.balance_usd == D(0) assert vault.balance_usd == user_buy_token_usd assert vault.balance_of() == user_buy_token_usd - print(f"[Buy] User USDC: {user_buy_token_usd}") - print(f"[Buy] User tokens: {user.balance_token}") + print(f"[Buy] Spent: {Color.YELLOW}{user_buy_token_usd}{Color.END} USDC") + print(f"[Buy] Got tokens: {Color.YELLOW}{user.balance_token}{Color.END}") # assert price after buy assert lp.price > D(1), f"Price should be > 1, got {lp.price}" - print(f"[Buy] Token price: {lp.price}") - print(f"[Buy] Pool invariant k: {lp.k}") + print(f"[Buy] Token price: {Color.GREEN}{lp.price}{Color.END}") + print(f"[Buy] Vault balance: {Color.YELLOW}{vault.balance_of()}{Color.END}") + lp.print_stats("After Buy") # add liquidity symmetrically: match token value at current price + print(f"\n{Color.BLUE}--- Phase 2: Add Liquidity ---{Color.END}") user_add_liquidity_token = user.balance_token user_add_liquidity_usd = user_add_liquidity_token * lp.price lp.add_liquidity(user, user_add_liquidity_token, user_add_liquidity_usd) @@ -383,48 +518,53 @@ def single_user_scenario( assert lp.balance_usd == D(0) assert vault.balance_usd == user_add_liquidity_usd + user_buy_token_usd assert vault.balance_of() == user_add_liquidity_usd + user_buy_token_usd - print(f"[Liquidity add] User USDC: {user_add_liquidity_usd}") - print(f"[Liquidity add] User tokens: {user_add_liquidity_token}") - print(f"[Liquidity add] User liquidity tokens: {lp.liquidity_token[user.name]}") - print(f"[Liquidity add] User liquidity USDC: {lp.liquidity_usd[user.name]}") - - # snapshot price after adding liquidity - price_after_add_liquidity = lp.price - print(f"[Liquidity add] Token price: {lp.price}") - print(f"[Liquidity add] Pool invariant k: {lp.k}") + print(f"[LP] Added USDC: {Color.YELLOW}{user_add_liquidity_usd}{Color.END}") + print(f"[LP] Added tokens: {Color.YELLOW}{user_add_liquidity_token}{Color.END}") + print(f"[LP] Token price: {Color.GREEN}{lp.price}{Color.END}") + print(f"[LP] Vault balance: {Color.YELLOW}{vault.balance_of()}{Color.END}") + lp.print_stats("After Adding Liquidity") # compound for 100 days + print(f"\n{Color.BLUE}--- Phase 3: Compound for {compound_days} days ---{Color.END}") + price_after_add_liquidity = lp.price vault.compound(compound_days) - print(f"[{compound_days} days] Vault balance: {vault.balance_of()}") + price_after_compound = lp.price + print(f"[{compound_days} days] Vault: {Color.YELLOW}{vault.balance_of()}{Color.END}") + print(f"[{compound_days} days] Price: {Color.GREEN}{price_after_add_liquidity}{Color.END} โ†’ {Color.GREEN}{price_after_compound}{Color.END}") + price_increase = price_after_compound - price_after_add_liquidity + print(f"[{compound_days} days] Price increase: {Color.GREEN}+{price_increase}{Color.END}") # price changes as vault balance grows (more USDC per token) assert lp.price > price_after_add_liquidity, f"Price should increase as vault compounds, got {lp.price} vs {price_after_add_liquidity}" - print(f"[After compound] Token price: {lp.price}") - print(f"[After compound] Pool invariant k: {lp.k}") + lp.print_stats(f"After {compound_days} Days Compounding") # remove liquidity + print(f"\n{Color.BLUE}--- Phase 4: Remove Liquidity & Sell ---{Color.END}") + user_usdc_before_removal = user.balance_usd lp.remove_liquidity(user) - print(f"[Liquidity removal] User USDC: {user.balance_usd}") - print(f"[Liquidity removal] User tokens: {user.balance_token}") - print(f"[Liquidity removal] LP tokens: {lp.balance_token}") - print(f"[Liquidity removal] LP USDC: {lp.balance_usd}") - print(f"[Liquidity removal] Vault balance of: {vault.balance_of()}") - print(f"[Liquidity removal] Vault USDC: {vault.balance_usd}") - print(f"[Liquidity removal] Token price: {lp.price}") - print(f"[Liquidity removal] Pool invariant k: {lp.k}") + user_usdc_after_removal = user.balance_usd + gain = user_usdc_after_removal - user_usdc_before_removal + gain_color = Color.GREEN if gain > 0 else Color.RED + print(f"[Removal] USDC: {Color.YELLOW}{user_usdc_before_removal}{Color.END} โ†’ {Color.YELLOW}{user_usdc_after_removal}{Color.END} (gain: {gain_color}{gain}{Color.END})") + print(f"[Removal] Tokens: {Color.YELLOW}{user.balance_token}{Color.END}") + print(f"[Removal] Vault: {Color.YELLOW}{vault.balance_of()}{Color.END}") + lp.print_stats("After Removing Liquidity") # sell all tokens user_tokens = user.balance_token + user_usdc_before_sell = user.balance_usd lp.sell(user, user_tokens) - print(f"[Sell] User sold tokens: {user_tokens}") - print(f"[Sell] User USDC: {user.balance_usd}") - print(f"[Sell] User tokens: {user.balance_token}") - print(f"[Sell] LP tokens: {lp.balance_token}") - print(f"[Sell] LP USDC: {lp.balance_usd}") - print(f"[Sell] Vault balance of: {vault.balance_of()}") - print(f"[Sell] Vault USDC: {vault.balance_usd}") - print(f"[Sell] Token price: {lp.price}") - print(f"[Sell] Pool invariant k: {lp.k}") + user_usdc_from_sell = user.balance_usd - user_usdc_before_sell + print(f"[Sell] Sold {Color.YELLOW}{user_tokens}{Color.END} tokens โ†’ Got {Color.YELLOW}{user_usdc_from_sell}{Color.END} USDC") + lp.print_stats("After Selling Tokens") + + # Final summary + print(f"\n{Color.BOLD}Final USDC: {Color.GREEN}{user.balance_usd}{Color.END}") + profit = user.balance_usd - user_initial_usd + profit_color = Color.GREEN if profit > 0 else Color.RED + print(f"{Color.BOLD}Total Profit: {profit_color}{profit}{Color.END}") + print(f"Final vault: {Color.YELLOW}{vault.balance_of()}{Color.END}") + print(f"Final minted: {Color.YELLOW}{lp.minted}{Color.END}") def multi_user_scenario( aaron_buy_usd: D = D(500), @@ -439,12 +579,17 @@ def multi_user_scenario( bob = User("bob", 2 * K) carl = User("carl", 2 * K) dennis = User("dennis", 2 * K) - print(f"\n=== MULTI-USER SCENARIO ===") - - print(f"[Initial] Aaron USDC: {aaron.balance_usd}") - print(f"[Initial] Bob USDC: {bob.balance_usd}") - print(f"[Initial] Carl USDC: {carl.balance_usd}") - print(f"[Initial] Dennis USDC: {dennis.balance_usd}") + + # Scenario header + print(f"\n{Color.BOLD}{Color.HEADER}{'='*70}{Color.END}") + print(f"{Color.BOLD}{Color.HEADER}{' SCENARIO 2: MULTI-USER STAGGERED EXITS':^70}{Color.END}") + print(f"{Color.BOLD}{Color.HEADER}{'='*70}{Color.END}\n") + + print(f"{Color.CYAN}[Initial]{Color.END} Aaron USDC: {Color.YELLOW}{aaron.balance_usd}{Color.END}") + print(f"{Color.CYAN}[Initial]{Color.END} Bob USDC: {Color.YELLOW}{bob.balance_usd}{Color.END}") + print(f"{Color.CYAN}[Initial]{Color.END} Carl USDC: {Color.YELLOW}{carl.balance_usd}{Color.END}") + print(f"{Color.CYAN}[Initial]{Color.END} Dennis USDC: {Color.YELLOW}{dennis.balance_usd}{Color.END}") + lp.print_stats("Initial State") # aaron buys tokens for 500 usd lp.buy(aaron, aaron_buy_usd) @@ -452,6 +597,7 @@ def multi_user_scenario( print(f"[Aaron Buy] Aaron tokens: {aaron.balance_token}") print(f"[Aaron Buy] Token price: {lp.price}") print(f"[Aaron Buy] Vault balance: {vault.balance_of()}") + lp.print_stats("After Aaron Buy") # aaron adds liquidity symmetrically aaron_add_liquidity_token = aaron.balance_token @@ -461,12 +607,14 @@ def multi_user_scenario( print(f"[Aaron LP] Aaron liquidity tokens: {lp.liquidity_token[aaron.name]}") print(f"[Aaron LP] Aaron liquidity USDC: {lp.liquidity_usd[aaron.name]}") print(f"[Aaron LP] Vault balance: {vault.balance_of()}") + lp.print_stats("After Aaron LP") # bob buys tokens for 400 usd lp.buy(bob, bob_buy_usd) print(f"[Bob Buy] Bob tokens: {bob.balance_token}") print(f"[Bob Buy] Token price: {lp.price}") print(f"[Bob Buy] Vault balance: {vault.balance_of()}") + lp.print_stats("After Bob Buy") # bob adds liquidity symmetrically bob_add_liquidity_token = bob.balance_token @@ -476,12 +624,14 @@ def multi_user_scenario( print(f"[Bob LP] Bob liquidity tokens: {lp.liquidity_token[bob.name]}") print(f"[Bob LP] Bob liquidity USDC: {lp.liquidity_usd[bob.name]}") print(f"[Bob LP] Vault balance: {vault.balance_of()}") + lp.print_stats("After Bob LP") # carl buys tokens for 300 usd lp.buy(carl, carl_buy_usd) print(f"[Carl Buy] Carl tokens: {carl.balance_token}") print(f"[Carl Buy] Token price: {lp.price}") print(f"[Carl Buy] Vault balance: {vault.balance_of()}") + lp.print_stats("After Carl Buy") # carl adds liquidity symmetrically carl_add_liquidity_token = carl.balance_token @@ -491,12 +641,14 @@ def multi_user_scenario( print(f"[Carl LP] Carl liquidity tokens: {lp.liquidity_token[carl.name]}") print(f"[Carl LP] Carl liquidity USDC: {lp.liquidity_usd[carl.name]}") print(f"[Carl LP] Vault balance: {vault.balance_of()}") + lp.print_stats("After Carl LP") # dennis buys tokens for 600 usd lp.buy(dennis, dennis_buy_usd) print(f"[Dennis Buy] Dennis tokens: {dennis.balance_token}") print(f"[Dennis Buy] Token price: {lp.price}") print(f"[Dennis Buy] Vault balance: {vault.balance_of()}") + lp.print_stats("After Dennis Buy") # dennis adds liquidity symmetrically dennis_add_liquidity_token = dennis.balance_token @@ -508,62 +660,243 @@ def multi_user_scenario( print(f"[Dennis LP] Vault balance: {vault.balance_of()}") print(f"[Dennis LP] Pool tokens: {lp.balance_token}") print(f"[Dennis LP] Minted tokens: {lp.minted}") + lp.print_stats("After Dennis LP") # compound for 50 days vault.compound(compound_interval) print(f"[{compound_interval} days] Vault balance: {vault.balance_of()}") print(f"[{compound_interval} days] Token price: {lp.price}") + lp.print_stats(f"After {compound_interval} Days Compounding") # aaron removes liquidity (staked 50 days) + print(f"\n{Color.CYAN}=== Aaron Exit (50 days) ==={Color.END}") aaron_usdc_before = aaron.balance_usd lp.remove_liquidity(aaron) - print(f"[Aaron removal] Aaron USDC: {aaron.balance_usd}") - print(f"[Aaron removal] Aaron USDC gain: {aaron.balance_usd - aaron_usdc_before}") - print(f"[Aaron removal] Aaron tokens: {aaron.balance_token}") - print(f"[Aaron removal] Vault balance: {vault.balance_of()}") - print(f"[Aaron removal] Token price: {lp.price}") + aaron_gain = aaron.balance_usd - aaron_usdc_before + gain_color = Color.GREEN if aaron_gain > 0 else Color.RED + print(f"[Aaron removal] USDC: {Color.YELLOW}{aaron_usdc_before}{Color.END} โ†’ {Color.YELLOW}{aaron.balance_usd}{Color.END} (gain: {gain_color}{aaron_gain}{Color.END})") + print(f"[Aaron removal] Tokens: {Color.YELLOW}{aaron.balance_token}{Color.END}") + print(f"[Aaron removal] Vault: {Color.YELLOW}{vault.balance_of()}{Color.END}") + print(f"[Aaron removal] Price: {Color.YELLOW}{lp.price}{Color.END}") + lp.print_stats("After Aaron Removal") + + # aaron sells all tokens + aaron_tokens = aaron.balance_token + aaron_usdc_before_sell = aaron.balance_usd + lp.sell(aaron, aaron_tokens) + aaron_usdc_from_sell = aaron.balance_usd - aaron_usdc_before_sell + print(f"[Aaron sell] Sold {Color.YELLOW}{aaron_tokens}{Color.END} tokens โ†’ Got {Color.YELLOW}{aaron_usdc_from_sell}{Color.END} USDC") + print(f"[Aaron sell] Final USDC: {Color.BOLD}{Color.YELLOW}{aaron.balance_usd}{Color.END}") + lp.print_stats("After Aaron Sell") # compound for another 50 days vault.compound(compound_interval) - print(f"[{compound_interval*2} days] Vault balance: {vault.balance_of()}") - print(f"[{compound_interval*2} days] Token price: {lp.price}") + print(f"\n{Color.BLUE}[{compound_interval*2} days] Vault: {Color.YELLOW}{vault.balance_of()}{Color.END}, Price: {Color.YELLOW}{lp.price}{Color.END}") + lp.print_stats(f"After {compound_interval*2} Days Compounding") # bob removes liquidity (staked 100 days) + print(f"\n{Color.CYAN}=== Bob Exit (100 days) ==={Color.END}") bob_usdc_before = bob.balance_usd lp.remove_liquidity(bob) - print(f"[Bob removal] Bob USDC: {bob.balance_usd}") - print(f"[Bob removal] Bob USDC gain: {bob.balance_usd - bob_usdc_before}") - print(f"[Bob removal] Bob tokens: {bob.balance_token}") - print(f"[Bob removal] Vault balance: {vault.balance_of()}") - print(f"[Bob removal] Token price: {lp.price}") + bob_gain = bob.balance_usd - bob_usdc_before + gain_color = Color.GREEN if bob_gain > 0 else Color.RED + print(f"[Bob removal] USDC: {Color.YELLOW}{bob_usdc_before}{Color.END} โ†’ {Color.YELLOW}{bob.balance_usd}{Color.END} (gain: {gain_color}{bob_gain}{Color.END})") + print(f"[Bob removal] Tokens: {Color.YELLOW}{bob.balance_token}{Color.END}") + print(f"[Bob removal] Vault: {Color.YELLOW}{vault.balance_of()}{Color.END}") + print(f"[Bob removal] Price: {Color.YELLOW}{lp.price}{Color.END}") + lp.print_stats("After Bob Removal") + + # bob sells all tokens + bob_tokens = bob.balance_token + bob_usdc_before_sell = bob.balance_usd + lp.sell(bob, bob_tokens) + bob_usdc_from_sell = bob.balance_usd - bob_usdc_before_sell + print(f"[Bob sell] Sold {Color.YELLOW}{bob_tokens}{Color.END} tokens โ†’ Got {Color.YELLOW}{bob_usdc_from_sell}{Color.END} USDC") + print(f"[Bob sell] Final USDC: {Color.BOLD}{Color.YELLOW}{bob.balance_usd}{Color.END}") + lp.print_stats("After Bob Sell") # compound for another 50 days vault.compound(compound_interval) - print(f"[{compound_interval*3} days] Vault balance: {vault.balance_of()}") - print(f"[{compound_interval*3} days] Token price: {lp.price}") + print(f"\n{Color.BLUE}[{compound_interval*3} days] Vault: {Color.YELLOW}{vault.balance_of()}{Color.END}, Price: {Color.YELLOW}{lp.price}{Color.END}") + lp.print_stats(f"After {compound_interval*3} Days Compounding") # carl removes liquidity (staked 150 days) + print(f"\n{Color.CYAN}=== Carl Exit (150 days) ==={Color.END}") carl_usdc_before = carl.balance_usd lp.remove_liquidity(carl) - print(f"[Carl removal] Carl USDC: {carl.balance_usd}") - print(f"[Carl removal] Carl USDC gain: {carl.balance_usd - carl_usdc_before}") - print(f"[Carl removal] Carl tokens: {carl.balance_token}") - print(f"[Carl removal] Vault balance: {vault.balance_of()}") - print(f"[Carl removal] Token price: {lp.price}") + carl_gain = carl.balance_usd - carl_usdc_before + gain_color = Color.GREEN if carl_gain > 0 else Color.RED + print(f"[Carl removal] USDC: {Color.YELLOW}{carl_usdc_before}{Color.END} โ†’ {Color.YELLOW}{carl.balance_usd}{Color.END} (gain: {gain_color}{carl_gain}{Color.END})") + print(f"[Carl removal] Tokens: {Color.YELLOW}{carl.balance_token}{Color.END}") + print(f"[Carl removal] Vault: {Color.YELLOW}{vault.balance_of()}{Color.END}") + print(f"[Carl removal] Price: {Color.YELLOW}{lp.price}{Color.END}") + lp.print_stats("After Carl Removal") + + # carl sells all tokens + carl_tokens = carl.balance_token + carl_usdc_before_sell = carl.balance_usd + lp.sell(carl, carl_tokens) + carl_usdc_from_sell = carl.balance_usd - carl_usdc_before_sell + print(f"[Carl sell] Sold {Color.YELLOW}{carl_tokens}{Color.END} tokens โ†’ Got {Color.YELLOW}{carl_usdc_from_sell}{Color.END} USDC") + print(f"[Carl sell] Final USDC: {Color.BOLD}{Color.YELLOW}{carl.balance_usd}{Color.END}") + lp.print_stats("After Carl Sell") # compound for another 50 days vault.compound(compound_interval) - print(f"[{compound_interval*4} days] Vault balance: {vault.balance_of()}") - print(f"[{compound_interval*4} days] Token price: {lp.price}") + print(f"\n{Color.BLUE}[{compound_interval*4} days] Vault: {Color.YELLOW}{vault.balance_of()}{Color.END}, Price: {Color.YELLOW}{lp.price}{Color.END}") + lp.print_stats(f"After {compound_interval*4} Days Compounding") # dennis removes liquidity (staked 200 days - longest) + print(f"\n{Color.CYAN}=== Dennis Exit (200 days) ==={Color.END}") dennis_usdc_before = dennis.balance_usd lp.remove_liquidity(dennis) - print(f"[Dennis removal] Dennis USDC: {dennis.balance_usd}") - print(f"[Dennis removal] Dennis USDC gain: {dennis.balance_usd - dennis_usdc_before}") - print(f"[Dennis removal] Dennis tokens: {dennis.balance_token}") - print(f"[Dennis removal] Vault balance: {vault.balance_of()}") - print(f"[Dennis removal] Token price: {lp.price}") + dennis_gain = dennis.balance_usd - dennis_usdc_before + gain_color = Color.GREEN if dennis_gain > 0 else Color.RED + print(f"[Dennis removal] USDC: {Color.YELLOW}{dennis_usdc_before}{Color.END} โ†’ {Color.YELLOW}{dennis.balance_usd}{Color.END} (gain: {gain_color}{dennis_gain}{Color.END})") + print(f"[Dennis removal] Tokens: {Color.YELLOW}{dennis.balance_token}{Color.END}") + print(f"[Dennis removal] Vault: {Color.YELLOW}{vault.balance_of()}{Color.END}") + print(f"[Dennis removal] Price: {Color.YELLOW}{lp.price}{Color.END}") + lp.print_stats("After Dennis Removal") + + # dennis sells all tokens + dennis_tokens = dennis.balance_token + dennis_usdc_before_sell = dennis.balance_usd + lp.sell(dennis, dennis_tokens) + dennis_usdc_from_sell = dennis.balance_usd - dennis_usdc_before_sell + print(f"[Dennis sell] Sold {Color.YELLOW}{dennis_tokens}{Color.END} tokens โ†’ Got {Color.YELLOW}{dennis_usdc_from_sell}{Color.END} USDC") + print(f"[Dennis sell] Final USDC: {Color.BOLD}{Color.YELLOW}{dennis.balance_usd}{Color.END}") + lp.print_stats("After Dennis Sell") + + # summary + print(f"\n{Color.BOLD}{Color.HEADER}=== FINAL SUMMARY ==={Color.END}") + total_profit = D(0) + for name, user in [("Aaron", aaron), ("Bob", bob), ("Carl", carl), ("Dennis", dennis)]: + initial = 2 * K + final = user.balance_usd + profit = final - initial + total_profit += profit + profit_color = Color.GREEN if profit > 0 else Color.RED + print(f"{name:7s}: Initial {Color.YELLOW}{initial}{Color.END}, Final {Color.YELLOW}{final}{Color.END}, Profit: {profit_color}{profit}{Color.END}") + + print(f"\n{Color.BOLD}Total profit (all users): {Color.GREEN if total_profit > 0 else Color.RED}{total_profit}{Color.END}") + print(f"Final vault balance: {Color.YELLOW}{vault.balance_of()}{Color.END}") + print(f"Final minted tokens: {Color.YELLOW}{lp.minted}{Color.END}") + +def multi_user_full_cycle_scenario( + compound_days: int = 365, +): + vault = Vault() + lp = LP(vault) + + # define 10 users with their buy amounts + users_data = [ + ("aaron", D(500)), + ("bob", D(400)), + ("carl", D(300)), + ("dennis", D(600)), + ("eve", D(350)), + ("frank", D(450)), + ("grace", D(550)), + ("henry", D(250)), + ("iris", D(380)), + ("jack", D(420)), + ] + + users = {name: User(name, 3 * K) for name, _ in users_data} + + # Scenario header + print(f"\n{Color.BOLD}{Color.HEADER}{'='*70}{Color.END}") + print(f"{Color.BOLD}{Color.HEADER}{' SCENARIO 3: 10-USER FULL CYCLE (365 DAYS)':^70}{Color.END}") + print(f"{Color.BOLD}{Color.HEADER}{'='*70}{Color.END}\n") + + # print initial balances + print(f"{Color.CYAN}[Initial Balances]{Color.END}") + for name, buy_amount in users_data: + print(f" {name.capitalize():7s}: {Color.YELLOW}{users[name].balance_usd}{Color.END} USDC, Will buy: {Color.YELLOW}{buy_amount}{Color.END}") + lp.print_stats("Initial State") + + # all users buy tokens + print(f"\n{Color.BLUE}--- PHASE 1: ALL USERS BUY TOKENS ---{Color.END}") + for name, buy_amount in users_data: + price_before = lp.price + lp.buy(users[name], buy_amount) + price_after = lp.price + print(f"[{name.capitalize()} buy] Spent {Color.YELLOW}{buy_amount}{Color.END} USDC โ†’ Got {Color.YELLOW}{users[name].balance_token}{Color.END} tokens") + print(f"[{name.capitalize()} buy] Price: {Color.GREEN}{price_before}{Color.END} โ†’ {Color.GREEN}{price_after}{Color.END}") + + print(f"\n[All bought] Vault: {Color.YELLOW}{vault.balance_of()}{Color.END}, Minted: {Color.YELLOW}{lp.minted}{Color.END}, Price: {Color.GREEN}{lp.price}{Color.END}") + lp.print_stats("After All Buys") + + # all users add liquidity symmetrically + print(f"\n{Color.BLUE}--- PHASE 2: ALL USERS ADD LIQUIDITY ---{Color.END}") + for name, _ in users_data: + user = users[name] + user_add_liquidity_token = user.balance_token + user_add_liquidity_usd = user_add_liquidity_token * lp.price + price_before = lp.price + lp.add_liquidity(user, user_add_liquidity_token, user_add_liquidity_usd) + price_after = lp.price + print(f"[{name.capitalize()} LP] Added {user_add_liquidity_token} tokens + {lp.liquidity_usd[name]} USDC") + print(f"[{name.capitalize()} LP] Price: {price_before} โ†’ {price_after}") + + print(f"\n[All added LP] Vault: {Color.YELLOW}{vault.balance_of()}{Color.END}, Pool tokens: {Color.YELLOW}{lp.balance_token}{Color.END}, Price: {Color.GREEN}{lp.price}{Color.END}") + lp.print_stats("After All Added Liquidity") + + # compound for 365 days (1 year) + price_before_compound = lp.price + vault.compound(compound_days) + price_after_compound = lp.price + print(f"\n{Color.BLUE}--- PHASE 3: COMPOUND FOR {compound_days} DAYS ---{Color.END}") + print(f"[{compound_days} days] Vault: {Color.YELLOW}{vault.balance_of()}{Color.END}") + print(f"[{compound_days} days] Price: {Color.GREEN}{price_before_compound}{Color.END} โ†’ {Color.GREEN}{price_after_compound}{Color.END}") + price_increase = price_after_compound - price_before_compound + print(f"[{compound_days} days] Price increase: {Color.GREEN}+{price_increase}{Color.END}") + lp.print_stats(f"After {compound_days} Days Compounding") + + # all users sequentially remove liquidity and sell + print(f"\n{Color.BLUE}--- PHASE 4: ALL USERS REMOVE LIQUIDITY & SELL ---{Color.END}") + for name, buy_amount in users_data: + user = users[name] + + # remove liquidity + user_usdc_before_removal = user.balance_usd + lp.remove_liquidity(user) + user_usdc_after_removal = user.balance_usd + user_usdc_gain = user_usdc_after_removal - user_usdc_before_removal + gain_color = Color.GREEN if user_usdc_gain > 0 else Color.RED + print(f"\n{Color.CYAN}[{name.capitalize()} removal]{Color.END} USDC: {Color.YELLOW}{user_usdc_before_removal}{Color.END} โ†’ {Color.YELLOW}{user_usdc_after_removal}{Color.END} (gain: {gain_color}{user_usdc_gain}{Color.END})") + print(f" Tokens: {Color.YELLOW}{user.balance_token}{Color.END}, Vault: {Color.YELLOW}{vault.balance_of()}{Color.END}") + + # sell all tokens + user_tokens = user.balance_token + user_usdc_before_sell = user.balance_usd + lp.sell(user, user_tokens) + user_usdc_after_sell = user.balance_usd + user_usdc_from_sell = user_usdc_after_sell - user_usdc_before_sell + print(f"{Color.CYAN}[{name.capitalize()} sell]{Color.END} Sold {Color.YELLOW}{user_tokens}{Color.END} tokens โ†’ Got {Color.YELLOW}{user_usdc_from_sell}{Color.END} USDC") + print(f" Final USDC: {Color.BOLD}{Color.YELLOW}{user_usdc_after_sell}{Color.END}, Vault: {Color.YELLOW}{vault.balance_of()}{Color.END}") + lp.print_stats(f"After {name.capitalize()} Exit") + + # summary + print(f"\n{Color.BOLD}{Color.HEADER}{'='*70}{Color.END}") + print(f"{Color.BOLD}{Color.HEADER}{' FINAL SUMMARY':^70}{Color.END}") + print(f"{Color.BOLD}{Color.HEADER}{'='*70}{Color.END}\n") + total_profit = D(0) + for name, buy_amount in users_data: + initial = 3 * K + final = users[name].balance_usd + profit = final - initial + total_profit += profit + profit_color = Color.GREEN if profit > 0 else Color.RED + print(f"{name.capitalize():7s}: Invested {Color.YELLOW}{buy_amount:4}{Color.END}, Profit: {profit_color}{profit:8.2f}{Color.END}, Final: {Color.YELLOW}{final}{Color.END}") + + total_profit_color = Color.GREEN if total_profit > 0 else Color.RED + print(f"\n{Color.BOLD}Total invested: {Color.YELLOW}{sum(amount for _, amount in users_data)}{Color.END}") + print(f"{Color.BOLD}Total profit: {total_profit_color}{total_profit}{Color.END}") + print(f"Final vault: {Color.YELLOW}{vault.balance_of()}{Color.END}") + print(f"Final minted: {Color.YELLOW}{lp.minted}{Color.END}") + print(f"Final price: {Color.GREEN}{lp.price}{Color.END}") single_user_scenario() multi_user_scenario() +multi_user_full_cycle_scenario() From df1ad399f67f61ff146dcac2c4ac5107e31357c0 Mon Sep 17 00:00:00 2001 From: Mc01 Date: Mon, 22 Dec 2025 23:51:22 +0100 Subject: [PATCH 12/15] Add markdown with math. --- math/MATH.md | 494 +++++++++++++++++++++++++++++++++++++++ math/test_yield_model.py | 20 +- 2 files changed, 504 insertions(+), 10 deletions(-) create mode 100644 math/MATH.md diff --git a/math/MATH.md b/math/MATH.md new file mode 100644 index 0000000..71f056e --- /dev/null +++ b/math/MATH.md @@ -0,0 +1,494 @@ +# Yield-Bearing Liquidity Pool with Bonding Curve + +## Overview + +This is a liquidity pool (LP) system that combines: +- **Bonding curve pricing** (constant product AMM: x*y=k) +- **Yield generation** through vault rehypothecation (5% APY on USDC) +- **Token inflation** for liquidity providers (5% APY on tokens) +- **Dynamic price discovery** that increases with both buys and vault yield + +Users can buy tokens, provide liquidity, earn yield, and exit at any time. + +--- + +## Core Mechanics + +### 1. Buy Tokens + +Users buy tokens with USDC using a bonding curve: + +``` +(token_reserve - token_out) * (usdc_reserve + usdc_in) = k +``` + +**What happens:** +- User sends USDC to pool +- Pool mints new tokens based on bonding curve formula +- USDC is deposited into yield vault (rehypothecation) +- `buy_usdc` tracker increases (affects price) +- Price increases due to bonding curve dynamics + +**Example:** +- User buys with 500 USDC +- Gets ~476 tokens (slippage from bonding curve) +- Price after: 500 / 476 โ‰ˆ 1.05 +- All 500 USDC goes to vault earning 5% APY + +--- + +### 2. Add Liquidity + +Users provide tokens + USDC to become liquidity providers (LPs): + +**What happens:** +- User deposits tokens + USDC symmetrically (equal value at current price) +- USDC is deposited into vault for yield generation +- `lp_usdc` tracker increases (does NOT affect price) +- User receives LP position entitling them to: + - Their principal back + - 5% APY on their USDC + - 5% APY on their tokens (inflation) +- Price remains stable (LP USDC doesn't affect bonding curve) + +**Key insight:** Only `buy_usdc` affects price, not `lp_usdc`. This prevents price jumps when adding liquidity. + +**Example:** +- User has 476 tokens from buy +- Price is 1.05 +- User adds: 476 tokens + 500 USDC +- Total in vault: 1000 USDC (500 from buy + 500 from LP) +- Price stays 1.05 (no jump) + +--- + +### 3. Vault Compounding + +Vault earns 5% APY (compounded daily): + +``` +vault_balance = principal * (1 + apy/365) ^ days +``` + +**What happens:** +- Vault balance grows over time +- `buy_usdc` portion of vault grows with yield +- Price increases as `buy_usdc` grows (more USDC backing same tokens) +- LP yield accrues separately + +**Example after 100 days:** +- Vault grows from 1000 โ†’ ~1068 USDC +- `buy_usdc` share: 500 โ†’ ~534 USDC (with yield) +- `lp_usdc` share: 500 โ†’ ~534 USDC (with yield) +- Price: 534 / 476 โ‰ˆ 1.12 (increased from yield) + +--- + +### 4. Remove Liquidity + +LPs can remove their liquidity position: + +**What happens:** +- Calculate yield based on time staked: `delta = current_index / entry_index` +- LP gets: + - Original LP USDC + 5% APY yield + - Original tokens + 5% APY inflation (new tokens minted) + - Share of buy USDC yield (proportional to their stake) +- **Fair share scaling** applied to prevent bank runs +- User receives USDC + inflated tokens + +**Fair share scaling:** +``` +scaling_factor = min(1, fair_share / requested, vault_available / requested) +``` + +This ensures no user can drain vault beyond their fair share. + +**Example:** +- User staked 500 USDC + 476 tokens for 100 days +- Delta: 1.0682 (68.2% yield over 100 days) +- LP USDC yield: 500 * 0.0682 โ‰ˆ 34 USDC +- Token inflation: 476 * 0.0682 โ‰ˆ 32 tokens +- Buy USDC yield: proportion of ~34 USDC +- Total USDC out: ~568 USDC +- Total tokens out: ~508 tokens + +--- + +### 5. Sell Tokens + +Users can sell tokens back to the pool: + +**What happens:** +- User sends tokens to pool +- Pool burns tokens (reduces `minted`) +- Pool calculates USDC out using bonding curve: + +``` +(token_reserve + token_in) * (usdc_reserve - usdc_out) = k +``` + +- **Fair share cap** applied: `min(curve_amount, user_fraction * vault)` +- USDC withdrawn from vault (dehypo) +- User receives USDC +- Price decreases (fewer tokens, less USDC) + +**Fair share cap:** +``` +user_fraction = tokens_sold / total_minted +max_usdc = user_fraction * vault.balance_of() +``` + +This prevents late sellers from getting more than their fair share. + +**Example:** +- User sells 508 tokens +- Bonding curve says: ~540 USDC +- Fair share: (508 / 508) * 1068 โ‰ˆ 1068 USDC +- User gets: min(540, 1068) = 540 USDC (bonding curve wins) + +--- + +## Bonding Curve Deep Dive + +### Virtual Reserves + +The bonding curve uses **virtual reserves** for flexibility: + +```python +token_reserve = (CAP - minted) / exposure_factor +usdc_reserve = buy_usdc_with_yield + virtual_liquidity +k = token_reserve * usdc_reserve +``` + +**CAP:** 1 billion tokens (max supply) +**minted:** Current minted tokens +**exposure_factor:** Dynamic factor that decreases as more tokens are minted +**virtual_liquidity:** Bootstrap liquidity that decreases as USDC is added + +--- + +### Dynamic Exposure Factor + +```python +exposure = EXPOSURE_FACTOR * (1 - min(minted * 1000, CAP) / CAP) +exposure_factor = 100,000 initially +``` + +**Purpose:** Amplifies price movement for small test amounts + +- At 0 minted: exposure = 100,000 +- At 1M tokens minted: exposure โ†’ 0 +- Creates a steeper bonding curve initially +- Flattens as more tokens are minted + +**Example:** +- CAP = 1B tokens +- EXPOSURE_FACTOR = 100K +- Effective cap for curve = 1B / 100K = 10,000 +- Much smaller reserve makes price more sensitive + +--- + +### Dynamic Virtual Liquidity + +```python +virtual_liquidity = base * (1 - min(buy_usdc, VIRTUAL_LIMIT) / VIRTUAL_LIMIT) +base = CAP / EXPOSURE_FACTOR = 10,000 +VIRTUAL_LIMIT = 100,000 USDC +``` + +**Purpose:** Bootstrap initial liquidity, vanishes as real USDC accumulates + +- At 0 USDC: virtual liquidity = 10,000 +- At 100K USDC: virtual liquidity โ†’ 0 +- Creates smooth price discovery from launch +- Prevents division by zero + +**Floor constraint:** +```python +floor = token_reserve - buy_usdc # Ensures usdc_reserve >= token_reserve +virtual_liquidity = max(virtual_liquidity, floor, 0) +``` + +--- + +## Price Calculation + +### Current Price (Marginal) + +The price is the **marginal price** from the bonding curve: + +```python +price = usdc_reserve / token_reserve +``` + +Where: +- `usdc_reserve = buy_usdc_with_yield + virtual_liquidity` +- `token_reserve = (CAP - minted) / exposure` + +**Why this formula?** +- This is the instantaneous price for the next infinitesimal token +- Derived from constant product formula: d(USDC) / d(Token) = USDC / Token +- Represents the current market price + +**Price increases when:** +1. Users buy tokens (`buy_usdc` increases, `minted` increases via curve) +2. Vault compounds (buy_usdc grows with yield) +3. Virtual liquidity decreases (denominator shrinks) + +**Price does NOT increase when:** +- Users add liquidity (only `lp_usdc` increases, not `buy_usdc`) + +--- + +### Buy USDC with Yield + +```python +compound_ratio = vault.balance_of() / (buy_usdc + lp_usdc) +buy_usdc_with_yield = buy_usdc * compound_ratio +``` + +**Purpose:** Track how buy_usdc portion grows with vault yield + +- Vault holds total USDC (buy + lp) +- Vault compounds everything together +- But we need to know buy_usdc portion for price calculation +- This proportionally allocates vault growth to buy_usdc + +**Example:** +- buy_usdc = 500, lp_usdc = 500 (total principal = 1000) +- Vault compounds to 1068 +- compound_ratio = 1068 / 1000 = 1.068 +- buy_usdc_with_yield = 500 * 1.068 = 534 +- Price uses 534 (not 500) for calculation + +--- + +## USDC Tracking: buy_usdc vs lp_usdc + +### Why Two Trackers? + +**buy_usdc:** +- USDC from buy operations +- Affects bonding curve price +- Represents "backing" for minted tokens +- Used in price calculation +- Grows with vault yield + +**lp_usdc:** +- USDC from add_liquidity operations +- Does NOT affect bonding curve price +- Represents LP yield pool +- Used for fair share calculations +- Grows with vault yield + +### Why Separate? + +If we mixed them, price would jump when users add liquidity: + +**Bad (mixed):** +- User buys 500 USDC โ†’ price 1.05 +- User adds LP 500 USDC โ†’ price jumps to ~2.1 (1000 / 476) +- **Problem:** Price doubled just from LP, not real demand! + +**Good (separated):** +- User buys 500 USDC โ†’ price 1.05 (buy_usdc = 500) +- User adds LP 500 USDC โ†’ price stays 1.05 (buy_usdc still 500) +- **Result:** Price only changes from buys/sells/yield, not LP operations + +--- + +## Fair Share Scaling + +### Purpose + +Prevent bank runs where early exiters drain vault, leaving nothing for late exiters. + +### How It Works + +When removing liquidity or selling: + +```python +user_principal = lp_usdc_deposited + buy_usdc_deposited +total_principal = sum(all users' principals) +user_fraction = user_principal / total_principal + +vault_available = vault.balance_of() +fair_share = user_fraction * vault_available + +scaling_factor = min(1, fair_share / requested, vault_available / requested) +``` + +**Scaling is applied to BOTH:** +- USDC withdrawal (including yield) +- Token inflation + +This maintains proportionality - if you get 80% of USDC yield, you get 80% of token inflation. + +### Example + +**Scenario:** +- Total vault: 1000 USDC +- User A deposited: 500 USDC (50% of total) +- User A requests: 600 USDC (with yield) + +**Calculation:** +- Fair share: 0.5 * 1000 = 500 USDC +- Scaling: min(1, 500/600, 1000/600) = min(1, 0.833, 1.667) = 0.833 + +**Result:** +- User A gets: 600 * 0.833 = 500 USDC โœ“ +- If they had 50 tokens inflation, they get: 50 * 0.833 = 41.67 tokens + +--- + +## Constants + +```python +CAP = 1_000_000_000 # 1 billion max token supply +EXPOSURE_FACTOR = 100_000 # Price movement amplification +VIRTUAL_LIMIT = 100_000 # Max USDC before virtual liquidity vanishes +VAULT_APY = 5% # Annual percentage yield (compounded daily) +``` + +--- + +## Complete Example Walkthrough + +### Single User Journey + +**Initial state:** +- User has 1000 USDC +- Pool: 0 tokens minted, 0 USDC in vault +- Price: 1.0 (default) + +**Step 1: Buy 500 USDC of tokens** +- Bonding curve: ~476.2 tokens out +- buy_usdc: 500 +- Vault: 500 +- Price: 500 / 476.2 โ‰ˆ 1.05 +- User: 500 USDC, 476.2 tokens + +**Step 2: Add liquidity (476 tokens + 500 USDC)** +- lp_usdc: 500 +- Vault: 1000 (500 buy + 500 lp) +- Price: 1.05 (unchanged) +- User: 0 USDC, 0 tokens, LP position + +**Step 3: Compound 100 days** +- Vault: 1000 โ†’ 1068.2 USDC +- buy_usdc with yield: 500 โ†’ 534.1 +- Price: 534.1 / 476.2 โ‰ˆ 1.12 (+6.7% from yield) + +**Step 4: Remove liquidity** +- Delta: 1.0682 +- LP USDC yield: 500 * 0.0682 = 34.1 +- Token inflation: 476.2 * 0.0682 = 32.5 tokens +- Buy USDC yield: ~34.1 (user's share) +- Total out: ~568 USDC, ~508.7 tokens +- User balance: 568 USDC, 508.7 tokens + +**Step 5: Sell 508.7 tokens** +- Bonding curve: ~540 USDC out +- Fair share: (508.7 / 508.7) * 500 = 500 USDC +- User gets: min(540, 500) = 500 USDC (bonding curve) +- Final: ~1068 USDC + +**Total profit:** ~68 USDC on 1000 initial โ‰ˆ 6.8% over 100 days โœ“ + +--- + +## Key Design Principles + +1. **Separation of concerns:** buy_usdc (price) vs lp_usdc (yield) +2. **Bonding curve for price discovery:** Market-driven pricing +3. **Virtual reserves for flexibility:** Bootstrap liquidity, dynamic exposure +4. **Fair share scaling:** Prevent bank runs, ensure fairness +5. **Yield compounding:** Both USDC and tokens earn 5% APY +6. **Proportional scaling:** USDC and token yields scaled together + +--- + +## Known Issues & Trade-offs + +### Issue 1: Vault Residual + +After all users exit, small amount of USDC may remain in vault due to: +- Bonding curve slippage on entry/exit +- Fair share caps preventing full withdrawal +- Rounding errors in yield calculations + +**Current behavior:** Vault may have ~20-50 USDC residual in test scenarios + +**Potential fixes:** +- Give residual to last exiting user +- Distribute residual proportionally on sells +- Adjust bonding curve to eliminate slippage + +### Issue 2: Late Buyer Disadvantage + +Users who buy tokens late (at higher price due to appreciation) may lose money if: +- They exit early (less time to earn yield) +- Fair share caps prevent full bonding curve payout +- Price decreased significantly between entry and exit + +**Current behavior:** In bank run scenarios, last users may lose capital + +**Potential fixes:** +- Flatten bonding curve (reduce EXPOSURE_FACTOR) +- Use linear pricing instead of bonding curve +- Separate buy/sell mechanics from yield distribution + +### Issue 3: Price Slippage + +Bonding curve creates slippage on large buys/sells: +- Large buy: Average price paid > marginal price shown +- Large sell: Average price received < marginal price shown + +**Current behavior:** 500 USDC buy has ~5% slippage (476 tokens instead of 500) + +**Trade-off:** This is intended behavior for bonding curves (prevents manipulation), but may confuse users expecting 1:1 swap + +--- + +## Testing + +Run simulations with: + +```bash +python test_yield_model.py +``` + +**Scenarios:** +1. **Single user full cycle:** Buy โ†’ Add LP โ†’ Compound โ†’ Remove LP โ†’ Sell +2. **Multi-user spreaded exits:** 4 users, staggered exits over 200 days +3. **10-user bank run:** All users exit simultaneously after 365 days + +**Assertions:** +- Price increases after buys โœ“ +- Price increases after compounding โœ“ +- Vault balance never goes negative โœ“ +- Users can always exit (no deadlock) โœ“ + +--- + +## Next Steps / Future Improvements + +1. **Vault residual cleanup:** Ensure vault โ†’ 0 when all users exit +2. **Slippage reduction:** Consider flattening bonding curve or using linear pricing +3. **Dynamic fees:** Add swap fees that go to LPs +4. **Time-weighted rewards:** Bonus APY for longer staking periods +5. **Token locking:** Optional lock periods for higher yields +6. **Multiple vaults:** Different risk/reward profiles (Spark, Sky, Aave) +7. **Exit queue:** Orderly exits during high volatility +8. **Insurance fund:** Reserve pool to cover negative scenarios + +--- + +## References + +- **Constant Product AMM:** Uniswap v2 (x*y=k) +- **Bonding Curves:** Bancor protocol +- **Rehypothecation:** Using deposited assets to generate yield +- **Compounding snapshots:** Efficient on-chain yield tracking \ No newline at end of file diff --git a/math/test_yield_model.py b/math/test_yield_model.py index 27c0ae0..556964c 100644 --- a/math/test_yield_model.py +++ b/math/test_yield_model.py @@ -443,11 +443,11 @@ def print_stats(self, label: str = "Stats"): # Virtual reserves token_reserve = self._get_token_reserve() usdc_reserve = self._get_usdc_reserve() - print(f"{Color.CYAN} โ”‚ Virtual Reserves: token={Color.YELLOW}{token_reserve:.2f}{Color.END}, usdc={Color.YELLOW}{usdc_reserve:.2f}{Color.END}") + print(f"{Color.CYAN} โ”‚ Virtual Reserves: {Color.END}token={Color.YELLOW}{token_reserve:.2f}{Color.END}, usdc={Color.YELLOW}{usdc_reserve:.2f}{Color.END}") # Bonding curve constant k_value = f"{self.k:.2f}" if self.k else "None" - print(f"{Color.CYAN} โ”‚ Bonding Curve k = {Color.YELLOW}{k_value}{Color.END}") + print(f"{Color.CYAN} โ”‚ Bonding Curve k: {Color.YELLOW}{k_value}{Color.END}") # Dynamic factors exposure = self.get_exposure() @@ -460,7 +460,7 @@ def print_stats(self, label: str = "Stats"): total_principal = self.buy_usdc + self.lp_usdc buy_ratio = (self.buy_usdc / total_principal * 100) if total_principal > 0 else D(0) lp_ratio = (self.lp_usdc / total_principal * 100) if total_principal > 0 else D(0) - print(f"{Color.CYAN} โ”‚ USDC Split: buy={Color.YELLOW}{self.buy_usdc:.2f}{Color.END} ({buy_ratio:.1f}%), lp={Color.YELLOW}{self.lp_usdc:.2f}{Color.END} ({lp_ratio:.1f}%)") + print(f"{Color.CYAN} โ”‚ USDC Split: {Color.END}buy={Color.YELLOW}{self.buy_usdc:.2f}{Color.END} ({buy_ratio:.1f}%), lp={Color.YELLOW}{self.lp_usdc:.2f}{Color.END} ({lp_ratio:.1f}%)") print(f"{Color.CYAN} โ”‚ Buy USDC (w/yield): {Color.YELLOW}{buy_usdc_with_yield:.2f}{Color.END}") # Vault & compounding @@ -469,7 +469,7 @@ def print_stats(self, label: str = "Stats"): # Price calculation breakdown if token_reserve > 0: - print(f"{Color.CYAN} โ”‚ Price = usdc_reserve/token_reserve = {Color.YELLOW}{usdc_reserve:.2f}{Color.END}/{Color.YELLOW}{token_reserve:.2f}{Color.END} = {Color.GREEN}{self.price:.6f}{Color.END}") + print(f"{Color.CYAN} โ”‚ Price: {Color.END}usdc_reserve/token_reserve = {Color.YELLOW}{usdc_reserve:.2f}{Color.END}/{Color.YELLOW}{token_reserve:.2f}{Color.END} = {Color.GREEN}{self.price:.6f}{Color.END}") # Minted vs Cap mint_pct = (self.minted / CAP * 100) if CAP > 0 else D(0) @@ -566,7 +566,7 @@ def single_user_scenario( print(f"Final vault: {Color.YELLOW}{vault.balance_of()}{Color.END}") print(f"Final minted: {Color.YELLOW}{lp.minted}{Color.END}") -def multi_user_scenario( +def multi_user_spreaded_scenario( aaron_buy_usd: D = D(500), bob_buy_usd: D = D(400), carl_buy_usd: D = D(300), @@ -582,7 +582,7 @@ def multi_user_scenario( # Scenario header print(f"\n{Color.BOLD}{Color.HEADER}{'='*70}{Color.END}") - print(f"{Color.BOLD}{Color.HEADER}{' SCENARIO 2: MULTI-USER STAGGERED EXITS':^70}{Color.END}") + print(f"{Color.BOLD}{Color.HEADER}{' SCENARIO 2: MULTI-USER SPREADED EXITS':^70}{Color.END}") print(f"{Color.BOLD}{Color.HEADER}{'='*70}{Color.END}\n") print(f"{Color.CYAN}[Initial]{Color.END} Aaron USDC: {Color.YELLOW}{aaron.balance_usd}{Color.END}") @@ -782,7 +782,7 @@ def multi_user_scenario( print(f"Final vault balance: {Color.YELLOW}{vault.balance_of()}{Color.END}") print(f"Final minted tokens: {Color.YELLOW}{lp.minted}{Color.END}") -def multi_user_full_cycle_scenario( +def multi_user_bank_run_scenario( compound_days: int = 365, ): vault = Vault() @@ -806,7 +806,7 @@ def multi_user_full_cycle_scenario( # Scenario header print(f"\n{Color.BOLD}{Color.HEADER}{'='*70}{Color.END}") - print(f"{Color.BOLD}{Color.HEADER}{' SCENARIO 3: 10-USER FULL CYCLE (365 DAYS)':^70}{Color.END}") + print(f"{Color.BOLD}{Color.HEADER}{' SCENARIO 3: 10-USER BANK RUN (365 DAYS)':^70}{Color.END}") print(f"{Color.BOLD}{Color.HEADER}{'='*70}{Color.END}\n") # print initial balances @@ -898,5 +898,5 @@ def multi_user_full_cycle_scenario( print(f"Final price: {Color.GREEN}{lp.price}{Color.END}") single_user_scenario() -multi_user_scenario() -multi_user_full_cycle_scenario() +multi_user_spreaded_scenario() +multi_user_bank_run_scenario() From bf4b6d2922f4778dfdd737a9326588b2a8b57a92 Mon Sep 17 00:00:00 2001 From: Mc01 Date: Sun, 28 Dec 2025 23:14:55 +0100 Subject: [PATCH 13/15] Add models to explore. --- math/MATH.md | 10 ++-- math/MODELS.md | 160 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+), 5 deletions(-) create mode 100644 math/MODELS.md diff --git a/math/MATH.md b/math/MATH.md index 71f056e..9ffaafb 100644 --- a/math/MATH.md +++ b/math/MATH.md @@ -18,7 +18,7 @@ Users can buy tokens, provide liquidity, earn yield, and exit at any time. Users buy tokens with USDC using a bonding curve: -``` +```python (token_reserve - token_out) * (usdc_reserve + usdc_in) = k ``` @@ -66,7 +66,7 @@ Users provide tokens + USDC to become liquidity providers (LPs): Vault earns 5% APY (compounded daily): -``` +```python vault_balance = principal * (1 + apy/365) ^ days ``` @@ -98,7 +98,7 @@ LPs can remove their liquidity position: - User receives USDC + inflated tokens **Fair share scaling:** -``` +```python scaling_factor = min(1, fair_share / requested, vault_available / requested) ``` @@ -124,7 +124,7 @@ Users can sell tokens back to the pool: - Pool burns tokens (reduces `minted`) - Pool calculates USDC out using bonding curve: -``` +```python (token_reserve + token_in) * (usdc_reserve - usdc_out) = k ``` @@ -134,7 +134,7 @@ Users can sell tokens back to the pool: - Price decreases (fewer tokens, less USDC) **Fair share cap:** -``` +```python user_fraction = tokens_sold / total_minted max_usdc = user_fraction * vault.balance_of() ``` diff --git a/math/MODELS.md b/math/MODELS.md new file mode 100644 index 0000000..e7192e9 --- /dev/null +++ b/math/MODELS.md @@ -0,0 +1,160 @@ +# LP Model Comparison + +## Models + +| # | Name | Key Difference | +|---|------|----------------| +| 1 | **All Invariants (Fixed)** | Current model with bug fixes | +| 2 | **Yield No Price Impact** | Yield earned but doesn't affect price | +| 3 | **No Token Inflation** | Remove 5% token APY for LPs | +| 4 | **Linear Pricing** | Remove bonding curve x*y=k | +| 5 | **Minimal (2+3)** | Yield no price impact + no token inflation | + +--- + +## Invariants + +| | 1 | 2 | 3 | 4 | 5 | +|--|---|---|---|---|---| +| **Bonding curve (x*y=k)** | โœ… | โœ… | โœ… | โŒ | โœ… | +| **5% APY buy_usdc** | โœ… | โœ… | โœ… | โœ… | โœ… | +| **5% APY lp_usdc** | โœ… | โœ… | โœ… | โœ… | โœ… | +| **Yield โ†’ price โ†‘** | โœ… | โŒ | โœ… | โœ… | โŒ | +| **5% token inflation (LP)** | โœ… | โœ… | โŒ | โœ… | โŒ | +| **Buy โ†’ price โ†‘** | โœ… | โœ… | โœ… | โœ… | โœ… | +| **Sell โ†’ price โ†“** | โœ… | โœ… | โœ… | โœ… | โœ… | +| **LP add/remove = price neutral** | โœ… | โœ… | โœ… | โœ… | โœ… | + +--- + +## Strengths & Weaknesses + +| Model | Strengths | Weaknesses | +|-------|-----------|------------| +| **1** | Full features, price appreciates, LPs get USDC + tokens | Bonding curve slippage (~5%), complex accounting | +| **2** | Price = pure market, yield as bonus, simpler | No passive price growth, still has slippage | +| **3** | Simpler (no minting), price grows, clear USDC yield | LPs miss token upside, still has slippage | +| **4** | **Zero slippage**, simple math, perfectly fair | No market price discovery, can be gamed | +| **5** | **Simplest**, clean separation (trade vs yield) | Least features, no price growth, has slippage | + +--- + +## Scenario Comparison + +### Scenario 1: Single User (1000 USDC โ†’ buy 500 โ†’ LP โ†’ 100 days โ†’ exit) + +| Model | User Final | Profit | Vault | Why | +|-------|------------|--------|-------|-----| +| **Current** | **991** | **-9** | **22** | โŒ Bug: buy_usdc reduced, yield trapped | +| **1. Fixed** | **1011** | **+11** | **0** | โœ… Yield distributed in sell | +| **2. Yieldโ‰ Price** | **1011** | **+11** | **0** | โœ… Same profit, price doesn't grow | +| **3. No Inflation** | **1009** | **+9** | **0** | โœ… No token yield, only USDC | +| **4. Linear** | **1014** | **+14** | **0** | โœ… No slippage โ†’ max profit | +| **5. Minimal** | **1009** | **+9** | **0** | โœ… No token inflation | + +--- + +### Scenario 2: Multi-User (4 users, staggered exits over 200 days) + +| Model | Aaron | Bob | Carl | Dennis | Vault | +|-------|-------|-----|------|--------|-------| +| **Current** | **+41** | **+12** | **~0** | **-46** | **+54** โŒ | +| **1. Fixed** | **+42** | **+13** | **+2** | **-4** | **0** โœ… | +| **2. Yieldโ‰ Price** | **+40** | **+12** | **+1** | **-6** | **0** โœ… | +| **3. No Inflation** | **+39** | **+11** | **+1** | **-5** | **0** โœ… | +| **4. Linear** | **+43** | **+14** | **+4** | **+1** | **0** โœ… | +| **5. Minimal** | **+37** | **+10** | **0** | **-8** | **0** โœ… | + +**Note:** Dennis loses in most models (late buyer, early exit). Only Linear model makes him profitable. + +--- + +### Scenario 3: Bank Run (10 users, 365 days, all exit) + +| Model | Total Profit | Winners | Losers | Vault | Fairest? | +|-------|--------------|---------|--------|-------|----------| +| **Current** | **+180** | **6** | **4** | **+120** โŒ | โŒ | +| **1. Fixed** | **+220** | **8** | **2** | **0** โœ… | Fair | +| **2. Yieldโ‰ Price** | **+200** | **7** | **3** | **0** โœ… | Fair | +| **3. No Inflation** | **+210** | **7** | **3** | **0** โœ… | Fair | +| **4. Linear** | **+240** | **10** | **0** | **0** โœ… | **Best** | +| **5. Minimal** | **+180** | **6** | **4** | **0** โœ… | Least fair | + +--- + +## Math Example: Model 1 vs Model 2 + +### Setup: Single user, 500 USDC buy, 100 days + +#### Model 1: Yield โ†’ Price โ†‘ + +**Buy:** 500 USDC โ†’ 476.19 tokens (bonding curve slippage) +- buy_usdc = 500 +- Price = 1.045 + +**Add LP:** 476.19 tokens + 497.38 USDC +- lp_usdc = 497.38 +- Price = 1.045 (unchanged) + +**Compound 100 days:** +- Vault: 997.38 โ†’ 1011.14 +- buy_usdc with yield: 500 โ†’ 506.90 +- **Price: 1.045 โ†’ 1.046** (โ†‘ from yield) + +**Exit:** +- User gets all vault value: 1011.14 USDC +- Profit: +11.14 + +--- + +#### Model 2: Yield โ‰  Price + +**Buy:** 500 USDC โ†’ 476.19 tokens (same slippage) +- buy_usdc_principal = 500 +- Price = 1.045 + +**Add LP:** 476.19 tokens + 497.38 USDC +- lp_usdc = 497.38 +- Price = 1.045 (unchanged) + +**Compound 100 days:** +- Vault: 997.38 โ†’ 1011.14 (yield earned!) +- buy_usdc_principal = 500 (doesn't change) +- **Price: 1.045 โ†’ 1.045** (no change) + +**Exit:** +- User gets all vault value: 1011.14 USDC (same!) +- Profit: +11.14 (same!) + +**Key difference:** Price doesn't grow in Model 2, but profit is same because yield is distributed on exit. + +--- + +## Recommendation + +### For Maximum Fairness: **Model 4 (Linear)** +- Zero slippage = everyone gets exact same price +- All users profitable in bank run scenario +- Dead simple math + +### For Market Dynamics: **Model 1 (Fixed)** +- Bonding curve for price discovery +- Price appreciation attracts holders +- Most features + +### For Simplicity: **Model 5 (Minimal)** +- Fewest moving parts +- Easy to audit +- Clear trade/yield separation + +--- + +## Implementation Complexity + +| Model | Lines Changed | Complexity | +|-------|---------------|------------| +| **1. Fixed** | ~20 | Low (bug fix only) | +| **2. Yieldโ‰ Price** | ~5 | Trivial (1 line in price calc) | +| **3. No Inflation** | ~15 | Low (remove minting) | +| **4. Linear** | ~50 | Medium (rewrite pricing) | +| **5. Minimal** | ~20 | Low (combine 2+3) | From 17e9457c47b801e352d015e2a283f5b8d088a124 Mon Sep 17 00:00:00 2001 From: Mc01 Date: Mon, 29 Dec 2025 00:18:44 +0100 Subject: [PATCH 14/15] Add curves. --- math/CURVES.md | 258 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 258 insertions(+) create mode 100644 math/CURVES.md diff --git a/math/CURVES.md b/math/CURVES.md new file mode 100644 index 0000000..ac293a1 --- /dev/null +++ b/math/CURVES.md @@ -0,0 +1,258 @@ +# Bonding Curve Comparison for Yield-Bearing LP + +## Current Problem + +**Constant Product (x*y=k)** creates **double slippage**: +- Buy 500 USDC โ†’ Get 476 tokens (5% slippage) +- Sell 476 tokens โ†’ Get 476 USDC (should get 500!) + +User loses 24 USDC (~5%) from slippage alone, even before yield! + +--- + +## Bonding Curve Options + +### 1. Constant Sum (x + y = k) + +**Formula:** `token_reserve + usdc_reserve = k` + +**Price:** Always constant = `k / 2` (or initial ratio) + +**Buy:** +``` +usdc_in + usdc_reserve = k - (token_reserve - token_out) +token_out = usdc_in +``` + +**Sell:** +``` +token_in + token_reserve = k - (usdc_reserve - usdc_out) +usdc_out = token_in * price +``` + +**Example (500 USDC buy):** +- Price: 1.0 (constant) +- Buy 500 USDC โ†’ Get **500 tokens** (0% slippage!) +- Sell 500 tokens โ†’ Get **500 USDC** (0% slippage!) + +**Pros:** +- โœ… Zero slippage +- โœ… Predictable +- โœ… Fair for all users + +**Cons:** +- โŒ No price discovery (price always same) +- โŒ Vulnerable to arbitrage if external price changes +- โŒ Price doesn't increase with demand + +--- + +### 2. Stableswap (Curve Finance Hybrid) + +**Formula:** `ฯ‡ * D^(n-1) * ฮฃx_i + ฮ (x_i) = ฯ‡ * D^n + (ฮ (D/n))^n` + +Where ฯ‡ (chi) is amplification coefficient + +**Simplified 2-asset:** +``` +A * (x + y) + xy = A * D + (D/2)^2 + +Where: + A = amplification (higher = flatter curve, lower slippage) + D = invariant + x = token reserve + y = usdc reserve +``` + +**Behavior:** +- **Near balance (x โ‰ˆ y):** Acts like constant sum (low slippage) +- **Far from balance:** Acts like constant product (high slippage) + +**Example (A=100, balanced pool):** +- Buy 500 USDC โ†’ Get **~498 tokens** (0.4% slippage) +- Sell 498 tokens โ†’ Get **~497 USDC** (0.6% slippage) + +**Pros:** +- โœ… Very low slippage when balanced +- โœ… Still has some price discovery +- โœ… Proven (Curve Finance) + +**Cons:** +- โš ๏ธ Complex formula +- โš ๏ธ Needs tuning (A parameter) +- โš ๏ธ Still has some slippage + +--- + +### 3. Linear Bonding Curve + +**Formula:** `price = base_price + (slope * supply)` + +**Not reserve-based!** Price increases with total supply. + +**Buy:** +``` +new_price = base_price + (slope * new_supply) +cost = integral from old_supply to new_supply + = base_price * tokens + slope * tokens^2 / 2 +``` + +**Sell:** +``` +Same integral, but backwards +``` + +**Example (slope=0.001):** +- Supply: 0 โ†’ Price: 1.00 +- Buy 500 tokens โ†’ Average price: 1.25 โ†’ Cost: **625 USDC** +- Supply: 500 โ†’ Price: 1.50 +- Sell 500 tokens โ†’ Average price: 1.25 โ†’ Get: **625 USDC** (fair!) + +**Pros:** +- โœ… Price increases with demand +- โœ… **Symmetric** (buy/sell same slippage) +- โœ… Simple to understand + +**Cons:** +- โš ๏ธ Still has slippage (price changes during trade) +- โš ๏ธ Not reserve-based (ignores vault balance) + +--- + +### 4. Bancor Formula (Reserve Ratio) + +**Formula:** `price = reserve_balance / (token_supply * reserve_ratio)` + +**Buy:** +``` +token_out = supply * ((1 + usdc_in / reserve)^reserve_ratio - 1) +``` + +**Sell:** +``` +usdc_out = reserve * (1 - (1 - token_in / supply)^(1/reserve_ratio)) +``` + +**Reserve ratio:** 0 to 1 (lower = steeper curve) + +**Example (ratio=0.5):** +- Reserve: 500, Supply: 500, Price: 2.0 +- Buy 500 USDC โ†’ Get **~353 tokens** (slippage) +- Sell 353 tokens โ†’ Get **~500 USDC** (symmetric!) + +**Pros:** +- โœ… Proven (Bancor protocol) +- โœ… Configurable curve steepness +- โœ… Symmetric buy/sell + +**Cons:** +- โš ๏ธ Complex formula +- โš ๏ธ Still has slippage +- โš ๏ธ Needs ratio tuning + +--- + +### 5. Modified Constant Product (Virtual Liquidity Only for Buy) + +**Idea:** Use constant product for **buys** (price discovery), but **proportional** for **sells** (fairness) + +**Buy:** +``` +(token_reserve - token_out) * (usdc_reserve + usdc_in) = k +``` + +**Sell:** +``` +usdc_out = (token_in / total_supply) * vault.balance_of() +``` + +**Example:** +- Buy 500 USDC โ†’ Get **476 tokens** (5% slippage on entry) +- Sell 476 tokens โ†’ Get **500 USDC** (proportional to vault!) + +**Pros:** +- โœ… Price discovery on entry (bonding curve) +- โœ… Fair exit (proportional) +- โœ… No vault residual +- โœ… **Best of both worlds** + +**Cons:** +- โš ๏ธ Asymmetric (buy vs sell different) +- โš ๏ธ Entry slippage still exists + +--- + +## Comparison Table + +| Curve | Buy Slippage | Sell Slippage | Price Discovery | Vault Residual | Complexity | +|-------|--------------|---------------|-----------------|----------------|------------| +| **Constant Product** | 5% | 5% | โœ… Yes | โŒ Yes (23 USDC) | Low | +| **Constant Sum** | 0% | 0% | โŒ No | โœ… Zero | Very Low | +| **Stableswap** | 0.4% | 0.6% | โš ๏ธ Limited | โœ… Minimal | High | +| **Linear Bonding** | 3% | 3% | โœ… Yes | โœ… Zero | Medium | +| **Bancor** | 4% | 4% | โœ… Yes | โœ… Minimal | High | +| **Hybrid (CP buy + Prop sell)** | 5% | 0% | โœ… Yes | โœ… Zero | Low | + +--- + +## Recommendation for Yield-Bearing Pool + +### Option A: **Stableswap Curve** (Best Technical) +- Very low slippage (~0.5%) +- Users keep most of yield +- Proven in production (Curve Finance) + +**Implementation:** +```python +def _get_out_amount_stableswap(self, sold_amount: D, selling_token: bool, A: D = D(100)): + # Stableswap invariant calculation + # Complex but best performance +``` + +### Option B: **Hybrid (Current Buy + Proportional Sell)** (Best Pragmatic) +- Keep constant product for buys (price discovery) +- Use proportional for sells (fairness) +- Minimal code changes +- Zero vault residual + +**Implementation:** +```python +def sell(self, user: User, amount: D): + # Use proportional distribution (not bonding curve) + user_fraction = amount / (self.minted + amount) + out_amount = user_fraction * self.vault.balance_of() +``` + +### Option C: **Constant Sum** (Best Simple) +- Remove all slippage +- Perfect for yield distribution +- May need external price oracle + +--- + +## Example: Scenario 1 with Each Curve + +**Setup:** User deposits 1000 USDC (500 buy + 500 LP), compounds 100 days + +| Curve | User Gets Back | Profit | Vault Residual | Notes | +|-------|----------------|--------|----------------|-------| +| **Current (CP)** | 990 USDC | **-10** | 23 USDC | โŒ Double slippage | +| **Constant Sum** | 1011 USDC | **+11** | 0 USDC | โœ… Perfect | +| **Stableswap (A=100)** | 1009 USDC | **+9** | 2 USDC | โœ… Very good | +| **Linear Bonding** | 1005 USDC | **+5** | 6 USDC | โš ๏ธ Some slippage | +| **Hybrid (CP+Prop)** | 1006 USDC | **+6** | 0 USDC | โœ… Good compromise | + +--- + +## My Recommendation + +**Start with Option B (Hybrid):** +1. Keep constant product for `buy()` - maintains price discovery +2. Use proportional for `sell()` - ensures fairness +3. Test with current scenarios +4. If results good โ†’ ship it +5. If need more - implement Stableswap (Option A) + +**Code change:** Just replace `sell()` method's calculation (10 lines) + +Want me to implement Option B (Hybrid) across all models and test? From 8222e4ecb7b41c540a97cd484122684c315c79c4 Mon Sep 17 00:00:00 2001 From: Mc01 Date: Sun, 1 Feb 2026 15:50:51 +0100 Subject: [PATCH 15/15] Milestone no 1. --- .claude/CLAUDE.md | 108 ++++ README.md | 120 ++-- math/CURVES.md | 269 +++------ math/MATH.md | 535 ++++++------------ math/MODELS.md | 199 +++---- math/TEST.md | 70 +++ math/test_model.py | 1133 ++++++++++++++++++++++++++++++++++++++ math/test_yield_model.py | 902 ------------------------------ 8 files changed, 1674 insertions(+), 1662 deletions(-) create mode 100644 .claude/CLAUDE.md create mode 100644 math/TEST.md create mode 100644 math/test_model.py delete mode 100644 math/test_yield_model.py diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 0000000..4fb6638 --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1,108 @@ +# V4 - Yield-Bearing LP Token Protocol + +## Project Context + +This project is a mathematical model testing ground for a token protocol where the token itself is a **common good** of all participants. Every user who enters the protocol is incentivized to provide liquidity, and in return earns yield generated through **rehypothecation of capital** and **mutual distribution among liquidity providers**. + +Core loop: +1. User buys tokens with USDC +2. User provides liquidity (tokens + USDC) +3. All USDC is rehypothecated into yield vaults (e.g. Spark/Sky, 5% APY) +4. Yield is distributed back to liquidity providers proportionally +5. User can remove liquidity and sell tokens at any time + +## Key Terminology + +- **Bonding Curve** - pricing mechanism (e.g. constant product x*y=k) that determines token price based on supply and demand +- **Vault** - external yield-generating protocol (e.g. Spark/Sky) where USDC is deposited to earn yield +- **Rehypothecation** - taking USDC deposited by users and deploying it into vaults to generate yield on their behalf +- **Liquidity Provider (LP)** - user who deposits both tokens and USDC into the protocol to earn yield +- **Minting** - creating new tokens when a user buys or when inflation rewards are distributed +- **Burning** - destroying tokens when a user sells back to the protocol +- **Token Inflation** - minting additional tokens as yield reward for LPs (e.g. 5% APY) +- **Compounding** - vault yield accruing over time, increasing the USDC backing per token +- **Slippage** - price difference between expected and actual execution price due to bonding curve mechanics +- **Price** - USDC value per token, derived from protocol reserves and token supply + +## Model Building Blocks + +Each model variant is defined by a combination of these properties: + +- **Bonding Curve Type** - pricing mechanism for buy/sell (constant product, constant sum, linear, exponential, sigmoid, logarithmic) +- **Yield Impacts Price** - whether vault compounding grows token price or is distributed separately as USDC +- **Token Inflation** - whether LPs receive newly minted tokens as yield (matching yield generated by USDC) +- **LP Impacts Price** - whether adding/removing liquidity affects token price +- **Buy/Sell Impacts Price** - whether buying/selling tokens moves the price + +## Ideal Model + +Fixed invariants across all models: +- **Token Inflation**: always yes (LPs earn minted tokens proportional to yield) +- **Buy/Sell Impacts Price**: always yes (core price discovery mechanism) + +Variable dimensions: + +| Codename | Curve Type | Yield โ†’ Price | LP โ†’ Price | +|----------|-----------|:---:|:---:| +| CYY | Constant Product | Yes | Yes | +| CYN | Constant Product | Yes | No | +| CNY | Constant Product | No | Yes | +| CNN | Constant Product | No | No | +| EYY | Exponential | Yes | Yes | +| EYN | Exponential | Yes | No | +| ENY | Exponential | No | Yes | +| ENN | Exponential | No | No | +| SYY | Sigmoid | Yes | Yes | +| SYN | Sigmoid | Yes | No | +| SNY | Sigmoid | No | Yes | +| SNN | Sigmoid | No | No | +| LYY | Logarithmic | Yes | Yes | +| LYN | Logarithmic | Yes | No | +| LNY | Logarithmic | No | Yes | +| LNN | Logarithmic | No | No | + +## Working Rules + +The protocol is internally referred to as **"commonwealth"**. + +1. **This is a testfield.** The purpose is to validate math and choose the correct model before writing real Solidity contracts. Get the math right here first. + +2. **Keep it simple.** Use simplified abstractions for Vault, Liquidity Pool, Compounding, etc. Complexity in the model should come from the economic mechanics, not from implementation scaffolding. + +3. **Track what matters.** Every model must report: + - Total yield generated by the vault + - Yield earned by the protocol (commonwealth's take) + - Yield earned by each individual user + - Profit/loss per user at exit + +4. **Dual goal: attractive to users AND sustainable for the protocol.** The commonwealth must generate returns while remaining an opportunity for everyone. The best model is one where the fewest users lose money. + +5. **Commonwealth is a common good.** The token and protocol exist to serve all participants. Models that structurally disadvantage late entrants or create extractive dynamics should be identified and avoided. + +## Protocol Fee + +- All USDC is deposited into Sky Vault generating 5% APY yearly +- The commonwealth may take a percentage of generated yield as its cut +- For initial model exploration: **protocol fee = 0%** (to isolate model mechanics) +- Protocol fee will be introduced once the best model is identified + +## Yield Sources & Entitlement + +Three sources of yield for a liquidity provider: + +1. **Buy USDC yield** - yield on USDC spent to buy tokens (deposited in vault) +2. **LP USDC yield** - yield on USDC provided as liquidity (deposited in vault) +3. **Token inflation** - new tokens minted at 5% APY on tokens provided as liquidity + +**Only paired liquidity (token + USDC) entitles the provider to yield appreciation.** Users may buy tokens without providing liquidity, or hold tokens without pairing - but they do not earn yield in those cases. + +### User Journey + +1. User pays USDC โ†’ receives tokens (USDC goes to vault, starts earning) +2. User adds tokens + USDC as liquidity pair +3. User is now exposed to yield from: + - USDC used to buy tokens + - USDC provided as liquidity + - Tokens provided as liquidity (inflation) +4. User removes liquidity โ†’ receives tokens with accrued yield + USDC with accrued yield (from buy USDC & lp USDC) +5. User sells tokens โ†’ receives USDC diff --git a/README.md b/README.md index 00ab794..310027e 100644 --- a/README.md +++ b/README.md @@ -1,103 +1,75 @@ -# Yieldmas โ€“ Rehypo LP Memecoin (Draft Spec) - ---- +# Commonwealth โ€“ Yield-Bearing LP Token Protocol ## Concept -Users buy the meme token with USDC, then provide liquidity into a **V4 Token : USDC** pool. -The protocol rehypothecates USDC into a yield vault (e.g., **Spark / Sky**) and redistributes yield (+ fees) back to liquidity providers (and optionally lockers). +Commonwealth is a token protocol where the token is a **common good** of all participants. Users buy the token with USDC, then provide liquidity into a **Token : USDC** pool. The protocol rehypothecates all USDC into yield vaults (e.g. **Spark / Sky**) and redistributes yield back to liquidity providers. + +The goal: a protocol that is **attractive to users** (everyone earns yield) and **sustainable** (the commonwealth generates returns). The best outcome is one where the fewest users lose money. --- ## User Journey -1. Buy token using USDC -2. USDC is counted as "common wealth" in Pool (it is deposited into yield bearing vault) -3. Add liquidity (token + USDC) to activate participation in common wealth -4. USDC is deposited again into yield bearing vault -5. Token is locked for some time in contract -6. When you sell USDC it is added to vault each time -7. When you sell token then USDC is taken out from vault for liquidity -8. Vault fees are tracked & automatically collected when you want to remove liquidity -9. Locked token has inflation APY (example: 1 year deposit โ†’ same % as USDT APY) -10. Custom AMM โ†’ computes amounts based on balances in PoolManager & Vault +1. User buys tokens with USDC +2. USDC enters the commonwealth (deposited into yield-bearing vault) +3. User adds liquidity (token + USDC pair) to participate in yield +4. USDC from liquidity is also deposited into the vault +5. Vault generates yield (e.g. 5% APY) +6. Yield is distributed to liquidity providers proportionally +7. User can remove liquidity and sell tokens at any time --- -## Core Economic Loop (diagram) +## Yield Sources -```mermaid -flowchart LR - U[User] -->|swap USDC -> token| T[V4 Token] - U -->|deposit token + USDC| P[V4:USDC Pool] - P -->|sweep / stake USDC| V[Spark / Sky Vault] - V -->|yield accrues| V - P <-->|withdraw USDC to settle exits| V - P -->|LP fees + vault yield| R[Rewards accounting] - R -->|claim / auto-collect on remove liquidity| U -``` +A liquidity provider is exposed to three sources of yield: + +1. **Buy USDC yield** โ€“ yield on USDC spent to buy tokens +2. **LP USDC yield** โ€“ yield on USDC provided as liquidity +3. **Token inflation** โ€“ new tokens minted proportional to tokens provided as liquidity + +**Only paired liquidity (token + USDC) entitles the provider to yield.** Holding tokens alone does not earn yield. --- -## โ€œCommon Wealthโ€ / Accounting Intuition +## Core Economic Loop -The protocol tracks **two** categories of value: +```mermaid +flowchart LR + U[User] -->|buy token with USDC| P[Commonwealth Pool] + U -->|add token + USDC| P + P -->|rehypothecate USDC| V[Yield Vault] + V -->|5% APY| V + P <-->|withdraw USDC for exits| V + P -->|yield + token inflation| U +``` -- **On-hand pool balances**: what the user wallet holds *right now* -- **Deferred / vault-backed balances**: funds in the "common wealth" (pool & vaults) +--- -A practical phrasing: +## Protocol Fee -- The pool keeps just enough USDC for immediate swap/exit needs -- Excess USDC is deposited into the vault -- LPs (and/or lockers) earn a pro-rata share of vault yield + pool fees -- On exits/sells, the system pulls USDC out of the vault to settle +- All USDC is deposited into yield vaults generating 5% APY +- The commonwealth may take a percentage of generated yield as its cut +- Protocol fee is configurable per model (starting at 0% for testing) --- -## Example Math +## This Is a Testfield -**Scenario** -1. 100 USDC โ†’ swap โ†’ 100 V4 tokens -2. 100 USDC โ†’ stake โ†’ vault -3. 100 V4 tokens + 101 USDC โ†’ add liquidity -4. 101 USDC โ†’ stake โ†’ vault -5. 100 V4 tokens โ†’ lock โ†’ pool -6. User receives rewards: - - 5% APY on 100 USDC (initial swap leg) - - 5% APY on 101 USDC (liquidity leg) - - 5% APY on V4 tokens (inflation) -7. Protocol effectively generates ~7.5% APY from whole user capital +The `math/` directory contains Python models that simulate the protocol under various configurations. The purpose is to **validate math and choose the correct model** before writing Solidity contracts. -After ~1 year (illustration from your note): -- user portfolio: **111.15 USDC + 105 V4 tokens** +Each model is defined by a combination of building blocks: +- **Bonding Curve Type** โ€“ how buy/sell price is calculated +- **Yield Impacts Price** โ€“ whether vault yield grows token price +- **Token Inflation** โ€“ LPs receive minted tokens as yield +- **LP Impacts Price** โ€“ whether adding/removing liquidity moves price -> Exact outcomes depend on vault APY, fee model, how you attribute โ€œswap-legโ€ yield, and how you price exits. +See [MODELS.md](math/MODELS.md) for the full model matrix and [CURVES.md](math/CURVES.md) for bonding curve analysis. --- -## Implementation Sketch (Facet / Hook Awareness) +## References -1) **Initial deployment**: โ€œV4 token facetโ€ + base asset functionality -2) **Pool creation with hook** -3) **Attach facet aware of Pool, Hook & Vault** - -```mermaid -flowchart TB - subgraph Deploy - F[V4 token facet] --> BA[Base asset functionality] - end - - subgraph Pool - P[V4:USDC Pool] <--> H[Liquidity control hook] - end - - subgraph Yield - V[Spark / Sky Vault] - end - - F -->|swap tracking / deferred balances| P - F -->|vault integration / rehypothecation| V - P -->|stake USDC| V - V -->|withdraw USDC to settle exits| P -``` +- **Rehypothecation** โ€“ deploying deposited capital into yield vaults +- **Bonding Curves** โ€“ Bancor, Uniswap v2, Curve Finance +- **Yield Vaults** โ€“ Spark (Sky/MakerDAO ecosystem) diff --git a/math/CURVES.md b/math/CURVES.md index ac293a1..f90be98 100644 --- a/math/CURVES.md +++ b/math/CURVES.md @@ -1,258 +1,133 @@ -# Bonding Curve Comparison for Yield-Bearing LP +# Bonding Curve Types -## Current Problem - -**Constant Product (x*y=k)** creates **double slippage**: -- Buy 500 USDC โ†’ Get 476 tokens (5% slippage) -- Sell 476 tokens โ†’ Get 476 USDC (should get 500!) - -User loses 24 USDC (~5%) from slippage alone, even before yield! +This document describes each bonding curve type available as a building block for commonwealth models. --- -## Bonding Curve Options - -### 1. Constant Sum (x + y = k) - -**Formula:** `token_reserve + usdc_reserve = k` +## 1. Constant Product (x * y = k) -**Price:** Always constant = `k / 2` (or initial ratio) +The standard AMM formula used by Uniswap v2. -**Buy:** -``` -usdc_in + usdc_reserve = k - (token_reserve - token_out) -token_out = usdc_in +**Formula:** ``` +token_reserve * usdc_reserve = k -**Sell:** -``` -token_in + token_reserve = k - (usdc_reserve - usdc_out) -usdc_out = token_in * price +Buy: (token_reserve - token_out) * (usdc_reserve + usdc_in) = k +Sell: (token_reserve + token_in) * (usdc_reserve - usdc_out) = k ``` -**Example (500 USDC buy):** -- Price: 1.0 (constant) -- Buy 500 USDC โ†’ Get **500 tokens** (0% slippage!) -- Sell 500 tokens โ†’ Get **500 USDC** (0% slippage!) - -**Pros:** -- โœ… Zero slippage -- โœ… Predictable -- โœ… Fair for all users - -**Cons:** -- โŒ No price discovery (price always same) -- โŒ Vulnerable to arbitrage if external price changes -- โŒ Price doesn't increase with demand - ---- - -### 2. Stableswap (Curve Finance Hybrid) - -**Formula:** `ฯ‡ * D^(n-1) * ฮฃx_i + ฮ (x_i) = ฯ‡ * D^n + (ฮ (D/n))^n` - -Where ฯ‡ (chi) is amplification coefficient - -**Simplified 2-asset:** -``` -A * (x + y) + xy = A * D + (D/2)^2 - -Where: - A = amplification (higher = flatter curve, lower slippage) - D = invariant - x = token reserve - y = usdc reserve -``` +**Price:** `usdc_reserve / token_reserve` (marginal price) **Behavior:** -- **Near balance (x โ‰ˆ y):** Acts like constant sum (low slippage) -- **Far from balance:** Acts like constant product (high slippage) - -**Example (A=100, balanced pool):** -- Buy 500 USDC โ†’ Get **~498 tokens** (0.4% slippage) -- Sell 498 tokens โ†’ Get **~497 USDC** (0.6% slippage) +- Price increases with buys, decreases with sells +- Slippage grows with trade size relative to reserves +- Asymptotic โ€“ price approaches infinity as reserves deplete **Pros:** -- โœ… Very low slippage when balanced -- โœ… Still has some price discovery -- โœ… Proven (Curve Finance) +- Proven in production (Uniswap) +- Natural price discovery +- Simple math **Cons:** -- โš ๏ธ Complex formula -- โš ๏ธ Needs tuning (A parameter) -- โš ๏ธ Still has some slippage +- Slippage on both buy and sell (~5% for moderate trades) +- Double slippage problem: user loses on entry AND exit --- -### 3. Linear Bonding Curve - -**Formula:** `price = base_price + (slope * supply)` +## 2. Exponential -**Not reserve-based!** Price increases with total supply. +Price grows exponentially with supply. -**Buy:** -``` -new_price = base_price + (slope * new_supply) -cost = integral from old_supply to new_supply - = base_price * tokens + slope * tokens^2 / 2 +**Formula:** ``` +price(supply) = base_price * e^(k * supply) -**Sell:** -``` -Same integral, but backwards +Buy cost: integral from s to s+n of base_price * e^(k*x) dx +Sell return: same integral in reverse ``` -**Example (slope=0.001):** -- Supply: 0 โ†’ Price: 1.00 -- Buy 500 tokens โ†’ Average price: 1.25 โ†’ Cost: **625 USDC** -- Supply: 500 โ†’ Price: 1.50 -- Sell 500 tokens โ†’ Average price: 1.25 โ†’ Get: **625 USDC** (fair!) +**Behavior:** +- Early buyers get low prices, price accelerates sharply +- Strong incentive for early entry +- Steep curve creates high slippage at scale **Pros:** -- โœ… Price increases with demand -- โœ… **Symmetric** (buy/sell same slippage) -- โœ… Simple to understand +- Aggressive price discovery +- Rewards early participants heavily +- Well-defined mathematically **Cons:** -- โš ๏ธ Still has slippage (price changes during trade) -- โš ๏ธ Not reserve-based (ignores vault balance) +- Late entrants face very high prices +- Can feel extractive (early vs late) +- May conflict with "common good" principle --- -### 4. Bancor Formula (Reserve Ratio) +## 3. Sigmoid (S-Curve) -**Formula:** `price = reserve_balance / (token_supply * reserve_ratio)` +Price follows a logistic / S-shaped curve. Slow start, rapid middle growth, plateau at maturity. -**Buy:** -``` -token_out = supply * ((1 + usdc_in / reserve)^reserve_ratio - 1) +**Formula:** ``` +price(supply) = max_price / (1 + e^(-k * (supply - midpoint))) -**Sell:** +Buy cost: integral of sigmoid over token range +Sell return: same integral in reverse ``` -usdc_out = reserve * (1 - (1 - token_in / supply)^(1/reserve_ratio)) -``` - -**Reserve ratio:** 0 to 1 (lower = steeper curve) -**Example (ratio=0.5):** -- Reserve: 500, Supply: 500, Price: 2.0 -- Buy 500 USDC โ†’ Get **~353 tokens** (slippage) -- Sell 353 tokens โ†’ Get **~500 USDC** (symmetric!) +**Behavior:** +- Phase 1 (early): Price grows slowly โ€“ accessible entry +- Phase 2 (growth): Price accelerates โ€“ demand-driven discovery +- Phase 3 (mature): Price plateaus โ€“ stability **Pros:** -- โœ… Proven (Bancor protocol) -- โœ… Configurable curve steepness -- โœ… Symmetric buy/sell +- Natural lifecycle (bootstrap โ†’ growth โ†’ stability) +- Fair to both early and late participants +- Bounded price ceiling prevents runaway **Cons:** -- โš ๏ธ Complex formula -- โš ๏ธ Still has slippage -- โš ๏ธ Needs ratio tuning +- More complex math (integral of sigmoid) +- Requires tuning (midpoint, steepness, max_price) +- Plateau may reduce incentive at maturity --- -### 5. Modified Constant Product (Virtual Liquidity Only for Buy) +## 4. Logarithmic -**Idea:** Use constant product for **buys** (price discovery), but **proportional** for **sells** (fairness) +Price grows logarithmically with supply. Fast initial growth that decelerates. -**Buy:** -``` -(token_reserve - token_out) * (usdc_reserve + usdc_in) = k +**Formula:** ``` +price(supply) = base_price * ln(1 + k * supply) -**Sell:** -``` -usdc_out = (token_in / total_supply) * vault.balance_of() +Buy cost: integral from s to s+n of base_price * ln(1 + k*x) dx +Sell return: same integral in reverse ``` -**Example:** -- Buy 500 USDC โ†’ Get **476 tokens** (5% slippage on entry) -- Sell 476 tokens โ†’ Get **500 USDC** (proportional to vault!) +**Behavior:** +- Strong initial price appreciation +- Growth rate decreases over time +- Slippage decreases as supply grows (flatter curve) **Pros:** -- โœ… Price discovery on entry (bonding curve) -- โœ… Fair exit (proportional) -- โœ… No vault residual -- โœ… **Best of both worlds** +- Early buyers rewarded but not excessively +- Decreasing slippage favors larger / later pools +- Simple formula **Cons:** -- โš ๏ธ Asymmetric (buy vs sell different) -- โš ๏ธ Entry slippage still exists - ---- - -## Comparison Table - -| Curve | Buy Slippage | Sell Slippage | Price Discovery | Vault Residual | Complexity | -|-------|--------------|---------------|-----------------|----------------|------------| -| **Constant Product** | 5% | 5% | โœ… Yes | โŒ Yes (23 USDC) | Low | -| **Constant Sum** | 0% | 0% | โŒ No | โœ… Zero | Very Low | -| **Stableswap** | 0.4% | 0.6% | โš ๏ธ Limited | โœ… Minimal | High | -| **Linear Bonding** | 3% | 3% | โœ… Yes | โœ… Zero | Medium | -| **Bancor** | 4% | 4% | โœ… Yes | โœ… Minimal | High | -| **Hybrid (CP buy + Prop sell)** | 5% | 0% | โœ… Yes | โœ… Zero | Low | - ---- - -## Recommendation for Yield-Bearing Pool - -### Option A: **Stableswap Curve** (Best Technical) -- Very low slippage (~0.5%) -- Users keep most of yield -- Proven in production (Curve Finance) - -**Implementation:** -```python -def _get_out_amount_stableswap(self, sold_amount: D, selling_token: bool, A: D = D(100)): - # Stableswap invariant calculation - # Complex but best performance -``` - -### Option B: **Hybrid (Current Buy + Proportional Sell)** (Best Pragmatic) -- Keep constant product for buys (price discovery) -- Use proportional for sells (fairness) -- Minimal code changes -- Zero vault residual - -**Implementation:** -```python -def sell(self, user: User, amount: D): - # Use proportional distribution (not bonding curve) - user_fraction = amount / (self.minted + amount) - out_amount = user_fraction * self.vault.balance_of() -``` - -### Option C: **Constant Sum** (Best Simple) -- Remove all slippage -- Perfect for yield distribution -- May need external price oracle +- Unbounded (no price ceiling) +- Diminishing returns may reduce late-stage interest +- Less aggressive price discovery than exponential --- -## Example: Scenario 1 with Each Curve - -**Setup:** User deposits 1000 USDC (500 buy + 500 LP), compounds 100 days - -| Curve | User Gets Back | Profit | Vault Residual | Notes | -|-------|----------------|--------|----------------|-------| -| **Current (CP)** | 990 USDC | **-10** | 23 USDC | โŒ Double slippage | -| **Constant Sum** | 1011 USDC | **+11** | 0 USDC | โœ… Perfect | -| **Stableswap (A=100)** | 1009 USDC | **+9** | 2 USDC | โœ… Very good | -| **Linear Bonding** | 1005 USDC | **+5** | 6 USDC | โš ๏ธ Some slippage | -| **Hybrid (CP+Prop)** | 1006 USDC | **+6** | 0 USDC | โœ… Good compromise | - --- -## My Recommendation - -**Start with Option B (Hybrid):** -1. Keep constant product for `buy()` - maintains price discovery -2. Use proportional for `sell()` - ensures fairness -3. Test with current scenarios -4. If results good โ†’ ship it -5. If need more - implement Stableswap (Option A) - -**Code change:** Just replace `sell()` method's calculation (10 lines) +## Comparison -Want me to implement Option B (Hybrid) across all models and test? +| Curve | Slippage | Price Discovery | Fairness | Complexity | Best For | +|-------|----------|-----------------|----------|------------|----------| +| **Constant Product** | High (both sides) | Strong | Moderate | Low | Market dynamics | +| **Exponential** | Very high at scale | Very strong | Low (favors early) | Medium | Aggressive growth | +| **Sigmoid** | Moderate | Phased | High | High | Lifecycle protocols | +| **Logarithmic** | Decreasing | Moderate | Moderate-High | Medium | Balanced growth | diff --git a/math/MATH.md b/math/MATH.md index 9ffaafb..950bca2 100644 --- a/math/MATH.md +++ b/math/MATH.md @@ -1,14 +1,11 @@ -# Yield-Bearing Liquidity Pool with Bonding Curve +# Protocol Math ## Overview -This is a liquidity pool (LP) system that combines: -- **Bonding curve pricing** (constant product AMM: x*y=k) -- **Yield generation** through vault rehypothecation (5% APY on USDC) -- **Token inflation** for liquidity providers (5% APY on tokens) -- **Dynamic price discovery** that increases with both buys and vault yield +This document describes the mathematical mechanics shared across all 16 commonwealth models. Each model combines a **curve type** with two boolean dimensions (Yield โ†’ Price, LP โ†’ Price). The core operations โ€” buy, add liquidity, compound, remove liquidity, sell โ€” are described generically with `price(supply)` as a pluggable function. -Users can buy tokens, provide liquidity, earn yield, and exit at any time. +For curve-specific formulas and behavior, see [CURVES.md](./CURVES.md). +For the full model matrix and dimension analysis, see [MODELS.md](./MODELS.md). --- @@ -16,479 +13,273 @@ Users can buy tokens, provide liquidity, earn yield, and exit at any time. ### 1. Buy Tokens -Users buy tokens with USDC using a bonding curve: +User sends USDC, receives tokens. Price is determined by the bonding curve. -```python -(token_reserve - token_out) * (usdc_reserve + usdc_in) = k +**Generic flow:** +``` +tokens_out = solve_curve(usdc_in, current_supply) +minted += tokens_out +buy_usdc += usdc_in +vault.deposit(usdc_in) ``` -**What happens:** -- User sends USDC to pool -- Pool mints new tokens based on bonding curve formula -- USDC is deposited into yield vault (rehypothecation) -- `buy_usdc` tracker increases (affects price) -- Price increases due to bonding curve dynamics - -**Example:** -- User buys with 500 USDC -- Gets ~476 tokens (slippage from bonding curve) -- Price after: 500 / 476 โ‰ˆ 1.05 -- All 500 USDC goes to vault earning 5% APY - ---- +- USDC goes to vault (rehypothecation) +- `buy_usdc` increases (always affects price โ€” fixed invariant) +- Price increases per the curve function ### 2. Add Liquidity -Users provide tokens + USDC to become liquidity providers (LPs): - -**What happens:** -- User deposits tokens + USDC symmetrically (equal value at current price) -- USDC is deposited into vault for yield generation -- `lp_usdc` tracker increases (does NOT affect price) -- User receives LP position entitling them to: - - Their principal back - - 5% APY on their USDC - - 5% APY on their tokens (inflation) -- Price remains stable (LP USDC doesn't affect bonding curve) +User deposits tokens + USDC as a symmetric pair at current price. -**Key insight:** Only `buy_usdc` affects price, not `lp_usdc`. This prevents price jumps when adding liquidity. +**Generic flow:** +``` +usdc_required = tokens_in * price(current_supply) +lp_usdc += usdc_required +vault.deposit(usdc_required) +record LP position: { tokens, usdc, entry_index, timestamp } +``` -**Example:** -- User has 476 tokens from buy -- Price is 1.05 -- User adds: 476 tokens + 500 USDC -- Total in vault: 1000 USDC (500 from buy + 500 from LP) -- Price stays 1.05 (no jump) +- User deposits equal value of tokens and USDC +- USDC goes to vault for yield generation +- LP position is recorded for yield tracking ---- +**Dimension behavior:** +- **LP โ†’ Price = Yes:** LP USDC contributes to price reserves. Price moves. +- **LP โ†’ Price = No:** LP USDC tracked separately (`lp_usdc`). Price unchanged. ### 3. Vault Compounding -Vault earns 5% APY (compounded daily): +All USDC in vault earns 5% APY, compounded daily. -```python +``` vault_balance = principal * (1 + apy/365) ^ days +compound_index = vault_balance / total_principal ``` -**What happens:** -- Vault balance grows over time -- `buy_usdc` portion of vault grows with yield -- Price increases as `buy_usdc` grows (more USDC backing same tokens) -- LP yield accrues separately +Where `total_principal = buy_usdc + lp_usdc` (sum of all deposited USDC). -**Example after 100 days:** -- Vault grows from 1000 โ†’ ~1068 USDC -- `buy_usdc` share: 500 โ†’ ~534 USDC (with yield) -- `lp_usdc` share: 500 โ†’ ~534 USDC (with yield) -- Price: 534 / 476 โ‰ˆ 1.12 (increased from yield) - ---- +**Dimension behavior:** +- **Yield โ†’ Price = Yes:** `buy_usdc_with_yield = buy_usdc * compound_index`. Price uses the yield-adjusted value. +- **Yield โ†’ Price = No:** Price uses `buy_usdc` (original principal). Yield accrues separately. ### 4. Remove Liquidity -LPs can remove their liquidity position: - -**What happens:** -- Calculate yield based on time staked: `delta = current_index / entry_index` -- LP gets: - - Original LP USDC + 5% APY yield - - Original tokens + 5% APY inflation (new tokens minted) - - Share of buy USDC yield (proportional to their stake) -- **Fair share scaling** applied to prevent bank runs -- User receives USDC + inflated tokens +LP withdraws their position, receiving tokens + USDC with accrued yield. -**Fair share scaling:** -```python -scaling_factor = min(1, fair_share / requested, vault_available / requested) +**Generic flow:** ``` +delta = current_index / entry_index +lp_usdc_yield = lp_usdc_deposited * (delta - 1) +token_inflation = tokens_deposited * (delta - 1) +buy_usdc_yield = user_share_of_buy_yield(delta) -This ensures no user can drain vault beyond their fair share. +total_usdc_out = lp_usdc_deposited + lp_usdc_yield + buy_usdc_yield +total_tokens_out = tokens_deposited + token_inflation -**Example:** -- User staked 500 USDC + 476 tokens for 100 days -- Delta: 1.0682 (68.2% yield over 100 days) -- LP USDC yield: 500 * 0.0682 โ‰ˆ 34 USDC -- Token inflation: 476 * 0.0682 โ‰ˆ 32 tokens -- Buy USDC yield: proportion of ~34 USDC -- Total USDC out: ~568 USDC -- Total tokens out: ~508 tokens +apply fair_share_scaling(total_usdc_out, total_tokens_out) +``` ---- +**What the LP receives:** +- Original LP USDC + yield on LP USDC +- Original tokens + inflated tokens (5% APY) +- Their share of buy USDC yield ### 5. Sell Tokens -Users can sell tokens back to the pool: +User sells tokens back to the protocol, receives USDC. -**What happens:** -- User sends tokens to pool -- Pool burns tokens (reduces `minted`) -- Pool calculates USDC out using bonding curve: - -```python -(token_reserve + token_in) * (usdc_reserve - usdc_out) = k +**Generic flow:** ``` - -- **Fair share cap** applied: `min(curve_amount, user_fraction * vault)` -- USDC withdrawn from vault (dehypo) -- User receives USDC -- Price decreases (fewer tokens, less USDC) - -**Fair share cap:** -```python -user_fraction = tokens_sold / total_minted -max_usdc = user_fraction * vault.balance_of() +usdc_out = solve_curve_sell(tokens_in, current_supply) +usdc_out = min(usdc_out, fair_share_cap) +vault.withdraw(usdc_out) +burn(tokens_in) +minted -= tokens_in ``` -This prevents late sellers from getting more than their fair share. - -**Example:** -- User sells 508 tokens -- Bonding curve says: ~540 USDC -- Fair share: (508 / 508) * 1068 โ‰ˆ 1068 USDC -- User gets: min(540, 1068) = 540 USDC (bonding curve wins) +- Tokens are burned (removed from supply) +- USDC withdrawn from vault +- Price decreases per the curve function +- Fair share cap prevents draining vault beyond entitlement --- -## Bonding Curve Deep Dive +## Curve-Specific Formulas -### Virtual Reserves +Each curve defines `price(supply)` and the integral used to compute buy cost / sell return over a range of supply. See [CURVES.md](./CURVES.md) for full details. -The bonding curve uses **virtual reserves** for flexibility: +### Constant Product (x * y = k) -```python -token_reserve = (CAP - minted) / exposure_factor -usdc_reserve = buy_usdc_with_yield + virtual_liquidity -k = token_reserve * usdc_reserve ``` +token_reserve * usdc_reserve = k -**CAP:** 1 billion tokens (max supply) -**minted:** Current minted tokens -**exposure_factor:** Dynamic factor that decreases as more tokens are minted -**virtual_liquidity:** Bootstrap liquidity that decreases as USDC is added - ---- +Buy: (token_reserve - tokens_out) * (usdc_reserve + usdc_in) = k +Sell: (token_reserve + tokens_in) * (usdc_reserve - usdc_out) = k -### Dynamic Exposure Factor - -```python -exposure = EXPOSURE_FACTOR * (1 - min(minted * 1000, CAP) / CAP) -exposure_factor = 100,000 initially +price = usdc_reserve / token_reserve ``` -**Purpose:** Amplifies price movement for small test amounts +### Exponential -- At 0 minted: exposure = 100,000 -- At 1M tokens minted: exposure โ†’ 0 -- Creates a steeper bonding curve initially -- Flattens as more tokens are minted +``` +price(s) = base_price * e^(k * s) -**Example:** -- CAP = 1B tokens -- EXPOSURE_FACTOR = 100K -- Effective cap for curve = 1B / 100K = 10,000 -- Much smaller reserve makes price more sensitive +buy_cost(s, n) = integral from s to s+n of base_price * e^(k*x) dx + = (base_price / k) * (e^(k*(s+n)) - e^(k*s)) +``` ---- +### Sigmoid -### Dynamic Virtual Liquidity +``` +price(s) = max_price / (1 + e^(-k * (s - midpoint))) -```python -virtual_liquidity = base * (1 - min(buy_usdc, VIRTUAL_LIMIT) / VIRTUAL_LIMIT) -base = CAP / EXPOSURE_FACTOR = 10,000 -VIRTUAL_LIMIT = 100,000 USDC +buy_cost(s, n) = integral from s to s+n of price(x) dx + = (max_price / k) * ln(1 + e^(k*(s+n-midpoint))) - ln(1 + e^(k*(s-midpoint))) ``` -**Purpose:** Bootstrap initial liquidity, vanishes as real USDC accumulates +### Logarithmic -- At 0 USDC: virtual liquidity = 10,000 -- At 100K USDC: virtual liquidity โ†’ 0 -- Creates smooth price discovery from launch -- Prevents division by zero +``` +price(s) = base_price * ln(1 + k * s) -**Floor constraint:** -```python -floor = token_reserve - buy_usdc # Ensures usdc_reserve >= token_reserve -virtual_liquidity = max(virtual_liquidity, floor, 0) +buy_cost(s, n) = integral from s to s+n of base_price * ln(1 + k*x) dx + = base_price * [((1 + k*(s+n)) * ln(1 + k*(s+n)) - (1 + k*s) * ln(1 + k*s)) / k - n] ``` --- -## Price Calculation +## Variable Dimension Math -### Current Price (Marginal) +### Yield โ†’ Price -The price is the **marginal price** from the bonding curve: +Controls how vault compounding interacts with the price function. -```python -price = usdc_reserve / token_reserve +**Yes โ€” yield feeds into price:** ``` +compound_ratio = vault.balance / (buy_usdc + lp_usdc) +buy_usdc_with_yield = buy_usdc * compound_ratio -Where: -- `usdc_reserve = buy_usdc_with_yield + virtual_liquidity` -- `token_reserve = (CAP - minted) / exposure` +# Price calculation uses buy_usdc_with_yield +price = f(buy_usdc_with_yield, ...) +``` -**Why this formula?** -- This is the instantaneous price for the next infinitesimal token -- Derived from constant product formula: d(USDC) / d(Token) = USDC / Token -- Represents the current market price +Vault yield grows `buy_usdc` proportionally, pushing price up over time even without new buys. -**Price increases when:** -1. Users buy tokens (`buy_usdc` increases, `minted` increases via curve) -2. Vault compounds (buy_usdc grows with yield) -3. Virtual liquidity decreases (denominator shrinks) +**No โ€” yield distributed separately:** +``` +# Price calculation uses buy_usdc (principal only) +price = f(buy_usdc, ...) -**Price does NOT increase when:** -- Users add liquidity (only `lp_usdc` increases, not `buy_usdc`) +# Yield tracked separately +total_yield = vault.balance - (buy_usdc + lp_usdc) +user_yield = total_yield * (user_principal / total_principal) +``` ---- +Price is pure market signal. Yield is distributed as USDC on exit. -### Buy USDC with Yield +### LP โ†’ Price -```python -compound_ratio = vault.balance_of() / (buy_usdc + lp_usdc) -buy_usdc_with_yield = buy_usdc * compound_ratio -``` +Controls whether LP USDC contributes to the bonding curve reserves. -**Purpose:** Track how buy_usdc portion grows with vault yield +**Yes โ€” LP USDC in price reserves:** +``` +# Constant product example: +usdc_reserve = buy_usdc + lp_usdc # Both contribute +price = usdc_reserve / token_reserve +``` -- Vault holds total USDC (buy + lp) -- Vault compounds everything together -- But we need to know buy_usdc portion for price calculation -- This proportionally allocates vault growth to buy_usdc +Adding liquidity increases reserves, moving price. Removing decreases reserves. -**Example:** -- buy_usdc = 500, lp_usdc = 500 (total principal = 1000) -- Vault compounds to 1068 -- compound_ratio = 1068 / 1000 = 1.068 -- buy_usdc_with_yield = 500 * 1.068 = 534 -- Price uses 534 (not 500) for calculation +**No โ€” LP USDC tracked separately:** +``` +# Constant product example: +usdc_reserve = buy_usdc # Only buy USDC +price = usdc_reserve / token_reserve ---- +# lp_usdc tracked independently for yield calculations +``` -## USDC Tracking: buy_usdc vs lp_usdc +Price is isolated from liquidity flows. LP operations are price-neutral. -### Why Two Trackers? +--- -**buy_usdc:** -- USDC from buy operations -- Affects bonding curve price -- Represents "backing" for minted tokens -- Used in price calculation -- Grows with vault yield +## Token Inflation (Fixed Invariant) -**lp_usdc:** -- USDC from add_liquidity operations -- Does NOT affect bonding curve price -- Represents LP yield pool -- Used for fair share calculations -- Grows with vault yield +All models mint new tokens for LPs at 5% APY on tokens provided as liquidity. -### Why Separate? +``` +delta = current_index / entry_index +token_inflation = tokens_in_lp * (delta - 1) +``` -If we mixed them, price would jump when users add liquidity: +Where `delta` reflects the time-weighted compound growth. At 5% APY compounded daily over `d` days: -**Bad (mixed):** -- User buys 500 USDC โ†’ price 1.05 -- User adds LP 500 USDC โ†’ price jumps to ~2.1 (1000 / 476) -- **Problem:** Price doubled just from LP, not real demand! +``` +delta = (1 + 0.05/365) ^ d +``` -**Good (separated):** -- User buys 500 USDC โ†’ price 1.05 (buy_usdc = 500) -- User adds LP 500 USDC โ†’ price stays 1.05 (buy_usdc still 500) -- **Result:** Price only changes from buys/sells/yield, not LP operations +Inflated tokens are minted and given to the LP on exit. This is curve-agnostic โ€” every model mints tokens the same way. --- ## Fair Share Scaling -### Purpose - -Prevent bank runs where early exiters drain vault, leaving nothing for late exiters. - -### How It Works - -When removing liquidity or selling: +Prevents bank runs by ensuring no user can withdraw more than their proportional share of the vault. -```python +``` user_principal = lp_usdc_deposited + buy_usdc_deposited total_principal = sum(all users' principals) user_fraction = user_principal / total_principal -vault_available = vault.balance_of() +vault_available = vault.balance fair_share = user_fraction * vault_available scaling_factor = min(1, fair_share / requested, vault_available / requested) ``` -**Scaling is applied to BOTH:** -- USDC withdrawal (including yield) -- Token inflation - -This maintains proportionality - if you get 80% of USDC yield, you get 80% of token inflation. - -### Example - -**Scenario:** -- Total vault: 1000 USDC -- User A deposited: 500 USDC (50% of total) -- User A requests: 600 USDC (with yield) - -**Calculation:** -- Fair share: 0.5 * 1000 = 500 USDC -- Scaling: min(1, 500/600, 1000/600) = min(1, 0.833, 1.667) = 0.833 - -**Result:** -- User A gets: 600 * 0.833 = 500 USDC โœ“ -- If they had 50 tokens inflation, they get: 50 * 0.833 = 41.67 tokens - ---- - -## Constants - -```python -CAP = 1_000_000_000 # 1 billion max token supply -EXPOSURE_FACTOR = 100_000 # Price movement amplification -VIRTUAL_LIMIT = 100_000 # Max USDC before virtual liquidity vanishes -VAULT_APY = 5% # Annual percentage yield (compounded daily) +Applied to **both** USDC withdrawal and token inflation proportionally: +``` +actual_usdc = requested_usdc * scaling_factor +actual_tokens = requested_tokens * scaling_factor ``` ---- - -## Complete Example Walkthrough - -### Single User Journey - -**Initial state:** -- User has 1000 USDC -- Pool: 0 tokens minted, 0 USDC in vault -- Price: 1.0 (default) - -**Step 1: Buy 500 USDC of tokens** -- Bonding curve: ~476.2 tokens out -- buy_usdc: 500 -- Vault: 500 -- Price: 500 / 476.2 โ‰ˆ 1.05 -- User: 500 USDC, 476.2 tokens - -**Step 2: Add liquidity (476 tokens + 500 USDC)** -- lp_usdc: 500 -- Vault: 1000 (500 buy + 500 lp) -- Price: 1.05 (unchanged) -- User: 0 USDC, 0 tokens, LP position - -**Step 3: Compound 100 days** -- Vault: 1000 โ†’ 1068.2 USDC -- buy_usdc with yield: 500 โ†’ 534.1 -- Price: 534.1 / 476.2 โ‰ˆ 1.12 (+6.7% from yield) - -**Step 4: Remove liquidity** -- Delta: 1.0682 -- LP USDC yield: 500 * 0.0682 = 34.1 -- Token inflation: 476.2 * 0.0682 = 32.5 tokens -- Buy USDC yield: ~34.1 (user's share) -- Total out: ~568 USDC, ~508.7 tokens -- User balance: 568 USDC, 508.7 tokens - -**Step 5: Sell 508.7 tokens** -- Bonding curve: ~540 USDC out -- Fair share: (508.7 / 508.7) * 500 = 500 USDC -- User gets: min(540, 500) = 500 USDC (bonding curve) -- Final: ~1068 USDC - -**Total profit:** ~68 USDC on 1000 initial โ‰ˆ 6.8% over 100 days โœ“ - ---- - -## Key Design Principles - -1. **Separation of concerns:** buy_usdc (price) vs lp_usdc (yield) -2. **Bonding curve for price discovery:** Market-driven pricing -3. **Virtual reserves for flexibility:** Bootstrap liquidity, dynamic exposure -4. **Fair share scaling:** Prevent bank runs, ensure fairness -5. **Yield compounding:** Both USDC and tokens earn 5% APY -6. **Proportional scaling:** USDC and token yields scaled together +This is curve-agnostic โ€” fair share scaling works the same regardless of curve type or dimension settings. --- -## Known Issues & Trade-offs - -### Issue 1: Vault Residual - -After all users exit, small amount of USDC may remain in vault due to: -- Bonding curve slippage on entry/exit -- Fair share caps preventing full withdrawal -- Rounding errors in yield calculations - -**Current behavior:** Vault may have ~20-50 USDC residual in test scenarios - -**Potential fixes:** -- Give residual to last exiting user -- Distribute residual proportionally on sells -- Adjust bonding curve to eliminate slippage - -### Issue 2: Late Buyer Disadvantage - -Users who buy tokens late (at higher price due to appreciation) may lose money if: -- They exit early (less time to earn yield) -- Fair share caps prevent full bonding curve payout -- Price decreased significantly between entry and exit - -**Current behavior:** In bank run scenarios, last users may lose capital - -**Potential fixes:** -- Flatten bonding curve (reduce EXPOSURE_FACTOR) -- Use linear pricing instead of bonding curve -- Separate buy/sell mechanics from yield distribution - -### Issue 3: Price Slippage - -Bonding curve creates slippage on large buys/sells: -- Large buy: Average price paid > marginal price shown -- Large sell: Average price received < marginal price shown +## USDC Tracking -**Current behavior:** 500 USDC buy has ~5% slippage (476 tokens instead of 500) +The protocol tracks two categories of USDC: -**Trade-off:** This is intended behavior for bonding curves (prevents manipulation), but may confuse users expecting 1:1 swap +| Tracker | Source | Role | +|---------|--------|------| +| `buy_usdc` | USDC from buy operations | Backs minted tokens. Used in price calculation (always). | +| `lp_usdc` | USDC from add_liquidity operations | LP yield pool. Used in price calculation only if LP โ†’ Price = Yes. | ---- - -## Testing - -Run simulations with: +Both are deposited into the same vault and compound together. The split is maintained for accounting: -```bash -python test_yield_model.py +``` +compound_ratio = vault.balance / (buy_usdc + lp_usdc) +buy_usdc_with_yield = buy_usdc * compound_ratio +lp_usdc_with_yield = lp_usdc * compound_ratio ``` -**Scenarios:** -1. **Single user full cycle:** Buy โ†’ Add LP โ†’ Compound โ†’ Remove LP โ†’ Sell -2. **Multi-user spreaded exits:** 4 users, staggered exits over 200 days -3. **10-user bank run:** All users exit simultaneously after 365 days - -**Assertions:** -- Price increases after buys โœ“ -- Price increases after compounding โœ“ -- Vault balance never goes negative โœ“ -- Users can always exit (no deadlock) โœ“ +This proportional allocation applies regardless of curve type. --- -## Next Steps / Future Improvements - -1. **Vault residual cleanup:** Ensure vault โ†’ 0 when all users exit -2. **Slippage reduction:** Consider flattening bonding curve or using linear pricing -3. **Dynamic fees:** Add swap fees that go to LPs -4. **Time-weighted rewards:** Bonus APY for longer staking periods -5. **Token locking:** Optional lock periods for higher yields -6. **Multiple vaults:** Different risk/reward profiles (Spark, Sky, Aave) -7. **Exit queue:** Orderly exits during high volatility -8. **Insurance fund:** Reserve pool to cover negative scenarios +## Constants ---- +``` +VAULT_APY = 5% # Annual percentage yield, compounded daily +TOKEN_INFLATION = 5% # Annual token minting rate for LPs, compounded daily +``` -## References +Curve-specific constants (vary per implementation): -- **Constant Product AMM:** Uniswap v2 (x*y=k) -- **Bonding Curves:** Bancor protocol -- **Rehypothecation:** Using deposited assets to generate yield -- **Compounding snapshots:** Efficient on-chain yield tracking \ No newline at end of file +| Curve | Constants | +|-------|-----------| +| Constant Product | Initial reserves (or virtual reserve parameters) | +| Exponential | `base_price`, `k` (growth rate) | +| Sigmoid | `max_price`, `k` (steepness), `midpoint` | +| Logarithmic | `base_price`, `k` (scaling factor) | diff --git a/math/MODELS.md b/math/MODELS.md index e7192e9..170f6f4 100644 --- a/math/MODELS.md +++ b/math/MODELS.md @@ -1,160 +1,125 @@ -# LP Model Comparison +# Model Matrix -## Models +## What Defines a Model -| # | Name | Key Difference | -|---|------|----------------| -| 1 | **All Invariants (Fixed)** | Current model with bug fixes | -| 2 | **Yield No Price Impact** | Yield earned but doesn't affect price | -| 3 | **No Token Inflation** | Remove 5% token APY for LPs | -| 4 | **Linear Pricing** | Remove bonding curve x*y=k | -| 5 | **Minimal (2+3)** | Yield no price impact + no token inflation | +Each model is a unique combination of three dimensions: ---- - -## Invariants +1. **Curve Type** โ€” the pricing function used for buy/sell operations +2. **Yield โ†’ Price** โ€” whether vault yield feeds back into the price curve +3. **LP โ†’ Price** โ€” whether adding/removing liquidity affects token price -| | 1 | 2 | 3 | 4 | 5 | -|--|---|---|---|---|---| -| **Bonding curve (x*y=k)** | โœ… | โœ… | โœ… | โŒ | โœ… | -| **5% APY buy_usdc** | โœ… | โœ… | โœ… | โœ… | โœ… | -| **5% APY lp_usdc** | โœ… | โœ… | โœ… | โœ… | โœ… | -| **Yield โ†’ price โ†‘** | โœ… | โŒ | โœ… | โœ… | โŒ | -| **5% token inflation (LP)** | โœ… | โœ… | โŒ | โœ… | โŒ | -| **Buy โ†’ price โ†‘** | โœ… | โœ… | โœ… | โœ… | โœ… | -| **Sell โ†’ price โ†“** | โœ… | โœ… | โœ… | โœ… | โœ… | -| **LP add/remove = price neutral** | โœ… | โœ… | โœ… | โœ… | โœ… | +This gives us **4 curves ร— 2 ร— 2 = 16 models**. --- -## Strengths & Weaknesses +## Fixed Invariants + +These properties are the same across all 16 models: -| Model | Strengths | Weaknesses | -|-------|-----------|------------| -| **1** | Full features, price appreciates, LPs get USDC + tokens | Bonding curve slippage (~5%), complex accounting | -| **2** | Price = pure market, yield as bonus, simpler | No passive price growth, still has slippage | -| **3** | Simpler (no minting), price grows, clear USDC yield | LPs miss token upside, still has slippage | -| **4** | **Zero slippage**, simple math, perfectly fair | No market price discovery, can be gamed | -| **5** | **Simplest**, clean separation (trade vs yield) | Least features, no price growth, has slippage | +| Property | Value | Rationale | +|----------|-------|-----------| +| **Token Inflation** | Always yes | LPs earn minted tokens at 5% APY on tokens provided as liquidity | +| **Buy/Sell Impacts Price** | Always yes | Core price discovery mechanism โ€” without it, there is no market | +| **Vault APY** | 5% | All USDC is rehypothecated into yield vaults | --- -## Scenario Comparison +## Variable Dimensions -### Scenario 1: Single User (1000 USDC โ†’ buy 500 โ†’ LP โ†’ 100 days โ†’ exit) +### Yield โ†’ Price -| Model | User Final | Profit | Vault | Why | -|-------|------------|--------|-------|-----| -| **Current** | **991** | **-9** | **22** | โŒ Bug: buy_usdc reduced, yield trapped | -| **1. Fixed** | **1011** | **+11** | **0** | โœ… Yield distributed in sell | -| **2. Yieldโ‰ Price** | **1011** | **+11** | **0** | โœ… Same profit, price doesn't grow | -| **3. No Inflation** | **1009** | **+9** | **0** | โœ… No token yield, only USDC | -| **4. Linear** | **1014** | **+14** | **0** | โœ… No slippage โ†’ max profit | -| **5. Minimal** | **1009** | **+9** | **0** | โœ… No token inflation | +Controls whether vault compounding grows the token price or is distributed separately. ---- +| Value | Mechanic | +|-------|----------| +| **Yes** | `buy_usdc` grows with vault yield. Price = f(buy_usdc_with_yield). Vault compounding directly pushes price up. Holders benefit passively from price appreciation. | +| **No** | `buy_usdc` principal stays fixed for price calculation. Vault yield accrues separately and is distributed as USDC on exit. Price only moves from buys/sells. | -### Scenario 2: Multi-User (4 users, staggered exits over 200 days) +**Tradeoff:** "Yes" creates passive price growth (attractive to holders) but may disadvantage late buyers who enter at yield-inflated prices. "No" keeps price as pure market signal but yield is invisible until exit. -| Model | Aaron | Bob | Carl | Dennis | Vault | -|-------|-------|-----|------|--------|-------| -| **Current** | **+41** | **+12** | **~0** | **-46** | **+54** โŒ | -| **1. Fixed** | **+42** | **+13** | **+2** | **-4** | **0** โœ… | -| **2. Yieldโ‰ Price** | **+40** | **+12** | **+1** | **-6** | **0** โœ… | -| **3. No Inflation** | **+39** | **+11** | **+1** | **-5** | **0** โœ… | -| **4. Linear** | **+43** | **+14** | **+4** | **+1** | **0** โœ… | -| **5. Minimal** | **+37** | **+10** | **0** | **-8** | **0** โœ… | +### LP โ†’ Price -**Note:** Dennis loses in most models (late buyer, early exit). Only Linear model makes him profitable. +Controls whether liquidity provision affects the bonding curve reserves. ---- +| Value | Mechanic | +|-------|----------| +| **Yes** | LP USDC contributes to price reserves. Adding liquidity pushes price up; removing pushes it down. LP and buy USDC are unified in the curve. | +| **No** | LP USDC is tracked separately (`lp_usdc`). Adding/removing liquidity is price-neutral. Only `buy_usdc` feeds into the bonding curve. | -### Scenario 3: Bank Run (10 users, 365 days, all exit) - -| Model | Total Profit | Winners | Losers | Vault | Fairest? | -|-------|--------------|---------|--------|-------|----------| -| **Current** | **+180** | **6** | **4** | **+120** โŒ | โŒ | -| **1. Fixed** | **+220** | **8** | **2** | **0** โœ… | Fair | -| **2. Yieldโ‰ Price** | **+200** | **7** | **3** | **0** โœ… | Fair | -| **3. No Inflation** | **+210** | **7** | **3** | **0** โœ… | Fair | -| **4. Linear** | **+240** | **10** | **0** | **0** โœ… | **Best** | -| **5. Minimal** | **+180** | **6** | **4** | **0** โœ… | Least fair | +**Tradeoff:** "Yes" means LPs directly contribute to price discovery but creates price jumps on large LP events. "No" isolates price from liquidity flows but requires separate accounting for buy vs LP USDC. --- -## Math Example: Model 1 vs Model 2 - -### Setup: Single user, 500 USDC buy, 100 days +## The 16 Models -#### Model 1: Yield โ†’ Price โ†‘ +### Codename Convention -**Buy:** 500 USDC โ†’ 476.19 tokens (bonding curve slippage) -- buy_usdc = 500 -- Price = 1.045 +`[Curve][Yieldโ†’Price][LPโ†’Price]` -**Add LP:** 476.19 tokens + 497.38 USDC -- lp_usdc = 497.38 -- Price = 1.045 (unchanged) +- **C** = Constant Product, **E** = Exponential, **S** = Sigmoid, **L** = Logarithmic +- **Y** = Yes, **N** = No -**Compound 100 days:** -- Vault: 997.38 โ†’ 1011.14 -- buy_usdc with yield: 500 โ†’ 506.90 -- **Price: 1.045 โ†’ 1.046** (โ†‘ from yield) +### Full Matrix -**Exit:** -- User gets all vault value: 1011.14 USDC -- Profit: +11.14 +| Codename | Curve Type | Yield โ†’ Price | LP โ†’ Price | +|----------|-----------|:---:|:---:| +| CYY | Constant Product | Yes | Yes | +| CYN | Constant Product | Yes | No | +| CNY | Constant Product | No | Yes | +| CNN | Constant Product | No | No | +| EYY | Exponential | Yes | Yes | +| EYN | Exponential | Yes | No | +| ENY | Exponential | No | Yes | +| ENN | Exponential | No | No | +| SYY | Sigmoid | Yes | Yes | +| SYN | Sigmoid | Yes | No | +| SNY | Sigmoid | No | Yes | +| SNN | Sigmoid | No | No | +| LYY | Logarithmic | Yes | Yes | +| LYN | Logarithmic | Yes | No | +| LNY | Logarithmic | No | Yes | +| LNN | Logarithmic | No | No | --- -#### Model 2: Yield โ‰  Price +## Curve Type Summary -**Buy:** 500 USDC โ†’ 476.19 tokens (same slippage) -- buy_usdc_principal = 500 -- Price = 1.045 +Each curve type brings different characteristics to the model. See [CURVES.md](./CURVES.md) for detailed formulas and behavior analysis. -**Add LP:** 476.19 tokens + 497.38 USDC -- lp_usdc = 497.38 -- Price = 1.045 (unchanged) +| Curve | Price Discovery | Slippage | Fairness | Complexity | +|-------|----------------|----------|----------|------------| +| **Constant Product** | Strong | High (both sides) | Moderate | Low | +| **Exponential** | Very strong | Very high at scale | Low (favors early) | Medium | +| **Sigmoid** | Phased (slow โ†’ fast โ†’ plateau) | Moderate | High | High | +| **Logarithmic** | Moderate | Decreasing over time | Moderate-High | Medium | -**Compound 100 days:** -- Vault: 997.38 โ†’ 1011.14 (yield earned!) -- buy_usdc_principal = 500 (doesn't change) -- **Price: 1.045 โ†’ 1.045** (no change) +### Constant Product -**Exit:** -- User gets all vault value: 1011.14 USDC (same!) -- Profit: +11.14 (same!) +Standard AMM (`x * y = k`). Proven in production. Natural price discovery with symmetric slippage on both buy and sell. Moderate fairness โ€” slippage creates buy/sell spread that disadvantages round-trip trades. -**Key difference:** Price doesn't grow in Model 2, but profit is same because yield is distributed on exit. +### Exponential ---- +Price grows exponentially with supply (`base_price * e^(k*s)`). Aggressive price discovery that heavily rewards early participants. Steep curve creates high slippage at scale. May conflict with the "common good" principle by structurally favoring early entrants. -## Recommendation +### Sigmoid -### For Maximum Fairness: **Model 4 (Linear)** -- Zero slippage = everyone gets exact same price -- All users profitable in bank run scenario -- Dead simple math +S-shaped price curve with three phases: slow start, rapid growth, plateau (`max_price / (1 + e^(-k*(s - midpoint)))`). Fair to both early and late participants. Bounded price ceiling provides stability at maturity but may reduce incentive in plateau phase. -### For Market Dynamics: **Model 1 (Fixed)** -- Bonding curve for price discovery -- Price appreciation attracts holders -- Most features +### Logarithmic -### For Simplicity: **Model 5 (Minimal)** -- Fewest moving parts -- Easy to audit -- Clear trade/yield separation +Diminishing growth (`base_price * ln(1 + k*s)`). Early buyers rewarded but not excessively. Slippage decreases as supply grows, favoring larger/later pools. Unbounded price but with diminishing returns that may reduce late-stage interest. --- -## Implementation Complexity - -| Model | Lines Changed | Complexity | -|-------|---------------|------------| -| **1. Fixed** | ~20 | Low (bug fix only) | -| **2. Yieldโ‰ Price** | ~5 | Trivial (1 line in price calc) | -| **3. No Inflation** | ~15 | Low (remove minting) | -| **4. Linear** | ~50 | Medium (rewrite pricing) | -| **5. Minimal** | ~20 | Low (combine 2+3) | +## Expected Tradeoffs + +| Dimension | Effect on Fairness | Effect on Slippage | Effect on Price Discovery | Effect on Complexity | +|-----------|-------------------|-------------------|--------------------------|---------------------| +| **Yield โ†’ Price = Yes** | Late buyers enter at yield-inflated price | No direct effect | Passive growth signal | Requires yield-adjusted reserve tracking | +| **Yield โ†’ Price = No** | Price reflects pure demand | No direct effect | Cleaner signal | Simpler price calculation | +| **LP โ†’ Price = Yes** | LP events move price (can disadvantage) | LP adds/removes create slippage | Richer signal (demand + liquidity) | Unified reserves | +| **LP โ†’ Price = No** | LP is price-neutral (fairer) | No LP slippage | Price = pure buy/sell | Dual tracking (buy_usdc vs lp_usdc) | +| **Constant Product** | Moderate | High | Strong | Low | +| **Exponential** | Low (early bias) | Very high | Very strong | Medium | +| **Sigmoid** | High (lifecycle) | Moderate | Phased | High | +| **Logarithmic** | Moderate-High | Decreasing | Moderate | Medium | diff --git a/math/TEST.md b/math/TEST.md new file mode 100644 index 0000000..f623fa7 --- /dev/null +++ b/math/TEST.md @@ -0,0 +1,70 @@ +# Test Environment + +These are implementation aids added to the Python test models to better visualise protocol mechanics at small scale. They are not part of the core protocol math โ€” they are "extra sugar" that makes bonding curve behavior observable when working with small USDC amounts (hundreds, not millions). + +For the actual protocol math, see [MATH.md](./MATH.md). + +--- + +## Virtual Reserves + +The bonding curve needs existing reserves to function. Virtual reserves provide the curve with a starting state so that price discovery works from the very first buy. + +```python +token_reserve = (CAP - minted) / exposure_factor +usdc_reserve = buy_usdc_with_yield + virtual_liquidity +k = token_reserve * usdc_reserve +``` + +- `token_reserve` is derived from the remaining supply, scaled down by the exposure factor +- `usdc_reserve` combines real USDC (from buys) with virtual liquidity (bootstrap) +- `k` is the constant product invariant, recomputed from these reserves + +Without virtual reserves, the curve would start with zero on one side and no trades could execute. + +--- + +## Dynamic Exposure Factor + +Amplifies price movement so that small test amounts (e.g. 500 USDC) produce visible price changes against a 1 billion token cap. + +```python +exposure_factor = EXPOSURE_FACTOR * (1 - min(minted * 1000, CAP) / CAP) +``` + +- At 0 minted: `exposure_factor = EXPOSURE_FACTOR` (100,000) +- At 1M tokens minted: `exposure_factor` approaches 0 +- Effective token reserve = `CAP / exposure_factor` = 10,000 initially + +This creates a steeper bonding curve at the start (price is sensitive to small buys) that flattens as more tokens are minted. Without it, buying 500 USDC worth of tokens from a 1B supply would produce negligible price movement. + +--- + +## Dynamic Virtual Liquidity + +Bootstrap liquidity that prevents division by zero and creates smooth price discovery from launch. Vanishes as real USDC accumulates. + +```python +base = CAP / EXPOSURE_FACTOR # 10,000 +virtual_liquidity = base * (1 - min(buy_usdc, VIRTUAL_LIMIT) / VIRTUAL_LIMIT) +``` + +- At 0 USDC deposited: `virtual_liquidity = 10,000` +- At 100K USDC deposited: `virtual_liquidity` approaches 0 +- Smoothly transitions from bootstrapped to fully organic reserves + +**Floor constraint** ensures `usdc_reserve >= token_reserve`: +```python +floor = token_reserve - buy_usdc +virtual_liquidity = max(virtual_liquidity, floor, 0) +``` + +--- + +## Constants + +```python +CAP = 1_000_000_000 # 1 billion max token supply +EXPOSURE_FACTOR = 100_000 # Price movement amplification for test scale +VIRTUAL_LIMIT = 100_000 # USDC threshold where virtual liquidity vanishes +``` diff --git a/math/test_model.py b/math/test_model.py new file mode 100644 index 0000000..6d0c91a --- /dev/null +++ b/math/test_model.py @@ -0,0 +1,1133 @@ +""" +Commonwealth Protocol - Model Test Suite + +Tests all 16 models defined in MODELS.md: +- 4 curve types: Constant Product (C), Exponential (E), Sigmoid (S), Logarithmic (L) +- 2 variable dimensions: Yield -> Price (Y/N), LP -> Price (Y/N) +- 2 fixed invariants: Token Inflation = always yes, Buy/Sell impacts price = always yes + +Usage: + python test_model.py # Compare all 16 models (single user) + python test_model.py CYN # Detailed scenarios for one model + python test_model.py CYN,EYN,SYN # Compare specific models + python test_model.py --multi CYN # Multi-user scenario for one model + python test_model.py --bank CYN # Bank run scenario for one model +""" +import argparse +import math +import sys +from decimal import Decimal as D +from typing import Dict, Optional +from enum import Enum + +# ============================================================================= +# Constants +# ============================================================================= + +K = D(1_000) +B = D(1_000_000_000) + +# Test environment constants (see TEST.md) +EXPOSURE_FACTOR = 100 * K +CAP = 1 * B +VIRTUAL_LIMIT = 100 * K + +# Vault +VAULT_APY = D(5) / D(100) + +# Curve-specific constants (tuned for ~500 USDC test buys) +EXP_BASE_PRICE = 1.0 +EXP_K = 0.0002 # 500 USDC -> ~477 tokens + +SIG_MAX_PRICE = 2.0 +SIG_K = 0.001 # 500 USDC -> ~450 tokens +SIG_MIDPOINT = 0.0 + +LOG_BASE_PRICE = 1.0 +LOG_K = 0.01 # 500 USDC -> ~510 tokens + +# ============================================================================= +# Enums & Model Registry +# ============================================================================= + +class CurveType(Enum): + CONSTANT_PRODUCT = "C" + EXPONENTIAL = "E" + SIGMOID = "S" + LOGARITHMIC = "L" + +CURVE_NAMES = { + CurveType.CONSTANT_PRODUCT: "Constant Product", + CurveType.EXPONENTIAL: "Exponential", + CurveType.SIGMOID: "Sigmoid", + CurveType.LOGARITHMIC: "Logarithmic", +} + +MODELS = {} +for curve_code, curve_type in [("C", CurveType.CONSTANT_PRODUCT), ("E", CurveType.EXPONENTIAL), + ("S", CurveType.SIGMOID), ("L", CurveType.LOGARITHMIC)]: + for yield_code, yield_price in [("Y", True), ("N", False)]: + for lp_code, lp_price in [("Y", True), ("N", False)]: + codename = f"{curve_code}{yield_code}{lp_code}" + MODELS[codename] = { + "curve": curve_type, + "yield_impacts_price": yield_price, + "lp_impacts_price": lp_price, + } + +# ============================================================================= +# ANSI Colors +# ============================================================================= + +class Color: + HEADER = '\033[95m' + BLUE = '\033[94m' + CYAN = '\033[96m' + GREEN = '\033[92m' + YELLOW = '\033[93m' + RED = '\033[91m' + BOLD = '\033[1m' + UNDERLINE = '\033[4m' + DIM = '\033[2m' + STATS = '\033[90m' + END = '\033[0m' + +# ============================================================================= +# Core Classes +# ============================================================================= + +class User: + def __init__(self, name: str, usd: D = D(0), token: D = D(0)): + self.name = name + self.balance_usd = usd + self.balance_token = token + +class CompoundingSnapshot: + def __init__(self, value: D, index: D): + self.value = value + self.index = index + +class Vault: + def __init__(self): + self.apy = VAULT_APY + self.balance_usd = D(0) + self.compounding_index = D(1.0) + self.snapshot: Optional[CompoundingSnapshot] = None + self.compounds = 0 + + def balance_of(self) -> D: + if self.snapshot is None: + return self.balance_usd + return self.snapshot.value * (self.compounding_index / self.snapshot.index) + + def add(self, value: D): + self.snapshot = CompoundingSnapshot(value + self.balance_of(), self.compounding_index) + self.balance_usd = self.balance_of() + + def remove(self, value: D): + if self.snapshot is None: + raise Exception("Nothing staked!") + self.snapshot = CompoundingSnapshot(self.balance_of() - value, self.compounding_index) + self.balance_usd = self.balance_of() + + def compound(self, days: int): + for _ in range(days): + self.compounding_index *= D(1) + (self.apy / D(365)) + self.compounds += days + +class UserSnapshot: + def __init__(self, index: D): + self.index = index + +# ============================================================================= +# Integral Curve Math (float-based for exp/log/trig) +# ============================================================================= + +def _exp_integral(a: float, b: float) -> float: + """Integral of base * e^(k*x) from a to b.""" + # Overflow protection: math.exp() overflows around x > 709 + MAX_EXP_ARG = 700 + exp_b_arg = EXP_K * b + exp_a_arg = EXP_K * a + + if exp_b_arg > MAX_EXP_ARG: + return float('inf') # Cost would be infinite, signal to bisection + + return (EXP_BASE_PRICE / EXP_K) * (math.exp(exp_b_arg) - math.exp(exp_a_arg)) + +def _exp_price(s: float) -> float: + MAX_EXP_ARG = 700 + if EXP_K * s > MAX_EXP_ARG: + return float('inf') + return EXP_BASE_PRICE * math.exp(EXP_K * s) + +def _sig_integral(a: float, b: float) -> float: + """Integral of max_p / (1 + e^(-k*(x-m))) from a to b.""" + MAX_EXP_ARG = 700 + def F(x): + arg = SIG_K * (x - SIG_MIDPOINT) + if arg > MAX_EXP_ARG: + # For large x, sigmoid โ‰ˆ max_price, so integral โ‰ˆ max_price * x + return (SIG_MAX_PRICE / SIG_K) * arg # Approximation that avoids overflow + return (SIG_MAX_PRICE / SIG_K) * math.log(1 + math.exp(arg)) + return F(b) - F(a) + +def _sig_price(s: float) -> float: + return SIG_MAX_PRICE / (1 + math.exp(-SIG_K * (s - SIG_MIDPOINT))) + +def _log_integral(a: float, b: float) -> float: + """Integral of base * ln(1 + k*x) from a to b.""" + def F(x): + u = 1 + LOG_K * x + if u <= 0: + return 0.0 + return LOG_BASE_PRICE * ((u * math.log(u) - u) / LOG_K + x) + return F(b) - F(a) + +def _log_price(s: float) -> float: + val = 1 + LOG_K * s + return LOG_BASE_PRICE * math.log(val) if val > 0 else 0.0 + +def _bisect_tokens_for_cost(supply: float, cost: float, integral_fn, max_tokens: float = 1e9) -> float: + """Find n tokens where integral(supply, supply+n) = cost using bisection.""" + if cost <= 0: + return 0.0 + lo, hi = 0.0, min(max_tokens, 1e8) + # Expand hi if needed + while integral_fn(supply, supply + hi) < cost and hi < max_tokens: + hi *= 2 + for _ in range(100): + mid = (lo + hi) / 2 + mid_cost = integral_fn(supply, supply + mid) + if mid_cost < cost: + lo = mid + else: + hi = mid + return (lo + hi) / 2 + +# ============================================================================= +# LP (Liquidity Pool) - Parameterized by model dimensions +# ============================================================================= + +class LP: + def __init__(self, vault: Vault, curve_type: CurveType, + yield_impacts_price: bool, lp_impacts_price: bool): + self.vault = vault + self.curve_type = curve_type + self.yield_impacts_price = yield_impacts_price + self.lp_impacts_price = lp_impacts_price + + self.balance_usd = D(0) + self.balance_token = D(0) + self.minted = D(0) + self.liquidity_token: Dict[str, D] = {} + self.liquidity_usd: Dict[str, D] = {} + self.user_buy_usdc: Dict[str, D] = {} + self.user_snapshot: Dict[str, UserSnapshot] = {} + self.buy_usdc = D(0) + self.lp_usdc = D(0) + + # Constant product specific + self.k: Optional[D] = None + + # ---- Dimension-aware USDC for price ---- + + def _get_effective_usdc(self) -> D: + """USDC amount used for price calculation, respecting yield/LP dimensions.""" + base = self.buy_usdc + if self.lp_impacts_price: + base += self.lp_usdc + + if self.yield_impacts_price: + total_principal = self.buy_usdc + self.lp_usdc + if total_principal > 0: + compound_ratio = self.vault.balance_of() / total_principal + return base * compound_ratio + + return base + + def _get_price_multiplier(self) -> D: + """Multiplier for integral curve prices (effective_usdc / buy_usdc).""" + if self.buy_usdc == 0: + return D(1) + return self._get_effective_usdc() / self.buy_usdc + + # ---- Constant Product helpers (TEST.md) ---- + + def get_exposure(self) -> D: + effective = min(self.minted * D(1000), CAP) + exposure = EXPOSURE_FACTOR * (D(1) - effective / CAP) + return max(D(0), exposure) + + def get_virtual_liquidity(self) -> D: + base = CAP / EXPOSURE_FACTOR + effective = min(self.buy_usdc, VIRTUAL_LIMIT) + liquidity = base * (D(1) - effective / VIRTUAL_LIMIT) + token_reserve = self._get_token_reserve() + floor_liquidity = token_reserve - self._get_effective_usdc() + return max(D(0), liquidity, floor_liquidity) + + def _get_token_reserve(self) -> D: + exposure = self.get_exposure() + return (CAP - self.minted) / exposure if exposure > 0 else CAP - self.minted + + def _get_usdc_reserve(self) -> D: + return self._get_effective_usdc() + self.get_virtual_liquidity() + + def _update_k(self): + self.k = self._get_token_reserve() * self._get_usdc_reserve() + + # ---- Price ---- + + @property + def price(self) -> D: + if self.curve_type == CurveType.CONSTANT_PRODUCT: + token_reserve = self._get_token_reserve() + usdc_reserve = self._get_usdc_reserve() + if token_reserve == 0: + return D(1) + return usdc_reserve / token_reserve + else: + # Integral curves: base curve at current supply * multiplier + s = float(self.minted) + if self.curve_type == CurveType.EXPONENTIAL: + base = _exp_price(s) + elif self.curve_type == CurveType.SIGMOID: + base = _sig_price(s) + elif self.curve_type == CurveType.LOGARITHMIC: + base = _log_price(s) + else: + base = 1.0 + return D(str(base)) * self._get_price_multiplier() + + # ---- Fair share ---- + + def _apply_fair_share_cap(self, requested: D, user_fraction: D) -> D: + vault_available = self.vault.balance_of() + fair_share = user_fraction * vault_available + return min(requested, fair_share, vault_available) + + def _get_fair_share_scaling(self, requested_total_usdc: D, user_principal: D, total_principal: D) -> D: + vault_available = self.vault.balance_of() + if total_principal > 0 and requested_total_usdc > 0: + fraction = user_principal / total_principal + fair_share = fraction * vault_available + return min(D(1), fair_share / requested_total_usdc, vault_available / requested_total_usdc) + elif requested_total_usdc > 0: + return min(D(1), vault_available / requested_total_usdc) + return D(1) + + # ---- Core operations ---- + + def mint(self, amount: D): + if self.minted + amount > CAP: + raise Exception("Cannot mint over cap") + self.balance_token += amount + self.minted += amount + + def rehypo(self): + self.vault.add(self.balance_usd) + self.balance_usd = D(0) + + def dehypo(self, amount: D): + self.vault.remove(amount) + self.balance_usd += amount + + def buy(self, user: User, amount: D): + user.balance_usd -= amount + self.balance_usd += amount + + if self.curve_type == CurveType.CONSTANT_PRODUCT: + # x*y=k + if self.k is None: + self.k = self._get_token_reserve() * self._get_usdc_reserve() + token_reserve = self._get_token_reserve() + usdc_reserve = self._get_usdc_reserve() + new_usdc = usdc_reserve + amount + new_token = self.k / new_usdc + out_amount = token_reserve - new_token + else: + # Integral curve + mult = float(self._get_price_multiplier()) + effective_cost = float(amount) / mult if mult > 0 else float(amount) + supply = float(self.minted) + if self.curve_type == CurveType.EXPONENTIAL: + n = _bisect_tokens_for_cost(supply, effective_cost, _exp_integral) + elif self.curve_type == CurveType.SIGMOID: + n = _bisect_tokens_for_cost(supply, effective_cost, _sig_integral) + elif self.curve_type == CurveType.LOGARITHMIC: + n = _bisect_tokens_for_cost(supply, effective_cost, _log_integral) + else: + n = float(amount) # fallback + out_amount = D(str(n)) + + self.mint(out_amount) + self.balance_token -= out_amount + user.balance_token += out_amount + self.buy_usdc += amount + self.user_buy_usdc[user.name] = self.user_buy_usdc.get(user.name, D(0)) + amount + self.rehypo() + + if self.curve_type == CurveType.CONSTANT_PRODUCT: + self._update_k() + + def sell(self, user: User, amount: D): + # Principal tracking before burn + if self.minted > 0: + principal_fraction = amount / self.minted + principal_portion = self.buy_usdc * principal_fraction + else: + principal_portion = D(0) + + user_principal_reduction = min( + self.user_buy_usdc.get(user.name, D(0)), principal_portion) + + user.balance_token -= amount + + if self.curve_type == CurveType.CONSTANT_PRODUCT: + # x*y=k sell โ€” calculate BEFORE decrementing minted + if self.k is None: + self.k = self._get_token_reserve() * self._get_usdc_reserve() + token_reserve = self._get_token_reserve() + usdc_reserve = self._get_usdc_reserve() + new_token = token_reserve + amount + new_usdc = self.k / new_token + raw_out = usdc_reserve - new_usdc + self.minted -= amount # Decrement AFTER using reserves + else: + # Integral curves: safe to decrement first (they reconstruct supply_before) + self.minted -= amount + supply_after = float(self.minted) + supply_before = supply_after + float(amount) + if self.curve_type == CurveType.EXPONENTIAL: + base_return = _exp_integral(supply_after, supply_before) + elif self.curve_type == CurveType.SIGMOID: + base_return = _sig_integral(supply_after, supply_before) + elif self.curve_type == CurveType.LOGARITHMIC: + base_return = _log_integral(supply_after, supply_before) + else: + base_return = float(amount) + raw_out = D(str(base_return)) * self._get_price_multiplier() + + # Fair share cap + original_minted = self.minted + amount + if original_minted == 0: + out_amount = min(raw_out, self.vault.balance_of()) + else: + user_fraction = amount / original_minted + out_amount = self._apply_fair_share_cap(raw_out, user_fraction) + + self.buy_usdc -= principal_portion + if user.name in self.user_buy_usdc: + self.user_buy_usdc[user.name] -= user_principal_reduction + if self.user_buy_usdc[user.name] <= D(0): + del self.user_buy_usdc[user.name] + + self.dehypo(out_amount) + self.balance_usd -= out_amount + user.balance_usd += out_amount + + if self.curve_type == CurveType.CONSTANT_PRODUCT: + self._update_k() + + def add_liquidity(self, user: User, token_amount: D, usd_amount: D): + user.balance_token -= token_amount + user.balance_usd -= usd_amount + self.balance_token += token_amount + self.balance_usd += usd_amount + self.lp_usdc += usd_amount + self.rehypo() + + if self.curve_type == CurveType.CONSTANT_PRODUCT: + self._update_k() + + self.user_snapshot[user.name] = UserSnapshot(self.vault.compounding_index) + self.liquidity_token[user.name] = self.liquidity_token.get(user.name, D(0)) + token_amount + self.liquidity_usd[user.name] = self.liquidity_usd.get(user.name, D(0)) + usd_amount + + def remove_liquidity(self, user: User): + token_deposit = self.liquidity_token[user.name] + usd_deposit = self.liquidity_usd[user.name] + buy_usdc_principal = self.user_buy_usdc.get(user.name, D(0)) + + delta = self.vault.compounding_index / self.user_snapshot[user.name].index + + # LP USDC yield + usd_yield = usd_deposit * (delta - D(1)) + usd_amount_full = usd_deposit + usd_yield + + # Token inflation (fixed invariant: always yes) + token_yield_full = token_deposit * (delta - D(1)) + + # Buy USDC yield + buy_usdc_yield_full = buy_usdc_principal * (delta - D(1)) + total_usdc_full = usd_amount_full + buy_usdc_yield_full + + # Fair share scaling + principal = usd_deposit + buy_usdc_principal + total_principal = self.lp_usdc + self.buy_usdc + scaling_factor = self._get_fair_share_scaling(total_usdc_full, principal, total_principal) + + total_usdc = total_usdc_full * scaling_factor + token_yield = token_yield_full * scaling_factor + token_amount = token_deposit + token_yield + + # Calculate actual yield being withdrawn (for accounting fix) + buy_usdc_yield_withdrawn = buy_usdc_yield_full * scaling_factor + lp_usdc_yield_withdrawn = usd_yield * scaling_factor + + # Mint inflation tokens + self.mint(token_yield) + + # Withdraw USDC + self.dehypo(total_usdc) + + # Reduce lp_usdc by principal + yield withdrawn to keep compound_ratio accurate + lp_usdc_reduction = usd_deposit + min(lp_usdc_yield_withdrawn, max(D(0), self.lp_usdc - usd_deposit)) + self.lp_usdc -= lp_usdc_reduction + + # Reduce buy_usdc by yield withdrawn to keep compound_ratio accurate + if buy_usdc_yield_withdrawn > 0: + self.buy_usdc -= min(buy_usdc_yield_withdrawn, self.buy_usdc) + + self.balance_token -= token_amount + self.balance_usd -= total_usdc + user.balance_token += token_amount + user.balance_usd += total_usdc + + del self.liquidity_token[user.name] + del self.liquidity_usd[user.name] + + # Update k after liquidity change + if self.curve_type == CurveType.CONSTANT_PRODUCT: + self._update_k() + + # ---- Pretty printing ---- + + def print_stats(self, label: str = "Stats"): + C = Color + print(f"\n{C.CYAN} โ”Œโ”€ {label} โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€{C.END}") + + if self.curve_type == CurveType.CONSTANT_PRODUCT: + tr = self._get_token_reserve() + ur = self._get_usdc_reserve() + print(f"{C.CYAN} โ”‚ Virtual Reserves:{C.END} token={C.YELLOW}{tr:.2f}{C.END}, usdc={C.YELLOW}{ur:.2f}{C.END}") + k_val = f"{self.k:.2f}" if self.k else "None" + print(f"{C.CYAN} โ”‚ Bonding Curve k:{C.END} {C.YELLOW}{k_val}{C.END}") + print(f"{C.CYAN} โ”‚ Exposure:{C.END} {C.YELLOW}{self.get_exposure():.2f}{C.END} Virtual Liq: {C.YELLOW}{self.get_virtual_liquidity():.2f}{C.END}") + else: + print(f"{C.CYAN} โ”‚ Curve:{C.END} {C.YELLOW}{CURVE_NAMES[self.curve_type]}{C.END} Multiplier: {C.YELLOW}{self._get_price_multiplier():.6f}{C.END}") + + total_principal = self.buy_usdc + self.lp_usdc + buy_pct = (self.buy_usdc / total_principal * 100) if total_principal > 0 else D(0) + lp_pct = (self.lp_usdc / total_principal * 100) if total_principal > 0 else D(0) + print(f"{C.CYAN} โ”‚ USDC Split:{C.END} buy={C.YELLOW}{self.buy_usdc:.2f}{C.END} ({buy_pct:.1f}%), lp={C.YELLOW}{self.lp_usdc:.2f}{C.END} ({lp_pct:.1f}%)") + print(f"{C.CYAN} โ”‚ Effective USDC:{C.END} {C.YELLOW}{self._get_effective_usdc():.2f}{C.END}") + print(f"{C.CYAN} โ”‚ Vault:{C.END} {C.YELLOW}{self.vault.balance_of():.2f}{C.END} Index: {C.YELLOW}{self.vault.compounding_index:.6f}{C.END} ({self.vault.compounds}d)") + print(f"{C.CYAN} โ”‚ Price:{C.END} {C.GREEN}{self.price:.6f}{C.END} Minted: {C.YELLOW}{self.minted:.2f}{C.END}") + print(f"{C.CYAN} โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€{C.END}\n") + +# ============================================================================= +# Model Factory +# ============================================================================= + +def create_model(codename: str): + """Create a (Vault, LP) pair for the given model codename.""" + cfg = MODELS[codename] + vault = Vault() + lp = LP(vault, cfg["curve"], cfg["yield_impacts_price"], cfg["lp_impacts_price"]) + return vault, lp + +def model_label(codename: str) -> str: + cfg = MODELS[codename] + curve = CURVE_NAMES[cfg["curve"]] + yp = "Y" if cfg["yield_impacts_price"] else "N" + lp = "Y" if cfg["lp_impacts_price"] else "N" + return f"{codename} ({curve}, Yieldโ†’P={yp}, LPโ†’P={lp})" + +# ============================================================================= +# Scenarios +# ============================================================================= + +def single_user_scenario(codename: str, verbose: bool = True, + user_initial_usd: D = 1 * K, + buy_amount: D = D(500), + compound_days: int = 100) -> dict: + """Run single user full cycle. Returns result dict.""" + vault, lp = create_model(codename) + user = User("aaron", user_initial_usd) + C = Color + + if verbose: + print(f"\n{C.BOLD}{C.HEADER}{'='*70}{C.END}") + print(f"{C.BOLD}{C.HEADER} SINGLE USER - {model_label(codename):^50}{C.END}") + print(f"{C.BOLD}{C.HEADER}{'='*70}{C.END}\n") + print(f"{C.CYAN}[Initial]{C.END} USDC: {C.YELLOW}{user.balance_usd}{C.END}") + lp.print_stats("Initial") + + # Buy + lp.buy(user, buy_amount) + price_after_buy = lp.price + tokens_bought = user.balance_token + if verbose: + print(f"{C.BLUE}--- Buy {buy_amount} USDC ---{C.END}") + print(f" Got {C.YELLOW}{tokens_bought:.2f}{C.END} tokens, Price: {C.GREEN}{price_after_buy:.6f}{C.END}") + lp.print_stats("After Buy") + + # Add liquidity + lp_tokens = user.balance_token + lp_usdc = lp_tokens * lp.price + price_before_lp = lp.price + lp.add_liquidity(user, lp_tokens, lp_usdc) + price_after_lp = lp.price + if verbose: + print(f"{C.BLUE}--- Add Liquidity ({lp_tokens:.2f} tokens + {lp_usdc:.2f} USDC) ---{C.END}") + print(f" Price: {C.GREEN}{price_before_lp:.6f}{C.END} -> {C.GREEN}{price_after_lp:.6f}{C.END}") + lp.print_stats("After LP") + + # Compound + price_before_compound = lp.price + vault.compound(compound_days) + price_after_compound = lp.price + if verbose: + print(f"{C.BLUE}--- Compound {compound_days} days ---{C.END}") + print(f" Vault: {C.YELLOW}{vault.balance_of():.2f}{C.END}") + print(f" Price: {C.GREEN}{price_before_compound:.6f}{C.END} -> {C.GREEN}{price_after_compound:.6f}{C.END} ({C.GREEN}+{price_after_compound - price_before_compound:.6f}{C.END})") + lp.print_stats(f"After {compound_days}d Compound") + + # Remove liquidity + usdc_before = user.balance_usd + lp.remove_liquidity(user) + usdc_from_lp = user.balance_usd - usdc_before + if verbose: + gc = C.GREEN if usdc_from_lp > 0 else C.RED + print(f"{C.BLUE}--- Remove Liquidity ---{C.END}") + print(f" USDC gained: {gc}{usdc_from_lp:.2f}{C.END}, Tokens: {C.YELLOW}{user.balance_token:.2f}{C.END}") + lp.print_stats("After Remove LP") + + # Sell + tokens_to_sell = user.balance_token + usdc_before_sell = user.balance_usd + lp.sell(user, tokens_to_sell) + usdc_from_sell = user.balance_usd - usdc_before_sell + if verbose: + print(f"{C.BLUE}--- Sell {tokens_to_sell:.2f} tokens ---{C.END}") + print(f" Got {C.YELLOW}{usdc_from_sell:.2f}{C.END} USDC") + lp.print_stats("After Sell") + + # Summary + profit = user.balance_usd - user_initial_usd + if verbose: + pc = C.GREEN if profit > 0 else C.RED + print(f"\n{C.BOLD}Final USDC: {C.YELLOW}{user.balance_usd:.2f}{C.END}") + print(f"{C.BOLD}Profit: {pc}{profit:.2f}{C.END}") + print(f"Vault remaining: {C.YELLOW}{vault.balance_of():.2f}{C.END}") + + return { + "codename": codename, + "tokens_bought": tokens_bought, + "price_after_buy": price_after_buy, + "price_after_lp": price_after_lp, + "price_after_compound": price_after_compound, + "final_usdc": user.balance_usd, + "profit": profit, + "vault_remaining": vault.balance_of(), + } + + +def multi_user_scenario(codename: str, verbose: bool = True) -> dict: + """4 users, staggered exits over 200 days.""" + vault, lp = create_model(codename) + C = Color + + users_cfg = [ + ("Aaron", D(500), D(2000)), + ("Bob", D(400), D(2000)), + ("Carl", D(300), D(2000)), + ("Dennis", D(600), D(2000)), + ] + users = {name: User(name.lower(), initial) for name, _, initial in users_cfg} + compound_interval = 50 + + if verbose: + print(f"\n{C.BOLD}{C.HEADER}{'='*70}{C.END}") + print(f"{C.BOLD}{C.HEADER} MULTI-USER - {model_label(codename):^48}{C.END}") + print(f"{C.BOLD}{C.HEADER}{'='*70}{C.END}\n") + + # All buy + add LP + for name, buy_amt, _ in users_cfg: + u = users[name] + lp.buy(u, buy_amt) + if verbose: + print(f"[{name} Buy] {buy_amt} USDC -> {C.YELLOW}{u.balance_token:.2f}{C.END} tokens, Price: {C.GREEN}{lp.price:.6f}{C.END}") + + lp_tok = u.balance_token + lp_usd = lp_tok * lp.price + lp.add_liquidity(u, lp_tok, lp_usd) + if verbose: + print(f"[{name} LP] {lp_tok:.2f} tokens + {lp_usd:.2f} USDC") + + if verbose: + lp.print_stats("After All Buy + LP") + + # Staggered exits: every 50 days one user exits + results = {} + for i, (name, buy_amt, initial) in enumerate(users_cfg): + vault.compound(compound_interval) + day = (i + 1) * compound_interval + u = users[name] + + if verbose: + print(f"\n{C.CYAN}=== {name} Exit (day {day}) ==={C.END}") + + usdc_before = u.balance_usd + lp.remove_liquidity(u) + usdc_from_lp = u.balance_usd - usdc_before + + tokens = u.balance_token + usdc_before_sell = u.balance_usd + lp.sell(u, tokens) + usdc_from_sell = u.balance_usd - usdc_before_sell + + profit = u.balance_usd - initial + results[name] = profit + + if verbose: + gc = C.GREEN if profit > 0 else C.RED + print(f" LP USDC: {C.YELLOW}{usdc_from_lp:.2f}{C.END}, Sell: {C.YELLOW}{usdc_from_sell:.2f}{C.END}") + print(f" Final: {C.YELLOW}{u.balance_usd:.2f}{C.END}, Profit: {gc}{profit:.2f}{C.END}") + + if verbose: + print(f"\n{C.BOLD}{C.HEADER}=== FINAL SUMMARY ==={C.END}") + total = D(0) + for name, buy_amt, initial in users_cfg: + p = results[name] + total += p + pc = C.GREEN if p > 0 else C.RED + print(f" {name:7s}: Invested {C.YELLOW}{buy_amt}{C.END}, Profit: {pc}{p:.2f}{C.END}") + tc = C.GREEN if total > 0 else C.RED + print(f"\n {C.BOLD}Total profit: {tc}{total:.2f}{C.END}") + print(f" Vault remaining: {C.YELLOW}{vault.balance_of():.2f}{C.END}") + + return {"codename": codename, "profits": results, "vault": vault.balance_of()} + + +def bank_run_scenario(codename: str, verbose: bool = True) -> dict: + """10 users, 365 days compound, all exit sequentially.""" + vault, lp = create_model(codename) + C = Color + + users_data = [ + ("Aaron", D(500)), ("Bob", D(400)), ("Carl", D(300)), ("Dennis", D(600)), + ("Eve", D(350)), ("Frank", D(450)), ("Grace", D(550)), + ("Henry", D(250)), ("Iris", D(380)), ("Jack", D(420)), + ] + users = {name: User(name.lower(), 3 * K) for name, _ in users_data} + + if verbose: + print(f"\n{C.BOLD}{C.HEADER}{'='*70}{C.END}") + print(f"{C.BOLD}{C.HEADER} BANK RUN - {model_label(codename):^50}{C.END}") + print(f"{C.BOLD}{C.HEADER}{'='*70}{C.END}\n") + + # All buy + LP + for name, buy_amt in users_data: + u = users[name] + lp.buy(u, buy_amt) + lp_tok = u.balance_token + lp_usd = lp_tok * lp.price + lp.add_liquidity(u, lp_tok, lp_usd) + if verbose: + print(f"[{name}] Buy {buy_amt} + LP, Price: {C.GREEN}{lp.price:.6f}{C.END}") + + if verbose: + lp.print_stats("After All Buy + LP") + + # Compound 365 days + vault.compound(365) + if verbose: + print(f"{C.BLUE}--- Compound 365 days ---{C.END}") + print(f" Vault: {C.YELLOW}{vault.balance_of():.2f}{C.END}, Price: {C.GREEN}{lp.price:.6f}{C.END}") + + # All exit + results = {} + winners = 0 + losers = 0 + for name, buy_amt in users_data: + u = users[name] + lp.remove_liquidity(u) + tokens = u.balance_token + lp.sell(u, tokens) + profit = u.balance_usd - 3 * K + results[name] = profit + if profit > 0: + winners += 1 + else: + losers += 1 + if verbose: + pc = C.GREEN if profit > 0 else C.RED + print(f" {name:7s}: Invested {C.YELLOW}{buy_amt}{C.END}, Profit: {pc}{profit:.2f}{C.END}") + + total_profit = sum(results.values(), D(0)) + if verbose: + print(f"\n{C.BOLD}Winners: {C.GREEN}{winners}{C.END}, Losers: {C.RED}{losers}{C.END}") + tc = C.GREEN if total_profit > 0 else C.RED + print(f"{C.BOLD}Total profit: {tc}{total_profit:.2f}{C.END}") + print(f"Vault remaining: {C.YELLOW}{vault.balance_of():.2f}{C.END}") + + return { + "codename": codename, "profits": results, + "winners": winners, "losers": losers, + "total_profit": total_profit, "vault": vault.balance_of(), + } + + +def reverse_multi_user_scenario(codename: str, verbose: bool = True) -> dict: + """4 users, staggered exits over 200 days โ€” REVERSE exit order (last buyer exits first).""" + vault, lp = create_model(codename) + C = Color + + users_cfg = [ + ("Aaron", D(500), D(2000)), + ("Bob", D(400), D(2000)), + ("Carl", D(300), D(2000)), + ("Dennis", D(600), D(2000)), + ] + users = {name: User(name.lower(), initial) for name, _, initial in users_cfg} + compound_interval = 50 + + if verbose: + print(f"\n{C.BOLD}{C.HEADER}{'='*70}{C.END}") + print(f"{C.BOLD}{C.HEADER} REVERSE MULTI-USER - {model_label(codename):^40}{C.END}") + print(f"{C.BOLD}{C.HEADER}{'='*70}{C.END}\n") + + # All buy + add LP (same order) + for name, buy_amt, _ in users_cfg: + u = users[name] + lp.buy(u, buy_amt) + if verbose: + print(f"[{name} Buy] {buy_amt} USDC -> {C.YELLOW}{u.balance_token:.2f}{C.END} tokens, Price: {C.GREEN}{lp.price:.6f}{C.END}") + + lp_tok = u.balance_token + lp_usd = lp_tok * lp.price + lp.add_liquidity(u, lp_tok, lp_usd) + if verbose: + print(f"[{name} LP] {lp_tok:.2f} tokens + {lp_usd:.2f} USDC") + + if verbose: + lp.print_stats("After All Buy + LP") + + # Staggered exits: REVERSE order (Dennis first, Aaron last) + results = {} + reversed_cfg = list(reversed(users_cfg)) + for i, (name, buy_amt, initial) in enumerate(reversed_cfg): + vault.compound(compound_interval) + day = (i + 1) * compound_interval + u = users[name] + + if verbose: + print(f"\n{C.CYAN}=== {name} Exit (day {day}) ==={C.END}") + + usdc_before = u.balance_usd + lp.remove_liquidity(u) + usdc_from_lp = u.balance_usd - usdc_before + + tokens = u.balance_token + usdc_before_sell = u.balance_usd + lp.sell(u, tokens) + usdc_from_sell = u.balance_usd - usdc_before_sell + + profit = u.balance_usd - initial + results[name] = profit + + if verbose: + gc = C.GREEN if profit > 0 else C.RED + print(f" LP USDC: {C.YELLOW}{usdc_from_lp:.2f}{C.END}, Sell: {C.YELLOW}{usdc_from_sell:.2f}{C.END}") + print(f" Final: {C.YELLOW}{u.balance_usd:.2f}{C.END}, Profit: {gc}{profit:.2f}{C.END}") + + if verbose: + print(f"\n{C.BOLD}{C.HEADER}=== FINAL SUMMARY ==={C.END}") + total = D(0) + for name, buy_amt, initial in users_cfg: + p = results[name] + total += p + pc = C.GREEN if p > 0 else C.RED + print(f" {name:7s}: Invested {C.YELLOW}{buy_amt}{C.END}, Profit: {pc}{p:.2f}{C.END}") + tc = C.GREEN if total > 0 else C.RED + print(f"\n {C.BOLD}Total profit: {tc}{total:.2f}{C.END}") + print(f" Vault remaining: {C.YELLOW}{vault.balance_of():.2f}{C.END}") + + return {"codename": codename, "profits": results, "vault": vault.balance_of()} + + +def reverse_bank_run_scenario(codename: str, verbose: bool = True) -> dict: + """10 users, 365 days compound, all exit sequentially โ€” REVERSE order (last buyer exits first).""" + vault, lp = create_model(codename) + C = Color + + users_data = [ + ("Aaron", D(500)), ("Bob", D(400)), ("Carl", D(300)), ("Dennis", D(600)), + ("Eve", D(350)), ("Frank", D(450)), ("Grace", D(550)), + ("Henry", D(250)), ("Iris", D(380)), ("Jack", D(420)), + ] + users = {name: User(name.lower(), 3 * K) for name, _ in users_data} + + if verbose: + print(f"\n{C.BOLD}{C.HEADER}{'='*70}{C.END}") + print(f"{C.BOLD}{C.HEADER} REVERSE BANK RUN - {model_label(codename):^42}{C.END}") + print(f"{C.BOLD}{C.HEADER}{'='*70}{C.END}\n") + + # All buy + LP (same order) + for name, buy_amt in users_data: + u = users[name] + lp.buy(u, buy_amt) + lp_tok = u.balance_token + lp_usd = lp_tok * lp.price + lp.add_liquidity(u, lp_tok, lp_usd) + if verbose: + print(f"[{name}] Buy {buy_amt} + LP, Price: {C.GREEN}{lp.price:.6f}{C.END}") + + if verbose: + lp.print_stats("After All Buy + LP") + + # Compound 365 days + vault.compound(365) + if verbose: + print(f"{C.BLUE}--- Compound 365 days ---{C.END}") + print(f" Vault: {C.YELLOW}{vault.balance_of():.2f}{C.END}, Price: {C.GREEN}{lp.price:.6f}{C.END}") + + # All exit โ€” REVERSE order (Jack first, Aaron last) + results = {} + winners = 0 + losers = 0 + for name, buy_amt in reversed(users_data): + u = users[name] + lp.remove_liquidity(u) + tokens = u.balance_token + lp.sell(u, tokens) + profit = u.balance_usd - 3 * K + results[name] = profit + if profit > 0: + winners += 1 + else: + losers += 1 + if verbose: + pc = C.GREEN if profit > 0 else C.RED + print(f" {name:7s}: Invested {C.YELLOW}{buy_amt}{C.END}, Profit: {pc}{profit:.2f}{C.END}") + + total_profit = sum(results.values(), D(0)) + if verbose: + print(f"\n{C.BOLD}Winners: {C.GREEN}{winners}{C.END}, Losers: {C.RED}{losers}{C.END}") + tc = C.GREEN if total_profit > 0 else C.RED + print(f"{C.BOLD}Total profit: {tc}{total_profit:.2f}{C.END}") + print(f"Vault remaining: {C.YELLOW}{vault.balance_of():.2f}{C.END}") + + return { + "codename": codename, "profits": results, + "winners": winners, "losers": losers, + "total_profit": total_profit, "vault": vault.balance_of(), + } + +# ============================================================================= +# Comparison Output +# ============================================================================= + +def run_comparison(codenames: list[str]): + """Run all scenarios for each model and print comprehensive comparison table.""" + C = Color + all_results = [] + + print(f"\n{C.DIM}Running scenarios...{C.END}", end="", flush=True) + + for code in codenames: + single_r = single_user_scenario(code, verbose=False) + multi_r = multi_user_scenario(code, verbose=False) + bank_r = bank_run_scenario(code, verbose=False) + rmulti_r = reverse_multi_user_scenario(code, verbose=False) + rbank_r = reverse_bank_run_scenario(code, verbose=False) + all_results.append({ + "codename": code, + "single": single_r, + "multi": multi_r, + "bank": bank_r, + "rmulti": rmulti_r, + "rbank": rbank_r, + }) + print(f"{C.DIM}.{C.END}", end="", flush=True) + + print(f"\r{' ' * 40}\r", end="") # Clear progress line + + # Header + print(f"\n{C.BOLD}{C.HEADER}{'='*175}{C.END}") + print(f"{C.BOLD}{C.HEADER} MODEL COMPARISON - All Scenarios (FIFO vs LIFO){C.END}") + print(f"{C.BOLD}{C.HEADER}{'='*175}{C.END}\n") + + # Short curve names + SHORT_CURVE = { + CurveType.CONSTANT_PRODUCT: "CP", + CurveType.EXPONENTIAL: "Exp", + CurveType.SIGMOID: "Sig", + CurveType.LOGARITHMIC: "Log", + } + + # Column headers - V = Vault after each scenario + print(f" {C.BOLD}{'Model':<6} {'Crv':<3} โ”‚ {'S':>6} โ”‚ {'M+':>6} {'M-':>6} {'#':>2} {'V':>5} โ”‚ {'B+':>6} {'B-':>7} {'#':>2} {'V':>5} โ”‚ {'RM+':>6} {'RM-':>6} {'#':>2} {'V':>5} โ”‚ {'RB+':>6} {'RB-':>7} {'#':>2} {'V':>5}{C.END}") + print(f" {'โ”€'*6} {'โ”€'*3} โ”‚ {'โ”€'*6} โ”‚ {'โ”€'*6} {'โ”€'*6} {'โ”€'*2} {'โ”€'*5} โ”‚ {'โ”€'*6} {'โ”€'*7} {'โ”€'*2} {'โ”€'*5} โ”‚ {'โ”€'*6} {'โ”€'*6} {'โ”€'*2} {'โ”€'*5} โ”‚ {'โ”€'*6} {'โ”€'*7} {'โ”€'*2} {'โ”€'*5}") + + for r in all_results: + code = r["codename"] + cfg = MODELS[code] + curve = SHORT_CURVE[cfg["curve"]] + + # Single user profit + single_profit = r["single"]["profit"] + single_color = C.GREEN if single_profit > 0 else C.RED + + # Helper to compute profits/losses/losers + def calc_stats(profits_dict): + gains = sum(p for p in profits_dict.values() if p > 0) + losses = sum(p for p in profits_dict.values() if p < 0) + losers = sum(1 for p in profits_dict.values() if p < 0) + return gains, losses, losers + + # Multi (FIFO) + m_gains, m_losses, m_losers = calc_stats(r["multi"]["profits"]) + m_vault = r["multi"]["vault"] + mv_color = C.GREEN if m_vault == 0 else C.YELLOW + + # Bank (FIFO) + b_gains, b_losses, b_losers = calc_stats(r["bank"]["profits"]) + b_vault = r["bank"]["vault"] + bv_color = C.GREEN if b_vault == 0 else C.YELLOW + + # RMulti (LIFO) + rm_gains, rm_losses, rm_losers = calc_stats(r["rmulti"]["profits"]) + rm_vault = r["rmulti"]["vault"] + rmv_color = C.GREEN if rm_vault == 0 else C.YELLOW + + # RBank (LIFO) + rb_gains, rb_losses, rb_losers = calc_stats(r["rbank"]["profits"]) + rb_vault = r["rbank"]["vault"] + rbv_color = C.GREEN if rb_vault == 0 else C.YELLOW + + print(f" {C.BOLD}{code:<6}{C.END} {curve:<3} โ”‚ " + f"{single_color}{single_profit:>6.1f}{C.END} โ”‚ " + f"{C.GREEN}{m_gains:>6.0f}{C.END} {C.RED}{m_losses:>6.0f}{C.END} {m_losers:>2} {mv_color}{m_vault:>5.0f}{C.END} โ”‚ " + f"{C.GREEN}{b_gains:>6.0f}{C.END} {C.RED}{b_losses:>7.0f}{C.END} {b_losers:>2} {bv_color}{b_vault:>5.0f}{C.END} โ”‚ " + f"{C.GREEN}{rm_gains:>6.0f}{C.END} {C.RED}{rm_losses:>6.0f}{C.END} {rm_losers:>2} {rmv_color}{rm_vault:>5.0f}{C.END} โ”‚ " + f"{C.GREEN}{rb_gains:>6.0f}{C.END} {C.RED}{rb_losses:>7.0f}{C.END} {rb_losers:>2} {rbv_color}{rb_vault:>5.0f}{C.END}") + + print() + + # Legend + print(f" {C.DIM}S = Single user profit โ”‚ M = Multi (4 users, FIFO) โ”‚ B = Bank run (10 users, FIFO) โ”‚ RM/RB = Reverse (LIFO){C.END}") + print(f" {C.DIM}+ = profits, - = losses, # = losers, V = vault remaining โ”‚ Crv: CP/Exp/Sig/Log{C.END}") + print() + +# ============================================================================= +# Main +# ============================================================================= + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Commonwealth Protocol - Model Test Suite", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + python test_model.py # Compare all 16 models (table view) + python test_model.py CYN # All scenarios for one model (verbose) + python test_model.py CYN,EYN,SYN # Compare specific models (table view) + python test_model.py --single # Single-user scenario for all models (verbose) + python test_model.py --single CYN,EYN # Single-user scenario for specific models (verbose) + python test_model.py --multi # Multi-user scenario for all models + python test_model.py --multi CYN # Multi-user scenario for one model + python test_model.py --bank CYN,EYN # Bank run scenario for specific models + python test_model.py --rmulti # Reverse multi-user (last buyer exits first) + python test_model.py --rbank # Reverse bank run (last buyer exits first) +""" + ) + parser.add_argument( + "models", nargs="?", default=None, + help="Model code(s) to test, comma-separated (e.g., CYN or CYN,EYN,SYN). Default: all models." + ) + parser.add_argument( + "--single", action="store_true", + help="Run single-user scenario (verbose output per model)" + ) + parser.add_argument( + "--multi", action="store_true", + help="Run multi-user scenario" + ) + parser.add_argument( + "--bank", action="store_true", + help="Run bank run scenario" + ) + parser.add_argument( + "--rmulti", action="store_true", + help="Run reverse multi-user scenario (last buyer exits first)" + ) + parser.add_argument( + "--rbank", action="store_true", + help="Run reverse bank run scenario (last buyer exits first)" + ) + parser.add_argument( + "--verbose", "-v", action="store_true", + help="Show detailed output for each model (only applies when running single model)" + ) + + args = parser.parse_args() + + # Parse model codes + if args.models: + codes = [c.strip().upper() for c in args.models.split(",")] + # Validate + for code in codes: + if code not in MODELS: + print(f"Unknown model: {code}") + print(f"Available: {', '.join(sorted(MODELS.keys()))}") + sys.exit(1) + else: + codes = list(MODELS.keys()) + + # Determine which scenarios to run + run_single = args.single + run_multi = args.multi + run_bank = args.bank + run_rmulti = args.rmulti + run_rbank = args.rbank + + # If no scenario flags specified, use smart defaults + if not (run_single or run_multi or run_bank or run_rmulti or run_rbank): + if len(codes) == 1: + # Single model: run all scenarios with verbose output + code = codes[0] + single_user_scenario(code, verbose=True) + multi_user_scenario(code, verbose=True) + bank_run_scenario(code, verbose=True) + reverse_multi_user_scenario(code, verbose=True) + reverse_bank_run_scenario(code, verbose=True) + sys.exit(0) + else: + # Multiple models: run comparison table + run_comparison(codes) + sys.exit(0) + + # Run requested scenarios + if run_single: + for code in codes: + single_user_scenario(code, verbose=True) + + if run_multi: + for code in codes: + multi_user_scenario(code, verbose=True) + + if run_bank: + for code in codes: + bank_run_scenario(code, verbose=True) + + if run_rmulti: + for code in codes: + reverse_multi_user_scenario(code, verbose=True) + + if run_rbank: + for code in codes: + reverse_bank_run_scenario(code, verbose=True) \ No newline at end of file diff --git a/math/test_yield_model.py b/math/test_yield_model.py deleted file mode 100644 index 556964c..0000000 --- a/math/test_yield_model.py +++ /dev/null @@ -1,902 +0,0 @@ -from decimal import Decimal as D -from typing import Dict, Optional - -# const -K = D(1_000) -B = D(1_000_000_000) - -# price movement amplification -EXPOSURE_FACTOR = 100 * K - -# cap -CAP = 1 * B - -# max USDC before virtual liquidity vanishes -VIRTUAL_LIMIT = 100 * K - -# ANSI color codes -class Color: - HEADER = '\033[95m' # Magenta - BLUE = '\033[94m' # Blue - CYAN = '\033[96m' # Cyan - GREEN = '\033[92m' # Green - YELLOW = '\033[93m' # Yellow - RED = '\033[91m' # Red - BOLD = '\033[1m' # Bold - UNDERLINE = '\033[4m' # Underline - DIM = '\033[2m' # Dim/faint - STATS = '\033[90m' # Gray (for technical stats) - END = '\033[0m' # Reset - -class User: - name: str - balance_usd: D - balance_token: D - - def __init__(self, name: str, usd: D = D(0), token: D = D(0)): - self.name = name - self.balance_usd = usd - self.balance_token = token - -class CompoundingSnapshot: - value: D - index: D - - def __init__(self, value: D, index: D): - self.value = value - self.index = index - -class Vault: - apy: D - balance_usd: D - compounding_index: D - snapshot: Optional[CompoundingSnapshot] - compounds: int - - def __init__(self): - self.apy = D(5) / D(100) - self.balance_usd = D(0) - self.compounding_index = D(1.0) - self.snapshot = None - self.compounds = 0 - - def balance_of(self) -> D: - if self.snapshot is None: - return self.balance_usd - return self.snapshot.value * (self.compounding_index / self.snapshot.index) - - def add(self, value: D): - self.snapshot = CompoundingSnapshot( - value + self.balance_of(), - self.compounding_index - ) - self.balance_usd = self.balance_of() - - def remove(self, value: D): - if self.snapshot is None: - raise Exception("Nothing staked!") - self.snapshot = CompoundingSnapshot( - self.balance_of() - value, - self.compounding_index - ) - self.balance_usd = self.balance_of() - - def compound(self, days: int): - # run compounding daily - for _ in range(days): - self.compounding_index *= D(1) + (self.apy / D(365)) - - # track compounds number - self.compounds += days - -class UserSnapshot: - index: D - - def __init__(self, index: D): - self.index = index - -class LP: - balance_usd: D - balance_token: D - minted: D - liquidity_token: Dict[str, D] - liquidity_usd: Dict[str, D] - user_buy_usdc: Dict[str, D] - user_snapshot: Dict[str, UserSnapshot] - vault: Vault - k: Optional[D] - buy_usdc: D # USDC from buy operations (affects bonding curve) - lp_usdc: D # USDC from LP operations (yield only) - virtual_liquidity: D # Bootstrap virtual USDC for bonding curve - - def __init__(self, vault: Vault): - self.balance_usd = D(0) - self.balance_token = D(0) - self.minted = D(0) - self.liquidity_token = {} - self.liquidity_usd = {} - self.user_buy_usdc = {} - self.user_snapshot = {} - self.vault = vault - self.k = None - self.buy_usdc = D(0) - self.lp_usdc = D(0) - self.virtual_liquidity = CAP / EXPOSURE_FACTOR # Bootstrap virtual liquidity - - def get_buy_usdc_with_yield(self) -> D: - """ - Get current buy_usdc value including compounded yield. - Buy USDC grows proportionally with total vault balance. - """ - if self.buy_usdc == 0 and self.lp_usdc == 0: - return D(0) - - total_principal = self.buy_usdc + self.lp_usdc - if total_principal == 0: - return D(0) - - # Buy USDC gets its share of vault yield - compound_ratio = self.vault.balance_of() / total_principal - return self.buy_usdc * compound_ratio - - @property - def price(self) -> D: - """Current token price calculated from bonding curve marginal price. - price = usdc_reserve / token_reserve - This is the instantaneous price for the next token based on x*y=k curve. - Only buy USDC affects price, not LP USDC.""" - token_reserve = self._get_token_reserve() - usdc_reserve = self._get_usdc_reserve() - if token_reserve == 0: - return D(1) # fallback if no reserves - return usdc_reserve / token_reserve - - def get_exposure(self) -> D: - """ - Dynamic exposure that decreases as more tokens are minted. - Reaches 0 at 1M tokens minted. - """ - # Amplify minting effect by 1000x to hit 0 at 1M tokens - effective = min(self.minted * D(1000), CAP) - exposure = EXPOSURE_FACTOR * (D(1) - effective / CAP) - return max(D(0), exposure) - - def get_virtual_liquidity(self) -> D: - """ - Dynamic virtual liquidity that decreases as more USDC is added. - Floor at 0 USDC, ceiling at 100K USDC. - """ - base = CAP / EXPOSURE_FACTOR # 10,000 - effective = min(self.buy_usdc, VIRTUAL_LIMIT) - liquidity = base * (D(1) - effective / VIRTUAL_LIMIT) - - # Floor >= 1: buy_usdc + virtual_liquidity >= token_reserve - token_reserve = self._get_token_reserve() - floor_liquidity = token_reserve - self.buy_usdc - - # Use the higher of liquidity or floor requirement - return max(D(0), liquidity, floor_liquidity) - - def _get_token_reserve(self) -> D: - """Virtual token reserve = (CAP - minted) / exposure""" - exposure = self.get_exposure() - return (CAP - self.minted) / exposure if exposure > 0 else CAP - self.minted - - def _get_usdc_reserve(self) -> D: - """Virtual USDC reserve = buy_usdc_with_yield + dynamic virtual_liquidity - Includes compounded yield so price increases as vault compounds.""" - return self.get_buy_usdc_with_yield() + self.get_virtual_liquidity() - - def _update_k(self): - """ - Update constant product invariant using virtual reserves with dynamic exposure: - k = (token_reserve) * (buy_usdc + virtual_liquidity) - Only buy_usdc affects bonding curve, not lp_usdc. - """ - self.k = self._get_token_reserve() * self._get_usdc_reserve() - - def _apply_fair_share_cap(self, requested_amount: D, user_fraction: D) -> D: - """ - Apply fair share cap to prevent bank runs. - Returns capped amount based on user's fraction of vault. - - Args: - requested_amount: Amount user would get from bonding curve - user_fraction: User's fraction of total pool (0 to 1) - - Returns: - Capped amount: min(requested_amount, fair_share, vault_available) - """ - vault_available = self.vault.balance_of() - fair_share = user_fraction * vault_available - return min(requested_amount, fair_share, vault_available) - - def _get_fair_share_scaling(self, requested_total_usdc: D, user_principal: D, total_principal: D) -> D: - """ - Calculate fair share scaling factor for withdrawals. - Returns a scaling factor between 0 and 1 to apply proportionally to USDC and tokens. - - Args: - requested_total_usdc: Total USDC user would get with full yield - user_principal: User's principal (LP USDC + buy USDC) - total_principal: Total principal in pool (all users' LP USDC + buy USDC) - - Returns: - Scaling factor (0 to 1) to apply to both USDC and token withdrawals - """ - vault_available = self.vault.balance_of() - - if total_principal > 0 and requested_total_usdc > 0: - fraction = user_principal / total_principal - fair_share = fraction * vault_available - # scale down if either fair_share or vault_available is insufficient - scaling_factor = min(D(1), fair_share / requested_total_usdc, vault_available / requested_total_usdc) - elif requested_total_usdc > 0: - # no principal tracked, just cap at vault available - scaling_factor = min(D(1), vault_available / requested_total_usdc) - else: - # no USDC requested, no scaling needed - scaling_factor = D(1) - - return scaling_factor - - def _get_out_amount(self, sold_amount: D, selling_token: bool) -> D: - """ - Calculate output using constant product with virtual reserves: - (token_reserve) * (buy_usdc + virtual_liquidity) = k - Uses dynamic exposure that decreases as more tokens are minted. - Only buy_usdc affects bonding curve, not lp_usdc. - Applies quadratic vault scaling on sells to prevent depletion. - """ - if self.k is None: - # First buy: initialize k with dynamic virtual liquidity - self.k = self._get_token_reserve() * self.get_virtual_liquidity() - - token_reserve = self._get_token_reserve() - usdc_reserve = self._get_usdc_reserve() - - if selling_token: - # Selling tokens, getting USDC (sell operation) - # User adds tokens back, removes USDC from buy pool - # (token_reserve + token_in) * (usdc_reserve - usdc_out) = k - new_token_reserve = token_reserve + sold_amount - new_usdc_reserve = self.k / new_token_reserve - usdc_out_curve = usdc_reserve - new_usdc_reserve - - if self.minted == 0: - # All tokens sold - use bonding curve with virtual reserves, no fair share scaling - vault_available = self.vault.balance_of() - return min(usdc_out_curve, vault_available) - - # apply fair share cap to prevent bank run - user_fraction = sold_amount / self.minted - return self._apply_fair_share_cap(usdc_out_curve, user_fraction) - else: - # Buying tokens with USDC - # User adds USDC to buy pool, mints tokens - # (token_reserve - token_out) * (usdc_reserve + usdc_in) = k - new_usdc_reserve = usdc_reserve + sold_amount - new_token_reserve = self.k / new_usdc_reserve - token_out = token_reserve - new_token_reserve - return token_out - - def mint(self, amount: D): - if self.minted + amount > CAP: - raise Exception("Cannot mint over cap") - self.balance_token += amount - self.minted += amount - - def add_liquidity(self, user: User, token_amount: D, usd_amount: D): - # take tokens from user - user.balance_token -= token_amount - user.balance_usd -= usd_amount - - # push tokens to pool - self.balance_token += token_amount - self.balance_usd += usd_amount - - # track LP USDC (does NOT affect bonding curve) - self.lp_usdc += usd_amount - - # put usdc on vault for yield generation - self.rehypo() - - # initialize or update k on first liquidity add - if self.k is None: - self._update_k() - - # snapshot compounding index - self.user_snapshot[user.name] = UserSnapshot( - self.vault.compounding_index - ) - - # compute liquidity - self.liquidity_token[user.name] = self.liquidity_token.get(user.name, D(0)) + token_amount - self.liquidity_usd[user.name] = self.liquidity_usd.get(user.name, D(0)) + usd_amount - - def remove_liquidity(self, user: User): - # get user's deposited amounts - token_deposit = self.liquidity_token[user.name] - usd_deposit = self.liquidity_usd[user.name] - buy_usdc_principal = self.user_buy_usdc.get(user.name, D(0)) - - # calculate yield based on compounding - delta = self.vault.compounding_index / self.user_snapshot[user.name].index - - # LP USDC yield (5% APY) - usd_yield = usd_deposit * (delta - D(1)) - usd_amount_full = usd_deposit + usd_yield - - # LP token inflation (5% APY) - token_yield_full = token_deposit * (delta - D(1)) - - # Buy USDC yield (5% APY) - buy_usdc_yield_full = buy_usdc_principal * (delta - D(1)) - total_usdc_full = usd_amount_full + buy_usdc_yield_full - - # calculate fair share scaling factor - principal = usd_deposit + buy_usdc_principal - total_principal = self.lp_usdc + self.buy_usdc - scaling_factor = self._get_fair_share_scaling(total_usdc_full, principal, total_principal) - - # apply scaling to both USDC and tokens proportionally - total_usdc = total_usdc_full * scaling_factor - token_yield = token_yield_full * scaling_factor - token_amount = token_deposit + token_yield - - # mint scaled inflation yield on tokens - self.mint(token_yield) - - # remove scaled USDC from vault - self.dehypo(total_usdc) - - # reduce lp_usdc by the original LP principal - self.lp_usdc -= usd_deposit - - # reduce buy_usdc by scaled buy yield - buy_usdc_yield_actual = buy_usdc_yield_full * scaling_factor - self.buy_usdc -= buy_usdc_yield_actual - - # remove funds from lp - self.balance_token -= token_amount - self.balance_usd -= total_usdc - - # send funds to user - user.balance_token += token_amount - user.balance_usd += total_usdc - - # clear user liquidity - del self.liquidity_token[user.name] - del self.liquidity_usd[user.name] - if user.name in self.user_buy_usdc: - del self.user_buy_usdc[user.name] - - def buy(self, user: User, amount: D): - # take usd - user.balance_usd -= amount - self.balance_usd += amount - - # compute out amount (token) using x*y=k - out_amount = self._get_out_amount(amount, selling_token=False) - - # always mint new tokens for buy operations (don't use LP tokens) - self.mint(out_amount) - - # give token - self.balance_token -= out_amount - user.balance_token += out_amount - - # track buy USDC (affects bonding curve) - self.buy_usdc += amount - - # track USDC used to buy tokens - self.user_buy_usdc[user.name] = self.user_buy_usdc.get(user.name, D(0)) + amount - - # rehypo (deposits all USDC to vault) - self.rehypo() - - # update invariant - self._update_k() - - def rehypo(self): - # add funds to vault - self.vault.add(self.balance_usd) - - # remove funds from lp - self.balance_usd = D(0) - - def sell(self, user: User, amount: D): - # take token - user.balance_token -= amount - - # burn tokens - self.minted -= amount - - # compute amount out (usd) using x*y=k - out_amount = self._get_out_amount(amount, selling_token=True) - - # update buy_usdc (reduces bonding curve reserve) - self.buy_usdc -= out_amount - - # dehypo - self.dehypo(out_amount) - - # give usd - self.balance_usd -= out_amount - user.balance_usd += out_amount - - # update invariant after swap - self._update_k() - - def dehypo(self, amount: D): - # remove from vault - self.vault.remove(amount) - - # add to lp - self.balance_usd += amount - - def print_stats(self, label: str = "Stats"): - """Print detailed mathematical statistics for nerds""" - # Use CYAN which is visible on both light and dark backgrounds - print(f"\n{Color.CYAN} โ”Œโ”€ ๐Ÿ“Š {label} (Math Under the Hood) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€{Color.END}") - - # Virtual reserves - token_reserve = self._get_token_reserve() - usdc_reserve = self._get_usdc_reserve() - print(f"{Color.CYAN} โ”‚ Virtual Reserves: {Color.END}token={Color.YELLOW}{token_reserve:.2f}{Color.END}, usdc={Color.YELLOW}{usdc_reserve:.2f}{Color.END}") - - # Bonding curve constant - k_value = f"{self.k:.2f}" if self.k else "None" - print(f"{Color.CYAN} โ”‚ Bonding Curve k: {Color.YELLOW}{k_value}{Color.END}") - - # Dynamic factors - exposure = self.get_exposure() - virtual_liq = self.get_virtual_liquidity() - buy_usdc_with_yield = self.get_buy_usdc_with_yield() - print(f"{Color.CYAN} โ”‚ Exposure Factor: {Color.YELLOW}{exposure:.2f}{Color.END} (decreases as tokens mint)") - print(f"{Color.CYAN} โ”‚ Virtual Liquidity: {Color.YELLOW}{virtual_liq:.2f}{Color.END} (decreases as USDC added)") - - # USDC tracking - total_principal = self.buy_usdc + self.lp_usdc - buy_ratio = (self.buy_usdc / total_principal * 100) if total_principal > 0 else D(0) - lp_ratio = (self.lp_usdc / total_principal * 100) if total_principal > 0 else D(0) - print(f"{Color.CYAN} โ”‚ USDC Split: {Color.END}buy={Color.YELLOW}{self.buy_usdc:.2f}{Color.END} ({buy_ratio:.1f}%), lp={Color.YELLOW}{self.lp_usdc:.2f}{Color.END} ({lp_ratio:.1f}%)") - print(f"{Color.CYAN} โ”‚ Buy USDC (w/yield): {Color.YELLOW}{buy_usdc_with_yield:.2f}{Color.END}") - - # Vault & compounding - print(f"{Color.CYAN} โ”‚ Vault Balance: {Color.YELLOW}{self.vault.balance_of():.2f}{Color.END}") - print(f"{Color.CYAN} โ”‚ Vault Index: {Color.YELLOW}{self.vault.compounding_index:.6f}{Color.END} ({self.vault.compounds} days)") - - # Price calculation breakdown - if token_reserve > 0: - print(f"{Color.CYAN} โ”‚ Price: {Color.END}usdc_reserve/token_reserve = {Color.YELLOW}{usdc_reserve:.2f}{Color.END}/{Color.YELLOW}{token_reserve:.2f}{Color.END} = {Color.GREEN}{self.price:.6f}{Color.END}") - - # Minted vs Cap - mint_pct = (self.minted / CAP * 100) if CAP > 0 else D(0) - print(f"{Color.CYAN} โ”‚ Minted: {Color.YELLOW}{self.minted:.2f}{Color.END} / {CAP} ({mint_pct:.4f}%)") - - print(f"{Color.CYAN} โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€{Color.END}\n") - -def single_user_scenario( - user_initial_usd: D = 1 * K, - user_buy_token_usd: D = D(500), - compound_days: int = 100, -): - vault = Vault() - lp = LP(vault) - user = User("aaron", user_initial_usd) - - # Scenario header - print(f"\n{Color.BOLD}{Color.HEADER}{'='*70}{Color.END}") - print(f"{Color.BOLD}{Color.HEADER}{' SCENARIO 1: SINGLE USER FULL CYCLE':^70}{Color.END}") - print(f"{Color.BOLD}{Color.HEADER}{'='*70}{Color.END}\n") - - print(f"{Color.CYAN}[Initial]{Color.END} User USDC: {Color.YELLOW}{user.balance_usd}{Color.END}") - lp.print_stats("Initial State") - - # buy tokens for 500 usd - print(f"\n{Color.BLUE}--- Phase 1: Buy Tokens ---{Color.END}") - lp.buy(user, user_buy_token_usd) - assert lp.balance_usd == D(0) - assert vault.balance_usd == user_buy_token_usd - assert vault.balance_of() == user_buy_token_usd - print(f"[Buy] Spent: {Color.YELLOW}{user_buy_token_usd}{Color.END} USDC") - print(f"[Buy] Got tokens: {Color.YELLOW}{user.balance_token}{Color.END}") - - # assert price after buy - assert lp.price > D(1), f"Price should be > 1, got {lp.price}" - print(f"[Buy] Token price: {Color.GREEN}{lp.price}{Color.END}") - print(f"[Buy] Vault balance: {Color.YELLOW}{vault.balance_of()}{Color.END}") - lp.print_stats("After Buy") - - # add liquidity symmetrically: match token value at current price - print(f"\n{Color.BLUE}--- Phase 2: Add Liquidity ---{Color.END}") - user_add_liquidity_token = user.balance_token - user_add_liquidity_usd = user_add_liquidity_token * lp.price - lp.add_liquidity(user, user_add_liquidity_token, user_add_liquidity_usd) - assert lp.balance_token == user_add_liquidity_token - assert lp.balance_usd == D(0) - assert vault.balance_usd == user_add_liquidity_usd + user_buy_token_usd - assert vault.balance_of() == user_add_liquidity_usd + user_buy_token_usd - print(f"[LP] Added USDC: {Color.YELLOW}{user_add_liquidity_usd}{Color.END}") - print(f"[LP] Added tokens: {Color.YELLOW}{user_add_liquidity_token}{Color.END}") - print(f"[LP] Token price: {Color.GREEN}{lp.price}{Color.END}") - print(f"[LP] Vault balance: {Color.YELLOW}{vault.balance_of()}{Color.END}") - lp.print_stats("After Adding Liquidity") - - # compound for 100 days - print(f"\n{Color.BLUE}--- Phase 3: Compound for {compound_days} days ---{Color.END}") - price_after_add_liquidity = lp.price - vault.compound(compound_days) - price_after_compound = lp.price - print(f"[{compound_days} days] Vault: {Color.YELLOW}{vault.balance_of()}{Color.END}") - print(f"[{compound_days} days] Price: {Color.GREEN}{price_after_add_liquidity}{Color.END} โ†’ {Color.GREEN}{price_after_compound}{Color.END}") - price_increase = price_after_compound - price_after_add_liquidity - print(f"[{compound_days} days] Price increase: {Color.GREEN}+{price_increase}{Color.END}") - - # price changes as vault balance grows (more USDC per token) - assert lp.price > price_after_add_liquidity, f"Price should increase as vault compounds, got {lp.price} vs {price_after_add_liquidity}" - lp.print_stats(f"After {compound_days} Days Compounding") - - # remove liquidity - print(f"\n{Color.BLUE}--- Phase 4: Remove Liquidity & Sell ---{Color.END}") - user_usdc_before_removal = user.balance_usd - lp.remove_liquidity(user) - user_usdc_after_removal = user.balance_usd - gain = user_usdc_after_removal - user_usdc_before_removal - gain_color = Color.GREEN if gain > 0 else Color.RED - print(f"[Removal] USDC: {Color.YELLOW}{user_usdc_before_removal}{Color.END} โ†’ {Color.YELLOW}{user_usdc_after_removal}{Color.END} (gain: {gain_color}{gain}{Color.END})") - print(f"[Removal] Tokens: {Color.YELLOW}{user.balance_token}{Color.END}") - print(f"[Removal] Vault: {Color.YELLOW}{vault.balance_of()}{Color.END}") - lp.print_stats("After Removing Liquidity") - - # sell all tokens - user_tokens = user.balance_token - user_usdc_before_sell = user.balance_usd - lp.sell(user, user_tokens) - user_usdc_from_sell = user.balance_usd - user_usdc_before_sell - print(f"[Sell] Sold {Color.YELLOW}{user_tokens}{Color.END} tokens โ†’ Got {Color.YELLOW}{user_usdc_from_sell}{Color.END} USDC") - lp.print_stats("After Selling Tokens") - - # Final summary - print(f"\n{Color.BOLD}Final USDC: {Color.GREEN}{user.balance_usd}{Color.END}") - profit = user.balance_usd - user_initial_usd - profit_color = Color.GREEN if profit > 0 else Color.RED - print(f"{Color.BOLD}Total Profit: {profit_color}{profit}{Color.END}") - print(f"Final vault: {Color.YELLOW}{vault.balance_of()}{Color.END}") - print(f"Final minted: {Color.YELLOW}{lp.minted}{Color.END}") - -def multi_user_spreaded_scenario( - aaron_buy_usd: D = D(500), - bob_buy_usd: D = D(400), - carl_buy_usd: D = D(300), - dennis_buy_usd: D = D(600), - compound_interval: int = 50, -): - vault = Vault() - lp = LP(vault) - aaron = User("aaron", 2 * K) - bob = User("bob", 2 * K) - carl = User("carl", 2 * K) - dennis = User("dennis", 2 * K) - - # Scenario header - print(f"\n{Color.BOLD}{Color.HEADER}{'='*70}{Color.END}") - print(f"{Color.BOLD}{Color.HEADER}{' SCENARIO 2: MULTI-USER SPREADED EXITS':^70}{Color.END}") - print(f"{Color.BOLD}{Color.HEADER}{'='*70}{Color.END}\n") - - print(f"{Color.CYAN}[Initial]{Color.END} Aaron USDC: {Color.YELLOW}{aaron.balance_usd}{Color.END}") - print(f"{Color.CYAN}[Initial]{Color.END} Bob USDC: {Color.YELLOW}{bob.balance_usd}{Color.END}") - print(f"{Color.CYAN}[Initial]{Color.END} Carl USDC: {Color.YELLOW}{carl.balance_usd}{Color.END}") - print(f"{Color.CYAN}[Initial]{Color.END} Dennis USDC: {Color.YELLOW}{dennis.balance_usd}{Color.END}") - lp.print_stats("Initial State") - - # aaron buys tokens for 500 usd - lp.buy(aaron, aaron_buy_usd) - assert vault.balance_of() == aaron_buy_usd - print(f"[Aaron Buy] Aaron tokens: {aaron.balance_token}") - print(f"[Aaron Buy] Token price: {lp.price}") - print(f"[Aaron Buy] Vault balance: {vault.balance_of()}") - lp.print_stats("After Aaron Buy") - - # aaron adds liquidity symmetrically - aaron_add_liquidity_token = aaron.balance_token - aaron_add_liquidity_usd = aaron_add_liquidity_token * lp.price - lp.add_liquidity(aaron, aaron_add_liquidity_token, aaron_add_liquidity_usd) - assert lp.liquidity_token[aaron.name] == aaron_add_liquidity_token - print(f"[Aaron LP] Aaron liquidity tokens: {lp.liquidity_token[aaron.name]}") - print(f"[Aaron LP] Aaron liquidity USDC: {lp.liquidity_usd[aaron.name]}") - print(f"[Aaron LP] Vault balance: {vault.balance_of()}") - lp.print_stats("After Aaron LP") - - # bob buys tokens for 400 usd - lp.buy(bob, bob_buy_usd) - print(f"[Bob Buy] Bob tokens: {bob.balance_token}") - print(f"[Bob Buy] Token price: {lp.price}") - print(f"[Bob Buy] Vault balance: {vault.balance_of()}") - lp.print_stats("After Bob Buy") - - # bob adds liquidity symmetrically - bob_add_liquidity_token = bob.balance_token - bob_add_liquidity_usd = bob_add_liquidity_token * lp.price - lp.add_liquidity(bob, bob_add_liquidity_token, bob_add_liquidity_usd) - assert lp.liquidity_token[bob.name] == bob_add_liquidity_token - print(f"[Bob LP] Bob liquidity tokens: {lp.liquidity_token[bob.name]}") - print(f"[Bob LP] Bob liquidity USDC: {lp.liquidity_usd[bob.name]}") - print(f"[Bob LP] Vault balance: {vault.balance_of()}") - lp.print_stats("After Bob LP") - - # carl buys tokens for 300 usd - lp.buy(carl, carl_buy_usd) - print(f"[Carl Buy] Carl tokens: {carl.balance_token}") - print(f"[Carl Buy] Token price: {lp.price}") - print(f"[Carl Buy] Vault balance: {vault.balance_of()}") - lp.print_stats("After Carl Buy") - - # carl adds liquidity symmetrically - carl_add_liquidity_token = carl.balance_token - carl_add_liquidity_usd = carl_add_liquidity_token * lp.price - lp.add_liquidity(carl, carl_add_liquidity_token, carl_add_liquidity_usd) - assert lp.liquidity_token[carl.name] == carl_add_liquidity_token - print(f"[Carl LP] Carl liquidity tokens: {lp.liquidity_token[carl.name]}") - print(f"[Carl LP] Carl liquidity USDC: {lp.liquidity_usd[carl.name]}") - print(f"[Carl LP] Vault balance: {vault.balance_of()}") - lp.print_stats("After Carl LP") - - # dennis buys tokens for 600 usd - lp.buy(dennis, dennis_buy_usd) - print(f"[Dennis Buy] Dennis tokens: {dennis.balance_token}") - print(f"[Dennis Buy] Token price: {lp.price}") - print(f"[Dennis Buy] Vault balance: {vault.balance_of()}") - lp.print_stats("After Dennis Buy") - - # dennis adds liquidity symmetrically - dennis_add_liquidity_token = dennis.balance_token - dennis_add_liquidity_usd = dennis_add_liquidity_token * lp.price - lp.add_liquidity(dennis, dennis_add_liquidity_token, dennis_add_liquidity_usd) - assert lp.liquidity_token[dennis.name] == dennis_add_liquidity_token - print(f"[Dennis LP] Dennis liquidity tokens: {lp.liquidity_token[dennis.name]}") - print(f"[Dennis LP] Dennis liquidity USDC: {lp.liquidity_usd[dennis.name]}") - print(f"[Dennis LP] Vault balance: {vault.balance_of()}") - print(f"[Dennis LP] Pool tokens: {lp.balance_token}") - print(f"[Dennis LP] Minted tokens: {lp.minted}") - lp.print_stats("After Dennis LP") - - # compound for 50 days - vault.compound(compound_interval) - print(f"[{compound_interval} days] Vault balance: {vault.balance_of()}") - print(f"[{compound_interval} days] Token price: {lp.price}") - lp.print_stats(f"After {compound_interval} Days Compounding") - - # aaron removes liquidity (staked 50 days) - print(f"\n{Color.CYAN}=== Aaron Exit (50 days) ==={Color.END}") - aaron_usdc_before = aaron.balance_usd - lp.remove_liquidity(aaron) - aaron_gain = aaron.balance_usd - aaron_usdc_before - gain_color = Color.GREEN if aaron_gain > 0 else Color.RED - print(f"[Aaron removal] USDC: {Color.YELLOW}{aaron_usdc_before}{Color.END} โ†’ {Color.YELLOW}{aaron.balance_usd}{Color.END} (gain: {gain_color}{aaron_gain}{Color.END})") - print(f"[Aaron removal] Tokens: {Color.YELLOW}{aaron.balance_token}{Color.END}") - print(f"[Aaron removal] Vault: {Color.YELLOW}{vault.balance_of()}{Color.END}") - print(f"[Aaron removal] Price: {Color.YELLOW}{lp.price}{Color.END}") - lp.print_stats("After Aaron Removal") - - # aaron sells all tokens - aaron_tokens = aaron.balance_token - aaron_usdc_before_sell = aaron.balance_usd - lp.sell(aaron, aaron_tokens) - aaron_usdc_from_sell = aaron.balance_usd - aaron_usdc_before_sell - print(f"[Aaron sell] Sold {Color.YELLOW}{aaron_tokens}{Color.END} tokens โ†’ Got {Color.YELLOW}{aaron_usdc_from_sell}{Color.END} USDC") - print(f"[Aaron sell] Final USDC: {Color.BOLD}{Color.YELLOW}{aaron.balance_usd}{Color.END}") - lp.print_stats("After Aaron Sell") - - # compound for another 50 days - vault.compound(compound_interval) - print(f"\n{Color.BLUE}[{compound_interval*2} days] Vault: {Color.YELLOW}{vault.balance_of()}{Color.END}, Price: {Color.YELLOW}{lp.price}{Color.END}") - lp.print_stats(f"After {compound_interval*2} Days Compounding") - - # bob removes liquidity (staked 100 days) - print(f"\n{Color.CYAN}=== Bob Exit (100 days) ==={Color.END}") - bob_usdc_before = bob.balance_usd - lp.remove_liquidity(bob) - bob_gain = bob.balance_usd - bob_usdc_before - gain_color = Color.GREEN if bob_gain > 0 else Color.RED - print(f"[Bob removal] USDC: {Color.YELLOW}{bob_usdc_before}{Color.END} โ†’ {Color.YELLOW}{bob.balance_usd}{Color.END} (gain: {gain_color}{bob_gain}{Color.END})") - print(f"[Bob removal] Tokens: {Color.YELLOW}{bob.balance_token}{Color.END}") - print(f"[Bob removal] Vault: {Color.YELLOW}{vault.balance_of()}{Color.END}") - print(f"[Bob removal] Price: {Color.YELLOW}{lp.price}{Color.END}") - lp.print_stats("After Bob Removal") - - # bob sells all tokens - bob_tokens = bob.balance_token - bob_usdc_before_sell = bob.balance_usd - lp.sell(bob, bob_tokens) - bob_usdc_from_sell = bob.balance_usd - bob_usdc_before_sell - print(f"[Bob sell] Sold {Color.YELLOW}{bob_tokens}{Color.END} tokens โ†’ Got {Color.YELLOW}{bob_usdc_from_sell}{Color.END} USDC") - print(f"[Bob sell] Final USDC: {Color.BOLD}{Color.YELLOW}{bob.balance_usd}{Color.END}") - lp.print_stats("After Bob Sell") - - # compound for another 50 days - vault.compound(compound_interval) - print(f"\n{Color.BLUE}[{compound_interval*3} days] Vault: {Color.YELLOW}{vault.balance_of()}{Color.END}, Price: {Color.YELLOW}{lp.price}{Color.END}") - lp.print_stats(f"After {compound_interval*3} Days Compounding") - - # carl removes liquidity (staked 150 days) - print(f"\n{Color.CYAN}=== Carl Exit (150 days) ==={Color.END}") - carl_usdc_before = carl.balance_usd - lp.remove_liquidity(carl) - carl_gain = carl.balance_usd - carl_usdc_before - gain_color = Color.GREEN if carl_gain > 0 else Color.RED - print(f"[Carl removal] USDC: {Color.YELLOW}{carl_usdc_before}{Color.END} โ†’ {Color.YELLOW}{carl.balance_usd}{Color.END} (gain: {gain_color}{carl_gain}{Color.END})") - print(f"[Carl removal] Tokens: {Color.YELLOW}{carl.balance_token}{Color.END}") - print(f"[Carl removal] Vault: {Color.YELLOW}{vault.balance_of()}{Color.END}") - print(f"[Carl removal] Price: {Color.YELLOW}{lp.price}{Color.END}") - lp.print_stats("After Carl Removal") - - # carl sells all tokens - carl_tokens = carl.balance_token - carl_usdc_before_sell = carl.balance_usd - lp.sell(carl, carl_tokens) - carl_usdc_from_sell = carl.balance_usd - carl_usdc_before_sell - print(f"[Carl sell] Sold {Color.YELLOW}{carl_tokens}{Color.END} tokens โ†’ Got {Color.YELLOW}{carl_usdc_from_sell}{Color.END} USDC") - print(f"[Carl sell] Final USDC: {Color.BOLD}{Color.YELLOW}{carl.balance_usd}{Color.END}") - lp.print_stats("After Carl Sell") - - # compound for another 50 days - vault.compound(compound_interval) - print(f"\n{Color.BLUE}[{compound_interval*4} days] Vault: {Color.YELLOW}{vault.balance_of()}{Color.END}, Price: {Color.YELLOW}{lp.price}{Color.END}") - lp.print_stats(f"After {compound_interval*4} Days Compounding") - - # dennis removes liquidity (staked 200 days - longest) - print(f"\n{Color.CYAN}=== Dennis Exit (200 days) ==={Color.END}") - dennis_usdc_before = dennis.balance_usd - lp.remove_liquidity(dennis) - dennis_gain = dennis.balance_usd - dennis_usdc_before - gain_color = Color.GREEN if dennis_gain > 0 else Color.RED - print(f"[Dennis removal] USDC: {Color.YELLOW}{dennis_usdc_before}{Color.END} โ†’ {Color.YELLOW}{dennis.balance_usd}{Color.END} (gain: {gain_color}{dennis_gain}{Color.END})") - print(f"[Dennis removal] Tokens: {Color.YELLOW}{dennis.balance_token}{Color.END}") - print(f"[Dennis removal] Vault: {Color.YELLOW}{vault.balance_of()}{Color.END}") - print(f"[Dennis removal] Price: {Color.YELLOW}{lp.price}{Color.END}") - lp.print_stats("After Dennis Removal") - - # dennis sells all tokens - dennis_tokens = dennis.balance_token - dennis_usdc_before_sell = dennis.balance_usd - lp.sell(dennis, dennis_tokens) - dennis_usdc_from_sell = dennis.balance_usd - dennis_usdc_before_sell - print(f"[Dennis sell] Sold {Color.YELLOW}{dennis_tokens}{Color.END} tokens โ†’ Got {Color.YELLOW}{dennis_usdc_from_sell}{Color.END} USDC") - print(f"[Dennis sell] Final USDC: {Color.BOLD}{Color.YELLOW}{dennis.balance_usd}{Color.END}") - lp.print_stats("After Dennis Sell") - - # summary - print(f"\n{Color.BOLD}{Color.HEADER}=== FINAL SUMMARY ==={Color.END}") - total_profit = D(0) - for name, user in [("Aaron", aaron), ("Bob", bob), ("Carl", carl), ("Dennis", dennis)]: - initial = 2 * K - final = user.balance_usd - profit = final - initial - total_profit += profit - profit_color = Color.GREEN if profit > 0 else Color.RED - print(f"{name:7s}: Initial {Color.YELLOW}{initial}{Color.END}, Final {Color.YELLOW}{final}{Color.END}, Profit: {profit_color}{profit}{Color.END}") - - print(f"\n{Color.BOLD}Total profit (all users): {Color.GREEN if total_profit > 0 else Color.RED}{total_profit}{Color.END}") - print(f"Final vault balance: {Color.YELLOW}{vault.balance_of()}{Color.END}") - print(f"Final minted tokens: {Color.YELLOW}{lp.minted}{Color.END}") - -def multi_user_bank_run_scenario( - compound_days: int = 365, -): - vault = Vault() - lp = LP(vault) - - # define 10 users with their buy amounts - users_data = [ - ("aaron", D(500)), - ("bob", D(400)), - ("carl", D(300)), - ("dennis", D(600)), - ("eve", D(350)), - ("frank", D(450)), - ("grace", D(550)), - ("henry", D(250)), - ("iris", D(380)), - ("jack", D(420)), - ] - - users = {name: User(name, 3 * K) for name, _ in users_data} - - # Scenario header - print(f"\n{Color.BOLD}{Color.HEADER}{'='*70}{Color.END}") - print(f"{Color.BOLD}{Color.HEADER}{' SCENARIO 3: 10-USER BANK RUN (365 DAYS)':^70}{Color.END}") - print(f"{Color.BOLD}{Color.HEADER}{'='*70}{Color.END}\n") - - # print initial balances - print(f"{Color.CYAN}[Initial Balances]{Color.END}") - for name, buy_amount in users_data: - print(f" {name.capitalize():7s}: {Color.YELLOW}{users[name].balance_usd}{Color.END} USDC, Will buy: {Color.YELLOW}{buy_amount}{Color.END}") - lp.print_stats("Initial State") - - # all users buy tokens - print(f"\n{Color.BLUE}--- PHASE 1: ALL USERS BUY TOKENS ---{Color.END}") - for name, buy_amount in users_data: - price_before = lp.price - lp.buy(users[name], buy_amount) - price_after = lp.price - print(f"[{name.capitalize()} buy] Spent {Color.YELLOW}{buy_amount}{Color.END} USDC โ†’ Got {Color.YELLOW}{users[name].balance_token}{Color.END} tokens") - print(f"[{name.capitalize()} buy] Price: {Color.GREEN}{price_before}{Color.END} โ†’ {Color.GREEN}{price_after}{Color.END}") - - print(f"\n[All bought] Vault: {Color.YELLOW}{vault.balance_of()}{Color.END}, Minted: {Color.YELLOW}{lp.minted}{Color.END}, Price: {Color.GREEN}{lp.price}{Color.END}") - lp.print_stats("After All Buys") - - # all users add liquidity symmetrically - print(f"\n{Color.BLUE}--- PHASE 2: ALL USERS ADD LIQUIDITY ---{Color.END}") - for name, _ in users_data: - user = users[name] - user_add_liquidity_token = user.balance_token - user_add_liquidity_usd = user_add_liquidity_token * lp.price - price_before = lp.price - lp.add_liquidity(user, user_add_liquidity_token, user_add_liquidity_usd) - price_after = lp.price - print(f"[{name.capitalize()} LP] Added {user_add_liquidity_token} tokens + {lp.liquidity_usd[name]} USDC") - print(f"[{name.capitalize()} LP] Price: {price_before} โ†’ {price_after}") - - print(f"\n[All added LP] Vault: {Color.YELLOW}{vault.balance_of()}{Color.END}, Pool tokens: {Color.YELLOW}{lp.balance_token}{Color.END}, Price: {Color.GREEN}{lp.price}{Color.END}") - lp.print_stats("After All Added Liquidity") - - # compound for 365 days (1 year) - price_before_compound = lp.price - vault.compound(compound_days) - price_after_compound = lp.price - print(f"\n{Color.BLUE}--- PHASE 3: COMPOUND FOR {compound_days} DAYS ---{Color.END}") - print(f"[{compound_days} days] Vault: {Color.YELLOW}{vault.balance_of()}{Color.END}") - print(f"[{compound_days} days] Price: {Color.GREEN}{price_before_compound}{Color.END} โ†’ {Color.GREEN}{price_after_compound}{Color.END}") - price_increase = price_after_compound - price_before_compound - print(f"[{compound_days} days] Price increase: {Color.GREEN}+{price_increase}{Color.END}") - lp.print_stats(f"After {compound_days} Days Compounding") - - # all users sequentially remove liquidity and sell - print(f"\n{Color.BLUE}--- PHASE 4: ALL USERS REMOVE LIQUIDITY & SELL ---{Color.END}") - for name, buy_amount in users_data: - user = users[name] - - # remove liquidity - user_usdc_before_removal = user.balance_usd - lp.remove_liquidity(user) - user_usdc_after_removal = user.balance_usd - user_usdc_gain = user_usdc_after_removal - user_usdc_before_removal - gain_color = Color.GREEN if user_usdc_gain > 0 else Color.RED - print(f"\n{Color.CYAN}[{name.capitalize()} removal]{Color.END} USDC: {Color.YELLOW}{user_usdc_before_removal}{Color.END} โ†’ {Color.YELLOW}{user_usdc_after_removal}{Color.END} (gain: {gain_color}{user_usdc_gain}{Color.END})") - print(f" Tokens: {Color.YELLOW}{user.balance_token}{Color.END}, Vault: {Color.YELLOW}{vault.balance_of()}{Color.END}") - - # sell all tokens - user_tokens = user.balance_token - user_usdc_before_sell = user.balance_usd - lp.sell(user, user_tokens) - user_usdc_after_sell = user.balance_usd - user_usdc_from_sell = user_usdc_after_sell - user_usdc_before_sell - print(f"{Color.CYAN}[{name.capitalize()} sell]{Color.END} Sold {Color.YELLOW}{user_tokens}{Color.END} tokens โ†’ Got {Color.YELLOW}{user_usdc_from_sell}{Color.END} USDC") - print(f" Final USDC: {Color.BOLD}{Color.YELLOW}{user_usdc_after_sell}{Color.END}, Vault: {Color.YELLOW}{vault.balance_of()}{Color.END}") - lp.print_stats(f"After {name.capitalize()} Exit") - - # summary - print(f"\n{Color.BOLD}{Color.HEADER}{'='*70}{Color.END}") - print(f"{Color.BOLD}{Color.HEADER}{' FINAL SUMMARY':^70}{Color.END}") - print(f"{Color.BOLD}{Color.HEADER}{'='*70}{Color.END}\n") - total_profit = D(0) - for name, buy_amount in users_data: - initial = 3 * K - final = users[name].balance_usd - profit = final - initial - total_profit += profit - profit_color = Color.GREEN if profit > 0 else Color.RED - print(f"{name.capitalize():7s}: Invested {Color.YELLOW}{buy_amount:4}{Color.END}, Profit: {profit_color}{profit:8.2f}{Color.END}, Final: {Color.YELLOW}{final}{Color.END}") - - total_profit_color = Color.GREEN if total_profit > 0 else Color.RED - print(f"\n{Color.BOLD}Total invested: {Color.YELLOW}{sum(amount for _, amount in users_data)}{Color.END}") - print(f"{Color.BOLD}Total profit: {total_profit_color}{total_profit}{Color.END}") - print(f"Final vault: {Color.YELLOW}{vault.balance_of()}{Color.END}") - print(f"Final minted: {Color.YELLOW}{lp.minted}{Color.END}") - print(f"Final price: {Color.GREEN}{lp.price}{Color.END}") - -single_user_scenario() -multi_user_spreaded_scenario() -multi_user_bank_run_scenario()