Increment
Search
⌃K

Smart Contract API

Create a Pair

createPair

/// SwapFactory.cdc
​
pub fun createPair(
token0Vault: @FungibleToken.Vault,
token1Vault: @FungibleToken.Vault,
accountCreationFee: @FungibleToken.Vault
): Address
  • Example transaction create_pair_usdc_fusd.cdc
import FiatToken from 0xb19436aae4d94622
import FUSD from 0x3c5959b568896393
​
/// Pair creator needs to pay the deployment fee of 0.001 Flow.
import FlowToken from 0x1654653399040a61
import SwapFactory from 0xb063c16cac85dbd1
​
/// Deploy a trading pair for USDC <-> FUSD if it doesn't exist; otherwise do nothing.
transaction() {
prepare(deployer: AuthAccount) {
let token0Vault <- FiatToken.createEmptyVault()
let token1Vault <- FUSD.createEmptyVault()
​
/// 'A.0xADDRESS.TokenName.Vault'
var token0Key = token0Vault.getType().identifier
/// Get token0 identifier
token0Key = token0Key.slice(from: 0, upTo: token0Key.length - 6)
var token1Key = token1Vault.getType().identifier
token1Key = token1Key.slice(from: 0, upTo: token1Key.length - 6)
/// Check whether pair has already existed or not.
let pairAddress = SwapFactory.getPairAddress(token0Key: token0Key, token1Key: token1Key)
if (pairAddress == nil) {
let flowVaultRef = deployer.borrow<&FlowToken.Vault>(from: /storage/flowTokenVault)!
assert(flowVaultRef.balance >= 0.002, message: "Insufficient balance to create pair, minimum balance requirement: 0.002 flow")
let fee <- flowVaultRef.withdraw(amount: 0.001)
SwapFactory.createPair(token0Vault: <-token0Vault, token1Vault: <-token1Vault, accountCreationFee: <-fee)
} else {
/// Pair already exists
destroy token0Vault
destroy token1Vault
}
}
}

Get Pair & LpToken Info

Get Pairs' Addresses

/// SwapFactory.cdc
​
pub fun getPairAddress(token0Key: String, token1Key: String): Address?
pub fun getAllPairsLength(): Int
pub fun getSlicedPairs(from: UInt64, to: UInt64): [Address]
Param
Type
Comments
from
UInt64
Start Index
to
UInt64
End Index
Return Value
[Address]
An array of deployed trading pair addresses
  • Get number of all deployed trading pairs and an array of deployed trading pair addresses.
  • Example script get_all_pair_addresses.cdc:
import SwapFactory from 0xb063c16cac85dbd1
​
pub fun main(): [Address] {
let len = SwapFactory.getAllPairsLength()
if (len == 0) {
return []
} else {
return SwapFactory.getSlicedPairs(from: 0, to: UInt64.max)
}
}

getPairInfo

/// SwapFactory.cdc
​
pub fun getPairInfo(token0Key: String, token1Key: String): AnyStruct?
pub fun getSlicedPairInfos(from: UInt64, to: UInt64): [AnyStruct]
Param
Type
Comments
token0Key
String
token0's unique identifier, e.g.: A.3c5959b568896393.FUSD
token1Key
String
token1's unique identifier, e.g.: A.b19436aae4d94622.FiatToken
Return Value
AnyStruct?
nil if the pair doesn't exist, otherwise returns detailed pair info, e.g.:
[
token0Key,
token1Key,
token0Balance,
token1Balance,
pairAddress,
lpTokenBalance
]
/// SwapPair.cdc
​
pub resource PairPublic: SwapInterfaces.PairPublic {
pub fun getPairInfo(): [AnyStruct] {}
}
  • Example script to get PairInfo given the pair address:
import SwapInterfaces from 0xb78ef7afa52ff906
import SwapConfig from 0xb78ef7afa52ff906
pub fun main(pairAddr: Address): [AnyStruct] {
let pairPublicRef = getAccount(pairAddr)
.getCapability<&{SwapInterfaces.PairPublic}>(SwapConfig.PairPublicPath)
.borrow()
?? panic("cannot borrow reference to PairPublic resource")
return pairPublicRef.getPairInfo()
}
  • Example script `get_all_pair_infos.cdc`:
import SwapFactory from 0xb063c16cac85dbd1
​
pub fun main(): [AnyStruct] {
let len = SwapFactory.getAllPairsLength()
if (len == 0) {
return []
} else {
return SwapFactory.getSlicedPairInfos(from: 0, to: UInt64.max)
}
}

LpTokenCollection

