[1]:
# we use bokeh for plotting
from bokeh.io import output_notebook, show
from bokeh.layouts import grid
from decimal import Decimal

output_notebook()
Loading BokehJS ...

How Terra Market Module works#

terra

The Market module enables atomic swaps between different Terra stablecoin denominations, and between Terra and Luna. This module ensures an available, liquid market, stable prices, and fair exchange rates between the protocol’s assets.

The price stability of TerraSDR is achieved through Terra<>Luna arbitrage activity against the protocol’s algorithmic market-maker, which expands and contracts Terra’s supply to maintain its peg.

Terra/Lunc Conventions#

There are two types of tokens that can be held by accounts and wallets in the Terra protocol:

  • Terra Stablecoins

  • Lunc

Terra Stablecoins are transactional assets that are track the exchange rate of various fiat currencies. By convention, given a fiat currency, the Terra peg that corresponds to it is Terra-<3-letter ISO4217 currency-code> (see here) abbreviated <country-code>T, where the T replaces the currency’s designator. For instance, TerraKRW is the peg for the Korean Won, and is abbreviated KRT.

Lunc, the native staking asset that entitles the staking delegator to mining rewards (including exchange rate ballot rewards) if bonded to an active validator. Lunc is also is necessary for making governance proposals and collateralizing the Terra economy.

Both Terra (of all denominations) and Lunc tokens are divisible up to microunits (x10-6). The micro-unit is considered the atomic unit of tokens, and cannot be further divided.

Below is a list of several denominations that are recognized by the protocol at the time of writing:

Denomination

Micro-Unit

Code

Value

Lunc

µLunc

ulunc

0.000001 Lunc

TerraSDR

µSDR

usdr

0.000001 SDTC

TerraKRW

µKRW

ukrw

0.000001 KRTC

TerraUSD

µUSD

uusd

0.000001 USTC

Terra Currency Vs Terra Peg#

When talking about the price of LUNC we make a clear distinction between the LUNC/TerraCurrency pair and the LUNC/TerraPeg pair.

Examples#

Denomination

Terra Currency

Terra Peg

LUNC/TerraCurrency

LUNC/TerraPeg

USTC

USTC

USD

LUNC/USTC

LUNC/USD

SDTC

SDTC

SDT

LUNC/SDTC

LUNC/SDR

KRTC

KRTC

KRW

LUNC/KRTC

LUNC/KRW

EUTC

EUTC

EUR

LUNC/EUTC

LUNC/EUR

Using Terra SDK#

In order to interact with the Terra blockchain, you’ll need a connection to a Terra node. This can be done through setting up an LCDClient:

[2]:
from terra_sdk.client.lcd import LCDClient

# connnecting to the blockchain
terra = LCDClient(chain_id="columbus-5", url="https://lcd.terra.dev")
node_info = terra.tendermint.node_info()
app_version = node_info["application_version"]

# print node info
print(f"terrad version: {app_version['version']}")
print(f"cosmos sdk version: {app_version['cosmos_sdk_version']}")
terrad version: 2.1.1
cosmos sdk version: v0.45.14

The Terra Classic Oracle#

The Oracle module provides the Terra blockchain with an up-to-date and accurate price feed of exchange rates of Luna against various Terra pegs so that the Market may provide fair exchanges between Terra<>Terra currency pairs, as well as Terra<>Luna.

As price information is extrinsic to the blockchain, the Terra network relies on validators to periodically vote on the current Luna exchange rate, with the protocol tallying up the results once per VotePeriod and updating the on-chain exchange rate as the weighted median of the ballot converted Cross Exchange Rates using ReferenceTerra.

[3]:
from terra_sdk.core import Dec, Coin, Coins
from cpy_amm.market import MarketQuote
import pprint

# retrieve quotes from blockchainn
coins = Coins(terra.oracle.exchange_rates())
# convert to MarketQuote
quotes = [MarketQuote(f"LUNC/{coin.denom[1:].upper()}", float(coin.amount)) for coin in coins]
# store quotes in dict
oracle_quotes = {quote.ticker: quote for quote in quotes}

# print
print(f"Ticker{'':<15} Price{'':<15}")
for quote in quotes:
    print (f"{quote.ticker:<15} {quote.price:<15}")
Ticker                Price
LUNC/AUD        0.000131711489528299
LUNC/CAD        0.000116239766435493
LUNC/CHF        7.8572897850015e-05
LUNC/CNY        0.000636569799073119
LUNC/DKK        0.000598899606697139
LUNC/EUR        8.0419794131138e-05
LUNC/GBP        6.906538193616e-05
LUNC/HKD        0.000687728183637236
LUNC/IDR        1.3198684021860687
LUNC/INR        0.007204464832973438
LUNC/JPY        0.01266391162663878
LUNC/KRW        0.11551258250900111
LUNC/MNT        0.3022639316305531
LUNC/MYR        0.000409610333781995
LUNC/NOK        0.00094273436302436
LUNC/PHP        0.004850733328192082
LUNC/SDR        6.5891498451622e-05
LUNC/SEK        0.000946662695596247
LUNC/SGD        0.000118666624237016
LUNC/THB        0.00309525340623296
LUNC/TWD        0.00273465042275629
LUNC/USD        8.7757930191757e-05

