DEX standard on FLOW

Abstract

The Flow ecosystem is experiencing continued growth and with this expansion comes a need to expand the DeFi ecosystem on the Flow blockchain. Expansion of DeFi on Flow requires new liquidity - typically through cross-chain bridges - as well as straightforward integration for wallets and developers to provide a simple and intuitive user experience. While the macro topic of liquidity is beyond the scope of this discussion, the initial focus of the nascent standard aims to enable ease of interoperability with, and composability of, DEXs which are a cornerstone concept serving as gateways to any DeFi ecosystem.

To that end we would like to commence the discussion focusing on a pool-based DEX standard on the Flow blockchain. We intentionally defer discussion on order-book based DEXs and how that may relate to the standard for a subsequent iteration.

The Flow blockchain already has several DEXs live on mainnet, namely Increment.Fi and Blocto swap, whose feedback on a future standard will help define what makes sense for the community.

Rationale

Drawing from observations in other ecosystems, it’s typical to leverage wrapper contracts or adapters to achieve interoperability across different DEXs in smart contract protocols, particularly in lending/borrowing protocols. The reasons for this are partly a lack of common standards and the gradual evolution of DeFi, as well as the influence of the address-centric security model in the resulting design thinking. The complexity this creates for developers when integrating DEXs into applications is a key motivating reason to avoid the same fragmentation on Flow from a lack of standards. It is our belief that establishing a minimal standard interface to simplify integration with DEXs provides the best experience for developers and the users, while also offering true composability without the complexity and compromises observed in other DeFi ecosystems.

Requirements and key user scenarios

The following requirements have been identified following initial discussions, high level prototyping, and analysis of existing DeFi protocols within the FLOW ecosystem, such as Increment.Fi and Blocto Swap.

DEX Swap

Requirements -

  • [ ] Alice as a user can easily swap token A from token B.
  • [ ] Alice as a user can perform quick swaps where funds are provided in the form of vault.
  • [ ] Alice as a user can provide minimum expected amount from a swap to safeguard herself from high slippage.
  • [ ] Trades should expire after a certain period of time.

An early draft swap interface version -

pub resource interface ImmediateSwap {

   /// @notice It will Swap the source token for to target token
   ///
   /// If the user wants to swap USDC to FLOW then the
   /// sourceToTargetTokenPath is [Type<FLOW>] and
   /// USDC would be the source token
   ///
   /// Necessary constraints
    /// - For the given source vault balance, Swapped target token amount should be
    ///   greater than or equal to minimumTargetTokenOut, otherwise swap would fail
    /// - If the swap settlement time i.e getCurrentBlock().timestamp is less than or
    ///   equal to the provided expiry then the swap would fail
    /// - Provided `recipient` capability should be valid otherwise the swap would fail
    /// - If the provided path doesn’t exists then the swap would fail.
	pub fun swapExactSourceToTargetTokenUsingPath(
		sourceToTargetTokenPath: Type[],
		sourceVault: @FungibleToken.Vault,
		minimumTargetTokenOut: UFix64,
		expiry: UInt64,
		recipient: Capability<&{FungibleToken.Receiver}>
	)


     /// @notice It will Swap the source token for to target token and           return the target token vault
    ///
    /// If the user wants to swap USDC to FLOW then the
    /// sourceToTargetTokenPath is [Type<FLOW>] and
    /// USDC would be the source token
    ///
    /// Necessary constraints
    /// - For the given source vault balance, Swapped target token amount should be
    ///   greater than or equal to minimumTargetTokenOut, otherwise swap would fail
    /// - If the swap settlement time i.e getCurrentBlock().timestamp is less than or equal to the provided expiry then the swap would fail
    /// - If the provided path doesn’t exists then the swap would fail.

	pub fun swapAndReturnExactSourceToTargetTokenUsingPath(
		sourceToTargetTokenPath: Type[],
		sourceVault: @FungibleToken.Vault,
		minimumTargetTokenOut: UFix64,
		expiry: UInt64
	): @FungibleToken.Vault



   /// @notice It will Swap the source token for to target token
   ///
   /// Necessary constraints
    /// - For the given source vault balance, Swapped target token amount should be
    ///   greater than or equal to minimumTargetTokenOut, otherwise swap would fail
    /// - If the swap settlement time i.e getCurrentBlock().timestamp is less than or
    ///   equal to the provided expiry then the swap would fail
    /// - Provided `recipient` capability should be valid otherwise the swap would fail
    /// - If the provided path doesn’t exists then the swap would fail. 
    pub fun swapExactSourceToTargetToken(
		targetToken: Type,
		sourceVault: @FungibleToken.Vault,
		minimumTargetTokenOut: UFix64,
		expiry: UInt64,
		recipient: Capability<&{FungibleToken.Receiver}>
	)
     
     /// @notice It will Swap the source token for to target token and           return the target token vault
    ///
    /// Necessary constraints
    /// - For the given source vault balance, Swapped target token amount should be
    ///   greater than or equal to minimumTargetTokenOut, otherwise swap would fail
    /// - If the swap settlement time i.e getCurrentBlock().timestamp is less than or equal to the provided expiry then the swap would fail
    /// - If the provided path doesn’t exists then the swap would fail.
    pub fun swapAndReturnExactSourceToTargetToken(
		targetToken: Type,
		sourceVault: @FungibleToken.Vault,
		minimumTargetTokenOut: UFix64,
		expiry: UInt64
	): @FungibleToken.Vault
}

