Skip to main content
Version: 5.0

Messages

Deposit

Overview

In Neutron DEX’s concentrated liquidity model, liquidity providers (LPs) can provide liquidity to a specific trading pair by depositing tokens at a specific price into one or both sides of the pair in “a liquidity pool”. When depositing into the pool LPs must choose a fee for their deposit. This fee will be paid every time a trader swaps through a liquidity pool. When a trader wants to buy or sell one of the tokens in the pair, they must first pay the pool fee, which is a percentage of the trade value, to the liquidity pool. This fee is then shared among the LPs in proportion to their share of the liquidity in the pool. In a concentrated liquidity DEX, LPs can set their own price range for each token in the pair. This means that they can set a higher price for selling their token and a lower price for buying the other token, or vice-versa. This allows LPs to earn more fees by capturing the spread between the buying and selling prices. The deposit message expects AmountsA, AmountsB, TickIndexesAToB, and Fees to be passed as equal lengthen arrays of values. This allows deposits to be made simultaneously across a range of positions.

Deposit Mechanism

When depositing into an LP position (PoolReserves) a user specifies amounts of TokenA and TokenB as well as a TickIndexAToB and a fee. The liquidity is added to the reserves for the respective ticks. Internally the DEX Normalizes TokenA and TokenB to Token0 and Token1 where Token0 is the smaller of the 2 alphabetically sorted tokens. As a convenience to users, they do not have to know the canonical Token0 and Token1 for a pair, instead they can assign either denom as TokenA or TokenB. TokenA and TokenB along with the TickIndexAToB will be normalized according to the canonical PairID.

TickIndexAToB specifies the tick providing the desired conversion rate from TokenA to TokenB. This means that TokenB will be deposited at TickIndexAToB+feeTickIndexAToB + fee and TokenA will be deposited at TickIndexAToB1+feeTickIndexAToB\cdot-1 + fee.

In the most basic case, when depositing into a pool, the ratio of Token0 to Token1 will be preserved. If a user does not provide tokens in the same ratio, then only a portion of their total deposit will be used so as to maintain the pool ratio.

true0=min(amountDeposited0,existingReserves0existingReserves1amountDeposited1)true_0 = min(amountDeposited_0, \frac{existingReserves_0}{existingReserves_1}\cdot amountDeposited_1)

true1=min(amountDeposited1,existingReserves1existingReserves0amountDeposited0)true_1 = min(amountDeposited_1, \frac{existingReserves_1}{existingReserves_0}\cdot amountDeposited_0)

NOTE: Most pools will only have Token1 OR Token0, so most deposits will only be providing one tokens to one side of the pool.

In return for depositing tokens into a pool the user is issued PoolShares corresponding to the specific liquidity pool in which they have deposited. These can be used in the future to withdraw the user’s pro-rata share of the PoolLiquidity. The PoolShares are also fungible denoms that can be bought, sold, and traded.

The amount of pool shares issued is calculated using the following formula:

valueDeposited=true0+p(i)true1valueDeposited = true_0 + p(i)\cdot true_1

newShares=valueDepositedtotalSharesvalueTotalnewShares = \frac{valueDeposited \cdot totalShares}{valueTotal}

Behind Enemy Lines

In order to maintain basic invariants of the DEX users cannot deposit liquidity at price cheaper than bid price for the opposing liquidity, also known as Behind-Enemy-Lines (BEL). If a deposit is placed BEL it will be ignored, but all other deposits will still succeed. The entire transaction can be failed atomically if the FailTxOnBEL option is set to true.

Autoswap

In rare cases where the target deposit pool has liquidity of both token0 and token1, autoswap is required to perform the deposit on that tick. Adding an autoswap fee prevents users from getting "free" swaps. For example, a user could deposit single-sided liquidity into a 2-sided and then withdraw both Token0 and Token1.

By default the autoswap option is enabled, which allows users to deposit their full deposit amount. Autoswap provides a mechanism for users to deposit the entirety of their specified deposit amounts by paying a fee equal to what it would cost to swap their deposit into a matching ratio. The fee for performing an autoswap is deducted from the total number of shares the the user is issued. To calculate the amount of tokens to charge the autoswap fee against we use the following formula.

existingReserves0existingReserves1=amountDeposited0+pricesamountDeposited1s\frac{existingReserves_0}{existingReserves_1} = \frac{amountDeposited_0 + price \cdot s} {amountDeposited_1 - s}

