Summary
Add a module that implements the functionality of uniswap
Problem Definition
As more assets are added to the hub/network, it will be useful to have a decentralized asset exchange. Uniswap is a fairly simple and accessible exchange that could fulfill this role.
Proposal
This module takes four different types of messages. One for each of the following actions: creating an exchange between two coins, swapping coins, adding liquidity to an exchange, removing liquidity from an exchange. A param for the native asset used to swap between coins will need to be specified. Each exchange will use its coin denomination as its unique id. Coins can only be directly swapped with the native asset. All other swaps will do an extra swap with the native asset to return the desired output coin.
Uniswap refers to the tokens receieved in exchange for contributing to a liquidity pool as UNI, that naming convention has been maintained in this write up. Uniswap uses x*y=k
invariant to set prices. In this implementation nativePool * coinPool = k
. Uniswap enforces a fee of %0.3 paid to liquidity providers, this can be added as a param. Uniswap allows for two types of orders, buy/sell. In a buy order, the input is calculated, the output is specified. In a sell order, the input is specified, the output is calculated
msgs.go
type MsgCreateExchange struct {
NewCoin string
}
Initializes a new exchange pair between the specified coin and the native asset
type MsgSwapOrder struct {
SwapDenom string // The desired denomination either to be bought or sold
Coin sdk.Coin // The specified amount to be either bought or sold
Bound sdk.Int // If buy order, maximum amount of coins to be sold; otherwise minimum amount of coins to be bought
Deadline time.Time // deadline for the transaction to still be considered valid
Recipient sdk.AccAddress // address output coin is being sent to
IsBuyOrder bool // boolean indicating whether the order should be treated as a buy or sell
}
If exchanging between 2 coins that are not the native asset, input coin -> native then native -> output coin. Otherwise, simply swap between native asset and specified coin. The cost of buying or selling is determined by current ratio at the time the transaction is included in a block. Bound is used to limit the amount a user is willing to buy or sell.
type MsgAddLiquidity struct {
DepositAmount sdk.Int // exact amount of native asset being add to the liquidity pool
ExchangeDenom string // denomination of the exchange being added to
MinLiquidity sdk.Int // lower bound UNI sender is willing to accept for deposited coins
MaxCoins sdk.Int // maximum amount of the coin the sender is willing to deposit.
Deadline time.Time
}
DepositAmount is 50% of the total value that is added. Must deposit an amount for native asset and coin pair; If first provider, then amount deposited determines exchange ratio (arbitrage traders bring prices to equilibrium if the ratio is off). The bounds are used to manage price changes between tx signed and executed. If the total liquidity is 0 for this exchange, then MaxCoins
is used for the amount of the exchange coin to be deposited
type MsgRemoveLiquidity struct {
WithdrawAmount sdk.Int // amount of UNI to be burned to withdraw liquidity from an exchange
ExchangeDenom string // denomination of the exchange being withdrawn from
MinNative sdk.Int // minimum amount of the native asset the sender is willing to accept
MinCoins sdk.Int // minimum amount of the exchange coin the sender is willing to accept
Deadline time.Time
}
ante.go
- exchange registry
- check if exchange already exists
- check if denom provided is valid?
- sell order
- check that sender has > msg.Input.Amount
- buy order
- check that sender has > msg.MaxCoins
- adding liquidy
- check that sender has sufficient amount of the native asset and exchange coin
- removing liquid
- check that sender has sufficient amount of UNI to burn
keeper.go
type Keeper struct {
// The key used to access the store which maps accounts to UNI balances
key sdk.StoreKey
// The reference to the CoinKeeper to modify balances after swaps or liquidity is deposited/withdrawn
ck BankKeeper
// The codec codec for binary encoding/decoding.
cdc *codec.Codec
CreateExchange(ctx sdk.context, coinDenom string)
Deposit(ctx sdk.context, amount sdk.Int, acc sdk.AccAddress) // Add Liquidity, adds specified amount of UNI to account
Withdraw(ctx sdk.context, amount sdk.Int, acc sdk.AccAddress) // Remove Liquidity, removes specified amount of UNI from account
// The following differs in where the fee is taken from
// Return the amount of coins sold given the output amount being bought
GetInputAmount(ctx sdk.context, outputAmount, inputDenom, outputDenom) sdk.Int // Buy order
// Return the amount of coins bought given the output amount being sold
GetOutputAmount(ctx sdk.context, inputAmount, inputDenom, outputDenom) sdk.Int // Sell order
}
Formulas for calculating buy/sell amount will be taken from the uniswap implementation
GetInputAmount() would correspond to GetOutputPrice()
GetOutputAmount() would correspond to GetInputPrice()
handleMsgCreateExchange
handleMsgSwapOrder
if msg.IsBuyOrder {
inputAmount := GetInputAmount(ctx, msg.Output.Amount, msg.InputDenom, msg.Output.Denom)
ck.SendCoins(ctx, exchangeAcc, senderAcc, msg.Output)
coinSold := sdk.NewCoin(msg.InputDenom, inputAmount)
ck.SendCoins(ctx, senderAcc, exchangeAcc, coinSold)
} else {
outputAmount := GetOutputAmount(ctx, msg.Input.Amount, msg.Input.Denom, msg.OutputDenom)
ck.SendCoins(ctx, senderAcc, exchangeAcc, msg.Input)
coinBought := sdk.NewCoin(msg.OutputDenom, outputAmount)
ck.SendCoins(ctx, exchangeAcc, senderAcc, coinBought)
}
handleAddLiquidity
UNI Minted = totalLiquidity * nativeDeposited / nativePool
coinDeposited = coinPool * nativeDeposited / nativePool
nativeCoin = sdk.NewCoin(nativeDenom, msg.DepositAmount)
exchangeCoin = sdk.NewCoin(msg.ExchangeDenom, coinDeposited)
ck.SendCoins(ctx, senderAcc, exchangeAcc, nativeCoin)
ck.SendCoins(ctx, senderAcc, exchangeAcc, exchangeCoin)
keeper.Deposit(ctx, UNI Minted)
totalLiquidity is the total number of UNI in existence
handleRemoveLiquidity
nativeWithdrawn = UNI burned * nativePool / totalLiquidity
coinWithdrawn = UNI burned * coinPool / totalLiquidity
nativeCoin = sdk.NewCoin(nativeDenom, msg.nativeWithdrawn)
exchangeCoin = sdk.NewCoin(msg.ExchangeDenom, coinWithdrawn)
Withdraw(ctx, UNI burned)
ck.SendCoins(ctx, exchangeAcc, senderAcc, nativeCoin)
ck.SendCoins(ctx, exchangeAcc, senderAcc, exchangeCoin)
params.go
- specify native asset for exchanging between coins
- specify fee percent to be taken for each swap
Notes:
Each exchange will need to maintain a balance of liquidity that is deposited into it.What would be the best way to create accounts for each exchange? If exchanges have accounts with the CoinKeeper then the total liquidity of each exchange can be retrieved from there.
For Admin Use