MIP 6: Bitcoin mAsset (mBTC) and new AMM-based design Source

AuthorOnur Solmaz
Discussions-Tohttps://forum.mstable.org/t/mip-6-bitcoin-masset-mbtc-and-new-amm-based-design/277
StatusRejected
Created2020-12-07

Simple Summary

A new mAsset, mBTC, is created, which unites various Bitcoin-backed ERC20 tokens. Mints, redeems and swaps with bAssets are not 1:1, and instead determined by a new AMM-based design.

Abstract

We propose a new mAsset, mBTC. Similar to mUSD, mBTC will be a derivative of BTC-backed assets, most likely wBTC, renBTC and sBTC at launch. However, unlike mUSD, mints, redeems and swaps will not always be 1:1, depending on the relative weights of the bAssets (underlying assets). Instead, mBTC will feature the first ever implementation of our new AMM design, the Incentivized Constant Sum Market Maker, or ICSMM for short. In the new design, there are upper and lower soft and hard limits for bAsset weights, which are generally ordered in ascending order as [0%,hard_min,soft_min,soft_max,hard_max,100%]. When all bAsset weights are in the 1:1 region, that is, between soft_min and soft_max, users can mint, redeem and swap 1:1. If a user action pushes weights beyond this region, increasing disincentives are applied to prevent further movement. Similarly, if a user action brings weights back into the 1:1 region, an incentive is applied. These incentives are computed by penalty functions for each bAsset, which themselves are combined under a single invariant per the usual AMM formulation. Finally, weights can never move beyond the hard limits, imposing limits on how much bAsset reserves can be drained.

Motivation

The AMM design used for mUSD, the first mAsset, is called the Constant Sum Market Maker, with the invariant \(\sum_i x_i = k\). With the CSMM, bAssets can be minted, redeemed and swapped 1:1. However, this design introduces a number shortcomings, namely:

  • Drained bAssets: Expensive bAssets are fully drained from the basket and cheaper bAssets fill it up as much as they can.
  • Restricted liquidity: Not being able to swap with all bAssets limits the swap opportunities.
  • Reduced composability: Not being able to mint or redeem with every bAsset hinders composability with other DeFi projects.

These problems were apparent even before the launch of mUSD v1. Because the system caps risk on individual assets, mUSD was launched with maximum with maximum weight limits of 55%.

We designed the ICSMM to deal with the shortcomings of CSMM. By introducing incentives, we not only solve all of the aforementioned problems, but also turn mAssets into fully functional AMMs, on par with other projects such as Curve Finance or Shell Protocol.

Specification

Overview

As mentioned before, the CSMM invariant is simply the sum of all bAsset reserves:

\[ \sum_i x_i = k \]

To obtain the ICSMM invariant, we multiply each reserve value with a “penalty term”

\[ \sum_i x_i \left(1 - \pi_i\left(\frac{x_i}{\sum_j x_j}\right)\right) = k \]

where \(\pii\) are the _penalty functions for each bAsset \(i\). These are functions of weight, and we will construct these to suit our needs from the AMM. Our main design criterion: they should yield 0 in the “target weight range”, defined by soft minimum (soft_min) and maximum values (soft_max), but should otherwise increase with increased distance from soft_min or soft_max. When every weight is in [soft_min, soft_max], our invariant becomes equal to the CSMM. This is the 1:1 region we mentioned in the abstract.

The weights are allowed to move in a “wiggle room” below soft_min and above soft_max, capped by hard_min and hard_max values respectively. In other words, weights are not allowed to go below hard_min or above hard_max. The lower the weight of a bAsset, the higher its price in the AMM relative to others. The higher the weight of of a bAsset, the lower its price in the AMM relative to others.

In AMM formulation, prices are obtained by taking the derivative of of the invariant function

\[ \frac{\partial f}{\partial x_i} = 1 - \pi_i(w_i)- w_i(1-w_i) \pi_i’(w_i) + \sum_{j\neq i}w_j^2\pi_j’(w_j) =: p_i(w) \]