Where s is the amount that would need to be swapped to match the exiting pool ratio. If we solve for s we get:

s=existingReserves0amountDeposited1r1amountDeposited0existingReserves1price+existingReserves0s = \frac{existingReserves_0 \cdot amountDeposited_1 - r_1\cdot amountDeposited_0}{existingReserves_1 \cdot price + existingReserves_0}

Thus if s > 0, the residual swap amount is sToken0sToken0. Otherwise the residual is sToken1-sToken1.

The autoswap fee can then be calculated as follows:

autoswapFee=(residual0+residual1price1To0)(1p(fee))autoswapFee = (residual_0 + residual_1 \cdot price1To0) \cdot (1 - p(fee))

Finally the shares issued is calculated as follows:

newShares=(valueDepositedautoswapFee)totalSharesvalueTotal+autoswapFeenewShares = \frac{(valueDeposited-autoswapFee )\cdot totalShares}{valueTotal+autoswapFee}

Deposit Message

message MsgDeposit {
string creator = 1;
string receiver = 2;
string token_a = 3;
string token_b = 4;
repeated string amounts_a = 5 [
(gogoproto.moretags) = "yaml:\"amounts_a\"",
(gogoproto.customtype) = "cosmossdk.io/math.Int",
(gogoproto.nullable) = false,
(gogoproto.jsontag) = "amounts_a"
];
repeated string amounts_b = 6 [
(gogoproto.moretags) = "yaml:\"amounts_b\"",
(gogoproto.customtype) = "cosmossdk.io/math.Int",
(gogoproto.nullable) = false,
(gogoproto.jsontag) = "amounts_b"
];
repeated int64 tick_indexes_a_to_b = 7;
repeated uint64 fees = 8;
repeated DepositOptions options = 9;
}

message DepositOptions {
bool disable_autoswap = 1;
bool fail_tx_on_bel = 2;
}

MsgDeposit

FeildDescription
Creator string (sdk.AccAddress)The account from which deposit Tokens will be debited
Receiver string (sdk.AccAddress)The account to which PoolShares will be issued
TokenA stringDenom for one side of the deposit
TokenB stringDenom for the opposing side of the deposit
AmountsA []sdk.IntAmounts of tokenA to deposit
AmountsB []sdk.IntAmounts of tokenB to deposit
TickIndexesAToB []int64Tick indexes to deposit at defined in terms of TokenA to TokenB
Fees []uint64Fees to use for each deposit
Options []DepositOptionsAdditional deposit options

DepositOptions

FieldDescription
disable_autoswap boolToggle to disable autoswap (default false)
fail_tx_on_bel boolToggle to fail entire transaction if behind-enemy-lines (default false)

Withdrawal

Overview

Withdraw is used to redeem PoolShares for the user’s pro-rata portion of tokens within a liquidity pool. Users can withdraw from a pool at any time. When Withdrawing from a pool they will receive Token0 and Token1 in the same ratio as what is currently present in the pool. When withdrawing the users PoolShares are burned and their account is credited with the withdrawn tokens.

Withdrawal Message

message MsgWithdrawal {
string creator = 1;
string receiver = 2;
string token_a = 3;
string token_b = 4;
repeated string shares_to_remove = 5 [
(gogoproto.moretags) = "yaml:\"shares_to_remove\"",
(gogoproto.customtype) = "cosmossdk.io/math.Int",
(gogoproto.nullable) = false,
(gogoproto.jsontag) = "shares_to_remove"
];
repeated int64 tick_indexes_a_to_b = 6;
repeated uint64 fees = 7;
}
FieldDescription
Creator string (sdk.AccAddress)The account from which the PoolShares are removed
Receiver string (sdk.AccAddress)The account to which the tokens are credited
TokenA stringDenom for one side of the deposit
TokenB stringDenom for the opposing side of the deposit
SharesToRemove []sdk.IntAmount of shares to remove from each pool
TickIndexesAToB []int64Tick indexes of the target LiquidityPools defined in terms of TokenA to TokenB
Fees []uint64Fee for the target LiquidityPools

MultiHop Swap

Overview

Multihop swap provides a swapping mechanism to achieve better prices by routing through a series of pools. Rather than swapping directly from TokenA to TokenB a user may be able to get a better price if they swap from TokenA to TokenC to TokenD to TokenB. When performing a multihop swap the user provides an array of Denoms that they would like to swap through. For example if the user supplied the following array of Denoms [“TokenA”, “TokenC”, “TokenD”, “TokenB”], the following swaps would be performed: Swap TokenA for TokenC; Swap TokenC for TokenD; Swap TokenD for TokenB.

