# How Does Curve V2 Repeg?

By [peihanhan](https://paragraph.com/@peihanhan) · 2023-03-03

---

### Formula Derivation

![](https://storage.googleapis.com/papyrus_images/2d7ecc0ecf748f3db9388ec983471df27aa9044b1833e5b2d8b10f0d871d601c.jpg)

### Variables related to price:

PRICE\_SIZE：256/（3-1） = 128

PRICE\_MASK: 128 binary 1s, bitwise\_and operations with any number x is still x

PRECISIONS: 1, 1, 1

packed\_prices

In the initial state:

    # Packing prices
        packed_prices: uint256 = 0
        for k in range(N_COINS-1):
            packed_prices = shift(packed_prices, PRICE_SIZE)
            p: uint256 = initial_prices[N_COINS-2 - k]  # / PRICE_PRECISION_MUL
            assert p < PRICE_MASK
            packed_prices = bitwise_or(p, packed_prices)
    
        self.price_scale_packed = packed_prices
        self.price_oracle_packed = packed_prices
        self.last_prices_packed = packed_prices
    

There are 3 types of price.

1.  last\_prices\_packed
    
2.  price\_scale\_packed: the current pegged price. “_the internal set of coefficient p = (p0; p1; : : :)which we call price\_scale in the code.”_
    
3.  price\_oracle\_packed
    

_#Why does the curve use bitwise operations to pack prices instead of using a list? to save gas._

### Exchange Process

1.  Update xp (xp\[i\]), calculate dy using Newton's equation
    
        dy = xp[j] - Math(math).newton_y(A_gamma[0], A_gamma[1], xp, self.D, j)
        
    
    Here, dy and xp both refer to balance \* price\_scale. The input xp\[i\] += xi \* price\_scale(i), and dy is the change in xp\[j\].
    
2.  Continue to update xp (xp\[j\])
    
        xp[j] -= dy
        
    
    Deduct fees, continue to update xp (xp\[j\])
    
        if j > 0:
            dy = dy * PRECISION / price_scale[j-1]
        dy /= prec_j
        
        dy -= self._fee(xp) * dy / 10**10
        assert dy >= min_dy, "Slippage"
        y -= dy
        
        self.balances[j] = y
        
    
    **Since dy is calculated when D is constant, if the pool is reduced by just dy, D will not change. However, now the pool is reduced by (dy-fee), which is smaller than dy,so the remaining y in the pool is larger than the y calculated when D is constant, so D will actually increase slightly.**
    
    \*Dynamic Fee
    

![](https://storage.googleapis.com/papyrus_images/4761a851d81874ebc6e21b4bc99e463780faedb5d400db53cdde8272debf8b91.jpg)

    def _fee(xp: uint256[N_COINS]) -> uint256:
        f: uint256 = Math(math).reduction_coefficient(xp, self.fee_gamma)
        return (self.mid_fee * f + self.out_fee * (10**18 - f)) / 10**18
    

f\_mid: 4\*10\*\*6；f\_out: 3\*10\*\*7

f range: \[0.04%, 0.3%)

The more the pool is pushed off by a swap (the further away the price is from price\_scale), the higher the fee.

3\. After calculating the price with newton\_y, the tweak\_price method is called.

    self.tweak_price(A_gamma, xp, ix, p, 0)
    

new\_D == 0, D will be recalculated in the tweak\_price method. The exchange fee will slightly change D, so D needs to be recalculated here.

### TWEAK\_PRICE

1.  Calculate P\_oracle using the last recorded price first, note not using the current swap price.
    

![](https://storage.googleapis.com/papyrus_images/0ba1f6dcf5863984d345e2d10fc2f1e319639fe49d36ad1953f84f13783ccb4c.jpg)

    if last_prices_timestamp < block.timestamp:
            # MA update required
            ma_half_time: uint256 = self.ma_half_time
            alpha: uint256 = Math(math).halfpow((block.timestamp - last_prices_timestamp) * 10**18 / ma_half_time, 10**10)
            packed_prices = 0
            for k in range(N_COINS-1):
                price_oracle[k] = (last_prices[k] * (10**18 - alpha) + price_oracle[k] * alpha) / 10**18
            for k in range(N_COINS-1):
                packed_prices = shift(packed_prices, PRICE_SIZE)
                p: uint256 = price_oracle[N_COINS-2 - k]  # / PRICE_PRECISION_MUL
                assert p < PRICE_MASK
                packed_prices = bitwise_or(p, packed_prices)
            self.price_oracle_packed = packed_prices
            self.last_prices_timestamp = block.timestamp
    

ma\_half\_time = 600

The time interval between the current time and the last recorded time is t. The shorter the interval, the closer alpha is to 1, and P\* _is closer to P\_prev; the longer the interval, the closer alpha is to 0, and P\*_ is closer to P\_last; when the interval = ma\_half\_time, alpha = 0.5

2\. Update last\_prices

    if p_i > 0:
            # Save the last price
            if i > 0:
                last_prices[i-1] = p_i
    

3\. Adjust pegged prices or not

_“We allow the reduction in Xcp but only such that the loss of value of Xcp doesn’t exceed half the profit we’ve made (which we track by tracking the increase of Xcp).”_

Equation: virtual\_price-10\*\*18 > (xcp\_profit-10\*\*18)/2 + self.allowed\_extra\_profit

Meaning: The value of lp must exceed half of the accumulated fee income.

Xcp: the value of the constant-product invariant at the equilibrium point.

At the equilibrium point (the values of each token in the pool are equal. for all i, j ：x\_i\*\*p\_i = x\_j\*\*p\_j, which is the state of the uniswap pool), the constant product and the constant sum formulas are separately valid:

![](https://storage.googleapis.com/papyrus_images/476cb7224f5096782b2b45427d6522794144faf6bb7157cdca282ca6333a660c.jpg)

if price\_scale doesn’t change, D controls the shape of the curve, and X\_cp is the square root of the rectangle formed by the intersection of the xy curve and the y=price\_scale\*x line with the origin.

    @internal
    @view
    def get_xcp(D: uint256) -> uint256:
        x: uint256[N_COINS] = empty(uint256[N_COINS])
        x[0] = D / N_COINS
        packed_prices: uint256 = self.price_scale_packed
        # No precisions here because we don't switch to "real" units
    
        for i in range(1, N_COINS):
            x[i] = D * 10**18 / (N_COINS * bitwise_and(packed_prices, PRICE_MASK))  # ... * PRICE_PRECISION_MUL)
            packed_prices = shift(packed_prices, -PRICE_SIZE)
    
        return Math(math).geometric_mean(x)
    

3-1: Calculate D

    if new_D == 0:
        # We will need this a few times (35k gas)
        D_unadjusted = Math(math).newton_D(A_gamma[0], A_gamma[1], _xp)
    

3-2: Calculate virtual\_price and xcp\_profit under the current price\_scale

    # Update profit numbers without price adjustment first
    xp[0] = D_unadjusted / N_COINS
    for k in range(N_COINS-1):
        xp[k+1] = D_unadjusted * 10**18 / (N_COINS * price_scale[k])
    xcp_profit: uint256 = 10**18
    virtual_price: uint256 = 10**18
    
    if old_virtual_price > 0:
        xcp: uint256 = Math(math).geometric_mean(xp)
        **virtual_price = 10**18 * xcp / total_supply**
        xcp_profit = old_xcp_profit * virtual_price / old_virtual_price
    

**In exchange, the total\_supply quantity does not change, only xcp will cause virtual\_price to change, and price\_scale has not changed at this point, so only the change in D will cause virtual\_price to change. As analyzed earlier, the increase in D here is entirely due to the fee, so the increase in virtual\_price is also entirely due to the fee. Therefore, virtual\_price is used to calculate xcp\_profit here, which is the accumulated fee income.**

3-3: Determine whether to repeg.

After repeg, X\_cp will definitely decrease. If the new X\_cp can satisfy > xcp\_profit/2, the current X\_cp can definitely satisfy it.

    if not needs_adjustment and (virtual_price * 2 - 10**18 > xcp_profit + 2*self.allowed_extra_profit):
        needs_adjustment = True
        self.not_adjusted = True
    

3-4: Repeg: move price\_scale towards price\_oracle

![](https://storage.googleapis.com/papyrus_images/fedda40c3a311feba8e2fa4a01aa98f6ea457f74b8de91cca4699bb5db327f8d.jpg)

    if needs_adjustment:
        adjustment_step: uint256 = self.adjustment_step
        norm: uint256 = 0
    
        for k in range(N_COINS-1):
            ratio: uint256 = price_oracle[k] * 10**18 / price_scale[k]
            if ratio > 10**18:
                ratio -= 10**18
            else:
                ratio = 10**18 - ratio
            norm += ratio**2
    
        if norm > adjustment_step ** 2 and old_virtual_price > 0:
            norm = Math(math).sqrt_int(norm / 10**18)  # Need to convert to 1e18 units!
    
            for k in range(N_COINS-1):
                p_new[k] = (price_scale[k] * (norm - adjustment_step) + adjustment_step * price_oracle[k]) / norm
    

![](https://storage.googleapis.com/papyrus_images/83855980c70c2948171eb0313bbc2929060e7a93835c5870d8a8d571dc2fe4db.jpg)

norm looks like standard deviation, measuring the deviation of P\_oracle relative to the current pegged P\_scale.

The equation in the whitepaper is actually a weighted average. The rate of change of P\_scale = s/norm \* the change rate from P\_scale to P\_oracle.

s: adjustment\_step = 2 \* 10\*\*15 → 0.002

No matter how far P\_oracle deviates from the current P\_scale, P\_scale always moves forward by 0.2% with each step.

3-5: Determine whether the new price\_scale meets the adjustment criteria.

**Price Transmission Order：P\_scale < P\_oracle < P\_last < P**

Uniswap, The user's "l" never changes, as long as the price returns to "mint", there will be no impermanent loss.

Curve V2: As the Xcp of the user is constantly re-pegged, it will continuously decrease. Even if the price is restored to mint, there will still be impermanent loss, but this impermanent loss will not exceed half of the accumulated handling fee. What is the impact?

Equilibrium State:

Uniswap: Always in equilibrium state. Curve: Only one point is in equilibrium state.

references：

Curve V2 whitepaper

Curve V2 pool code

---

*Originally published on [peihanhan](https://paragraph.com/@peihanhan/how-does-curve-v2-repeg)*