where \(f(x)\) is the left-hand side of the invariant and weights are defined as \(wi=x_i/\sum_j x\). Note the price expression is independent of total bAsset reserves. Since they are only functions of weights \(w = (w_1, \dots, w_n)\), we denote them as \(p_i(w)\). We also note that our invariant is _liquidity insensitive, that is, it yields the same prices at the same weights, regardless of the total amount of liquidity in the AMM. We think that this property is important for a basket of stablecoins.

We want the penalty functions to be easy to compute on chain. To this end, we define them for each bAsset \(i\) as

\[ \pi_i(w) = \begin{cases} \pi_{\min,i} \left(\dfrac{w - s_{\min,i}}{h_{\min,i} - s_{\min,i}}\right)^{a_{\min,i}} & h_{\min,i} < w < s_{\min,i} \\
0 & s_{\min,i} \leq w \leq s_{\max,i} \\
\pi_{\max,i} \left(\dfrac{w - s_{\max,i}}{h_{\max,i} - s_{\max,i}}\right)^{a_{\max,i}} & s_{\max,i} < w < h_{\max,i} \end{cases} \]

where \(h{\min,i}\) is the hard minimum, \(s{\min,i}\) is the soft minimum, \(\pi{\min,i}\) is the value of the function at the hard minimum, \( a{\min,i} \in \mathbb{Z}_{>0} \) is the convexity parameter of the weight floor, \(h{\max,i}\) is the hard maximum, \(s{\max,i}\) is the soft maximum, \(\pi{\max,i}\) is the value of the function at the hard maximum, and \(a{\max,i} \in \mathbb{Z}_{>0}\) is the convexity parameter of the weight ceiling.

Rationale

There were various design goals we wanted to achieve while we tackle the problems described in Motivation:

  • Be able to flexibly target specific bAsset weights.
  • Make mAssets more composable.
  • Guarantee a minimum amount of liquidity.
  • Increase swap volume.

We came to the conclusion that the most logical solution to our problems was to introduce an invariant-based model to mAssets, effectively turning them into AMMs that work within the bounds defined by hard limits.

Technical Specification

In this section, we outline the algorithms used in computing output amounts in Python 3-like pseudocode. The variable FULL_SCALE is a big number, e.g. 1e18, that we use to set the precision of integer operations. For more information, check out the existing math implementation in mStable repositories.

Computing the penalty function and its derivative

The penalty function is computed as

def pi(w: int):
    "Evaluate the penalty function at weight `w`"
    result = 0
    if w < soft_min:
        result += (
            floor_penalty
            * (w - soft_min) ** floor_exponent
            // (hard_min - soft_min) ** floor_exponent
        )
    elif soft_max < w:
        result = (
            ceiling_penalty
            * (w - soft_max) ** ceiling_exponent
            // (hard_max - soft_max) ** ceiling_exponent
        )
    return result

The derivative of the penalty function is computed as

def dpidw(w: int):
    "Evaluate the derivative of the penalty function at weight `w`"
    result = 0
    if w < soft_min:
        result += (
            FULL_SCALE
            * floor_penalty
            * floor_exponent
            * (w - soft_min)
            ** (floor_exponent - 1)
            // (hard_min - soft_min)
            ** floor_exponent
        )
    elif soft_max < w:
        result += (
            FULL_SCALE
            * ceiling_penalty
            * ceiling_exponent
            * (w - soft_max)
            ** (ceiling_exponent - 1)
            // (hard_max - soft_max)
            ** ceiling_exponent
        )
    return result

Computing the invariant and its derivative

The invariant is computed as

def invariant(x: List[int]):
    """Compute the invariant f(x) for a given array of supplies `x`.
    `penalty[]` is an array of objects for each bAsset, which can
    compute the penalty function `pi(w)` and its derivative `dpidw(w)`."""
    result = 0
    sum_ = sum(x)

    for i in range(n_basset):
        w = FULL_SCALE * x[i] // sum_
        result += x[i] * (FULL_SCALE - penalty[i].pi(w))

    result = result // FULL_SCALE
    return result

On the other hand, its derivative is computed for a given array of weights as