The underlying swaps (hops) within a Multihop Swap are performed using the same mechanism as the basic Swap function. Unlike the the basic swap however, the complete amount of specified by AmountIn will always be used. If there is insufficient liquidity in a route to swap 100% of the AmountIn the route will fail. Additionally, rather than supply an explicit argument for TokenIn the first denom in each Routes array is used as the TokenIn.

MultihopSwap also allows users to set an ExitLimitPrice. For a route to succeed the final conversion rate for the EntryToken to ExitToken must be less than the ExitLimitPrice. For a Multihop swap to succeed the following test must be satisfied:

ExitLimitPrice<=AmountOfExitTokenAmountInExitLimitPrice <= \frac{AmountOfExitToken}{AmountIn}

Multihop swap also allows users to supply multiple different routes. By default, the first route that does not run out of liquidity, hit the ExitLimitPrice or return an error will be used. Multihop swap also provides a PickBestRoute option. When PickBestRoute is true all routes will be run and the route that results in the greatest amount of TokenOut at final hop will be used. This option to dynamically pick the best route at runtime significantly reduces the risk of front running. Note that a successful MultiHop swap can produce dust on any pool in swaps through. This dust is credited to the caller.

Multihop Swap Message

message MultiHopRoute {
repeated string hops = 1;
}

message MsgMultiHopSwap {
string creator = 1;
string receiver = 2;
repeated MultiHopRoute routes = 3;
string amount_in = 4 [
(gogoproto.moretags) = "yaml:\"amount_in\"",
(gogoproto.customtype) = "cosmossdk.io/math.Int",
(gogoproto.nullable) = false,
(gogoproto.jsontag) = "amount_in"
];
string exit_limit_price = 5 [
(gogoproto.moretags) = "yaml:\"exit_limit_price\"",
(gogoproto.customtype) = "github.com/neutron-org/neutron/v4/utils/math.PrecDec",
(gogoproto.nullable) = false,
(gogoproto.jsontag) = "exit_limit_price"
];
bool pick_best_route = 6;
}

MsgMultiHopSwap

FieldDescription
Creator string (sdk.AccAddress)Account from which TokenIn is debited
Receiver string (sdk.AccAddress)Account to which TokenOut is credited
Routes []MultiHopRouteArray of possible routes
AmountIn sdk.IntAmount of TokenIn to swap
ExitLimitPrice sdk.DecMinimum price that that must be satisfied for a route to succeed
PickBestRoute boolIf true all routes are run and the route with the best price is used (default false)

Multihop Route

FieldDescription
Hops []StringArray of denoms to route through

Place Limit Order

Overview

Limit orders provide the primary mechanism for trading on the Neutron DEX Dex. Limit orders can provide liquidity to the Dex (“Maker LimitOrders”) and/or can be used to trade against preexisting liquidity (“Taker Limit Orders”). All limit orders provide a limitSellPrice which represents the lowest allowable price at which a trade will be executed. Limit price can be converted to TickIndex with the following formula:

log1.0001(price)=TickIndexlog_{1.0001}(price) = TickIndex

Taker limit orders will swap through all liquidity at ticks less than or equal to the TickIndex. Maker limit orders will be placed at 1TickIndex-1\cdot TickIndex.

Maker limit orders provide new liquidity to the dex that can be swapped through by other traders (either via Multihop Swap or a Taker Limit Order.) The liquidity supplied by a maker limit order is stored in a LimitOrderTranche at a specific tick. Once the tranche has been fully or partially filled via another order the user can withdraw the proceeds from that tranche. Maker limit orders can also be cancelled at any time. Maker only limit order’s are created with the following order types: GOOD\_TIL\_CANCELLED, JUST\_IN\_TIME and GOOD\_TIL\_TIME. All Maker limit order will first try to fill via existing liquidity on the DEX priced below their limit_sell_price, any amount of TokenIn that cannot be immediately swapped will be placed into a LimitOrderTranche. The proceeds from this initial swap will be deposited directly back to the Receiver.

Taker limit orders do not add liquidity to the dex, instead they trade against existing TickLiquidity. Taker orders will either fail at the time of transaction or be completed immediately. Successful taker orders will deposit the proceeds directly back into the Receiver account.

