Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 19 additions & 10 deletions cadence/contracts/FlowCreditMarket.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -1185,20 +1185,31 @@ access(all) contract FlowCreditMarket {
// - also to make allowlist pattern work with automated liquidation, initiator of this automation will need actual handle on a dex in order to pass it to FCM

/// Allowlist of permitted DeFiActions Swapper types for DEX liquidations
/// TODO: unused! To remove, must re-deploy existing contracts
access(self) var allowedSwapperTypes: {Type: Bool}

/// A trusted DEX (or set of DEXes) used by FCM as a pricing oracle and trading counterparty for liquidations.
/// The SwapperProvider implementation MUST return a Swapper for all possible (ordered) pairs of supported tokens.
/// If [X1, X2, ..., Xn] is the set of supported tokens, then the SwapperProvider must return a Swapper for all pairs:
/// (Xi, Xj) where i∈[1,n], j∈[1,n], i≠j
///
/// FCM does not attempt to construct multi-part paths (using multiple Swappers) or compare prices across Swappers.
/// It relies directly on the Swapper's returned by the configured SwapperProvider.
access(self) let dex: {DeFiActions.SwapperProvider}

/// Max allowed deviation in basis points between DEX-implied price and oracle price
access(self) var dexOracleDeviationBps: UInt16

/// Max slippage allowed in basis points for DEX liquidations
/// TODO(jord): revisit this. Is this ever necessary if we are also checking dexOracleDeviationBps? Do we want both a spot price check and a slippage from spot price check?
/// TODO: unused! To remove, must re-deploy existing contracts
access(self) var dexMaxSlippageBps: UInt64

/// Max route hops allowed for DEX liquidations
// TODO(jord): unused
/// TODO: unused! To remove, must re-deploy existing contracts
access(self) var dexMaxRouteHops: UInt64

init(defaultToken: Type, priceOracle: {DeFiActions.PriceOracle}) {
init(defaultToken: Type, priceOracle: {DeFiActions.PriceOracle}, dex: {DeFiActions.SwapperProvider}) {
pre {
priceOracle.unitOfAccount() == defaultToken:
"Price oracle must return prices in terms of the default token"
Expand Down Expand Up @@ -1230,6 +1241,7 @@ access(all) contract FlowCreditMarket {
self.lastUnpausedAt = nil
self.protocolLiquidationFeeBps = 0
self.allowedSwapperTypes = {}
self.dex = dex
self.dexOracleDeviationBps = UInt16(300) // 3% default
self.dexMaxSlippageBps = 100
self.dexMaxRouteHops = 3
Expand Down Expand Up @@ -1531,14 +1543,12 @@ access(all) contract FlowCreditMarket {
let postHealth = FlowCreditMarket.healthComputation(effectiveCollateral: Ce_post, effectiveDebt: De_post)
assert(postHealth <= self.liquidationTargetHF, message: "Liquidation must not exceed target health: \(postHealth)>\(self.liquidationTargetHF)")

// TODO(jord): uncomment following when implementing dex logic https://github.com/onflow/FlowCreditMarket/issues/94
/*
// Compare the liquidation offer to liquidation via DEX. If the DEX would provide a better price, reject the offer.
let swapper = self.dex!.getSwapper(inType: seizeType, outType: debtType)! // TODO: will revert if pair unsupported
let swapper = self.dex.getSwapper(inType: seizeType, outType: debtType)! // TODO: will revert if pair unsupported
// Get a quote: "how much collateral do I need to give you to get `repayAmount` debt tokens"
let quote = swapper.quoteIn(forDesired: repayAmount, reverse: false)
assert(seizeAmount < quote.inAmount, message: "Liquidation offer must be better than that offered by DEX")

// Compare the DEX price to the oracle price and revert if they diverge beyond configured threshold.
let Pcd_dex = quote.outAmount / quote.inAmount // price of collateral, denominated in debt token, implied by dex quote (D/C)
// Compute the absolute value of the difference between the oracle price and dex price
Expand All @@ -1548,7 +1558,6 @@ access(all) contract FlowCreditMarket {
let Pcd_dex_oracle_diffBps = UInt16(Pcd_dex_oracle_diffPct * 10_000.0) // cannot overflow because Pcd_dex_oracle_diffPct<=1

assert(Pcd_dex_oracle_diffBps <= self.dexOracleDeviationBps, message: "Too large difference between dex/oracle prices diff=\(Pcd_dex_oracle_diffBps)bps")
*/

// Execute the liquidation
return <- self._doLiquidation(pid: pid, repayment: <-repayment, debtType: debtType, seizeType: seizeType, seizeAmount: seizeAmount)
Expand Down Expand Up @@ -2100,7 +2109,7 @@ access(all) contract FlowCreditMarket {
)
}

// Returns health value of this position if the given amount of the specified token were withdrawn without
// Returns health value of this position if the given amount of the specified token were withdrawn without
// using the top up source.
// NOTE: This method can return health values below 1.0, which aren't actually allowed. This indicates
// that the proposed withdrawal would fail (unless a top up source is available and used).
Expand Down Expand Up @@ -3082,12 +3091,12 @@ access(all) contract FlowCreditMarket {
///
access(all) resource PoolFactory {
/// Creates the contract-managed Pool and saves it to the canonical path, reverting if one is already stored
access(all) fun createPool(defaultToken: Type, priceOracle: {DeFiActions.PriceOracle}) {
access(all) fun createPool(defaultToken: Type, priceOracle: {DeFiActions.PriceOracle}, dex: {DeFiActions.SwapperProvider}) {
pre {
FlowCreditMarket.account.storage.type(at: FlowCreditMarket.PoolStoragePath) == nil:
"Storage collision - Pool has already been created & saved to \(FlowCreditMarket.PoolStoragePath)"
}
let pool <- create Pool(defaultToken: defaultToken, priceOracle: priceOracle)
let pool <- create Pool(defaultToken: defaultToken, priceOracle: priceOracle, dex: dex)
FlowCreditMarket.account.storage.save(<-pool, to: FlowCreditMarket.PoolStoragePath)
let cap = FlowCreditMarket.account.capabilities.storage.issue<&Pool>(FlowCreditMarket.PoolStoragePath)
FlowCreditMarket.account.capabilities.unpublish(FlowCreditMarket.PoolPublicPath)
Expand Down
35 changes: 35 additions & 0 deletions cadence/contracts/mocks/MockDexSwapper.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,33 @@ import "DeFiActionsUtils"
/// Do NOT use in production.
access(all) contract MockDexSwapper {

/// Holds the set of available swappers which will be provided to users of SwapperProvider.
/// inType -> outType -> Swapper
access(self) let swappers: {Type: {Type: Swapper}}

init() {
self.swappers = {}
}

access(all) fun getSwapper(inType: Type, outType: Type): Swapper? {
if let swappersForInType = self.swappers[inType] {
return swappersForInType[outType]
}
return nil
}

/// Used by testing code to configure the DEX with swappers.
/// Overwrites existing swapper with same types, if any.
access(all) fun _addSwapper(swapper: Swapper) {
if self.swappers[swapper.inType()] == nil {
self.swappers[swapper.inType()] = { swapper.outType(): swapper }
} else {
let swappersForInType = self.swappers[swapper.inType()]!
swappersForInType[swapper.outType()] = swapper
self.swappers[swapper.inType()] = swappersForInType
}
}

access(all) struct BasicQuote : DeFiActions.Quote {
access(all) let inType: Type
access(all) let outType: Type
Expand All @@ -21,9 +48,11 @@ access(all) contract MockDexSwapper {
}
}

/// NOTE: reverse swaps are unsupported.
access(all) struct Swapper : DeFiActions.Swapper {
access(self) let inVault: Type
access(self) let outVault: Type
/// source for output tokens only (reverse swaps unsupported)
access(self) let vaultSource: Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>
access(self) let priceRatio: UFix64 // out per unit in
access(contract) var uniqueID: DeFiActions.UniqueIdentifier?
Expand Down Expand Up @@ -86,4 +115,10 @@ access(all) contract MockDexSwapper {
access(contract) view fun copyID(): DeFiActions.UniqueIdentifier? { return self.uniqueID }
access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) { self.uniqueID = id }
}

access(all) struct SwapperProvider : DeFiActions.SwapperProvider {
access(all) fun getSwapper(inType: Type, outType: Type): Swapper? {
return MockDexSwapper.getSwapper(inType: inType, outType: outType)
}
}
}
Loading