def invariant_deriv_weights(w: List[int], i: int):
    """Compute the derivative of the invariant df/dx_i with respect
    to the bAsset index `i` for an array of weights `w`."""
    result = FULL_SCALE
    result -= penalty[i].pi(w[i])
    result -= (
        w[i] * (FULL_SCALE - w[i])
        * penalty[i].dpidw(w[i]) // FULL_SCALE**2
    )

    for j, p in enumerate(penalty_functions):
        if j == i: continue
        result += w[j] * w[j] \
            * penalty[j].dpidw(w[j]) // FULL_SCALE**2

    return result

Additionally, we have the function that computes the derivative for given reserves, wrapping the function we have given above

def invariant_deriv(x: List[int], i: int):
    w = [FULL_SCALE * x_j // sum(x) for x_j in x]
    return eval_deriv_weights(w, i)

Solving the invariant equation

We use Newton’s method to solve for a reserve value, given a mAsset supply (right-hand side of the invariant equation, \(k\))

def solve_invariant(x: List[int], rhs: int, i: int, max_iter=25):
    x_i = x[i]
    tol = 2 * rhs // FULL_SCALE

    for n_iter in range(max_iter):
        res = invariant(x) - rhs
        deriv = invariant_deriv(x, i)
        x_i = x_i - (res * FULL_SCALE) // deriv
        x[i] = x_i

        if abs(res) <= tol:
            return x_i

    raise Exception("Solution did not converge in %d iterations"%max_iter)

Checking whether the reserve change is allowed

User actions will modify reserves in a certain way, and we need to check after each action whether the final values are within the allowed range.

def in_bounds(x: List[int]):
    sum_ = sum(x)
    for i in range(n_basset):
        w = FULL_SCALE * x[i] // sum_
        if not (penalty[i].hard_min <= w and w <= penalty[i].hard_max):
            return False
    return True

Computing mint output

The amount of mAsset received for minting with a certain amount of bAsset is computed as

def compute_mint(i: int, quantity: int):
    """Compute the amount of mAsset received for minting
    with `quantity` amount of bAsset index `i`."""

    x = get_current_reserves()
    k_init = invariant(x)
    x[i] += quantity
    k_final = invariant(x)
    total_minted = k_final - k_init

    if not in_bounds(x):
        raise Exception("Mint not allowed")

    return total_minted

Compute redeem output

The amount of bAsset received for redeeming a certain amount of mAsset is computed as

def compute_redeem(i: int, quantity: int):
    """Compute the amount of bAsset index `i` received for
    redeeming `quantity` amount of mAsset."""

    redemption_fee = quantity * swap_fee_rate // FULL_SCALE
    deducted_quantity = quantity - redemption_fee

    x = get_current_reserves()
    k_init = invariant(x)
    k_final = k_init - deducted_quantity
    new_reserve = solve_invariant(x, k_final, i)
    total_received = x[i] - new_reserve
    x[i] = new_reserve

    if not in_bounds(x):
        raise Exception("Redeem not allowed")

    return total_received

Compute swap output

The output of swapping a certain amount of a bAsset to another one is computed as

def compute_swap(input_idx: int, output_idx: int, quantity: int):
    """Compute the amount of bAsset received for swapping
    `quantity` amount of index `input_idx` to index `output_idx`."""

    x = get_current_reserves()
    k1 = invariant(x)
    x[input_idx] += quantity
    k2 = invariant(x)
    total_minted = k2 - k1
    swap_fee = total_minted * swap_fee_rate // FULL_SCALE
    deducted_quantity = total_minted - swap_fee
    k3 = k2 - deducted_quantity
    new_reserve = solve_invariant(x, k3, output_idx)
    total_received = x[output_idx] - new_reserve
    x[output_idx] = new_reserve

    if not in_bounds(x):
        raise Exception("Swap not allowed")

    return total_received

Configurable Values (Via MCCP)

Each bAsset has the following configurable values:

  • soft_min
  • hard_min
  • floor_penalty
  • floor_exponent
  • soft_max
  • hard_max
  • ceiling_penalty
  • ceiling_exponent

Copyright and related rights waived via CC0.