As a final step for all limit orders, we check that the true price for the trade (amountOut/amountInamountOut/amountIn) is greater than or equal to the supplied limit price. Due to rounding behavior in the dex, it is possible for the true price to be less than the limitPrice even though all the liquidity traded through is priced above the LimitSellPrice. All limit orders also have an optional MinAverageSellPrice field, if supplied this field will be used instead of the LimitSellPrice for this true price check.

Order types

FILL_OR_KILL

Fill-or-Kill limit orders are taker limit orders that either successfully swap 100% of the supplied AmountIn or return an error. If there is insufficient liquidity to complete the trade at or above the supplied LimitSellPrice a Fill-or-Kill order will return an error of ErrFoKLimitOrderNotFilled.

IMMEDIATE_OR_CANCEL

Immediate-or-Cancel limit orders are taker orders that will swap as much as of the AmountIn as possible given available liquidity above the supplied LimitSellPrice. Unlike Fill-or-Kill orders they will still successfully complete even if they are only able to partially trade through the AmountIn at the LimitSellPrice or better.

GOOD_TIL_CANCELLED

Good-til-Cancelled limit orders are hybrid maker and taker limit orders. They will attempt to trade the supplied AmountIn at the LimitSellPrice or better. However, if the total AmountIn cannot be traded at the limit price the remaining amount will be placed as a maker limit order. The proceeds from the taker portion are deposited into the user’s account immediately, however, the proceeds from the maker portion must be explicitly withdrawn via WithdrawLimitOrder.

GOOD_TIL_TIME;

Good-til-Time limit orders function exactly the same as Good-til-Cancelled limit orders, first trying to trade as a taker limit order and then placing any remaining amount as a maker limit order. However, the maker portion of the limit order has a specified ExpirationTime. After the ExpirationTime the order will be automatically cancelled and can no longer be traded against. When withdrawing a Good-til-Time limit order the user will receive both the successfully traded portion of the limit order (TokenOut) as well as any remaining untraded amount (TokenIn).

JUST_IN_TIME;

Just-in-Time limit orders are an advanced maker limit order order that provides tradeable liquidity for exactly one block. They operate the same as GOOD_TIL_TIME limit orders, but have a fixed expiration of one block. At the beginning of the block after the Just-in-Time order was submitted the order is cancelled and any untraded portion will no longer be usable as active liquidity.

PlaceLimitOrder Message

message MsgPlaceLimitOrder {
string creator = 1;
string receiver = 2;
string token_in = 3;
string token_out = 4;
// DEPRECATED: tick_index_in_to_out will be removed in future release; limit_sell_price should be used instead.
int64 tick_index_in_to_out = 5 [deprecated = true];
string amount_in = 7 [
(gogoproto.moretags) = "yaml:\"amount_in\"",
(gogoproto.customtype) = "cosmossdk.io/math.Int",
(gogoproto.nullable) = false,
(gogoproto.jsontag) = "amount_in"
];
LimitOrderType order_type = 8;
// expirationTime is only valid iff orderType == GOOD_TIL_TIME.
google.protobuf.Timestamp expiration_time = 9 [
(gogoproto.stdtime) = true,
(gogoproto.nullable) = true
];
string max_amount_out = 10 [
(gogoproto.moretags) = "yaml:\"max_amount_out\"",
(gogoproto.customtype) = "cosmossdk.io/math.Int",
(gogoproto.nullable) = true,
(gogoproto.jsontag) = "max_amount_out"
];
string limit_sell_price = 11 [
(gogoproto.moretags) = "yaml:\"limit_sell_price\"",
(gogoproto.customtype) = "github.com/neutron-org/neutron/v5/utils/math.PrecDec",
(gogoproto.nullable) = true,
(gogoproto.jsontag) = "limit_sell_price",
(amino.encoding) = "cosmos_dec_bytes"
];
// min_average_sell_price is an optional parameter that sets a required minimum average price for the entire trade.
// if the min_average_sell_price is not met the trade will fail.
// If min_average_sell_price is omitted limit_sell_price will be used instead
string min_average_sell_price = 12 [
(gogoproto.moretags) = "yaml:\"min_average_sell_price\"",
(gogoproto.customtype) = "github.com/neutron-org/neutron/v5/utils/math.PrecDec",
(gogoproto.nullable) = true,
(gogoproto.jsontag) = "min_average_sell_price",
(amino.encoding) = "cosmos_dec_bytes"
];
}

