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
126 changes: 123 additions & 3 deletions cadence/contracts/TidalProtocol.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,50 @@ access(all) contract TidalProtocol {
}
}

/// Computes the amount of `withdrawSnap` token obtainable after fully
/// liquidating a position and pulling up to `topUpAvailable` of the
/// `topUpSnap` token. The calculation operates entirely on the provided
/// `PositionView`, so callers can unit test the logic without spinning up
/// on-chain state.
access(all) view fun calculateCloseoutBalance(
view: PositionView,
withdrawSnap: TokenSnapshot,
topUpSnap: TokenSnapshot,
topUpAvailable: UInt128
): UInt128 {
// Value (in default-token units) available from the external top-up source
var additions: UInt128 = DeFiActionsMathUtils.mul(topUpAvailable, topUpSnap.price)
var subtractions: UInt128 = 0

// Aggregate all existing position balances regardless of token type
for tokenType in view.balances.keys {
let bal = view.balances[tokenType]!
let snap = view.snapshots[tokenType]!
if bal.direction == BalanceDirection.Credit {
let trueCredit = TidalProtocol.scaledBalanceToTrueBalance(
bal.scaledBalance,
interestIndex: snap.creditIndex
)
additions = additions + DeFiActionsMathUtils.mul(trueCredit, snap.price)
} else {
let trueDebt = TidalProtocol.scaledBalanceToTrueBalance(
bal.scaledBalance,
interestIndex: snap.debitIndex
)
subtractions = subtractions + DeFiActionsMathUtils.mul(trueDebt, snap.price)
}
}

if additions <= subtractions {
return 0
}
let netQuote = additions - subtractions
if withdrawSnap.price == 0 {
return 0
}
return DeFiActionsMathUtils.div(netQuote, withdrawSnap.price)
}

// ----- End Phase 0 additions ---------------------------------------------

/// Pool
Expand Down Expand Up @@ -1145,6 +1189,65 @@ access(all) contract TidalProtocol {
return DeFiActionsMathUtils.toUFix64RoundDown(availableTokens + collateralTokenCount)
}

