Skip to main content
Version: Next

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 of Flow 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 the FlowList.
  • PositionList is a list comprising Position objects.