Liquidity Provider (LP) of a trading pair will receive corresponding FT (i.e. LpToken) representing LP's pro rata share of that pair. LP may provide liquidity to different trading pairs and receive multiple different LpTokens. All these LpTokens are grouped within the LpTokenCollection resource, with methods exposing LpToken details:
/// SwapFactory.cdc
​
pub resource LpTokenCollection: SwapInterfaces.LpTokenCollectionPublic {
​
pub fun getAllLPTokens(): [Address] {}
pub fun getLpTokenBalance(pairAddr: Address): UFix64 {}
pub fun deposit(pairAddr: Address, lpTokenVault: @FungibleToken.Vault) {}
pub fun withdraw(pairAddr: Address, amount: UFix64): @FungibleToken.Vault {}
}
  • Example script to check all the pairs LP has provided non-zero liquidity to, along with pair's LpToken balance:
import SwapConfig from 0xb78ef7afa52ff906
import SwapInterfaces from 0xb78ef7afa52ff906
import SwapFactory from 0xb063c16cac85dbd1
​
/// Return { pairAddress: pairLpTokenBalance }
pub fun main(lpAddr: Address): {Address: UFix64} {
var lpTokenCollectionPublicPath = SwapConfig.LpTokenCollectionPublicPath
let lpTokenCollectionCap = getAccount(lpAddr).getCapability<&{SwapInterfaces.LpTokenCollectionPublic}>(lpTokenCollectionPublicPath)
if lpTokenCollectionCap.check() == false {
// No meaningful liquidity found
return {}
}
let lpTokenCollectionRef = lpTokenCollectionCap.borrow()!
let pairs = lpTokenCollectionRef.getAllLPTokens()
let lpBalances: {Address: UFix64} = {}
for pair in pairs {
var lpTokenAmount = lpTokenCollectionRef.getLpTokenBalance(pairAddr: pair)
lpBalances[pair] = lpTokenAmount
}
return lpBalances
}

Swap

The most widely used way to contruct a Swap transaction is to use the SwapRouter, which provides a set of useful methods:

Calculate output / input amount

getAmountsOut

/// SwapRouter.cdc
​
pub fun getAmountsOut(
amountIn: UFix64,
tokenKeyPath: [String]
): [UFix64]
Param
Type
Comments
amountIn
UFix64
Input token amount for the FT tokenKeyPath[0], e.g. 50.0
tokenKeyPath
[String]
An array of FT identifiers denoting the chained-swap path, e.g.: [A.3c5959b568896393.FUSD, A.1654653399040a61.FlowToken, A.b19436aae4d94622.FiatToken] => denoting the swap path of [FUSD -> Flow -> USDC].
​
tokenKeyPath.length must be >= 2, pools for each consecutive pair of FTs must exist and have liquidity.
Return Vaule
[UFix64]
Calculated maximum output FT amounts following the given swap path, e.g. [50.0, 10.0, 48.0]
  • Given the input amount of a FT, with an array of FT identifiers denoting the chained-swap path, calculates all subsequent maximum output token amounts.
  • Useful for calculating output token amounts before calling Perform chained-swap​

getAmountsIn

/// SwapRouter.cdc
​
pub fun getAmountsIn(
amountOut: UFix64,
tokenKeyPath: [String]
): [UFix64]
Param
Type
Comments
amountOut
UFix64
Expected output amount of the FT to receive, e.g. 48.0
tokenKeyPath
[String]
An array of FT identifiers denoting the chained-swap path, e.g.: [A.3c5959b568896393.FUSD, A.1654653399040a61.FlowToken, A.b19436aae4d94622.FiatToken] => denoting the swap path of [FUSD -> Flow -> USDC].
​
tokenKeyPath.length must be >= 2, pools for each consecutive pair of FTs must exist and have liquidity.
Return Value
[UFix64]
Calculated minimum input FT amounts following the given swap path, e.g. [50.0, 10.0, 48.0]
  • Given the expected output amount of a FT, with an array of FT identifiers denoting the chained-swap path, calculates the minimum input FT amounts required to buy the given amountOut.
  • Useful for calculating input token amounts before calling Perform chained-swap​

Perform chained-swap

swapExactTokensForTokens

