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
114 changes: 114 additions & 0 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 {
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.maximumAvailable())
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 @@ -1830,6 +1933,10 @@ access(all) contract TidalProtocol {
let pool = self.pool.borrow()!
return pool.availableBalance(pid: self.id, type: type, pullFromTopUpSource: pullFromTopUpSource)
}
access(all) fun closeoutBalance(type: Type, pullFromTopUpSource: Bool): UFix64 {
let pool = self.pool.borrow()!
return pool.simulateCloseoutWithdrawalAmount(pid: self.id, type: type)
}
/// Returns the current health of the position
access(all) fun getHealth(): UInt128 {
let pool = self.pool.borrow()!
Expand Down Expand Up @@ -2037,6 +2144,13 @@ access(all) contract TidalProtocol {
let pool = self.pool.borrow()!
return pool.availableBalance(pid: self.positionID, type: self.type, pullFromTopUpSource: self.pullFromTopUpSource)
}
access(all) fun maximumAvailable(): UFix64 {
if !self.pool.check() {
return 0.0
}
let pool = self.pool.borrow()!
return pool.simulateCloseoutWithdrawalAmount(pid: self.positionID, type: 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
61 changes: 61 additions & 0 deletions cadence/tests/calculate_closeout_amount_test.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import Test
import "TidalProtocol"
import "DeFiActionsMathUtils"
import "FungibleToken"
import "MOET"
import "test_helpers.cdc"
import "MockYieldToken"

access(all) fun setup() {
deployContracts()
}

access(all) fun snap(price: UFix64): TidalProtocol.TokenSnapshot {
return TidalProtocol.TokenSnapshot(
price: DeFiActionsMathUtils.toUInt128(price),
credit: DeFiActionsMathUtils.e24,
debit: DeFiActionsMathUtils.e24,
risk: TidalProtocol.RiskParams(
cf: DeFiActionsMathUtils.e24,
bf: DeFiActionsMathUtils.e24,
lb: DeFiActionsMathUtils.e24
)
)
}

access(all) let WAD: UInt128 = 1_000_000_000_000_000_000_000_000

access(all) fun test_calculateCloseoutBalance_handlesOtherTokens() {
let tWithdraw = Type<@MOET.Vault>()
let tColl = Type<@MockYieldToken.Vault>()

let snaps: {Type: TidalProtocol.TokenSnapshot} = {}
snaps[tWithdraw] = snap(price: 1.0)
snaps[tColl] = snap(price: 2.0)

let balances: {Type: TidalProtocol.InternalBalance} = {}
balances[tWithdraw] = TidalProtocol.InternalBalance(direction: TidalProtocol.BalanceDirection.Debit,
scaledBalance: DeFiActionsMathUtils.toUInt128(50.0))
balances[tColl] = TidalProtocol.InternalBalance(direction: TidalProtocol.BalanceDirection.Credit,
scaledBalance: DeFiActionsMathUtils.toUInt128(100.0))

let view = TidalProtocol.PositionView(
balances: balances,
snapshots: snaps,
def: tWithdraw,
min: WAD,
max: WAD
)

let topUpAvail: UInt128 = DeFiActionsMathUtils.toUInt128(60.0)

let result = TidalProtocol.calculateCloseoutBalance(
view: view,
withdrawSnap: snaps[tWithdraw]!,
topUpSnap: snaps[tWithdraw]!,
topUpAvailable: topUpAvail
)

let expected = DeFiActionsMathUtils.toUInt128(210.0)
Test.assertEqual(expected, result)
}
Loading