Terra Documentation
  • Docs
  • Whitepaper
  • Agora
  • GitHub

›MODULE SPECS

Overview

  • Introduction
  • Columbus-3 Mainnet

Terra Protocol

  • How Terra Works
  • Developer Guide
  • MODULE SPECS

    • Auth
    • Bank
    • Supply
    • Distribution
    • Staking
    • Slashing
    • Oracle
    • Market
    • Treasury
    • Governance

User Manual

    NODE SOFTWARE

    • Installation
    • User Guide
    • Light Client Daemon
    • terracli Reference
    • Ledger Nano Support
    • Production Settings
    • Join a Network
    • Deploy a Testnet

Validator Handbook

  • Introduction
  • Validator FAQ
  • GUIDES

    • Getting Started
    • Security Best Practices
    • Exchange Rate Oracle

Contribute

  • Contribution guidelines for Terra Core
  • Help to translate

Market

The Market module contains the logic for atomic swaps between Terra currencies (e.g. UST<>KRT), as well as between Terra and Luna (e.g. SDT<>Luna).

The ability to guarantee an available, liquid market with fair exchange rates between different Terra denominations and between Terra and Luna is critical for user-adoption and price-stability.

As mentioned in the protocol, the price stability of TerraSDR's peg to the SDR is achieved through Terra<>Luna arbitrage activity against the protocol's algorithmic market-maker which expands and contracts Terra supply to maintain the peg.

Swap Fees

Since Terra's price feed is derived from validator oracles, there is necessarily a delay between the on-chain reported price and the actual realtime price.

This difference is on the order of about 1 minute (our oracle VotePeriod is 30 seconds), which is negligible for nearly all practical transactions. However an attacker could take advantage of this lag and extract value out of the network through a front-running attack.

To defend against this, the Market module enforces the following swap fees

  • a Tobin Tax (set at 0.25%) for spot-converting Terra<>Terra swaps

    To illustrate, assume that oracle reports that the Luna<>SDT exchange rate is 10, and for Luna<>KRT, 10,000. Sending in 1 SDT will get you 0.1 Luna, which is 1000 KRT. After applying the Tobin Tax, you'll end up with 975 KRT (0.25% of 1000 is 25), a better rate than could get in any forex market.

  • a minimum spread (set at 2%) for Terra<>Luna swaps

    Using the same exchange rates above, swapping 1 SDT will return 980 KRT worth of Luna (2% of 1000 is 20, taken as the swap fee). In the other direction, 1 Luna would give you 9.8 SDT (2% of 10 = 0.2), or 9800 KRT (2% of 10,000 = 200).

Constant Product Market-Maker

Starting with Columbus-3 (Vodka testnet), Terra now uses a Constant Product market-making algorithm to ensure liquidity for Terra<>Luna swaps.

Before, Terra had enforced a daily Luna supply change cap such that Luna could inflate or deflate only up to the cap in any given 24 hour period, after which further swaps would fail. This was to prevent excessive volatility in Luna supply which could lead to divesting attacks (a large increase in Terra supply putting the peg at risk) or consensus attacks (a large increase in Luna supply being staked can lead to a consensus attack on the blockchain).

Now, with Constant Product, we define a value CPCPCP 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 through adjusting the spread.

Our implementation of Constant Product diverges from Uniswap's, as we use the fiat value of Luna instead of the size of the Luna pool. This nuance means changes in Luna's price don't affect the product, but rather the size of the Luna pool.

CP=PoolTerra∗FiatLunaCP = Pool_{Terra} * Fiat_{Luna}CP=Pool​Terra​​∗Fiat​Luna​​

For example, we'll start with equal pools of Terra and Luna, both worth 1000 SDR total. The size of the Terra pool is 1000 UST, 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.

CP = 1000000 SDR
(1000 SDT) * (1000 SDR of Luna) = 1000000 SDR
(1100 SDT) * (909.0909... SDR of Luna) = 1000000 SDR

This algorithm ensures that the Terra protocol remains liquid for Terra<>Luna swaps. Of course, this specific example was meant to be more illustrative than realistic -- with much larger liquidity pools used in production, the magnitude of the spread is diminished.

Virtual Liquidity Pools

The market starts out with two liquidity pools of equal sizes, one representing Terra (all denominations) and another representing Luna, initialiazed by the parameter BasePool.

