diff --git a/.gitmodules b/.gitmodules index d2babe94..82aadd90 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "DeFiBlocks"] path = DeFiBlocks - url = https://github.com/onflow/DeFiBlocks + url = git@github.com:onflow/DeFiBlocks.git diff --git a/DeFiBlocks b/DeFiBlocks index 4a510908..a2dcbd50 160000 --- a/DeFiBlocks +++ b/DeFiBlocks @@ -1 +1 @@ -Subproject commit 4a510908f1fd24da84543bb045bb4f7605e9c8f0 +Subproject commit a2dcbd506152a31fe713617f8ab684315d4cbf44 diff --git a/cadence/contracts/MOET.cdc b/cadence/contracts/MOET.cdc index 4ab12018..09aaa3db 100644 --- a/cadence/contracts/MOET.cdc +++ b/cadence/contracts/MOET.cdc @@ -149,8 +149,11 @@ access(all) contract MOET : FungibleToken { access(all) fun deposit(from: @{FungibleToken.Vault}) { let vault <- from as! @MOET.Vault - self.balance = self.balance + vault.balance + let amount = vault.balance + vault.balance = 0.0 destroy vault + + self.balance = self.balance + amount } access(all) fun createEmptyVault(): @MOET.Vault { diff --git a/cadence/contracts/TidalProtocol.cdc b/cadence/contracts/TidalProtocol.cdc index 67a40e35..5aefa497 100644 --- a/cadence/contracts/TidalProtocol.cdc +++ b/cadence/contracts/TidalProtocol.cdc @@ -1,66 +1,47 @@ +import "Burner" import "FungibleToken" import "ViewResolver" import "MetadataViews" import "FungibleTokenMetadataViews" +import "DFBUtils" import "DFB" import "MOET" -access(all) contract TidalProtocol: FungibleToken { +access(all) contract TidalProtocol { - access(all) entitlement Withdraw + /// The canonical StoragePath where the primary TidalProtocol Pool is stored + access(all) let PoolStoragePath: StoragePath + /// The canonical StoragePath where the PoolFactory resource is stored + access(all) let PoolFactoryPath: StoragePath + /// The canonical PublicPath where the primary TidalProtocol Pool can be accessed publicly + access(all) let PoolPublicPath: PublicPath - // REMOVED: FlowVault resource implementation (previously lines 12-56) - // The FlowVault resource has been removed to prevent type conflicts - // with the real FlowToken.Vault when integrating with Tidal contracts. - // All references to FlowVault will now use FlowToken.Vault instead. + /* --- EVENTS ---- */ + + access(all) event Opened(pid: UInt64, poolUUID: UInt64) + access(all) event Deposited(pid: UInt64, poolUUID: UInt64, type: String, amount: UFix64, depositedUUID: UInt64) + access(all) event Withdrawn(pid: UInt64, poolUUID: UInt64, type: String, amount: UFix64, withdrawnUUID: UInt64) + access(all) event Rebalanced(pid: UInt64, poolUUID: UInt64, atHealth: UFix64, amount: UFix64, fromUnder: Bool) + + /* --- CONSTRUCTS & INTERNAL METHODS ---- */ access(all) entitlement EPosition access(all) entitlement EGovernance access(all) entitlement EImplementation - // RESTORED: BalanceSheet and health computation from Dieter's implementation - // A convenience function for computing a health value from effective collateral and debt values. - access(all) fun healthComputation(effectiveCollateral: UFix64, effectiveDebt: UFix64): UFix64 { - var health = 0.0 - - if effectiveCollateral == 0.0 { - health = 0.0 - } else if effectiveDebt == 0.0 { - health = UFix64.max - } else if (effectiveDebt / effectiveCollateral) == 0.0 { - // If debt is so small relative to collateral that division rounds to zero, - // the health is essentially infinite - health = UFix64.max - } else { - health = effectiveCollateral / effectiveDebt - } - - return health - } - - access(all) struct BalanceSheet { - access(all) let effectiveCollateral: UFix64 - access(all) let effectiveDebt: UFix64 - access(all) let health: UFix64 - - init(effectiveCollateral: UFix64, effectiveDebt: UFix64) { - self.effectiveCollateral = effectiveCollateral - self.effectiveDebt = effectiveDebt - self.health = TidalProtocol.healthComputation(effectiveCollateral: effectiveCollateral, effectiveDebt: effectiveDebt) - } - } - - // A structure used internally to track a position's balance for a particular token. + /// InternalBalance + /// + /// A structure used internally to track a position's balance for a particular token access(all) struct InternalBalance { + /// The current direction of the balance - Credit (owed to borrower) or Debit (owed to protocol) access(all) var direction: BalanceDirection - - // Interally, position balances are tracked using a "scaled balance". The "scaled balance" is the - // actual balance divided by the current interest index for the associated token. This means we don't - // need to update the balance of a position as time passes, even as interest rates change. We only need - // to update the scaled balance when the user deposits or withdraws funds. The interest index - // is a number relatively close to 1.0, so the scaled balance will be roughly of the same order - // of magnitude as the actual balance (thus we can use UFix64 for the scaled balance). + /// Internally, position balances are tracked using a "scaled balance". The "scaled balance" is the + /// actual balance divided by the current interest index for the associated token. This means we don't + /// need to update the balance of a position as time passes, even as interest rates change. We only need + /// to update the scaled balance when the user deposits or withdraws funds. The interest index + /// is a number relatively close to 1.0, so the scaled balance will be roughly of the same order + /// of magnitude as the actual balance (thus we can use UFix64 for the scaled balance). access(all) var scaledBalance: UFix64 init() { @@ -68,6 +49,10 @@ access(all) contract TidalProtocol: FungibleToken { self.scaledBalance = 0.0 } + /// Records a deposit of the defined amount, updating the inner scaledBalance as well as relevant values in the + /// provided TokenState. It's assumed the TokenState and InternalBalance relate to the same token Type, but + /// since neither struct have values defining the associated token, callers should be sure to make the arguments + /// do in fact relate to the same token Type. access(all) fun recordDeposit(amount: UFix64, tokenState: auth(EImplementation) &TokenState) { if self.direction == BalanceDirection.Credit { // Depositing into a credit position just increases the balance. @@ -113,6 +98,10 @@ access(all) contract TidalProtocol: FungibleToken { } } + /// Records a withdrawal of the defined amount, updating the inner scaledBalance as well as relevant values in + /// the provided TokenState. It's assumed the TokenState and InternalBalance relate to the same token Type, but + /// since neither struct have values defining the associated token, callers should be sure to make the arguments + /// do in fact relate to the same token Type. access(all) fun recordWithdrawal(amount: UFix64, tokenState: &TokenState) { if self.direction == BalanceDirection.Debit { // Withdrawing from a debit position just increases the debt amount. @@ -160,21 +149,52 @@ access(all) contract TidalProtocol: FungibleToken { } } + /// BalanceSheet + /// + /// An struct containing a position's overview in terms of its effective collateral and debt as well as its + /// current health + access(all) struct BalanceSheet { + /// A position's withdrawable value based on collateral deposits against the Pool's collateral and borrow factors + access(all) let effectiveCollateral: UFix64 + /// A position's withdrawn value based on withdrawals against the Pool's collateral and borrow factors + access(all) let effectiveDebt: UFix64 + /// The health of the related position + access(all) let health: UFix64 + + init(effectiveCollateral: UFix64, effectiveDebt: UFix64) { + self.effectiveCollateral = effectiveCollateral + self.effectiveDebt = effectiveDebt + self.health = TidalProtocol.healthComputation(effectiveCollateral: effectiveCollateral, effectiveDebt: effectiveDebt) + } + } + + /// Entitlement mapping enabling authorized references on nested resources within InternalPosition access(all) entitlement mapping ImplementationUpdates { EImplementation -> Mutate EImplementation -> FungibleToken.Withdraw } - // RESTORED: InternalPosition as resource per Dieter's design - // This MUST be a resource to properly manage queued deposits + /// InternalPosition + /// + /// An internal resource used to track deposits, withdrawals, balances, and queued deposits to an open position. access(all) resource InternalPosition { - access(mapping ImplementationUpdates) var balances: {Type: InternalBalance} - access(mapping ImplementationUpdates) var queuedDeposits: @{Type: {FungibleToken.Vault}} + /// The target health of the position access(EImplementation) var targetHealth: UFix64 + /// The minimum health of the position, below which a position is considered undercollateralized access(EImplementation) var minHealth: UFix64 + /// The maximum health of the position, above which a position is considered overcollateralized access(EImplementation) var maxHealth: UFix64 - access(EImplementation) var drawDownSink: {DFB.Sink}? - access(EImplementation) var topUpSource: {DFB.Source}? + /// The balances of deposited and withdrawn token types + access(mapping ImplementationUpdates) var balances: {Type: InternalBalance} + /// Funds that have been deposited but must be asynchronously added to the Pool's reserves and recorded + access(mapping ImplementationUpdates) var queuedDeposits: @{Type: {FungibleToken.Vault}} + /// A DeFiBlocks Sink that if non-nil will enable the Pool to push overflown value automatically when the + /// position exceeds its maximum health based on the value of deposited collateral versus withdrawals + access(mapping ImplementationUpdates) var drawDownSink: {DFB.Sink}? + /// A DeFiBlocks Source that if non-nil will enable the Pool to pull underflown value automatically when the + /// position falls below its minimum health based on the value of deposited collateral versus withdrawals. If + /// this value is not set, liquidation may occur in the event of undercollateralization. + access(mapping ImplementationUpdates) var topUpSource: {DFB.Source}? init() { self.balances = {} @@ -186,116 +206,100 @@ access(all) contract TidalProtocol: FungibleToken { self.topUpSource = nil } + /// Sets the InternalPosition's drawDownSink. If `nil`, the Pool will not be able to push overflown value when + /// the position exceeds its maximum health. Note, if a non-nil value is provided, the Sink MUST accept MOET + /// deposits or the operation will revert. access(EImplementation) fun setDrawDownSink(_ sink: {DFB.Sink}?) { + pre { + sink?.getSinkType() ?? Type<@MOET.Vault>() == Type<@MOET.Vault>(): + "Invalid Sink provided - Sink \(sink.getType().identifier) must accept MOET" + } self.drawDownSink = sink } - + /// Sets the InternalPosition's topUpSource. If `nil`, the Pool will not be able to pull underflown value when + /// the position falls below its minimum health which may result in liquidation. access(EImplementation) fun setTopUpSource(_ source: {DFB.Source}?) { self.topUpSource = source } } + /// InterestCurve + /// + /// A simple interface to calculate interest rate access(all) struct interface InterestCurve { - access(all) fun interestRate(creditBalance: UFix64, debitBalance: UFix64): UFix64 - { + access(all) fun interestRate(creditBalance: UFix64, debitBalance: UFix64): UFix64 { post { result <= 1.0: "Interest rate can't exceed 100%" } } } + /// SimpleInterestCurve + /// + /// A simple implementation of the InterestCurve interface. access(all) struct SimpleInterestCurve: InterestCurve { access(all) fun interestRate(creditBalance: UFix64, debitBalance: UFix64): UFix64 { - return 0.0 - } - } - - // A multiplication function for interest calcuations. It assumes that both values are very close to 1 - // and represent fixed point numbers with 16 decimal places of precision. - access(all) fun interestMul(_ a: UInt64, _ b: UInt64): UInt64 { - let aScaled = a / 100000000 - let bScaled = b / 100000000 - - return aScaled * bScaled - } - - // Converts a yearly interest rate (as a UFix64) to a per-second multiplication factor - // (stored in a UInt64 as a fixed point number with 16 decimal places). The input to this function will be - // just the relative interest rate (e.g. 0.05 for 5% interest), but the result will be - // the per-second multiplier (e.g. 1.000000000001). - access(all) fun perSecondInterestRate(yearlyRate: UFix64): UInt64 { - // Covert the yearly rate to an integer maintaning the 10^8 multiplier of UFix64. - // We would need to multiply by an additional 10^8 to match the promised multiplier of - // 10^16. HOWEVER, since we are about to divide by 31536000, we can save multiply a factor - // 1000 smaller, and then divide by 31536. - let yearlyScaledValue = UInt64.fromBigEndianBytes(yearlyRate.toBigEndianBytes())! * 100000 - let perSecondScaledValue = (yearlyScaledValue / 31536) + 10000000000000000 - - return perSecondScaledValue - } - - // Updates an interest index to reflect the passage of time. The result is: - // newIndex = oldIndex * perSecondRate^seconds - access(all) fun compoundInterestIndex(oldIndex: UInt64, perSecondRate: UInt64, elapsedSeconds: UFix64): UInt64 { - var result = oldIndex - var current = perSecondRate - var secondsCounter = UInt64(elapsedSeconds) - - while secondsCounter > 0 { - if secondsCounter & 1 == 1 { - result = TidalProtocol.interestMul(result, current) - } - current = TidalProtocol.interestMul(current, current) - secondsCounter = secondsCounter >> 1 + return 0.0 // TODO } - - return result - } - - access(all) fun scaledBalanceToTrueBalance(scaledBalance: UFix64, interestIndex: UInt64): UFix64 { - // The interest index is essentially a fixed point number with 16 decimal places, we convert - // it to a UFix64 by copying the byte representation, and then dividing by 10^8 (leaving and - // additional 10^8 as required for the UFix64 representation). - let indexMultiplier = UFix64.fromBigEndianBytes(interestIndex.toBigEndianBytes())! / 100000000.0 - return scaledBalance * indexMultiplier - } - - access(all) fun trueBalanceToScaledBalance(trueBalance: UFix64, interestIndex: UInt64): UFix64 { - // The interest index is essentially a fixed point number with 16 decimal places, we convert - // it to a UFix64 by copying the byte representation, and then dividing by 10^8 (leaving and - // additional 10^8 as required for the UFix64 representation). - let indexMultiplier = UFix64.fromBigEndianBytes(interestIndex.toBigEndianBytes())! / 100000000.0 - return trueBalance / indexMultiplier } + /// TokenState + /// + /// The TokenState struct tracks values related to a single token Type within the Pool. access(all) struct TokenState { + /// The timestamp at which the TokenState was last updated access(all) var lastUpdate: UFix64 + /// The total credit balance of the related Token across the whole Pool in which this TokenState resides access(all) var totalCreditBalance: UFix64 + /// The total debit balance of the related Token across the whole Pool in which this TokenState resides access(all) var totalDebitBalance: UFix64 + /// The index of the credit interest for the related token. Interest on a token is stored as an "index" which + /// can be thought of as “how many actual tokens does 1 unit of scaled balance represent right now?” access(all) var creditInterestIndex: UInt64 + /// The index of the debit interest for the related token. Interest on a token is stored as an "index" which + /// can be thought of as “how many actual tokens does 1 unit of scaled balance represent right now?” access(all) var debitInterestIndex: UInt64 + /// The interest rate for credit of the associated token access(all) var currentCreditRate: UInt64 + /// The interest rate for debit of the associated token access(all) var currentDebitRate: UInt64 + /// The interest curve implementation used to calculate interest rate access(all) var interestCurve: {InterestCurve} - - // RESTORED: Deposit rate limiting from Dieter's implementation + /// The rate at which depositCapacity can increase over time access(all) var depositRate: UFix64 + /// The limit on deposits of the related token access(all) var depositCapacity: UFix64 + /// The upper bound on total deposits of the related token, limiting how much depositCapacity can reach access(all) var depositCapacityCap: UFix64 + init(interestCurve: {InterestCurve}, depositRate: UFix64, depositCapacityCap: UFix64) { + self.lastUpdate = getCurrentBlock().timestamp + self.totalCreditBalance = 0.0 + self.totalDebitBalance = 0.0 + self.creditInterestIndex = 10000000000000000 + self.debitInterestIndex = 10000000000000000 + self.currentCreditRate = 10000000000000000 + self.currentDebitRate = 10000000000000000 + self.interestCurve = interestCurve + self.depositRate = depositRate + self.depositCapacity = depositCapacityCap + self.depositCapacityCap = depositCapacityCap + } + + /// Updates the totalCreditBalance by the provided amount access(all) fun updateCreditBalance(amount: Fix64) { // temporary cast the credit balance to a signed value so we can add/subtract let adjustedBalance = Fix64(self.totalCreditBalance) + amount - self.totalCreditBalance = UFix64(adjustedBalance) + self.totalCreditBalance = adjustedBalance > 0.0 ? UFix64(adjustedBalance) : 0.0 } access(all) fun updateDebitBalance(amount: Fix64) { // temporary cast the debit balance to a signed value so we can add/subtract let adjustedBalance = Fix64(self.totalDebitBalance) + amount - self.totalDebitBalance = UFix64(adjustedBalance) + self.totalDebitBalance = adjustedBalance > 0.0 ? UFix64(adjustedBalance) : 0.0 } - // RESTORED: Enhanced updateInterestIndices with deposit capacity update + // Enhanced updateInterestIndices with deposit capacity update access(all) fun updateInterestIndices() { let currentTime = getCurrentBlock().timestamp let timeDelta = currentTime - self.lastUpdate @@ -303,7 +307,7 @@ access(all) contract TidalProtocol: FungibleToken { self.debitInterestIndex = TidalProtocol.compoundInterestIndex(oldIndex: self.debitInterestIndex, perSecondRate: self.currentDebitRate, elapsedSeconds: timeDelta) self.lastUpdate = currentTime - // RESTORED: Update deposit capacity based on time + // Update deposit capacity based on time let newDepositCapacity = self.depositCapacity + (self.depositRate * timeDelta) if newDepositCapacity >= self.depositCapacityCap { self.depositCapacity = self.depositCapacityCap @@ -312,13 +316,12 @@ access(all) contract TidalProtocol: FungibleToken { } } - // RESTORED: Deposit limit function from Dieter's implementation + // Deposit limit function access(all) fun depositLimit(): UFix64 { // Each deposit is limited to 5% of the total deposit capacity return self.depositCapacity * 0.05 } - // RESTORED: Rename to updateForTimeChange to match Dieter's implementation access(all) fun updateForTimeChange() { self.updateInterestIndices() } @@ -334,10 +337,10 @@ access(all) contract TidalProtocol: FungibleToken { let debitRate = self.interestCurve.interestRate(creditBalance: self.totalCreditBalance, debitBalance: self.totalDebitBalance) let debitIncome = self.totalDebitBalance * (1.0 + debitRate) - - // Calculate insurance amount (0.1% of credit balance) - let insuranceAmount = self.totalCreditBalance * 0.001 - + + // Calculate insurance amount (0.1% of credit balance) + let insuranceAmount = self.totalCreditBalance * 0.001 + // Calculate credit rate, ensuring we don't have underflows var creditRate: UFix64 = 0.0 if debitIncome >= insuranceAmount { @@ -347,84 +350,45 @@ access(all) contract TidalProtocol: FungibleToken { // but since we can't represent negative rates in our model, we'll use 0.0 creditRate = 0.0 } - + self.currentCreditRate = TidalProtocol.perSecondInterestRate(yearlyRate: creditRate) self.currentDebitRate = TidalProtocol.perSecondInterestRate(yearlyRate: debitRate) } - - // RESTORED: Parameterized init from Dieter's implementation - init(interestCurve: {InterestCurve}, depositRate: UFix64, depositCapacityCap: UFix64) { - self.lastUpdate = getCurrentBlock().timestamp - self.totalCreditBalance = 0.0 - self.totalDebitBalance = 0.0 - self.creditInterestIndex = 10000000000000000 - self.debitInterestIndex = 10000000000000000 - self.currentCreditRate = 10000000000000000 - self.currentDebitRate = 10000000000000000 - self.interestCurve = interestCurve - self.depositRate = depositRate - self.depositCapacity = depositCapacityCap - self.depositCapacityCap = depositCapacityCap - } } + /// Pool + /// + /// A Pool is the primary logic for protocol operations. It contains the global state of all positions, credit and + /// debit balances for each supported token type, and reserves as they are deposited to positions. access(all) resource Pool { - // A simple version number that is incremented whenever one or more interest indices - // are updated. This is used to detect when the interest indices need to be updated in - // InternalPositions. - access(EImplementation) var version: UInt64 - - // Global state for tracking each token + /// Global state for tracking each token access(self) var globalLedger: {Type: TokenState} - - // Individual user positions - RESTORED as resources per Dieter's design + /// Individual user positions access(self) var positions: @{UInt64: InternalPosition} - - // The actual reserves of each token + /// The actual reserves of each token access(self) var reserves: @{Type: {FungibleToken.Vault}} - - // Auto-incrementing position identifier counter + /// Auto-incrementing position identifier counter access(self) var nextPositionID: UInt64 - - // The default token type used as the "unit of account" for the pool. + /// The default token type used as the "unit of account" for the pool. access(self) let defaultToken: Type - - // RESTORED: Price oracle from Dieter's implementation - // A price oracle that will return the price of each token in terms of the default token. + /// A price oracle that will return the price of each token in terms of the default token. access(self) var priceOracle: {DFB.PriceOracle} - - // RESTORED: Position update queue from Dieter's implementation - access(EImplementation) var positionsNeedingUpdates: [UInt64] - access(self) var positionsProcessedPerCallback: UInt64 - - // RESTORED: Collateral and borrow factors from Dieter's implementation - // These dictionaries determine borrowing limits. Each token has a collateral factor and a - // borrow factor. - // - // When determining the total collateral amount that can be borrowed against, the value of the - // token (as given by the oracle) is multiplied by the collateral factor. So, a token with a - // collateral factor of 0.8 would only allow you to borrow 80% as much as if you had a the same - // value of a token with a collateral factor of 1.0. The total "effective collateral" for a - // position is the value of each token multiplied by its collateral factor. - // - // At the same time, the "borrow factor" determines if the user can borrow against all of that - // effective collateral, or if they can only borrow a portion of it to manage risk. + /// Together with borrowFactor, collateralFactor determines borrowing limits for each token + /// When determining the withdrawable loan amount, the value of the token (provided by the PriceOracle) is + /// multiplied by the collateral factor. The total "effective collateral" for a position is the value of each + /// token deposited to the position multiplied by its collateral factor access(self) var collateralFactor: {Type: UFix64} + /// Together with collateralFactor, borrowFactor determines borrowing limits for each token + /// The borrowFactor determines how much of a position's "effective collateral" can be borrowed against as a + /// percentage between 0.0 and 1.0 access(self) var borrowFactor: {Type: UFix64} - - // REMOVED: Static exchange rates and liquidation thresholds - // These have been replaced by dynamic oracle pricing and risk factors - - // RESTORED: tokenState() helper function from Dieter's implementation - // A convenience function that returns a reference to a particular token state, making sure - // it's up-to-date for the passage of time. This should always be used when accessing a token - // state to avoid missing interest updates (duplicate calls to updateForTimeChange() are a nop - // within a single block). - access(self) fun tokenState(type: Type): auth(EImplementation) &TokenState { - let state = &self.globalLedger[type]! as auth(EImplementation) &TokenState - state.updateForTimeChange() - return state - } + /// The count of positions to update per asynchronous update + access(self) var positionsProcessedPerCallback: UInt64 + /// Position update queue to be processed as an asynchronous update + access(EImplementation) var positionsNeedingUpdates: [UInt64] + /// A simple version number that is incremented whenever one or more interest indices are updated. This is used + /// to detect when the interest indices need to be updated in InternalPositions. + access(EImplementation) var version: UInt64 init(defaultToken: Type, priceOracle: {DFB.PriceOracle}) { pre { @@ -452,862 +416,840 @@ access(all) contract TidalProtocol: FungibleToken { // Vaults will be added when tokens are first deposited } - // Add a new token type to the pool - // This function should only be called by governance in the future - access(EGovernance) fun addSupportedToken( - tokenType: Type, - collateralFactor: UFix64, - borrowFactor: UFix64, - interestCurve: {InterestCurve}, - depositRate: UFix64, - depositCapacityCap: UFix64 - ) { - pre { - self.globalLedger[tokenType] == nil: "Token type already supported" - tokenType.isSubtype(of: Type<@{FungibleToken.Vault}>()): - "Invalid token type \(tokenType.identifier) - tokenType must be a FungibleToken Vault implementation" - collateralFactor > 0.0 && collateralFactor <= 1.0: "Collateral factor must be between 0 and 1" - borrowFactor > 0.0 && borrowFactor <= 1.0: "Borrow factor must be between 0 and 1" - depositRate > 0.0: "Deposit rate must be positive" - depositCapacityCap > 0.0: "Deposit capacity cap must be positive" - } - - // Add token to global ledger with its interest curve and deposit parameters - self.globalLedger[tokenType] = TokenState( - interestCurve: interestCurve, - depositRate: depositRate, - depositCapacityCap: depositCapacityCap - ) - - // Set collateral factor (what percentage of value can be used as collateral) - self.collateralFactor[tokenType] = collateralFactor - - // Set borrow factor (risk adjustment for borrowed amounts) - self.borrowFactor[tokenType] = borrowFactor - } + /////////////// + // GETTERS + /////////////// - // Get supported token types - access(all) fun getSupportedTokens(): [Type] { + /// Returns an array of the supported token Types + access(all) view fun getSupportedTokens(): [Type] { return self.globalLedger.keys } - // Check if a token type is supported - access(all) fun isTokenSupported(tokenType: Type): Bool { + /// Returns whether a given token Type is supported or not + access(all) view fun isTokenSupported(tokenType: Type): Bool { return self.globalLedger[tokenType] != nil } - access(EPosition) fun deposit(pid: UInt64, funds: @{FungibleToken.Vault}) { - pre { - self.positions[pid] != nil: "Invalid position ID" - self.globalLedger[funds.getType()] != nil: "Invalid token type" - funds.balance > 0.0: "Deposit amount must be positive" + /// Returns the current reserve balance for the specified token type. + access(all) view fun reserveBalance(type: Type): UFix64 { + let vaultRef = (&self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?) + if vaultRef == nil { + return 0.0 } + return vaultRef!.balance + } - // Get a reference to the user's position and global token state for the affected token. - let type = funds.getType() - let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! - let tokenState = self.tokenState(type: type) - - // If this position doesn't currently have an entry for this token, create one. - if position.balances[type] == nil { - position.balances[type] = InternalBalance() - } + /// Returns a position's balance available for withdrawal of a given Vault type. If pullFromTopUpSource is true, + /// the calculation will be made assuming the position is topped up if the withdrawal amount puts the Position + /// below its min health. If pullFromTopUpSource is true, the calculation will return the balance currently + /// available without topping up the position. + access(all) fun availableBalance(pid: UInt64, type: Type, pullFromTopUpSource: Bool): UFix64 { + let position = self._borrowPosition(pid: pid) - // Update the global interest indices on the affected token to reflect the passage of time. - // REMOVED: This is now handled by tokenState() helper function - // tokenState.updateInterestIndices() + if pullFromTopUpSource && position.topUpSource != nil { + let topUpSource = position.topUpSource! + let sourceType = topUpSource.getSourceType() + let sourceAmount = topUpSource.minimumAvailable() - // CHANGE: Create vault if it doesn't exist yet - if self.reserves[type] == nil { - self.reserves[type] <-! funds.createEmptyVault() + return self.fundsAvailableAboveTargetHealthAfterDepositing( + pid: pid, + withdrawType: type, + targetHealth: position.minHealth, + depositType: sourceType, + depositAmount: sourceAmount + ) + } else { + return self.fundsAvailableAboveTargetHealth( + pid: pid, + type: type, + targetHealth: position.minHealth + ) } - let reserveVault = (&self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! + } - // Reflect the deposit in the position's balance - position.balances[type]!.recordDeposit(amount: funds.balance, tokenState: tokenState) + /// 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 + access(all) fun positionHealth(pid: UInt64): UFix64 { + let position = self._borrowPosition(pid: pid) - // Update the internal interest rate to reflect the new credit balance - tokenState.updateInterestRates() + // Get the position's collateral and debt values in terms of the default token. + var effectiveCollateral = 0.0 + var effectiveDebt = 0.0 - // Add the money to the reserves - reserveVault.deposit(from: <-funds) - } + for type in position.balances.keys { + let balance = position.balances[type]! + let tokenState = self._borrowUpdatedTokenState(type: type) + if balance.direction == BalanceDirection.Credit { + let trueBalance = TidalProtocol.scaledBalanceToTrueBalance(scaledBalance: balance.scaledBalance, + interestIndex: tokenState.creditInterestIndex) - // RESTORED: Public deposit function from Dieter's implementation - // Allows anyone to deposit funds into any position - access(all) fun depositToPosition(pid: UInt64, from: @{FungibleToken.Vault}) { - self.depositAndPush(pid: pid, from: <-from, pushToDrawDownSink: false) - } + let tokenPrice = self.priceOracle.price(ofToken: type)! + let value = tokenPrice * trueBalance + effectiveCollateral = effectiveCollateral + (value * self.collateralFactor[type]!) + } else { + let trueBalance = TidalProtocol.scaledBalanceToTrueBalance(scaledBalance: balance.scaledBalance, + interestIndex: tokenState.debitInterestIndex) - // RESTORED: Enhanced deposit with queue processing and rebalancing from Dieter's implementation - access(EPosition) fun depositAndPush(pid: UInt64, from: @{FungibleToken.Vault}, pushToDrawDownSink: Bool) { - pre { - self.positions[pid] != nil: "Invalid position ID" - self.globalLedger[from.getType()] != nil: "Invalid token type" + let tokenPrice = self.priceOracle.price(ofToken: type)! + let value = tokenPrice * trueBalance + effectiveDebt = effectiveDebt + (value / self.borrowFactor[type]!) + } } - if from.balance == 0.0 { - destroy from - return + // Calculate the health as the ratio of collateral to debt. + if effectiveDebt == 0.0 { + return 1.0 } + return effectiveCollateral / effectiveDebt + } - // Get a reference to the user's position and global token state for the affected token. - let type = from.getType() - let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! - let tokenState = self.tokenState(type: type) + /// Returns the quantity of funds of a specified token which would need to be deposited to bring the position to + /// the provided target health. This function will return 0.0 if the position is already at or over that health + /// value. + access(all) fun fundsRequiredForTargetHealth(pid: UInt64, type: Type, targetHealth: UFix64): UFix64 { + return self.fundsRequiredForTargetHealthAfterWithdrawing( + pid: pid, + depositType: type, + targetHealth: targetHealth, + withdrawType: self.defaultToken, + withdrawAmount: 0.0 + ) + } - // Update time-based state - // REMOVED: This is now handled by tokenState() helper function - // tokenState.updateForTimeChange() + /// Returns the details of a given position as a PositionDetails external struct + access(all) fun getPositionDetails(pid: UInt64): PositionDetails { + let position = self._borrowPosition(pid: pid) + let balances: [PositionBalance] = [] - // RESTORED: Deposit rate limiting from Dieter's implementation - let depositAmount = from.balance - let depositLimit = tokenState.depositLimit() - - if depositAmount > depositLimit { - // The deposit is too big, so we need to queue the excess - let queuedDeposit <- from.withdraw(amount: depositAmount - depositLimit) - - if position.queuedDeposits[type] == nil { - position.queuedDeposits[type] <-! queuedDeposit - } else { - position.queuedDeposits[type]!.deposit(from: <-queuedDeposit) - } - } - - // If this position doesn't currently have an entry for this token, create one. - if position.balances[type] == nil { - position.balances[type] = InternalBalance() - } - - // CHANGE: Create vault if it doesn't exist yet - if self.reserves[type] == nil { - self.reserves[type] <-! from.createEmptyVault() - } - let reserveVault = (&self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! - - // Reflect the deposit in the position's balance - position.balances[type]!.recordDeposit(amount: from.balance, tokenState: tokenState) - - // Add the money to the reserves - reserveVault.deposit(from: <-from) + for type in position.balances.keys { + let balance = position.balances[type]! + let tokenState = self._borrowUpdatedTokenState(type: type) + let trueBalance = balance.direction == BalanceDirection.Credit + ? TidalProtocol.scaledBalanceToTrueBalance(scaledBalance: balance.scaledBalance, interestIndex: tokenState.creditInterestIndex) + : TidalProtocol.scaledBalanceToTrueBalance(scaledBalance: balance.scaledBalance, interestIndex: tokenState.debitInterestIndex) - // RESTORED: Rebalancing and queue management - if pushToDrawDownSink { - self.rebalancePosition(pid: pid, force: true) + balances.append(PositionBalance( + vaultType: type, + direction: balance.direction, + balance: trueBalance + )) } - self.queuePositionForUpdateIfNecessary(pid: pid) - } + let health = self.positionHealth(pid: pid) + let defaultTokenAvailable = self.availableBalance(pid: pid, type: self.defaultToken, pullFromTopUpSource: false) - access(EPosition) fun withdraw(pid: UInt64, amount: UFix64, type: Type): @{FungibleToken.Vault} { - // RESTORED: Call the enhanced function with pullFromTopUpSource = false for backward compatibility - return <- self.withdrawAndPull(pid: pid, type: type, amount: amount, pullFromTopUpSource: false) + return PositionDetails( + balances: balances, + poolDefaultToken: self.defaultToken, + defaultTokenAvailableBalance: defaultTokenAvailable, + health: health + ) } - // RESTORED: Enhanced withdraw with top-up source integration from Dieter's implementation - access(EPosition) fun withdrawAndPull( - pid: UInt64, - type: Type, - amount: UFix64, - pullFromTopUpSource: Bool - ): @{FungibleToken.Vault} { - pre { - self.positions[pid] != nil: "Invalid position ID" - self.globalLedger[type] != nil: "Invalid token type" - amount > 0.0: "Withdrawal amount must be positive" + /// Returns the quantity of funds of a specified token which would need to be deposited in order to bring the + /// position to the target health assuming we also withdraw a specified amount of another token. This function + /// will return 0.0 if the position would already be at or over the target health value after the proposed + /// withdrawal. + access(all) fun fundsRequiredForTargetHealthAfterWithdrawing( + pid: UInt64, + depositType: Type, + targetHealth: UFix64, + withdrawType: Type, + withdrawAmount: UFix64 + ): UFix64 { + if depositType == withdrawType && withdrawAmount > 0.0 { + // If the deposit and withdrawal types are the same, we compute the required deposit assuming + // no withdrawal (which is less work) and increase that by the withdraw amount at the end + return self.fundsRequiredForTargetHealth(pid: pid, type: depositType, targetHealth: targetHealth) + withdrawAmount } - // Get a reference to the user's position and global token state for the affected token. - let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! - let tokenState = self.tokenState(type: type) - - // Update the global interest indices on the affected token to reflect the passage of time. - // REMOVED: This is now handled by tokenState() helper function - // tokenState.updateForTimeChange() - - // RESTORED: Top-up source integration from Dieter's implementation - // Preflight to see if the funds are available - let topUpSource = position.topUpSource as! auth(FungibleToken.Withdraw) &{DFB.Source}? - let topUpType = topUpSource?.getSourceType() ?? self.defaultToken + let balanceSheet = self._getUpdatedBalanceSheet(pid: pid) + let position = self._borrowPosition(pid: pid) - let requiredDeposit = self.fundsRequiredForTargetHealthAfterWithdrawing( - pid: pid, - depositType: topUpType, - targetHealth: position.minHealth, - withdrawType: type, - withdrawAmount: amount - ) + var effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral + var effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt - var canWithdraw = false + if withdrawAmount != 0.0 { + if position.balances[withdrawType] == nil || position.balances[withdrawType]!.direction == BalanceDirection.Debit { + // If the position doesn't have any collateral for the withdrawn token, we can just compute how much + // additional effective debt the withdrawal will create. + effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt + + (withdrawAmount * self.priceOracle.price(ofToken: withdrawType)! / self.borrowFactor[withdrawType]!) + } else { + let withdrawTokenState = self._borrowUpdatedTokenState(type: withdrawType) + // REMOVED: This is now handled by tokenState() helper function + // withdrawTokenState.updateForTimeChange() - if requiredDeposit == 0.0 { - // We can service this withdrawal without any top up - canWithdraw = true - } else { - // We need more funds to service this withdrawal, see if they are available from the top up source - if pullFromTopUpSource && topUpSource != nil { - // If we have to rebalance, let's try to rebalance to the target health, not just the minimum - let idealDeposit = self.fundsRequiredForTargetHealthAfterWithdrawing( - pid: pid, - depositType: topUpType, - targetHealth: position.targetHealth, - withdrawType: type, - withdrawAmount: amount + // The user has a collateral position in the given token, we need to figure out if this withdrawal + // will flip over into debt, or just draw down the collateral. + let collateralBalance = position.balances[withdrawType]!.scaledBalance + let trueCollateral = TidalProtocol.scaledBalanceToTrueBalance( + scaledBalance: collateralBalance, + interestIndex: withdrawTokenState.creditInterestIndex ) - let pulledVault <- (topUpSource!).withdrawAvailable(maxAmount: idealDeposit) - - // NOTE: We requested the "ideal" deposit, but we compare against the required deposit here. - // The top up source may not have enough funds get us to the target health, but could have - // enough to keep us over the minimum. - if pulledVault.balance >= requiredDeposit { - // We can service this withdrawal if we deposit funds from our top up source - self.depositAndPush(pid: pid, from: <-pulledVault, pushToDrawDownSink: false) - canWithdraw = true + if trueCollateral >= withdrawAmount { + // This withdrawal will draw down collateral, but won't create debt, we just need to account + // for the collateral decrease. + effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral - + (withdrawAmount * self.priceOracle.price(ofToken: withdrawType)! * self.collateralFactor[withdrawType]!) } else { - // We can't get the funds required to service this withdrawal, so we need to redeposit what we got - self.depositAndPush(pid: pid, from: <-pulledVault, pushToDrawDownSink: false) + // The withdrawal will wipe out all of the collateral, and create some debt. + effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt + + ((withdrawAmount - trueCollateral) * self.priceOracle.price(ofToken: withdrawType)! / self.borrowFactor[withdrawType]!) + + effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral - + (trueCollateral * self.priceOracle.price(ofToken: withdrawType)! * self.collateralFactor[withdrawType]!) } } } - if !canWithdraw { - // We can't service this withdrawal, so we just abort - panic("Cannot withdraw \(amount) of \(type.identifier) from position ID \(pid) - Insufficient funds for withdrawal") - } + // We now have new effective collateral and debt values that reflect the proposed withdrawal (if any!) + // Now we can figure out how many of the given token would need to be deposited to bring the position + // to the target health value. + var healthAfterWithdrawal = TidalProtocol.healthComputation( + effectiveCollateral: effectiveCollateralAfterWithdrawal, + effectiveDebt: effectiveDebtAfterWithdrawal + ) - // If this position doesn't currently have an entry for this token, create one. - if position.balances[type] == nil { - position.balances[type] = InternalBalance() + if healthAfterWithdrawal >= targetHealth { + // The position is already at or above the target health, so we don't need to deposit anything. + return 0.0 } - let reserveVault = (&self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! - - // Reflect the withdrawal in the position's balance - position.balances[type]!.recordWithdrawal(amount: amount, tokenState: tokenState) + // For situations where the required deposit will BOTH pay off debt and accumulate collateral, we keep + // track of the number of tokens that went towards paying off debt. + var debtTokenCount = 0.0 - // Ensure that this withdrawal doesn't cause the position to be overdrawn. - assert(self.positionHealth(pid: pid) >= 1.0, message: "Position is overdrawn") + if position.balances[depositType] != nil && position.balances[depositType]!.direction == BalanceDirection.Debit { + // The user has a debt position in the given token, we start by looking at the health impact of paying off + // the entire debt. + let depositTokenState = self._borrowUpdatedTokenState(type: depositType) + // REMOVED: This is now handled by tokenState() helper function + // depositTokenState.updateForTimeChange() + let debtBalance = position.balances[depositType]!.scaledBalance + let trueDebt = TidalProtocol.scaledBalanceToTrueBalance( + scaledBalance: debtBalance, + interestIndex: depositTokenState.debitInterestIndex + ) + let debtEffectiveValue = self.priceOracle.price(ofToken: depositType)! * trueDebt / self.borrowFactor[depositType]! - // Queue for update if necessary - self.queuePositionForUpdateIfNecessary(pid: pid) + // Check what the new health would be if we paid off all of this debt + let potentialHealth = TidalProtocol.healthComputation( + effectiveCollateral: effectiveCollateralAfterWithdrawal, + effectiveDebt: effectiveDebtAfterWithdrawal - debtEffectiveValue + ) - return <- reserveVault.withdraw(amount: amount) - } + // Does paying off all of the debt reach the target health? Then we're done. + if potentialHealth >= targetHealth { + // We can reach the target health by paying off some or all of the debt. We can easily + // compute how many units of the token would be needed to reach the target health. + let healthChange = targetHealth - healthAfterWithdrawal + let requiredEffectiveDebt = healthChange * effectiveCollateralAfterWithdrawal / (targetHealth * targetHealth) - // RESTORED: Position queue management from Dieter's implementation - access(self) fun queuePositionForUpdateIfNecessary(pid: UInt64) { - if self.positionsNeedingUpdates.contains(pid) { - // If this position is already queued for an update, no need to check anything else - return - } else { - // If this position is not already queued for an update, we need to check if it needs one - let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! + // The amount of the token to pay back, in units of the token. + let paybackAmount = requiredEffectiveDebt * self.borrowFactor[depositType]! / self.priceOracle.price(ofToken: depositType)! - if position.queuedDeposits.length > 0 { - // This position has deposits that need to be processed, so we need to queue it for an update - self.positionsNeedingUpdates.append(pid) - return + return paybackAmount + } else { + // We can pay off the entire debt, but we still need to deposit more to reach the target health. + // We have logic below that can determine the collateral deposition required to reach the target health + // from this new health position. Rather than copy that logic here, we fall through into it. But first + // we have to record the amount of tokens that went towards debt payback and adjust the effective + // debt to reflect that it has been paid off. + debtTokenCount = trueDebt + effectiveDebtAfterWithdrawal = effectiveDebtAfterWithdrawal - debtEffectiveValue + healthAfterWithdrawal = potentialHealth } + } - let positionHealth = self.positionHealth(pid: pid) + // At this point, we're either dealing with a position that didn't have a debt position in the deposit + // token, or we've accounted for the debt payoff and adjusted the effective debt above. - if positionHealth < position.minHealth || positionHealth > position.maxHealth { - // This position is outside the configured health bounds, we queue it for an update - self.positionsNeedingUpdates.append(pid) - return - } - } + // Now we need to figure out how many tokens would need to be deposited (as collateral) to reach the + // target health. We can rearrange the health equation to solve for the required collateral: + // targetHealth = effectiveCollateral / effectiveDebt + // targetHealth * effectiveDebt = effectiveCollateral + // requiredCollateral = targetHealth * effectiveDebtAfterWithdrawal + + // We need to increase the effective collateral from its current value to the required value, so we + // multiply the required health change by the effective debt, and turn that into a token amount. + let healthChange = targetHealth - healthAfterWithdrawal + let requiredEffectiveCollateral = healthChange * effectiveDebtAfterWithdrawal + + // The amount of the token to deposit, in units of the token. + let collateralTokenCount = requiredEffectiveCollateral / self.priceOracle.price(ofToken: depositType)! / self.collateralFactor[depositType]! + + // debtTokenCount is the number of tokens that went towards debt, zero if there was no debt. + return collateralTokenCount + debtTokenCount } - // RESTORED: Position rebalancing from Dieter's implementation - // Rebalances the position to the target health value. If force is true, the position will be - // rebalanced even if it is currently healthy, otherwise, this function will do nothing if the - // position is within the min/max health bounds. - access(EPosition) fun rebalancePosition(pid: UInt64, force: Bool) { - let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! - let balanceSheet = self.positionBalanceSheet(pid: pid) + /// Returns the quantity of the specified token that could be withdrawn while still keeping the position's + /// health at or above the provided target. + access(all) fun fundsAvailableAboveTargetHealth(pid: UInt64, type: Type, targetHealth: UFix64): UFix64 { + return self.fundsAvailableAboveTargetHealthAfterDepositing( + pid: pid, + withdrawType: type, + targetHealth: targetHealth, + depositType: self.defaultToken, + depositAmount: 0.0 + ) + } - if !force && (balanceSheet.health >= position.minHealth && balanceSheet.health <= position.maxHealth) { - // We aren't forcing the update, and the position is already between its desired min and max. Nothing to do! - return + /// Returns the quantity of the specified token that could be withdrawn while still keeping the position's health + /// at or above the provided target, assuming we also deposit a specified amount of another token. + access(all) fun fundsAvailableAboveTargetHealthAfterDepositing( + pid: UInt64, + withdrawType: Type, + targetHealth: UFix64, + depositType: Type, + depositAmount: UFix64 + ): UFix64 { + if depositType == withdrawType && depositAmount > 0.0 { + // If the deposit and withdrawal types are the same, we compute the available funds assuming + // no deposit (which is less work) and increase that by the deposit amount at the end + return self.fundsAvailableAboveTargetHealth(pid: pid, type: withdrawType, targetHealth: targetHealth) + depositAmount } - if balanceSheet.health < position.targetHealth { - // The position is undercollateralized, see if the source can get more collateral to bring it up to the target health. - if position.topUpSource != nil { - let topUpSource = position.topUpSource! - let idealDeposit = self.fundsRequiredForTargetHealth( - pid: pid, - type: topUpSource.getSourceType(), - targetHealth: position.targetHealth - ) + let balanceSheet = self._getUpdatedBalanceSheet(pid: pid) + let position = self._borrowPosition(pid: pid) - let pulledVault <- topUpSource.withdrawAvailable(maxAmount: idealDeposit) - self.depositAndPush(pid: pid, from: <-pulledVault, pushToDrawDownSink: false) - } - } else if balanceSheet.health > position.targetHealth { - // The position is overcollateralized, we'll withdraw funds to match the target health and offer it to the sink. - if position.drawDownSink != nil { - let drawDownSink = position.drawDownSink! - let sinkType = drawDownSink.getSinkType() - let idealWithdrawal = self.fundsAvailableAboveTargetHealth( - pid: pid, - type: sinkType, - targetHealth: position.targetHealth + var effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral + var effectiveDebtAfterDeposit = balanceSheet.effectiveDebt + + if depositAmount != 0.0 { + if position.balances[depositType] == nil || position.balances[depositType]!.direction == BalanceDirection.Credit { + // If there's no debt for the deposit token, we can just compute how much additional effective collateral the deposit will create. + effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral + + (depositAmount * self.priceOracle.price(ofToken: depositType)! * self.collateralFactor[depositType]!) + } else { + let depositTokenState = self._borrowUpdatedTokenState(type: depositType) + + // The user has a debt position in the given token, we need to figure out if this deposit + // will result in net collateral, or just bring down the debt. + let debtBalance = position.balances[depositType]!.scaledBalance + let trueDebt = TidalProtocol.scaledBalanceToTrueBalance( + scaledBalance: debtBalance, + interestIndex: depositTokenState.debitInterestIndex ) - // Compute how many tokens of the sink's type are available to hit our target health. - let sinkCapacity = drawDownSink.minimumCapacity() - let sinkAmount = (idealWithdrawal > sinkCapacity) ? sinkCapacity : idealWithdrawal - - if sinkAmount > 0.0 { - let sinkVault <- self.withdrawAndPull( - pid: pid, - type: sinkType, - amount: sinkAmount, - pullFromTopUpSource: false - ) + if trueDebt >= depositAmount { + // This deposit will pay down some debt, but won't result in net collateral, we + // just need to account for the debt decrease. + effectiveDebtAfterDeposit = balanceSheet.effectiveDebt - + (depositAmount * self.priceOracle.price(ofToken: depositType)! / self.borrowFactor[depositType]!) + } else { + // The deposit will wipe out all of the debt, and create some collateral. + effectiveDebtAfterDeposit = balanceSheet.effectiveDebt - + (trueDebt * self.priceOracle.price(ofToken: depositType)! / self.borrowFactor[depositType]!) - // Push what we can into the sink, and redeposit the rest - drawDownSink.depositCapacity(from: &sinkVault as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) - - if sinkVault.balance > 0.0 { - self.depositAndPush(pid: pid, from: <-sinkVault, pushToDrawDownSink: false) - } else { - destroy sinkVault - } + effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral + + ((depositAmount - trueDebt) * self.priceOracle.price(ofToken: depositType)! * self.collateralFactor[depositType]!) } } } - } - // RESTORED: Provider functions for sink/source from Dieter's implementation - access(EPosition) fun provideDrawDownSink(pid: UInt64, sink: {DFB.Sink}?) { - let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! - position.setDrawDownSink(sink) - } - - access(EPosition) fun provideTopUpSource(pid: UInt64, source: {DFB.Source}?) { - let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! - position.setTopUpSource(source) - } + // We now have new effective collateral and debt values that reflect the proposed deposit (if any!) + // Now we can figure out how many of the withdrawal token are available while keeping the position + // at or above the target health value. + var healthAfterDeposit = TidalProtocol.healthComputation( + effectiveCollateral: effectiveCollateralAfterDeposit, + effectiveDebt: effectiveDebtAfterDeposit + ) - // RESTORED: Available balance with source integration from Dieter's implementation - access(all) fun availableBalance(pid: UInt64, type: Type, pullFromTopUpSource: Bool): UFix64 { - let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! + if healthAfterDeposit <= targetHealth { + // The position is already at or below the target health, so we can't withdraw anything. + return 0.0 + } - if pullFromTopUpSource && position.topUpSource != nil { - let topUpSource = position.topUpSource! - let sourceType = topUpSource.getSourceType() - let sourceAmount = topUpSource.minimumAvailable() + // For situations where the available withdrawal will BOTH draw down collateral and create debt, we keep + // track of the number of tokens that are available from collateral + var collateralTokenCount = 0.0 - return self.fundsAvailableAboveTargetHealthAfterDepositing( - pid: pid, - withdrawType: type, - targetHealth: position.minHealth, - depositType: sourceType, - depositAmount: sourceAmount - ) - } else { - return self.fundsAvailableAboveTargetHealth( - pid: pid, - type: type, - targetHealth: position.minHealth + if position.balances[withdrawType] != nil && position.balances[withdrawType]!.direction == BalanceDirection.Credit { + // The user has a credit position in the withdraw token, we start by looking at the health impact of pulling out all + // of that collateral + let withdrawTokenState = self._borrowUpdatedTokenState(type: withdrawType) + // REMOVED: This is now handled by tokenState() helper function + // withdrawTokenState.updateForTimeChange() + let creditBalance = position.balances[withdrawType]!.scaledBalance + let trueCredit = TidalProtocol.scaledBalanceToTrueBalance( + scaledBalance: creditBalance, + interestIndex: withdrawTokenState.creditInterestIndex ) - } - } + let collateralEffectiveValue = self.priceOracle.price(ofToken: withdrawType)! * trueCredit * self.collateralFactor[withdrawType]! - // Returns the health of the given position, which is the ratio of the position's effective collateral - // to its debt (as denominated in the default token). ("Effective collateral" means the - // value of each credit balance times the liquidation threshold for that token. i.e. the maximum borrowable amount) - access(all) fun positionHealth(pid: UInt64): UFix64 { - let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! + // Check what the new health would be if we took out all of this collateral + let potentialHealth = TidalProtocol.healthComputation( + effectiveCollateral: effectiveCollateralAfterDeposit - collateralEffectiveValue, + effectiveDebt: effectiveDebtAfterDeposit + ) - // Get the position's collateral and debt values in terms of the default token. - var effectiveCollateral = 0.0 - var effectiveDebt = 0.0 + // Does drawing down all of the collateral go below the target health? Then the max withdrawal comes from collateral only. + if potentialHealth <= targetHealth { + // We will hit the health target before using up all of the withdraw token credit. We can easily + // compute how many units of the token would bring the position down to the target health. + let availableHealth = healthAfterDeposit - targetHealth + let availableEffectiveValue = effectiveDebtAfterDeposit == 0.0 ? effectiveCollateralAfterDeposit : availableHealth * effectiveDebtAfterDeposit - for type in position.balances.keys { - let balance = position.balances[type]! - let tokenState = self.tokenState(type: type) - if balance.direction == BalanceDirection.Credit { - let trueBalance = TidalProtocol.scaledBalanceToTrueBalance(scaledBalance: balance.scaledBalance, - interestIndex: tokenState.creditInterestIndex) + // The amount of the token we can take using that amount of health + let availableTokenCount = availableEffectiveValue / self.collateralFactor[withdrawType]! / self.priceOracle.price(ofToken: withdrawType)! - // RESTORED: Oracle-based pricing from Dieter's implementation - let tokenPrice = self.priceOracle.price(ofToken: type)! - let value = tokenPrice * trueBalance - effectiveCollateral = effectiveCollateral + (value * self.collateralFactor[type]!) + return availableTokenCount } else { - let trueBalance = TidalProtocol.scaledBalanceToTrueBalance(scaledBalance: balance.scaledBalance, - interestIndex: tokenState.debitInterestIndex) - - // RESTORED: Oracle-based pricing for debt calculation - let tokenPrice = self.priceOracle.price(ofToken: type)! - let value = tokenPrice * trueBalance - effectiveDebt = effectiveDebt + (value / self.borrowFactor[type]!) + // We can flip this credit position into a debit position, before hitting the target health. + // We have logic below that can determine health changes for debit positions. Rather than copy that here, + // fall through into it. But first we have to record the amount of tokens that are available as collateral + // and then adjust the effective collateral to reflect that it has come out + collateralTokenCount = trueCredit + effectiveCollateralAfterDeposit = effectiveCollateralAfterDeposit - collateralEffectiveValue + // NOTE: The above invalidates the healthAfterDeposit value, but it's not used below... } } - // Calculate the health as the ratio of collateral to debt. - if effectiveDebt == 0.0 { - return 1.0 - } - return effectiveCollateral / effectiveDebt + // At this point, we're either dealing with a position that didn't have a credit balance in the withdraw + // token, or we've accounted for the credit balance and adjusted the effective collateral above. + + // We can calculate the available debt increase that would bring us to the target health + var availableDebtIncrease = (effectiveCollateralAfterDeposit / targetHealth) - effectiveDebtAfterDeposit + + let availableTokens = availableDebtIncrease * self.borrowFactor[withdrawType]! / self.priceOracle.price(ofToken: withdrawType)! + + return availableTokens + collateralTokenCount } - // RESTORED: Position balance sheet calculation from Dieter's implementation - access(self) fun positionBalanceSheet(pid: UInt64): BalanceSheet { - let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! - let priceOracle = &self.priceOracle as &{DFB.PriceOracle} + /// 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): UFix64 { + let balanceSheet = self._getUpdatedBalanceSheet(pid: pid) + let position = self._borrowPosition(pid: pid) + let tokenState = self._borrowUpdatedTokenState(type: type) - // Get the position's collateral and debt values in terms of the default token. - var effectiveCollateral = 0.0 - var effectiveDebt = 0.0 + var effectiveCollateralIncrease = 0.0 + var effectiveDebtDecrease = 0.0 - for type in position.balances.keys { - let balance = position.balances[type]! - let tokenState = self.tokenState(type: type) - if balance.direction == BalanceDirection.Credit { - let trueBalance = TidalProtocol.scaledBalanceToTrueBalance(scaledBalance: balance.scaledBalance, - interestIndex: tokenState.creditInterestIndex) - - let value = priceOracle.price(ofToken: type)! * trueBalance + if position.balances[type] == nil || position.balances[type]!.direction == BalanceDirection.Credit { + // Since the user has no debt in the given token, we can just compute how much + // additional collateral this deposit will create. + effectiveCollateralIncrease = amount * self.priceOracle.price(ofToken: type)! * self.collateralFactor[type]! + } else { + // The user has a debit position in the given token, we need to figure out if this deposit + // will only pay off some of the debt, or if it will also create new collateral. + let debtBalance = position.balances[type]!.scaledBalance + let trueDebt = TidalProtocol.scaledBalanceToTrueBalance( + scaledBalance: debtBalance, + interestIndex: tokenState.debitInterestIndex + ) - effectiveCollateral = effectiveCollateral + (value * self.collateralFactor[type]!) + if trueDebt >= amount { + // This deposit will wipe out some or all of the debt, but won't create new collateral, we + // just need to account for the debt decrease. + effectiveDebtDecrease = amount * self.priceOracle.price(ofToken: type)! / self.borrowFactor[type]! } else { - let trueBalance = TidalProtocol.scaledBalanceToTrueBalance(scaledBalance: balance.scaledBalance, - interestIndex: tokenState.debitInterestIndex) + // This deposit will wipe out all of the debt, and create new collateral. + effectiveDebtDecrease = trueDebt * self.priceOracle.price(ofToken: type)! / self.borrowFactor[type]! + effectiveCollateralIncrease = (amount - trueDebt) * self.priceOracle.price(ofToken: type)! * self.collateralFactor[type]! + } + } - let value = priceOracle.price(ofToken: type)! * trueBalance + return TidalProtocol.healthComputation( + effectiveCollateral: balanceSheet.effectiveCollateral + effectiveCollateralIncrease, + effectiveDebt: balanceSheet.effectiveDebt - effectiveDebtDecrease + ) + } + + // 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). + access(all) fun healthAfterWithdrawal(pid: UInt64, type: Type, amount: UFix64): UFix64 { + let balanceSheet = self._getUpdatedBalanceSheet(pid: pid) + let position = self._borrowPosition(pid: pid) + let tokenState = self._borrowUpdatedTokenState(type: type) + + var effectiveCollateralDecrease = 0.0 + var effectiveDebtIncrease = 0.0 + + if position.balances[type] == nil || position.balances[type]!.direction == BalanceDirection.Debit { + // The user has no credit position in the given token, we can just compute how much + // additional effective debt this withdrawal will create. + effectiveDebtIncrease = amount * self.priceOracle.price(ofToken: type)! / self.borrowFactor[type]! + } else { + // The user has a credit position in the given token, we need to figure out if this withdrawal + // will only draw down some of the collateral, or if it will also create new debt. + let creditBalance = position.balances[type]!.scaledBalance + let trueCredit = TidalProtocol.scaledBalanceToTrueBalance( + scaledBalance: creditBalance, + interestIndex: tokenState.creditInterestIndex + ) - effectiveDebt = effectiveDebt + (value / self.borrowFactor[type]!) + if trueCredit >= amount { + // This withdrawal will draw down some collateral, but won't create new debt, we + // just need to account for the collateral decrease. + effectiveCollateralDecrease = amount * self.priceOracle.price(ofToken: type)! * self.collateralFactor[type]! + } else { + // The withdrawal will wipe out all of the collateral, and create new debt. + effectiveDebtIncrease = (amount - trueCredit) * self.priceOracle.price(ofToken: type)! / self.borrowFactor[type]! + effectiveCollateralDecrease = trueCredit * self.priceOracle.price(ofToken: type)! * self.collateralFactor[type]! } } - return BalanceSheet(effectiveCollateral: effectiveCollateral, effectiveDebt: effectiveDebt) + return TidalProtocol.healthComputation( + effectiveCollateral: balanceSheet.effectiveCollateral - effectiveCollateralDecrease, + effectiveDebt: balanceSheet.effectiveDebt + effectiveDebtIncrease + ) } - access(all) fun createPosition(): UInt64 { + /////////////////////////// + // POSITION MANAGEMENT + /////////////////////////// + + /// Creates a lending position against the provided collateral funds, depositing the loaned amount to the + /// given Sink. If a Source is provided, the position will be configured to pull loan repayment when the loan + /// becomes undercollateralized, preferring repayment to outright liquidation. + access(all) fun createPosition( + funds: @{FungibleToken.Vault}, + issuanceSink: {DFB.Sink}, + repaymentSource: {DFB.Source}?, + pushToDrawDownSink: Bool + ): UInt64 { + pre { + self.globalLedger[funds.getType()] != nil: "Invalid token type \(funds.getType().identifier) - not supported by this Pool" + } + // construct a new InternalPosition, assigning it the current position ID let id = self.nextPositionID self.nextPositionID = self.nextPositionID + 1 self.positions[id] <-! create InternalPosition() - return id - } - // Helper function for testing – returns the current reserve balance for the specified token type. - access(all) fun reserveBalance(type: Type): UFix64 { - // CHANGE: Handle case where no vault exists yet for this token type - let vaultRef = (&self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?) - if vaultRef == nil { - return 0.0 - } - return vaultRef!.balance - } + emit Opened(pid: id, poolUUID: self.uuid) - // Add getPositionDetails function that's used by DFB implementations - access(all) fun getPositionDetails(pid: UInt64): PositionDetails { - let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! - let balances: [PositionBalance] = [] - - for type in position.balances.keys { - let balance = position.balances[type]! - let tokenState = self.tokenState(type: type) - - let trueBalance = balance.direction == BalanceDirection.Credit - ? TidalProtocol.scaledBalanceToTrueBalance(scaledBalance: balance.scaledBalance, interestIndex: tokenState.creditInterestIndex) - : TidalProtocol.scaledBalanceToTrueBalance(scaledBalance: balance.scaledBalance, interestIndex: tokenState.debitInterestIndex) - - balances.append(PositionBalance( - type: type, - direction: balance.direction, - balance: trueBalance - )) + // assign issuance & repayment connectors within the InternalPosition + let iPos = self._borrowPosition(pid: id) + let fundsType = funds.getType() + iPos.setDrawDownSink(issuanceSink) + if repaymentSource != nil { + iPos.setTopUpSource(repaymentSource) } - - let health = self.positionHealth(pid: pid) - - return PositionDetails( - balances: balances, - poolDefaultToken: self.defaultToken, - defaultTokenAvailableBalance: 0.0, // TODO: Calculate this properly - health: health + + // deposit the initial funds & return the position ID + self.depositAndPush( + pid: id, + from: <-funds, + pushToDrawDownSink: pushToDrawDownSink ) + return id } - // RESTORED: Advanced position health management functions from Dieter's implementation - - // The quantity of funds of a specified token which would need to be deposited to bring the - // position to the target health. This function will return 0.0 if the position is already at or over - // that health value. - access(all) fun fundsRequiredForTargetHealth(pid: UInt64, type: Type, targetHealth: UFix64): UFix64 { - return self.fundsRequiredForTargetHealthAfterWithdrawing( - pid: pid, - depositType: type, - targetHealth: targetHealth, - withdrawType: self.defaultToken, - withdrawAmount: 0.0 - ) + /// Allows anyone to deposit funds into any position. If the provided Vault is not supported by the Pool, the + /// operation reverts. + access(all) fun depositToPosition(pid: UInt64, from: @{FungibleToken.Vault}) { + self.depositAndPush(pid: pid, from: <-from, pushToDrawDownSink: false) } - // The quantity of funds of a specified token which would need to be deposited to bring the - // position to the target health assuming we also withdraw a specified amount of another - // token. This function will return 0.0 if the position would already be at or over the target - // health value after the proposed withdrawal. - access(all) fun fundsRequiredForTargetHealthAfterWithdrawing( - pid: UInt64, - depositType: Type, - targetHealth: UFix64, - withdrawType: Type, - withdrawAmount: UFix64 - ): UFix64 { - if depositType == withdrawType && withdrawAmount > 0.0 { - // If the deposit and withdrawal types are the same, we compute the required deposit assuming - // no withdrawal (which is less work) and increase that by the withdraw amount at the end - return self.fundsRequiredForTargetHealth(pid: pid, type: depositType, targetHealth: targetHealth) + withdrawAmount + /// Deposits the provided funds to the specified position with the configurable `pushToDrawDownSink` option. If + /// `pushToDrawDownSink` is true, excess value putting the position above its max health is pushed to the + /// position's configured `drawDownSink`. + access(EPosition) fun depositAndPush(pid: UInt64, from: @{FungibleToken.Vault}, pushToDrawDownSink: Bool) { + pre { + self.positions[pid] != nil: "Invalid position ID \(pid) - could not find an InternalPosition with the requested ID in the Pool" + self.globalLedger[from.getType()] != nil: "Invalid token type \(from.getType().identifier) - not supported by this Pool" } - let balanceSheet = self.positionBalanceSheet(pid: pid) - let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! + if from.balance == 0.0 { + Burner.burn(<-from) + return + } - var effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral - var effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt + // Get a reference to the user's position and global token state for the affected token. + let type = from.getType() + let position = self._borrowPosition(pid: pid) + let tokenState = self._borrowUpdatedTokenState(type: type) + let amount = from.balance + let depositedUUID = from.uuid - if withdrawAmount != 0.0 { - if position.balances[withdrawType] == nil || position.balances[withdrawType]!.direction == BalanceDirection.Debit { - // If the position doesn't have any collateral for the withdrawn token, we can just compute how much - // additional effective debt the withdrawal will create. - effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt + - (withdrawAmount * self.priceOracle.price(ofToken: withdrawType)! / self.borrowFactor[withdrawType]!) - } else { - let withdrawTokenState = self.tokenState(type: withdrawType) - // REMOVED: This is now handled by tokenState() helper function - // withdrawTokenState.updateForTimeChange() + // Update time-based state + // REMOVED: This is now handled by tokenState() helper function + // tokenState.updateForTimeChange() - // The user has a collateral position in the given token, we need to figure out if this withdrawal - // will flip over into debt, or just draw down the collateral. - let collateralBalance = position.balances[withdrawType]!.scaledBalance - let trueCollateral = TidalProtocol.scaledBalanceToTrueBalance( - scaledBalance: collateralBalance, - interestIndex: withdrawTokenState.creditInterestIndex - ) + // Deposit rate limiting + let depositAmount = from.balance + let depositLimit = tokenState.depositLimit() - if trueCollateral >= withdrawAmount { - // This withdrawal will draw down collateral, but won't create debt, we just need to account - // for the collateral decrease. - effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral - - (withdrawAmount * self.priceOracle.price(ofToken: withdrawType)! * self.collateralFactor[withdrawType]!) - } else { - // The withdrawal will wipe out all of the collateral, and create some debt. - effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt + - ((withdrawAmount - trueCollateral) * self.priceOracle.price(ofToken: withdrawType)! / self.borrowFactor[withdrawType]!) + if depositAmount > depositLimit { + // The deposit is too big, so we need to queue the excess + let queuedDeposit <- from.withdraw(amount: depositAmount - depositLimit) - effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral - - (trueCollateral * self.priceOracle.price(ofToken: withdrawType)! * self.collateralFactor[withdrawType]!) - } + if position.queuedDeposits[type] == nil { + position.queuedDeposits[type] <-! queuedDeposit + } else { + position.queuedDeposits[type]!.deposit(from: <-queuedDeposit) } } - // We now have new effective collateral and debt values that reflect the proposed withdrawal (if any!) - // Now we can figure out how many of the given token would need to be deposited to bring the position - // to the target health value. - var healthAfterWithdrawal = TidalProtocol.healthComputation( - effectiveCollateral: effectiveCollateralAfterWithdrawal, - effectiveDebt: effectiveDebtAfterWithdrawal - ) - - if healthAfterWithdrawal >= targetHealth { - // The position is already at or above the target health, so we don't need to deposit anything. - return 0.0 + // If this position doesn't currently have an entry for this token, create one. + if position.balances[type] == nil { + position.balances[type] = InternalBalance() } - // For situations where the required deposit will BOTH pay off debt and accumulate collateral, we keep - // track of the number of tokens that went towards paying off debt. - var debtTokenCount = 0.0 - - if position.balances[depositType] != nil && position.balances[depositType]!.direction == BalanceDirection.Debit { - // The user has a debt position in the given token, we start by looking at the health impact of paying off - // the entire debt. - let depositTokenState = self.tokenState(type: depositType) - // REMOVED: This is now handled by tokenState() helper function - // depositTokenState.updateForTimeChange() - - let debtBalance = position.balances[depositType]!.scaledBalance - let trueDebt = TidalProtocol.scaledBalanceToTrueBalance( - scaledBalance: debtBalance, - interestIndex: depositTokenState.debitInterestIndex - ) - let debtEffectiveValue = self.priceOracle.price(ofToken: depositType)! * trueDebt / self.borrowFactor[depositType]! - - // Check what the new health would be if we paid off all of this debt - let potentialHealth = TidalProtocol.healthComputation( - effectiveCollateral: effectiveCollateralAfterWithdrawal, - effectiveDebt: effectiveDebtAfterWithdrawal - debtEffectiveValue - ) - - // Does paying off all of the debt reach the target health? Then we're done. - if potentialHealth >= targetHealth { - // We can reach the target health by paying off some or all of the debt. We can easily - // compute how many units of the token would be needed to reach the target health. - let healthChange = targetHealth - healthAfterWithdrawal - let requiredEffectiveDebt = healthChange * effectiveCollateralAfterWithdrawal / (targetHealth * targetHealth) - - // The amount of the token to pay back, in units of the token. - let paybackAmount = requiredEffectiveDebt * self.borrowFactor[depositType]! / self.priceOracle.price(ofToken: depositType)! - - return paybackAmount - } else { - // We can pay off the entire debt, but we still need to deposit more to reach the target health. - // We have logic below that can determine the collateral deposition required to reach the target health - // from this new health position. Rather than copy that logic here, we fall through into it. But first - // we have to record the amount of tokens that went towards debt payback and adjust the effective - // debt to reflect that it has been paid off. - debtTokenCount = trueDebt - effectiveDebtAfterWithdrawal = effectiveDebtAfterWithdrawal - debtEffectiveValue - healthAfterWithdrawal = potentialHealth - } + // CHANGE: Create vault if it doesn't exist yet + if self.reserves[type] == nil { + self.reserves[type] <-! from.createEmptyVault() } + let reserveVault = (&self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! - // At this point, we're either dealing with a position that didn't have a debt position in the deposit - // token, or we've accounted for the debt payoff and adjusted the effective debt above. - - // Now we need to figure out how many tokens would need to be deposited (as collateral) to reach the - // target health. We can rearrange the health equation to solve for the required collateral: - // targetHealth = effectiveCollateral / effectiveDebt - // targetHealth * effectiveDebt = effectiveCollateral - // requiredCollateral = targetHealth * effectiveDebtAfterWithdrawal + // Reflect the deposit in the position's balance + position.balances[type]!.recordDeposit(amount: from.balance, tokenState: tokenState) - // We need to increase the effective collateral from its current value to the required value, so we - // multiply the required health change by the effective debt, and turn that into a token amount. - let healthChange = targetHealth - healthAfterWithdrawal - let requiredEffectiveCollateral = healthChange * effectiveDebtAfterWithdrawal + // Add the money to the reserves + reserveVault.deposit(from: <-from) - // The amount of the token to deposit, in units of the token. - let collateralTokenCount = requiredEffectiveCollateral / self.priceOracle.price(ofToken: depositType)! / self.collateralFactor[depositType]! + // Rebalancing and queue management + if pushToDrawDownSink { + self.rebalancePosition(pid: pid, force: true) + } - // debtTokenCount is the number of tokens that went towards debt, zero if there was no debt. - return collateralTokenCount + debtTokenCount + self._queuePositionForUpdateIfNecessary(pid: pid) + emit Deposited(pid: pid, poolUUID: self.uuid, type: type.identifier, amount: amount, depositedUUID: depositedUUID) } - // Returns the quantity of the specified token that could be withdrawn while still keeping the position's health - // at or above the provided target. - access(all) fun fundsAvailableAboveTargetHealth(pid: UInt64, type: Type, targetHealth: UFix64): UFix64 { - return self.fundsAvailableAboveTargetHealthAfterDepositing( - pid: pid, - withdrawType: type, - targetHealth: targetHealth, - depositType: self.defaultToken, - depositAmount: 0.0 - ) + /// Withdraws the requested funds from the specified position. Callers should be careful that the withdrawal + /// does not put their position under its target health, especially if the position doesn't have a configured + /// `topUpSource` from which to repay borrowed funds in the event of undercollaterlization. + access(EPosition) fun withdraw(pid: UInt64, amount: UFix64, type: Type): @{FungibleToken.Vault} { + // Call the enhanced function with pullFromTopUpSource = false for backward compatibility + return <- self.withdrawAndPull(pid: pid, type: type, amount: amount, pullFromTopUpSource: false) } - // Returns the quantity of the specified token that could be withdrawn while still keeping the position's health - // at or above the provided target, assuming we also deposit a specified amount of another token. - access(all) fun fundsAvailableAboveTargetHealthAfterDepositing( - pid: UInt64, - withdrawType: Type, - targetHealth: UFix64, - depositType: Type, - depositAmount: UFix64 - ): UFix64 { - if depositType == withdrawType && depositAmount > 0.0 { - // If the deposit and withdrawal types are the same, we compute the available funds assuming - // no deposit (which is less work) and increase that by the deposit amount at the end - return self.fundsAvailableAboveTargetHealth(pid: pid, type: withdrawType, targetHealth: targetHealth) + depositAmount + /// Withdraws the requested funds from the specified position with the configurable `pullFromTopUpSource` + /// option. If `pullFromTopUpSource` is true, deficient value putting the position below its min health is + /// pulled from the position's configured `topUpSource`. + access(EPosition) fun withdrawAndPull( + pid: UInt64, + type: Type, + amount: UFix64, + pullFromTopUpSource: Bool + ): @{FungibleToken.Vault} { + pre { + self.positions[pid] != nil: "Invalid position ID \(pid) - could not find an InternalPosition with the requested ID in the Pool" + self.globalLedger[type] != nil: "Invalid token type \(type.identifier) - not supported by this Pool" + } + if amount == 0.0 { + return <- DFBUtils.getEmptyVault(type) } - let balanceSheet = self.positionBalanceSheet(pid: pid) - let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! + // Get a reference to the user's position and global token state for the affected token. + let position = self._borrowPosition(pid: pid) + let tokenState = self._borrowUpdatedTokenState(type: type) - var effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral - var effectiveDebtAfterDeposit = balanceSheet.effectiveDebt + // Update the global interest indices on the affected token to reflect the passage of time. + // REMOVED: This is now handled by tokenState() helper function + // tokenState.updateForTimeChange() - if depositAmount != 0.0 { - if position.balances[depositType] == nil || position.balances[depositType]!.direction == BalanceDirection.Credit { - // If there's no debt for the deposit token, we can just compute how much additional effective collateral the deposit will create. - effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral + - (depositAmount * self.priceOracle.price(ofToken: depositType)! * self.collateralFactor[depositType]!) - } else { - let depositTokenState = self.tokenState(type: depositType) + // Preflight to see if the funds are available + let topUpSource = position.topUpSource as auth(FungibleToken.Withdraw) &{DFB.Source}? + let topUpType = topUpSource?.getSourceType() ?? self.defaultToken - // The user has a debt position in the given token, we need to figure out if this deposit - // will result in net collateral, or just bring down the debt. - let debtBalance = position.balances[depositType]!.scaledBalance - let trueDebt = TidalProtocol.scaledBalanceToTrueBalance( - scaledBalance: debtBalance, - interestIndex: depositTokenState.debitInterestIndex + let requiredDeposit = self.fundsRequiredForTargetHealthAfterWithdrawing( + pid: pid, + depositType: topUpType, + targetHealth: position.minHealth, + withdrawType: type, + withdrawAmount: amount + ) + + var canWithdraw = false + + if requiredDeposit == 0.0 { + // We can service this withdrawal without any top up + canWithdraw = true + } else { + // We need more funds to service this withdrawal, see if they are available from the top up source + if pullFromTopUpSource && topUpSource != nil { + // If we have to rebalance, let's try to rebalance to the target health, not just the minimum + let idealDeposit = self.fundsRequiredForTargetHealthAfterWithdrawing( + pid: pid, + depositType: topUpType, + targetHealth: position.targetHealth, + withdrawType: type, + withdrawAmount: amount ) - if trueDebt >= depositAmount { - // This deposit will pay down some debt, but won't result in net collateral, we - // just need to account for the debt decrease. - effectiveDebtAfterDeposit = balanceSheet.effectiveDebt - - (depositAmount * self.priceOracle.price(ofToken: depositType)! / self.borrowFactor[depositType]!) + let pulledVault <- topUpSource!.withdrawAvailable(maxAmount: idealDeposit) + + // NOTE: We requested the "ideal" deposit, but we compare against the required deposit here. + // The top up source may not have enough funds get us to the target health, but could have + // enough to keep us over the minimum. + if pulledVault.balance >= requiredDeposit { + // We can service this withdrawal if we deposit funds from our top up source + self.depositAndPush(pid: pid, from: <-pulledVault, pushToDrawDownSink: false) + canWithdraw = true } else { - // The deposit will wipe out all of the debt, and create some collateral. - effectiveDebtAfterDeposit = balanceSheet.effectiveDebt - - (trueDebt * self.priceOracle.price(ofToken: depositType)! / self.borrowFactor[depositType]!) - - effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral + - ((depositAmount - trueDebt) * self.priceOracle.price(ofToken: depositType)! * self.collateralFactor[depositType]!) + // We can't get the funds required to service this withdrawal, so we need to redeposit what we got + self.depositAndPush(pid: pid, from: <-pulledVault, pushToDrawDownSink: false) } } } - // We now have new effective collateral and debt values that reflect the proposed deposit (if any!) - // Now we can figure out how many of the withdrawal token are available while keeping the position - // at or above the target health value. - var healthAfterDeposit = TidalProtocol.healthComputation( - effectiveCollateral: effectiveCollateralAfterDeposit, - effectiveDebt: effectiveDebtAfterDeposit - ) - - if healthAfterDeposit <= targetHealth { - // The position is already at or below the target health, so we can't withdraw anything. - return 0.0 + if !canWithdraw { + // We can't service this withdrawal, so we just abort + panic("Cannot withdraw \(amount) of \(type.identifier) from position ID \(pid) - Insufficient funds for withdrawal") } - // For situations where the available withdrawal will BOTH draw down collateral and create debt, we keep - // track of the number of tokens that are available from collateral - var collateralTokenCount = 0.0 - - if position.balances[withdrawType] != nil && position.balances[withdrawType]!.direction == BalanceDirection.Credit { - // The user has a credit position in the withdraw token, we start by looking at the health impact of pulling out all - // of that collateral - let withdrawTokenState = self.tokenState(type: withdrawType) - // REMOVED: This is now handled by tokenState() helper function - // withdrawTokenState.updateForTimeChange() - - let creditBalance = position.balances[withdrawType]!.scaledBalance - let trueCredit = TidalProtocol.scaledBalanceToTrueBalance( - scaledBalance: creditBalance, - interestIndex: withdrawTokenState.creditInterestIndex - ) - let collateralEffectiveValue = self.priceOracle.price(ofToken: withdrawType)! * trueCredit * self.collateralFactor[withdrawType]! - - // Check what the new health would be if we took out all of this collateral - let potentialHealth = TidalProtocol.healthComputation( - effectiveCollateral: effectiveCollateralAfterDeposit - collateralEffectiveValue, - effectiveDebt: effectiveDebtAfterDeposit - ) + // If this position doesn't currently have an entry for this token, create one. + if position.balances[type] == nil { + position.balances[type] = InternalBalance() + } - // Does drawing down all of the collateral go below the target health? Then the max withdrawal comes from collateral only. - if potentialHealth <= targetHealth { - // We will hit the health target before using up all of the withdraw token credit. We can easily - // compute how many units of the token would bring the position down to the target health. - let availableHealth = healthAfterDeposit - targetHealth - let availableEffectiveValue = availableHealth * effectiveDebtAfterDeposit + let reserveVault = (&self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! - // The amount of the token we can take using that amount of health - let availableTokenCount = availableEffectiveValue / self.collateralFactor[withdrawType]! / self.priceOracle.price(ofToken: withdrawType)! + // Reflect the withdrawal in the position's balance + position.balances[type]!.recordWithdrawal(amount: amount, tokenState: tokenState) - return availableTokenCount - } else { - // We can flip this credit position into a debit position, before hitting the target health. - // We have logic below that can determine health changes for debit positions. Rather than copy that here, - // fall through into it. But first we have to record the amount of tokens that are available as collateral - // and then adjust the effective collateral to reflect that it has come out - collateralTokenCount = trueCredit - effectiveCollateralAfterDeposit = effectiveCollateralAfterDeposit - collateralEffectiveValue - // NOTE: The above invalidates the healthAfterDeposit value, but it's not used below... - } - } + // Ensure that this withdrawal doesn't cause the position to be overdrawn. + assert(self.positionHealth(pid: pid) >= 1.0, message: "Position is overdrawn") - // At this point, we're either dealing with a position that didn't have a credit balance in the withdraw - // token, or we've accounted for the credit balance and adjusted the effective collateral above. + // Queue for update if necessary + self._queuePositionForUpdateIfNecessary(pid: pid) - // We can calculate the available debt increase that would bring us to the target health - var availableDebtIncrease = (effectiveCollateralAfterDeposit / targetHealth) - effectiveDebtAfterDeposit + let withdrawn <- reserveVault.withdraw(amount: amount) - let availableTokens = availableDebtIncrease * self.borrowFactor[withdrawType]! / self.priceOracle.price(ofToken: withdrawType)! + emit Withdrawn(pid: pid, poolUUID: self.uuid, type: type.identifier, amount: withdrawn.balance, withdrawnUUID: withdrawn.uuid) - return availableTokens + collateralTokenCount + return <- withdrawn } - // Returns the health the position would have if the given amount of the specified token were deposited. - access(all) fun healthAfterDeposit(pid: UInt64, type: Type, amount: UFix64): UFix64 { - let balanceSheet = self.positionBalanceSheet(pid: pid) - let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! - let tokenState = self.tokenState(type: type) + /// Sets the InternalPosition's drawDownSink. If `nil`, the Pool will not be able to push overflown value when + /// the position exceeds its maximum health. Note, if a non-nil value is provided, the Sink MUST accept the + /// Pool's default deposits or the operation will revert. + access(EPosition) fun provideDrawDownSink(pid: UInt64, sink: {DFB.Sink}?) { + let position = self._borrowPosition(pid: pid) + position.setDrawDownSink(sink) + } - var effectiveCollateralIncrease = 0.0 - var effectiveDebtDecrease = 0.0 + /// Sets the InternalPosition's topUpSource. If `nil`, the Pool will not be able to pull underflown value when + /// the position falls below its minimum health which may result in liquidation. + access(EPosition) fun provideTopUpSource(pid: UInt64, source: {DFB.Source}?) { + let position = self._borrowPosition(pid: pid) + position.setTopUpSource(source) + } - if position.balances[type] == nil || position.balances[type]!.direction == BalanceDirection.Credit { - // Since the user has no debt in the given token, we can just compute how much - // additional collateral this deposit will create. - effectiveCollateralIncrease = amount * self.priceOracle.price(ofToken: type)! * self.collateralFactor[type]! - } else { - // The user has a debit position in the given token, we need to figure out if this deposit - // will only pay off some of the debt, or if it will also create new collateral. - let debtBalance = position.balances[type]!.scaledBalance - let trueDebt = TidalProtocol.scaledBalanceToTrueBalance( - scaledBalance: debtBalance, - interestIndex: tokenState.debitInterestIndex - ) + /////////////////////// + // POOL MANAGEMENT + /////////////////////// - if trueDebt >= amount { - // This deposit will wipe out some or all of the debt, but won't create new collateral, we - // just need to account for the debt decrease. - effectiveDebtDecrease = amount * self.priceOracle.price(ofToken: type)! / self.borrowFactor[type]! - } else { - // This deposit will wipe out all of the debt, and create new collateral. - effectiveDebtDecrease = trueDebt * self.priceOracle.price(ofToken: type)! / self.borrowFactor[type]! - effectiveCollateralIncrease = (amount - trueDebt) * self.priceOracle.price(ofToken: type)! * self.collateralFactor[type]! - } + /// Adds a new token type to the pool with the given parameters defining borrowing limits on collateral, + /// interest accumulation, deposit rate limiting, and deposit size capacity + access(EGovernance) fun addSupportedToken( + tokenType: Type, + collateralFactor: UFix64, + borrowFactor: UFix64, + interestCurve: {InterestCurve}, + depositRate: UFix64, + depositCapacityCap: UFix64 + ) { + pre { + self.globalLedger[tokenType] == nil: "Token type already supported" + tokenType.isSubtype(of: Type<@{FungibleToken.Vault}>()): + "Invalid token type \(tokenType.identifier) - tokenType must be a FungibleToken Vault implementation" + collateralFactor > 0.0 && collateralFactor <= 1.0: "Collateral factor must be between 0 and 1" + borrowFactor > 0.0 && borrowFactor <= 1.0: "Borrow factor must be between 0 and 1" + depositRate > 0.0: "Deposit rate must be positive" + depositCapacityCap > 0.0: "Deposit capacity cap must be positive" + DFBUtils.definingContractIsFungibleToken(tokenType): + "Invalid token contract definition for tokenType \(tokenType.identifier) - defining contract is not FungibleToken conformant" } - return TidalProtocol.healthComputation( - effectiveCollateral: balanceSheet.effectiveCollateral + effectiveCollateralIncrease, - effectiveDebt: balanceSheet.effectiveDebt - effectiveDebtDecrease + // Add token to global ledger with its interest curve and deposit parameters + self.globalLedger[tokenType] = TokenState( + interestCurve: interestCurve, + depositRate: depositRate, + depositCapacityCap: depositCapacityCap ) + + // Set collateral factor (what percentage of value can be used as collateral) + self.collateralFactor[tokenType] = collateralFactor + + // Set borrow factor (risk adjustment for borrowed amounts) + self.borrowFactor[tokenType] = borrowFactor } - // 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). - access(all) fun healthAfterWithdrawal(pid: UInt64, type: Type, amount: UFix64): UFix64 { - let balanceSheet = self.positionBalanceSheet(pid: pid) - let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! - let tokenState = self.tokenState(type: type) + /// Rebalances the position to the target health value. If `force` is `true`, the position will be rebalanced + /// even if it is currently healthy. Otherwise, this function will do nothing if the position is within the + /// min/max health bounds. + access(EPosition) fun rebalancePosition(pid: UInt64, force: Bool) { + let position = self._borrowPosition(pid: pid) + let balanceSheet = self._getUpdatedBalanceSheet(pid: pid) - var effectiveCollateralDecrease = 0.0 - var effectiveDebtIncrease = 0.0 + if !force && (balanceSheet.health >= position.minHealth && balanceSheet.health <= position.maxHealth) { + // We aren't forcing the update, and the position is already between its desired min and max. Nothing to do! + return + } - if position.balances[type] == nil || position.balances[type]!.direction == BalanceDirection.Debit { - // The user has no credit position in the given token, we can just compute how much - // additional effective debt this withdrawal will create. - effectiveDebtIncrease = amount * self.priceOracle.price(ofToken: type)! / self.borrowFactor[type]! - } else { - // The user has a credit position in the given token, we need to figure out if this withdrawal - // will only draw down some of the collateral, or if it will also create new debt. - let creditBalance = position.balances[type]!.scaledBalance - let trueCredit = TidalProtocol.scaledBalanceToTrueBalance( - scaledBalance: creditBalance, - interestIndex: tokenState.creditInterestIndex - ) + if balanceSheet.health < position.targetHealth { + // The position is undercollateralized, see if the source can get more collateral to bring it up to the target health. + if position.topUpSource != nil { + let topUpSource = position.topUpSource! as auth(FungibleToken.Withdraw) &{DFB.Source} + let idealDeposit = self.fundsRequiredForTargetHealth( + pid: pid, + type: topUpSource.getSourceType(), + targetHealth: position.targetHealth + ) - if trueCredit >= amount { - // This withdrawal will draw down some collateral, but won't create new debt, we - // just need to account for the collateral decrease. - effectiveCollateralDecrease = amount * self.priceOracle.price(ofToken: type)! * self.collateralFactor[type]! - } else { - // The withdrawal will wipe out all of the collateral, and create new debt. - effectiveDebtIncrease = (amount - trueCredit) * self.priceOracle.price(ofToken: type)! / self.borrowFactor[type]! - effectiveCollateralDecrease = trueCredit * self.priceOracle.price(ofToken: type)! * self.collateralFactor[type]! + let pulledVault <- topUpSource.withdrawAvailable(maxAmount: idealDeposit) + + emit Rebalanced(pid: pid, poolUUID: self.uuid, atHealth: balanceSheet.health, amount: pulledVault.balance, fromUnder: true) + + self.depositAndPush(pid: pid, from: <-pulledVault, pushToDrawDownSink: false) } - } + } else if balanceSheet.health > position.targetHealth { + // The position is overcollateralized, we'll withdraw funds to match the target health and offer it to the sink. + if position.drawDownSink != nil { + let drawDownSink = position.drawDownSink! + let sinkType = drawDownSink.getSinkType() + let idealWithdrawal = self.fundsAvailableAboveTargetHealth( + pid: pid, + type: sinkType, + targetHealth: position.targetHealth + ) - return TidalProtocol.healthComputation( - effectiveCollateral: balanceSheet.effectiveCollateral - effectiveCollateralDecrease, - effectiveDebt: balanceSheet.effectiveDebt + effectiveDebtIncrease - ) + // Compute how many tokens of the sink's type are available to hit our target health. + let sinkCapacity = drawDownSink.minimumCapacity() + let sinkAmount = (idealWithdrawal > sinkCapacity) ? sinkCapacity : idealWithdrawal + + if sinkAmount > 0.0 && sinkType == self.defaultToken { // second conditional included for sake of tracer bullet + // BUG: Calling through to withdrawAndPull results in an insufficient funds from the position's + // topUpSource. These funds should come from the protocol or reserves, not from the user's + // funds. To unblock here, we just mint MOET when a position is overcollateralized + // let sinkVault <- self.withdrawAndPull( + // pid: pid, + // type: sinkType, + // amount: sinkAmount, + // pullFromTopUpSource: false + // ) + + let tokenState = self._borrowUpdatedTokenState(type: self.defaultToken) + if position.balances[self.defaultToken] == nil { + position.balances[self.defaultToken] = InternalBalance() + } + position.balances[self.defaultToken]!.recordWithdrawal(amount: sinkAmount, tokenState: tokenState) + let sinkVault <- TidalProtocol._borrowMOETMinter().mintTokens(amount: sinkAmount) + + emit Rebalanced(pid: pid, poolUUID: self.uuid, atHealth: balanceSheet.health, amount: sinkVault.balance, fromUnder: false) + + // Push what we can into the sink, and redeposit the rest + drawDownSink.depositCapacity(from: &sinkVault as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) + if sinkVault.balance > 0.0 { + self.depositAndPush(pid: pid, from: <-sinkVault, pushToDrawDownSink: false) + } else { + Burner.burn(<-sinkVault) + } + } + } + } } - // RESTORED: Async update infrastructure from Dieter's implementation + /// Executes asynchronous updates on positions that have been queued up to the lesser of the queue length or + /// the configured positionsProcessedPerCallback value access(EImplementation) fun asyncUpdate() { // TODO: In the production version, this function should only process some positions (limited by positionsProcessedPerCallback) AND // it should schedule each update to run in its own callback, so a revert() call from one update (for example, if a source or @@ -1316,484 +1258,426 @@ access(all) contract TidalProtocol: FungibleToken { while self.positionsNeedingUpdates.length > 0 && processed < self.positionsProcessedPerCallback { let pid = self.positionsNeedingUpdates.removeFirst() self.asyncUpdatePosition(pid: pid) - self.queuePositionForUpdateIfNecessary(pid: pid) + self._queuePositionForUpdateIfNecessary(pid: pid) processed = processed + 1 } } - // RESTORED: Async position update from Dieter's implementation - access(EImplementation) fun asyncUpdatePosition(pid: UInt64) { - let position = (&self.positions[pid] as auth(EImplementation) &InternalPosition?)! + /// Executes an asynchronous update on the specified position + access(EImplementation) fun asyncUpdatePosition(pid: UInt64) { + let position = self._borrowPosition(pid: pid) + + // First check queued deposits, their addition could affect the rebalance we attempt later + for depositType in position.queuedDeposits.keys { + let queuedVault <- position.queuedDeposits.remove(key: depositType)! + let queuedAmount = queuedVault.balance + let depositTokenState = self._borrowUpdatedTokenState(type: depositType) + let maxDeposit = depositTokenState.depositLimit() + + if maxDeposit >= queuedAmount { + // We can deposit all of the queued deposit, so just do it and remove it from the queue + self.depositAndPush(pid: pid, from: <-queuedVault, pushToDrawDownSink: false) + } else { + // We can only deposit part of the queued deposit, so do that and leave the rest in the queue + // for the next time we run. + let depositVault <- queuedVault.withdraw(amount: maxDeposit) + self.depositAndPush(pid: pid, from: <-depositVault, pushToDrawDownSink: false) + + // We need to update the queued vault to reflect the amount we used up + position.queuedDeposits[depositType] <-! queuedVault + } + } + + // Now that we've deposited a non-zero amount of any queued deposits, we can rebalance + // the position if necessary. + self.rebalancePosition(pid: pid, force: false) + } + + //////////////// + // INTERNAL + //////////////// + + /// Queues a position for asynchronous updates if the position has been marked as requiring an update + access(self) fun _queuePositionForUpdateIfNecessary(pid: UInt64) { + if self.positionsNeedingUpdates.contains(pid) { + // If this position is already queued for an update, no need to check anything else + return + } else { + // If this position is not already queued for an update, we need to check if it needs one + let position = self._borrowPosition(pid: pid) + + if position.queuedDeposits.length > 0 { + // This position has deposits that need to be processed, so we need to queue it for an update + self.positionsNeedingUpdates.append(pid) + return + } + + let positionHealth = self.positionHealth(pid: pid) + + if positionHealth < position.minHealth || positionHealth > position.maxHealth { + // This position is outside the configured health bounds, we queue it for an update + self.positionsNeedingUpdates.append(pid) + return + } + } + } + + /// Returns a position's BalanceSheet containing its effective collateral and debt as well as its current health + access(self) fun _getUpdatedBalanceSheet(pid: UInt64): BalanceSheet { + let position = self._borrowPosition(pid: pid) + let priceOracle = &self.priceOracle as &{DFB.PriceOracle} - // First check queued deposits, their addition could affect the rebalance we attempt later - for depositType in position.queuedDeposits.keys { - let queuedVault <- position.queuedDeposits.remove(key: depositType)! - let queuedAmount = queuedVault.balance - let depositTokenState = self.tokenState(type: depositType) - - let maxDeposit = depositTokenState.depositLimit() + // Get the position's collateral and debt values in terms of the default token. + var effectiveCollateral = 0.0 + var effectiveDebt = 0.0 - if maxDeposit >= queuedAmount { - // We can deposit all of the queued deposit, so just do it and remove it from the queue - self.depositAndPush(pid: pid, from: <-queuedVault, pushToDrawDownSink: false) + for type in position.balances.keys { + let balance = position.balances[type]! + let tokenState = self._borrowUpdatedTokenState(type: type) + if balance.direction == BalanceDirection.Credit { + let trueBalance = TidalProtocol.scaledBalanceToTrueBalance(scaledBalance: balance.scaledBalance, + interestIndex: tokenState.creditInterestIndex) + + let value = priceOracle.price(ofToken: type)! * trueBalance + + effectiveCollateral = effectiveCollateral + (value * self.collateralFactor[type]!) } else { - // We can only deposit part of the queued deposit, so do that and leave the rest in the queue - // for the next time we run. - let depositVault <- queuedVault.withdraw(amount: maxDeposit) - self.depositAndPush(pid: pid, from: <-depositVault, pushToDrawDownSink: false) + let trueBalance = TidalProtocol.scaledBalanceToTrueBalance(scaledBalance: balance.scaledBalance, + interestIndex: tokenState.debitInterestIndex) - // We need to update the queued vault to reflect the amount we used up - position.queuedDeposits[depositType] <-! queuedVault + let value = priceOracle.price(ofToken: type)! * trueBalance + + effectiveDebt = effectiveDebt + (value / self.borrowFactor[type]!) } } - // Now that we've deposited a non-zero amount of any queued deposits, we can rebalance - // the position if necessary. - self.rebalancePosition(pid: pid, force: false) + return BalanceSheet(effectiveCollateral: effectiveCollateral, effectiveDebt: effectiveDebt) + } + + /// A convenience function that returns a reference to a particular token state, making sure it's up-to-date for + /// the passage of time. This should always be used when accessing a token state to avoid missing interest + /// updates (duplicate calls to updateForTimeChange() are a nop within a single block). + access(self) fun _borrowUpdatedTokenState(type: Type): auth(EImplementation) &TokenState { + let state = &self.globalLedger[type]! as auth(EImplementation) &TokenState + state.updateForTimeChange() + return state + } + + /// Returns an authorized reference to the requested InternalPosition or `nil` if the position does not exist + access(self) view fun _borrowPosition(pid: UInt64): auth(EImplementation) &InternalPosition { + return &self.positions[pid] as auth(EImplementation) &InternalPosition? + ?? panic("Invalid position ID \(pid) - could not find an InternalPosition with the requested ID in the Pool") + } + } + + /// PoolFactory + /// + /// Resource enabling the contract account to create the contract's Pool. This pattern is used in place of contract + /// methods to ensure limited access to pool creation. While this could be done in contract's init, doing so here + /// will allow for the setting of the Pool's PriceOracle without the introduction of a concrete PriceOracle defining + /// contract which would include an external contract dependency. + /// + 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: {DFB.PriceOracle}) { + pre { + TidalProtocol.account.storage.type(at: TidalProtocol.PoolStoragePath) == nil: + "Storage collision - Pool has already been created & saved to \(TidalProtocol.PoolStoragePath)" + } + let pool <- create Pool(defaultToken: defaultToken, priceOracle: priceOracle) + TidalProtocol.account.storage.save(<-pool, to: TidalProtocol.PoolStoragePath) + let cap = TidalProtocol.account.capabilities.storage.issue<&Pool>(TidalProtocol.PoolStoragePath) + TidalProtocol.account.capabilities.unpublish(TidalProtocol.PoolPublicPath) + TidalProtocol.account.capabilities.publish(cap, at: TidalProtocol.PoolPublicPath) } } + /// Position + /// + /// A Position is an external object representing ownership of value deposited to the protocol. From a Position, an + /// actor can deposit and withdraw funds as well as construct DeFiBlocks components enabling value flows in and out + /// of the Position from within the context of DeFiBlocks stacks. + /// + // TODO: Consider making this a resource given how critical it is to accessing a loan access(all) struct Position { + /// The unique ID of the Position used to track deposits and withdrawals to the Pool access(self) let id: UInt64 + /// An authorized Capability to which the Position was opened access(self) let pool: Capability - // Returns the balances (both positive and negative) for all tokens in this position. - access(all) fun getBalances(): [PositionBalance] { - let pool = self.pool.borrow()! - return pool.getPositionDetails(pid: self.id).balances + init(id: UInt64, pool: Capability) { + pre { + pool.check(): "Invalid Pool Capability provided - cannot construct Position" + } + self.id = id + self.pool = pool } - // Returns the maximum amount of the given token type that could be withdrawn from this position. - access(all) fun getAvailableBalance(type: Type): UFix64 { + /// Returns the balances (both positive and negative) for all tokens in this position. + access(all) fun getBalances(): [PositionBalance] { let pool = self.pool.borrow()! - return pool.availableBalance(pid: self.id, type: type, pullFromTopUpSource: false) + return pool.getPositionDetails(pid: self.id).balances } - - // RESTORED: Enhanced available balance from Dieter's implementation + /// Returns the balance available for withdrawal of a given Vault type. If pullFromTopUpSource is true, the + /// calculation will be made assuming the position is topped up if the withdrawal amount puts the Position + /// below its min health. If pullFromTopUpSource is true, the calculation will return the balance currently + /// available without topping up the position. access(all) fun availableBalance(type: Type, pullFromTopUpSource: Bool): UFix64 { let pool = self.pool.borrow()! return pool.availableBalance(pid: self.id, type: type, pullFromTopUpSource: pullFromTopUpSource) } - - // RESTORED: Health functions from Dieter's implementation + /// Returns the current health of the position access(all) fun getHealth(): UFix64 { let pool = self.pool.borrow()! return pool.positionHealth(pid: self.id) } - + /// Returns the Position's target health access(all) fun getTargetHealth(): UFix64 { - // DIETER'S DESIGN: Position is just a relay struct, return 0.0 - return 0.0 + return 0.0 // TODO } - + /// Sets the target health of the Position access(all) fun setTargetHealth(targetHealth: UFix64) { - // DIETER'S DESIGN: Position is just a relay struct, do nothing + // TODO } - + /// Returns the minimum health of the Position access(all) fun getMinHealth(): UFix64 { - // DIETER'S DESIGN: Position is just a relay struct, return 0.0 - return 0.0 + return 0.0 // TODO } - + /// Sets the minimum health of the Position access(all) fun setMinHealth(minHealth: UFix64) { - // DIETER'S DESIGN: Position is just a relay struct, do nothing + // TODO } - + /// Returns the maximum health of the Position access(all) fun getMaxHealth(): UFix64 { - // DIETER'S DESIGN: Position is just a relay struct, return 0.0 + // TODO return 0.0 } - + /// Sets the maximum health of the position access(all) fun setMaxHealth(maxHealth: UFix64) { - // DIETER'S DESIGN: Position is just a relay struct, do nothing + // TODO } - - // Returns the maximum amount of the given token type that could be deposited into this position. + /// Returns the maximum amount of the given token type that could be deposited into this position access(all) fun getDepositCapacity(type: Type): UFix64 { // There's no limit on deposits from the position's perspective return UFix64.max } - - // RESTORED: Simple deposit that calls depositAndPush with pushToDrawDownSink = false + /// Deposits funds to the Position without pushing to the drawDownSink if the deposit puts the Position above + /// its maximum health access(all) fun deposit(from: @{FungibleToken.Vault}) { let pool = self.pool.borrow()! pool.depositAndPush(pid: self.id, from: <-from, pushToDrawDownSink: false) } - - // RESTORED: Enhanced deposit from Dieter's implementation + /// Deposits funds to the Position enabling the caller to configure whether excess value should be pushed to the + /// drawDownSink if the deposit puts the Position above its maximum health access(all) fun depositAndPush(from: @{FungibleToken.Vault}, pushToDrawDownSink: Bool) { let pool = self.pool.borrow()! pool.depositAndPush(pid: self.id, from: <-from, pushToDrawDownSink: pushToDrawDownSink) } - - // RESTORED: Simple withdraw that calls withdrawAndPull with pullFromTopUpSource = false - access(all) fun withdraw(type: Type, amount: UFix64): @{FungibleToken.Vault} { + /// Withdraws funds from the Position without pulling from the topUpSource if the deposit puts the Position below + /// its minimum health + access(FungibleToken.Withdraw) fun withdraw(type: Type, amount: UFix64): @{FungibleToken.Vault} { return <- self.withdrawAndPull(type: type, amount: amount, pullFromTopUpSource: false) } - - // RESTORED: Enhanced withdraw from Dieter's implementation - access(all) fun withdrawAndPull(type: Type, amount: UFix64, pullFromTopUpSource: Bool): @{FungibleToken.Vault} { + /// Withdraws funds from the Position enabling the caller to configure whether insufficient value should be + /// pulled from the topUpSource if the deposit puts the Position below its minimum health + access(FungibleToken.Withdraw) fun withdrawAndPull(type: Type, amount: UFix64, pullFromTopUpSource: Bool): @{FungibleToken.Vault} { let pool = self.pool.borrow()! return <- pool.withdrawAndPull(pid: self.id, type: type, amount: amount, pullFromTopUpSource: pullFromTopUpSource) } - - // Returns a NEW sink for the given token type that will accept deposits of that token and - // update the position's collateral and/or debt accordingly. Note that calling this method multiple - // times will create multiple sinks, each of which will continue to work regardless of how many - // other sinks have been created. + /// Returns a new Sink for the given token type that will accept deposits of that token and update the + /// position's collateral and/or debt accordingly. Note that calling this method multiple times will create + /// multiple sinks, each of which will continue to work regardless of how many other sinks have been created. access(all) fun createSink(type: Type): {DFB.Sink} { - // RESTORED: Create enhanced sink with pushToDrawDownSink option + // create enhanced sink with pushToDrawDownSink option return self.createSinkWithOptions(type: type, pushToDrawDownSink: false) } - - // RESTORED: Enhanced sink creation from Dieter's implementation + /// Returns a new Sink for the given token type and pushToDrawDownSink opetion that will accept deposits of that + /// token and update the position's collateral and/or debt accordingly. Note that calling this method multiple + /// times will create multiple sinks, each of which will continue to work regardless of how many other sinks + /// have been created. access(all) fun createSinkWithOptions(type: Type, pushToDrawDownSink: Bool): {DFB.Sink} { let pool = self.pool.borrow()! return PositionSink(id: self.id, pool: self.pool, type: type, pushToDrawDownSink: pushToDrawDownSink) } - - // 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(all) fun createSource(type: Type): {DFB.Source} { - // RESTORED: Create enhanced source with pullFromTopUpSource option + /// 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): {DFB.Source} { + // Create enhanced source with pullFromTopUpSource = true return self.createSourceWithOptions(type: type, pullFromTopUpSource: false) } - - // RESTORED: Enhanced source creation from Dieter's implementation - access(all) fun createSourceWithOptions(type: Type, pullFromTopUpSource: Bool): {DFB.Source} { + /// 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): {DFB.Source} { let pool = self.pool.borrow()! return PositionSource(id: self.id, pool: self.pool, type: type, pullFromTopUpSource: pullFromTopUpSource) } - - // RESTORED: Provider functions implementation from Dieter's design - // Provides a sink to the Position that will have tokens proactively pushed into it when the - // position has excess collateral. (Remember that sinks do NOT have to accept all tokens provided - // to them; the sink can choose to accept only some (or none) of the tokens provided, leaving the position - // overcollateralized.) - // - // Each position can have only one sink, and the sink must accept the default token type - // configured for the pool. Providing a new sink will replace the existing sink. Pass nil - // to configure the position to not push tokens. - access(all) fun provideSink(sink: {DFB.Sink}?) { + /// Provides a sink to the Position that will have tokens proactively pushed into it when the position has + /// excess collateral. (Remember that sinks do NOT have to accept all tokens provided to them; the sink can + /// choose to accept only some (or none) of the tokens provided, leaving the position overcollateralized). + /// + /// Each position can have only one sink, and the sink must accept the default token type configured for the + /// pool. Providing a new sink will replace the existing sink. Pass nil to configure the position to not push + /// tokens when the Position exceeds its maximum health. + access(FungibleToken.Withdraw) fun provideSink(sink: {DFB.Sink}?) { let pool = self.pool.borrow()! pool.provideDrawDownSink(pid: self.id, sink: sink) } - - // Provides a source to the Position that will have tokens proactively pulled from it when the - // position has insufficient collateral. If the source can cover the position's debt, the position - // will not be liquidated. - // - // Each position can have only one source, and the source must accept the default token type - // configured for the pool. Providing a new source will replace the existing source. Pass nil - // to configure the position to not pull tokens. + /// Provides a source to the Position that will have tokens proactively pulled from it when the position has + /// insufficient collateral. If the source can cover the position's debt, the position will not be liquidated. + /// + /// Each position can have only one source, and the source must accept the default token type configured for the + /// pool. Providing a new source will replace the existing source. Pass nil to configure the position to not + /// pull tokens. access(all) fun provideSource(source: {DFB.Source}?) { let pool = self.pool.borrow()! pool.provideTopUpSource(pid: self.id, source: source) } - - init(id: UInt64, pool: Capability) { - self.id = id - self.pool = pool - } - } - - // CHANGE: Removed FlowToken-specific implementation - // Helper for unit-tests – creates a new Pool with a generic default token - // Tests should specify the actual token type they want to use - access(all) fun createTestPool(defaultTokenThreshold: UFix64): @Pool { - // For backward compatibility, we'll panic here - // Tests should use createPool with explicit token type - panic("Use createPool with explicit token type instead") - } - - // CHANGE: Removed - tests should use proper token minting - // This function is kept for backward compatibility but will panic - access(all) fun createTestVault(balance: UFix64): @{FungibleToken.Vault} { - panic("Use proper token minting instead of createTestVault") - } - - // CHANGE: Add a proper pool creation function for tests - access(all) fun createPool(defaultToken: Type, priceOracle: {DFB.PriceOracle}): @Pool { - return <- create Pool(defaultToken: defaultToken, priceOracle: priceOracle) - } - - // RESTORED: Helper function to create a test pool with dummy oracle - access(all) fun createTestPoolWithOracle(defaultToken: Type): @Pool { - let oracle = DummyPriceOracle(defaultToken: defaultToken) - return <- create Pool(defaultToken: defaultToken, priceOracle: oracle) - } - - // Helper for unit-tests - initializes a pool with a vault containing the specified balance - access(all) fun createTestPoolWithBalance(defaultTokenThreshold: UFix64, initialBalance: UFix64): @Pool { - // CHANGE: This function is deprecated - tests should create pools with explicit token types - panic("Use createPool with explicit token type and deposit tokens separately") - } - - // Events are now handled by FungibleToken standard - // Total supply tracking - access(all) var totalSupply: UFix64 - - // Storage paths - access(all) let VaultStoragePath: StoragePath - access(all) let VaultPublicPath: PublicPath - access(all) let ReceiverPublicPath: PublicPath - access(all) let AdminStoragePath: StoragePath - - // FungibleToken contract interface requirement - access(all) fun createEmptyVault(vaultType: Type): @{FungibleToken.Vault} { - // CHANGE: This contract doesn't create vaults - it's a lending protocol - panic("TidalProtocol doesn't create vaults - use the token's contract") } - // ViewResolver conformance for metadata - access(all) view fun getContractViews(resourceType: Type?): [Type] { - return [ - Type(), - Type(), - Type(), - Type() - ] - } + /// PositionSink + /// + /// A DeFiBlocks connector enabling deposits to a Position from within a DeFiBlocks stack. This Sink is intended to + /// be constructed from a Position object. + access(all) struct PositionSink: DFB.Sink { + /// An optional DFB.UniqueIdentifier that identifies this Sink with the DeFiBlocks stack its a part of + access(contract) let uniqueID: DFB.UniqueIdentifier? + /// An authorized Capability on the Pool for which the related Position is in + access(self) let pool: Capability + /// The ID of the position in the Pool + access(self) let positionID: UInt64 + /// The Type of Vault this Sink accepts + access(self) let type: Type + /// Whether deposits through this Sink to the Position should push available value to the Position's + /// drawDownSink + access(self) let pushToDrawDownSink: Bool - access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? { - switch viewType { - case Type(): - return FungibleTokenMetadataViews.FTView( - ftDisplay: self.resolveContractView(resourceType: nil, viewType: Type()) as! FungibleTokenMetadataViews.FTDisplay?, - ftVaultData: self.resolveContractView(resourceType: nil, viewType: Type()) as! FungibleTokenMetadataViews.FTVaultData? - ) - case Type(): - let media = MetadataViews.Media( - file: MetadataViews.HTTPFile( - url: "https://example.com/TidalProtocol-logo.svg" - ), - mediaType: "image/svg+xml" - ) - return FungibleTokenMetadataViews.FTDisplay( - name: "TidalProtocol Token", - symbol: "ALPF", - description: "TidalProtocol is a decentralized lending protocol on Flow blockchain", - externalURL: MetadataViews.ExternalURL("https://TidalProtocol.com"), - logos: MetadataViews.Medias([media]), - socials: { - "twitter": MetadataViews.ExternalURL("https://twitter.com/TidalProtocol") - } - ) - case Type(): - return FungibleTokenMetadataViews.FTVaultData( - storagePath: self.VaultStoragePath, - receiverPath: self.ReceiverPublicPath, - metadataPath: self.VaultPublicPath, - receiverLinkedType: Type<&{FungibleToken.Receiver}>(), - metadataLinkedType: Type<&{FungibleToken.Balance, ViewResolver.Resolver}>(), - createEmptyVaultFunction: (fun(): @{FungibleToken.Vault} { - // CHANGE: TidalProtocol doesn't create vaults - panic("TidalProtocol doesn't create vaults") - }) - ) - case Type(): - return FungibleTokenMetadataViews.TotalSupply( - totalSupply: TidalProtocol.totalSupply - ) + init(id: UInt64, pool: Capability, type: Type, pushToDrawDownSink: Bool) { + self.uniqueID = nil + self.positionID = id + self.pool = pool + self.type = type + self.pushToDrawDownSink = pushToDrawDownSink } - return nil - } - // DFB.Sink implementation for TidalProtocol - access(all) struct TidalProtocolSink: DFB.Sink { - access(contract) let uniqueID: {DFB.UniqueIdentifier}? - access(contract) let pool: auth(EPosition) &Pool - access(contract) let positionID: UInt64 - + /// Returns the Type of Vault this Sink accepts on deposits access(all) view fun getSinkType(): Type { - // CHANGE: For now, return a generic FungibleToken.Vault type - // The actual type depends on what tokens the pool accepts - return Type<@{FungibleToken.Vault}>() + return self.type } - + /// Returns the minimum capacity this Sink can accept as deposits access(all) fun minimumCapacity(): UFix64 { - // For now, return 0 as there's no minimum - return 0.0 + return self.pool.check() ? UFix64.max : 0.0 } - + /// Deposits the funds from the provided Vault reference to the related Position access(all) fun depositCapacity(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) { - let amount = from.balance - if amount > 0.0 { - let vault <- from.withdraw(amount: amount) - self.pool.deposit(pid: self.positionID, funds: <-vault) - } - } - - init(pool: auth(EPosition) &Pool, positionID: UInt64) { - self.uniqueID = nil - self.pool = pool - self.positionID = positionID - } - } - - // DFB.Source implementation for TidalProtocol - access(all) struct TidalProtocolSource: DFB.Source { - access(contract) let uniqueID: {DFB.UniqueIdentifier}? - access(contract) let pool: auth(EPosition) &Pool - access(contract) let positionID: UInt64 - access(contract) let tokenType: Type - - access(all) view fun getSourceType(): Type { - return self.tokenType - } - - access(all) fun minimumAvailable(): UFix64 { - // Return the available balance for withdrawal - let position = self.pool.getPositionDetails(pid: self.positionID) - for balance in position.balances { - if balance.type == self.tokenType && balance.direction == BalanceDirection.Credit { - return balance.balance - } - } - return 0.0 - } - - access(FungibleToken.Withdraw) fun withdrawAvailable(maxAmount: UFix64): @{FungibleToken.Vault} { - let available = self.minimumAvailable() - let withdrawAmount = available < maxAmount ? available : maxAmount - if withdrawAmount > 0.0 { - return <- self.pool.withdraw(pid: self.positionID, amount: withdrawAmount, type: self.tokenType) - } else { - // Create an empty vault by getting one from the pool's reserves - // For now, just panic as we can't create empty vaults directly - panic("Cannot create empty vault for type: ".concat(self.tokenType.identifier)) + if let pool = self.pool.borrow() { + pool.depositAndPush( + pid: self.positionID, + from: <-from.withdraw(amount: from.balance), + pushToDrawDownSink: self.pushToDrawDownSink + ) } } - - init(pool: auth(EPosition) &Pool, positionID: UInt64, tokenType: Type) { - self.uniqueID = nil - self.pool = pool - self.positionID = positionID - self.tokenType = tokenType - } } - // RESTORED: Enhanced position sink from Dieter's implementation - access(all) struct PositionSink: DFB.Sink { - access(contract) let uniqueID: {DFB.UniqueIdentifier}? + /// PositionSource + /// + /// A DeFiBlocks connector enabling withdrawals from a Position from within a DeFiBlocks stack. This Source is + /// intended to be constructed from a Position object. + /// + access(all) struct PositionSource: DFB.Source { + /// An optional DFB.UniqueIdentifier that identifies this Sink with the DeFiBlocks stack its a part of + access(contract) let uniqueID: DFB.UniqueIdentifier? + /// An authorized Capability on the Pool for which the related Position is in access(self) let pool: Capability - access(self) let id: UInt64 + /// The ID of the position in the Pool + access(self) let positionID: UInt64 + /// The Type of Vault this Sink provides access(self) let type: Type - access(self) let pushToDrawDownSink: Bool - - access(all) view fun getSinkType(): Type { - return self.type - } - - access(all) fun minimumCapacity(): UFix64 { - // A position object has no limit to deposits - return UFix64.max - } - - access(all) fun depositCapacity(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) { - let pool = self.pool.borrow()! - pool.depositAndPush(pid: self.id, from: <-from.withdraw(amount: from.balance), pushToDrawDownSink: self.pushToDrawDownSink) - } + /// Whether withdrawals through this Sink from the Position should pull value from the Position's topUpSource + /// in the event the withdrawal puts the position under its target health + access(self) let pullFromTopUpSource: Bool - init(id: UInt64, pool: Capability, type: Type, pushToDrawDownSink: Bool) { + init(id: UInt64, pool: Capability, type: Type, pullFromTopUpSource: Bool) { self.uniqueID = nil - self.id = id + self.positionID = id self.pool = pool self.type = type - self.pushToDrawDownSink = pushToDrawDownSink + self.pullFromTopUpSource = pullFromTopUpSource } - } - - // RESTORED: Enhanced position source from Dieter's implementation - access(all) struct PositionSource: DFB.Source { - access(contract) let uniqueID: {DFB.UniqueIdentifier}? - access(all) let pool: Capability - access(all) let id: UInt64 - access(all) let type: Type - access(all) let pullFromTopUpSource: Bool + /// Returns the Type of Vault this Source provides on withdrawals access(all) view fun getSourceType(): Type { return self.type } - + /// Returns the minimum availble this Source can provide on withdrawal access(all) fun minimumAvailable(): UFix64 { + if !self.pool.check() { + return 0.0 + } let pool = self.pool.borrow()! - return pool.availableBalance(pid: self.id, type: self.type, pullFromTopUpSource: self.pullFromTopUpSource) + return pool.availableBalance(pid: self.positionID, type: self.type, pullFromTopUpSource: self.pullFromTopUpSource) } - + /// Withdraws up to the max amount as the sourceType Vault access(FungibleToken.Withdraw) fun withdrawAvailable(maxAmount: UFix64): @{FungibleToken.Vault} { + if !self.pool.check() { + return <- DFBUtils.getEmptyVault(self.type) + } let pool = self.pool.borrow()! - let available = pool.availableBalance(pid: self.id, type: self.type, pullFromTopUpSource: self.pullFromTopUpSource) + let available = pool.availableBalance(pid: self.positionID, type: self.type, pullFromTopUpSource: self.pullFromTopUpSource) let withdrawAmount = (available > maxAmount) ? maxAmount : available if withdrawAmount > 0.0 { - return <- pool.withdrawAndPull(pid: self.id, type: self.type, amount: withdrawAmount, pullFromTopUpSource: self.pullFromTopUpSource) + return <- pool.withdrawAndPull(pid: self.positionID, type: self.type, amount: withdrawAmount, pullFromTopUpSource: self.pullFromTopUpSource) } else { // Create an empty vault - this is a limitation we need to handle properly - panic("Cannot create empty vault for type: ".concat(self.type.identifier)) + return <- DFBUtils.getEmptyVault(self.type) } } - - init(id: UInt64, pool: Capability, type: Type, pullFromTopUpSource: Bool) { - self.uniqueID = nil - self.id = id - self.pool = pool - self.type = type - self.pullFromTopUpSource = pullFromTopUpSource - } } - // TidalProtocol starts here! - + /// BalanceDirection + /// + /// The direction of a given balance access(all) enum BalanceDirection: UInt8 { + /// Denotes that a balance that is withdrawable from the protocol access(all) case Credit + /// Denotes that a balance that is due to the protocol access(all) case Debit } - // RESTORED: DummyPriceOracle for testing from Dieter's design pattern - access(all) struct DummyPriceOracle: DFB.PriceOracle { - access(self) var prices: {Type: UFix64} - access(self) let defaultToken: Type - - access(all) view fun unitOfAccount(): Type { - return self.defaultToken - } - - access(all) fun price(ofToken: Type): UFix64 { - return self.prices[ofToken] ?? 1.0 - } - - access(all) fun setPrice(ofToken: Type, price: UFix64) { - self.prices[ofToken] = price - } - - init(defaultToken: Type) { - self.defaultToken = defaultToken - self.prices = {defaultToken: 1.0} - } - } - - // A structure returned externally to report a position's balance for a particular token. - // This structure is NOT used internally. + /// PositionBalance + /// + /// A structure returned externally to report a position's balance for a particular token. + /// This structure is NOT used internally. access(all) struct PositionBalance { - access(all) let type: Type + /// The token type for which the balance details relate to + access(all) let vaultType: Type + /// Whether the balance is a Credit or Debit access(all) let direction: BalanceDirection + /// The balance of the token for the related Position access(all) let balance: UFix64 - init(type: Type, direction: BalanceDirection, balance: UFix64) { - self.type = type + init(vaultType: Type, direction: BalanceDirection, balance: UFix64) { + self.vaultType = vaultType self.direction = direction self.balance = balance } } - // A structure returned externally to report all of the details associated with a position. - // This structure is NOT used internally. + /// PositionDetails + /// + /// A structure returned externally to report all of the details associated with a position. + /// This structure is NOT used internally. access(all) struct PositionDetails { + /// Balance details about each Vault Type deposited to the related Position access(all) let balances: [PositionBalance] + /// The default token Type of the Pool in which the related position is held access(all) let poolDefaultToken: Type + /// The available balance of the Pool's default token Type access(all) let defaultTokenAvailableBalance: UFix64 + /// The current health of the related position access(all) let health: UFix64 init(balances: [PositionBalance], poolDefaultToken: Type, defaultTokenAvailableBalance: UFix64, health: UFix64) { @@ -1804,14 +1688,146 @@ access(all) contract TidalProtocol: FungibleToken { } } + /* --- PUBLIC METHODS ---- */ + + /// Takes out a TidalProtocol loan with the provided collateral, returning a Position that can be used to manage + /// collateral and borrowed fund flows + /// + /// @param collateral: The collateral used as the basis for a loan. Only certain collateral types are supported, so + /// callers should be sure to check the provided Vault is supported to prevent reversion. + /// @param issuanceSink: The DeFiBlocks Sink connector where the protocol will deposit borrowed funds. If the + /// position becomes overcollateralized, additional funds will be borrowed (to maintain target LTV) and + /// deposited to the provided Sink. + /// @param repaymentSource: An optional DeFiBlocks Source connector from which the protocol will attempt to source + /// borrowed funds in the event of undercollateralization prior to liquidating. If none is provided, the + /// position health will not be actively managed on the down side, meaning liquidation is possible as soon as + /// the loan becomes undercollateralized. + /// + /// @return the Position via which the caller can manage their position + /// + access(all) fun openPosition( + collateral: @{FungibleToken.Vault}, + issuanceSink: {DFB.Sink}, + repaymentSource: {DFB.Source}?, + pushToDrawDownSink: Bool + ): Position { + let pid = self._borrowPool().createPosition( + funds: <-collateral, + issuanceSink: issuanceSink, + repaymentSource: repaymentSource, + pushToDrawDownSink: pushToDrawDownSink + ) + let cap = self.account.capabilities.storage.issue(self.PoolStoragePath) + return Position(id: pid, pool: cap) + } + + /// Returns a health value computed from the provided effective collateral and debt values where health is a ratio + /// of effective collateral over effective debt + access(all) view fun healthComputation(effectiveCollateral: UFix64, effectiveDebt: UFix64): UFix64 { + var health = 0.0 + + if effectiveCollateral == 0.0 { + health = 0.0 + } else if effectiveDebt == 0.0 { + health = UFix64.max + } else if (effectiveDebt / effectiveCollateral) == 0.0 { + // If debt is so small relative to collateral that division rounds to zero, + // the health is essentially infinite + health = UFix64.max + } else { + health = effectiveCollateral / effectiveDebt + } + + return health + } + + /// A multiplication function for interest calculations. It assumes that both values are very close to 1 and + /// represent fixed point numbers with 16 decimal places of precision. + access(all) view fun interestMul(_ a: UInt64, _ b: UInt64): UInt64 { + let aScaled = a / 100000000 + let bScaled = b / 100000000 + + return aScaled * bScaled + } + + /// Converts a yearly interest rate (as a UFix64) to a per-second multiplication factor (stored in a UInt64 as a + /// fixed point number with 16 decimal places). The input to this function will be just the relative interest rate + /// (e.g. 0.05 for 5% interest), but the result will be the per-second multiplier (e.g. 1.000000000001). + access(all) view fun perSecondInterestRate(yearlyRate: UFix64): UInt64 { + // Covert the yearly rate to an integer maintaning the 10^8 multiplier of UFix64. + // We would need to multiply by an additional 10^8 to match the promised multiplier of + // 10^16. HOWEVER, since we are about to divide by 31536000, we can save multiply a factor + // 1000 smaller, and then divide by 31536. + let yearlyScaledValue = UInt64.fromBigEndianBytes(yearlyRate.toBigEndianBytes())! * 100000 + let perSecondScaledValue = (yearlyScaledValue / 31536) + 10000000000000000 + + return perSecondScaledValue + } + + /// Returns the compounded interest index reflecting the passage of time + /// The result is: newIndex = oldIndex * perSecondRate ^ seconds + access(all) view fun compoundInterestIndex(oldIndex: UInt64, perSecondRate: UInt64, elapsedSeconds: UFix64): UInt64 { + var result = oldIndex + var current = perSecondRate + var secondsCounter = UInt64(elapsedSeconds) + + while secondsCounter > 0 { + if secondsCounter & 1 == 1 { + result = TidalProtocol.interestMul(result, current) + } + current = TidalProtocol.interestMul(current, current) + secondsCounter = secondsCounter >> 1 + } + + return result + } + + /// Transforms the provided `scaledBalance` to a true balance (or actual balance) where the true balance is the + /// scaledBalance + accrued interest and the scaled balance is the amount a borrower has actually interacted with + /// (via deposits or withdrawals) + access(all) view fun scaledBalanceToTrueBalance(scaledBalance: UFix64, interestIndex: UInt64): UFix64 { + // The interest index is essentially a fixed point number with 16 decimal places, we convert + // it to a UFix64 by copying the byte representation, and then dividing by 10^8 (leaving and + // additional 10^8 as required for the UFix64 representation). + let indexMultiplier = UFix64.fromBigEndianBytes(interestIndex.toBigEndianBytes())! / 100000000.0 + return scaledBalance * indexMultiplier + } + + /// Transforms the provided `trueBalance` to a scaled balance where the scaled balance is the amount a borrower has + /// actually interacted with (via deposits or withdrawals) and the true balance is the amount with respect to + /// accrued interest + access(all) view fun trueBalanceToScaledBalance(trueBalance: UFix64, interestIndex: UInt64): UFix64 { + // The interest index is essentially a fixed point number with 16 decimal places, we convert + // it to a UFix64 by copying the byte representation, and then dividing by 10^8 (leaving and + // additional 10^8 as required for the UFix64 representation). + let indexMultiplier = UFix64.fromBigEndianBytes(interestIndex.toBigEndianBytes())! / 100000000.0 + return trueBalance / indexMultiplier + } + + /* --- INTERNAL METHODS --- */ + + /// Returns an authorized reference to the contract account's Pool resource + access(self) view fun _borrowPool(): auth(EPosition) &Pool { + return self.account.storage.borrow(from: self.PoolStoragePath) + ?? panic("Could not borrow reference to internal TidalProtocol Pool resource") + } + + /// Returns a reference to the contract account's MOET Minter resource + access(self) view fun _borrowMOETMinter(): &MOET.Minter { + return self.account.storage.borrow<&MOET.Minter>(from: MOET.AdminStoragePath) + ?? panic("Could not borrow reference to internal MOET Minter resource") + } + init() { - // Initialize total supply - self.totalSupply = 0.0 - - // Set up storage paths - self.VaultStoragePath = /storage/TidalProtocolVault - self.VaultPublicPath = /public/TidalProtocolVault - self.ReceiverPublicPath = /public/TidalProtocolReceiver - self.AdminStoragePath = /storage/TidalProtocolAdmin + self.PoolStoragePath = StoragePath(identifier: "tidalProtocolPool_\(self.account.address)")! + self.PoolFactoryPath = StoragePath(identifier: "tidalProtocolPoolFactory_\(self.account.address)")! + self.PoolPublicPath = PublicPath(identifier: "tidalProtocolPool_\(self.account.address)")! + + // save PoolFactory in storage + self.account.storage.save( + <-create PoolFactory(), + to: self.PoolFactoryPath + ) + let factory = self.account.storage.borrow<&PoolFactory>(from: self.PoolFactoryPath)! } -} \ No newline at end of file +} diff --git a/cadence/contracts/mocks/MockOracle.cdc b/cadence/contracts/mocks/MockOracle.cdc new file mode 100644 index 00000000..ac771760 --- /dev/null +++ b/cadence/contracts/mocks/MockOracle.cdc @@ -0,0 +1,69 @@ +import "FungibleToken" + +import "DFB" + +/// +/// THIS CONTRACT IS A MOCK AND IS NOT INTENDED FOR USE IN PRODUCTION +/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +/// +access(all) contract MockOracle { + + /// token price denominated in USD + access(self) let mockedPrices: {Type: UFix64} + /// the token type in which prices are denominated + access(self) let unitOfAccount: Type + /// bps up or down by which current price moves when bumpPrice is called + access(self) let bumpVariance: UInt16 + + access(all) struct PriceOracle : DFB.PriceOracle { + + /// Returns the asset type serving as the price basis - e.g. USD in FLOW/USD + access(all) view fun unitOfAccount(): Type { + return MockOracle.unitOfAccount + } + + /// Returns the latest price data for a given asset denominated in unitOfAccount() + access(all) fun price(ofToken: Type): UFix64? { + if ofToken == self.unitOfAccount() { + return 1.0 + } + return MockOracle.mockedPrices[ofToken] + } + } + + // resets the price of the token within 0-bumpVariance (bps) of the current price + // allows for mocked data to have variability + access(all) fun bumpPrice(forToken: Type) { + if forToken == self.unitOfAccount { + return + } + let current = self.mockedPrices[forToken] + ?? panic("MockOracle does not have a price set for token \(forToken.identifier)") + let sign = revertibleRandom(modulo: 2) // 0 - down | 1 - up + let variance = self.convertToBPS(revertibleRandom(modulo: self.bumpVariance)) // bps up or down + if sign == 0 { + self.mockedPrices[forToken] = current - (current * variance) + } else { + self.mockedPrices[forToken] = current + (current * variance) + } + } + + access(all) fun setPrice(forToken: Type, price: UFix64) { + self.mockedPrices[forToken] = price + } + + access(self) view fun convertToBPS(_ variance: UInt16): UFix64 { + var res = UFix64(variance) + for i in InclusiveRange(0, 3) { + res = res / 10.0 + } + return res + } + + init(unitOfAccountIdentifier: String) { + self.mockedPrices = {} + // e.g. vault.getType().identifier == 'A.0ae53cb6e3f42a79.FlowToken.Vault' + self.unitOfAccount = CompositeType(unitOfAccountIdentifier) ?? panic("Invalid unitOfAccountIdentifier \(unitOfAccountIdentifier)") + self.bumpVariance = 100 // 0.1% variance + } +} diff --git a/cadence/contracts/mocks/MockTidalProtocolConsumer.cdc b/cadence/contracts/mocks/MockTidalProtocolConsumer.cdc new file mode 100644 index 00000000..14072d8a --- /dev/null +++ b/cadence/contracts/mocks/MockTidalProtocolConsumer.cdc @@ -0,0 +1,56 @@ +import "FungibleToken" + +import "DFB" +import "TidalProtocol" + +/// THIS CONTRACT IS NOT SAFE FOR PRODUCTION - FOR TEST USE ONLY +/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +/// +/// A simple contract enabling the persistent storage of a Position similar to a pattern expected for platforms +/// building on top of TidalProtocol's lending protocol +/// +access(all) contract MockTidalProtocolConsumer { + + /// Canonical path for where the wrapper is to be stored + access(all) let WrapperStoragePath: StoragePath + + /// Opens a TidalProtocol Position and returns a PositionWrapper containing that new position + /// + access(all) + fun createPositionWrapper( + collateral: @{FungibleToken.Vault}, + issuanceSink: {DFB.Sink}, + repaymentSource: {DFB.Source}?, + pushToDrawDownSink: Bool + ): @PositionWrapper { + return <- create PositionWrapper( + position: TidalProtocol.openPosition( + collateral: <-collateral, + issuanceSink: issuanceSink, + repaymentSource: repaymentSource, + pushToDrawDownSink: pushToDrawDownSink + ) + ) + } + + /// A simple resource encapsulating a TidalProtocol Position + access(all) resource PositionWrapper { + + access(self) let position: TidalProtocol.Position + + init(position: TidalProtocol.Position) { + self.position = position + } + + /// NOT SAFE FOR PRODUCTION + /// + /// Returns a reference to the wrapped Position + access(all) fun borrowPosition(): &TidalProtocol.Position { + return &self.position + } + } + + init() { + self.WrapperStoragePath = /storage/tidalProtocolPositionWrapper + } +} diff --git a/cadence/scripts/tidal-protocol/get_available_balance.cdc b/cadence/scripts/tidal-protocol/get_available_balance.cdc new file mode 100644 index 00000000..ad7caf93 --- /dev/null +++ b/cadence/scripts/tidal-protocol/get_available_balance.cdc @@ -0,0 +1,13 @@ +import "TidalProtocol" + +access(all) +fun main(pid: UInt64, vaultIdentifier: String, pullFromTopUpSource: Bool): UFix64 { + let vaultType = CompositeType(vaultIdentifier) ?? panic("Invalid vaultIdentifier \(vaultIdentifier)") + + let protocolAddress= Type<@TidalProtocol.Pool>().address! + + let pool = getAccount(protocolAddress).capabilities.borrow<&TidalProtocol.Pool>(TidalProtocol.PoolPublicPath) + ?? panic("Could not find a configured TidalProtocol Pool in account \(protocolAddress) at path \(TidalProtocol.PoolPublicPath)") + + return pool.availableBalance(pid: pid, type: vaultType, pullFromTopUpSource: pullFromTopUpSource) +} diff --git a/cadence/scripts/tidal-protocol/get_reserve_balance_for_type.cdc b/cadence/scripts/tidal-protocol/get_reserve_balance_for_type.cdc new file mode 100644 index 00000000..9b57511d --- /dev/null +++ b/cadence/scripts/tidal-protocol/get_reserve_balance_for_type.cdc @@ -0,0 +1,17 @@ +import "TidalProtocol" + +/// Returns the Pool's reserve balance for a given Vault type +/// +/// @param vaultIdentifier: The Type identifier (e.g. vault.getType().identifier) of the related token vault +/// +access(all) +fun main(vaultIdentifier: String): UFix64 { + let vaultType = CompositeType(vaultIdentifier) ?? panic("Invalid vaultIdentifier \(vaultIdentifier)") + + let protocolAddress= Type<@TidalProtocol.Pool>().address! + + let pool = getAccount(protocolAddress).capabilities.borrow<&TidalProtocol.Pool>(TidalProtocol.PoolPublicPath) + ?? panic("Could not find a configured TidalProtocol Pool in account \(protocolAddress) at path \(TidalProtocol.PoolPublicPath)") + + return pool.reserveBalance(type: vaultType) +} diff --git a/cadence/scripts/tidal-protocol/pool_exists.cdc b/cadence/scripts/tidal-protocol/pool_exists.cdc new file mode 100644 index 00000000..fc10792e --- /dev/null +++ b/cadence/scripts/tidal-protocol/pool_exists.cdc @@ -0,0 +1,9 @@ +import "TidalProtocol" + +/// Returns whether there is a Pool stored in the provided account's address. This address would normally be the +/// TidalProtocol contract address +/// +access(all) +fun main(address: Address): Bool { + return getAccount(address).storage.type(at: TidalProtocol.PoolStoragePath) == Type<@TidalProtocol.Pool>() +} diff --git a/cadence/scripts/tidal-protocol/position_health.cdc b/cadence/scripts/tidal-protocol/position_health.cdc new file mode 100644 index 00000000..9d3697db --- /dev/null +++ b/cadence/scripts/tidal-protocol/position_health.cdc @@ -0,0 +1,13 @@ +import "TidalProtocol" + +/// Returns the position health for a given position id, reverting if the position does not exist +/// +/// @param pid: The Position ID +/// +access(all) +fun main(pid: UInt64): UFix64 { + let protocolAddress= Type<@TidalProtocol.Pool>().address! + return getAccount(protocolAddress).capabilities.borrow<&TidalProtocol.Pool>(TidalProtocol.PoolPublicPath) + ?.positionHealth(pid: pid) + ?? panic("Could not find a configured TidalProtocol Pool in account \(protocolAddress) at path \(TidalProtocol.PoolPublicPath)") +} diff --git a/cadence/scripts/tokens/get_balance.cdc b/cadence/scripts/tokens/get_balance.cdc new file mode 100644 index 00000000..1617e427 --- /dev/null +++ b/cadence/scripts/tokens/get_balance.cdc @@ -0,0 +1,8 @@ +import "FungibleToken" + +/// Returns a account's balance of a FungibleToken Vault with public Capability published at the provided path +/// +access(all) +fun main(address: Address, vaultPublicPath: PublicPath): UFix64? { + return getAccount(address).capabilities.borrow<&{FungibleToken.Vault}>(vaultPublicPath)?.balance ?? nil +} diff --git a/cadence/tests/platform_integration_test.cdc b/cadence/tests/platform_integration_test.cdc new file mode 100644 index 00000000..141c7bfe --- /dev/null +++ b/cadence/tests/platform_integration_test.cdc @@ -0,0 +1,239 @@ +import Test +import BlockchainHelpers + +import "MOET" + +import "test_helpers.cdc" + +/* + Platform integration tests covering the path used by platforms using TidalProtocol to create + and manage new positions. These tests currently only cover the happy path, ensuring that + transactions creating & updating positions succeed. + */ + +access(all) let protocolAccount = Test.getAccount(0x0000000000000007) + +access(all) var snapshot: UInt64 = 0 + +access(all) let defaultTokenIdentifier = "A.0000000000000007.MOET.Vault" +access(all) let flowTokenIdentifier = "A.0000000000000003.FlowToken.Vault" + +access(all) let flowVaultStoragePath = /storage/flowTokenVault + +access(all) +fun setup() { + deployContracts() + + var err = Test.deployContract( + name: "MockOracle", + path: "../contracts/mocks/MockOracle.cdc", + arguments: [defaultTokenIdentifier] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "MockTidalProtocolConsumer", + path: "../contracts/mocks/MockTidalProtocolConsumer.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "FungibleTokenStack", + path: "../../DeFiBlocks/cadence/contracts/connectors/FungibleTokenStack.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) +} + +access(all) +fun testDeploymentSucceeds() { + log("Success: contracts deployed") +} + +access(all) +fun testCreatePoolSucceeds() { + snapshot = getCurrentBlockHeight() + + createAndStorePool(signer: protocolAccount, defaultTokenIdentifier: defaultTokenIdentifier, beFailed: false) + + let existsRes = executeScript("../scripts/tidal-protocol/pool_exists.cdc", [protocolAccount.address]) + Test.expect(existsRes, Test.beSucceeded()) + + let exists = existsRes.returnValue as! Bool + Test.assert(exists) +} + +access(all) +fun testCreateUserPositionSucceeds() { + Test.reset(to: snapshot) + + // mock setup + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: flowTokenIdentifier, price: 1.0) + + // create pool & add FLOW as supported token in globalLedger + createAndStorePool(signer: protocolAccount, defaultTokenIdentifier: defaultTokenIdentifier, beFailed: false) + addSupportedTokenSimpleInterestCurve( + signer: protocolAccount, + tokenTypeIdentifier: flowTokenIdentifier, + collateralFactor: 0.8, + borrowFactor: 1.0, + depositRate: 1_000_000.0, + depositCapacityCap: 1_000_000.0 + ) + + let collateralAmount = 1_000.0 // FLOW + + // configure user account + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + mintFlow(to: user, amount: collateralAmount) + + // ensure user does not have a MOET balance + var moetBalance = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)! + Test.assertEqual(0.0, moetBalance) + + // ensure there is not yet a position open - fails as there are no open positions yet + getAvailableBalance(pid: 0, vaultIdentifier: defaultTokenIdentifier, pullFromTopUpSource: false, beFailed: true) + + // open the position & push to drawDownSink - forces MOET to downstream test sink which is user's MOET Vault + let res = executeTransaction("./transactions/mock-tidal-protocol-consumer/create_wrapped_position.cdc", + [collateralAmount, flowVaultStoragePath, true], // amount, vaultStoragePath, pushToDrawDownSink + user + ) + Test.expect(res, Test.beSucceeded()) + + // ensure the position is now open + let pidZeroBalance = getAvailableBalance(pid: 0, vaultIdentifier: defaultTokenIdentifier, pullFromTopUpSource: false, beFailed: false) + Test.assert(pidZeroBalance > 0.0) + + // ensure MOET has flown to the user's MOET Vault via the VaultSink provided when opening the position + moetBalance = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)! + Test.assert(moetBalance > 0.0) +} + +access(all) +fun testUndercollateralizedPositionRebalanceSucceeds() { + Test.reset(to: snapshot) + + let initialFlowPrice = 1.0 // initial price of FLOW set in the mock oracle + let priceChange = 0.2 // the percentage difference in the price of FLOW + + // mock setup + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: flowTokenIdentifier, price: initialFlowPrice) + + // create pool & add FLOW as supported token in globalLedger + createAndStorePool(signer: protocolAccount, defaultTokenIdentifier: defaultTokenIdentifier, beFailed: false) + addSupportedTokenSimpleInterestCurve( + signer: protocolAccount, + tokenTypeIdentifier: flowTokenIdentifier, + collateralFactor: 0.8, + borrowFactor: 1.0, + depositRate: 1_000_000.0, + depositCapacityCap: 1_000_000.0 + ) + + let collateralAmount = 1_000.0 // FLOW used when opening the position + + // configure user account + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + mintFlow(to: user, amount: collateralAmount) + + // open the position & push to drawDownSink - forces MOET to downstream test sink which is user's MOET Vault + let res = executeTransaction("./transactions/mock-tidal-protocol-consumer/create_wrapped_position.cdc", + [collateralAmount, flowVaultStoragePath, true], // amount, vaultStoragePath, pushToDrawDownSink + user + ) + Test.expect(res, Test.beSucceeded()) + + // check how much MOET the user has after borrowing + let moetBalanceBeforeRebalance = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)! + let availableBeforePriceChange = getAvailableBalance(pid: 0, vaultIdentifier: defaultTokenIdentifier, pullFromTopUpSource: false, beFailed: false) + let healthBeforePriceChange = getPositionHealth(pid: 0, beFailed: false) + + // decrease the price of the collateral + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: flowTokenIdentifier, price: initialFlowPrice * (1.0 - priceChange)) + let availableAfterPriceChange = getAvailableBalance(pid: 0, vaultIdentifier: defaultTokenIdentifier, pullFromTopUpSource: true, beFailed: false) + let healthAfterPriceChange = getPositionHealth(pid: 0, beFailed: false) + + // rebalance should pull from the topUpSource, decreasing the MOET in the user's Vault since we use a VaultSource + // as a topUpSource when opening the Position + rebalancePosition(signer: protocolAccount, pid: 0, force: true, beFailed: false) + + let moetBalanceAfterRebalance = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)! + let healthAfterRebalance = getPositionHealth(pid: 0, beFailed: false) + + // NOTE - exact amounts are not tested here, this is purely a behavioral test though we may update these tests + + // user's MOET vault balance decreases due to withdrawal by pool via topUpSource + Test.assert(moetBalanceBeforeRebalance > moetBalanceAfterRebalance) + // the amount available should decrease after the collateral value has decreased + Test.assert(availableBeforePriceChange < availableAfterPriceChange) + // the health should decrease after the collateral value has decreased + Test.assert(healthBeforePriceChange > healthAfterPriceChange) + // the health should increase after rebalancing from undercollateralized state + Test.assert(healthAfterPriceChange < healthAfterRebalance) +} + +access(all) +fun testOvercollateralizedPositionRebalanceSucceeds() { + Test.reset(to: snapshot) + + let initialFlowPrice = 1.0 // initial price of FLOW set in the mock oracle + let priceChange = 1.2 // the percentage difference in the price of FLOW + + // mock setup + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: flowTokenIdentifier, price: initialFlowPrice) + + // create pool & add FLOW as supported token in globalLedger + createAndStorePool(signer: protocolAccount, defaultTokenIdentifier: defaultTokenIdentifier, beFailed: false) + addSupportedTokenSimpleInterestCurve( + signer: protocolAccount, + tokenTypeIdentifier: flowTokenIdentifier, + collateralFactor: 0.8, + borrowFactor: 1.0, + depositRate: 1_000_000.0, + depositCapacityCap: 1_000_000.0 + ) + + let collateralAmount = 1_000.0 // FLOW used when opening the position + + // configure user account + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + mintFlow(to: user, amount: collateralAmount) + + // open the position & push to drawDownSink - forces MOET to downstream test sink which is user's MOET Vault + let res = executeTransaction("./transactions/mock-tidal-protocol-consumer/create_wrapped_position.cdc", + [collateralAmount, flowVaultStoragePath, true], // amount, vaultStoragePath, pushToDrawDownSink + user + ) + Test.expect(res, Test.beSucceeded()) + + // check how much MOET the user has after borrowing + let moetBalanceBeforeRebalance = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)! + let availableBeforePriceChange = getAvailableBalance(pid: 0, vaultIdentifier: defaultTokenIdentifier, pullFromTopUpSource: false, beFailed: false) + let healthBeforePriceChange = getPositionHealth(pid: 0, beFailed: false) + + // decrease the price of the collateral + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: flowTokenIdentifier, price: initialFlowPrice * (priceChange)) + let availableAfterPriceChange = getAvailableBalance(pid: 0, vaultIdentifier: defaultTokenIdentifier, pullFromTopUpSource: true, beFailed: false) + let healthAfterPriceChange = getPositionHealth(pid: 0, beFailed: false) + + // rebalance should pull from the topUpSource, decreasing the MOET in the user's Vault since we use a VaultSource + // as a topUpSource when opening the Position + rebalancePosition(signer: protocolAccount, pid: 0, force: true, beFailed: false) + + let moetBalanceAfterRebalance = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)! + let healthAfterRebalance = getPositionHealth(pid: 0, beFailed: false) + + // NOTE - exact amounts are not tested here, this is purely a behavioral test though we may update these tests + + // user's MOET vault balance increase due to deposit by pool to drawDownSink + Test.assert(moetBalanceBeforeRebalance < moetBalanceAfterRebalance) + // the amount available increase after the collateral value has increased + Test.assert(availableBeforePriceChange < availableAfterPriceChange) + // the health should increase after the collateral value has decreased + Test.assert(healthBeforePriceChange < healthAfterPriceChange) + // the health should decrease after rebalancing from overcollateralized state + Test.assert(healthAfterPriceChange > healthAfterRebalance) +} diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc new file mode 100644 index 00000000..0f6c5a82 --- /dev/null +++ b/cadence/tests/test_helpers.cdc @@ -0,0 +1,148 @@ +import Test +import "TidalProtocol" + +/* --- Test execution helpers --- */ + +access(all) +fun _executeScript(_ path: String, _ args: [AnyStruct]): Test.ScriptResult { + return Test.executeScript(Test.readFile(path), args) +} + +access(all) +fun _executeTransaction(_ path: String, _ args: [AnyStruct], _ signer: Test.TestAccount): Test.TransactionResult { + let txn = Test.Transaction( + code: Test.readFile(path), + authorizers: [signer.address], + signers: [signer], + arguments: args + ) + return Test.executeTransaction(txn) +} + +/* --- Setup helpers --- */ + +// Common test setup function that deploys all required contracts +access(all) fun deployContracts() { + var err = Test.deployContract( + name: "DFBUtils", + path: "../../DeFiBlocks/cadence/contracts/utils/DFBUtils.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "DFB", + path: "../../DeFiBlocks/cadence/contracts/interfaces/DFB.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + let initialSupply = 0.0 + err = Test.deployContract( + name: "MOET", + path: "../contracts/MOET.cdc", + arguments: [initialSupply] + ) + Test.expect(err, Test.beNil()) + + err = Test.deployContract( + name: "TidalProtocol", + path: "../contracts/TidalProtocol.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) +} + +/* --- Script Helpers --- */ + +access(all) +fun getBalance(address: Address, vaultPublicPath: PublicPath): UFix64? { + let res = _executeScript("../scripts/tokens/get_balance.cdc", [address, vaultPublicPath]) + Test.expect(res, Test.beSucceeded()) + return res.returnValue as! UFix64? +} + +access(all) +fun getReserveBalance(vaultIdentifier: String): UFix64 { + let res = _executeScript("../scripts/tidal-protocol/get_reserve_balance_for_type.cdc", [vaultIdentifier]) + Test.expect(res, Test.beSucceeded()) + return res.returnValue as! UFix64 +} + +access(all) +fun getAvailableBalance(pid: UInt64, vaultIdentifier: String, pullFromTopUpSource: Bool, beFailed: Bool): UFix64 { + let res = _executeScript("../scripts/tidal-protocol/get_available_balance.cdc", + [pid, vaultIdentifier, pullFromTopUpSource] + ) + Test.expect(res, beFailed ? Test.beFailed() : Test.beSucceeded()) + return res.status == Test.ResultStatus.failed ? 0.0 : res.returnValue as! UFix64 +} + +access(all) +fun getPositionHealth(pid: UInt64, beFailed: Bool): UFix64 { + let res = _executeScript("../scripts/tidal-protocol/position_health.cdc", + [pid] + ) + Test.expect(res, beFailed ? Test.beFailed() : Test.beSucceeded()) + return res.status == Test.ResultStatus.failed ? 0.0 : res.returnValue as! UFix64 +} + +/* --- Transaction Helpers --- */ + +access(all) +fun createAndStorePool(signer: Test.TestAccount, defaultTokenIdentifier: String, beFailed: Bool) { + let createRes = _executeTransaction( + "../transactions/tidal-protocol/pool-factory/create_and_store_pool.cdc", + [defaultTokenIdentifier], + signer + ) + Test.expect(createRes, beFailed ? Test.beFailed() : Test.beSucceeded()) +} + +access(all) +fun setMockOraclePrice(signer: Test.TestAccount, forTokenIdentifier: String, price: UFix64) { + let setRes = _executeTransaction( + "./transactions/mock-oracle/set_price.cdc", + [forTokenIdentifier, price], + signer + ) + Test.expect(setRes, Test.beSucceeded()) +} + +access(all) +fun addSupportedTokenSimpleInterestCurve( + signer: Test.TestAccount, + tokenTypeIdentifier: String, + collateralFactor: UFix64, + borrowFactor: UFix64, + depositRate: UFix64, + depositCapacityCap: UFix64 +) { + let additionRes = _executeTransaction( + "../transactions/tidal-protocol/pool-governance/add_supported_token_simple_interest_curve.cdc", + [ tokenTypeIdentifier, collateralFactor, borrowFactor, depositRate, depositCapacityCap ], + signer + ) + Test.expect(additionRes, Test.beSucceeded()) +} + +access(all) +fun rebalancePosition(signer: Test.TestAccount, pid: UInt64, force: Bool, beFailed: Bool) { + let rebalanceRes = _executeTransaction( + "../transactions/tidal-protocol/pool-management/rebalance_position.cdc", + [ pid, force ], + signer + ) + Test.expect(rebalanceRes, beFailed ? Test.beFailed() : Test.beSucceeded()) +} + +access(all) +fun setupMoetVault(_ signer: Test.TestAccount, beFailed: Bool) { + let setupRes = _executeTransaction("../transactions/moet/setup_vault.cdc", [], signer) + Test.expect(setupRes, beFailed ? Test.beFailed() : Test.beSucceeded()) +} + +access(all) +fun mintMoet(signer: Test.TestAccount, to: Address, amount: UFix64, beFailed: Bool) { + let mintRes = _executeTransaction("../transactions/moet/mint_moet.cdc", [to, amount], signer) + Test.expect(mintRes, beFailed ? Test.beFailed() : Test.beSucceeded()) +} diff --git a/cadence/tests/transactions/mock-oracle/set_price.cdc b/cadence/tests/transactions/mock-oracle/set_price.cdc new file mode 100644 index 00000000..285e9939 --- /dev/null +++ b/cadence/tests/transactions/mock-oracle/set_price.cdc @@ -0,0 +1,18 @@ +import "MockOracle" + +/// Upserts the provided Token & price pairing in the MockOracle contract +/// +/// @param vaultIdentifier: The Vault's Type identifier +/// e.g. vault.getType().identifier == 'A.0ae53cb6e3f42a79.FlowToken.Vault' +/// @param price: The price to set the token to in the MockOracle denominated in USD +transaction(forTokenIdentifier: String, price: UFix64) { + let tokenType: Type + + prepare(signer: &Account) { + self.tokenType = CompositeType(forTokenIdentifier) ?? panic("Invalid Type \(forTokenIdentifier)") + } + + execute { + MockOracle.setPrice(forToken: self.tokenType, price: price) + } +} diff --git a/cadence/tests/transactions/mock-tidal-protocol-consumer/create_wrapped_position.cdc b/cadence/tests/transactions/mock-tidal-protocol-consumer/create_wrapped_position.cdc new file mode 100644 index 00000000..059c5fc6 --- /dev/null +++ b/cadence/tests/transactions/mock-tidal-protocol-consumer/create_wrapped_position.cdc @@ -0,0 +1,76 @@ +import "FungibleToken" + +import "DFB" +import "FungibleTokenStack" + +import "MOET" +import "MockTidalProtocolConsumer" + +/// TEST TRANSACTION - DO NOT USE IN PRODUCTION +/// +/// Opens a Position with the amount of funds source from the Vault at the provided StoragePath and wraps it in a +/// MockTidalProtocolConsumer PositionWrapper +/// +transaction(amount: UFix64, vaultStoragePath: StoragePath, pushToDrawDownSink: Bool) { + + // the funds that will be used as collateral for a TidalProtocol loan + let collateral: @{FungibleToken.Vault} + // this DeFiBlocks Sink that will receive the loaned funds + let sink: {DFB.Sink} + // DEBUG: this DeFiBlocks Source that will allow for the repayment of a loan if the position becomes undercollateralized + let source: {DFB.Source} + // the signer's account in which to store a PositionWrapper + let account: auth(SaveValue) &Account + + prepare(signer: auth(BorrowValue, SaveValue, IssueStorageCapabilityController, PublishCapability, UnpublishCapability) &Account) { + // configure a MOET Vault to receive the loaned amount + if signer.storage.type(at: MOET.VaultStoragePath) == nil { + // save a new MOET Vault + signer.storage.save(<-MOET.createEmptyVault(vaultType: Type<@MOET.Vault>()), to: MOET.VaultStoragePath) + // issue un-entitled Capability + let vaultCap = signer.capabilities.storage.issue<&MOET.Vault>(MOET.VaultStoragePath) + // publish receiver Capability, unpublishing any that may exist to prevent collision + signer.capabilities.unpublish(MOET.VaultPublicPath) + signer.capabilities.publish(vaultCap, at: MOET.VaultPublicPath) + } + // assign a Vault Capability to be used in the VaultSink + let depositVaultCap = signer.capabilities.get<&{FungibleToken.Vault}>(MOET.VaultPublicPath) + let withdrawVaultCap = signer.capabilities.storage.issue(MOET.VaultStoragePath) + assert(depositVaultCap.check(), + message: "Invalid MOET Vault public Capability issued - ensure the Vault is properly configured") + assert(withdrawVaultCap.check(), + message: "Invalid MOET Vault private Capability issued - ensure the Vault is properly configured") + + // withdraw the collateral from the signer's stored Vault + let collateralSource = signer.storage.borrow(from: vaultStoragePath) + ?? panic("Could not borrow reference to Vault from \(vaultStoragePath)") + self.collateral <- collateralSource.withdraw(amount: amount) + // construct the DeFiBlocks Sink that will receive the loaned amount + let uniqueID = DFB.UniqueIdentifier() + self.sink = FungibleTokenStack.VaultSink( + max: nil, + depositVault: depositVaultCap, + uniqueID: uniqueID + ) + self.source = FungibleTokenStack.VaultSource( + min: nil, + withdrawVault: withdrawVaultCap, + uniqueID: uniqueID + ) + + // assign the signer's account enabling the execute block to save the wrapper + self.account = signer + } + + execute { + // open a position & save in the Wrapper + let wrapper <- MockTidalProtocolConsumer.createPositionWrapper( + collateral: <-self.collateral, + issuanceSink: self.sink, + repaymentSource: self.source, + pushToDrawDownSink: pushToDrawDownSink + ) + // save the wrapper into the signer's account - reverts on storage collision + self.account.storage.save(<-wrapper, to: MockTidalProtocolConsumer.WrapperStoragePath) + } +} diff --git a/cadence/transactions/mocks/oracle/bump_price.cdc b/cadence/transactions/mocks/oracle/bump_price.cdc new file mode 100644 index 00000000..2fbffe5c --- /dev/null +++ b/cadence/transactions/mocks/oracle/bump_price.cdc @@ -0,0 +1,18 @@ +import "MockOracle" + +/// Bumps the current token price in the MockOracle contract by some percentage (up or down) between +/// 0-bumpVariance (default 1%) randomly chosen using revertibleRandom +/// +/// @param vaultIdentifier: The Vault's Type identifier +/// e.g. vault.getType().identifier == 'A.0ae53cb6e3f42a79.FlowToken.Vault' +transaction(forTokenIdentifier: String) { + let tokenType: Type + + prepare(signer: &Account) { + self.tokenType = CompositeType(forTokenIdentifier) ?? panic("Invalid Type \(forTokenIdentifier)") + } + + execute { + MockOracle.bumpPrice(forToken: self.tokenType) + } +} diff --git a/cadence/transactions/mocks/oracle/set_price.cdc b/cadence/transactions/mocks/oracle/set_price.cdc new file mode 100644 index 00000000..285e9939 --- /dev/null +++ b/cadence/transactions/mocks/oracle/set_price.cdc @@ -0,0 +1,18 @@ +import "MockOracle" + +/// Upserts the provided Token & price pairing in the MockOracle contract +/// +/// @param vaultIdentifier: The Vault's Type identifier +/// e.g. vault.getType().identifier == 'A.0ae53cb6e3f42a79.FlowToken.Vault' +/// @param price: The price to set the token to in the MockOracle denominated in USD +transaction(forTokenIdentifier: String, price: UFix64) { + let tokenType: Type + + prepare(signer: &Account) { + self.tokenType = CompositeType(forTokenIdentifier) ?? panic("Invalid Type \(forTokenIdentifier)") + } + + execute { + MockOracle.setPrice(forToken: self.tokenType, price: price) + } +} diff --git a/cadence/transactions/moet/mint_moet.cdc b/cadence/transactions/moet/mint_moet.cdc new file mode 100644 index 00000000..1a0ddd43 --- /dev/null +++ b/cadence/transactions/moet/mint_moet.cdc @@ -0,0 +1,29 @@ +import "FungibleToken" + +import "MOET" + +/// Mints MOET using the Minter stored in the signer's account and deposits to the recipients MOET Vault. If the +/// recipient's MOET Vault is not configured with a public Capability or the signer does not have a MOET Minter +/// stored, the transaction will revert. +/// +/// @param to: The recipient's Flow address +/// @param amount: How many MOET tokens to mint to the recipient's account +/// +transaction(to: Address, amount: UFix64) { + + let receiver: &{FungibleToken.Vault} + let minter: &MOET.Minter + + prepare(signer: auth(BorrowValue) &Account) { + self.minter = signer.storage.borrow<&MOET.Minter>(from: MOET.AdminStoragePath) + ?? panic("Could not borrow reference to MOET Minter from signer's account at path \(MOET.AdminStoragePath)") + self.receiver = getAccount(to).capabilities.borrow<&{FungibleToken.Vault}>(MOET.VaultPublicPath) + ?? panic("Could not borrow reference to MOET Vault from recipient's account at path \(MOET.VaultPublicPath)") + } + + execute { + self.receiver.deposit( + from: <-self.minter.mintTokens(amount: amount) + ) + } +} diff --git a/cadence/transactions/moet/setup_vault.cdc b/cadence/transactions/moet/setup_vault.cdc new file mode 100644 index 00000000..3dfa7810 --- /dev/null +++ b/cadence/transactions/moet/setup_vault.cdc @@ -0,0 +1,29 @@ +import "FungibleToken" + +import "MOET" + +/// Creates & stores a MOET Vault in the signer's account, also configuring its public Vault Capability +/// +transaction { + + prepare(signer: auth(BorrowValue, SaveValue, IssueStorageCapabilityController, PublishCapability, UnpublishCapability) &Account) { + // configure if nothing is found at canonical path + if signer.storage.type(at: MOET.VaultStoragePath) == nil { + // save the new vault + signer.storage.save(<-MOET.createEmptyVault(vaultType: Type<@MOET.Vault>()), to: MOET.VaultStoragePath) + // publish a public capability on the Vault + let cap = signer.capabilities.storage.issue<&{FungibleToken.Vault}>(MOET.VaultStoragePath) + signer.capabilities.unpublish(MOET.VaultPublicPath) + signer.capabilities.unpublish(MOET.ReceiverPublicPath) + signer.capabilities.publish(cap, at: MOET.VaultPublicPath) + signer.capabilities.publish(cap, at: MOET.ReceiverPublicPath) + // issue an authorized capability to initialize a CapabilityController on the account, but do not publish + signer.capabilities.storage.issue(MOET.VaultStoragePath) + } + + // ensure proper configuration + if signer.storage.type(at: MOET.VaultStoragePath) != Type<@MOET.Vault>(){ + panic("Could not configure MOET Vault at \(MOET.VaultStoragePath) - check for collision and try again") + } + } +} diff --git a/cadence/transactions/tidal-protocol/pool-factory/create_and_store_pool.cdc b/cadence/transactions/tidal-protocol/pool-factory/create_and_store_pool.cdc new file mode 100644 index 00000000..164ea283 --- /dev/null +++ b/cadence/transactions/tidal-protocol/pool-factory/create_and_store_pool.cdc @@ -0,0 +1,30 @@ +import "FungibleToken" + +import "DFB" +import "TidalProtocol" +import "MockOracle" + +/// THIS TRANSACTION IS NOT INTENDED FOR PRODUCTION +/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +/// +/// Creates the protocol pool in the TidalProtocol account via the stored PoolFactory resource +/// +/// @param defaultTokenIdentifier: The Type identifier (e.g. resource.getType().identifier) of the Pool's default token +/// +transaction(defaultTokenIdentifier: String) { + + let factory: &TidalProtocol.PoolFactory + let defaultToken: Type + let oracle: {DFB.PriceOracle} + + prepare(signer: auth(BorrowValue) &Account) { + self.factory = signer.storage.borrow<&TidalProtocol.PoolFactory>(from: TidalProtocol.PoolFactoryPath) + ?? panic("Could not find PoolFactory in signer's account") + self.defaultToken = CompositeType(defaultTokenIdentifier) ?? panic("Invalid defaultTokenIdentifier \(defaultTokenIdentifier)") + self.oracle = MockOracle.PriceOracle() + } + + execute { + self.factory.createPool(defaultToken: self.defaultToken, priceOracle: self.oracle) + } +} diff --git a/cadence/transactions/tidal-protocol/pool-governance/add_supported_token_simple_interest_curve.cdc b/cadence/transactions/tidal-protocol/pool-governance/add_supported_token_simple_interest_curve.cdc new file mode 100644 index 00000000..e2468a77 --- /dev/null +++ b/cadence/transactions/tidal-protocol/pool-governance/add_supported_token_simple_interest_curve.cdc @@ -0,0 +1,32 @@ +import "TidalProtocol" + +/// Adds a token type as supported to the stored pool, reverting if a Pool is not found +/// +transaction( + tokenTypeIdentifier: String, + collateralFactor: UFix64, + borrowFactor: UFix64, + depositRate: UFix64, + depositCapacityCap: UFix64 +) { + let tokenType: Type + let pool: auth(TidalProtocol.EGovernance) &TidalProtocol.Pool + + prepare(signer: auth(BorrowValue) &Account) { + self.tokenType = CompositeType(tokenTypeIdentifier) + ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") + self.pool = signer.storage.borrow(from: TidalProtocol.PoolStoragePath) + ?? panic("Could not borrow reference to Pool from \(TidalProtocol.PoolStoragePath) - ensure a Pool has been configured") + } + + execute { + self.pool.addSupportedToken( + tokenType: self.tokenType, + collateralFactor: collateralFactor, + borrowFactor: borrowFactor, + interestCurve: TidalProtocol.SimpleInterestCurve(), + depositRate: depositRate, + depositCapacityCap: depositCapacityCap + ) + } +} diff --git a/cadence/transactions/tidal-protocol/pool-management/rebalance_position.cdc b/cadence/transactions/tidal-protocol/pool-management/rebalance_position.cdc new file mode 100644 index 00000000..d8923ed5 --- /dev/null +++ b/cadence/transactions/tidal-protocol/pool-management/rebalance_position.cdc @@ -0,0 +1,20 @@ +import "TidalProtocol" + +/// Rebalances a TidalProtocol position by it's Position ID with the provided `force` value +/// +/// @param pid: The position ID to rebalance +/// @param force: Whether the rebalance execution should be forced or not. If `false`, the rebalance executes only if +/// the position is beyond its min/max health. If `true`, the rebalance executes regardless of its relative health. +/// +transaction(pid: UInt64, force: Bool) { + let pool: auth(TidalProtocol.EPosition) &TidalProtocol.Pool + + prepare(signer: auth(BorrowValue) &Account) { + self.pool = signer.storage.borrow(from: TidalProtocol.PoolStoragePath) + ?? panic("Could not borrow reference to Pool from \(TidalProtocol.PoolStoragePath) - ensure a Pool has been configured") + } + + execute { + self.pool.rebalancePosition(pid: pid, force: force) + } +} \ No newline at end of file diff --git a/flow.json b/flow.json index 6fccc3f4..3deb2769 100644 --- a/flow.json +++ b/flow.json @@ -1,9 +1,33 @@ { "contracts": { + "FungibleTokenStack": { + "source": "./DeFiBlocks/cadence/contracts/connectors/FungibleTokenStack.cdc", + "aliases": { + "testing": "0x0000000000000006" + } + }, "DFB": { "source": "./DeFiBlocks/cadence/contracts/interfaces/DFB.cdc", "aliases": { - "testing": "0000000000000008" + "testing": "0x0000000000000006" + } + }, + "DFBUtils": { + "source": "./DeFiBlocks/cadence/contracts/utils/DFBUtils.cdc", + "aliases": { + "testing": "0x0000000000000006" + } + }, + "MockOracle": { + "source": "./cadence/contracts/mocks/MockOracle.cdc", + "aliases": { + "testing": "0000000000000007" + } + }, + "MockTidalProtocolConsumer": { + "source": "./cadence/contracts/mocks/MockTidalProtocolConsumer.cdc", + "aliases": { + "testing": "0000000000000007" } }, "TidalProtocol": { @@ -15,7 +39,7 @@ "MOET": { "source": "./cadence/contracts/MOET.cdc", "aliases": { - "testing": "0000000000000009" + "testing": "0000000000000007" } } }, @@ -62,7 +86,8 @@ "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1d7e57aa55817448", - "testnet": "631e88ae7f1d7c20" + "testnet": "631e88ae7f1d7c20", + "testing": "0x0000000000000001" } }, "NonFungibleToken": { @@ -71,7 +96,8 @@ "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1d7e57aa55817448", - "testnet": "631e88ae7f1d7c20" + "testnet": "631e88ae7f1d7c20", + "testing": "0x0000000000000001" } }, "ViewResolver": { @@ -80,7 +106,8 @@ "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1d7e57aa55817448", - "testnet": "631e88ae7f1d7c20" + "testnet": "631e88ae7f1d7c20", + "testing": "0x0000000000000001" } } }, @@ -102,13 +129,7 @@ "deployments": { "emulator": { "emulator-account": [ - "DFB", - "TidalProtocol", - "MOET" - ] - }, - "testing": { - "emulator-account": [ + "DFBUtils", "DFB", "TidalProtocol", "MOET"