diff --git a/.gitignore b/.gitignore index 6528a0da..935312f9 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ coverage.json /.pr-body.md /.github/pr_bodies/ lcov.info +.flow-fork-cache diff --git a/cadence/contracts/FlowALPPositionResources.cdc b/cadence/contracts/FlowALPPositionResources.cdc index ac53efb8..6284af1d 100644 --- a/cadence/contracts/FlowALPPositionResources.cdc +++ b/cadence/contracts/FlowALPPositionResources.cdc @@ -7,6 +7,19 @@ import "FlowALPModels" access(all) contract FlowALPPositionResources { + /// A single authorized Capability to the Pool, shared across all Position resources. + /// Issued once at pool creation time to avoid per-position capability controller bloat. + access(self) var poolCap: Capability? + + /// Sets the contract-level pool capability. Called once by FlowALPv0 when the Pool is created. + access(account) fun setPoolCap(cap: Capability) { + FlowALPPositionResources.poolCap = cap + } + + access(self) fun borrowPool(): auth(FlowALPModels.EPosition) &{FlowALPModels.PositionPool} { + return self.poolCap!.borrow()! + } + /// Position /// /// A Position is a resource representing ownership of value deposited to the protocol. @@ -23,24 +36,13 @@ access(all) contract FlowALPPositionResources { /// The unique ID of the Position used to track deposits and withdrawals to the Pool access(all) let id: UInt64 - /// An authorized Capability to the Pool for which this Position was opened. - access(self) let pool: Capability - - init( - id: UInt64, - pool: Capability - ) { - pre { - pool.check(): - "Invalid Pool Capability provided - cannot construct Position" - } + init(id: UInt64) { self.id = id - self.pool = pool } /// Returns the balances (both positive and negative) for all tokens in this position. access(all) fun getBalances(): [FlowALPModels.PositionBalance] { - let pool = self.pool.borrow()! + let pool = FlowALPPositionResources.borrowPool() return pool.getPositionDetails(pid: self.id).balances } @@ -49,54 +51,54 @@ access(all) contract FlowALPPositionResources { /// below its min health. If pullFromTopUpSource is false, 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()! + let pool = FlowALPPositionResources.borrowPool() return pool.availableBalance(pid: self.id, type: type, pullFromTopUpSource: pullFromTopUpSource) } /// Returns the current health of the position access(all) fun getHealth(): UFix128 { - let pool = self.pool.borrow()! + let pool = FlowALPPositionResources.borrowPool() return pool.positionHealth(pid: self.id) } /// Returns the Position's target health (unitless ratio ≥ 1.0) access(all) fun getTargetHealth(): UFix64 { - let pool = self.pool.borrow()! + let pool = FlowALPPositionResources.borrowPool() let pos = pool.borrowPosition(pid: self.id) return FlowALPMath.toUFix64Round(pos.getTargetHealth()) } /// Sets the target health of the Position access(FlowALPModels.EPositionAdmin) fun setTargetHealth(targetHealth: UFix64) { - let pool = self.pool.borrow()! + let pool = FlowALPPositionResources.borrowPool() let pos = pool.borrowPosition(pid: self.id) pos.setTargetHealth(UFix128(targetHealth)) } /// Returns the minimum health of the Position access(all) fun getMinHealth(): UFix64 { - let pool = self.pool.borrow()! + let pool = FlowALPPositionResources.borrowPool() let pos = pool.borrowPosition(pid: self.id) return FlowALPMath.toUFix64Round(pos.getMinHealth()) } /// Sets the minimum health of the Position access(FlowALPModels.EPositionAdmin) fun setMinHealth(minHealth: UFix64) { - let pool = self.pool.borrow()! + let pool = FlowALPPositionResources.borrowPool() let pos = pool.borrowPosition(pid: self.id) pos.setMinHealth(UFix128(minHealth)) } /// Returns the maximum health of the Position access(all) fun getMaxHealth(): UFix64 { - let pool = self.pool.borrow()! + let pool = FlowALPPositionResources.borrowPool() let pos = pool.borrowPosition(pid: self.id) return FlowALPMath.toUFix64Round(pos.getMaxHealth()) } /// Sets the maximum health of the position access(FlowALPModels.EPositionAdmin) fun setMaxHealth(maxHealth: UFix64) { - let pool = self.pool.borrow()! + let pool = FlowALPPositionResources.borrowPool() let pos = pool.borrowPosition(pid: self.id) pos.setMaxHealth(UFix128(maxHealth)) } @@ -123,7 +125,7 @@ access(all) contract FlowALPPositionResources { from: @{FungibleToken.Vault}, pushToDrawDownSink: Bool ) { - let pool = self.pool.borrow()! + let pool = FlowALPPositionResources.borrowPool() pool.depositAndPush( pid: self.id, from: <-from, @@ -148,7 +150,7 @@ access(all) contract FlowALPPositionResources { amount: UFix64, pullFromTopUpSource: Bool ): @{FungibleToken.Vault} { - let pool = self.pool.borrow()! + let pool = FlowALPPositionResources.borrowPool() return <- pool.withdrawAndPull( pid: self.id, type: type, @@ -179,10 +181,8 @@ access(all) contract FlowALPPositionResources { type: Type, pushToDrawDownSink: Bool ): {DeFiActions.Sink} { - let pool = self.pool.borrow()! return PositionSink( id: self.id, - pool: self.pool, type: type, pushToDrawDownSink: pushToDrawDownSink ) @@ -210,10 +210,8 @@ access(all) contract FlowALPPositionResources { type: Type, pullFromTopUpSource: Bool ): {DeFiActions.Source} { - let pool = self.pool.borrow()! return PositionSource( id: self.id, - pool: self.pool, type: type, pullFromTopUpSource: pullFromTopUpSource ) @@ -230,7 +228,7 @@ access(all) contract FlowALPPositionResources { /// /// Pass nil to configure the position to not push tokens when the Position exceeds its maximum health. access(FlowALPModels.EPositionAdmin) fun provideSink(sink: {DeFiActions.Sink}?) { - let pool = self.pool.borrow()! + let pool = FlowALPPositionResources.borrowPool() pool.lockPosition(self.id) let pos = pool.borrowPosition(pid: self.id) pos.setDrawDownSink(sink) @@ -246,7 +244,7 @@ access(all) contract FlowALPPositionResources { /// /// Pass nil to configure the position to not pull tokens. access(FlowALPModels.EPositionAdmin) fun provideSource(source: {DeFiActions.Source}?) { - let pool = self.pool.borrow()! + let pool = FlowALPPositionResources.borrowPool() pool.lockPosition(self.id) let pos = pool.borrowPosition(pid: self.id) pos.setTopUpSource(source) @@ -262,7 +260,7 @@ access(all) contract FlowALPPositionResources { /// of either cannot accept/provide sufficient funds for rebalancing, the rebalance will still occur but will /// not cause the position to reach its target health. access(FlowALPModels.EPosition | FlowALPModels.ERebalance) fun rebalance(force: Bool) { - let pool = self.pool.borrow()! + let pool = FlowALPPositionResources.borrowPool() pool.rebalancePosition(pid: self.id, force: force) } } @@ -320,11 +318,8 @@ access(all) contract FlowALPPositionResources { /// Creates and returns a new Position resource. /// This remains account-scoped so only the protocol account can mint canonical wrappers. - access(account) fun createPosition( - id: UInt64, - pool: Capability - ): @Position { - return <- create Position(id: id, pool: pool) + access(account) fun createPosition(id: UInt64): @Position { + return <- create Position(id: id) } /// Creates and returns a new PositionManager resource @@ -342,9 +337,6 @@ access(all) contract FlowALPPositionResources { /// An optional DeFiActions.UniqueIdentifier that identifies this Sink with the DeFiActions stack its a part of access(contract) var uniqueID: DeFiActions.UniqueIdentifier? - /// An authorized Capability on the Pool for which the related Position is in - access(self) let pool: Capability - /// The ID of the position in the Pool access(self) let positionID: UInt64 @@ -357,13 +349,11 @@ access(all) contract FlowALPPositionResources { init( id: UInt64, - pool: Capability, type: Type, pushToDrawDownSink: Bool ) { self.uniqueID = nil self.positionID = id - self.pool = pool self.type = type self.pushToDrawDownSink = pushToDrawDownSink } @@ -375,13 +365,13 @@ access(all) contract FlowALPPositionResources { /// Returns the minimum capacity this Sink can accept as deposits access(all) fun minimumCapacity(): UFix64 { - return self.pool.check() ? UFix64.max : 0.0 + return FlowALPPositionResources.poolCap?.check() ?? false ? 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}) { - if let pool = self.pool.borrow() { - pool.depositAndPush( + if let pool = FlowALPPositionResources.poolCap?.borrow() { + pool!.depositAndPush( pid: self.positionID, from: <-from.withdraw(amount: from.balance), pushToDrawDownSink: self.pushToDrawDownSink @@ -416,9 +406,6 @@ access(all) contract FlowALPPositionResources { /// An optional DeFiActions.UniqueIdentifier that identifies this Sink with the DeFiActions stack its a part of access(contract) var uniqueID: DeFiActions.UniqueIdentifier? - /// An authorized Capability on the Pool for which the related Position is in - access(self) let pool: Capability - /// The ID of the position in the Pool access(self) let positionID: UInt64 @@ -431,13 +418,11 @@ access(all) contract FlowALPPositionResources { init( id: UInt64, - pool: Capability, type: Type, pullFromTopUpSource: Bool ) { self.uniqueID = nil self.positionID = id - self.pool = pool self.type = type self.pullFromTopUpSource = pullFromTopUpSource } @@ -449,11 +434,11 @@ access(all) contract FlowALPPositionResources { /// Returns the minimum available this Source can provide on withdrawal access(all) fun minimumAvailable(): UFix64 { - if !self.pool.check() { + if FlowALPPositionResources.poolCap?.check() != true { return 0.0 } - let pool = self.pool.borrow()! + let pool = FlowALPPositionResources.borrowPool() return pool.availableBalance( pid: self.positionID, type: self.type, @@ -463,11 +448,11 @@ access(all) contract FlowALPPositionResources { /// Withdraws up to the max amount as the sourceType Vault access(FungibleToken.Withdraw) fun withdrawAvailable(maxAmount: UFix64): @{FungibleToken.Vault} { - if !self.pool.check() { + if FlowALPPositionResources.poolCap?.check() != true { return <- DeFiActionsUtils.getEmptyVault(self.type) } - let pool = self.pool.borrow()! + let pool = FlowALPPositionResources.borrowPool() let available = pool.availableBalance( pid: self.positionID, type: self.type, @@ -503,4 +488,8 @@ access(all) contract FlowALPPositionResources { self.uniqueID = id } } + + init() { + self.poolCap = nil + } } diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 5051d079..2b5c557a 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -314,7 +314,7 @@ access(all) contract FlowALPv0 { if let tokenState = self.state.getTokenState(tokenType) { return tokenState.getInsuranceRate() } - + return nil } @@ -549,7 +549,7 @@ access(all) contract FlowALPv0 { post { !self.state.isPositionLocked(pid): "Position is not unlocked" } - + self.lockPosition(pid) let positionView = self.buildPositionView(pid: pid) @@ -569,7 +569,7 @@ access(all) contract FlowALPv0 { let Pc_oracle = self.config.getPriceOracle().price(ofToken: seizeType)! // collateral price given by oracle ($/C) // Price of collateral, denominated in debt token, implied by oracle (D/C) // Oracle says: "1 unit of collateral is worth `Pcd_oracle` units of debt" - let Pcd_oracle = Pc_oracle / Pd_oracle + let Pcd_oracle = Pc_oracle / Pd_oracle // Compute the health factor which would result if we were to accept this liquidation let Ce_pre = balanceSheet.effectiveCollateral // effective collateral pre-liquidation @@ -580,7 +580,7 @@ access(all) contract FlowALPv0 { // Ce_seize = effective value of seized collateral ($) let Ce_seize = FlowALPMath.effectiveCollateral(credit: UFix128(seizeAmount), price: UFix128(Pc_oracle), collateralFactor: Fc) // De_seize = effective value of repaid debt ($) - let De_seize = FlowALPMath.effectiveDebt(debit: UFix128(repayAmount), price: UFix128(Pd_oracle), borrowFactor: Fd) + let De_seize = FlowALPMath.effectiveDebt(debit: UFix128(repayAmount), price: UFix128(Pd_oracle), borrowFactor: Fd) let Ce_post = Ce_pre - Ce_seize // position's total effective collateral after liquidation ($) let De_post = De_pre - De_seize // position's total effective debt after liquidation ($) let postHealth = FlowALPMath.healthComputation(effectiveCollateral: Ce_post, effectiveDebt: De_post) @@ -599,9 +599,9 @@ access(all) contract FlowALPv0 { message: "DEX/oracle price deviation too large. Dex price: \(Pcd_dex), Oracle price: \(Pcd_oracle)") // Execute the liquidation let seizedCollateral <- self._doLiquidation(pid: pid, repayment: <-repayment, debtType: debtType, seizeType: seizeType, seizeAmount: seizeAmount) - + self.unlockPosition(pid) - + return <- seizedCollateral } @@ -611,7 +611,7 @@ access(all) contract FlowALPv0 { access(self) fun _doLiquidation(pid: UInt64, repayment: @{FungibleToken.Vault}, debtType: Type, seizeType: Type, seizeAmount: UFix64): @{FungibleToken.Vault} { pre { !self.isPausedOrWarmup(): "Liquidations are paused by governance" - // position must have debt and collateral balance + // position must have debt and collateral balance } let repayAmount = repayment.balance @@ -1021,7 +1021,7 @@ access(all) contract FlowALPv0 { // Create and return the Position resource - let position <- FlowALPPositionResources.createPosition(id: id, pool: poolCap) + let position <- FlowALPPositionResources.createPosition(id: id) self.unlockPosition(id) return <-position @@ -1455,7 +1455,7 @@ access(all) contract FlowALPv0 { // Validate constraint: non-zero rate requires swapper if insuranceRate > 0.0 { assert( - tsRef.getInsuranceSwapper() != nil, + tsRef.getInsuranceSwapper() != nil, message:"Cannot set non-zero insurance rate without an insurance swapper configured for \(tokenType.identifier)", ) } @@ -1474,13 +1474,13 @@ access(all) contract FlowALPv0 { self.isTokenSupported(tokenType: tokenType): "Unsupported token type" } let tsRef = self.state.borrowTokenState(tokenType) - ?? panic("Invariant: token state missing") + ?? panic("Invariant: token state missing") if let swapper = swapper { // Validate swapper types match assert(swapper.inType() == tokenType, message: "Swapper input type must match token type") assert(swapper.outType() == Type<@MOET.Vault>(), message: "Swapper output type must be MOET") - + } else { // cannot remove swapper if insurance rate > 0 assert( @@ -1564,7 +1564,7 @@ access(all) contract FlowALPv0 { let tsRef = self.state.borrowTokenState(tokenType) ?? panic("Invariant: token state missing") tsRef.setStabilityFeeRate(stabilityFeeRate) - + FlowALPEvents.emitStabilityFeeRateUpdated( poolUUID: self.uuid, tokenType: tokenType.identifier, @@ -1585,7 +1585,7 @@ access(all) contract FlowALPv0 { fundRef.balance >= amount, message: "Insufficient stability fund balance. Available: \(fundRef.balance), requested: \(amount)" ) - + let withdrawn <- fundRef.withdraw(amount: amount) recipient.deposit(from: <-withdrawn) @@ -2056,7 +2056,7 @@ access(all) contract FlowALPv0 { access(self) fun updateInterestRatesAndCollectInsurance(tokenType: Type) { let tokenState = self._borrowUpdatedTokenState(type: tokenType) tokenState.updateInterestRates() - + // Collect insurance if swapper is configured // Ensure reserves exist for this token type if !self.state.hasReserve(tokenType) { @@ -2138,7 +2138,7 @@ access(all) contract FlowALPv0 { access(all) fun getDefaultToken(): Type { return self.state.getDefaultToken() } - + /// Returns the deposit capacity and deposit capacity cap for a given token type access(all) fun getDepositCapacityInfo(type: Type): {String: UFix64} { let tokenState = self._borrowUpdatedTokenState(type: type) @@ -2189,6 +2189,16 @@ access(all) contract FlowALPv0 { /* --- INTERNAL METHODS --- */ + /// Returns an authorized reference to the contract-managed Pool resource. + /// Used internally by Position, PositionSink, and PositionSource instead of + /// issuing per-position storage capabilities. + access(self) fun _borrowPool(): Capability { + let poolCap = FlowALPv0.account.capabilities.storage.issue( + FlowALPv0.PoolStoragePath + ) + return poolCap + } + /// 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) @@ -2210,5 +2220,6 @@ access(all) contract FlowALPv0 { to: self.PoolFactoryPath ) let factory = self.account.storage.borrow<&PoolFactory>(from: self.PoolFactoryPath)! + FlowALPPositionResources.setPoolCap(cap: self._borrowPool()) } }