/// SwapRouter.cdc
​
pub fun swapExactTokensForTokens(
exactVaultIn: @FungibleToken.Vault,
amountOutMin: UFix64,
tokenKeyPath: [String],
deadline: UFix64
): @FungibleToken.Vault
Param
Type
Comments
exactVaultIn
@FT.Vault
Input FT to sell, its full balance will be used.
amountOutMin
UFix64
The minimum amount of output token that must be received, otherwise the tx will revert.
tokenKeyPath
[String]
An array of FT identifiers denoting the chained-swap path, e.g.: [A.3c5959b568896393.FUSD, A.1654653399040a61.FlowToken, A.b19436aae4d94622.FiatToken] => denoting the swap path of [FUSD -> Flow -> USDC].
​
tokenKeyPath.length must be >= 2, pools for each consecutive pair of FTs must exist and have liquidity.
deadline
UFix64
Unix timestamp after which the tx will revert.
Return Value
@FT.Vault
Output FT resource
  • To receive as many output FT as possible for swapping the exact amount of input FT, by following the given swap path.
  • Example transaction swap_exact_flow_to_usdc.cdc:
import FungibleToken from 0xf233dcee88fe0abe
import SwapRouter from 0xa6850776a94e6551
​
transaction(
exactAmountIn: UFix64,
amountOutMin: UFix64,
path: [String],
to: Address,
deadline: UFix64
) {
prepare(userAccount: AuthAccount) {
let tokenInVaultPath = /storage/flowTokenVault
let tokenOutReceiverPath = /public/USDCVaultReceiver
​
let inVaultRef = userAccount.borrow<&FungibleToken.Vault>(from: tokenInVaultPath)
?? panic("Could not borrow reference to the owner's in FT.Vault")
/// Note: Receiver (to) should already have out FT.Vault initialized, otherwise tx reverts.
let outReceiverRef = getAccount(to).getCapability(tokenOutReceiverPath)
.borrow<&{FungibleToken.Receiver}>()
?? panic("Could not borrow receiver reference to the recipient's out FT.Vault")
​
let exactVaultIn <- inVaultRef.withdraw(amount: exactAmountIn)
let vaultOut <- SwapRouter.swapExactTokensForTokens(
exactVaultIn: <-exactVaultIn,
amountOutMin: amountOutMin,
tokenKeyPath: path,
deadline: deadline
)
outReceiverRef.deposit(from: <-vaultOut)
}
}

swapTokensForExactTokens