Market Making Algorithm#

Terra uses a Constant Product market-making algorithm to ensure liquidity for Terra<>Luna swaps.

With Constant Product, a value, $ CP $, is set to the size of the Terra pool multiplied by a set fiat value of Luna, and ensure our market-maker maintains it as invariant during any swaps by adjusting the spread.

ℹ️ NOTE
The Terra blockchain’s implementation of Constant Product diverges from Uniswap’s, as the fiat value of Lunc is used instead of the size of the Luna pool. This nuance means changes in the price of Luna does not affect the product, but rather the size of the Luna pool.
\[CP = Pool_{Terra} * Pool_{Luna} * (Price_{Luna}/Price_{SDR})\]
ℹ️ NOTE
1. \((Price_{Luna}/Price_{SDR})\) means the price of the pair \(\text{LUNC/SDR}\)
2. Dividing both sides by the price of the pair \({\text{LUNC/SDR}}\) gives a Uniswap like constant product formula where \(CP\) changes everytime the Oracle publishes a new price for the pair: It is constant piecewise
\[CP^\text{Uniswap} = Pool_{Terra} * Pool_{Luna} \text{ where } CP^\text{Uniswap} = {CP \over {(Price_{Luna}/Price_{SDR})}}\]

For example, start with equal pools of Terra and Luna, both worth 1000 SDR total. The size of the Terra pool is 1000 SDT, and assuming the price of Luna<>SDR is 0.5, the size of the Luna pool is 2000 Luna. A swap of 100 SDT for Luna would return around 90.91 SDR worth of Luna (≈ 181.82 Luna). The offer of 100 SDT is added to the Terra pool, and the 90.91 SDT worth of Luna are taken out of the Luna pool.

[5]:
from cpy_amm.market import Pool, MarketQuote, MarketPair
from cpy_amm.plotting import cp_amm_autoviz

# size of the SDT pool
sdt_pool_size = 1000
# LUNC/SDR market quote
lunc_sdr = MarketQuote("LUNC/SDR", 0.5)
# liquidity pool made up of reserves SDT
sdt_pool = Pool("SDT", sdt_pool_size)
# liquidity pool made up of reserves of LUNC
lunc_pool = Pool("LUNC", sdt_pool_size/lunc_sdr.price)
# create a market for SDT/LUNC
mkt = MarketPair(sdt_pool, lunc_pool, 0, 0.5)
# autoviz for constant product amm pools
cp_amm_autoviz(mkt)

Virtual Liquidity Pools#

The market starts out with two liquidity pools of equal sizes, one representing all denominations of Terra and another representing Lunc. The parameter BasePool defines the initial size, $ Pool_{Base} $ , of the Terra and Lunc liquidity pools.

Rather than keeping track of the sizes of the two pools, this information is encoded in a number , which the blockchain stores as TerraPoolDelta. This represents the deviation of the Terra pool from its base size in units µSDR.

The size of the Terra and Lunc liquidity pools can be generated from using the following formulas:

\[Pool_{Terra} = Pool_{Base} + \delta\]
\[Pool_{Lunc} = ({Pool_{Base}})^2 / Pool_{Terra}\]
ℹ️ NOTE
The size of the Terra and Lunc liquidity pools using the above formulas are expressed in FIAT value
\(Pool_{Terra} = \text{1000 SDTC tokens}\)
\(Pool_{Lunc} = \text{1000 SDR worth of LUNC tokens}\)
  1. It assumes \(\text{1 SDTC = 1 SDR}\)

  2. This is where the peg value comes from

Token Vs Fiat value using the same example as above#

[7]:
from cpy_amm.market import Pool, MarketPair
from cpy_amm.plotting import new_constant_product_figure, new_price_impact_figure, new_pool_figure