Fetch Token Prices

Requirements -

  • [ ] Alice as a user needs to know how much amount of target token she gets if she provide x amount of source token.
  • [ ] Alice as a user needs to know how much amount of source token she needs to get x amount of target token.
  • [ ] For the DEX developers it is very important to have optimal routing to provide best quotation for its users, It is not possible to do so without providing the optimal path that gets computed off-chain because of various factors like liquidity is scattered and not concentrated to different pool, direct liquidity pair doesn’t exists etc.
    1. Challenges - It is not possible to compute the optimal route onchain efficiently.

Assumptions -

  1. End user doesn’t care about the fees, As it only needs to know how much it get if it provides x amount of source token.
  2. Calculated quotation is valid only at the time of calculation, There is no guarantee that user would get the same result after submission of the same values because there are chances of having change in pool values after the quotation calculation.

Initial DEX standard proposal draft -

pub resource interface ImmediateSwapQuotation {
	
/// @notice Provides the quotation of the target token amount for the
/// corresponding provided sell amount i.e amount of source tokens.
///
/// If the source to target token path doesn't exists then below function
/// would return `nil`.
/// Below function would return the quoted amount after deduction of the fees.
///
/// If the sourceToTargetTokenPath is [Type<FLOW>, Type<BLOCTO>]. 
/// Where sourceToTargetTokenPath[0] is the source token while 
/// sourceToTargetTokenPath[sourceToTargetTokenPath.length -1] is 
/// target token. i.e. FLOW and BLOCTO respectively.
///
/// @param sourceToTargetTokenPath Offchain computed optimal path from
/// source token to target token.
/// @param sellAmount Amount of source token user wants to sell to buy target token.
/// @return Amount of target token user would get after selling `sellAmount`.
///
	pub fun getExactSellQuoteUsingPath(
	   sourceToTargetTokenPath: Type[],
	   sellAmount: UFix64
      ): UFix64?


/// @notice Provides the quotation of the source token amount if user wants to
/// buy provided buyAmount, i.e. amount of target token.
///
/// If the source to target token path doesn't exists then below function
/// would return `nil`.
/// Below function would return the quoted amount after deduction of the fees.
///
/// If the sourceToTargetTokenPath is [Type<FLOW>, Type<BLOCTO>]. 
/// Where sourceToTargetTokenPath[0] is the source token while 
/// sourceToTargetTokenPath[sourceToTargetTokenPath.length -1] is 
/// target token. i.e. FLOW and BLOCTO respectively.
///
/// @param sourceToTargetTokenPath Offchain computed optimal path from
/// source token to target token.
/// @param buyAmount Amount of target token user wants to buy.
/// @return Amount of source token user has to pay to buy provided `buyAmount` of target token.
///
       pub run getExactBuyQuoteUsingPath(
		sourceToTargetTokenPath: Type[],
	      buyAmount: UFix64
       ): UFix64?


/// @notice It provides the quotation for selling the source token i.e. `sellAmount` for the corresponding target token.
///
/// If the source to target token path doesn't exists then below function
/// would return `nil`.
/// Below function would return the quoted amount after deduction of the fees.
///
/// @param sourceToken Type of token that user wants to sell.
/// @param targetToken Type of token that user would get in return after selling source token.
/// @param sellAmount Amount of source token user is willing to sell.
/// @return Returns the amount of target token user would get after selling provided source token i.e. `sellAmount`
///
       pub fun getExactSellQuote(
	     sourceToken: Type,
	     targetToken: Type,
	     sellAmount: UFix64
       ): UFix64?


/// @notice It provides the quotation for buying the target token i.e. `buyAmount` for the /// corresponding source token.
///
/// If the source to target token path doesn't exists then below function
/// would return `nil`.
/// Below function would return the quoted amount after deduction of the fees.
///
/// @param sourceToken Type of token that user wants to provide to buy ///target token.
/// @param targetToken Type of token that user wants to buy.
/// @param buyAmount Amount of target token user is willing to buy.
/// @return Returns the amount of source token user would have to pay to /// buy provided target token amount i.e. buyAmount.
///
      pub fun getExactBuyQuote(
	    sourceToken: Type,
	    targetToken: Type,
	    buyAmount: UFix64
      ): UFix64?

Community input feedback is requested and appreciated!

As always, the above draft proposals are presented to stimulate discussion and debate and are likely some ways from being finalized. We invite any interested community members to share any feedback, thoughts, questions or concerns in the thread. Together with your help we’re excited for these discussions to shake out the remaining details and enable the a FLIP to be proposed!

4 Likes

In the ImmediateSwap interface:

  • Suggest to change the type of expiry parameter to UFix64, as onchain timestamp getCurrentBlock().timestamp → UFix64

  • Suggest to change function name to be more meaningful:
    i) swapAndReturnExactSourceToTargetTokenUsingPath → swapExactSourceToTargetTokenUsingPathAndReturn;
    ii) swapAndReturnExactSourceToTargetToken → swapExactSourceToTargetTokenAndReturn

  • To add symmetric (and useful) functions:

