Skip to content
Closed
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
2 changes: 1 addition & 1 deletion DeFiActions
96 changes: 94 additions & 2 deletions cadence/contracts/TidalProtocol.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ access(all) contract TidalProtocol {
if pullFromTopUpSource && position.topUpSource != nil {
let topUpSource = position.topUpSource!
let sourceType = topUpSource.getSourceType()
let sourceAmount = topUpSource.minimumAvailable()
let sourceAmount = topUpSource.minimumAvailable(liquidate: false)
log(" [CONTRACT] Calling to fundsAvailableAboveTargetHealthAfterDepositing with sourceAmount \(sourceAmount) and targetHealth \(position.minHealth)")

return self.fundsAvailableAboveTargetHealthAfterDepositing(
Expand All @@ -471,6 +471,95 @@ access(all) contract TidalProtocol {
}
}

/// Simulates the withdrawable amount of `type` if the position were fully rebalanced (liquidation scenario).
/// Returns the amount in units of `type`.
access(all) fun simulateLiquidationAmount(pid: UInt64, type: Type): UFix64 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm realizing we may be overloading the term "liquidation" with this. When we say "liquidation" here, does this have anything to do with the liquidation path in the sense one would think of with respect to a lending protocol? In other words, are we using liquidation here to mean fully exiting the position or reclaiming collateral in the case of bad debt?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in this case it means fully closing/exiting a position at that moment

let position = self._borrowPosition(pid: pid)

// If there's no top-up source configured, nothing can be pulled during liquidation.
if position.topUpSource == nil {
log("simulateLiquidationAmount: no topUpSource found for position \(pid) - returning 0.0")
return 0.0
}

let topUpSource = position.topUpSource!
let sourceType = topUpSource.getSourceType()

// Prices (in default token units)
let maybeDepositTokenPrice = self.priceOracle.price(ofToken: type)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this the price of type being withdrawn?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, it is, I think the confusion is between deposit as a noun and deposit as a verb, I'll rename it

let maybeSourceTokenPrice = self.priceOracle.price(ofToken: sourceType)
if maybeDepositTokenPrice == nil || maybeSourceTokenPrice == nil {
log("simulateLiquidationAmount: missing price(s); returning 0.0")
return 0.0
}
let uintDepositTokenPrice = DeFiActionsMathUtils.toUInt128(maybeDepositTokenPrice!)
let uintSourceTokenPrice = DeFiActionsMathUtils.toUInt128(maybeSourceTokenPrice!)

// Amount available from the top-up source under liquidation semantics
let sourceAmountUFix = topUpSource.minimumAvailable(liquidate: true)
let uintSourceAmount = DeFiActionsMathUtils.toUInt128(sourceAmountUFix)

// ----- Deposit token leg (this position's balance in `type`) -----
let maybeDepositBalance = position.balances[type]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't a balance also exist for withdrawals on a type? I think the variable name is throwing me off since I'd think the balance returned is the amount available for withdrawal from the position, which in my mind conflicts with deposit here.

Also, I think we need to check the full position balance across all withdrawals, not just the source type and provided type. Once we're dealing with balances related to more than two type, I think the balances of the third type (and beyond) will be omitted from this calculation.

For instance, let's say the topUpSource provides MOET and the requested Type is FLOW, how would this method consider a credit or debit in a third token type?

var depositCreditQuote: UInt128 = 0 // value (in default token) of credit in `type`
var depositDebtQuote: UInt128 = 0 // value (in default token) of debt in `type`

if maybeDepositBalance != nil {
let depositTokenState = self._borrowUpdatedTokenState(type: type)
let bal = maybeDepositBalance!
if bal.direction == BalanceDirection.Credit {
let trueCredit = TidalProtocol.scaledBalanceToTrueBalance(
bal.scaledBalance,
interestIndex: depositTokenState.creditInterestIndex
)
depositCreditQuote = DeFiActionsMathUtils.mul(trueCredit, uintDepositTokenPrice)
} else {
let trueDebt = TidalProtocol.scaledBalanceToTrueBalance(
bal.scaledBalance,
interestIndex: depositTokenState.debitInterestIndex
)
depositDebtQuote = DeFiActionsMathUtils.mul(trueDebt, uintDepositTokenPrice)
}
}

// ----- Source token debt leg (debt in the top-up token we must cover first) -----
let maybeSourceBalance = position.balances[sourceType]
var sourceDebtQuote: UInt128 = 0
if maybeSourceBalance?.direction == BalanceDirection.Debit {
let sourceTokenState = self._borrowUpdatedTokenState(type: sourceType)
let trueSourceDebt = TidalProtocol.scaledBalanceToTrueBalance(
maybeSourceBalance!.scaledBalance,
interestIndex: sourceTokenState.debitInterestIndex
)
sourceDebtQuote = DeFiActionsMathUtils.mul(trueSourceDebt, uintSourceTokenPrice)
}

// ----- Source token available (credit we can pull during liquidation) -----
let sourceAvailQuote = DeFiActionsMathUtils.mul(uintSourceAmount, uintSourceTokenPrice)

// Net value in default-token quote terms
// netQuote = depositCreditQuote + sourceAvailQuote - sourceDebtQuote - depositDebtQuote
var netQuote: UInt128 = 0
let additions = depositCreditQuote + sourceAvailQuote
let subtractions = sourceDebtQuote + depositDebtQuote
if additions > subtractions {
netQuote = additions - subtractions
} else {
// Nothing left for withdrawal in `type` after covering debts
log("simulateLiquidationAmount: net quote is non-positive; returning 0.0")
return 0.0
}

// Convert net quote value back into `type` units
if uintDepositTokenPrice == 0 {
log("simulateLiquidationAmount: deposit token price is zero; returning 0.0")
return 0.0
}
let uintLiquidationAmountInType = DeFiActionsMathUtils.div(netQuote, uintDepositTokenPrice)

return DeFiActionsMathUtils.toUFix64Round(uintLiquidationAmountInType)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would think we want to round down here since this amount denotes a withdrawal from a position. Is that right?

}

/// Returns the health of the given position, which is the ratio of the position's effective collateral to its
/// debt as denominated in the Pool's default token. "Effective collateral" means the value of each credit balance
/// times the liquidation threshold for that token. i.e. the maximum borrowable amount
Expand Down Expand Up @@ -1825,11 +1914,14 @@ access(all) contract TidalProtocol {
return self.type
}
/// Returns the minimum availble this Source can provide on withdrawal
access(all) fun minimumAvailable(): UFix64 {
access(all) fun minimumAvailable(liquidate: Bool): UFix64 {
if !self.pool.check() {
return 0.0
}
let pool = self.pool.borrow()!
if liquidate {
return pool.simulateLiquidationAmount(pid: self.positionID, type: self.type)
}
return pool.availableBalance(pid: self.positionID, type: self.type, pullFromTopUpSource: self.pullFromTopUpSource)
}
/// Withdraws up to the max amount as the sourceType Vault
Expand Down
Loading