/// SwapRouter.cdc
​
pub fun swapTokensForExactTokens(
vaultInMax: @FungibleToken.Vault,
exactAmountOut: UFix64,
tokenKeyPath: [String],
deadline: UFix64
): @[FungibleToken.Vault] {
Param
Type
Comments
vaultInMax
@FT.Vault
Input FT to sell, whose balance is the maximum amount can be used before the tx reverts. Any remaining input token will be put in returnValue[1].
exactAmountOut
UFix64
The exact amount of output FT expected to receive.
tokenKeyPath
[String]
An array of FT identifiers denoting the chained-swap path, e.g.: [A.3c5959b568896393.FUSD, A.1654653399040a61.FlowToken, A.b19436aae4d94622.FiatToken] => denoting the swap path of [FUSD -> Flow -> USDC].
​
tokenKeyPath.length must be >= 2, pools for each consecutive pair of FTs must exist and have liquidity.
deadline
UFix64
Unix timestamp after which the tx will revert.
Return Value
@[FT.Vault]
2-element array of FT resources. * returnValue[0]: output token resource, whose balance will be exactAmountOut. * returnValue[1]: Any remaining input token resource.
  • To receive the exact amount of output FT for swapping as few input FT as possible, by following the given swap path.
  • Example transaction swap_flow_to_exact_usdc.cdc:
import FungibleToken from 0xf233dcee88fe0abe
import SwapRouter from 0xa6850776a94e6551
​
transaction(
amountInMax: UFix64,
exactAmountOut: UFix64,
path: [String],
to: Address,
deadline: UFix64
) {
prepare(userAccount: AuthAccount) {
let tokenInVaultPath = /storage/flowTokenVault
let tokenOutReceiverPath = /public/USDCVaultReceiver
let inVaultRef = userAccount.borrow<&FungibleToken.Vault>(from: tokenInVaultPath)
?? panic("Could not borrow reference to the owner's in FT.Vault")
/// Note: Receiver (to) should already have out FT.Vault initialized, otherwise tx reverts.
let outReceiverRef = getAccount(to).getCapability(tokenOutReceiverPath)
.borrow<&{FungibleToken.Receiver}>()
?? panic("Could not borrow receiver reference to the recipient's out FT.Vault")
let vaultInMax <- inVaultRef.withdraw(amount: amountInMax)
let swapResVault <- SwapRouter.swapTokensForExactTokens(
vaultInMax: <-vaultInMax,
exactAmountOut: exactAmountOut,
tokenKeyPath: path,
deadline: deadline
)
let vaultOut <- swapResVault.removeFirst()
let vaultInLeft <- swapResVault.removeLast()
destroy swapResVault
outReceiverRef.deposit(from: <-vaultOut)
/// Deposit any remaining input FT back
inVaultRef.deposit(from: <-vaultInLeft)
}
}
  • Note: Using flow-cli tool in the commandline environment, or fcl-js library as long with tx.cdc / script.cdc to interact with smart contracts directly.

Add & Remove Liquidity

AddLiquidity

/// SwapPair.cdc
​
pub fun addLiquidity(
tokenAVault: @FungibleToken.Vault,
tokenBVault: @FungibleToken.Vault
): @FungibleToken.Vault
  • It's not recommended to use the low-level addLiquidity method directly, unless you're the 1st LP to set the initial price.
  • Use below example transaction to do slippage check and add liquidity at the ideal ratio:
import FungibleToken from 0xf233dcee88fe0abe
import SwapFactory from 0xb063c16cac85dbd1
import SwapInterfaces from 0xb78ef7afa52ff906
import SwapConfig from 0xb78ef7afa52ff906
​
transaction(
token0Key: String,
token1Key: String,
token0InDesired: UFix64,
token1InDesired: UFix64,
token0InMin: UFix64,
token1InMin: UFix64,
token0VaultPath: StoragePath,
token1VaultPath: StoragePath,
) {
prepare(lp: AuthAccount) {
let pairAddr = SwapFactory.getPairAddress(token0Key: token0Key, token1Key: token1Key)
?? panic("AddLiquidity: nonexistent pair ".concat(token0Key).concat(" <-> ").concat(token1Key).concat(", create pair first"))
let pairPublicRef = getAccount(pairAddr).getCapability<&{SwapInterfaces.PairPublic}>(SwapConfig.PairPublicPath).borrow()!
/*
pairInfo = [
SwapPair.token0Key,
SwapPair.token1Key,
SwapPair.token0Vault.balance,
SwapPair.token1Vault.balance,
SwapPair.account.address,
SwapPair.totalSupply
]
*/
let pairInfo = pairPublicRef.getPairInfo()
var token0In = 0.0
var token1In = 0.0
var token0Reserve = 0.0
var token1Reserve = 0.0
if token0Key == (pairInfo[0] as! String) {
token0Reserve = (pairInfo[2] as! UFix64)
token1Reserve = (pairInfo[3] as! UFix64)
} else {
token0Reserve = (pairInfo[3] as! UFix64)
token1Reserve = (pairInfo[2] as! UFix64)
}
if token0Reserve == 0.0 && token1Reserve == 0.0 {
token0In = token0InDesired
token1In = token1InDesired
} else {
var amount1Optimal = SwapConfig.quote(amountA: token0InDesired, reserveA: token0Reserve, reserveB: token1Reserve)
if (amount1Optimal <= token1InDesired) {
assert(amount1Optimal >= token1InMin, message: "SLIPPAGE_OFFSET_TOO_LARGE expect min".concat(token1InMin.toString()).concat(" got ").concat(amount1Optimal.toString()))
token0In = token0InDesired
token1In = amount1Optimal
} else {
var amount0Optimal = SwapConfig.quote(amountA: token1InDesired, reserveA: token1Reserve, reserveB: token0Reserve)
assert(amount0Optimal <= token0InDesired)
assert(amount0Optimal >= token0InMin, message: "SLIPPAGE_OFFSET_TOO_LARGE expect min".concat(token0InMin.toString()).concat(" got ").concat(amount0Optimal.toString()))
token0In = amount0Optimal
token1In = token1InDesired
}
}
let token0Vault <- lp.borrow<&FungibleToken.Vault>(from: token0VaultPath)!.withdraw(amount: token0In)
let token1Vault <- lp.borrow<&FungibleToken.Vault>(from: token1VaultPath)!.withdraw(amount: token1In)
/// SwapPair.addLiquidity()
let lpTokenVault <- pairPublicRef.addLiquidity(
tokenAVault: <- token0Vault,
tokenBVault: <- token1Vault
)
let lpTokenCollectionStoragePath = SwapConfig.LpTokenCollectionStoragePath
let lpTokenCollectionPublicPath = SwapConfig.LpTokenCollectionPublicPath
var lpTokenCollectionRef = lp.borrow<&SwapFactory.LpTokenCollection>(from: lpTokenCollectionStoragePath)
/// Initialize LpTokenCollection resource for lp if necessary.
if lpTokenCollectionRef == nil {
destroy <- lp.load<@AnyResource>(from: lpTokenCollectionStoragePath)
lp.save(<-SwapFactory.createEmptyLpTokenCollection(), to: lpTokenCollectionStoragePath)
lp.link<&{SwapInterfaces.LpTokenCollectionPublic}>(lpTokenCollectionPublicPath, target: lpTokenCollectionStoragePath)
lpTokenCollectionRef = lp.borrow<&SwapFactory.LpTokenCollection>(from: lpTokenCollectionStoragePath)
}
lpTokenCollectionRef!.deposit(pairAddr: pairAddr, lpTokenVault: <- lpTokenVault)
}
}

RemoveLiquidity

/// SwapPair.cdc
​
pub fun removeLiquidity(
lpTokenVault: @FungibleToken.Vault
) : @[FungibleToken.Vault] {
  • Example transaction below to burn lpToken and deposit back removed two-sided liquidities:
import FungibleToken from 0xf233dcee88fe0abe
import SwapFactory from 0xb063c16cac85dbd1
import SwapInterfaces from 0xb78ef7afa52ff906
import SwapConfig from 0xb78ef7afa52ff906
​
transaction(
lpTokenAmount: UFix64,
token0Key: String,
token1Key: String,
token0VaultPath: StoragePath,
token1VaultPath: StoragePath
) {
prepare(lp: AuthAccount) {
let pairAddr = SwapFactory.getPairAddress(token0Key: token0Key, token1Key: token1Key)
?? panic("RemoveLiquidity: nonexistent pair ".concat(token0Key).concat(" <-> ").concat(token1Key).concat(", create pair first"))
let lpTokenCollectionRef = lp.borrow<&SwapFactory.LpTokenCollection>(from: SwapConfig.LpTokenCollectionStoragePath)
?? panic("RemoveLiquidity: cannot borrow reference to LpTokenCollection")
​
let lpTokenRemove <- lpTokenCollectionRef.withdraw(pairAddr: pairAddr, amount: lpTokenAmount)
let tokens <- getAccount(pairAddr).getCapability<&{SwapInterfaces.PairPublic}>(SwapConfig.PairPublicPath).borrow()!.removeLiquidity(lpTokenVault: <-lpTokenRemove)
let token0Vault <- tokens[0].withdraw(amount: tokens[0].balance)
let token1Vault <- tokens[1].withdraw(amount: tokens[1].balance)
destroy tokens
​
let lpToken0Vault = lp.borrow<&FungibleToken.Vault>(from: token0VaultPath)
?? panic("RemoveLiquidity: cannot borrow reference to token0Vault from lp account")
let lpToken1Vault = lp.borrow<&FungibleToken.Vault>(from: token1VaultPath)
?? panic("RemoveLiquidity: cannot borrow reference to token1Vault from lp account")
if token0Vault.isInstance(lpToken0Vault.getType()) {
lpToken0Vault.deposit(from: <-token0Vault)
lpToken1Vault.deposit(from: <-token1Vault)
} else {
lpToken0Vault.deposit(from: <-token1Vault)
lpToken1Vault.deposit(from: <-token0Vault)
}
}
}

Build a TWAP Oracle

  • DEX-based TWAP (time-weighted-average-price) oracles can be built using the last cumulative prices recorded in each SwapPair.
  • However, to correctly use & integrate the twap-oracle into your projects, you must ensure the sampling of the cumulative price data are kept up to date. As long as your oracle is up to date, you can depend on it to produce average prices.
Check below 2 examples for the sampling (update()) and twap-data consuming (twap()).

Fixed-window TWAP oracle example:

import StableSwapFactory from 0xStableSwapFactoryAddr
import SwapFactory from 0xSwapFactoryAddr
import SwapInterfaces from 0xSwapInterfacesAddr
import SwapConfig from 0xSwapConfigAddr
​
/// Fixed window oracle
///
/// Calculate the average price for the entire period based on the on-chain dex swap pair.
/// note that the price average is only guaranteed to be over at least 1 period, but may be over a longer period
///
pub contract FixedWindowOracleExample {
/// Window period of the average in seconds
pub let PERIOD: UInt64
/// A.contractAddr.contractName: A.11111111.FlowToken, A.2222222.FUSD
pub let token0Key: String
pub let token1Key: String
pub let isStableswap: Bool
/// pair address in dex
pub let pairAddr: Address
​
/// Average price for each PERIOD, updated once per PERIOD (updating interval could be longer than 1 PERIOD)
pub var price0Average: UFix64
pub var price1Average: UFix64
​
/// Cumulative price/timestamp for the last update
pub var price0CumulativeLastScaled: UInt256
pub var price1CumulativeLastScaled: UInt256
pub var blockTimestampLast: UFix64
​
​
/// Sampling: update the accumulated price if it exceeds the period.
pub fun update() {
let now = getCurrentBlock().timestamp
let timeElapsed = now - self.blockTimestampLast
assert(timeElapsed >= UFix64(self.PERIOD), message: "PERIOD_NOT_ELAPSED ".concat(timeElapsed.toString().concat("s")))
​
let res = SwapConfig.getCurrentCumulativePrices(pairAddr: self.pairAddr)
let currentPrice0CumulativeScaled = res[0]
let currentPrice1CumulativeScaled = res[1]
let timeElapsedScaled = SwapConfig.UFix64ToScaledUInt256(timeElapsed)
let price0AverageScaled = SwapConfig.underflowSubtractUInt256(currentPrice0CumulativeScaled, self.price0CumulativeLastScaled) * SwapConfig.scaleFactor / timeElapsedScaled
let price1AverageScaled = SwapConfig.underflowSubtractUInt256(currentPrice1CumulativeScaled, self.price1CumulativeLastScaled) * SwapConfig.scaleFactor / timeElapsedScaled
​
self.price0Average = SwapConfig.ScaledUInt256ToUFix64(price0AverageScaled)
self.price1Average = SwapConfig.ScaledUInt256ToUFix64(price1AverageScaled)
​
self.price0CumulativeLastScaled = currentPrice0CumulativeScaled
self.price1CumulativeLastScaled = currentPrice1CumulativeScaled
self.blockTimestampLast = now
}
​
/// Queries twap price data
/// Returns 0.0 for data n/a or invalid input token
pub fun twap(tokenKey: String): UFix64 {
if (tokenKey == self.token0Key) {
return self.price0Average
} else if (tokenKey == self.token1Key) {
return self.price1Average
} else {
return 0.0
}
}
​
/// @Param - token{A|B}Key: e.g. A.f8d6e0586b0a20c7.FUSD
/// @Param - isStableswap: whether the twap is for stableswap pair or not
/// @Param - period: average period (in seconds)
init(tokenAKey: String, tokenBKey: String, isStableswap: Bool, period: UInt64) {
self.PERIOD = period
self.isStableswap = isStableswap
self.pairAddr = isStableswap ?
StableSwapFactory.getPairAddress(token0Key: tokenAKey, token1Key: tokenBKey) ?? panic("non-existent stableswap-pair") :
SwapFactory.getPairAddress(token0Key: tokenAKey, token1Key: tokenBKey) ?? panic("non-existent pair")
​
let pairPublicRef = getAccount(self.pairAddr).getCapability<&{SwapInterfaces.PairPublic}>(SwapConfig.PairPublicPath).borrow()
?? panic("cannot borrow reference to PairPublic")
let pairInfo = pairPublicRef.getPairInfo()
self.token0Key = pairInfo[0] as! String
self.token1Key = pairInfo[1] as! String
let reserve0 = pairInfo[2] as! UFix64
let reserve1 = pairInfo[3] as! UFix64
assert(reserve0 * reserve1 != 0.0, message: "There's no liquidity in the pair")
​
self.price0CumulativeLastScaled = pairPublicRef.getPrice0CumulativeLastScaled()
self.price1CumulativeLastScaled = pairPublicRef.getPrice1CumulativeLastScaled()
self.blockTimestampLast = pairPublicRef.getBlockTimestampLast()
self.price0Average = 0.0
self.price1Average = 0.0
}
}

Sliding-window TWAP oracle example

import StableSwapFactory from 0xStableSwapFactoryAddr
import SwapFactory from 0xSwapFactoryAddr
import SwapInterfaces from 0xSwapInterfacesAddr
import SwapConfig from 0xSwapConfigAddr
​
/// Sliding window oracle
///
pub contract SlidingWindowOracleExample {
/// The amount of time (in seconds) the moving average should be computed, e.g. 24 hours
pub let windowSize: UInt64
/// The number of observation data stored for windowSize.
/// As granularity increases from 2, more frequent updates are needed, but moving averages become more precise.
/// twap data is computed over intervals with sizes in the range: [windowSize - (windowSize / granularity) * 2, windowSize]
pub let granularity: UInt64
/// The amount of time once an update() is needed, periodSize * granularity == windowSize.
pub let periodSize: UInt64
/// A.contractAddr.contractName: A.11111111.FlowToken, A.2222222.FUSD
pub let token0Key: String
pub let token1Key: String
pub let isStableswap: Bool
/// pair address in dex
pub let pairAddr: Address
/// An array of price observation data of the pair
access(self) let pairObservations: [Observation]
​
pub struct Observation {
pub let timestamp: UFix64
pub let price0CumulativeScaled: UInt256
pub let price1CumulativeScaled: UInt256
​
init(t: UFix64, p0Scaled: UInt256, p1Scaled: UInt256) {
self.timestamp = t
self.price0CumulativeScaled = p0Scaled
self.price1CumulativeScaled = p1Scaled
}
}
​
​
/// Returns the index of the observation corresponding to the given timestamp
pub fun observationIndexOf(timestamp: UFix64): UInt64 {
return UInt64(timestamp) / self.periodSize % self.granularity
}
​
/// Returns the index of the earliest observation of a windowSize (relative to the given timestamp)
pub fun firstObservationIndexInWindow(timestamp: UFix64): UInt64 {
let idx = self.observationIndexOf(timestamp: timestamp)
return (idx + 1) % self.granularity
}
​
/// Sampling: update the cumulative price for the observation at the current timestamp.
/// Each observation is updated at most once per periodSize.
pub fun update() {
let now = getCurrentBlock().timestamp
let idx = self.observationIndexOf(timestamp: now)
let ob = self.pairObservations[idx]
let timeElapsed = now - ob.timestamp
​
if (timeElapsed > UFix64(self.periodSize)) {
let timeElapsedScaled = SwapConfig.UFix64ToScaledUInt256(timeElapsed)
let res = SwapConfig.getCurrentCumulativePrices(pairAddr: self.pairAddr)
let currentPrice0CumulativeScaled = res[0]
let currentPrice1CumulativeScaled = res[1]
self.pairObservations[idx] = Observation(t: now, p0Scaled: currentPrice0CumulativeScaled, p1Scaled: currentPrice1CumulativeScaled)
}
}
​
/// Queries twap price data of the time range [now - [windowSize, windowSize - 2 * periodSize], now]
/// Returns 0.0 for data n/a or invalid input token
pub fun twap(tokenKey: String): UFix64 {
let now = getCurrentBlock().timestamp
let first_ob_idx = self.firstObservationIndexInWindow(timestamp: now)
let first_ob = self.pairObservations[first_ob_idx]
let timeElapsed = now - first_ob.timestamp
​
assert(UInt64(timeElapsed) <= self.windowSize, message: "missing historical observations, more update() needed")
assert(UInt64(timeElapsed) >= self.windowSize - self.periodSize * 2, message: "should never happen")
​
let res = SwapConfig.getCurrentCumulativePrices(pairAddr: self.pairAddr)
let currentPrice0CumulativeScaled = res[0]
let currentPrice1CumulativeScaled = res[1]
let timeElapsedScaled = SwapConfig.UFix64ToScaledUInt256(timeElapsed)
​
if (tokenKey == self.token0Key) {
let price0AverageScaled = SwapConfig.underflowSubtractUInt256(currentPrice0CumulativeScaled, first_ob.price0CumulativeScaled) * SwapConfig.scaleFactor / timeElapsedScaled
return SwapConfig.ScaledUInt256ToUFix64(price0AverageScaled)
} else if (tokenKey == self.token1Key) {
let price1AverageScaled = SwapConfig.underflowSubtractUInt256(currentPrice1CumulativeScaled, first_ob.price1CumulativeScaled) * SwapConfig.scaleFactor / timeElapsedScaled
return SwapConfig.ScaledUInt256ToUFix64(price1AverageScaled)
} else {
return 0.0
}
}
​
/// @Param - token{A|B}Key: e.g. A.f8d6e0586b0a20c7.FUSD
/// @Param - isStableswap: whether the twap is for stableswap pair or not
/// @Param - windowSize: The amount of time (in seconds) the moving average should be computed, e.g.: 24 hours (86400)
/// @Param - granularity: The number of observation data stored for windowSize, e.g.: 24. The more granularity, the more precise the moving average, but with the cost of more frequent updates are needed.
init(tokenAKey: String, tokenBKey: String, isStableswap: Bool, windowSize: UInt64, granularity: UInt64) {
pre {
granularity > 1 && granularity <= windowSize: "invalid granularity"
windowSize / granularity * granularity == windowSize: "windowSize not-divisible by granularity"
}
post {
UInt64(self.pairObservations.length) == granularity: "pairObservations array not initialized"
}
​
self.windowSize = windowSize
self.granularity = granularity
self.periodSize = windowSize / granularity
self.isStableswap = isStableswap
self.pairAddr = isStableswap ?
StableSwapFactory.getPairAddress(token0Key: tokenAKey, token1Key: tokenBKey) ?? panic("non-existent stableswap-pair") :
SwapFactory.getPairAddress(token0Key: tokenAKey, token1Key: tokenBKey) ?? panic("non-existent pair")
​
let pairPublicRef = getAccount(self.pairAddr).getCapability<&{SwapInterfaces.PairPublic}>(SwapConfig.PairPublicPath).borrow()
?? panic("cannot borrow reference to PairPublic")
let pairInfo = pairPublicRef.getPairInfo()
self.token0Key = pairInfo[0] as! String
self.token1Key = pairInfo[1] as! String
let reserve0 = pairInfo[2] as! UFix64
let reserve1 = pairInfo[3] as! UFix64
assert(reserve0 * reserve1 != 0.0, message: "There's no liquidity in the pair")
​
self.pairObservations = []
var i: UInt64 = 0
while (i < granularity) {
self.pairObservations.append(Observation(t: 0.0, p0Scaled: 0, p1Scaled: 0))
i = i + 1
}
}
}

Flashloan

Flashloan Interfaces

/// interface in SwapInterfaces.cdc
/// Before using the flashloan you need to first implement this interface and plugin customized logic (see example below)
​
pub resource interface FlashLoanExecutor {
/// @params: User-definited extra data passed to executor for further auth/check/decode
pub fun executeAndRepay(loanedToken: @FungibleToken.Vault, params: {String: AnyStruct}): @FungibleToken.Vault
}

Example usage

// Example_FlashloanLiquidation.cdc
​
import FungibleToken from 0xFungibleTokenAddr
import SwapConfig from 0xSwapConfigAddr
import SwapFactory from 0xSwapFactoryAddr
import SwapInterfaces from 0xSwapInterfacesAddr
​
pub contract Example_FlashloanLiquidation {
// Specific address to receive flashloan-liquidation profits, used as auth purposes
pub let profitReceiver: Address
​
/// Implement the flashloan interface
pub resource FlashloanExecutor: SwapInterfaces.FlashLoanExecutor {
pub fun executeAndRepay(loanedToken: @FungibleToken.Vault, params: {String: AnyStruct}): @FungibleToken.Vault {
pre {
params.containsKey("profitReceiver") && ((params["profitReceiver"]! as! Address) == Example_FlashloanLiquidation.profitReceiver): "not-authorized caller"
}
​
/*
Do magic - custom logic goes here. E.g.:
- 0. Flashloan request $USDC from FUSD/USDC pool (in `do_flashloan.transaction.cdc`)
- 1. Liquidate underwater borrower by repaying borrowed $USDC and grab borrower's collateralized $Flow
- 2. Swap $Flow -> $USDC through IncrementSwap (cannot use flashloan-ed pool then) or BloctoSwap
- 3. Repay {flashloan-ed $USDC + fees} back to FUSD/USDC pool and keep remaining $USDC as profit
*/
​
// TODO: plugin detailed example here
let amountIn = loanedToken.balance
​
// TODO: plugin detailed example here
/// amountOut = amountIn x (1 + fee%)
let amountOut = amountIn * (1.0 + UFix64(SwapFactory.getFlashloanRateBps()) / 10000.0) + SwapConfig.ufix64NonZeroMin
​
// TODO: modify this
return <-loanedToken
}
}
​
init(profitReceiver: Address) {
self.profitReceiver = profitReceiver
​
// Set up FlashLoanExecutor resource
let pathStr = "swap_flashloan_executor_path"
let executorPrivatePath = PrivatePath(identifier: pathStr)!
let executorStoragePath = StoragePath(identifier: pathStr)!
destroy <-self.account.load<@AnyResource>(from: executorStoragePath)
self.account.save(<- create FlashloanExecutor(), to: executorStoragePath)
self.account.link<&{SwapInterfaces.FlashLoanExecutor}>(executorPrivatePath, target: executorStoragePath)
}
}
/// do_flashloan.transaction.cdc - tx that triggers flashloan from a SwapPair
​
import FungibleToken from 0xFungibleTokenAddr
import SwapConfig from 0xSwapConfigAddr
import SwapFactory from 0xSwapFactoryAddr
import SwapInterfaces from 0xSwapInterfacesAddr
​
/*
E.g.: Flashloan request only $USDC from FUSD/USDC pool
*/
transaction(pairAddr: Address, requestedVaultType: Type, requestedAmount: UFix64) {
prepare(signer: AuthAccount) {
let pairRef = getAccount(pairAddr).getCapability<&{SwapInterfaces.PairPublic}>(SwapConfig.PairPublicPath).borrow()
?? panic("cannot borrow reference to PairPublic")
​
// TODO: add additional args? and generalize this transaction
let args: {String: AnyStruct} = {
"profitReceiver": signer.address
}
let executorCap = signer.getCapability<&{SwapInterfaces.FlashLoanExecutor}>(/private/swap_flashloan_executor_path)
pairRef.flashloan(executorCap: executorCap, requestedTokenVaultType: requestedVaultType, requestedAmount: requestedAmount, params: args)
}
}
​