/*
    sourceVault should have enough balance to perform pathed-swap and generate at least exactTargetAmount of target token, otherwise reverts.
    @recipient - [Receiver capability of remaining sourceVault, Receiver capability of targetVault]
*/
pub fun swapSourceToExactTargetTokenUsingPath(
       sourceToTargetTokenPath: Type[],
       sourceVault: @FungibleToken.Vault,
       exactTargetAmount: UFix64,
       expiry: UFix64,
       recipient: [Capability<&{FungibleToken.Receiver}>; 2]
)

/*
    @return - [remaining sourceVault, targetVault]
*/
pub fun swapSourceToExactTargetTokenUsingPathAndReturn(
    sourceToTargetTokenPath: Type[],
    sourceVault: @FungibleToken.Vault,
    exactTargetAmount: UFix64,
    expiry: UFix64
): @[FungibleToken.Vault; 2]

pub fun swapSourceToExactTargetToken(
  sourceVault: @FungibleToken.Vault,
  targetToken: Type,
  exactTargetOut: UFix64,
  expiry: UFix64,
  recipient: [Capability<&{FungibleToken.Receiver}>; 2]

)

pub fun swapSourceToExactTargetTokenAndReturn(
  sourceVault: @FungibleToken.Vault,
  targetToken: Type,
  exactTargetOut: UFix64,
  expiry: UFix64
): @[FungibleToken.Vault; 2]

BTW as current interface actually duplicates, should we just leave xxxAndReturn() functions and get rid of capability in the function argument?

  • To confirm the behavior of swapExactSourceToTargetToken function: since no path is given, so it’ll go through as long as the requirement of minimumTargetTokenOut is met, this is expected right?
1 Like

In the ImmediateSwapQuotation interface:

It’s not obvious with buyAmount and sellAmount - who is buying/selling for what? Given that there’re already paramaters sourceToTargetTokenPath / sourceToken / targetToken, I suggest to change the name to sourceAmount and/or targetAmount.

2 Likes

Thanks @dryrunner for the feedback,

  • Suggest to change the type of expiry parameter to UFix64, as onchain timestamp getCurrentBlock().timestamp → UFix64