In practice, rather than keeping track of the sizes of the two pools, the information is encoded in a number δ\deltaδ, which the blockchain stores as TerraPoolDelta, representing the deviation of the Terra pool from its base size in units µSDR.

The size of the Terra and Luna liquidity pools can be generated from δ\deltaδ using the following formulas:

PoolTerra=PoolBase+δPoolLuna=(PoolBase)2/PoolTerra\begin{aligned} Pool_{Terra} &= Pool_{Base} + \delta \\ \\ Pool_{Luna} &= ({Pool_{Base}})^2 / Pool_{Terra} \end{aligned}​Pool​Terra​​​​Pool​Luna​​​​​=Pool​Base​​+δ​=(Pool​Base​​)​2​​/Pool​Terra​​​​

At the end of each block, the market module will attempt 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, with lower periods meaning faster recovery times, denoting more sensitivity to changing prices.

This mechanism ensures liquidity and acts as a sort of low-pass filter, allowing for the spread fee (which is a function of TerraPoolDelta) to drop back down when changes in price are interpreted by the network as a lasting, rising trend in the true price of the peg rather than noisy spikes from temporary trading activity.

Swap Procedure

  1. Market module receives MsgSwap message and performs basic validation checks

  2. Calculate exchange rate askaskask and spreadspreadspread using k.ComputeSwap()

  3. Update TerraPoolDelta with k.ApplySwapToPool()

  4. Transfer OfferCoin from account to module using supply.SendCoinsFromAccountToModule()

  5. Burn offered coins, with supply.BurnCoins().

  6. Let fee=spread∗askfee = spread * askfee=spread∗ask, this is the spread fee.

  7. Mint ask−feeask - feeask−fee coins of AskDenom with supply.MintCoins(). This implicitly applies the spread fee as the feefeefee coins are burned.

  8. Send newly minted coins to trader with supply.SendCoinsFromModuleToAccount()

  9. Emit swap event to publicize swap and record spread fee

If the trader's Account has insufficient balance to execute the swap, the swap transaction fails.

Upon successful completion of Terra<>Luna swaps, a portion of the coins to be credited to the user's account is withheld as the spread fee.

Seigniorage

For Luna swaps into Terra, the Luna that recaptured by the protocol is burned and is called seigniorage -- the value generated from issuing new Terra. At the end of the epoch, the total seigniorage for the epoch will be calculated and reintroduced into the economy as ballot rewards for the exchange rate oracle and to the community pool by the Treasury module, described more fully here.

Message Types

MsgSwap

Swap Request

// MsgSwap contains a swap request
type MsgSwap struct {
    Trader    sdk.AccAddress `json:"trader" yaml:"trader"`         // Address of the trader
    OfferCoin sdk.Coin       `json:"offer_coin" yaml:"offer_coin"` // Coin being offered
    AskDenom  string         `json:"ask_denom" yaml:"ask_denom"`   // Denom of the coin to swap to
}

A MsgSwap transaction denotes the Trader's intent to swap their balance of OfferCoin for new denomination AskDenom, for both Terra<>Terra and Terra<>Luna swaps.

State

Terra Pool Delta δ

  • k.GetTerraPoolDelta(ctx) sdk.Dec
  • k.SetTerraPoolDelta(ctx, delta sdk.Dec)

An sdk.Dec that represents the difference between size of current Terra pool and its original base size, valued in µSDR.

Functions

k.ComputeSwap()

func (k Keeper) ComputeSwap(ctx sdk.Context, offerCoin sdk.Coin, askDenom string)
    (retDecCoin sdk.DecCoin, spread sdk.Dec, err sdk.Error)

This function detects the swap type from the offer and ask denominations and returns:

  1. The amount of asked coins that should be returned for a given offerCoin. This is achieved by first spot-converting offerCoin to µSDR and then from µSDR to the desired askDenom with the proper exchange rate reported from by the Oracle.

  2. The spread % that should be taken as a swap fee given the swap type. Terra<>Terra swaps simply have the Tobin Tax spread fee. Terra<>Luna spreads are the greater of MinSpread and spread from Constant Product pricing.

If the offerCoin's denomination is the same as askDenom, this will raise ErrRecursiveSwap.