enum LimitOrderType {
GOOD_TIL_CANCELLED = 0;
FILL_OR_KILL = 1;
IMMEDIATE_OR_CANCEL = 2;
JUST_IN_TIME = 3;
GOOD_TIL_TIME = 4;
}

MsgPlaceLimitOrder

FieldDesciption
Creator string (sdk.AccAddress)Account from which TokenIn is debited
Receiver string (sdk.AccAddress)Account to which TokenOut is credited or that will be allowed to withdraw or cancel a maker order
TokenIn stringToken being “sold”
TokenOutToken being “bought”
AmountIn sdk.IntAmount of TokenIn to be traded
OrderType orderTypeType of limit order to be used. Must be one of: GOOD_TIL_CANCELLED, FILL_OR_KILL, IMMEDIATE_OR_CANCEL, JUST_IN_TIME, or GOOD_TIL_TIME
LimitSellPrice sdk.DecLimit sell price
MaxAmountOut sdk.IntThe maximum TokenOut a user wants to receive
ExpirationTime time.TimeExpiration time for order. Only valid for GOOD_TIL_TIME limit orders
MinAverageSellPrice sdk.DecOptional Minimum price that must be satisfied by the true output of a limit order

Cancel Limit Order

Overview

Standard Maker limit orders (Good-til-cancelled & Good-til-Time) can be cancelled at any time (even if they have been filled). Once a limit order is cancelled any remaining TokenIn as well as TokenOut profits are returned to the Creaator.

Cancel Limit Order Message

message MsgCancelLimitOrder {
string creator = 1;
string tranche_key = 2;
}

MsgCancelLimitOrder

FieldDescription
Creator string (sdk.AccAddress)Account which controls the limit order and to which TookenIn and TokenOut is credited
TrancheKey stringTrancheKey for the target limit order

Withdraw Filled Limit Order

Overview

Once a limit order has been filled – either partially or in its entirety, it can be withdrawn at any time. Withdrawing from a limit order credits all available proceeds to the user. Withdraw can be called on a limit order multiple times as new proceeds become available.

Withdraw Filled Limit Order Message

message MsgWithdrawFilledLimitOrder {
string creator = 1;
string tranche_key = 2;
}

MsgWithdrawFilledLimitOrder

FieldDescription
Creator string (sdk.AccAddress)Account which controls the limit order and to which proceeds are credited
TrancheKey stringTrancheKey for the target limit order

UpdateParams

Overview

Used to update DEX params

UpdateParamsMsg

message MsgUpdateParams {
string authority = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
// NOTE: All parameters must be supplied.
Params params = 2 [
(gogoproto.nullable) = false,
(amino.dont_omitempty) = true
];
}

message Params {
option (gogoproto.goproto_stringer) = false;
repeated uint64 fee_tiers = 1;
uint64 max_jits_per_block = 3;
uint64 good_til_purge_allowance = 4;
bool paused = 5;
}
FieldDescription
FeeTiers uint64Fee tiers is the list of allowable fees that can be used for LP deposits
MaxJitsPerBlock uint64Maximum number of JIT limit orders allowed to be placed in a single block
GoodTilPurgeAllowance uint64Amount of gas reserved for purging non-JIT limit orders in BeginBlocker
Paused boolOption to disable all messages to the dex

Gas Estimates

Below are basic gas estimates for various Dex operations. Depending on the exact state of the dex and the inputs of the message being sent real gas costs can vary substantially. For operations that touch multiple ticks the gas cost can be estimated using the formula:

gasUsed=fixedGasCost+perTickGasnTicksgasUsed = fixedGasCost + perTickGas*nTicks

These estimates only consider the gas costs at the message server level and below; application level (ie. AnteHandler) gas costs are not included.

Deposit

CASEFIXED_GASPER_TICK_GAS
New pools2182548076
Adding to existing pools1906939100

*assumes single-sided deposits

Withdrawal

FIXED_GASPER_TICK_GAS
2182532215

PlaceLimitOrder

CASEFIXED_GASPER_TICK_GAS
Taker only limit order442137779
Maker only GTC31095N/A
Maker only JIT48095N/A
Maker only GoodTil49107N/A

WithdrawLimitOrder

FIXED_GASPER_TICK_GAS
24992N/A

CancelLimitOrder

FIXED_GASPER_TICK_GAS
25451N/A