# size of the SDT pool
base_pool = 1000
# we start with equal pools so delta=0
terra_pool_delta = 0
# liquidity pool made up of reserves SDT
terra_pool = Pool("SDTC", base_pool+terra_pool_delta)
# using the formula above
lunc_pool_fiat = Pool("LUNC", base_pool**2/terra_pool.balance)
# using the previous example
lunc_pool_token = Pool("LUNC", sdt_pool_size/lunc_sdr.price)
# create fiat and token markets
fiat_mkt = MarketPair(terra_pool, lunc_pool_fiat, 0, lunc_sdr.price)
token_mkt = MarketPair(terra_pool, lunc_pool_token, 0, lunc_sdr.price)
# pools fiat vs tokens
pool_fiat_figure = new_pool_figure(terra_pool, lunc_pool_fiat, steps=["Initial deposit"])
pool_token_figure = new_pool_figure(terra_pool, lunc_pool_token, steps=["Initial deposit"])
# constant product fiat vs tokens
cp_figure = new_constant_product_figure(fiat_mkt)
cp_figure = new_constant_product_figure(token_mkt, bokeh_figure=cp_figure)
# price impact fiat vs tokens
price_impact_figure = new_price_impact_figure(fiat_mkt)
price_impact_figure = new_price_impact_figure(token_mkt, bokeh_figure=price_impact_figure)
# display figures
show(grid([[pool_fiat_figure, pool_token_figure], [cp_figure, price_impact_figure]], sizing_mode="stretch_both"))

Using data from Columbus-5#

[8]:
from terra_sdk.core import Dec

# convert µSDR (1µSDR=0.000001SDR) to any denom supported by the Oracle eg. µSDR -> SDR, µSDR -> USD etc.
def usdr_to_denom(usdr: Dec, denom: str) -> float:
    #get LUNC/DENOM FX from oracle
    lunc_denom_fx = oracle_quotes[f"LUNC/{denom}"].price
    #get LUNC/SDR FX from oracle and convert to SDR/LUNC
    sdr_lunc_fx = 1/oracle_quotes[f"LUNC/SDR"].price
    #Convert µSDR -> SDR -> DENOM using LUNC as pivot currency
    usdr_in_denom = 0.000001*float(usdr)*sdr_lunc_fx*lunc_denom_fx
    return usdr_in_denom

# retrieve the base pool state variable from Columbus-5
base_pool = usdr_to_denom(terra.market.parameters()["base_pool"], "SDR")
# retrieve the terra pool delta state variable from Columbus-5
terra_pool_delta = usdr_to_denom(terra.market.terra_pool_delta(), "SDR")

print(f"base pool parameter from Columbus-5: {base_pool:.10f}")
print(f"terra pool  delta parameter from Columbus-5: {terra_pool_delta:.10f}")
base pool parameter from Columbus-5: 100000000.0000000000
terra pool  delta parameter from Columbus-5: 0.0000000000
[11]:
from cpy_amm.market import Pool, MarketPair
from cpy_amm.plotting import cp_amm_autoviz

# liquidity pool made up of reserves SDTC
terra_pool = Pool("SDTC", (base_pool+terra_pool_delta))
# liquidity pool made up of reserves of LUNC
lunc_pool = Pool("LUNC", base_pool**2/terra_pool.balance)
# create a market for SDTC/LUNC
mkt = MarketPair(terra_pool, lunc_pool, 0, lunc_sdr.price)
# autoviz for constant product amm pools
cp_amm_autoviz(mkt)

Replenish pools#

At the end of each block, the market module attempts to replenish the pools by decreasing the magnitude of \(\delta\) between the Terra and Luna pools. The rate at which the pools will be replenished toward equilibrium is set by the parameter PoolRecoveryPeriod. Lower periods mean lower sensitivity to trades: previous trades are more quickly forgotten and the market is able to offer more liquidity.

This mechanism ensures liquidity and acts as a low-pass filter, allowing for the spread fee (which is a function of TerraPoolDelta) to drop back down when there is a change in demand, causing a necessary change in supply which needs to be absorbed.

ℹ️ NOTE
Replenish pools acts as a money printer for the protocol
The pool is at equilibrium when the mid price is 1 eg. swap 1 USTC for 1 USD worth of LUNC
Arbing the pool will result in a new mid price eg. swap 1.2 USTC for 1 USD worth of LUNC
ReplenishPool will create new tokens (mint?) to trade the pool back to equilibrium
ReplenishPool changes the total supply

Replenish pool by decreasing the magnitude of \(\delta\)#

In the example below we start with 1000 SDTC and 2000 LUNC in the pools. We swap 300 SDTC for ~462 LUNC and there is now 1300 SDTC and ~1538 LUNC in the pools. The “replenish” process will then mint a total of ~462 LUNC to swap against USTC until we go back to the initial 1000 SDTC and 2000 LUNC tokens in the pool.
This process happens during the pool recovery period which is currently 18 blocks.
[12]:
from cpy_amm.market import Pool, MarketQuote, MarketPair, TradeOrder
from cpy_amm.swap import constant_product_swap
from cpy_amm.plotting import new_pool_figure, new_constant_product_figure