I completely agree with this.

  • Suggest to change function name to be more meaningful:
    i) swapAndReturnExactSourceToTargetTokenUsingPath → swapExactSourceToTargetTokenUsingPathAndReturn;
    ii) swapAndReturnExactSourceToTargetToken → swapExactSourceToTargetTokenAndReturn

This also makes sense to me.

/*
    sourceVault should have enough balance to perform pathed-swap and generate at least exactTargetAmount of target token, otherwise reverts.
    @recipient - [Receiver capability of remaining sourceVault, Receiver capability of targetVault]
*/
pub fun swapSourceToExactTargetTokenUsingPath(
       sourceToTargetTokenPath: Type[],
       sourceVault: @FungibleToken.Vault,
       exactTargetAmount: UFix64,
       expiry: UFix64,
       recipient: [Capability<&{FungibleToken.Receiver}>; 2]
)

For the above, I think it would be better to return a vault for the source token while use capability for the recipient.

/*
    @return - [remaining sourceVault, targetVault]
*/
pub fun swapSourceToExactTargetTokenUsingPathAndReturn(
    sourceToTargetTokenPath: Type[],
    sourceVault: @FungibleToken.Vault,
    exactTargetAmount: UFix64,
    expiry: UFix64
): @[FungibleToken.Vault; 2]

For this I agree

BTW as current interface actually duplicates, should we just leave xxxAndReturn() functions and get rid of capability in the function argument?

I kinda agree on this as well, To make the interface light weight.

  • To confirm the behavior of swapExactSourceToTargetToken function: since no path is given, so it’ll go through as long as the requirement of minimumTargetTokenOut is met, this is expected right?

Yes

2 Likes

Exciting to support the standard and more advanced DeFi users.

That being said, there are several considerations:

  • ImmediateSwap needs a better documentation. If it wants to be a standard, it should at least specify the definition of each function parameter.

  • The definition of path is not consistent across the two interfaces. If we want to convert USDC to FLOW, the path is currently [Type<FLOW>] in ImmediateSwap and [Type<USDC>, Type<FLOW>] in ImmediateSwapQuotation . They should be consistent.

  • It lacks type safety for the involved tokens. Ensuring the input and output tokens’ types should be part of the constraints.
    *Will there be some standard for error reporting & debugging?

  • Given the target users are advanced dex users, some functions may be redundant. Specifically, these are not needed:

    • swapExactSourceToTargetToken
    • swapAndReturnExactSourceToTargetToken
    • getExactSellQuote
    • getExactBuyQuote

Thanks for taking the time and providing your valuable feedback.

  • ImmediateSwap needs a better documentation. If it wants to be a standard, it should at least specify the definition of each function parameter.
  • The definition of path is not consistent across the two interfaces. If we want to convert USDC to FLOW, the path is currently [Type<FLOW>] in ImmediateSwap and [Type<USDC>, Type<FLOW>] in ImmediateSwapQuotation . They should be consistent.

Thanks for pointing out, Will fix those things in the FLIP.

It lacks type safety for the involved tokens. Ensuring the input and output tokens’ types should be part of the constraints.

Can you please broaden this, What kind of attack scenarios or issues may arise with the proposed interface. Just want a little bit context.

Will there be some standard for error reporting & debugging?

Not for now but if you have something in mind then happy to talk about that stuff as well.

Given the target users are advanced dex users, some functions may be redundant. Specifically, these are not needed:

Yeah I kind of agree but the intention was to keep the simpler interface as well to make it easier for the early adoptoers like wallet integrators or any third party integrators. but if all the community inclined to remove those API then I am happy to do so.

1 Like

Here is the proposed FLIP for pool-based Dex on FLOW https://github.com/onflow/flips/pull/112

Please have a look and provide as much feedback as possible. We intend to develop in an open environment, If anyone wants me to help to create an example implementation for dex and documentation please join the telegram channel to discuss the deFi-related stuff and coordinate for more work related to dex standard adoption

2 Likes

Hello Community, It seems we are pretty low on energy for DEX standard for a while so I want to revisit again with you guys to get more feedback on the FLIP if possible. I am happy to organise a meeting to discuss this synchronously.

Here is the testnet deployed version of the contract based on the above specification - Flow View Source

Please have a look and let me know if anything doesn’t make sense.

1 Like