PulseTrade
PulseTrade, also referred to as the order system, is a trading mechanism engineered to mitigate the price impact of high-volume trades. It achieves this by allowing users to submit long-term orders that execute incrementally, thereby reducing the trade's overall price impact. Furthermore, to enhance the user experience and facilitate flexible fund management, the system locks only the funds required for each step of the order. The remaining funds are locked on-demand as the order progresses.
A unique feature of the order system allows off-chain solvers to propose matches between orders. These matches can be executed instantaneously, providing orders with efficient execution without the need for long-term waiting. As compensation for their input, solvers receive a fee. Therefore, the order system serves as a potent tool for traders aiming to execute high-volume trades with minimal price impact and optimal efficiency.
Specifically, an order can be placed to sell a specified quantity of one token for another in incremental steps. This order executes against a particular pool or a path among several pools. The data structure of an order is as follows:
message Order {
uint64 id = 1;
string creator = 2;
uint64 pool_id = 3;
string token_in = 4;
string token_out = 5;
bool whitelisted_route = 6;
bool allow_matching = 7;
string amount_per_step = 8 [
(cosmos_proto.scalar) = "cosmos.Int",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.nullable) = false
];
string remaining_amount = 9 [
(cosmos_proto.scalar) = "cosmos.Int",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.nullable) = false
];
string deposited_amount = 10 [
(cosmos_proto.scalar) = "cosmos.Int",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.nullable) = false
];
int64 min_millis_interval = 11;
string max_step_spot_price = 12 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
string max_matching_spot_price = 13 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = true
];
}
The Order
message is a protobuf message comprising the following fields:
id
: auto-generated integer that serves as the order identifier.creator
: the account address of the order's creator.pool_id
: identifier of the pool to process the order.token_in
: type of token to sell.token_out
: type of token to buy.whitelisted_route
: a boolean value indicating whether the order should use a specific pool or a pre-approved route between the input and output tokens.allow_matching
: a boolean value indicating if solvers can match the order.amount_per_step
: amount of token to sell per step.remaining_amount
: the unprocessed tokens for sale, updated as the order is processed.deposited_amount
: amount of tokens deposited for processing in the next step.min_millis_interval
: integer representing the minimum time interval between steps in milliseconds.max_step_spot_price
: the maximum price for executing the order step-by-step. If the pool's computed price exceeds this limit, the order remains in the queue.max_matching_spot_price
: the highest price allowed for matching proposals.
Whitelisted Routes
The amm module supports both batch and single step swaps. Users may want to place multi-step orders. To ensure efficient execution, only a governance-approved set of multi-step orders is allowed. Governance can maintain a list of approved routes, each defining a multi-step route from one token to another. These routes are bidirectional. The structure of an approved route is as follows:
message WhitelistedRoute {
repeated RouteStep steps = 1 [(gogoproto.nullable) = false];
bool enabled = 2;
}
message RouteStep {
uint64 pool_id = 1;
string token_in = 2;
string token_out = 3;
}
Order Queues
The order system maintains two main queues to respect timing and price criteria:
- Execution Queue: These orders are ready for single-step execution. This implies:
- The next step's
amountIn
has been taken from the user and is in the amm vault. - The order's
min_millis_interval
has elapsed since the last step execution. - At each block's beginning, if the order's
max_step_spot_price
criteria is met, it can be executed for one step and moved to the scheduler queue. - The key for this queue is composed of:
whitelisted | poolId | tokenIn | tokenOut | maxPrice | orderId
. Orders for the same pair are sorted bymaxPrice
.
- The next step's
- Schedule Queue: These orders have been executed for at least one step and will be added to the execution queue once the required time interval has passed since their last execution. These orders are stored with a key of form:
time | orderId
, meaning that orders are sorted by their next execution time, equal too.MinMillisInterval+lastExecution
. It is worth mentioning that theo.MinMillisInterval
is moved to the nearest number in the range of minimum and maximum number allowed by order parameters.
When a user submits a new order, the first step of their order is immediately scheduled for execution. Therefore, we:
- Take the one-step amount from the user account and transfer it to our vault.
- Insert the step into the Execution Queue.
Execution at each block:
To execute orders, we iterate through pairs in the system and first calculate the current spot price for each pair. We then query orders using a key equal to or larger than whitelisted | poolId | tokenIn | tokenOut | currentPrice
. We improve performance and minimize price impact by initially offsetting orders in opposite directions, then executing the remaining orders against pools. After an order step is executed, it is added to the scheduler queue for its subsequent execution.
Block-by-Block Scheduling:
For each block, we first examine the scheduling queue to schedule trades. We select trades from the queue with a key equal to or larger than the current block time and add them to the execution queue. It's important to note that all orders in the scheduling queue have already been executed at least once; thus, their spot price is reasonable and closely aligned with the current spot price. This makes them likely candidates for execution.
Order Matching
To enhance the user experience, we offer a zero price impact matching system that also eliminates the need for long-term order execution. Users can enable order matching and set a price limit. It's important to note that the step-by-step execution price limit is separate from the matching price limit. Users may choose to set a higher max price for matching, as it allows for instant execution without any impact on the price.
A solver can review orders enabled for matching and identify compatible orders within the system. Since the entire order amount is not locked upon submission, users might not have the required amount for a complete order execution at the time of matching. Therefore, solvers must ensure that the proposed orders are executable with the available resources.
In addition to matching existing orders, solvers can also include another type of order known as a Virtual Order. A virtual order is created dynamically during the matching process and does not persist in the system's state. For example, if a user encounters an order selling BTC for PRYZM and wishes to purchase the BTC using PRYZM, they can utilize the virtual order feature to create a corresponding order (selling PRYZM for BTC) on the fly and propose a match. Similar to a regular order, users can set a max price as well as a max amount for virtual orders. The user will then need to pay the actual matched amount, which will be less than or equal to the specified maximum, to the AMM module.
With these considerations in mind, solvers can submit a list of match proposals by providing the following information for each pair:
message PairMatchProposal {
uint64 pool_id = 1;
bool whitelisted_route = 2;
string token_in = 3;
string token_out = 4;
repeated ProposalOrder buy_orders = 5 [(gogoproto.nullable) = false];
repeated ProposalOrder sell_orders = 6 [(gogoproto.nullable) = false];
}
message ProposalOrder {
uint64 id = 1; // should be set to zero for virtual orders
string max_amount_in = 2 [
(cosmos_proto.scalar) = "cosmos.Int",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.nullable) = true
]; // this must be provided if and only if virtual=true
string max_price = 3 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = true
]; // this must be provided if and only if virtual=true
bool virtual = 4;
}
Buy orders are for purchasing token_out
, and sell orders are the reverse. When a list of matching orders is proposed, the AMM module attempts to offset these orders and fulfill as many as possible at the current pool-provided price. Note that not all orders will be completely executed after a match proposal; there may be some remaining for future execution through step execution or another match proposal.
Solvers receive a portion of the matched amount from both sides of the orders. This ratio is defined in the module parameters as MatchingSolverFeeRatio
.