# retrieve the base pool state variable from Columbus-5
base_pool = 1000
# retrieve the terra pool delta state variable from Columbus-5
terra_pool_delta = 0
# LUNC/SDR = 0.5 SDR
lunc_sdr = MarketQuote("LUNC/SDR", 0.5)
# liquidity pool made up of reserves SDT
terra_pool = Pool("SDTC", (base_pool+terra_pool_delta))
# liquidity pool made up of reserves of LUNC
lunc_pool = Pool("LUNC", base_pool**2/terra_pool.balance/lunc_sdr.price)
# create a market for SDTC/LUNC
mkt = MarketPair(terra_pool, lunc_pool, 0, lunc_sdr.price)
# constant product curve
cp_figure = new_constant_product_figure(mkt, x_max=2000)
# swap 300 SDTC for LUNC
trade_order = TradeOrder("SDTC/LUNC", 300, mkt.swap_fee)
# swap 300 sdtc_in for dy LUNC tokens
constant_product_swap(mkt, trade_order)
# constant product fiat vs tokens
cp_figure = new_constant_product_figure(mkt, bokeh_figure=cp_figure, x_max=2000)
# update terra pool delta
terra_pool_delta = terra_pool.balance-terra_pool.initial_deposit
# get pool recovery period
pool_recovery_period = terra.market.parameters()["pool_recovery_period"]
# replenish amount per 1 block
replenish_amount = terra_pool_delta / pool_recovery_period
# simulate Replenish Pools at the end of each blocks during the pool recovery period (currently 18 blocks)
for block in range(1, pool_recovery_period+1):
    # replenish amount per 1 block
    terra_pool_delta -= replenish_amount
    # liquidity pool made up of reserves SDT
    terra_pool.reserves.append(base_pool+terra_pool_delta)
    # liquidity pool made up of reserves of LUNC
    lunc_pool.reserves.append(base_pool**2/terra_pool.balance/lunc_sdr.price)
    # constant product
    cp_figure = new_constant_product_figure(mkt, bokeh_figure=cp_figure, x_max=2000)
# display pools after replenish pools (18 blocks)
pool_figure = new_pool_figure(terra_pool, lunc_pool, steps=[f"b{i}" for i in range(0,len(lunc_pool.reserves))])
show(grid([[cp_figure], [pool_figure]], sizing_mode="stretch_both"))

Replenish pool by minting tokens and swapping against the pool#

Same thing as above, except that in this case, we use the ~462 LUNC to swap against the pool. It gives the same result: Replenish Pool swaps against the pool using tokens it has created, it changes the total supply

[19]:
from cpy_amm.market import Pool, MarketQuote, MarketPair
from cpy_amm.swap import constant_product_swap
from cpy_amm.plotting import new_pool_figure, new_constant_product_figure

# retrieve the base pool state variable from Columbus-5
base_pool = 1000
# retrieve the terra pool delta state variable from Columbus-5
terra_pool_delta = 0
# LUNC/SDR = 0.5 SDR
lunc_sdr = MarketQuote("LUNC/SDR", 0.5)
# liquidity pool made up of reserves SDT
terra_pool = Pool("SDTC", (base_pool+terra_pool_delta))
# liquidity pool made up of reserves of LUNC
lunc_pool = Pool("LUNC", base_pool**2/terra_pool.balance/lunc_sdr.price)
# create a market for SDTC/LUNC
mkt = MarketPair(terra_pool, lunc_pool, 0, lunc_sdr.price)
# constant product curve
cp_figure = new_constant_product_figure(mkt, x_max=2000)
# swap 300 SDTC for LUNC
trade_order = TradeOrder("LUNC/SDTC", 300, mkt.swap_fee)
# swap sdtc_in for dy LUNC tokens
lunc_out, _ = constant_product_swap(mkt, trade_order)
# constant product curve
cp_figure = new_constant_product_figure(mkt, bokeh_figure=cp_figure, x_max=2000)
# get pool recovery period
pool_recovery_period = terra.market.parameters()["pool_recovery_period"]
# replenish amount per 1 block
lunc_in = lunc_out / pool_recovery_period
# swap lunc_in LUNC for SDTC to bring delta back to 0
trade_order = TradeOrder("LUNC/SDTC", -lunc_in, mkt.swap_fee)
# simulate Replenish Pools at the end of each blocks during the pool recovery period (currently 18 blocks)
for block in range(1, pool_recovery_period+1):
    # swap sdtc_in for dy LUNC tokens
    constant_product_swap(mkt, trade_order)
    # constant product
    cp_figure = new_constant_product_figure(mkt, bokeh_figure=cp_figure, x_max=2000)
# display pools after replenish pools (18 blocks)
pool_figure = new_pool_figure(terra_pool, lunc_pool, steps=[f"b{i}" for i in range(0,len(lunc_pool.reserves))])
show(grid([[cp_figure], [pool_figure]], sizing_mode="stretch_both"))