FlowTrade Module
Overview
The FlowTrade module, built on the Cosmos-SDK, facilitates token swaps within a specified timeframe. Users can establish a stream of tokens, distributed over a certain period, to be exchanged for another token in an open market. The tokens can be swapped either continuously or at set intervals during the swap period. The token price is computed based on the total quantity of each token and the time elapsed since the flow's commencement.
Concepts
Flow
A flow is an event where a specific quantity of one token is traded for another. Users can initiate a flow by securing a
deposit, and other users may participate by contributing a certain amount of tokens. The token offered for sale by the
flow creator is termed the token-out
, while the token supplied by the buyers is referred to as the token-in
. A flow
comprises the following information:
- id: Auto-generated identifier for a specific flow.
- creator: Address of the flow's creator/owner.
- flow info: Data pertaining to the flow.
- name: Flow's name.
- description: Brief overview of the flow.
- url: Referring URL.
- start time: Time the swap distribution commences.
- end time: Time the flow concludes.
- dist interval: Interval for updating the distribution index and executing token swaps.
- If dist interval matches the flow duration, all tokens are swapped once after the flow concludes.
- If dist interval is set to 0, the flow updates whenever tokens are added or removed.
- treasury address: Destination address for the swapped tokens post-flow.
- total token-out: Creator's total token provision for swapping.
- token-in denom: Accepted token for purchasing the out tokens.
- token-out claimable after: Time when buyers can claim purchased tokens.
- token-in claimable after: Time when the creator can claim swapped tokens.
- stoppable: Determines if the flow's creator can halt the flow.
- allow immediate token-out claim if stopped: Enables buyers to claim tokens immediately post-flow halt.
- allow immediate token-in claim if stopped: Allows the flow's creator to claim tokens immediately post-flow halt.
- dist index: Global index for tracking applied purchases.
- last dist update: Latest dist index update time.
- token-out balance: Remaining out tokens for sale.
- token-in balance: Current token-in amount for buying token-out.
- spent token-in: Amount of in-tokens already used.
- total shares: Total number of user shares.
- live price: The latest price of token-out in terms of token-in.
- status: Flow's current state.
WAITING
: Flow is pending start.ACTIVE
: Flow is ongoing.ENDED
: Flow has concluded. This is a final state.STOPPED
: Flow has halted. This is a final state.
- creation deposit: Initial deposit from the flow's creator to deter spam. It's returned to the creator when the flow ends. This value is replicated from the module parameters at flow creation.
- token-out fee ratio: Fee ratio from token-out, replicated from the module parameters at flow creation.
- token-in fee ratio: Fee ratio from token-in, replicated from module parameters at flow creation.
- automatic treasury collection: If true, swapped tokens are automatically sent to the treasury address at flow's end. This option is only available when another module creates a flow using the keeper API.
- claimed token-in: Amount of used in-tokens claimed by the flow's creator.
- checked out: Indicates if the flow is checked out, i.e., the creation deposit and any leftover out tokens are returned to the creator.
- limit price: The minimum price for the token-out in terms of token-in; in each swap interval, if the calculated price is less than this limit, the swap doesn't happen in that turn.
- exit window duration: The duration of the exit window before swap interval, in which users can only exit the flow and joining is not permitted. This duration is used to protect joiners from buying the token-out with a higher price when someone joins with a huge amount of token-in.
The flow updates according to the distribution interval, and swaps tokens based on the time passed since the flow's commencement.
if currentTime > flow.EndTime {
// if flow has ended, use its end time instead of current time
updateTime = flow.EndTime
} else if flow.DistInterval == 0 {
// always update if dist interval is zero
updateTime = currentTime
} else {
// find the proper update time based on the interval,
// which is the last time before currentTime which is a multiplier of the distInterval
diff = ((currentTime - lastUpdated) / flow.DistInterval) * flow.DistInterval
updateTime = lastUpdated + diff
}
// calculate the ratio of the passed time from last update to the total remaining time from last update
timeWeight = (updateTime - lastUpdated) / (flow.EndTime - lastUpdated)
// calculate the amount of in/out tokens for the current distribution
outToSwap = flow.TokenOutBalance * timeWeight
inToSwap = flow.TokenInBalance * timeWeight
// calculate the live price (in / out)
flow.LivePrice = inToSwap / outToSwap
if flow.LivePrice < flow.LimitPrice {
return
}
// swap the tokens
flow.TokenOutBalance -= outToSwap
flow.TokenInBalance -= inToSwap
flow.SpentTokenIn += inToSwap
// update the dist index
indexDiff = outToSwap / flow.TotalShares
flow.DistIndex = flow.DistIndex + indexDiff
Position
A position refers to a user's participation status within a flow. It's created for each user who joins a flow and stores the following details:
- Flow: The associated flow
- Owner: The owner's address
- Operator: The address to which the owner has delegated position management
- Dist index: An index indicating the already applied purchase amount
- Token-in balance: The current token-in amount for buying token-out
- Spent token-in: The quantity of tokens already spent
- Shares: This position's shares from the flow
- Purchased token-out: The token-out amount purchased by the user
- Pending purchase: The amount of purchased tokens paid for but not yet included in purchased_token_out due to rounding errors
- Claimed amount: The purchase amount claimed by the user and already transferred to their account
The position updates as the flow changes, and includes calculating purchased token-out and remaining token-in tokens for the remaining sale.
indexDiff = flow.DistIndex - position.DistIndex
position.DistIndex = flow.DistIndex
purchased = (position.Shares * indexDiff) + position.PendingPurchase
position.PurchasedTokenOut += int(purchasedTruncated)
position.PendingPurchase = decimal(purchased)
newInBalance = flow.TokenInBalance * postion.Shares / flow.TotalShares
position.SpentTokenIn += (position.TokenInBalance - newInBalance)
position.TokenInBalance = newInBalance
Joining a flow
To participate in a flow, users provide token-in
amounts. Their distribution interval share depends on when they
provide this amount. When a user increases their flow position balance, the following happens:
flow.UpdateDistIndex()
position.UpdateDistIndex() // if position already exists
if flow.TotalShares == 0 || flow.TokenInBalance == 0 {
newShares = amount
} else {
newShares = flow.TotalShares * amount / flow.TokenInBalance
}
// add share and amount to the position
position.Shares += newShares
position.TokenInBalance += amount.Amount
// add share and amount to the flow
flow.TotalShares += newShares
flow.TokenInBalance += amount
Early Join
Users are able to join a flow from the time it is created; But the swap and distribution of tokens is not started until the start time of the flow. The provided tokens will be locked in the FlowTrade module until the flow is ended or the user withdraws them. This can be useful for users to discover and consider the parameters of the flow and decide the strategy to join the flow.
Exiting a flow
Users may withdraw their remaining un-swapped token-in
to exit a flow. When a user reduces their position balance for
a flow, the following operations are performed:
flow.UpdateDistIndex()
position.UpdateDistIndex()
if position.TokenInBalance == amount {
// withdraw all shares if position's in balance is equal to the amount
reducedShares = position.Shares
} else {
// calculate share, ceil the decimal value
// flow.TokenInBalance is never zero since it's been checked before in this method
reducedShares = ceil(flow.TotalShares * amount / flow.TokenInBalance)
}
// subtract share and amount from the position
position.Shares -= reducedShares
position.TokenInBalance -= amount
// subtract share and amount from the flow
flow.TotalShares -= reducedShares
flow.TokenInBalance -= amount
Transitions
End Block
The EndBlocker
function is invoked at every block to check if any flows have ended based on their prescribed end
times. If a flow has ended, it updates the flow's distribution index one last time, changes its status to "ENDED", and
triggers an event. Moreover, if the automatic treasury collection feature is enabled for the flow, the function collects
the revenue for the treasury account.
Messages
MsgUpdateParams
This message updates the module's parameters.
message MsgUpdateParams {
string authority = 1;
Params params = 2 [(gogoproto.nullable) = false];
}
MsgCreateFlow
This message initiates a new flow and returns the auto-generated flow ID in the response. It also deducts a creation deposit from the creator's account to prevent spamming.
message MsgCreateFlow {
string creator = 1;
FlowCreationRequest request = 2 [(gogoproto.nullable) = false];
}
message MsgCreateFlowResponse {
Flow flow = 1 [(gogoproto.nullable) = false];
}
MsgJoinFlow
This message allows a user to join an existing flow with a specified amount. The user's address and signer must be identical when they join a flow for the first time. However, if the user designates an operator for their position using MsgSetOperator, the signer can be the operator's address.
message MsgJoinFlow {
uint64 flow = 1;
string address = 2;
cosmos.base.v1beta1.Coin amount = 3 [(gogoproto.nullable) = false];
string signer = 4;
}
message MsgJoinFlowResponse {
Position position = 1 [(gogoproto.nullable) = false];
}
MsgExitFlow
This function permits a user to withdraw a particular amount of tokens from a flow. The amount specified cannot surpass the total remaining tokens in the user's position.
message MsgExitFlow {
uint64 flow = 1;
string address = 2;
cosmos.base.v1beta1.Coin amount = 3 [(gogoproto.nullable) = false];
string signer = 4;
}
message MsgExitFlowResponse {
Position position = 1 [(gogoproto.nullable) = false];
}
MsgSetOperator
This message assigns an operator for a position, allowing the operator's address to manage the position by joining or exiting the flow, and claiming tokens.
message MsgSetOperator {
uint64 flow = 1;
string owner = 2;
string operator = 3;
}
MsgClaimTokenIn
Allows the flow creator to claim the flow's revenue and transfer it to a specified treasury address. It also refunds the creation deposit to the creator once the flow has ended. The treasury address can be left blank.
This message returns an error if the token-in-claimable-after
time hasn't passed. The response indicates the
claimed token-in
amount and the deducted fee.
message MsgClaimTokenIn {
string creator = 1;
uint64 flow = 2;
string treasury_address = 3;
}
message MsgClaimTokenInResponse {
cosmos.base.v1beta1.Coin claimed = 1 [(gogoproto.nullable) = false];
cosmos.base.v1beta1.Coin fee = 2 [(gogoproto.nullable) = false];
}
MsgClaimTokenOut
Allows the flow participants to claim their purchased tokens once the token-out-claimable-after
time has passed. The
response includes the claimed token-out
amount and the fee deducted.
message MsgClaimTokenOut {
uint64 flow = 1;
string address = 2;
string signer = 3;
}
message MsgClaimTokenOutResponse {
cosmos.base.v1beta1.Coin claimed = 1 [(gogoproto.nullable) = false];
cosmos.base.v1beta1.Coin fee = 2 [(gogoproto.nullable) = false];
}
MsgStopFlow
Allows the flow creator to stop the flow. It returns an error if the flow's stoppable
attribute is false.
message MsgStopFlow {
uint64 flow_id = 1;
string creator = 2;
}
Queries
Params
Queries the module's parameters.
message QueryParamsRequest {}
message QueryParamsResponse {
Params params = 1 [(gogoproto.nullable) = false];
}
Flow
Fetches the latest state of a specific flow using its ID.
message QueryGetFlowRequest {
uint64 id = 1;
}
message QueryGetFlowResponse {
Flow flow = 1 [(gogoproto.nullable) = false];
}
FlowAll
Fetches all flows.
message QueryAllFlowRequest {
cosmos.base.query.v1beta1.PageRequest pagination = 1;
}
message QueryAllFlowResponse {
repeated Flow flow = 1 [(gogoproto.nullable) = false];
cosmos.base.query.v1beta1.PageResponse pagination = 2;
}
Position
Fetches a user's position in a particular flow.
message QueryGetPositionRequest {
uint64 flow = 1;
string owner = 2;
}
message QueryGetPositionResponse {
Position position = 1 [(gogoproto.nullable) = false];
}
FlowPositions
Fetches a list of positions for a specific flow.
message QueryGetFlowPositionsRequest {
uint64 flow = 1;
cosmos.base.query.v1beta1.PageRequest pagination = 2;
}
message QueryGetFlowPositionsResponse {
repeated Position position = 1 [(gogoproto.nullable) = false];
cosmos.base.query.v1beta1.PageResponse pagination = 2;
}
UserPositions
This function fetches a list of all positions a user has opened.
message QueryGetUserPositionsRequest {
string owner = 1;
cosmos.base.query.v1beta1.PageRequest pagination = 2;
}
message QueryGetUserPositionsResponse {
repeated Position position = 1 [(gogoproto.nullable) = false];
cosmos.base.query.v1beta1.PageResponse pagination = 2;
}
Events
EventSetParams
This event triggers when module parameters get updated. It includes the updated Params
object.
message EventSetParams {
Params params = 1 [(gogoproto.nullable) = false];
}
EventFlowCreated
This event triggers when a new flow is created. It includes the Flow
object, representing the new flow.
message EventFlowCreated {
Flow flow = 1 [(gogoproto.nullable) = false];
}
EventFlowEnded
This event triggers when a flow ends. It includes the ID of the ending flow.
message EventFlowEnded {
uint64 flow_id = 1;
}
EventFlowStopped
This event triggers when a flow stops. It includes the ID of the stopped flow.
message EventFlowStopped {
uint64 flow_id = 1;
}
EventFlowCheckedOut
This event triggers when a flow is checked out. It includes the ID of the checked-out flow, the returned deposit, and the remaining unswapped token-out if the flow stops before the end time.
message EventFlowCheckedOut {
uint64 flow_id = 1;
cosmos.base.v1beta1.Coin returned_deposit = 2;
cosmos.base.v1beta1.Coin returned_token_out = 3;
}
EventFlowTokenInClaimed
This event triggers when the flow creator claims token-in
. It includes the flow ID, the claimed token-in
amount, the
fee, and the treasury address receiving the tokens.
message EventFlowTokenInClaimed {
uint64 flow_id = 1;
cosmos.base.v1beta1.Coin amount = 2 [(gogoproto.nullable) = false];
cosmos.base.v1beta1.Coin fee = 3 [(gogoproto.nullable) = false];
string treasury = 4;
}
EventFlowTokenOutClaimed
This event triggers when a user claims their purchased token-out
. It includes the flow ID, the position owner, the
claimed token-out
amount, and the fee.
message EventFlowTokenOutClaimed {
uint64 flow_id = 1;
string owner = 2;
cosmos.base.v1beta1.Coin amount = 3 [(gogoproto.nullable) = false];
cosmos.base.v1beta1.Coin fee = 4 [(gogoproto.nullable) = false];
}
EventJoinFlow
This event triggers when a user joins a flow or increases their position balance. It includes the flow ID, the user's
address, and the amount of token-in
the user provided.
message EventJoinFlow {
uint64 flow_id = 1;
string address = 2;
cosmos.base.v1beta1.Coin amount = 3 [(gogoproto.nullable) = false];
}
EventExitFlow
EventExitFlow is emitted when a user exits a flow or reduces their position balance. It includes the flow ID, user's
address, and the amount of token-in
withdrawn by the user.
message EventExitFlow {
uint64 flow_id = 1;
string address = 2;
cosmos.base.v1beta1.Coin amount = 3 [(gogoproto.nullable) = false];
}
EventSetOperator
EventSetOperator is fired when an operator is assigned to a position. It provides the flow ID, the position owner's address, and the operator's address.
message EventSetOperator {
uint64 flow_id = 1;
string owner = 2;
string operator = 3;
}
EventSetFlow
EventSetFlow is emitted upon flow update. It contains the updated Flow
object.
message EventSetFlow {
Flow flow = 1 [(gogoproto.nullable) = false];
}
EventSetPosition
EventSetPosition is emitted upon position update. It carries the updated Position
object.
message EventSetPosition {
Position position = 1 [(gogoproto.nullable) = false];
}
State
Params
Module parameters are stored under this store key:
([]byte("v1/params/")) -> ProtoBuf(Params)
Flow
Flows are stored using their auto-generated ID as the key:
(
[]byte("v1/flow/") |
[]byte(id) // as uint64 in 8 bytes
) -> ProtoBuf(Flow)
The flow ID is a uint64
that starts from 1 and increments with each new flow. The count of created flows is stored
like so:
(
[]byte("v1/flow-count/")
) -> []byte(count)
To process active flows at their end time, a mapping of these flows is kept, indexed by their end time:
(
[]byte("v1/active-flow/") |
[]byte(flowEndTime) | // in sortable string format
[]byte(id) // as uint64 in 8 bytes
) -> []byte(id)
Position
Positions are stored using the following structure, using the flow ID and the position owner's address:
(
[]byte("v1/position/") |
[]byte(flowId) | // as uint64 in 8 bytes
[]byte(ownerAddress) // length-prefixed
) -> ProtoBuf(Position)
An index is also maintained for querying positions of an owner with the following structure:
(
[]byte("v1/position-by-owner/") |
[]byte(ownerAddress) // length-prefixed
[]byte(flowId) | // as uint64 in 8 bytes
) -> []byte(positionKey)
Parameters
FlowCreationDeposit
This is the deposit amount taken from the flow creator and returned after the flow ends.
- Type:
cosmos.base.v1beta1.Coin
- Default Value:
1000000stake
MinFlowDuration
This is the shortest duration a flow can have.
- Type:
time.Duration
- Range:
[0, inf)
- Default Value:
1h
MinDurationToFlowStart
This is the minimum duration between the time a flow is created and when it starts.
- Type:
time.Duration
- Range:
[0, inf)
- Default Value:
1h
TokenOutFeeRatio
This parameter represents the protocol's fee ratio deducted from the outgoing token.
- Type:
sdk.Dec
- Range:
[0, 0.9]
- Default Value:
0
TokenInFeeRatio
This parameter denotes the protocol's fee ratio deducted from the incoming token.
- Type:
sdk.Dec
- Range:
[0, 0.9]
- Default Value:
0
Genesis
The genesis state of this module comprises the following properties:
Params
defines the parameters for the flowtrade module.FlowList
is a list consisting ofFlow
objects.FlowCount
signifies the number of inserted flows and is used to create a new flow's ID. It cannot be smaller than the highest ID in theFlowList
.PositionList
is a list comprisingPosition
objects.