/// Simulates the withdrawable amount of `type` if the position were fully rebalanced (liquidation scenario).
/// Returns the amount in units of `type`.
access(all) fun simulateCloseoutWithdrawalAmount(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.

Does this need to be integrated into the pool's withdrawal path? It's not clear to me how the checks within the withdrawAndPull method pass without also risking withdrawals exceed their health limits on standard withdrawals

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("simulateCloseoutWithdrawalAmount: 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 maybeWithdrawPrice = self.priceOracle.price(ofToken: type)
let maybeSourcePrice = self.priceOracle.price(ofToken: sourceType)
if maybeWithdrawPrice == nil || maybeSourcePrice == nil {
log("simulateCloseoutWithdrawalAmount: missing price(s); returning 0.0")
return 0.0
}
// Build a view of the position for pure math
let view = self.buildPositionView(pid: pid)

// Snapshots for withdraw and source tokens
let withdrawState = self._borrowUpdatedTokenState(type: type)
let withdrawSnap = TidalProtocol.TokenSnapshot(
price: DeFiActionsMathUtils.toUInt128(maybeWithdrawPrice!),
credit: withdrawState.creditInterestIndex,
debit: withdrawState.debitInterestIndex,
risk: TidalProtocol.RiskParams(
cf: DeFiActionsMathUtils.toUInt128(self.collateralFactor[type]!),
bf: DeFiActionsMathUtils.toUInt128(self.borrowFactor[type]!),
lb: DeFiActionsMathUtils.e24 + 50_000_000_000_000_000_000_000
)
)

let sourceState = self._borrowUpdatedTokenState(type: sourceType)
let sourceSnap = TidalProtocol.TokenSnapshot(
price: DeFiActionsMathUtils.toUInt128(maybeSourcePrice!),
credit: sourceState.creditInterestIndex,
debit: sourceState.debitInterestIndex,
risk: TidalProtocol.RiskParams(
cf: DeFiActionsMathUtils.toUInt128(self.collateralFactor[sourceType]!),
bf: DeFiActionsMathUtils.toUInt128(self.borrowFactor[sourceType]!),
lb: DeFiActionsMathUtils.e24 + 50_000_000_000_000_000_000_000
)
)

let topUpAvailable = DeFiActionsMathUtils.toUInt128(topUpSource.minimumAvailable())
let uintAmount = TidalProtocol.calculateCloseoutBalance(
view: view,
withdrawSnap: withdrawSnap,
topUpSnap: sourceSnap,
topUpAvailable: topUpAvailable
)
return DeFiActionsMathUtils.toUFix64RoundDown(uintAmount)
}

/// Returns the position's health if the given amount of the specified token were deposited
access(all) fun healthAfterDeposit(pid: UInt64, type: Type, amount: UFix64): UInt128 {
let balanceSheet = self._getUpdatedBalanceSheet(pid: pid)
Expand Down Expand Up @@ -1906,15 +2009,15 @@ access(all) contract TidalProtocol {
/// Returns a new Source for the given token type that will service withdrawals of that token and update the
/// position's collateral and/or debt accordingly. Note that calling this method multiple times will create
/// multiple sources, each of which will continue to work regardless of how many other sources have been created.
access(FungibleToken.Withdraw) fun createSource(type: Type): {DeFiActions.Source} {
access(FungibleToken.Withdraw) fun createSource(type: Type): {DeFiActions.Source, DeFiActions.Liquidator} {
// Create enhanced source with pullFromTopUpSource = true
return self.createSourceWithOptions(type: type, pullFromTopUpSource: false)
}
/// Returns a new Source for the given token type and pullFromTopUpSource option that will service withdrawals
/// of that token and update the position's collateral and/or debt accordingly. Note that calling this method
/// multiple times will create multiple sources, each of which will continue to work regardless of how many
/// other sources have been created.
access(FungibleToken.Withdraw) fun createSourceWithOptions(type: Type, pullFromTopUpSource: Bool): {DeFiActions.Source} {
access(FungibleToken.Withdraw) fun createSourceWithOptions(type: Type, pullFromTopUpSource: Bool): {DeFiActions.Source, DeFiActions.Liquidator} {
let pool = self.pool.borrow()!
return PositionSource(id: self.id, pool: self.pool, type: type, pullFromTopUpSource: pullFromTopUpSource)
}
Expand Down Expand Up @@ -2004,7 +2107,7 @@ access(all) contract TidalProtocol {
/// A DeFiActions connector enabling withdrawals from a Position from within a DeFiActions stack. This Source is
/// intended to be constructed from a Position object.
///
access(all) struct PositionSource: DeFiActions.Source {
access(all) struct PositionSource: DeFiActions.Source, DeFiActions.Liquidator {
/// An optional DeFiActions.UniqueIdentifier that identifies this Sink with the DeFiActions stack its a part of
access(contract) var uniqueID: DeFiActions.UniqueIdentifier?
/// An authorized Capability on the Pool for which the related Position is in
Expand Down Expand Up @@ -2037,6 +2140,23 @@ access(all) contract TidalProtocol {
let pool = self.pool.borrow()!
return pool.availableBalance(pid: self.positionID, type: self.type, pullFromTopUpSource: self.pullFromTopUpSource)
}
access(all) view fun getLiquidationType(): Type {
return self.type
}
access(all) fun liquidationAmount(): UFix64 {
if let pool = self.pool.borrow() {
return pool.simulateCloseoutWithdrawalAmount(pid: self.positionID, type: self.type)
}
return 0.0
}
access(FungibleToken.Withdraw) fun liquidate(data: AnyStruct?): @{FungibleToken.Vault} {
if let pool = self.pool.borrow() {
let amt = pool.simulateCloseoutWithdrawalAmount(pid: self.positionID, type: self.type)
return <- self.withdrawAvailable(maxAmount: amt)
}
// If the pool capability is invalid, return an empty vault of the correct type
return <- DeFiActionsUtils.getEmptyVault(self.type)
}
/// Withdraws up to the max amount as the sourceType Vault
access(FungibleToken.Withdraw) fun withdrawAvailable(maxAmount: UFix64): @{FungibleToken.Vault} {
if !self.pool.check() {
Expand Down
Loading