k.ComputeSwap() uses k.ComputeInternalSwap() internally, which just contains the logic for calculating proper ask coins to exchange, without the Constant Product spread.

k.ApplySwapToPool()

func (k Keeper) ApplySwapToPool(ctx sdk.Context, offerCoin sdk.Coin, askCoin sdk.DecCoin) sdk.Error

k.ApplySwapToPools() is called during the swap to update the blockchain's measure of δ\deltaδ, TerraPoolDelta, when the balances of the Terra and Luna liquidity pools have changed.

Terra currencies share the same liquidity pool, so TerraPoolDelta remains unaltered during Terra<>Terra swaps.

For Terra<>Luna swaps, the relative sizes of the pools will be different after the swap, and δ\deltaδ will be updated with the following formulas:

  • For Terra to Luna, δ′=δ+OfferμSDR\delta' = \delta + Offer_{\mu SDR}δ​′​​=δ+Offer​μSDR​​
  • For Luna to Terra, δ′=δ−AskμSDR\delta' = \delta - Ask_{\mu SDR}δ​′​​=δ−Ask​μSDR​​

Transitions

End-Block

Market module calls k.ReplenishPools() at the end of every block, which decreases the value of TerraPoolDelta (which measures the difference between Terra and Luna pools) depending on PoolRecoveryPeriod, prprpr.

δt+1=δt−(δt/pr)\delta_{t+1} = \delta_{t} - (\delta_{t}/{pr})δ​t+1​​=δ​t​​−(δ​t​​/pr)

This allows the network to sharply increase spread fees in during acute price fluctuations, and automatically return the spread to normal after some time when the price change is long term.

Parameters

The subspace for the Market module is market.

type Params struct {
    PoolRecoveryPeriod int64   `json:"pool_recovery_period" yaml:"pool_recovery_period"`
    BasePool           sdk.Dec `json:"base_pool" yaml:"base_pool"`
    MinSpread          sdk.Dec `json:"min_spread" yaml:"min_spread"`
    TobinTax           sdk.Dec `json:"tobin_tax" yaml:"tobin_tax"`
}

PoolRecoveryPeriod

Number of blocks it takes for the Terra & Luna pools to naturally "reset" toward equilibrium (δ→0\delta \to 0δ→0) through automated pool replenishing.

  • type: int64
  • default value: core.BlocksPerDay (1 day)

BasePool

Initial starting size of both Terra and Luna liquidity pools.

  • type: sdk.Dec
  • default value: sdk.NewDec(250000 * core.MicroUnit) (250,000 SDR = 250,000,000,000 µSDR)

MinSpread

Minimum spread charged on Terra<>Luna swaps to prevent leaking value from front-running attacks.

  • type: sdk.Dec
  • default value: sdk.NewDecWithPrec(2, 2) (2%)

TobinTax

A fee added on for swap between Terra currencies (spot-trading).

  • type: sdk.Dec
  • default value: sdk.NewDecWithPrec(25, 4) (0.25%)

Events

The Market module emits the following events

swap

KeyValue
"offer"offered coins
"trader"trader's address
"swap_coin"swapped coins
"swap_fee"spread fee

Errors

ErrNoEffectivePrice

Called when a price for the asset is not registered with the oracle.

ErrInvalidOfferCoin

Called when insufficient or too large of quantity of coins are being requested for a swap

ErrRecursiveSwap

Called when Ask and Offer coin denominations are equal.

Last updated on 11/30/2019
← OracleTreasury →
  • Swap Fees
  • Constant Product Market-Maker
    • Virtual Liquidity Pools
  • Swap Procedure
    • Seigniorage
  • Message Types
    • MsgSwap
  • State
    • Terra Pool Delta δ
  • Functions
    • k.ComputeSwap()
    • k.ApplySwapToPool()
  • Transitions
    • End-Block
  • Parameters
    • PoolRecoveryPeriod
    • BasePool
    • MinSpread
    • TobinTax
  • Events
    • swap
  • Errors
    • ErrNoEffectivePrice
    • ErrInvalidOfferCoin
    • ErrRecursiveSwap
Terra Documentation
Docs
Getting Started (or other categories)Guides (or other categories)API Reference (or other categories)
Community
User ShowcaseStack OverflowProject ChatTwitter
More
BlogGitHubStar
Follow @terra_money
© 2019 Terra