Skip to content

Commit

Permalink
feat: rebalancing donations (only manual donation)
Browse files Browse the repository at this point in the history
This commit lays the foundations to reintroduce donation support for TwoCrypto pools.

We first introduce a new contract-level invariant: D_total = D + D_rebalance.

Where:
* D_total is the total amount of liquidity in the pool.
* D represents the depositors' liquidity (what can be withdrawn).
* D_rebalance is a siloed reserve of liquidity used to cover rebalancing losses or absorb rebalancing gains.

To reintroduce donations safely, during rebalancing (triggered by `tweak_price`), the pool taps into a separate reserve (`D_rebalance`) from depositors' liquidity to cover losses.

`tweak_price` no longer relies on `xcp_profit` (referred to as `profit` here) to decide whether it should attempt to rebalance. Instead, it uses the ratio between `D_rebalance` and `D_total`. The threshold for attempting a rebalance has been arbitrarily set to 0.1% for now but will be parameterized by `allowed_extra_profit`, which will be repurposed since it is no longer used for the old condition.

Furthermore, a new function `donate` has been added to the pool to manually donate funds for rebalancing. The function works similarly to `add_liquidity` except it doesn't mint any LP tokens and doesn't inflate `virtual_price`.

In the future, fees collected by normal operations in the pool (`exchange`, `add_liquidity`, etc.) will increase `D_rebalance`, allowing the removal of `xcp_profit` related code.
  • Loading branch information
AlbertoCentonze committed Jun 17, 2024
1 parent 727c13d commit 969ad6d
Showing 1 changed file with 109 additions and 25 deletions.
134 changes: 109 additions & 25 deletions contracts/main/CurveTwocrypto.vy
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ future_A_gamma_time: public(uint256) # <------ Time when ramping is finished.

balances: public(uint256[N_COINS])
D: public(uint256)
D_rebalance: public(uint256)
profit: uint256
profit_checkpoint: uint256 # <--- Full profit at last claim of admin fees.

Expand Down Expand Up @@ -569,6 +570,66 @@ def add_liquidity(

return d_token

@external
@nonreentrant("lock")
def donate(amounts: uint256[N_COINS]):

A_gamma: uint256[2] = self._A_gamma()
xp: uint256[N_COINS] = self.balances
amountsp: uint256[N_COINS] = empty(uint256[N_COINS])
old_D: uint256 = 0

assert amounts[0] + amounts[1] > 0 # dev: no coins to add

# --------------------- Get prices, balances -----------------------------

price_scale: uint256 = self.cached_price_scale

# -------------------------------------- Update balances and calculate xp.
xp_old: uint256[N_COINS] = xp
amounts_received: uint256[N_COINS] = empty(uint256[N_COINS])

########################## TRANSFER IN <-------

for i in range(N_COINS):
if amounts[i] > 0:
# Updates self.balances here:
amounts_received[i] = self._transfer_in(
i,
amounts[i],
msg.sender,
False, # <--------------------- Disable optimistic transfers.
)
xp[i] = xp[i] + amounts_received[i]

xp = [
xp[0] * PRECISIONS[0],
unsafe_div(xp[1] * price_scale * PRECISIONS[1], PRECISION)
]
xp_old = [
xp_old[0] * PRECISIONS[0],
unsafe_div(xp_old[1] * price_scale * PRECISIONS[1], PRECISION)
]

for i in range(N_COINS):
if amounts_received[i] > 0:
amountsp[i] = xp[i] - xp_old[i]

# -------------------- Calculate LP tokens to mint -----------------------
if self.future_A_gamma_time > block.timestamp: # <--- A_gamma is ramping.
# ----- Recalculate the invariant if A or gamma are undergoing a ramp.
old_D = MATH.newton_D(A_gamma[0], A_gamma[1], xp_old, 0)
else:
old_D = self.D

D: uint256 = MATH.newton_D(A_gamma[0], A_gamma[1], xp, 0)
self.D_rebalance = D - old_D

if old_D > 0:
self.tweak_price(A_gamma, xp, D)
else:
raise "Donation not allowed for empty pool"


@external
@nonreentrant("lock")
Expand Down Expand Up @@ -920,9 +981,10 @@ def tweak_price(

self.profit = profit

# ------------ Rebalance liquidity if there's enough profits to adjust it:
if virtual_price * 2 - 10**18 > profit + 2 * rebalancing_params[0]:
# allowed_extra_profit --------^
rebalancing_reserves: uint256 = self.D_rebalance
if (
rebalancing_reserves * 10**18 / (self.D + rebalancing_reserves)
) > 10**15: # <--- 0.1% of total D (rebalancing + liquidity) is donations

# ------------------- Get adjustment step ----------------------------

Expand Down Expand Up @@ -961,29 +1023,51 @@ def tweak_price(
]

# ------------------------------------------ Update D with new xp.
D: uint256 = MATH.newton_D(A_gamma[0], A_gamma[1], xp, 0)

# ------------------------------------- Convert xp to real prices.
xp = [
unsafe_div(D, N_COINS),
D * PRECISION / (N_COINS * new_price_scale)
]

# ---------- Calculate new virtual_price using new xp and D. Reuse
# `old_virtual_price` (but it has new virtual_price).
old_virtual_price = unsafe_div(
10**18 * isqrt(xp[0] * xp[1]), total_supply
) # <----- unsafe_div because we did safediv before (if vp>1e18)

# ---------------------------- Proceed if we've got enough profit.
if (
old_virtual_price > 10**18 and
2 * old_virtual_price - 10**18 > profit
):

self.D = D
self.virtual_price = old_virtual_price
D_total: uint256 = MATH.newton_D(A_gamma[0], A_gamma[1], xp, 0)

# We define D as the total value of the pool, including donations.
# The following invariant should always hold: D_total = D + D_donation.
# D is the value of the user deposits expressed in invariant terms.

can_rebalance: bool = False

D_delta: uint256 = 0
old_D_total: uint256 = self.D + self.D_rebalance

# if the old D_total is greater than the new D_total, it means that
# the pool has lost value. In this case, we need to check if the
# rebalancing reserves can cover the loss. If it can, we decrease
# the rebalancing reserves.
if old_D_total > D_total:
D_delta = old_D_total - D_total
# if the rebalancing reserves can cover the loss, we use them.
if (self.D_rebalance > D_delta):
self.D_rebalance -= D_delta

can_rebalance = True
# if the old D_total is less than the new D_total, it means that
# the pool has gained value. This can happen when the oracle price goes
# in the direction of a future price.
# In this case we increase the rebalancing reserves, with the profit made
# by the rebalance to avoid people extract value from the pool.
else:
D_delta = D_total - old_D_total
self.D_rebalance += D_delta

can_rebalance = True

# TODO handle case where D_total == old_D_total

if can_rebalance:
self.cached_price_scale = new_price_scale
# Just `D` represents the value of the user deposits.
D: uint256 = D_total - self.D_rebalance
# Virtual price is calculated taking into account **only** the
# user deposits. This way donations can't be used to inflate
# the virtual price.
self.virtual_price = unsafe_div(
10**18 * self.get_xcp(D, new_price_scale), total_supply
)

return new_price_scale

Expand Down

0 comments on commit 969ad6d

Please sign in to comment.