From 6e2e1366703828301c37724ea36b031ed42e68ce Mon Sep 17 00:00:00 2001 From: sisyphusSmiling Date: Tue, 16 Dec 2025 18:11:09 -0700 Subject: [PATCH 1/8] add FlowTransactionScheduler to flow.json dependencies set --- flow.json | 447 +++++++++++++++++++++++++++++------------------------- 1 file changed, 237 insertions(+), 210 deletions(-) diff --git a/flow.json b/flow.json index ce26064..a72b42d 100644 --- a/flow.json +++ b/flow.json @@ -1,155 +1,190 @@ { - "contracts": { - "DeFiActions": { - "source": "./FlowActions/cadence/contracts/interfaces/DeFiActions.cdc", - "aliases": { - "mainnet": "6d888f175c158410", - "testing": "0000000000000006", - "testnet": "0b11b1848a8aa2c0" - } - }, - "DeFiActionsUtils": { - "source": "./FlowActions/cadence/contracts/utils/DeFiActionsUtils.cdc", - "aliases": { - "mainnet": "6d888f175c158410", - "testing": "0000000000000006", - "testnet": "0b11b1848a8aa2c0" - } - }, - "DummyConnectors": { - "source": "./cadence/contracts/mocks/DummyConnectors.cdc", - "aliases": { - "testing": "0000000000000007" - } - }, - "FungibleTokenConnectors": { - "source": "./FlowActions/cadence/contracts/connectors/FungibleTokenConnectors.cdc", - "aliases": { - "testing": "0000000000000006" - } - }, - "MOET": { - "source": "./cadence/contracts/MOET.cdc", - "aliases": { - "testing": "0000000000000007" - } - }, - "MockOracle": { - "source": "./cadence/contracts/mocks/MockOracle.cdc", - "aliases": { - "testing": "0000000000000007" - } - }, - "MockFlowCreditMarketConsumer": { - "source": "./cadence/contracts/mocks/MockFlowCreditMarketConsumer.cdc", - "aliases": { - "testing": "0000000000000008" - } - }, - "MockDexSwapper": { - "source": "./cadence/contracts/mocks/MockDexSwapper.cdc", - "aliases": { - "testing": "0000000000000007" - } - }, - "MockYieldToken": { - "source": "./cadence/contracts/mocks/MockYieldToken.cdc", - "aliases": { - "testing": "0000000000000007" - } - }, - "FlowCreditMarket": { - "source": "./cadence/contracts/FlowCreditMarket.cdc", - "aliases": { - "testing": "0000000000000007" - } - }, - "FlowCreditMarketMath": { - "source": "./cadence/lib/FlowCreditMarketMath.cdc", - "aliases": { - "testing": "0000000000000007" - } - } - }, - "dependencies": { - "Burner": { - "source": "mainnet://f233dcee88fe0abe.Burner", - "hash": "71af18e227984cd434a3ad00bb2f3618b76482842bae920ee55662c37c8bf331", - "aliases": { - "emulator": "f8d6e0586b0a20c7", - "mainnet": "f233dcee88fe0abe", - "testnet": "9a0766d93b6608b7" - } - }, - "FlowToken": { - "source": "mainnet://1654653399040a61.FlowToken", - "hash": "cefb25fd19d9fc80ce02896267eb6157a6b0df7b1935caa8641421fe34c0e67a", - "aliases": { - "emulator": "0ae53cb6e3f42a79", - "mainnet": "1654653399040a61", - "testnet": "7e60df042a9c0868" - } - }, - "FungibleToken": { - "source": "mainnet://f233dcee88fe0abe.FungibleToken", - "hash": "23c1159cf99b2b039b6b868d782d57ae39b8d784045d81597f100a4782f0285b", - "aliases": { - "emulator": "ee82856bf20e2aa6", - "mainnet": "f233dcee88fe0abe", - "testnet": "9a0766d93b6608b7" - } - }, - "FungibleTokenMetadataViews": { - "source": "mainnet://f233dcee88fe0abe.FungibleTokenMetadataViews", - "hash": "dff704a6e3da83997ed48bcd244aaa3eac0733156759a37c76a58ab08863016a", - "aliases": { - "emulator": "ee82856bf20e2aa6", - "mainnet": "f233dcee88fe0abe", - "testnet": "9a0766d93b6608b7" - } - }, - "MetadataViews": { - "source": "mainnet://1d7e57aa55817448.MetadataViews", - "hash": "9032f46909e729d26722cbfcee87265e4f81cd2912e936669c0e6b510d007e81", - "aliases": { - "emulator": "f8d6e0586b0a20c7", - "mainnet": "1d7e57aa55817448", - "testnet": "631e88ae7f1d7c20" - } - }, - "NonFungibleToken": { - "source": "mainnet://1d7e57aa55817448.NonFungibleToken", - "hash": "b63f10e00d1a814492822652dac7c0574428a200e4c26cb3c832c4829e2778f0", - "aliases": { - "emulator": "f8d6e0586b0a20c7", - "mainnet": "1d7e57aa55817448", - "testnet": "631e88ae7f1d7c20" - } - }, - "ViewResolver": { - "source": "mainnet://1d7e57aa55817448.ViewResolver", - "hash": "374a1994046bac9f6228b4843cb32393ef40554df9bd9907a702d098a2987bde", - "aliases": { - "emulator": "f8d6e0586b0a20c7", - "mainnet": "1d7e57aa55817448", - "testnet": "631e88ae7f1d7c20" - } - } - }, - "networks": { - "emulator": "127.0.0.1:3569", - "mainnet": "access.mainnet.nodes.onflow.org:9000", - "testing": "127.0.0.1:3569", - "testnet": "access.devnet.nodes.onflow.org:9000" - }, - "accounts": { - "emulator-account": { - "address": "f8d6e0586b0a20c7", - "key": { - "type": "file", - "location": "emulator-account.pkey" - } - }, + "contracts": { + "DeFiActions": { + "source": "./FlowActions/cadence/contracts/interfaces/DeFiActions.cdc", + "aliases": { + "mainnet": "6d888f175c158410", + "testing": "0000000000000006", + "testnet": "0b11b1848a8aa2c0" + } + }, + "DeFiActionsUtils": { + "source": "./FlowActions/cadence/contracts/utils/DeFiActionsUtils.cdc", + "aliases": { + "mainnet": "6d888f175c158410", + "testing": "0000000000000006", + "testnet": "0b11b1848a8aa2c0" + } + }, + "DummyConnectors": { + "source": "./cadence/contracts/mocks/DummyConnectors.cdc", + "aliases": { + "testing": "0000000000000007" + } + }, + "FlowCreditMarket": { + "source": "./cadence/contracts/FlowCreditMarket.cdc", + "aliases": { + "testing": "0000000000000007" + } + }, + "FlowCreditMarketMath": { + "source": "./cadence/lib/FlowCreditMarketMath.cdc", + "aliases": { + "testing": "0000000000000007" + } + }, + "FungibleTokenConnectors": { + "source": "./FlowActions/cadence/contracts/connectors/FungibleTokenConnectors.cdc", + "aliases": { + "testing": "0000000000000006" + } + }, + "MOET": { + "source": "./cadence/contracts/MOET.cdc", + "aliases": { + "testing": "0000000000000007" + } + }, + "MockDexSwapper": { + "source": "./cadence/contracts/mocks/MockDexSwapper.cdc", + "aliases": { + "testing": "0000000000000007" + } + }, + "MockFlowCreditMarketConsumer": { + "source": "./cadence/contracts/mocks/MockFlowCreditMarketConsumer.cdc", + "aliases": { + "testing": "0000000000000008" + } + }, + "MockOracle": { + "source": "./cadence/contracts/mocks/MockOracle.cdc", + "aliases": { + "testing": "0000000000000007" + } + }, + "MockYieldToken": { + "source": "./cadence/contracts/mocks/MockYieldToken.cdc", + "aliases": { + "testing": "0000000000000007" + } + } + }, + "dependencies": { + "Burner": { + "source": "mainnet://f233dcee88fe0abe.Burner", + "hash": "71af18e227984cd434a3ad00bb2f3618b76482842bae920ee55662c37c8bf331", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "f233dcee88fe0abe", + "testnet": "9a0766d93b6608b7" + } + }, + "FlowFees": { + "source": "mainnet://f919ee77447b7497.FlowFees", + "hash": "341cc0f3cc847d6b787c390133f6a5e6c867c111784f09c5c0083c47f2f1df64", + "aliases": { + "emulator": "e5a8b7f23e8b548f", + "mainnet": "f919ee77447b7497", + "testnet": "912d5440f7e3769e" + } + }, + "FlowStorageFees": { + "source": "mainnet://e467b9dd11fa00df.FlowStorageFees", + "hash": "a92c26fb2ea59725441fa703aa4cd811e0fc56ac73d649a8e12c1e72b67a8473", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "e467b9dd11fa00df", + "testnet": "8c5303eaa26202d6" + } + }, + "FlowToken": { + "source": "mainnet://1654653399040a61.FlowToken", + "hash": "f82389e2412624ffa439836b00b42e6605b0c00802a4e485bc95b8930a7eac38", + "aliases": { + "emulator": "0ae53cb6e3f42a79", + "mainnet": "1654653399040a61", + "testnet": "7e60df042a9c0868" + } + }, + "FlowTransactionScheduler": { + "source": "mainnet://e467b9dd11fa00df.FlowTransactionScheduler", + "hash": "c701f26f6a8e993b2573ec8700142f61c9ca936b199af8cc75dee7d9b19c9e95", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "e467b9dd11fa00df", + "testnet": "8c5303eaa26202d6" + } + }, + "FungibleToken": { + "source": "mainnet://f233dcee88fe0abe.FungibleToken", + "hash": "4b74edfe7d7ddfa70b703c14aa731a0b2e7ce016ce54d998bfd861ada4d240f6", + "aliases": { + "emulator": "ee82856bf20e2aa6", + "mainnet": "f233dcee88fe0abe", + "testnet": "9a0766d93b6608b7" + } + }, + "FungibleTokenMetadataViews": { + "source": "mainnet://f233dcee88fe0abe.FungibleTokenMetadataViews", + "hash": "70477f80fd7678466c224507e9689f68f72a9e697128d5ea54d19961ec856b3c", + "aliases": { + "emulator": "ee82856bf20e2aa6", + "mainnet": "f233dcee88fe0abe", + "testnet": "9a0766d93b6608b7" + } + }, + "MetadataViews": { + "source": "mainnet://1d7e57aa55817448.MetadataViews", + "hash": "b290b7906d901882b4b62e596225fb2f10defb5eaaab4a09368f3aee0e9c18b1", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "1d7e57aa55817448", + "testnet": "631e88ae7f1d7c20" + } + }, + "NonFungibleToken": { + "source": "mainnet://1d7e57aa55817448.NonFungibleToken", + "hash": "a258de1abddcdb50afc929e74aca87161d0083588f6abf2b369672e64cf4a403", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "1d7e57aa55817448", + "testnet": "631e88ae7f1d7c20" + } + }, + "ViewResolver": { + "source": "mainnet://1d7e57aa55817448.ViewResolver", + "hash": "374a1994046bac9f6228b4843cb32393ef40554df9bd9907a702d098a2987bde", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "1d7e57aa55817448", + "testnet": "631e88ae7f1d7c20" + } + } + }, + "networks": { + "emulator": "127.0.0.1:3569", + "mainnet": "access.mainnet.nodes.onflow.org:9000", + "testing": "127.0.0.1:3569", + "testnet": "access.devnet.nodes.onflow.org:9000" + }, + "accounts": { + "emulator-account": { + "address": "f8d6e0586b0a20c7", + "key": { + "type": "file", + "location": "emulator-account.pkey" + } + }, + "mainnet-deployer": { + "address": "6b00ff876c299c61", + "key": { + "type": "google-kms", + "hashAlgorithm": "SHA2_256", + "resourceID": "projects/dl-flow-devex-production/locations/us-central1/keyRings/tidal-keyring/cryptoKeys/tidal_admin_pk/cryptoKeyVersions/1" + } + }, "testnet-deployer": { "address": "426f0458ced60037", "key": { @@ -157,63 +192,55 @@ "hashAlgorithm": "SHA2_256", "resourceID": "projects/dl-flow-devex-staging/locations/us-central1/keyRings/tidal-keyring/cryptoKeys/tidal_admin_pk/cryptoKeyVersions/1" } + } + }, + "deployments": { + "emulator": { + "emulator-account": [ + "DeFiActionsUtils", + "DeFiActions", + "FlowCreditMarketMath", + { + "name": "MOET", + "args": [ + { + "value": "1000000.00000000", + "type": "UFix64" + } + ] + }, + "FlowCreditMarket" + ] }, - "mainnet-deployer": { - "address": "6b00ff876c299c61", - "key": { - "type": "google-kms", - "hashAlgorithm": "SHA2_256", - "resourceID": "projects/dl-flow-devex-production/locations/us-central1/keyRings/tidal-keyring/cryptoKeys/tidal_admin_pk/cryptoKeyVersions/1" - } - } - }, - "deployments": { - "emulator": { - "emulator-account": [ - "DeFiActionsUtils", - "DeFiActions", - "FlowCreditMarketMath", - { - "name": "MOET", - "args": [ - { - "value": "1000000.00000000", - "type": "UFix64" - } - ] - }, - "FlowCreditMarket" - ] - }, - "testnet": { - "testnet-deployer": [ - "FlowCreditMarketMath", - { - "name": "MOET", - "args": [ - { - "value": "0.0", - "type": "UFix64" - } - ] - }, - "FlowCreditMarket" - ] - }, - "mainnet": { - "mainnet-deployer": [ - "FlowCreditMarketMath", - { - "name": "MOET", - "args": [ - { - "value": "0.0", - "type": "UFix64" - } - ] - }, - "FlowCreditMarket" - ] - } - } -} + "mainnet": { + "mainnet-deployer": [ + "FlowCreditMarketMath", + { + "name": "MOET", + "args": [ + { + "value": "0.00000000", + "type": "UFix64" + } + ] + }, + "FlowCreditMarket" + ] + }, + "testnet": { + "testnet-deployer": [ + "FlowCreditMarketMath", + { + "name": "MOET", + "args": [ + { + "value": "0.00000000", + "type": "UFix64" + } + ] + }, + "FlowCreditMarket" + ] + } + } +} \ No newline at end of file From c31b1de67022c5d223d9e31f307248f908eb44ff Mon Sep 17 00:00:00 2001 From: sisyphusSmiling Date: Wed, 17 Dec 2025 16:36:34 -0700 Subject: [PATCH 2/8] add IRegistry interface to FCM --- cadence/contracts/FlowCreditMarket.cdc | 63 ++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/cadence/contracts/FlowCreditMarket.cdc b/cadence/contracts/FlowCreditMarket.cdc index 8fa9ff9..04f21eb 100644 --- a/cadence/contracts/FlowCreditMarket.cdc +++ b/cadence/contracts/FlowCreditMarket.cdc @@ -1,6 +1,7 @@ import "Burner" import "FungibleToken" import "ViewResolver" +import "FlowTransactionScheduler" import "DeFiActionsUtils" import "DeFiActions" @@ -502,7 +503,7 @@ access(all) contract FlowCreditMarket { } } - + /// Risk parameters for a token used in effective collateral/debt computations. /// - collateralFactor: fraction applied to credit value to derive effective collateral @@ -650,7 +651,55 @@ access(all) contract FlowCreditMarket { } } - + /// Internal Registry + /// + access(all) entitlement Register + access(all) entitlement Schedule + + /// IRegistry + /// + /// An interface for a resource enabling arbitrary logic on Position creation & close. These are intended to allow + /// FCM to schedule transaction for new Positions on creation. Registries should maintain an implicit association + /// with Pool resources such that each Pool has an associated Registry for it. While there is only one Pool at the + /// moment, this structure allows for both upgradeability of the current contract as well as extensibility in the + /// event governance chooses to add multi-pool support in the future. Also note that this is codified as an + /// interface here to resolve potential circular dependencies compared to a canonical registry contract. + /// + access(all) resource interface IRegistry { + /// Registers a new position in the identified pool + access(Register) fun registerPosition(poolUUID: UInt64, pid: UInt64, rebalanceConfig: {String: AnyStruct}?) + /// Unregisters a position in the identified pool + access(Register) fun unregisterPosition(poolUUID: UInt64, pid: UInt64): Bool + } + + /// Derives the storage path for the Registry implementation for the given pool + /// + /// @param forPool - the UUID of the pool to derive the storage path for + /// + /// @return the storage path for the Registry implementation for the given pool + access(all) view fun deriveRegistryStoragePath(forPool: UInt64): StoragePath { + return StoragePath(identifier: "flowCreditMarketRegistry_\(forPool)")! + } + + /// Derives the storage path for the Registry implementation for the given pool + /// + /// @param forPool - the UUID of the pool to derive the storage path for + /// + /// @return the storage path for the Registry implementation for the given pool + access(all) view fun deriveRegistryPublicPath(forPool: UInt64): PublicPath { + return PublicPath(identifier: "flowCreditMarketRegistry_\(forPool)")! + } + + /// Returns an authorized reference on the Registry implementation for the given pool + /// + /// @param forPool - the UUID of the pool to borrow the registry for + /// + /// @return an authorized reference on the Registry implementation for the given pool + access(self) view fun _borrowFCMRegistry(forPool: UInt64): auth(Register) &{IRegistry} { + let path = self.deriveRegistryStoragePath(forPool: forPool) + return self.account.storage.borrow(from: path) + ?? panic("Could not borrow FCMRegistry for pool \(forPool) from path \(path)") + } /// Pool /// @@ -1676,7 +1725,7 @@ access(all) contract FlowCreditMarket { withdrawType: Type, effectiveCollateral: UFix128, effectiveDebt: UFix128, - targetHealth: UFix128 + targetHealth: UFix128 ): UFix64 { var effectiveCollateralAfterDeposit = effectiveCollateral var effectiveDebtAfterDeposit = effectiveDebt @@ -1894,6 +1943,12 @@ access(all) contract FlowCreditMarket { from: <-funds, pushToDrawDownSink: pushToDrawDownSink ) + + // register the new position with the Pool's associated registry + let registry = FlowCreditMarket._borrowFCMRegistry(forPool: self.uuid) + // NOTE: rebalanceConfig allows for more granular recurring rebalance configuration per position in the future + registry.registerPosition(poolUUID: self.uuid, pid: id, rebalanceConfig: nil) + return id } @@ -2331,7 +2386,7 @@ access(all) contract FlowCreditMarket { let sinkCapacity = drawDownSink.minimumCapacity() let sinkAmount = (idealWithdrawal > sinkCapacity) ? sinkCapacity : idealWithdrawal - if sinkAmount > 0.0 && sinkType == Type<@MOET.Vault>() { + if sinkAmount > 0.0 && sinkType == Type<@MOET.Vault>() { let tokenState = self._borrowUpdatedTokenState(type: Type<@MOET.Vault>()) if position.balances[Type<@MOET.Vault>()] == nil { position.balances[Type<@MOET.Vault>()] = InternalBalance(direction: BalanceDirection.Credit, scaledBalance: 0.0 as UFix128) From f83355a5ca0d1ca67ac706a5da77b19cb1485ad3 Mon Sep 17 00:00:00 2001 From: sisyphusSmiling Date: Wed, 17 Dec 2025 18:15:51 -0700 Subject: [PATCH 3/8] add rebalance scheduling contract to new FCMRegistry --- .../contracts/FlowCreditMarketRegistry.cdc | 303 ++++++++++++++++++ flow.json | 9 +- 2 files changed, 311 insertions(+), 1 deletion(-) create mode 100644 cadence/contracts/FlowCreditMarketRegistry.cdc diff --git a/cadence/contracts/FlowCreditMarketRegistry.cdc b/cadence/contracts/FlowCreditMarketRegistry.cdc new file mode 100644 index 0000000..6b9827c --- /dev/null +++ b/cadence/contracts/FlowCreditMarketRegistry.cdc @@ -0,0 +1,303 @@ +import "Burner" +import "MetadataViews" +import "FungibleToken" +import "FlowToken" +import "FlowTransactionScheduler" +import "FlowCreditMarket" + +access(all) contract FlowCreditMarketRegistry { + + access(all) event Registered(poolUUID: UInt64, positionID: UInt64) + access(all) event Unregistered(poolUUID: UInt64, positionID: UInt64) + access(all) event RebalanceHandlerError(poolUUID: UInt64, positionID: UInt64, whileExecuting: UInt64?, errorMessage: String) + + access(all) resource Registry : FlowCreditMarket.IRegistry { + access(all) let registeredPositions: {UInt64: Bool} + access(all) var defaultRebalanceRecurringConfig: {String: AnyStruct} + access(all) let rebalanceConfigs: {UInt64: {String: AnyStruct}} + + init(defaultRebalanceRecurringConfig: {String: AnyStruct}) { + self.registeredPositions = {} + self.defaultRebalanceRecurringConfig = defaultRebalanceRecurringConfig + self.rebalanceConfigs = {} + } + + access(all) view fun getRebalanceHandlerScheduledTxnData(pid: UInt64): {String: AnyStruct} { + return self.rebalanceConfigs[pid] ?? self.defaultRebalanceRecurringConfig + } + + access(FlowCreditMarket.Register) fun registerPosition(poolUUID: UInt64, pid: UInt64, rebalanceConfig: {String: AnyStruct}?) { + pre { + self.registeredPositions[pid] == nil: + "Position \(pid) is already registered" + } + self.registeredPositions[pid] = true + if rebalanceConfig != nil { + self.rebalanceConfigs[pid] = rebalanceConfig! + } + + // configure a rebalance handler for this position identified by it's pool:position + let rebalanceHandler = FlowCreditMarketRegistry._initRebalanceHandler(poolUUID: poolUUID, positionID: pid) + + // emit the registered event + emit Registered(poolUUID: poolUUID, positionID: pid) + + // schedule the first rebalance + rebalanceHandler.scheduleNextRebalance(whileExecuting: nil, data: rebalanceConfig ?? self.defaultRebalanceRecurringConfig) + } + + access(FlowCreditMarket.Register) fun unregisterPosition(poolUUID: UInt64, pid: UInt64): Bool { + let removed = self.registeredPositions.remove(key: pid) + if removed == true { + emit Unregistered(poolUUID: poolUUID, positionID: pid) + FlowCreditMarketRegistry._cleanupRebalanceHandler(poolUUID: poolUUID, positionID: pid) + } + return removed == true + } + + access(FlowCreditMarket.EGovernance) fun setDefaultRebalanceRecurringConfig(config: {String: AnyStruct}) { + pre { + config["interval"] as? UFix64 != nil: + "interval: UFix64 is required" + config["priority"] as? UInt8 != nil && (config["priority"]! as? UInt8 ?? UInt8.max) <= 2: + "priority: UInt8 is required and must be between 0 and 2 to match FlowTransactionScheduler.Priority raw values (0: High, 1: Medium, 2: Low)" + config["executionEffort"] as? UInt64 != nil: + "executionEffort: UInt64 is required" + config["force"] as? Bool != nil: + "force: Bool is required" + } + self.defaultRebalanceRecurringConfig = config + } + } + + access(all) resource RebalanceHandler : FlowTransactionScheduler.TransactionHandler { + access(all) let poolUUID: UInt64 + access(all) let positionID: UInt64 + access(all) let scheduledTxns: @{UInt64: FlowTransactionScheduler.ScheduledTransaction} + access(self) var selfCapability: Capability? + + init(poolUUID: UInt64, positionID: UInt64) { + self.poolUUID = poolUUID + self.positionID = positionID + self.scheduledTxns <- {} + self.selfCapability = nil + } + + access(all) view fun getViews(): [Type] { + return [ Type(), Type() ] + } + access(all) fun resolveView(_ viewType: Type): AnyStruct? { + if viewType == Type() { + return FlowCreditMarketRegistry.deriveRebalanceHandlerStoragePath(poolUUID: self.poolUUID, positionID: self.positionID) + } else if viewType == Type() { + return FlowCreditMarketRegistry.deriveRebalanceHandlerPublicPath(poolUUID: self.poolUUID, positionID: self.positionID) + } else if viewType == Type() { + return MetadataViews.Display( + name: "Flow Credit Market Pool Position Rebalance Scheduled Transaction Handler", + description: "Scheduled Transaction Handler that can execute rebalance transactions on behalf of a Flow Credit Market Pool with UUID \(self.poolUUID) and Position ID \(self.positionID)", + thumbnail: MetadataViews.HTTPFile(url: "") + ) + } + return nil + } + /// Returns the IDs of the scheduled transactions. + /// NOTE: this does not include externally scheduled transactions + /// + /// @return [UInt64]: The IDs of the scheduled transactions + /// + access(all) view fun getScheduledTransactionIDs(): [UInt64] { + return self.scheduledTxns.keys + } + /// Borrows a reference to the internally-managed scheduled transaction or nil if not found. + /// NOTE: this does not include externally scheduled transactions + /// + /// @param id: The ID of the scheduled transaction + /// + /// @return &FlowTransactionScheduler.ScheduledTransaction?: The reference to the scheduled transaction, or nil + /// if the scheduled transaction is not found + /// + access(all) view fun borrowScheduledTransaction(id: UInt64): &FlowTransactionScheduler.ScheduledTransaction? { + return &self.scheduledTxns[id] + } + /// Executes a scheduled rebalance on the underlying FCM Position + /// + /// @param id: The ID of the scheduled transaction to execute + /// @param data: The data for the scheduled transaction + /// + access(FlowTransactionScheduler.Execute) fun executeTransaction(id: UInt64, data: AnyStruct?) { + let _data = data as? {String: AnyStruct} ?? {"force": false} + let force = _data["force"] as? Bool ?? false + + // borrow the pool + let pool = FlowCreditMarketRegistry._borrowAuthPool(self.poolUUID) + if pool == nil { + emit RebalanceHandlerError(poolUUID: self.poolUUID, positionID: self.positionID, whileExecuting: id, errorMessage: "POOL_NOT_FOUND") + return + } + + // call rebalance on the pool + let unwrappedPool = pool! + // THIS CALL MAY REVERT - upstream systems should account for instances where rebalancing forces a revert + unwrappedPool.rebalancePosition(pid: self.positionID, force: force) + + // schedule the next rebalance if internally-managed + let isInternallyManaged = self.borrowScheduledTransaction(id: id) != nil + if isInternallyManaged { + let err = self.scheduleNextRebalance(whileExecuting: id, data: nil) + if err != nil { + emit RebalanceHandlerError(poolUUID: self.poolUUID, positionID: self.positionID, whileExecuting: id, errorMessage: err!) + } + } + } + /// Schedules the next rebalance on the underlying FCM Position + /// + /// @param whileExecuting: The ID of the scheduled transaction that is currently executing + /// @param data: The data for the scheduled transaction + /// + access(FlowCreditMarket.Schedule) fun scheduleNextRebalance(whileExecuting: UInt64?, data: {String: AnyStruct}?): String? { + // check for a valid self capability before attempting to schedule the next rebalance + if self.selfCapability?.check() != true { return "INVALID_SELF_CAPABILITY"; } + let selfCapability = self.selfCapability! + + // borrow the registry & get the recurring config for this position + let registry = FlowCreditMarketRegistry.borrowRegistry(self.poolUUID) + if registry == nil { + return "REGISTRY_NOT_FOUND" + } + let unwrappedRegistry = registry! + let recurringConfig = data ?? unwrappedRegistry.getRebalanceHandlerScheduledTxnData(pid: self.positionID) + // get the recurring config values + let interval = recurringConfig["interval"] as? UFix64 + let priorityRaw = recurringConfig["priority"] as? UInt8 + let executionEffort = recurringConfig["executionEffort"] as? UInt64 + if interval == nil || priorityRaw == nil || executionEffort == nil || (priorityRaw! as? UInt8 ?? UInt8.max) > 2 { + return "INVALID_RECURRING_CONFIG" + } + + // schedule the next rebalance based on the recurring config + let priority = FlowTransactionScheduler.Priority(rawValue: priorityRaw!)! + let timestamp = getCurrentBlock().timestamp + UFix64(interval!) + let estimate = FlowTransactionScheduler.estimate( + data: recurringConfig, + timestamp: timestamp, + priority: priority, + executionEffort: executionEffort! + ) + + if estimate.flowFee == nil { + return "INVALID_SCHEDULED_TXN_ESTIMATE: \(estimate.error ?? "UNKNOWN_ERROR")" + } + // withdraw the fees for the scheduled transaction + let feeAmount = estimate.flowFee! + var fees <- FlowCreditMarketRegistry._withdrawFees(amount: feeAmount) + if fees == nil { + destroy fees + return "FAILED_TO_WITHDRAW_FEES" + } else { + // schedule the next rebalance + let unwrappedFees <- fees! + let txn <- FlowTransactionScheduler.schedule( + handlerCap: selfCapability, + data: recurringConfig, + timestamp: timestamp, + priority: priority, + executionEffort: executionEffort!, + fees: <-unwrappedFees + ) + let txnID = txn.id + self.scheduledTxns[txnID] <-! txn + return nil + } + } + access(contract) fun _setSelfCapability(_ handlerCap: Capability) { + pre { + self.selfCapability == nil: + "Self capability is already set" + handlerCap.check() == true: + "Handler capability is not valid" + handlerCap.borrow()!.uuid == self.uuid: + "Handler capability is not for this handler" + } + self.selfCapability = handlerCap + } + } + + /* PUBLIC METHODS */ + + access(all) view fun deriveRebalanceHandlerStoragePath(poolUUID: UInt64, positionID: UInt64): StoragePath { + return StoragePath(identifier: "flowCreditMarketRebalanceHandler_\(poolUUID)_\(positionID)")! + } + + access(all) view fun deriveRebalanceHandlerPublicPath(poolUUID: UInt64, positionID: UInt64): PublicPath { + return PublicPath(identifier: "flowCreditMarketRebalanceHandler_\(poolUUID)_\(positionID)")! + } + + access(all) view fun borrowRegistry(_ poolUUID: UInt64): &Registry? { + let registryPath = FlowCreditMarket.deriveRegistryPublicPath(forPool: poolUUID) + return self.account.capabilities.borrow<&Registry>(registryPath) + } + + access(all)view fun borrowRebalanceHandler(poolUUID: UInt64, positionID: UInt64): &RebalanceHandler? { + let handlerPath = self.deriveRebalanceHandlerPublicPath(poolUUID: poolUUID, positionID: positionID) + return self.account.capabilities.borrow<&RebalanceHandler>(handlerPath) + } + + /* INTERNAL METHODS */ + + access(self) fun _initRebalanceHandler(poolUUID: UInt64, positionID: UInt64): auth(FlowCreditMarket.Schedule) &RebalanceHandler { + let storagePath = self.deriveRebalanceHandlerStoragePath(poolUUID: poolUUID, positionID: positionID) + let publicPath = self.deriveRebalanceHandlerPublicPath(poolUUID: poolUUID, positionID: positionID) + // initialize the RebalanceHandler if it doesn't exist + if self.account.storage.type(at: storagePath) == nil { + let rebalanceHandler <- create RebalanceHandler(poolUUID: poolUUID, positionID: positionID) + self.account.storage.save(<-rebalanceHandler, to: storagePath) + self.account.capabilities.unpublish(publicPath) + let pubCap = self.account.capabilities.storage.issue<&RebalanceHandler>(storagePath) + self.account.capabilities.publish(pubCap, at: publicPath) + } + // borrow the RebalanceHandler, set its internal capability & return + let rebalanceHandler = self.account.storage.borrow(from: storagePath) ?? panic("Failed to initialize RebalanceHandler") + let handlerCap = self.account.capabilities.storage.issue(storagePath) + rebalanceHandler._setSelfCapability(handlerCap) + return rebalanceHandler + } + + access(self) fun _cleanupRebalanceHandler(poolUUID: UInt64, positionID: UInt64) { + let storagePath = self.deriveRebalanceHandlerStoragePath(poolUUID: poolUUID, positionID: positionID) + let publicPath = self.deriveRebalanceHandlerPublicPath(poolUUID: poolUUID, positionID: positionID) + if self.account.storage.type(at: storagePath) == nil { + return + } + self.account.capabilities.unpublish(publicPath) + self.account.capabilities.storage.forEachController(forPath: storagePath, fun(_ controller: &StorageCapabilityController): Bool { + controller.delete() + return true + }) + let removed <- self.account.storage.load<@RebalanceHandler>(from: storagePath) + Burner.burn(<-removed) + } + + access(self) view fun _borrowAuthPool(_ poolUUID: UInt64): auth(FlowCreditMarket.EPosition) &FlowCreditMarket.Pool? { + let poolPath = FlowCreditMarket.PoolStoragePath + return self.account.storage.borrow(from: poolPath) + } + + access(self) fun _withdrawFees(amount: UFix64): @FlowToken.Vault? { + let vaultPath = /storage/flowTokenVault + let vault = self.account.storage.borrow(from: vaultPath) + if vault?.balance ?? 0.0 < amount { + return nil + } + return <-vault!.withdraw(amount: amount) as! @FlowToken.Vault + } + + init() { + let poolUUID = self.account.storage.borrow<&FlowCreditMarket.Pool>(from: FlowCreditMarket.PoolStoragePath)?.uuid + ?? panic("Cannot initialize FlowCreditMarketScheduler without an initialized FlowCreditMarket Pool in storage") + let path = FlowCreditMarket.deriveRegistryStoragePath(forPool: poolUUID) + + // TODO: update config schema for scheduled txn data formats + let defaultRebalanceRecurringConfig = {"force": false} + self.account.storage.save(<-create Registry(defaultRebalanceRecurringConfig: defaultRebalanceRecurringConfig), to: path) + } +} diff --git a/flow.json b/flow.json index a72b42d..68b911c 100644 --- a/flow.json +++ b/flow.json @@ -34,6 +34,12 @@ "testing": "0000000000000007" } }, + "FlowCreditMarketRegistry": { + "source": "./cadence/contracts/FlowCreditMarketRegistry.cdc", + "aliases": { + "testing": "0000000000000007" + } + }, "FungibleTokenConnectors": { "source": "./FlowActions/cadence/contracts/connectors/FungibleTokenConnectors.cdc", "aliases": { @@ -209,7 +215,8 @@ } ] }, - "FlowCreditMarket" + "FlowCreditMarket", + "FlowCreditMarketRegistry" ] }, "mainnet": { From fe24a038a054d55b5df7d84b462223d548a6c6a5 Mon Sep 17 00:00:00 2001 From: sisyphusSmiling Date: Thu, 18 Dec 2025 17:07:11 -0700 Subject: [PATCH 4/8] removed unused import --- cadence/contracts/FlowCreditMarket.cdc | 1 - 1 file changed, 1 deletion(-) diff --git a/cadence/contracts/FlowCreditMarket.cdc b/cadence/contracts/FlowCreditMarket.cdc index 04f21eb..f140fa2 100644 --- a/cadence/contracts/FlowCreditMarket.cdc +++ b/cadence/contracts/FlowCreditMarket.cdc @@ -1,7 +1,6 @@ import "Burner" import "FungibleToken" import "ViewResolver" -import "FlowTransactionScheduler" import "DeFiActionsUtils" import "DeFiActions" From 8f766556ef6c029e17cb649f5c01c86e0a75b552 Mon Sep 17 00:00:00 2001 From: sisyphusSmiling Date: Thu, 18 Dec 2025 17:51:51 -0700 Subject: [PATCH 5/8] update FCMRegistry & FCM + add backfill txns --- cadence/contracts/FlowCreditMarket.cdc | 21 +- .../contracts/FlowCreditMarketRegistry.cdc | 198 ++++++++++++++++-- .../get_next_position_id.cdc | 13 ++ .../registry/registry/backfill_positions.cdc | 28 +++ .../registry/backfill_specific_positions.cdc | 28 +++ 5 files changed, 263 insertions(+), 25 deletions(-) create mode 100644 cadence/scripts/flow-credit-market/get_next_position_id.cdc create mode 100644 cadence/transactions/flow-credit-market/registry/registry/backfill_positions.cdc create mode 100644 cadence/transactions/flow-credit-market/registry/registry/backfill_specific_positions.cdc diff --git a/cadence/contracts/FlowCreditMarket.cdc b/cadence/contracts/FlowCreditMarket.cdc index f140fa2..a23c067 100644 --- a/cadence/contracts/FlowCreditMarket.cdc +++ b/cadence/contracts/FlowCreditMarket.cdc @@ -714,7 +714,7 @@ access(all) contract FlowCreditMarket { /// The actual reserves of each token access(self) var reserves: @{Type: {FungibleToken.Vault}} /// Auto-incrementing position identifier counter - access(self) var nextPositionID: UInt64 + access(all) var nextPositionID: UInt64 /// The default token type used as the "unit of account" for the pool. access(self) let defaultToken: Type /// A price oracle that will return the price of each token in terms of the default token. @@ -948,6 +948,23 @@ access(all) contract FlowCreditMarket { ) } + /// Returns whether a given position exists in the pool + /// + /// @param pid: The Position ID + /// + /// @return Bool: True if the position exists, false otherwise + access(all) view fun positionExists(pid: UInt64): Bool { + return self.positions[pid] != nil + } + + /// Returns the IDs of all positions in the pool + /// NOTE: This will likely need pagination to scale as the number of positions grows + /// + /// @return [UInt64]: The IDs of all positions in the pool + access(all) view fun getPositionIDs(): [UInt64] { + return self.positions.keys + } + /// Returns the details of a given position as a PositionDetails external struct access(all) fun getPositionDetails(pid: UInt64): PositionDetails { if self.debugLogging { log(" [CONTRACT] getPositionDetails(pid: \(pid))") } @@ -968,7 +985,7 @@ access(all) contract FlowCreditMarket { )) } - let health = self.positionHealth(pid: pid) + let health: UFix128 = self.positionHealth(pid: pid) let defaultTokenAvailable = self.availableBalance(pid: pid, type: self.defaultToken, pullFromTopUpSource: false) return PositionDetails( diff --git a/cadence/contracts/FlowCreditMarketRegistry.cdc b/cadence/contracts/FlowCreditMarketRegistry.cdc index 6b9827c..541ee6b 100644 --- a/cadence/contracts/FlowCreditMarketRegistry.cdc +++ b/cadence/contracts/FlowCreditMarketRegistry.cdc @@ -5,15 +5,51 @@ import "FlowToken" import "FlowTransactionScheduler" import "FlowCreditMarket" +/// FlowCreditMarketRegistry +/// +/// This contract defines constructs for managing registration of Positions on a per-Pool basis. On registration, a +/// rebalance handler is initialized and scheduled for the first rebalance. The rebalance handler is responsible for +/// scheduling subsequent rebalance transactions for the position. +/// +/// The registry also manages the default rebalance configuration for all registered positions and provides the ability +/// to set a custom per-position rebalance configuration in the event such functionality is added in the future. +/// +/// NOTE: Position rebalancing can fail for reasons outside of FCM's control. It's recommended that monitoring systems +/// account for instances where rebalance and subsequent RebalanceHandler transaction scheduling fail and the FCM +/// maintainers to start up the rebalancing process again. It may be desirable to add such functionality into the +/// Position objects themselves so that end users or upstream protocols/platforms can start up the rebalancing process +/// themselves. +/// access(all) contract FlowCreditMarketRegistry { - access(all) event Registered(poolUUID: UInt64, positionID: UInt64) - access(all) event Unregistered(poolUUID: UInt64, positionID: UInt64) - access(all) event RebalanceHandlerError(poolUUID: UInt64, positionID: UInt64, whileExecuting: UInt64?, errorMessage: String) + /// Emitted when a position is registered in the registry + access(all) event Registered(poolUUID: UInt64, positionID: UInt64, registryUUID: UInt64) + /// Emitted when a position is unregistered from the registry + access(all) event Unregistered(poolUUID: UInt64, positionID: UInt64, registryUUID: UInt64) + /// Emitted when an error occurs in the rebalance handler + access(all) event RebalanceHandlerError(poolUUID: UInt64, positionID: UInt64, registryUUID: UInt64, whileExecuting: UInt64?, errorMessage: String) + /// Registry + /// + /// A resource that manages the registration and unregistration of positions in the registry associated with the + /// identified pool. It also manages the scheduling of rebalance transactions for the registered positions. + /// + /// The registry is associated with a pool and each position in the pool has an associated rebalance handler. + /// The rebalance handler is responsible for scheduling rebalance transactions for the position. + /// + /// The registry is also responsible for managing the default rebalance configuration for all registered positions + /// and provides the ability to set a custom per-position rebalance configuration in the event such functionality is + /// added in the future. access(all) resource Registry : FlowCreditMarket.IRegistry { + /// A map of registered positions by their Position ID access(all) let registeredPositions: {UInt64: Bool} + /// The default rebalance configuration for all registered positions + /// See RebalanceHandler.scheduleNextRebalance for the expected configuration format access(all) var defaultRebalanceRecurringConfig: {String: AnyStruct} + /// A map of custom rebalance configurations by position ID. While not currently supported in FCM, adding this + /// allows for extensibility in the event governance chooses to add custom rebalance configurations for + /// registered positions in the future. + /// See RebalanceHandler.scheduleNextRebalance for the expected configuration format access(all) let rebalanceConfigs: {UInt64: {String: AnyStruct}} init(defaultRebalanceRecurringConfig: {String: AnyStruct}) { @@ -22,14 +58,26 @@ access(all) contract FlowCreditMarketRegistry { self.rebalanceConfigs = {} } - access(all) view fun getRebalanceHandlerScheduledTxnData(pid: UInt64): {String: AnyStruct} { + /// Returns the rebalance configuration for the identified position + /// + /// @param pid: The ID of the position + /// + /// @return {String: AnyStruct}: The rebalance configuration for the position + access(all) view fun getRebalanceHandlerScheduledTxnConfig(pid: UInt64): {String: AnyStruct} { return self.rebalanceConfigs[pid] ?? self.defaultRebalanceRecurringConfig } + /// Registers a position in the registry associated with the identified pool and position ID + /// + /// @param poolUUID: The UUID of the pool + /// @param pid: The ID of the position + /// @param rebalanceConfig: The rebalance configuration for the position access(FlowCreditMarket.Register) fun registerPosition(poolUUID: UInt64, pid: UInt64, rebalanceConfig: {String: AnyStruct}?) { pre { self.registeredPositions[pid] == nil: "Position \(pid) is already registered" + FlowCreditMarketRegistry._borrowAuthPool(poolUUID)?.positionExists(pid: pid) == true: + "Position \(pid) does not exist in pool \(poolUUID) - cannot register non-existent position" } self.registeredPositions[pid] = true if rebalanceConfig != nil { @@ -40,21 +88,30 @@ access(all) contract FlowCreditMarketRegistry { let rebalanceHandler = FlowCreditMarketRegistry._initRebalanceHandler(poolUUID: poolUUID, positionID: pid) // emit the registered event - emit Registered(poolUUID: poolUUID, positionID: pid) + emit Registered(poolUUID: poolUUID, positionID: pid, registryUUID: self.uuid) // schedule the first rebalance rebalanceHandler.scheduleNextRebalance(whileExecuting: nil, data: rebalanceConfig ?? self.defaultRebalanceRecurringConfig) } + /// Unregisters a position in the registry associated with the identified pool and position ID + /// + /// @param poolUUID: The UUID of the pool + /// @param pid: The ID of the position + /// + /// @return Bool: True if the position was unregistered, false otherwise access(FlowCreditMarket.Register) fun unregisterPosition(poolUUID: UInt64, pid: UInt64): Bool { let removed = self.registeredPositions.remove(key: pid) if removed == true { - emit Unregistered(poolUUID: poolUUID, positionID: pid) + emit Unregistered(poolUUID: poolUUID, positionID: pid, registryUUID: self.uuid) FlowCreditMarketRegistry._cleanupRebalanceHandler(poolUUID: poolUUID, positionID: pid) } return removed == true } + /// Sets the default rebalance recurring configuration for the registry + /// + /// @param config: The default rebalance configuration for all registered positions access(FlowCreditMarket.EGovernance) fun setDefaultRebalanceRecurringConfig(config: {String: AnyStruct}) { pre { config["interval"] as? UFix64 != nil: @@ -70,10 +127,23 @@ access(all) contract FlowCreditMarketRegistry { } } + /// RebalanceHandler + /// + /// A resource that manages the scheduling of rebalance transactions for a position in a pool. + /// + /// The rebalance handler is associated with a pool and position and is responsible for scheduling rebalance + /// transactions for the position. + /// + /// The rebalance handler is also responsible for managing the scheduled transactions for the position. + /// access(all) resource RebalanceHandler : FlowTransactionScheduler.TransactionHandler { + /// The UUID of the pool associated with the rebalance handler access(all) let poolUUID: UInt64 + /// The ID of the position associated with the rebalance handler access(all) let positionID: UInt64 + /// A map of scheduled transactions by their ID access(all) let scheduledTxns: @{UInt64: FlowTransactionScheduler.ScheduledTransaction} + /// The self capability for the rebalance handler access(self) var selfCapability: Capability? init(poolUUID: UInt64, positionID: UInt64) { @@ -83,9 +153,17 @@ access(all) contract FlowCreditMarketRegistry { self.selfCapability = nil } + /* MetadataViews.Display conformance */ + + /// Returns the views supported by the rebalance handler access(all) view fun getViews(): [Type] { - return [ Type(), Type() ] + return [ Type(), Type(), Type() ] } + /// Resolves a view type to a view object + /// + /// @param viewType: The type of the view to resolve + /// + /// @return AnyStruct?: The view object, or nil if the view is not supported access(all) fun resolveView(_ viewType: Type): AnyStruct? { if viewType == Type() { return FlowCreditMarketRegistry.deriveRebalanceHandlerStoragePath(poolUUID: self.poolUUID, positionID: self.positionID) @@ -104,7 +182,6 @@ access(all) contract FlowCreditMarketRegistry { /// NOTE: this does not include externally scheduled transactions /// /// @return [UInt64]: The IDs of the scheduled transactions - /// access(all) view fun getScheduledTransactionIDs(): [UInt64] { return self.scheduledTxns.keys } @@ -115,15 +192,14 @@ access(all) contract FlowCreditMarketRegistry { /// /// @return &FlowTransactionScheduler.ScheduledTransaction?: The reference to the scheduled transaction, or nil /// if the scheduled transaction is not found - /// access(all) view fun borrowScheduledTransaction(id: UInt64): &FlowTransactionScheduler.ScheduledTransaction? { return &self.scheduledTxns[id] } - /// Executes a scheduled rebalance on the underlying FCM Position + /// Executes a scheduled rebalance on the underlying FCM Position. If the scheduled transaction is internally-managed, + /// the next rebalance will be scheduled, otherwise the execution is treated as a "fire once" transaction. /// /// @param id: The ID of the scheduled transaction to execute /// @param data: The data for the scheduled transaction - /// access(FlowTransactionScheduler.Execute) fun executeTransaction(id: UInt64, data: AnyStruct?) { let _data = data as? {String: AnyStruct} ?? {"force": false} let force = _data["force"] as? Bool ?? false @@ -131,7 +207,7 @@ access(all) contract FlowCreditMarketRegistry { // borrow the pool let pool = FlowCreditMarketRegistry._borrowAuthPool(self.poolUUID) if pool == nil { - emit RebalanceHandlerError(poolUUID: self.poolUUID, positionID: self.positionID, whileExecuting: id, errorMessage: "POOL_NOT_FOUND") + emit RebalanceHandlerError(poolUUID: self.poolUUID, positionID: self.positionID, registryUUID: self.uuid, whileExecuting: id, errorMessage: "POOL_NOT_FOUND") return } @@ -145,15 +221,18 @@ access(all) contract FlowCreditMarketRegistry { if isInternallyManaged { let err = self.scheduleNextRebalance(whileExecuting: id, data: nil) if err != nil { - emit RebalanceHandlerError(poolUUID: self.poolUUID, positionID: self.positionID, whileExecuting: id, errorMessage: err!) + emit RebalanceHandlerError(poolUUID: self.poolUUID, positionID: self.positionID, registryUUID: self.uuid, whileExecuting: id, errorMessage: err!) } } + // clean up internally-managed historical scheduled transactions + self._cleanupScheduledTransactions() } - /// Schedules the next rebalance on the underlying FCM Position + /// Schedules the next rebalance on the underlying FCM Position. Defaults to the Registry's recurring config + /// for the handled position if `data` is not provided. Otherwise the next scheduled transaction is configured + /// with the provided data. /// /// @param whileExecuting: The ID of the scheduled transaction that is currently executing - /// @param data: The data for the scheduled transaction - /// + /// @param data: The optional recurring config for the scheduled transaction access(FlowCreditMarket.Schedule) fun scheduleNextRebalance(whileExecuting: UInt64?, data: {String: AnyStruct}?): String? { // check for a valid self capability before attempting to schedule the next rebalance if self.selfCapability?.check() != true { return "INVALID_SELF_CAPABILITY"; } @@ -165,12 +244,12 @@ access(all) contract FlowCreditMarketRegistry { return "REGISTRY_NOT_FOUND" } let unwrappedRegistry = registry! - let recurringConfig = data ?? unwrappedRegistry.getRebalanceHandlerScheduledTxnData(pid: self.positionID) + let recurringConfig = data ?? unwrappedRegistry.getRebalanceHandlerScheduledTxnConfig(pid: self.positionID) // get the recurring config values let interval = recurringConfig["interval"] as? UFix64 let priorityRaw = recurringConfig["priority"] as? UInt8 let executionEffort = recurringConfig["executionEffort"] as? UInt64 - if interval == nil || priorityRaw == nil || executionEffort == nil || (priorityRaw! as? UInt8 ?? UInt8.max) > 2 { + if interval == nil || priorityRaw == nil || (priorityRaw as? UInt8 ?? UInt8.max) > 2 || executionEffort == nil { return "INVALID_RECURRING_CONFIG" } @@ -209,6 +288,9 @@ access(all) contract FlowCreditMarketRegistry { return nil } } + /// Sets the self capability for the rebalance handler so that it can schedule its own future transactions + /// + /// @param handlerCap: The capability to set for the rebalance handler access(contract) fun _setSelfCapability(_ handlerCap: Capability) { pre { self.selfCapability == nil: @@ -220,23 +302,65 @@ access(all) contract FlowCreditMarketRegistry { } self.selfCapability = handlerCap } + /// Cleans up the internally-managed scheduled transactions + access(self) fun _cleanupScheduledTransactions() { + // if there are no scheduled transactions, return + if self.scheduledTxns.length == 0 { + return + } + // limit to prevent running into computation limits + let limit = 25 + // iterate over the scheduled transactions and remove those that are not scheduled + for i, id in self.scheduledTxns.keys { + if i >= limit { + break + } + let ref = self.borrowScheduledTransaction(id: id) + if ref != nil && ref!.status() != FlowTransactionScheduler.Status.Scheduled { + destroy <- self.scheduledTxns.remove(key: id) + } + } + } } /* PUBLIC METHODS */ + /// Derives the storage path for the rebalance handler associated with the identified pool and position + /// + /// @param poolUUID: The UUID of the pool + /// @param positionID: The ID of the position + /// + /// @return StoragePath: The storage path for the rebalance handler access(all) view fun deriveRebalanceHandlerStoragePath(poolUUID: UInt64, positionID: UInt64): StoragePath { return StoragePath(identifier: "flowCreditMarketRebalanceHandler_\(poolUUID)_\(positionID)")! } + /// Derives the public path for the rebalance handler associated with the identified pool and position + /// + /// @param poolUUID: The UUID of the pool + /// @param positionID: The ID of the position + /// + /// @return PublicPath: The public path for the rebalance handler access(all) view fun deriveRebalanceHandlerPublicPath(poolUUID: UInt64, positionID: UInt64): PublicPath { return PublicPath(identifier: "flowCreditMarketRebalanceHandler_\(poolUUID)_\(positionID)")! } + /// Borrows a reference to the registry associated with the identified pool + /// + /// @param poolUUID: The UUID of the pool + /// + /// @return &Registry?: The reference to the registry, or nil if the registry is not found access(all) view fun borrowRegistry(_ poolUUID: UInt64): &Registry? { let registryPath = FlowCreditMarket.deriveRegistryPublicPath(forPool: poolUUID) return self.account.capabilities.borrow<&Registry>(registryPath) } + /// Borrows a reference to the rebalance handler associated with the identified pool and position + /// + /// @param poolUUID: The UUID of the pool + /// @param positionID: The ID of the position + /// + /// @return &RebalanceHandler?: The reference to the rebalance handler, or nil if the rebalance handler is not found access(all)view fun borrowRebalanceHandler(poolUUID: UInt64, positionID: UInt64): &RebalanceHandler? { let handlerPath = self.deriveRebalanceHandlerPublicPath(poolUUID: poolUUID, positionID: positionID) return self.account.capabilities.borrow<&RebalanceHandler>(handlerPath) @@ -244,6 +368,12 @@ access(all) contract FlowCreditMarketRegistry { /* INTERNAL METHODS */ + /// Initializes a new rebalance handler associated with the identified pool and position + /// + /// @param poolUUID: The UUID of the pool + /// @param positionID: The ID of the position + /// + /// @return auth(FlowCreditMarket.Schedule) &RebalanceHandler: The initialized rebalance handler access(self) fun _initRebalanceHandler(poolUUID: UInt64, positionID: UInt64): auth(FlowCreditMarket.Schedule) &RebalanceHandler { let storagePath = self.deriveRebalanceHandlerStoragePath(poolUUID: poolUUID, positionID: positionID) let publicPath = self.deriveRebalanceHandlerPublicPath(poolUUID: poolUUID, positionID: positionID) @@ -256,12 +386,17 @@ access(all) contract FlowCreditMarketRegistry { self.account.capabilities.publish(pubCap, at: publicPath) } // borrow the RebalanceHandler, set its internal capability & return - let rebalanceHandler = self.account.storage.borrow(from: storagePath) ?? panic("Failed to initialize RebalanceHandler") + let rebalanceHandler = self.account.storage.borrow(from: storagePath) + ?? panic("Failed to initialize RebalanceHandler") let handlerCap = self.account.capabilities.storage.issue(storagePath) rebalanceHandler._setSelfCapability(handlerCap) return rebalanceHandler } + /// Cleans up the rebalance handler associated with the identified pool and position + /// + /// @param poolUUID: The UUID of the pool + /// @param positionID: The ID of the position access(self) fun _cleanupRebalanceHandler(poolUUID: UInt64, positionID: UInt64) { let storagePath = self.deriveRebalanceHandlerStoragePath(poolUUID: poolUUID, positionID: positionID) let publicPath = self.deriveRebalanceHandlerPublicPath(poolUUID: poolUUID, positionID: positionID) @@ -277,11 +412,21 @@ access(all) contract FlowCreditMarketRegistry { Burner.burn(<-removed) } + /// Borrows a reference to the pool associated with the identified pool + /// + /// @param poolUUID: The UUID of the pool + /// + /// @return auth(FlowCreditMarket.EPosition) &FlowCreditMarket.Pool?: The reference to the pool, or nil if the pool is not found access(self) view fun _borrowAuthPool(_ poolUUID: UInt64): auth(FlowCreditMarket.EPosition) &FlowCreditMarket.Pool? { let poolPath = FlowCreditMarket.PoolStoragePath return self.account.storage.borrow(from: poolPath) } + /// Withdraws the identified amount of Flow tokens from the FlowToken vault or `nil` if the funds are unavailable + /// + /// @param amount: The amount of Flow tokens to withdraw + /// + /// @return @FlowToken.Vault?: The vault with the withdrawn amount, or nil if the withdrawal failed access(self) fun _withdrawFees(amount: UFix64): @FlowToken.Vault? { let vaultPath = /storage/flowTokenVault let vault = self.account.storage.borrow(from: vaultPath) @@ -294,10 +439,17 @@ access(all) contract FlowCreditMarketRegistry { init() { let poolUUID = self.account.storage.borrow<&FlowCreditMarket.Pool>(from: FlowCreditMarket.PoolStoragePath)?.uuid ?? panic("Cannot initialize FlowCreditMarketScheduler without an initialized FlowCreditMarket Pool in storage") - let path = FlowCreditMarket.deriveRegistryStoragePath(forPool: poolUUID) + let storagePath = FlowCreditMarket.deriveRegistryStoragePath(forPool: poolUUID) + let publicPath = FlowCreditMarket.deriveRegistryPublicPath(forPool: poolUUID) - // TODO: update config schema for scheduled txn data formats - let defaultRebalanceRecurringConfig = {"force": false} - self.account.storage.save(<-create Registry(defaultRebalanceRecurringConfig: defaultRebalanceRecurringConfig), to: path) + let defaultRebalanceRecurringConfig = { + "interval": 60.0 * 10.0, // 10 minutes in seconds + "priority": 2, // Low priority + "executionEffort": 999, // 999 gas units + "force": false // Do not force rebalance + } + self.account.storage.save(<-create Registry(defaultRebalanceRecurringConfig: defaultRebalanceRecurringConfig), to: storagePath) + let pubCap = self.account.capabilities.storage.issue<&Registry>(storagePath) + self.account.capabilities.publish(pubCap, at: publicPath) } } diff --git a/cadence/scripts/flow-credit-market/get_next_position_id.cdc b/cadence/scripts/flow-credit-market/get_next_position_id.cdc new file mode 100644 index 0000000..b5c111c --- /dev/null +++ b/cadence/scripts/flow-credit-market/get_next_position_id.cdc @@ -0,0 +1,13 @@ +import "FlowCreditMarket" + +/// Returns the next position ID for a given pool +/// +/// @param poolUUID: The UUID of the pool +/// +access(all) +fun main(): UInt64 { + let pool = getAuthAccount(Type<@FlowCreditMarket.Pool>().address!).storage.borrow<&FlowCreditMarket.Pool>( + from:FlowCreditMarket.PoolStoragePath + ) ?? panic("Could not borrow reference to Pool from \(FlowCreditMarket.PoolStoragePath) - ensure a Pool has been configured") + return pool.nextPositionID +} diff --git a/cadence/transactions/flow-credit-market/registry/registry/backfill_positions.cdc b/cadence/transactions/flow-credit-market/registry/registry/backfill_positions.cdc new file mode 100644 index 0000000..90029d7 --- /dev/null +++ b/cadence/transactions/flow-credit-market/registry/registry/backfill_positions.cdc @@ -0,0 +1,28 @@ +import "FlowCreditMarket" +import "FlowCreditMarketRegistry" + +/// INTENDED FOR BETA PURPOSES ONLY +/// +/// This transaction will backfill all unregistered Pool positions into the Registry +transaction { + + let pool: &FlowCreditMarket.Pool + let registry: auth(FlowCreditMarket.Register) &FlowCreditMarketRegistry.Registry + + prepare(signer: auth(BorrowValue) &Account) { + self.pool = signer.storage.borrow(from: FlowCreditMarket.PoolStoragePath) + ?? panic("Could not borrow reference to Pool from \(FlowCreditMarket.PoolStoragePath) - ensure a Pool has been configured") + let registryPath = FlowCreditMarket.deriveRegistryStoragePath(forPool: self.pool.uuid) + self.registry = signer.storage.borrow(from: registryPath) + ?? panic("Could not borrow reference to Registry from \(registryPath) - ensure a Registry has been configured") + } + + execute { + for pid in self.pool.getPositionIDs() { + if self.registry.registeredPositions[pid] != nil { + continue + } + self.registry.registerPosition(poolUUID: self.pool.uuid, pid: pid, rebalanceConfig: nil) + } + } +} diff --git a/cadence/transactions/flow-credit-market/registry/registry/backfill_specific_positions.cdc b/cadence/transactions/flow-credit-market/registry/registry/backfill_specific_positions.cdc new file mode 100644 index 0000000..52b718a --- /dev/null +++ b/cadence/transactions/flow-credit-market/registry/registry/backfill_specific_positions.cdc @@ -0,0 +1,28 @@ +import "FlowCreditMarket" +import "FlowCreditMarketRegistry" + +/// INTENDED FOR BETA PURPOSES ONLY +/// +/// This transaction will backfill the provided positions into the Registry +transaction(pids: [UInt64]) { + + let pool: &FlowCreditMarket.Pool + let registry: auth(FlowCreditMarket.Register) &FlowCreditMarketRegistry.Registry + + prepare(signer: auth(BorrowValue) &Account) { + self.pool = signer.storage.borrow(from: FlowCreditMarket.PoolStoragePath) + ?? panic("Could not borrow reference to Pool from \(FlowCreditMarket.PoolStoragePath) - ensure a Pool has been configured") + let registryPath = FlowCreditMarket.deriveRegistryStoragePath(forPool: self.pool.uuid) + self.registry = signer.storage.borrow(from: registryPath) + ?? panic("Could not borrow reference to Registry from \(registryPath) - ensure a Registry has been configured") + } + + execute { + for pid in pids { + if self.registry.registeredPositions[pid] != nil { + continue + } + self.registry.registerPosition(poolUUID: self.pool.uuid, pid: pid, rebalanceConfig: nil) + } + } +} From cb2a55e11ff13d6f49144a3bed79fe8ab7bf16dc Mon Sep 17 00:00:00 2001 From: sisyphusSmiling Date: Thu, 18 Dec 2025 18:01:07 -0700 Subject: [PATCH 6/8] add FCMRegistry supporting transactions --- .../schedule_next_rebalance_internal.cdc | 22 +++++++++++ .../registry/backfill_specific_positions.cdc | 2 + ...default_rebalance_recurring_config.cdc.cdc | 38 +++++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 cadence/transactions/flow-credit-market/registry/rebalance-handler/schedule_next_rebalance_internal.cdc create mode 100644 cadence/transactions/flow-credit-market/registry/registry/set_default_rebalance_recurring_config.cdc.cdc diff --git a/cadence/transactions/flow-credit-market/registry/rebalance-handler/schedule_next_rebalance_internal.cdc b/cadence/transactions/flow-credit-market/registry/rebalance-handler/schedule_next_rebalance_internal.cdc new file mode 100644 index 0000000..953d8b0 --- /dev/null +++ b/cadence/transactions/flow-credit-market/registry/rebalance-handler/schedule_next_rebalance_internal.cdc @@ -0,0 +1,22 @@ +import "FlowCreditMarket" +import "FlowCreditMarketRegistry" + +/// Schedules the RebalanceHandler's next internally-managed scheduled rebalancing +/// +/// @param pid: The ID of the position to rebalance in the canonical Pool +transaction(pid: UInt64) { + + let handler: auth(FlowCreditMarket.Schedule) &FlowCreditMarketRegistry.RebalanceHandler + + prepare(signer: auth(BorrowValue) &Account) { + let pool = signer.storage.borrow(from: FlowCreditMarket.PoolStoragePath) + ?? panic("Could not borrow reference to Pool from \(FlowCreditMarket.PoolStoragePath) - ensure a Pool has been configured") + let handlerPath = FlowCreditMarketRegistry.deriveRebalanceHandlerStoragePath(poolUUID: pool.uuid, positionID: pid) + self.handler = signer.storage.borrow(from: handlerPath) + ?? panic("Could not borrow reference to RebalanceHandler from \(handlerPath) - ensure a RebalanceHandler has been configured") + } + + execute { + self.handler.scheduleNextRebalance(whileExecuting: nil, data: nil) + } +} diff --git a/cadence/transactions/flow-credit-market/registry/registry/backfill_specific_positions.cdc b/cadence/transactions/flow-credit-market/registry/registry/backfill_specific_positions.cdc index 52b718a..5d8ead3 100644 --- a/cadence/transactions/flow-credit-market/registry/registry/backfill_specific_positions.cdc +++ b/cadence/transactions/flow-credit-market/registry/registry/backfill_specific_positions.cdc @@ -4,6 +4,8 @@ import "FlowCreditMarketRegistry" /// INTENDED FOR BETA PURPOSES ONLY /// /// This transaction will backfill the provided positions into the Registry +/// +/// @param pids: The IDs of the positions to backfill - registration fails if the PID doesn't exist in the canonical pool transaction(pids: [UInt64]) { let pool: &FlowCreditMarket.Pool diff --git a/cadence/transactions/flow-credit-market/registry/registry/set_default_rebalance_recurring_config.cdc.cdc b/cadence/transactions/flow-credit-market/registry/registry/set_default_rebalance_recurring_config.cdc.cdc new file mode 100644 index 0000000..2241739 --- /dev/null +++ b/cadence/transactions/flow-credit-market/registry/registry/set_default_rebalance_recurring_config.cdc.cdc @@ -0,0 +1,38 @@ +import "FlowCreditMarket" +import "FlowCreditMarketRegistry" + +/// This transaction will set the default rebalance recurring configuration for the Registry, defining the default +/// scheduled transaction configuration for all registered positions. +/// +/// @param interval: The interval at which to rebalance (in seconds) +/// @param priority: The priority of the rebalance (0: High, 1: Medium, 2: Low) +/// @param executionEffort: The execution effort of the rebalance +/// @param force: The force rebalance flag +transaction( + interval: UFix64, + priority: UInt8, + executionEffort: UInt64, + force: Bool +) { + + let registry: auth(FlowCreditMarket.Register) &FlowCreditMarketRegistry.Registry + let config: {String: AnyStruct} + + prepare(signer: auth(BorrowValue) &Account) { + let pool = signer.storage.borrow(from: FlowCreditMarket.PoolStoragePath) + ?? panic("Could not borrow reference to Pool from \(FlowCreditMarket.PoolStoragePath) - ensure a Pool has been configured") + let registryPath = FlowCreditMarket.deriveRegistryStoragePath(forPool: pool.uuid) + self.registry = signer.storage.borrow(from: registryPath) + ?? panic("Could not borrow reference to Registry from \(registryPath) - ensure a Registry has been configured") + self.config = { + "interval": interval, + "priority": priority, + "executionEffort": executionEffort, + "force": force + } + } + + execute { + self.registry.setDefaultRebalanceRecurringConfig(config: self.config) + } +} From 8c5f22de6be4e3b1baf3bcfca12da4c705e4ee3f Mon Sep 17 00:00:00 2001 From: sisyphusSmiling Date: Thu, 18 Dec 2025 18:15:50 -0700 Subject: [PATCH 7/8] fix failing tests --- cadence/contracts/FlowCreditMarketRegistry.cdc | 7 +++++++ .../flow-credit-market/get_next_position_id.cdc | 2 -- .../scripts/flow-credit-market/get_position_ids.cdc | 11 +++++++++++ cadence/tests/test_helpers.cdc | 6 ++++++ 4 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 cadence/scripts/flow-credit-market/get_position_ids.cdc diff --git a/cadence/contracts/FlowCreditMarketRegistry.cdc b/cadence/contracts/FlowCreditMarketRegistry.cdc index 541ee6b..320092c 100644 --- a/cadence/contracts/FlowCreditMarketRegistry.cdc +++ b/cadence/contracts/FlowCreditMarketRegistry.cdc @@ -67,6 +67,13 @@ access(all) contract FlowCreditMarketRegistry { return self.rebalanceConfigs[pid] ?? self.defaultRebalanceRecurringConfig } + /// Returns the IDs of the positions that have a custom rebalance configuration + /// + /// @return [UInt64]: The IDs of the positions that have a custom rebalance configuration + access(all) view fun getIDsWithCustomConfig(): [UInt64] { + return self.rebalanceConfigs.keys + } + /// Registers a position in the registry associated with the identified pool and position ID /// /// @param poolUUID: The UUID of the pool diff --git a/cadence/scripts/flow-credit-market/get_next_position_id.cdc b/cadence/scripts/flow-credit-market/get_next_position_id.cdc index b5c111c..6f20a50 100644 --- a/cadence/scripts/flow-credit-market/get_next_position_id.cdc +++ b/cadence/scripts/flow-credit-market/get_next_position_id.cdc @@ -2,8 +2,6 @@ import "FlowCreditMarket" /// Returns the next position ID for a given pool /// -/// @param poolUUID: The UUID of the pool -/// access(all) fun main(): UInt64 { let pool = getAuthAccount(Type<@FlowCreditMarket.Pool>().address!).storage.borrow<&FlowCreditMarket.Pool>( diff --git a/cadence/scripts/flow-credit-market/get_position_ids.cdc b/cadence/scripts/flow-credit-market/get_position_ids.cdc new file mode 100644 index 0000000..d6ebd4d --- /dev/null +++ b/cadence/scripts/flow-credit-market/get_position_ids.cdc @@ -0,0 +1,11 @@ +import "FlowCreditMarket" + +/// Returns all Position IDs in the canonical Pool +/// +access(all) +fun main(): [UInt64] { + let pool = getAuthAccount(Type<@FlowCreditMarket.Pool>().address!).storage.borrow<&FlowCreditMarket.Pool>( + from:FlowCreditMarket.PoolStoragePath + ) ?? panic("Could not borrow reference to Pool from \(FlowCreditMarket.PoolStoragePath) - ensure a Pool has been configured") + return pool.getPositionIDs() +} diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index 02f8141..885ce2a 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -88,6 +88,12 @@ fun deployContracts() { arguments: [] ) Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "FlowCreditMarketRegistry", + path: "../contracts/FlowCreditMarketRegistry.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) // NOTE: Do not publish beta capability here; some tests create the Pool later and // publishing before pool creation will fail. Tests that need the cap should call From d38e76d9a2eae36659d8b3e26e2787e74c2ff9f2 Mon Sep 17 00:00:00 2001 From: sisyphusSmiling Date: Thu, 18 Dec 2025 18:22:40 -0700 Subject: [PATCH 8/8] fix failing tests --- cadence/tests/auto_borrow_behavior_test.cdc | 8 ++++++ cadence/tests/cap_test.cdc | 8 ++++++ ...nds_available_above_target_health_test.cdc | 7 +++++ .../funds_required_for_target_health_test.cdc | 7 +++++ cadence/tests/governance_parameters_test.cdc | 7 +++++ cadence/tests/insolvency_redemption_test.cdc | 7 +++++ cadence/tests/liquidation_phase1_test.cdc | 7 +++++ cadence/tests/liquidation_phase2_dex_test.cdc | 7 +++++ cadence/tests/platform_integration_test.cdc | 28 +++++++++++++++++++ cadence/tests/pool_creation_workflow_test.cdc | 7 +++++ .../tests/position_lifecycle_happy_test.cdc | 7 +++++ .../rebalance_overcollateralised_test.cdc | 7 +++++ .../rebalance_undercollateralised_test.cdc | 7 +++++ cadence/tests/reserve_withdrawal_test.cdc | 7 +++++ cadence/tests/test_helpers.cdc | 6 ---- .../tests/token_governance_addition_test.cdc | 7 +++++ cadence/tests/zero_debt_withdrawal_test.cdc | 7 +++++ 17 files changed, 135 insertions(+), 6 deletions(-) diff --git a/cadence/tests/auto_borrow_behavior_test.cdc b/cadence/tests/auto_borrow_behavior_test.cdc index 5856698..71d4657 100644 --- a/cadence/tests/auto_borrow_behavior_test.cdc +++ b/cadence/tests/auto_borrow_behavior_test.cdc @@ -31,6 +31,14 @@ fun testAutoBorrowBehaviorWithTargetHealth() { setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: moetTokenIdentifier, price: initialPrice) createAndStorePool(signer: protocolAccount, defaultTokenIdentifier: moetTokenIdentifier, beFailed: false) + + // Must be deployed after the Pool is created + var err = Test.deployContract( + name: "FlowCreditMarketRegistry", + path: "../contracts/FlowCreditMarketRegistry.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) // Add Flow token support with collateralFactor=0.8 addSupportedTokenSimpleInterestCurve( diff --git a/cadence/tests/cap_test.cdc b/cadence/tests/cap_test.cdc index 8eb0112..2eaf966 100644 --- a/cadence/tests/cap_test.cdc +++ b/cadence/tests/cap_test.cdc @@ -23,6 +23,14 @@ fun setup() { createAndStorePool(signer: protocolAccount, defaultTokenIdentifier: defaultTokenIdentifier, beFailed: false) + // Must be deployed after the Pool is created + var err = Test.deployContract( + name: "FlowCreditMarketRegistry", + path: "../contracts/FlowCreditMarketRegistry.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + let exists = poolExists(address: protocolAccount.address) Test.assert(exists) diff --git a/cadence/tests/funds_available_above_target_health_test.cdc b/cadence/tests/funds_available_above_target_health_test.cdc index 424e42a..7e5e1e9 100644 --- a/cadence/tests/funds_available_above_target_health_test.cdc +++ b/cadence/tests/funds_available_above_target_health_test.cdc @@ -63,6 +63,13 @@ fun setup() { // create the Pool & add FLOW as suppoorted token createAndStorePool(signer: protocolAccount, defaultTokenIdentifier: defaultTokenIdentifier, beFailed: false) + // Must be deployed after the Pool is created + var err = Test.deployContract( + name: "FlowCreditMarketRegistry", + path: "../contracts/FlowCreditMarketRegistry.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) addSupportedTokenSimpleInterestCurve( signer: protocolAccount, tokenTypeIdentifier: flowTokenIdentifier, diff --git a/cadence/tests/funds_required_for_target_health_test.cdc b/cadence/tests/funds_required_for_target_health_test.cdc index dd5b5c3..0580dc7 100644 --- a/cadence/tests/funds_required_for_target_health_test.cdc +++ b/cadence/tests/funds_required_for_target_health_test.cdc @@ -60,6 +60,13 @@ fun setup() { // create the Pool & add FLOW as suppoorted token createAndStorePool(signer: protocolAccount, defaultTokenIdentifier: defaultTokenIdentifier, beFailed: false) + // Must be deployed after the Pool is created + var err = Test.deployContract( + name: "FlowCreditMarketRegistry", + path: "../contracts/FlowCreditMarketRegistry.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) addSupportedTokenSimpleInterestCurve( signer: protocolAccount, tokenTypeIdentifier: flowTokenIdentifier, diff --git a/cadence/tests/governance_parameters_test.cdc b/cadence/tests/governance_parameters_test.cdc index 38ba16c..f64b1f1 100644 --- a/cadence/tests/governance_parameters_test.cdc +++ b/cadence/tests/governance_parameters_test.cdc @@ -15,6 +15,13 @@ access(all) fun test_setGovernanceParams_and_exercise_paths() { // Create pool createAndStorePool(signer: protocolAccount, defaultTokenIdentifier: defaultTokenIdentifier, beFailed: false) + // Must be deployed after the Pool is created + var err = Test.deployContract( + name: "FlowCreditMarketRegistry", + path: "../contracts/FlowCreditMarketRegistry.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) // 1) Exercise setInsuranceRate and negative-credit-rate branch // Set a relatively high insurance rate and construct a state with tiny debit income diff --git a/cadence/tests/insolvency_redemption_test.cdc b/cadence/tests/insolvency_redemption_test.cdc index 004120b..568f520 100644 --- a/cadence/tests/insolvency_redemption_test.cdc +++ b/cadence/tests/insolvency_redemption_test.cdc @@ -25,6 +25,13 @@ fun setup() { setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: flowTokenIdentifier, price: 1.0) createAndStorePool(signer: protocolAccount, defaultTokenIdentifier: defaultTokenIdentifier, beFailed: false) + // Must be deployed after the Pool is created + var err = Test.deployContract( + name: "FlowCreditMarketRegistry", + path: "../contracts/FlowCreditMarketRegistry.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) grantPoolCapToConsumer() addSupportedTokenSimpleInterestCurve( signer: protocolAccount, diff --git a/cadence/tests/liquidation_phase1_test.cdc b/cadence/tests/liquidation_phase1_test.cdc index 2cfae90..2bedc6e 100644 --- a/cadence/tests/liquidation_phase1_test.cdc +++ b/cadence/tests/liquidation_phase1_test.cdc @@ -25,6 +25,13 @@ fun setup() { setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: flowTokenIdentifier, price: 1.0) createAndStorePool(signer: protocolAccount, defaultTokenIdentifier: defaultTokenIdentifier, beFailed: false) + // Must be deployed after the Pool is created + var err = Test.deployContract( + name: "FlowCreditMarketRegistry", + path: "../contracts/FlowCreditMarketRegistry.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) grantPoolCapToConsumer() addSupportedTokenSimpleInterestCurve( signer: protocolAccount, diff --git a/cadence/tests/liquidation_phase2_dex_test.cdc b/cadence/tests/liquidation_phase2_dex_test.cdc index 496427a..a18dcad 100644 --- a/cadence/tests/liquidation_phase2_dex_test.cdc +++ b/cadence/tests/liquidation_phase2_dex_test.cdc @@ -16,6 +16,13 @@ fun setup() { setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: Type<@FlowToken.Vault>().identifier, price: 1.0) createAndStorePool(signer: protocolAccount, defaultTokenIdentifier: Type<@MOET.Vault>().identifier, beFailed: false) + // Must be deployed after the Pool is created + var err = Test.deployContract( + name: "FlowCreditMarketRegistry", + path: "../contracts/FlowCreditMarketRegistry.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) grantPoolCapToConsumer() addSupportedTokenSimpleInterestCurve( signer: protocolAccount, diff --git a/cadence/tests/platform_integration_test.cdc b/cadence/tests/platform_integration_test.cdc index 9bc467d..1be927c 100644 --- a/cadence/tests/platform_integration_test.cdc +++ b/cadence/tests/platform_integration_test.cdc @@ -37,6 +37,13 @@ fun testDeploymentSucceeds() { access(all) fun testCreatePoolSucceeds() { createAndStorePool(signer: protocolAccount, defaultTokenIdentifier: defaultTokenIdentifier, beFailed: false) + // Must be deployed after the Pool is created + var err = Test.deployContract( + name: "FlowCreditMarketRegistry", + path: "../contracts/FlowCreditMarketRegistry.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) let existsRes = _executeScript("../scripts/flow-credit-market/pool_exists.cdc", [protocolAccount.address]) Test.expect(existsRes, Test.beSucceeded()) @@ -54,6 +61,13 @@ fun testCreateUserPositionSucceeds() { // create pool & add FLOW as supported token in globalLedger createAndStorePool(signer: protocolAccount, defaultTokenIdentifier: defaultTokenIdentifier, beFailed: false) + // Must be deployed after the Pool is created + var err = Test.deployContract( + name: "FlowCreditMarketRegistry", + path: "../contracts/FlowCreditMarketRegistry.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) addSupportedTokenSimpleInterestCurve( signer: protocolAccount, tokenTypeIdentifier: flowTokenIdentifier, @@ -105,6 +119,13 @@ fun testUndercollateralizedPositionRebalanceSucceeds() { // create pool & add FLOW as supported token in globalLedger createAndStorePool(signer: protocolAccount, defaultTokenIdentifier: defaultTokenIdentifier, beFailed: false) + // Must be deployed after the Pool is created + var err = Test.deployContract( + name: "FlowCreditMarketRegistry", + path: "../contracts/FlowCreditMarketRegistry.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) addSupportedTokenSimpleInterestCurve( signer: protocolAccount, tokenTypeIdentifier: flowTokenIdentifier, @@ -169,6 +190,13 @@ fun testOvercollateralizedPositionRebalanceSucceeds() { // create pool & add FLOW as supported token in globalLedger createAndStorePool(signer: protocolAccount, defaultTokenIdentifier: defaultTokenIdentifier, beFailed: false) + // Must be deployed after the Pool is created + var err = Test.deployContract( + name: "FlowCreditMarketRegistry", + path: "../contracts/FlowCreditMarketRegistry.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) addSupportedTokenSimpleInterestCurve( signer: protocolAccount, tokenTypeIdentifier: flowTokenIdentifier, diff --git a/cadence/tests/pool_creation_workflow_test.cdc b/cadence/tests/pool_creation_workflow_test.cdc index e58d0db..42d492e 100644 --- a/cadence/tests/pool_creation_workflow_test.cdc +++ b/cadence/tests/pool_creation_workflow_test.cdc @@ -31,6 +31,13 @@ access(all) fun testPoolCreationSucceeds() { // --- act --------------------------------------------------------------- createAndStorePool(signer: protocolAccount, defaultTokenIdentifier: defaultTokenIdentifier, beFailed: false) + // Must be deployed after the Pool is created + var err = Test.deployContract( + name: "FlowCreditMarketRegistry", + path: "../contracts/FlowCreditMarketRegistry.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) // --- assert ------------------------------------------------------------ let exists = poolExists(address: protocolAccount.address) diff --git a/cadence/tests/position_lifecycle_happy_test.cdc b/cadence/tests/position_lifecycle_happy_test.cdc index 5bbf20f..c1e960b 100644 --- a/cadence/tests/position_lifecycle_happy_test.cdc +++ b/cadence/tests/position_lifecycle_happy_test.cdc @@ -39,6 +39,13 @@ fun testPositionLifecycleHappyPath() { // create pool & enable token createAndStorePool(signer: protocolAccount, defaultTokenIdentifier: defaultTokenIdentifier, beFailed: false) + // Must be deployed after the Pool is created + var err = Test.deployContract( + name: "FlowCreditMarketRegistry", + path: "../contracts/FlowCreditMarketRegistry.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) addSupportedTokenSimpleInterestCurve( signer: protocolAccount, tokenTypeIdentifier: flowTokenIdentifier, diff --git a/cadence/tests/rebalance_overcollateralised_test.cdc b/cadence/tests/rebalance_overcollateralised_test.cdc index 97acb0b..9c85758 100644 --- a/cadence/tests/rebalance_overcollateralised_test.cdc +++ b/cadence/tests/rebalance_overcollateralised_test.cdc @@ -33,6 +33,13 @@ fun testRebalanceOvercollateralised() { setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: moetTokenIdentifier, price: initialPrice) createAndStorePool(signer: protocolAccount, defaultTokenIdentifier: moetTokenIdentifier, beFailed: false) + // Must be deployed after the Pool is created + var err = Test.deployContract( + name: "FlowCreditMarketRegistry", + path: "../contracts/FlowCreditMarketRegistry.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) addSupportedTokenSimpleInterestCurve( signer: protocolAccount, tokenTypeIdentifier: flowTokenIdentifier, diff --git a/cadence/tests/rebalance_undercollateralised_test.cdc b/cadence/tests/rebalance_undercollateralised_test.cdc index 9e784d9..4ef5376 100644 --- a/cadence/tests/rebalance_undercollateralised_test.cdc +++ b/cadence/tests/rebalance_undercollateralised_test.cdc @@ -31,6 +31,13 @@ fun testRebalanceUndercollateralised() { // pool + token support createAndStorePool(signer: protocolAccount, defaultTokenIdentifier: defaultTokenIdentifier, beFailed: false) + // Must be deployed after the Pool is created + var err = Test.deployContract( + name: "FlowCreditMarketRegistry", + path: "../contracts/FlowCreditMarketRegistry.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) addSupportedTokenSimpleInterestCurve( signer: protocolAccount, tokenTypeIdentifier: flowTokenIdentifier, diff --git a/cadence/tests/reserve_withdrawal_test.cdc b/cadence/tests/reserve_withdrawal_test.cdc index 59a7e93..531160e 100644 --- a/cadence/tests/reserve_withdrawal_test.cdc +++ b/cadence/tests/reserve_withdrawal_test.cdc @@ -21,6 +21,13 @@ access(all) fun testReserveWithdrawalGovernanceControlled() { // create pool createAndStorePool(signer: protocolAccount, defaultTokenIdentifier: defaultTokenIdentifier, beFailed: false) + // Must be deployed after the Pool is created + var err = Test.deployContract( + name: "FlowCreditMarketRegistry", + path: "../contracts/FlowCreditMarketRegistry.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) // Setup MOET vault for treasury account setupMoetVault(treasury, beFailed: false) diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index 885ce2a..02f8141 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -88,12 +88,6 @@ fun deployContracts() { arguments: [] ) Test.expect(err, Test.beNil()) - err = Test.deployContract( - name: "FlowCreditMarketRegistry", - path: "../contracts/FlowCreditMarketRegistry.cdc", - arguments: [] - ) - Test.expect(err, Test.beNil()) // NOTE: Do not publish beta capability here; some tests create the Pool later and // publishing before pool creation will fail. Tests that need the cap should call diff --git a/cadence/tests/token_governance_addition_test.cdc b/cadence/tests/token_governance_addition_test.cdc index 0cf69c4..df1b051 100644 --- a/cadence/tests/token_governance_addition_test.cdc +++ b/cadence/tests/token_governance_addition_test.cdc @@ -26,6 +26,13 @@ fun testAddSupportedTokenSucceedsAndDuplicateFails() { // create pool first createAndStorePool(signer: protocolAccount, defaultTokenIdentifier: defaultTokenIdentifier, beFailed: false) + // Must be deployed after the Pool is created + var err = Test.deployContract( + name: "FlowCreditMarketRegistry", + path: "../contracts/FlowCreditMarketRegistry.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) // add FLOW token support addSupportedTokenSimpleInterestCurve( diff --git a/cadence/tests/zero_debt_withdrawal_test.cdc b/cadence/tests/zero_debt_withdrawal_test.cdc index 733ac91..3f32ef8 100644 --- a/cadence/tests/zero_debt_withdrawal_test.cdc +++ b/cadence/tests/zero_debt_withdrawal_test.cdc @@ -29,6 +29,13 @@ fun testZeroDebtFullWithdrawalAvailable() { // 2. pool + token support createAndStorePool(signer: protocolAccount, defaultTokenIdentifier: defaultTokenIdentifier, beFailed: false) + // Must be deployed after the Pool is created + var err = Test.deployContract( + name: "FlowCreditMarketRegistry", + path: "../contracts/FlowCreditMarketRegistry.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) addSupportedTokenSimpleInterestCurve( signer: protocolAccount, tokenTypeIdentifier: flowTokenIdentifier,