From f8e0c64e7e222f620b7006a471456c2373792346 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Fri, 27 Feb 2026 11:34:07 -0500 Subject: [PATCH 01/26] multi debt --- cadence/contracts/FlowALPv0.cdc | 156 ++++++++-- cadence/tests/contracts/DummyToken.cdc | 213 ++++++++++++++ .../multi_token_reserve_borrowing_test.cdc | 274 ++++++++++++++++++ cadence/tests/test_helpers.cdc | 8 + .../tests/transactions/dummy_token/mint.cdc | 17 ++ .../transactions/dummy_token/setup_vault.cdc | 14 + flow.json | 6 + 7 files changed, 669 insertions(+), 19 deletions(-) create mode 100644 cadence/tests/contracts/DummyToken.cdc create mode 100644 cadence/tests/multi_token_reserve_borrowing_test.cdc create mode 100644 cadence/tests/transactions/dummy_token/mint.cdc create mode 100644 cadence/tests/transactions/dummy_token/setup_vault.cdc diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 52c40a90..0a4908c1 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -579,6 +579,56 @@ access(all) contract FlowALPv0 { /// Possibly an attack vector on automated rebalancing, if multiple positions are rebalanced in the same transaction. self.topUpSource = source } + + /// Returns the current collateral type for this position based on existing Credit balances + /// Returns nil if no Credit balance exists yet (allows any collateral type for first deposit) + access(all) fun getCollateralType(): Type? { + for type in self.balances.keys { + if self.balances[type]!.direction == BalanceDirection.Credit { + return type + } + } + return nil + } + + /// Returns the current debt type for this position based on existing Debit balances + /// Returns nil if no Debit balance exists yet (allows any debt type for first borrow) + access(all) fun getDebtType(): Type? { + for type in self.balances.keys { + if self.balances[type]!.direction == BalanceDirection.Debit { + return type + } + } + return nil + } + + /// Validates that the given token type can be used as collateral for this position + /// Panics if position already has a different collateral type + access(EImplementation) fun validateCollateralType(_ type: Type) { + let existingType = self.getCollateralType() + if existingType == nil { + // No collateral yet, allow any type + return + } + + if existingType! != type { + panic("Position already has collateral type ".concat(existingType!.identifier).concat(". Cannot deposit ").concat(type.identifier).concat(". Only one collateral type allowed per position.")) + } + } + + /// Validates that the given token type can be used as debt for this position + /// Panics if position already has a different debt type + access(EImplementation) fun validateDebtType(_ type: Type) { + let existingType = self.getDebtType() + if existingType == nil { + // No debt yet, allow any type + return + } + + if existingType! != type { + panic("Position already has debt type ".concat(existingType!.identifier).concat(". Cannot borrow ").concat(type.identifier).concat(". Only one debt type allowed per position.")) + } + } } /// InterestCurve @@ -2024,6 +2074,9 @@ access(all) contract FlowALPv0 { let debtState = self._borrowUpdatedTokenState(type: debtType) if position.balances[debtType] == nil { + // Liquidation is repaying debt - validate single debt type + position.validateDebtType(debtType) + position.balances[debtType] = InternalBalance(direction: BalanceDirection.Debit, scaledBalance: 0.0) } position.balances[debtType]!.recordDeposit(amount: UFix128(repayAmount), tokenState: debtState) @@ -2031,8 +2084,22 @@ access(all) contract FlowALPv0 { // Withdraw seized collateral from position and send to liquidator let seizeState = self._borrowUpdatedTokenState(type: seizeType) if position.balances[seizeType] == nil { + // MOET cannot be seized as collateral (it should never exist as collateral) + if seizeType == Type<@MOET.Vault>() { + panic("Cannot seize MOET as collateral. MOET should not exist as collateral in any position.") + } + + // Liquidation is seizing collateral - validate single collateral type + position.validateCollateralType(seizeType) + position.balances[seizeType] = InternalBalance(direction: BalanceDirection.Credit, scaledBalance: 0.0) } + + // Additional safety check: MOET should never be seized as collateral + if seizeType == Type<@MOET.Vault>() && position.balances[seizeType]!.direction == BalanceDirection.Credit { + panic("Cannot seize MOET as collateral. MOET should not exist as collateral in any position.") + } + position.balances[seizeType]!.recordWithdrawal(amount: UFix128(seizeAmount), tokenState: seizeState) let seizeReserveRef = (&self.reserves[seizeType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! let seizedCollateral <- seizeReserveRef.withdraw(amount: seizeAmount) @@ -2779,6 +2846,14 @@ access(all) contract FlowALPv0 { // If this position doesn't currently have an entry for this token, create one. if position.balances[type] == nil { + // MOET cannot be used as collateral (only as debt) + if type == Type<@MOET.Vault>() { + panic("MOET cannot be deposited as collateral. MOET can only be borrowed (debt), not used as collateral.") + } + + // Validate single collateral type constraint + position.validateCollateralType(type) + position.balances[type] = InternalBalance( direction: BalanceDirection.Credit, scaledBalance: 0.0 @@ -2796,6 +2871,13 @@ access(all) contract FlowALPv0 { // This only records the portion of the deposit that was accepted, not any queued portions, // as the queued deposits will be processed later (by this function being called again), and therefore // will be recorded at that time. + + // MOET cannot be deposited as collateral (Credit direction) + // It can only be deposited to repay debt (Debit direction) + if type == Type<@MOET.Vault>() && position.balances[type]!.direction == BalanceDirection.Credit { + panic("MOET cannot be deposited as collateral. MOET can only be borrowed (debt), not used as collateral.") + } + let acceptedAmount = from.balance position.balances[type]!.recordDeposit( amount: UFix128(acceptedAmount), @@ -2982,20 +3064,28 @@ access(all) contract FlowALPv0 { // If this position doesn't currently have an entry for this token, create one. if position.balances[type] == nil { + // When withdrawing a token that doesn't exist in position yet, + // we'll be creating a debit (debt). Validate single debt type. + position.validateDebtType(type) + position.balances[type] = InternalBalance( direction: BalanceDirection.Credit, scaledBalance: 0.0 ) } - let reserveVault = (&self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! - // Reflect the withdrawal in the position's balance + let wasCredit = position.balances[type]!.direction == BalanceDirection.Credit let uintAmount = UFix128(amount) position.balances[type]!.recordWithdrawal( amount: uintAmount, tokenState: tokenState ) + + // If we flipped from Credit to Debit, validate debt type constraint + if wasCredit && position.balances[type]!.direction == BalanceDirection.Debit { + position.validateDebtType(type) + } // Attempt to pull additional collateral from the top-up source (if configured) // to keep the position above minHealth after the withdrawal. // Regardless of whether a top-up occurs, the position must be healthy post-withdrawal. @@ -3020,18 +3110,30 @@ access(all) contract FlowALPv0 { // Queue for update if necessary self._queuePositionForUpdateIfNecessary(pid: pid) - let withdrawn <- reserveVault.withdraw(amount: amount) + // Get tokens either by minting (MOET) or from reserves (other tokens) + var withdrawn: @{FungibleToken.Vault}? <- nil + if type == Type<@MOET.Vault>() { + // For MOET, mint new tokens + withdrawn <-! FlowALPv0._borrowMOETMinter().mintTokens(amount: amount) + } else { + // For other tokens, withdraw from reserves + assert(self.reserves[type] != nil, message: "Cannot withdraw \(type.identifier) - no reserves available") + let reserveVault = (&self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! + assert(reserveVault.balance >= amount, message: "Insufficient reserves for \(type.identifier): need \(amount), have \(reserveVault.balance)") + withdrawn <-! reserveVault.withdraw(amount: amount) + } + let unwrappedWithdrawn <- withdrawn! emit Withdrawn( pid: pid, poolUUID: self.uuid, vaultType: type, - amount: withdrawn.balance, - withdrawnUUID: withdrawn.uuid + amount: unwrappedWithdrawn.balance, + withdrawnUUID: unwrappedWithdrawn.uuid ) self._unlockPosition(pid) - return <- withdrawn + return <- unwrappedWithdrawn } /////////////////////// @@ -3453,40 +3555,56 @@ access(all) contract FlowALPv0 { let sinkCapacity = drawDownSink.minimumCapacity() let sinkAmount = (idealWithdrawal > sinkCapacity) ? sinkCapacity : idealWithdrawal - // TODO(jord): we enforce in setDrawDownSink that the type is MOET -> we should panic here if that does not hold (currently silently fail) - 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( + // Support multiple token types: MOET (minted) or other tokens (from reserves) + if sinkAmount > 0.0 { + let tokenState = self._borrowUpdatedTokenState(type: sinkType) + if position.balances[sinkType] == nil { + // Rebalancing is borrowing/withdrawing to push to sink - validate single debt type + position.validateDebtType(sinkType) + + position.balances[sinkType] = InternalBalance( direction: BalanceDirection.Credit, scaledBalance: 0.0 ) } - // record the withdrawal and mint the tokens + // Record the withdrawal let uintSinkAmount = UFix128(sinkAmount) - position.balances[Type<@MOET.Vault>()]!.recordWithdrawal( + position.balances[sinkType]!.recordWithdrawal( amount: uintSinkAmount, tokenState: tokenState ) - let sinkVault <- FlowALPv0._borrowMOETMinter().mintTokens(amount: sinkAmount) + + // Get tokens either by minting (MOET) or from reserves (other tokens) + var sinkVault: @{FungibleToken.Vault}? <- nil + if sinkType == Type<@MOET.Vault>() { + // For MOET, mint new tokens + sinkVault <-! FlowALPv0._borrowMOETMinter().mintTokens(amount: sinkAmount) + } else { + // For other tokens, withdraw from reserves + assert(self.reserves[sinkType] != nil, message: "Cannot withdraw \(sinkAmount) of \(sinkType.identifier) - token not in reserves") + let reserveVault = (&self.reserves[sinkType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! + assert(reserveVault.balance >= sinkAmount, message: "Insufficient reserves for \(sinkType.identifier): available \(reserveVault.balance), needed \(sinkAmount)") + sinkVault <-! reserveVault.withdraw(amount: sinkAmount) + } + let unwrappedSinkVault <- sinkVault! emit Rebalanced( pid: pid, poolUUID: self.uuid, atHealth: balanceSheet.health, - amount: sinkVault.balance, + amount: unwrappedSinkVault.balance, fromUnder: false ) // Push what we can into the sink, and redeposit the rest - drawDownSink.depositCapacity(from: &sinkVault as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) - if sinkVault.balance > 0.0 { + drawDownSink.depositCapacity(from: &unwrappedSinkVault as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) + if unwrappedSinkVault.balance > 0.0 { self._depositEffectsOnly( pid: pid, - from: <-sinkVault, + from: <-unwrappedSinkVault, ) } else { - Burner.burn(<-sinkVault) + Burner.burn(<-unwrappedSinkVault) } } } diff --git a/cadence/tests/contracts/DummyToken.cdc b/cadence/tests/contracts/DummyToken.cdc new file mode 100644 index 00000000..d4ccc85f --- /dev/null +++ b/cadence/tests/contracts/DummyToken.cdc @@ -0,0 +1,213 @@ +import "FungibleToken" +import "MetadataViews" +import "FungibleTokenMetadataViews" + +access(all) contract DummyToken : FungibleToken { + + /// Total supply of DummyToken in existence + access(all) var totalSupply: UFix64 + + /// Storage and Public Paths + access(all) let VaultStoragePath: StoragePath + access(all) let VaultPublicPath: PublicPath + access(all) let ReceiverPublicPath: PublicPath + access(all) let AdminStoragePath: StoragePath + + /// The event that is emitted when new tokens are minted + access(all) event Minted(type: String, amount: UFix64, toUUID: UInt64, minterUUID: UInt64) + /// Emitted whenever a new Minter is created + access(all) event MinterCreated(uuid: UInt64) + + /// createEmptyVault + /// + /// Function that creates a new Vault with a balance of zero + /// and returns it to the calling context. A user must call this function + /// and store the returned Vault in their storage in order to allow their + /// account to be able to receive deposits of this token type. + /// + access(all) fun createEmptyVault(vaultType: Type): @DummyToken.Vault { + return <- create Vault(balance: 0.0) + } + + access(all) view fun getContractViews(resourceType: Type?): [Type] { + return [ + Type(), + Type(), + Type(), + Type() + ] + } + + access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? { + switch viewType { + case Type(): + return FungibleTokenMetadataViews.FTView( + ftDisplay: self.resolveContractView(resourceType: nil, viewType: Type()) as! FungibleTokenMetadataViews.FTDisplay?, + ftVaultData: self.resolveContractView(resourceType: nil, viewType: Type()) as! FungibleTokenMetadataViews.FTVaultData? + ) + case Type(): + let media = MetadataViews.Media( + file: MetadataViews.HTTPFile( + url: "https://assets.website-files.com/5f6294c0c7a8cdd643b1c820/5f6294c0c7a8cda55cb1c936_Flow_Wordmark.svg" + ), + mediaType: "image/svg+xml" + ) + let medias = MetadataViews.Medias([media]) + return FungibleTokenMetadataViews.FTDisplay( + name: "Dummy Token", + symbol: "DUMMY", + description: "A simple test token for testing multi-token borrowing", + externalURL: MetadataViews.ExternalURL("https://example.com"), + logos: medias, + socials: {} + ) + case Type(): + return FungibleTokenMetadataViews.FTVaultData( + storagePath: self.VaultStoragePath, + receiverPath: self.ReceiverPublicPath, + metadataPath: self.VaultPublicPath, + receiverLinkedType: Type<&DummyToken.Vault>(), + metadataLinkedType: Type<&DummyToken.Vault>(), + createEmptyVaultFunction: (fun(): @{FungibleToken.Vault} { + return <-DummyToken.createEmptyVault(vaultType: Type<@DummyToken.Vault>()) + }) + ) + case Type(): + return FungibleTokenMetadataViews.TotalSupply( + totalSupply: DummyToken.totalSupply + ) + } + return nil + } + + /* --- CONSTRUCTS --- */ + + /// Vault + /// + /// Each user stores an instance of only the Vault in their storage + /// The functions in the Vault and governed by the pre and post conditions + /// in FungibleToken when they are called. + /// The checks happen at runtime whenever a function is called. + /// + /// Resources can only be created in the context of the contract that they + /// are defined in, so there is no way for a malicious user to create Vaults + /// out of thin air. A special Minter resource needs to be defined to mint + /// new tokens. + /// + access(all) resource Vault: FungibleToken.Vault { + + /// The total balance of this vault + access(all) var balance: UFix64 + + /// Identifies the destruction of a Vault even when destroyed outside of Buner.burn() scope + access(all) event ResourceDestroyed(uuid: UInt64 = self.uuid, balance: UFix64 = self.balance) + + init(balance: UFix64) { + self.balance = balance + } + + /// Called when a fungible token is burned via the `Burner.burn()` method + access(contract) fun burnCallback() { + if self.balance > 0.0 { + DummyToken.totalSupply = DummyToken.totalSupply - self.balance + } + self.balance = 0.0 + } + + access(all) view fun getViews(): [Type] { + return DummyToken.getContractViews(resourceType: nil) + } + + access(all) fun resolveView(_ view: Type): AnyStruct? { + return DummyToken.resolveContractView(resourceType: nil, viewType: view) + } + + access(all) view fun getSupportedVaultTypes(): {Type: Bool} { + let supportedTypes: {Type: Bool} = {} + supportedTypes[self.getType()] = true + return supportedTypes + } + + access(all) view fun isSupportedVaultType(type: Type): Bool { + return self.getSupportedVaultTypes()[type] ?? false + } + + access(all) view fun isAvailableToWithdraw(amount: UFix64): Bool { + return amount <= self.balance + } + + access(FungibleToken.Withdraw) fun withdraw(amount: UFix64): @DummyToken.Vault { + self.balance = self.balance - amount + return <-create Vault(balance: amount) + } + + access(all) fun deposit(from: @{FungibleToken.Vault}) { + let vault <- from as! @DummyToken.Vault + let amount = vault.balance + vault.balance = 0.0 + destroy vault + + self.balance = self.balance + amount + } + + access(all) fun createEmptyVault(): @DummyToken.Vault { + return <-create Vault(balance: 0.0) + } + } + + /// Minter + /// + /// Resource object that token admin accounts can hold to mint new tokens. + /// + access(all) resource Minter { + /// Identifies when a Minter is destroyed, coupling with MinterCreated event to trace Minter UUIDs + access(all) event ResourceDestroyed(uuid: UInt64 = self.uuid) + + init() { + emit MinterCreated(uuid: self.uuid) + } + + /// mintTokens + /// + /// Function that mints new tokens, adds them to the total supply, + /// and returns them to the calling context. + /// + access(all) fun mintTokens(amount: UFix64): @DummyToken.Vault { + DummyToken.totalSupply = DummyToken.totalSupply + amount + let vault <-create Vault(balance: amount) + emit Minted(type: vault.getType().identifier, amount: amount, toUUID: vault.uuid, minterUUID: self.uuid) + return <-vault + } + } + + init(initialMint: UFix64) { + + self.totalSupply = 0.0 + + let address = self.account.address + self.VaultStoragePath = StoragePath(identifier: "dummyTokenVault_\(address)")! + self.VaultPublicPath = PublicPath(identifier: "dummyTokenVault_\(address)")! + self.ReceiverPublicPath = PublicPath(identifier: "dummyTokenReceiver_\(address)")! + self.AdminStoragePath = StoragePath(identifier: "dummyTokenAdmin_\(address)")! + + + // Create a public capability to the stored Vault that exposes + // the `deposit` method and getAcceptedTypes method through the `Receiver` interface + // and the `balance` method through the `Balance` interface + // + self.account.storage.save(<-create Vault(balance: self.totalSupply), to: self.VaultStoragePath) + let vaultCap = self.account.capabilities.storage.issue<&DummyToken.Vault>(self.VaultStoragePath) + self.account.capabilities.publish(vaultCap, at: self.VaultPublicPath) + let receiverCap = self.account.capabilities.storage.issue<&DummyToken.Vault>(self.VaultStoragePath) + self.account.capabilities.publish(receiverCap, at: self.ReceiverPublicPath) + + // Create a Minter & mint the initial supply of tokens to the contract account's Vault + let admin <- create Minter() + + self.account.capabilities.borrow<&Vault>(self.ReceiverPublicPath)!.deposit( + from: <- admin.mintTokens(amount: initialMint) + ) + + self.account.storage.save(<-admin, to: self.AdminStoragePath) + } +} diff --git a/cadence/tests/multi_token_reserve_borrowing_test.cdc b/cadence/tests/multi_token_reserve_borrowing_test.cdc new file mode 100644 index 00000000..0c3fdc27 --- /dev/null +++ b/cadence/tests/multi_token_reserve_borrowing_test.cdc @@ -0,0 +1,274 @@ +import Test +import BlockchainHelpers + +import "MOET" +import "FlowToken" +import "DummyToken" +import "FlowALPv0" +import "test_helpers.cdc" + +access(all) +fun setup() { + deployContracts() + // DummyToken is now configured in flow.json and deployed automatically +} + +/// Tests reserve-based borrowing with distinct token types +/// Scenario: +/// 1. User1: deposits FLOW collateral → borrows MOET +/// 2. User2: deposits DummyToken collateral → borrows FLOW (from User1's reserves) +/// 3. User2: repays FLOW debt → withdraws DummyToken +/// 4. User1: repays MOET debt → withdraws FLOW +access(all) +fun testMultiTokenReserveBorrowing() { + log("=== Starting Multi-Token Reserve Borrowing Test ===") + + // Setup oracle prices + let dummyTokenIdentifier = "A.0000000000000007.DummyToken.Vault" + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0) + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: MOET_TOKEN_IDENTIFIER, price: 1.0) + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: dummyTokenIdentifier, price: 10.0) + log("✓ Oracle prices set: FLOW=$1, MOET=$1, DUMMY=$10") + + // Create pool with MOET as default token (borrowable via minting) + createAndStorePool(signer: PROTOCOL_ACCOUNT, defaultTokenIdentifier: MOET_TOKEN_IDENTIFIER, beFailed: false) + log("✓ Pool created with MOET as default token") + + // Add FLOW as supported token (can be both collateral and debt) + addSupportedTokenZeroRateCurve( + signer: PROTOCOL_ACCOUNT, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + collateralFactor: 0.8, + borrowFactor: 0.77, + depositRate: 1_000_000.0, + depositCapacityCap: 1_000_000.0 + ) + log("✓ FLOW added as supported token (CF=0.8, BF=0.77)") + + // Add DummyToken as supported collateral + addSupportedTokenZeroRateCurve( + signer: PROTOCOL_ACCOUNT, + tokenTypeIdentifier: dummyTokenIdentifier, + collateralFactor: 0.8, + borrowFactor: 0.77, + depositRate: 1_000_000.0, + depositCapacityCap: 1_000_000.0 + ) + log("✓ DummyToken added as supported collateral (CF=0.8, BF=0.77)") + + // ===== USER 1: Deposit FLOW collateral, borrow MOET ===== + log("") + log("--- User 1: Deposit FLOW, Borrow MOET ---") + + let user1 = Test.createAccount() + setupMoetVault(user1, beFailed: false) + mintFlow(to: user1, amount: 2_000.0) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user1) + + let user1InitialFlow = getBalance(address: user1.address, vaultPublicPath: /public/flowTokenReceiver)! + let user1InitialMoet = getBalance(address: user1.address, vaultPublicPath: MOET.VaultPublicPath) ?? 0.0 + log("User1 initial - FLOW: ".concat(user1InitialFlow.toString()).concat(", MOET: ").concat(user1InitialMoet.toString())) + + // User1 deposits 1000 FLOW as collateral (no auto-borrow) + let createPos1Res = executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [1_000.0, FLOW_VAULT_STORAGE_PATH, false], + user1 + ) + Test.expect(createPos1Res, Test.beSucceeded()) + log("✓ User1 deposited 1000 FLOW as collateral") + + let pid1: UInt64 = 0 + + // User1 borrows MOET (via minting) + // Effective collateral = 1000 FLOW * $1 * 0.8 = $800 + // Can borrow up to $800 * 0.77 = $616 + let user1MoetBorrowAmount = 400.0 + borrowFromPosition( + signer: user1, + positionId: pid1, + tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + amount: user1MoetBorrowAmount, + beFailed: false + ) + log("✓ User1 borrowed ".concat(user1MoetBorrowAmount.toString()).concat(" MOET (via minting)")) + + let user1MoetBalance = getBalance(address: user1.address, vaultPublicPath: MOET.VaultPublicPath)! + Test.assert(user1MoetBalance >= user1MoetBorrowAmount - 0.01, + message: "User1 should have ~".concat(user1MoetBorrowAmount.toString()).concat(" MOET")) + log("✓ User1 now has ".concat(user1MoetBalance.toString()).concat(" MOET")) + + // Check User1 position health + var health1 = getPositionHealth(pid: pid1, beFailed: false) + log("User1 position health: ".concat(health1.toString())) + // Expected: 800 / 400 = 2.0 + Test.assert(health1 >= UFix128(1.99) && health1 <= UFix128(2.01), + message: "Expected User1 health ~2.0") + + // Now User1's FLOW collateral is in the pool and can be borrowed by others! + log("✓ User1's 1000 FLOW is now in the pool as reserves") + + // ===== USER 2: Deposit DummyToken collateral, borrow FLOW ===== + log("") + log("--- User 2: Deposit DummyToken, Borrow FLOW (from reserves) ---") + + let user2 = Test.createAccount() + setupMoetVault(user2, beFailed: false) + setupDummyTokenVault(user2) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user2) + + // Mint 100 DUMMY tokens to user2 (worth $1000 at $10 each) + mintDummyToken(to: user2, amount: 100.0) + + let user2InitialFlow = getBalance(address: user2.address, vaultPublicPath: /public/flowTokenReceiver)! + let user2InitialDummy = getBalance(address: user2.address, vaultPublicPath: DummyToken.VaultPublicPath)! + log("User2 initial - FLOW: ".concat(user2InitialFlow.toString()).concat(", DUMMY: ").concat(user2InitialDummy.toString())) + + // User2 deposits 100 DUMMY as collateral + let createPos2Res = executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [100.0, DummyToken.VaultStoragePath, false], + user2 + ) + Test.expect(createPos2Res, Test.beSucceeded()) + log("✓ User2 deposited 100 DUMMY tokens as collateral") + + let pid2: UInt64 = 1 + + // User2 borrows FLOW (via reserves - from User1's collateral!) + // Effective collateral = 100 DUMMY * $10 * 0.8 = $800 + // Can borrow up to $800 * 0.77 = $616 + let user2FlowBorrowAmount = 300.0 + borrowFromPosition( + signer: user2, + positionId: pid2, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + amount: user2FlowBorrowAmount, + beFailed: false + ) + log("✓ User2 borrowed ".concat(user2FlowBorrowAmount.toString()).concat(" FLOW (via reserves from User1's collateral!)")) + + let user2FlowBalance = getBalance(address: user2.address, vaultPublicPath: /public/flowTokenReceiver)! + let user2FlowReceived = user2FlowBalance - user2InitialFlow + Test.assert(user2FlowReceived >= user2FlowBorrowAmount - 0.01, + message: "User2 should have received ~".concat(user2FlowBorrowAmount.toString()).concat(" FLOW")) + log("✓ User2 now has ".concat(user2FlowReceived.toString()).concat(" FLOW borrowed from reserves")) + + // Check User2 position health + var health2 = getPositionHealth(pid: pid2, beFailed: false) + log("User2 position health: ".concat(health2.toString())) + // Expected: 800 / (300 / 0.77) = 800 / 389.61 = 2.053 + // Effective debt accounts for borrow factor: debt / BF + Test.assert(health2 >= UFix128(2.05) && health2 <= UFix128(2.06), + message: "Expected User2 health ~2.053 (debt scaled by BF=0.77)") + + // Verify both positions exist + log("") + log("✓ Both positions active:") + log(" - User1 (pid=0): 1000 FLOW collateral, 400 MOET debt") + log(" - User2 (pid=1): 100 DUMMY collateral, 300 FLOW debt") + + // ===== USER 2: Repay FLOW debt, withdraw DummyToken ===== + log("") + log("--- User 2: Repay FLOW, Withdraw DummyToken ---") + + // User2 repays FLOW debt + depositToPosition( + signer: user2, + positionID: pid2, + amount: user2FlowBorrowAmount, + vaultStoragePath: FLOW_VAULT_STORAGE_PATH, + pushToDrawDownSink: false + ) + log("✓ User2 repaid ".concat(user2FlowBorrowAmount.toString()).concat(" FLOW debt")) + + // Check User2 health after repayment (should be very high - no debt) + health2 = getPositionHealth(pid: pid2, beFailed: false) + log("User2 health after repayment: ".concat(health2.toString())) + Test.assert(health2 > UFix128(100.0), message: "Expected very high health after repayment") + + // User2 withdraws DummyToken collateral + // Note: FLOW debt is repaid (nets to 0), so can withdraw collateral safely + withdrawFromPosition( + signer: user2, + positionId: pid2, + tokenTypeIdentifier: dummyTokenIdentifier, + amount: user2InitialDummy, + pullFromTopUpSource: false + ) + log("✓ User2 withdrew ".concat(user2InitialDummy.toString()).concat(" DUMMY collateral")) + + // Verify User2 got their DUMMY tokens back + let user2FinalDummy = getBalance(address: user2.address, vaultPublicPath: DummyToken.VaultPublicPath)! + Test.assert(user2FinalDummy >= user2InitialDummy - 0.01, + message: "User2 should get back ~".concat(user2InitialDummy.toString()).concat(" DUMMY")) + log("✓ User2 received back ".concat(user2FinalDummy.toString()).concat(" DUMMY tokens")) + + // ===== USER 1: Repay MOET debt, withdraw FLOW ===== + log("") + log("--- User 1: Repay MOET, Withdraw FLOW ---") + + // User1 repays MOET debt + depositToPosition( + signer: user1, + positionID: pid1, + amount: user1MoetBorrowAmount, + vaultStoragePath: MOET.VaultStoragePath, + pushToDrawDownSink: false + ) + log("✓ User1 repaid ".concat(user1MoetBorrowAmount.toString()).concat(" MOET debt")) + + // Check User1 health after repayment + health1 = getPositionHealth(pid: pid1, beFailed: false) + log("User1 health after repayment: ".concat(health1.toString())) + Test.assert(health1 > UFix128(100.0), message: "Expected very high health after repayment") + + // User1 withdraws FLOW collateral + withdrawFromPosition( + signer: user1, + positionId: pid1, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + amount: 1_000.0, + pullFromTopUpSource: false + ) + log("✓ User1 withdrew 1000 FLOW collateral") + + // Verify User1 got their FLOW back + let user1FinalFlow = getBalance(address: user1.address, vaultPublicPath: /public/flowTokenReceiver)! + // User1 should have close to their initial balance back + Test.assert(user1FinalFlow >= user1InitialFlow - 1.0, + message: "User1 should get back approximately their initial FLOW") + log("✓ User1 received back ".concat(user1FinalFlow.toString()).concat(" FLOW")) + + log("") + log("=== Multi-Token Reserve Borrowing Test Complete ===") + log("") + log("Summary:") + log(" ✓ User1 deposited FLOW, borrowed MOET (via minting)") + log(" ✓ User2 deposited DummyToken, borrowed FLOW (via reserves from User1)") + log(" ✓ User2 repaid FLOW, withdrew DummyToken") + log(" ✓ User1 repaid MOET, withdrew FLOW") + log(" ✓ Reserve-based borrowing works across distinct token types!") +} + +// Helper function to setup DummyToken vault +access(all) +fun setupDummyTokenVault(_ account: Test.TestAccount) { + let result = executeTransaction( + "./transactions/dummy_token/setup_vault.cdc", + [], + account + ) + Test.expect(result, Test.beSucceeded()) +} + +// Helper function to mint DummyToken +access(all) +fun mintDummyToken(to: Test.TestAccount, amount: UFix64) { + let result = executeTransaction( + "./transactions/dummy_token/mint.cdc", + [amount, to.address], + PROTOCOL_ACCOUNT + ) + Test.expect(result, Test.beSucceeded()) +} diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index 9548c24e..e2d1d70b 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -97,6 +97,14 @@ fun deployContracts() { ) Test.expect(err, Test.beNil()) + // Deploy DummyToken for multi-token testing + err = Test.deployContract( + name: "DummyToken", + path: "./contracts/DummyToken.cdc", + arguments: [initialSupply] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( name: "FlowALPv0", path: "../contracts/FlowALPv0.cdc", diff --git a/cadence/tests/transactions/dummy_token/mint.cdc b/cadence/tests/transactions/dummy_token/mint.cdc new file mode 100644 index 00000000..845dc664 --- /dev/null +++ b/cadence/tests/transactions/dummy_token/mint.cdc @@ -0,0 +1,17 @@ +import "DummyToken" +import "FungibleToken" + +transaction(amount: UFix64, recipient: Address) { + prepare(signer: auth(Storage) &Account) { + let minter = signer.storage.borrow<&DummyToken.Minter>(from: DummyToken.AdminStoragePath) + ?? panic("Could not borrow minter") + + let tokens <- minter.mintTokens(amount: amount) + + let receiverRef = getAccount(recipient) + .capabilities.borrow<&{FungibleToken.Receiver}>(DummyToken.ReceiverPublicPath) + ?? panic("Could not borrow receiver") + + receiverRef.deposit(from: <-tokens) + } +} diff --git a/cadence/tests/transactions/dummy_token/setup_vault.cdc b/cadence/tests/transactions/dummy_token/setup_vault.cdc new file mode 100644 index 00000000..982f32a5 --- /dev/null +++ b/cadence/tests/transactions/dummy_token/setup_vault.cdc @@ -0,0 +1,14 @@ +import "DummyToken" +import "FungibleToken" + +transaction { + prepare(signer: auth(Storage, Capabilities) &Account) { + if signer.storage.borrow<&DummyToken.Vault>(from: DummyToken.VaultStoragePath) == nil { + signer.storage.save(<-DummyToken.createEmptyVault(vaultType: Type<@DummyToken.Vault>()), to: DummyToken.VaultStoragePath) + + let cap = signer.capabilities.storage.issue<&DummyToken.Vault>(DummyToken.VaultStoragePath) + signer.capabilities.publish(cap, at: DummyToken.VaultPublicPath) + signer.capabilities.publish(cap, at: DummyToken.ReceiverPublicPath) + } + } +} diff --git a/flow.json b/flow.json index 2924d43c..09fb87eb 100644 --- a/flow.json +++ b/flow.json @@ -46,6 +46,12 @@ "testing": "0000000000000007" } }, + "DummyToken": { + "source": "./cadence/tests/contracts/DummyToken.cdc", + "aliases": { + "testing": "0000000000000007" + } + }, "FlowALPMath": { "source": "./cadence/lib/FlowALPMath.cdc", "aliases": { From cfbcd1a7e7d6cb30c2451ce0c69ab61d2417300a Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Fri, 27 Feb 2026 12:30:18 -0500 Subject: [PATCH 02/26] Apply suggestion from @nialexsan --- cadence/contracts/FlowALPv0.cdc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index abadc6f1..c6add83e 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -1339,7 +1339,7 @@ access(all) contract FlowALPv0 { } // If this position doesn't currently have an entry for this token, create one. - if position.balances[type] == nil { + if position.getBalance(type) == nil { // MOET cannot be used as collateral (only as debt) if type == Type<@MOET.Vault>() { panic("MOET cannot be deposited as collateral. MOET can only be borrowed (debt), not used as collateral.") From 7b1a1440639445c0190609e5f33c23df20e441a1 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Fri, 27 Feb 2026 12:50:28 -0500 Subject: [PATCH 03/26] fix refs --- cadence/contracts/FlowALPv0.cdc | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index c6add83e..98e83dd6 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -585,7 +585,8 @@ access(all) contract FlowALPv0 { // Withdraw seized collateral from position and send to liquidator let seizeState = self._borrowUpdatedTokenState(type: seizeType) - if position.getBalance(seizeType) == nil { + let positionBalance = position.getBalance(seizeType) + if positionBalance == nil { // MOET cannot be seized as collateral (it should never exist as collateral) if seizeType == Type<@MOET.Vault>() { panic("Cannot seize MOET as collateral. MOET should not exist as collateral in any position.") @@ -597,7 +598,7 @@ access(all) contract FlowALPv0 { position.setBalance(seizeType, FlowALPModels.InternalBalance(direction: FlowALPModels.BalanceDirection.Credit, scaledBalance: 0.0)) } // Additional safety check: MOET should never be seized as collateral - if seizeType == Type<@MOET.Vault>() && position.balances[seizeType]!.direction == BalanceDirection.Credit { + if seizeType == Type<@MOET.Vault>() && positionBalance!.direction == FlowALPModels.BalanceDirection.Credit { panic("Cannot seize MOET as collateral. MOET should not exist as collateral in any position.") } @@ -1338,8 +1339,9 @@ access(all) contract FlowALPv0 { position.depositToQueue(type, vault: <-queuedForUserLimit) } + let positionBalance = position.getBalance(type) // If this position doesn't currently have an entry for this token, create one. - if position.getBalance(type) == nil { + if positionBalance == nil { // MOET cannot be used as collateral (only as debt) if type == Type<@MOET.Vault>() { panic("MOET cannot be deposited as collateral. MOET can only be borrowed (debt), not used as collateral.") @@ -1368,7 +1370,7 @@ access(all) contract FlowALPv0 { // MOET cannot be deposited as collateral (Credit direction) // It can only be deposited to repay debt (Debit direction) - if type == Type<@MOET.Vault>() && position.balances[type]!.direction == BalanceDirection.Credit { + if type == Type<@MOET.Vault>() && positionBalance!.direction == FlowALPModels.BalanceDirection.Credit { panic("MOET cannot be deposited as collateral. MOET can only be borrowed (debt), not used as collateral.") } @@ -1556,8 +1558,10 @@ access(all) contract FlowALPv0 { panic("Cannot withdraw \(amount) of \(type.identifier) from position ID \(pid) - Insufficient funds for withdrawal") } + let positionBalance = position.getBalance(type) + // If this position doesn't currently have an entry for this token, create one. - if position.getBalance(type) == nil { + if positionBalance == nil { // When withdrawing a token that doesn't exist in position yet, // we'll be creating a debit (debt). Validate single debt type. position.validateDebtType(type) @@ -1569,7 +1573,7 @@ access(all) contract FlowALPv0 { } // Reflect the withdrawal in the position's balance - let wasCredit = position.balances[type]!.direction == BalanceDirection.Credit + let wasCredit = positionBalance!.direction == FlowALPModels.BalanceDirection.Credit let uintAmount = UFix128(amount) position.borrowBalance(type)!.recordWithdrawal( amount: uintAmount, @@ -1577,7 +1581,7 @@ access(all) contract FlowALPv0 { ) // If we flipped from Credit to Debit, validate debt type constraint - if wasCredit && position.balances[type]!.direction == BalanceDirection.Debit { + if wasCredit && positionBalance!.direction == FlowALPModels.BalanceDirection.Debit { position.validateDebtType(type) } // Attempt to pull additional collateral from the top-up source (if configured) @@ -1611,7 +1615,7 @@ access(all) contract FlowALPv0 { withdrawn <-! FlowALPv0._borrowMOETMinter().mintTokens(amount: amount) } else { // For other tokens, withdraw from reserves - assert(self.reserves[type] != nil, message: "Cannot withdraw \(type.identifier) - no reserves available") + assert(self.state.borrowReserve(type) != nil, message: "Cannot withdraw \(type.identifier) - no reserves available") let reserveVault = self.state.borrowReserve(type)! assert(reserveVault.balance >= amount, message: "Insufficient reserves for \(type.identifier): need \(amount), have \(reserveVault.balance)") withdrawn <-! reserveVault.withdraw(amount: amount) @@ -2028,8 +2032,8 @@ access(all) contract FlowALPv0 { sinkVault <-! FlowALPv0._borrowMOETMinter().mintTokens(amount: sinkAmount) } else { // For other tokens, withdraw from reserves - assert(self.reserves[sinkType] != nil, message: "Cannot withdraw \(sinkAmount) of \(sinkType.identifier) - token not in reserves") - let reserveVault = (&self.reserves[sinkType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! + assert(self.state.borrowReserve(sinkType) != nil, message: "Cannot withdraw \(sinkAmount) of \(sinkType.identifier) - token not in reserves") + let reserveVault = self.state.borrowReserve(sinkType)! assert(reserveVault.balance >= sinkAmount, message: "Insufficient reserves for \(sinkType.identifier): available \(reserveVault.balance), needed \(sinkAmount)") sinkVault <-! reserveVault.withdraw(amount: sinkAmount) } From c492f20f44621728eefaa7967a15282654f7813c Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Fri, 27 Feb 2026 12:54:42 -0500 Subject: [PATCH 04/26] fix balance --- cadence/contracts/FlowALPv0.cdc | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 98e83dd6..c0dcc1e5 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -1558,7 +1558,7 @@ access(all) contract FlowALPv0 { panic("Cannot withdraw \(amount) of \(type.identifier) from position ID \(pid) - Insufficient funds for withdrawal") } - let positionBalance = position.getBalance(type) + var positionBalance = position.getBalance(type) // If this position doesn't currently have an entry for this token, create one. if positionBalance == nil { @@ -1570,6 +1570,9 @@ access(all) contract FlowALPv0 { direction: FlowALPModels.BalanceDirection.Credit, scaledBalance: 0.0 )) + + // Re-fetch the balance after creating it + positionBalance = position.getBalance(type) } // Reflect the withdrawal in the position's balance @@ -1581,6 +1584,8 @@ access(all) contract FlowALPv0 { ) // If we flipped from Credit to Debit, validate debt type constraint + // Re-fetch balance to check if direction changed + positionBalance = position.getBalance(type) if wasCredit && positionBalance!.direction == FlowALPModels.BalanceDirection.Debit { position.validateDebtType(type) } From a7f0828735c652eef7fc4abda21ec9b2c353422a Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Fri, 27 Feb 2026 18:23:13 -0500 Subject: [PATCH 05/26] revert MOET collateral restriction --- cadence/contracts/FlowALPv0.cdc | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index c0dcc1e5..9fc88cec 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -587,20 +587,11 @@ access(all) contract FlowALPv0 { let seizeState = self._borrowUpdatedTokenState(type: seizeType) let positionBalance = position.getBalance(seizeType) if positionBalance == nil { - // MOET cannot be seized as collateral (it should never exist as collateral) - if seizeType == Type<@MOET.Vault>() { - panic("Cannot seize MOET as collateral. MOET should not exist as collateral in any position.") - } - // Liquidation is seizing collateral - validate single collateral type position.validateCollateralType(seizeType) position.setBalance(seizeType, FlowALPModels.InternalBalance(direction: FlowALPModels.BalanceDirection.Credit, scaledBalance: 0.0)) } - // Additional safety check: MOET should never be seized as collateral - if seizeType == Type<@MOET.Vault>() && positionBalance!.direction == FlowALPModels.BalanceDirection.Credit { - panic("Cannot seize MOET as collateral. MOET should not exist as collateral in any position.") - } position.borrowBalance(seizeType)!.recordWithdrawal(amount: UFix128(seizeAmount), tokenState: seizeState) let seizeReserveRef = self.state.borrowReserve(seizeType)! @@ -1342,11 +1333,6 @@ access(all) contract FlowALPv0 { let positionBalance = position.getBalance(type) // If this position doesn't currently have an entry for this token, create one. if positionBalance == nil { - // MOET cannot be used as collateral (only as debt) - if type == Type<@MOET.Vault>() { - panic("MOET cannot be deposited as collateral. MOET can only be borrowed (debt), not used as collateral.") - } - // Validate single collateral type constraint position.validateCollateralType(type) @@ -1368,12 +1354,6 @@ access(all) contract FlowALPv0 { // as the queued deposits will be processed later (by this function being called again), and therefore // will be recorded at that time. - // MOET cannot be deposited as collateral (Credit direction) - // It can only be deposited to repay debt (Debit direction) - if type == Type<@MOET.Vault>() && positionBalance!.direction == FlowALPModels.BalanceDirection.Credit { - panic("MOET cannot be deposited as collateral. MOET can only be borrowed (debt), not used as collateral.") - } - let acceptedAmount = from.balance position.borrowBalance(type)!.recordDeposit( amount: UFix128(acceptedAmount), From 4c9e4520a2fbcdf76709bfcbd696f1e0bd34abc2 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Fri, 27 Feb 2026 19:21:12 -0500 Subject: [PATCH 06/26] fix test --- .../multi_token_reserve_borrowing_test.cdc | 80 ++++++++----------- 1 file changed, 33 insertions(+), 47 deletions(-) diff --git a/cadence/tests/multi_token_reserve_borrowing_test.cdc b/cadence/tests/multi_token_reserve_borrowing_test.cdc index 0c3fdc27..b4c093af 100644 --- a/cadence/tests/multi_token_reserve_borrowing_test.cdc +++ b/cadence/tests/multi_token_reserve_borrowing_test.cdc @@ -16,19 +16,17 @@ fun setup() { /// Tests reserve-based borrowing with distinct token types /// Scenario: /// 1. User1: deposits FLOW collateral → borrows MOET -/// 2. User2: deposits DummyToken collateral → borrows FLOW (from User1's reserves) -/// 3. User2: repays FLOW debt → withdraws DummyToken +/// 2. User2: deposits MOET collateral → borrows FLOW (from User1's reserves) +/// 3. User2: repays FLOW debt → withdraws MOET /// 4. User1: repays MOET debt → withdraws FLOW access(all) fun testMultiTokenReserveBorrowing() { log("=== Starting Multi-Token Reserve Borrowing Test ===") // Setup oracle prices - let dummyTokenIdentifier = "A.0000000000000007.DummyToken.Vault" setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0) setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: MOET_TOKEN_IDENTIFIER, price: 1.0) - setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: dummyTokenIdentifier, price: 10.0) - log("✓ Oracle prices set: FLOW=$1, MOET=$1, DUMMY=$10") + log("✓ Oracle prices set: FLOW=$1, MOET=$1") // Create pool with MOET as default token (borrowable via minting) createAndStorePool(signer: PROTOCOL_ACCOUNT, defaultTokenIdentifier: MOET_TOKEN_IDENTIFIER, beFailed: false) @@ -45,16 +43,8 @@ fun testMultiTokenReserveBorrowing() { ) log("✓ FLOW added as supported token (CF=0.8, BF=0.77)") - // Add DummyToken as supported collateral - addSupportedTokenZeroRateCurve( - signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: dummyTokenIdentifier, - collateralFactor: 0.8, - borrowFactor: 0.77, - depositRate: 1_000_000.0, - depositCapacityCap: 1_000_000.0 - ) - log("✓ DummyToken added as supported collateral (CF=0.8, BF=0.77)") + // Note: MOET is already added as the default/mintable token when pool was created + // It can be used as both collateral and debt // ===== USER 1: Deposit FLOW collateral, borrow MOET ===== log("") @@ -108,35 +98,34 @@ fun testMultiTokenReserveBorrowing() { // Now User1's FLOW collateral is in the pool and can be borrowed by others! log("✓ User1's 1000 FLOW is now in the pool as reserves") - // ===== USER 2: Deposit DummyToken collateral, borrow FLOW ===== + // ===== USER 2: Deposit MOET collateral, borrow FLOW ===== log("") - log("--- User 2: Deposit DummyToken, Borrow FLOW (from reserves) ---") + log("--- User 2: Deposit MOET, Borrow FLOW (from reserves) ---") let user2 = Test.createAccount() setupMoetVault(user2, beFailed: false) - setupDummyTokenVault(user2) grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user2) - // Mint 100 DUMMY tokens to user2 (worth $1000 at $10 each) - mintDummyToken(to: user2, amount: 100.0) + // Mint 1000 MOET to user2 (worth $1000 at $1 each) + mintMoet(signer: PROTOCOL_ACCOUNT, to: user2.address, amount: 1000.0, beFailed: false) let user2InitialFlow = getBalance(address: user2.address, vaultPublicPath: /public/flowTokenReceiver)! - let user2InitialDummy = getBalance(address: user2.address, vaultPublicPath: DummyToken.VaultPublicPath)! - log("User2 initial - FLOW: ".concat(user2InitialFlow.toString()).concat(", DUMMY: ").concat(user2InitialDummy.toString())) + let user2InitialMoet = getBalance(address: user2.address, vaultPublicPath: MOET.VaultPublicPath)! + log("User2 initial - FLOW: ".concat(user2InitialFlow.toString()).concat(", MOET: ").concat(user2InitialMoet.toString())) - // User2 deposits 100 DUMMY as collateral + // User2 deposits 1000 MOET as collateral let createPos2Res = executeTransaction( "../transactions/flow-alp/position/create_position.cdc", - [100.0, DummyToken.VaultStoragePath, false], + [1000.0, MOET.VaultStoragePath, false], user2 ) Test.expect(createPos2Res, Test.beSucceeded()) - log("✓ User2 deposited 100 DUMMY tokens as collateral") + log("✓ User2 deposited 1000 MOET as collateral") let pid2: UInt64 = 1 // User2 borrows FLOW (via reserves - from User1's collateral!) - // Effective collateral = 100 DUMMY * $10 * 0.8 = $800 + // Effective collateral = 1000 MOET * $1 * 0.8 = $800 // Can borrow up to $800 * 0.77 = $616 let user2FlowBorrowAmount = 300.0 borrowFromPosition( @@ -157,22 +146,20 @@ fun testMultiTokenReserveBorrowing() { // Check User2 position health var health2 = getPositionHealth(pid: pid2, beFailed: false) log("User2 position health: ".concat(health2.toString())) - // Expected: 800 / (300 / 0.77) = 800 / 389.61 = 2.053 - // Effective debt accounts for borrow factor: debt / BF - Test.assert(health2 >= UFix128(2.05) && health2 <= UFix128(2.06), - message: "Expected User2 health ~2.053 (debt scaled by BF=0.77)") + // Health = 1000 MOET CF / (300 FLOW / BF) ≈ 2.567 + Test.assert(health2 >= UFix128(2.5) && health2 <= UFix128(2.6), + message: "Expected User2 health ~2.567") - // Verify both positions exist log("") log("✓ Both positions active:") log(" - User1 (pid=0): 1000 FLOW collateral, 400 MOET debt") - log(" - User2 (pid=1): 100 DUMMY collateral, 300 FLOW debt") + log(" - User2 (pid=1): 1000 MOET collateral, 300 FLOW debt") - // ===== USER 2: Repay FLOW debt, withdraw DummyToken ===== + // ===== USER 2: Repay FLOW debt, withdraw MOET ===== log("") - log("--- User 2: Repay FLOW, Withdraw DummyToken ---") + log("--- User 2: Repay FLOW, Withdraw MOET ---") - // User2 repays FLOW debt + // User2 repays FLOW debt (borrowed from reserves) depositToPosition( signer: user2, positionID: pid2, @@ -187,22 +174,21 @@ fun testMultiTokenReserveBorrowing() { log("User2 health after repayment: ".concat(health2.toString())) Test.assert(health2 > UFix128(100.0), message: "Expected very high health after repayment") - // User2 withdraws DummyToken collateral - // Note: FLOW debt is repaid (nets to 0), so can withdraw collateral safely + // User2 withdraws MOET collateral withdrawFromPosition( signer: user2, positionId: pid2, - tokenTypeIdentifier: dummyTokenIdentifier, - amount: user2InitialDummy, + tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + amount: user2InitialMoet, pullFromTopUpSource: false ) - log("✓ User2 withdrew ".concat(user2InitialDummy.toString()).concat(" DUMMY collateral")) + log("✓ User2 withdrew ".concat(user2InitialMoet.toString()).concat(" MOET collateral")) - // Verify User2 got their DUMMY tokens back - let user2FinalDummy = getBalance(address: user2.address, vaultPublicPath: DummyToken.VaultPublicPath)! - Test.assert(user2FinalDummy >= user2InitialDummy - 0.01, - message: "User2 should get back ~".concat(user2InitialDummy.toString()).concat(" DUMMY")) - log("✓ User2 received back ".concat(user2FinalDummy.toString()).concat(" DUMMY tokens")) + // Verify User2 got their MOET back + let user2FinalMoet = getBalance(address: user2.address, vaultPublicPath: MOET.VaultPublicPath)! + Test.assert(user2FinalMoet >= user2InitialMoet - 0.01, + message: "User2 should get back ~".concat(user2InitialMoet.toString()).concat(" MOET")) + log("✓ User2 received back ".concat(user2FinalMoet.toString()).concat(" MOET tokens")) // ===== USER 1: Repay MOET debt, withdraw FLOW ===== log("") @@ -245,8 +231,8 @@ fun testMultiTokenReserveBorrowing() { log("") log("Summary:") log(" ✓ User1 deposited FLOW, borrowed MOET (via minting)") - log(" ✓ User2 deposited DummyToken, borrowed FLOW (via reserves from User1)") - log(" ✓ User2 repaid FLOW, withdrew DummyToken") + log(" ✓ User2 deposited MOET, borrowed FLOW (via reserves from User1)") + log(" ✓ User2 repaid FLOW, withdrew MOET") log(" ✓ User1 repaid MOET, withdrew FLOW") log(" ✓ Reserve-based borrowing works across distinct token types!") } From a916839cb30ec00d738b249f327e6f349b028994 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Fri, 27 Feb 2026 20:14:45 -0500 Subject: [PATCH 07/26] fix testing transactions --- .../position-manager/borrow_from_position.cdc | 16 +++++++++------- .../position-manager/withdraw_from_position.cdc | 16 +++++++++------- flow.json | 10 +++++----- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/cadence/tests/transactions/position-manager/borrow_from_position.cdc b/cadence/tests/transactions/position-manager/borrow_from_position.cdc index 7549438f..9f39f631 100644 --- a/cadence/tests/transactions/position-manager/borrow_from_position.cdc +++ b/cadence/tests/transactions/position-manager/borrow_from_position.cdc @@ -1,5 +1,6 @@ import "FungibleToken" import "FlowToken" +import "MOET" import "FlowALPv0" import "FlowALPModels" @@ -38,15 +39,16 @@ transaction( } // Get receiver for the specific token type - // For FlowToken, use the standard path + var receiverRef: &{FungibleToken.Receiver}? = nil if tokenTypeIdentifier == "A.0000000000000003.FlowToken.Vault" { - self.receiverVault = signer.storage.borrow<&{FungibleToken.Receiver}>(from: /storage/flowTokenVault) - ?? panic("Could not borrow FlowToken vault receiver") - } else { - // For other tokens, try to find a matching vault - // This is a simplified approach for testing - panic("Unsupported token type for borrow: \(tokenTypeIdentifier)") + // For FlowToken, use the standard path + receiverRef = signer.storage.borrow<&{FungibleToken.Receiver}>(from: /storage/flowTokenVault) + } else if tokenTypeIdentifier == "A.0000000000000007.MOET.Vault" { + // For MOET, use the MOET vault path + receiverRef = signer.storage.borrow<&{FungibleToken.Receiver}>(from: MOET.VaultStoragePath) } + + self.receiverVault = receiverRef ?? panic("Could not borrow vault receiver for token type: \(tokenTypeIdentifier). Ensure vault is set up.") } execute { diff --git a/cadence/tests/transactions/position-manager/withdraw_from_position.cdc b/cadence/tests/transactions/position-manager/withdraw_from_position.cdc index 336df4c5..fc9f54e3 100644 --- a/cadence/tests/transactions/position-manager/withdraw_from_position.cdc +++ b/cadence/tests/transactions/position-manager/withdraw_from_position.cdc @@ -1,5 +1,6 @@ import "FungibleToken" import "FlowToken" +import "MOET" import "FlowALPv0" import "FlowALPModels" @@ -39,15 +40,16 @@ transaction( } // Get receiver for the specific token type - // For FlowToken, use the standard path + var receiverRef: &{FungibleToken.Receiver}? = nil if tokenTypeIdentifier == "A.0000000000000003.FlowToken.Vault" { - self.receiverVault = signer.storage.borrow<&{FungibleToken.Receiver}>(from: /storage/flowTokenVault) - ?? panic("Could not borrow FlowToken vault receiver") - } else { - // For other tokens, try to find a matching vault - // This is a simplified approach for testing - panic("Unsupported token type for withdrawal: \(tokenTypeIdentifier)") + // For FlowToken, use the standard path + receiverRef = signer.storage.borrow<&{FungibleToken.Receiver}>(from: /storage/flowTokenVault) + } else if tokenTypeIdentifier == "A.0000000000000007.MOET.Vault" { + // For MOET, use the MOET vault path + receiverRef = signer.storage.borrow<&{FungibleToken.Receiver}>(from: MOET.VaultStoragePath) } + + self.receiverVault = receiverRef ?? panic("Could not borrow vault receiver for token type: \(tokenTypeIdentifier). Ensure vault is set up.") } execute { diff --git a/flow.json b/flow.json index f2472210..72e51c9d 100644 --- a/flow.json +++ b/flow.json @@ -64,14 +64,14 @@ "testing": "0000000000000007" } }, - "FlowALPModels": { - "source": "./cadence/contracts/FlowALPModels.cdc", + "FlowALPMath": { + "source": "./cadence/lib/FlowALPMath.cdc", "aliases": { "testing": "0000000000000007" } }, - "FlowALPMath": { - "source": "./cadence/lib/FlowALPMath.cdc", + "FlowALPModels": { + "source": "./cadence/contracts/FlowALPModels.cdc", "aliases": { "testing": "0000000000000007" } @@ -443,4 +443,4 @@ ] } } -} +} \ No newline at end of file From 2b1ed97a20daff97b4b350daa26d5c7a13b0cafd Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Fri, 27 Feb 2026 22:39:31 -0500 Subject: [PATCH 08/26] add tests --- .../debt_type_constraint_simple_test.cdc | 113 ++++ .../debt_type_constraint_three_token_test.cdc | 321 ++++++++++++ .../tests/single_token_constraint_test.cdc | 481 ++++++++++++++++++ .../withdraw_from_position.cdc | 4 + 4 files changed, 919 insertions(+) create mode 100644 cadence/tests/debt_type_constraint_simple_test.cdc create mode 100644 cadence/tests/debt_type_constraint_three_token_test.cdc create mode 100644 cadence/tests/single_token_constraint_test.cdc diff --git a/cadence/tests/debt_type_constraint_simple_test.cdc b/cadence/tests/debt_type_constraint_simple_test.cdc new file mode 100644 index 00000000..ff0175c5 --- /dev/null +++ b/cadence/tests/debt_type_constraint_simple_test.cdc @@ -0,0 +1,113 @@ +import Test +import BlockchainHelpers + +import "MOET" +import "FlowToken" +import "FlowALPv0" +import "test_helpers.cdc" + +/// Simple test to verify debt type constraint is enforced + +access(all) +fun setup() { + deployContracts() + + // Setup oracle prices + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0) + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: MOET_TOKEN_IDENTIFIER, price: 1.0) + + // Create pool with MOET as default token + createAndStorePool(signer: PROTOCOL_ACCOUNT, defaultTokenIdentifier: MOET_TOKEN_IDENTIFIER, beFailed: false) + + // Add FLOW as supported token + addSupportedTokenZeroRateCurve( + signer: PROTOCOL_ACCOUNT, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + collateralFactor: 0.8, + borrowFactor: 0.77, + depositRate: 1_000_000.0, + depositCapacityCap: 1_000_000.0 + ) +} + +/// Test that a position with FLOW debt cannot borrow MOET (second debt type) +access(all) +fun testDebtTypeConstraint() { + log("=== Test: Debt Type Constraint ===") + + // Create user1 to provide FLOW reserves + let user1 = Test.createAccount() + setupMoetVault(user1, beFailed: false) + transferFlowTokens(to: user1, amount: 10_000.0) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user1) + + // User1 deposits FLOW (creates reserves) + let createPos1 = executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [5_000.0, FLOW_VAULT_STORAGE_PATH, false], + user1 + ) + Test.expect(createPos1, Test.beSucceeded()) + log("✓ User1 deposited 5000 FLOW to create reserves") + + // Create user2 with MOET collateral + let user2 = Test.createAccount() + setupMoetVault(user2, beFailed: false) + mintMoet(signer: PROTOCOL_ACCOUNT, to: user2.address, amount: 10_000.0, beFailed: false) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user2) + + // User2 creates position with MOET collateral + let createPos2 = executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [5_000.0, MOET.VaultStoragePath, false], + user2 + ) + Test.expect(createPos2, Test.beSucceeded()) + log("✓ User2 created position with 5000 MOET collateral") + + let pid: UInt64 = 1 // User2's position + + // User2 borrows FLOW (first debt type, from reserves) + borrowFromPosition( + signer: user2, + positionId: pid, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + amount: 1_000.0, + beFailed: false + ) + log("✓ User2 borrowed 1000 FLOW (first debt type)") + + // Verify User2 has FLOW debt + let details = getPositionDetails(pid: pid, beFailed: false) + let flowDebt = getDebitBalanceForType(details: details, vaultType: CompositeType(FLOW_TOKEN_IDENTIFIER)!) + Test.assert(flowDebt >= 1_000.0 - 0.01, message: "User2 should have ~1000 FLOW debt") + log("✓ User2 has FLOW debt: ".concat(flowDebt.toString())) + + // User2 tries to borrow MOET (second debt type, via minting) + // Need to withdraw MORE than collateral amount to flip MOET from Credit to Debit + // This should FAIL with debt type constraint error + let borrowMoet = executeTransaction( + "./transactions/position-manager/borrow_from_position.cdc", + [pid, MOET_TOKEN_IDENTIFIER, 6_000.0], // More than 5000 collateral + user2 + ) + + // Check if it failed + if borrowMoet.status == Test.ResultStatus.succeeded { + log("❌ ERROR: Borrowing MOET should have failed but succeeded!") + log("❌ Debt type constraint is NOT enforced") + Test.assert(false, message: "Debt type constraint should prevent borrowing MOET after FLOW") + } else { + log("✓ Borrowing MOET correctly failed") + + // Check error message + let errorMsg = borrowMoet.error?.message ?? "" + if errorMsg.contains("debt type") || errorMsg.contains("Only one debt type") { + log("✓ Error message mentions debt type constraint: ".concat(errorMsg)) + } else { + log("⚠ Warning: Error message doesn't mention debt type: ".concat(errorMsg)) + } + } + + log("=== Test Complete ===\n") +} diff --git a/cadence/tests/debt_type_constraint_three_token_test.cdc b/cadence/tests/debt_type_constraint_three_token_test.cdc new file mode 100644 index 00000000..4058e000 --- /dev/null +++ b/cadence/tests/debt_type_constraint_three_token_test.cdc @@ -0,0 +1,321 @@ +import Test +import BlockchainHelpers + +import "MOET" +import "FlowToken" +import "DummyToken" +import "FlowALPv0" +import "test_helpers.cdc" + +/// Three-token test to properly verify debt type constraint enforcement +/// Uses DummyToken, FLOW, and MOET to test that a position cannot have multiple debt types + +access(all) let DUMMY_TOKEN_IDENTIFIER = "A.0000000000000007.DummyToken.Vault" + +access(all) var snapshot: UInt64 = 0 + +access(all) +fun setup() { + deployContracts() + + // Setup oracle prices for all three tokens + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0) + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: MOET_TOKEN_IDENTIFIER, price: 1.0) + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: DUMMY_TOKEN_IDENTIFIER, price: 1.0) + + // Create pool with MOET as default token + createAndStorePool(signer: PROTOCOL_ACCOUNT, defaultTokenIdentifier: MOET_TOKEN_IDENTIFIER, beFailed: false) + + // Add FLOW as supported token + addSupportedTokenZeroRateCurve( + signer: PROTOCOL_ACCOUNT, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + collateralFactor: 0.8, + borrowFactor: 0.77, + depositRate: 1_000_000.0, + depositCapacityCap: 1_000_000.0 + ) + + // Add DummyToken as supported token + addSupportedTokenZeroRateCurve( + signer: PROTOCOL_ACCOUNT, + tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER, + collateralFactor: 0.8, + borrowFactor: 0.77, + depositRate: 1_000_000.0, + depositCapacityCap: 1_000_000.0 + ) + + snapshot = getCurrentBlockHeight() +} + +/// Test that a position with DummyToken collateral and FLOW debt cannot borrow MOET (second debt type) +access(all) +fun testCannotBorrowSecondDebtType() { + log("=== Test: Cannot Borrow Second Debt Type (3 Tokens) ===\n") + + // ===== Setup: Create reserves for FLOW and MOET ===== + + // User to provide FLOW reserves + let flowProvider = Test.createAccount() + setupMoetVault(flowProvider, beFailed: false) + transferFlowTokens(to: flowProvider, amount: 10_000.0) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, flowProvider) + + let createFlowPos = executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [5_000.0, FLOW_VAULT_STORAGE_PATH, false], + flowProvider + ) + Test.expect(createFlowPos, Test.beSucceeded()) + log("✓ FlowProvider deposited 5000 FLOW (creates FLOW reserves)") + + // ===== Main Test User ===== + + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + setupDummyTokenVault(user) + mintDummyToken(to: user, amount: 10_000.0) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) + + log("✓ User created with DummyToken") + + // User creates position with DummyToken collateral + let createPosRes = executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [5_000.0, DummyToken.VaultStoragePath, false], + user + ) + Test.expect(createPosRes, Test.beSucceeded()) + log("✓ User created position with 5000 DummyToken collateral\n") + + let pid: UInt64 = 1 // User's position ID + + // Verify position has DummyToken collateral + var details = getPositionDetails(pid: pid, beFailed: false) + let dummyCredit = getCreditBalanceForType(details: details, vaultType: CompositeType(DUMMY_TOKEN_IDENTIFIER)!) + Test.assert(dummyCredit >= 5_000.0 - 0.01, message: "Position should have ~5000 DummyToken collateral") + log("Position state:") + log(" - DummyToken collateral: ".concat(dummyCredit.toString())) + + // ===== Step 1: Borrow FLOW (first debt type) ===== + + log("\n--- Step 1: Borrow FLOW (first debt type) ---") + + borrowFromPosition( + signer: user, + positionId: pid, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + amount: 1_000.0, + beFailed: false + ) + log("✓ User borrowed 1000 FLOW (first debt type, from reserves)") + + // Verify position now has FLOW debt + details = getPositionDetails(pid: pid, beFailed: false) + let flowDebt = getDebitBalanceForType(details: details, vaultType: CompositeType(FLOW_TOKEN_IDENTIFIER)!) + Test.assert(flowDebt >= 1_000.0 - 0.01, message: "Position should have ~1000 FLOW debt") + + log("\nPosition state after borrowing FLOW:") + log(" - DummyToken collateral: ".concat(dummyCredit.toString())) + log(" - FLOW debt: ".concat(flowDebt.toString())) + + // Check position health + let health = getPositionHealth(pid: pid, beFailed: false) + log(" - Health: ".concat(health.toString())) + Test.assert(health >= UFix128(1.1), message: "Position should be healthy") + + // ===== Step 2: Try to borrow MOET (second debt type) - SHOULD FAIL ===== + + log("\n--- Step 2: Try to borrow MOET (second debt type) ---") + + let borrowMoetRes = executeTransaction( + "./transactions/position-manager/borrow_from_position.cdc", + [pid, MOET_TOKEN_IDENTIFIER, 500.0], + user + ) + + // Verify it FAILED + if borrowMoetRes.status == Test.ResultStatus.succeeded { + log("❌ ERROR: Borrowing MOET should have failed but succeeded!") + log("❌ Debt type constraint is NOT enforced!") + Test.assert(false, message: "Should not be able to borrow MOET after already having FLOW debt") + } else { + log("✅ Borrowing MOET correctly FAILED") + + // Check error message + let errorMsg = borrowMoetRes.error?.message ?? "" + if errorMsg.contains("debt type") || errorMsg.contains("Only one debt type") { + log("✅ Error message mentions debt type constraint") + } else { + log("⚠️ Warning: Error message doesn't clearly mention debt type constraint") + } + } + + log("\n=== Test Complete: Debt Type Constraint Verified ===") +} + +/// Test that multiple borrows of the SAME debt type still work +access(all) +fun testCanBorrowSameDebtTypeMultipleTimes() { + Test.reset(to: snapshot) + log("\n=== Test: Can Borrow Same Debt Type Multiple Times (3 Tokens) ===\n") + + // Setup FLOW reserves + let flowProvider = Test.createAccount() + setupMoetVault(flowProvider, beFailed: false) + transferFlowTokens(to: flowProvider, amount: 10_000.0) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, flowProvider) + + let createFlowPos = executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [5_000.0, FLOW_VAULT_STORAGE_PATH, false], + flowProvider + ) + Test.expect(createFlowPos, Test.beSucceeded()) + log("✓ FlowProvider deposited 5000 FLOW (creates reserves)") + + // Main user + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + setupDummyTokenVault(user) + mintDummyToken(to: user, amount: 10_000.0) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) + + // Create position with DummyToken collateral + let createPosRes = executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [5_000.0, DummyToken.VaultStoragePath, false], + user + ) + Test.expect(createPosRes, Test.beSucceeded()) + log("✓ User created position with 5000 DummyToken collateral") + + let pid: UInt64 = 1 + + // Borrow FLOW (first time) + borrowFromPosition( + signer: user, + positionId: pid, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + amount: 500.0, + beFailed: false + ) + log("✓ Borrowed 500 FLOW (first borrow)") + + // Borrow FLOW (second time) - should SUCCEED + borrowFromPosition( + signer: user, + positionId: pid, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + amount: 300.0, + beFailed: false + ) + log("✓ Borrowed 300 more FLOW (second borrow - same type)") + + // Borrow FLOW (third time) - should SUCCEED + borrowFromPosition( + signer: user, + positionId: pid, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + amount: 200.0, + beFailed: false + ) + log("✓ Borrowed 200 more FLOW (third borrow - same type)") + + // Verify total FLOW debt + let details = getPositionDetails(pid: pid, beFailed: false) + let flowDebt = getDebitBalanceForType(details: details, vaultType: CompositeType(FLOW_TOKEN_IDENTIFIER)!) + Test.assert(flowDebt >= 1_000.0 - 0.01, message: "Should have ~1000 total FLOW debt") + log("✓ Total FLOW debt: ".concat(flowDebt.toString())) + + log("\n=== Test Complete: Same Debt Type Borrowing Works ===") +} + +/// Test that withdrawing collateral while having debt works +access(all) +fun testCanWithdrawCollateralWithDebt() { + Test.reset(to: snapshot) + log("\n=== Test: Can Withdraw Collateral While Having Debt (3 Tokens) ===\n") + + // Setup FLOW reserves + let flowProvider = Test.createAccount() + setupMoetVault(flowProvider, beFailed: false) + transferFlowTokens(to: flowProvider, amount: 10_000.0) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, flowProvider) + + let createFlowPos = executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [5_000.0, FLOW_VAULT_STORAGE_PATH, false], + flowProvider + ) + Test.expect(createFlowPos, Test.beSucceeded()) + log("✓ FlowProvider deposited 5000 FLOW") + + // Main user + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + setupDummyTokenVault(user) + mintDummyToken(to: user, amount: 10_000.0) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) + + // Create position with DummyToken collateral + let createPosRes = executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [5_000.0, DummyToken.VaultStoragePath, false], + user + ) + Test.expect(createPosRes, Test.beSucceeded()) + log("✓ User created position with 5000 DummyToken collateral") + + let pid: UInt64 = 1 + + // Borrow FLOW + borrowFromPosition( + signer: user, + positionId: pid, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + amount: 1_000.0, + beFailed: false + ) + log("✓ Borrowed 1000 FLOW") + + // Withdraw some DummyToken collateral while debt exists + withdrawFromPosition( + signer: user, + positionId: pid, + tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER, + amount: 1_000.0, + pullFromTopUpSource: false + ) + log("✓ Withdrew 1000 DummyToken collateral (while having FLOW debt)") + + // Verify position is still healthy + let health = getPositionHealth(pid: pid, beFailed: false) + log("✓ Position health after withdrawal: ".concat(health.toString())) + Test.assert(health >= UFix128(1.1), message: "Position should still be healthy") + + log("\n=== Test Complete: Collateral Withdrawal Works ===") +} + +// Helper functions + +access(all) +fun setupDummyTokenVault(_ account: Test.TestAccount) { + let result = executeTransaction( + "./transactions/dummy_token/setup_vault.cdc", + [], + account + ) + Test.expect(result, Test.beSucceeded()) +} + +access(all) +fun mintDummyToken(to: Test.TestAccount, amount: UFix64) { + let result = executeTransaction( + "./transactions/dummy_token/mint.cdc", + [amount, to.address], + PROTOCOL_ACCOUNT + ) + Test.expect(result, Test.beSucceeded()) +} diff --git a/cadence/tests/single_token_constraint_test.cdc b/cadence/tests/single_token_constraint_test.cdc new file mode 100644 index 00000000..f56978ad --- /dev/null +++ b/cadence/tests/single_token_constraint_test.cdc @@ -0,0 +1,481 @@ +import Test +import BlockchainHelpers + +import "MOET" +import "FlowToken" +import "FlowALPv0" +import "test_helpers.cdc" + +/// Tests that verify single collateral and single debt token type constraints per position +/// +/// Each position should only allow: +/// - ONE collateral token type (Credit balance) +/// - ONE debt token type (Debit balance) + +access(all) var snapshot: UInt64 = 0 + +access(all) +fun setup() { + deployContracts() + + // Setup oracle prices + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0) + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: MOET_TOKEN_IDENTIFIER, price: 1.0) + + // Create pool with MOET as default token (borrowable via minting) + createAndStorePool(signer: PROTOCOL_ACCOUNT, defaultTokenIdentifier: MOET_TOKEN_IDENTIFIER, beFailed: false) + + // Add FLOW as supported token (can be both collateral and debt) + addSupportedTokenZeroRateCurve( + signer: PROTOCOL_ACCOUNT, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + collateralFactor: 0.8, + borrowFactor: 0.77, + depositRate: 1_000_000.0, + depositCapacityCap: 1_000_000.0 + ) + + // MOET is already added as the default token when pool was created + + snapshot = getCurrentBlockHeight() +} + +/// Test that a position with FLOW collateral cannot add MOET collateral +access(all) +fun testCannotAddSecondCollateralType() { + log("=== Test: Cannot Add Second Collateral Type ===") + + // Create user with both FLOW and MOET + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + transferFlowTokens(to: user, amount: 2_000.0) + mintMoet(signer: PROTOCOL_ACCOUNT, to: user.address, amount: 2_000.0, beFailed: false) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) + + // Create position with FLOW collateral + let createPosRes = executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [1_000.0, FLOW_VAULT_STORAGE_PATH, false], + user + ) + Test.expect(createPosRes, Test.beSucceeded()) + log("✓ Created position with FLOW collateral") + + let pid: UInt64 = 0 + + // Try to deposit MOET to the same position - should FAIL + let depositMoetRes = executeTransaction( + "./transactions/position-manager/deposit_to_position.cdc", + [pid, 500.0, MOET.VaultStoragePath, false], + user + ) + Test.expect(depositMoetRes, Test.beFailed()) + log("✓ Depositing MOET to FLOW-collateral position correctly failed") + + // Verify error message mentions collateral type constraint + let errorMsg = depositMoetRes.error?.message ?? "" + Test.assert( + errorMsg.contains("collateral") || errorMsg.contains("drawDownSink"), + message: "Error should mention collateral type constraint. Got: ".concat(errorMsg) + ) + log("✓ Error message mentions collateral type constraint") + + log("=== Test Passed: Cannot Add Second Collateral Type ===\n") +} + +/// Test that a position with MOET debt cannot borrow FLOW +access(all) +fun testCannotAddSecondDebtType() { + Test.reset(to: snapshot) + log("=== Test: Cannot Add Second Debt Type ===") + + // Create user1 with FLOW to provide reserves + let user1 = Test.createAccount() + setupMoetVault(user1, beFailed: false) + transferFlowTokens(to: user1, amount: 5_000.0) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user1) + + // User1 deposits FLOW to create reserves + let createPos1Res = executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [3_000.0, FLOW_VAULT_STORAGE_PATH, false], + user1 + ) + Test.expect(createPos1Res, Test.beSucceeded()) + log("✓ User1 deposited 3000 FLOW (creates FLOW reserves)") + + // Create user2 with MOET collateral (NOT FLOW) + let user2 = Test.createAccount() + setupMoetVault(user2, beFailed: false) + mintMoet(signer: PROTOCOL_ACCOUNT, to: user2.address, amount: 3_000.0, beFailed: false) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user2) + + // User2 creates position with MOET collateral + let createPos2Res = executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [2_000.0, MOET.VaultStoragePath, false], + user2 + ) + Test.expect(createPos2Res, Test.beSucceeded()) + log("✓ User2 created position with 2000 MOET collateral") + + let pid: UInt64 = 1 // User2's position ID + + // User2 borrows FLOW (first debt type) - borrows from reserves created by User1 + borrowFromPosition( + signer: user2, + positionId: pid, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + amount: 300.0, + beFailed: false + ) + log("✓ User2 borrowed 300 FLOW (first debt type, from reserves)") + + // Verify position has FLOW debt + let details = getPositionDetails(pid: pid, beFailed: false) + let flowDebt = getDebitBalanceForType(details: details, vaultType: CompositeType(FLOW_TOKEN_IDENTIFIER)!) + Test.assert(flowDebt > 0.0, message: "Position should have FLOW debt") + log("✓ Position has FLOW debt: ".concat(flowDebt.toString())) + + // Try to borrow MOET (second debt type) - should FAIL + // This creates MOET debt via minting, different from FLOW debt + borrowFromPosition( + signer: user2, + positionId: pid, + tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + amount: 200.0, + beFailed: true // Expect failure + ) + log("✓ Borrowing MOET after FLOW correctly failed") + + log("=== Test Passed: Cannot Add Second Debt Type ===\n") +} + +/// Test that a position with MOET collateral cannot add FLOW collateral +access(all) +fun testCannotAddFlowToMoetCollateral() { + Test.reset(to: snapshot) + log("=== Test: Cannot Add FLOW to MOET Collateral ===") + + // Create user with both tokens + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + transferFlowTokens(to: user, amount: 2_000.0) + mintMoet(signer: PROTOCOL_ACCOUNT, to: user.address, amount: 2_000.0, beFailed: false) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) + + // Create position with MOET collateral + let createPosRes = executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [1_000.0, MOET.VaultStoragePath, false], + user + ) + Test.expect(createPosRes, Test.beSucceeded()) + log("✓ Created position with MOET collateral") + + let pid: UInt64 = 0 + + // Try to deposit FLOW to the same position - should FAIL + let depositFlowRes = executeTransaction( + "./transactions/position-manager/deposit_to_position.cdc", + [pid, 500.0, FLOW_VAULT_STORAGE_PATH, false], + user + ) + Test.expect(depositFlowRes, Test.beFailed()) + log("✓ Depositing FLOW to MOET-collateral position correctly failed") + + log("=== Test Passed: Cannot Add FLOW to MOET Collateral ===\n") +} + +/// Test that a position with FLOW collateral and MOET debt can withdraw both token types +/// (Withdraw FLOW = reduce collateral, Withdraw MOET = borrow more debt) +access(all) +fun testCanWithdrawBothCollateralAndDebtTokens() { + Test.reset(to: snapshot) + log("=== Test: Can Withdraw Both Collateral and Debt Tokens ===") + + // Create user with FLOW collateral + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + transferFlowTokens(to: user, amount: 5_000.0) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) + + // Create position with FLOW collateral + let createPosRes = executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [3_000.0, FLOW_VAULT_STORAGE_PATH, false], + user + ) + Test.expect(createPosRes, Test.beSucceeded()) + log("✓ Created position with 3000 FLOW collateral") + + let pid: UInt64 = 0 + + // Borrow MOET (create debt) + borrowFromPosition( + signer: user, + positionId: pid, + tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + amount: 500.0, + beFailed: false + ) + log("✓ Borrowed 500 MOET (created debt)") + + // Verify position has both FLOW collateral and MOET debt + var details = getPositionDetails(pid: pid, beFailed: false) + let flowCredit = getCreditBalanceForType(details: details, vaultType: CompositeType(FLOW_TOKEN_IDENTIFIER)!) + let moetDebt = getDebitBalanceForType(details: details, vaultType: CompositeType(MOET_TOKEN_IDENTIFIER)!) + Test.assert(flowCredit > 0.0, message: "Position should have FLOW collateral") + Test.assert(moetDebt > 0.0, message: "Position should have MOET debt") + log("✓ Position has FLOW collateral: ".concat(flowCredit.toString())) + log("✓ Position has MOET debt: ".concat(moetDebt.toString())) + + // Withdraw FLOW (reduce collateral) - should SUCCEED + withdrawFromPosition( + signer: user, + positionId: pid, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + amount: 500.0, + pullFromTopUpSource: false + ) + log("✓ Withdrew 500 FLOW from collateral reserves") + + // Verify FLOW collateral decreased + details = getPositionDetails(pid: pid, beFailed: false) + let flowCreditAfter = getCreditBalanceForType(details: details, vaultType: CompositeType(FLOW_TOKEN_IDENTIFIER)!) + Test.assert(flowCreditAfter < flowCredit, message: "FLOW collateral should have decreased") + log("✓ FLOW collateral after withdrawal: ".concat(flowCreditAfter.toString())) + + // Withdraw MOET (borrow more debt) - should SUCCEED + borrowFromPosition( + signer: user, + positionId: pid, + tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + amount: 200.0, + beFailed: false + ) + log("✓ Borrowed additional 200 MOET (increased debt)") + + // Verify MOET debt increased + details = getPositionDetails(pid: pid, beFailed: false) + let moetDebtAfter = getDebitBalanceForType(details: details, vaultType: CompositeType(MOET_TOKEN_IDENTIFIER)!) + Test.assert(moetDebtAfter > moetDebt, message: "MOET debt should have increased") + log("✓ MOET debt after additional borrow: ".concat(moetDebtAfter.toString())) + + // Verify position is still healthy + let health = getPositionHealth(pid: pid, beFailed: false) + Test.assert(health >= UFix128(1.1), message: "Position should maintain healthy ratio") + log("✓ Position health: ".concat(health.toString())) + + log("=== Test Passed: Can Withdraw Both Collateral and Debt Tokens ===\n") +} + +/// Test that withdrawing collateral token beyond balance (creating debt) with different type fails +access(all) +fun testCannotWithdrawCollateralBeyondBalanceWithDifferentDebt() { + Test.reset(to: snapshot) + log("=== Test: Cannot Create Second Debt Type by Over-Withdrawing Collateral ===") + + // Create user1 with FLOW to provide reserves + let user1 = Test.createAccount() + setupMoetVault(user1, beFailed: false) + transferFlowTokens(to: user1, amount: 5_000.0) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user1) + + // User1 deposits FLOW to create reserves + let createPos1Res = executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [3_000.0, FLOW_VAULT_STORAGE_PATH, false], + user1 + ) + Test.expect(createPos1Res, Test.beSucceeded()) + log("✓ User1 deposited 3000 FLOW (creates FLOW reserves)") + + // Create user2 with small MOET collateral + let user2 = Test.createAccount() + setupMoetVault(user2, beFailed: false) + mintMoet(signer: PROTOCOL_ACCOUNT, to: user2.address, amount: 3_000.0, beFailed: false) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user2) + + // User2 creates position with 100 MOET collateral (small amount) + let createPos2Res = executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [100.0, MOET.VaultStoragePath, false], + user2 + ) + Test.expect(createPos2Res, Test.beSucceeded()) + log("✓ User2 created position with 100 MOET collateral") + + let pid: UInt64 = 1 // User2's position ID + + // User2 borrows FLOW (first debt type, from reserves) + borrowFromPosition( + signer: user2, + positionId: pid, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + amount: 30.0, + beFailed: false + ) + log("✓ User2 borrowed 30 FLOW (first debt type, from reserves)") + + // Verify position has: MOET collateral (100), FLOW debt (30) + let details = getPositionDetails(pid: pid, beFailed: false) + let moetCredit = getCreditBalanceForType(details: details, vaultType: CompositeType(MOET_TOKEN_IDENTIFIER)!) + let flowDebt = getDebitBalanceForType(details: details, vaultType: CompositeType(FLOW_TOKEN_IDENTIFIER)!) + Test.assert(moetCredit >= 100.0 - 0.01, message: "Should have ~100 MOET collateral") + Test.assert(flowDebt >= 30.0 - 0.01, message: "Should have ~30 FLOW debt") + log("✓ Position has MOET collateral: ".concat(moetCredit.toString())) + log("✓ Position has FLOW debt: ".concat(flowDebt.toString())) + + // Now try to withdraw 200 MOET (more than the 100 collateral) + // This would flip MOET from Credit to Debit, creating MOET debt + // Should FAIL because we already have FLOW debt + withdrawFromPosition( + signer: user2, + positionId: pid, + tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + amount: 200.0, + pullFromTopUpSource: false + ) + // Note: This might succeed or fail depending on health constraints + // The actual constraint validation happens when Credit flips to Debit + + log("✓ Withdrawal attempt completed") + log("=== Test Passed: Cannot Create Second Debt Type by Over-Withdrawing Collateral ===\n") +} + +/// Test that multiple deposits of the SAME collateral type work fine +access(all) +fun testMultipleDepositsOfSameCollateralTypeSucceed() { + Test.reset(to: snapshot) + log("=== Test: Multiple Deposits of Same Collateral Type Succeed ===") + + // Create user with FLOW + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + transferFlowTokens(to: user, amount: 5_000.0) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) + + // Create position with FLOW collateral + let createPosRes = executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [1_000.0, FLOW_VAULT_STORAGE_PATH, false], + user + ) + Test.expect(createPosRes, Test.beSucceeded()) + log("✓ Created position with 1000 FLOW collateral") + + let pid: UInt64 = 0 + + // Get initial balance + var details = getPositionDetails(pid: pid, beFailed: false) + var flowCredit = getCreditBalanceForType(details: details, vaultType: CompositeType(FLOW_TOKEN_IDENTIFIER)!) + log("Initial FLOW credit balance: ".concat(flowCredit.toString())) + + // Deposit more FLOW to the same position - should SUCCEED + depositToPosition( + signer: user, + positionID: pid, + amount: 500.0, + vaultStoragePath: FLOW_VAULT_STORAGE_PATH, + pushToDrawDownSink: false + ) + log("✓ Deposited additional 500 FLOW to same position") + + // Verify balance increased + details = getPositionDetails(pid: pid, beFailed: false) + flowCredit = getCreditBalanceForType(details: details, vaultType: CompositeType(FLOW_TOKEN_IDENTIFIER)!) + Test.assert(flowCredit >= 1_500.0 - 0.01, message: "FLOW credit should be ~1500") + log("✓ FLOW credit balance after second deposit: ".concat(flowCredit.toString())) + + // Deposit even more FLOW - should SUCCEED + depositToPosition( + signer: user, + positionID: pid, + amount: 1_000.0, + vaultStoragePath: FLOW_VAULT_STORAGE_PATH, + pushToDrawDownSink: false + ) + log("✓ Deposited additional 1000 FLOW to same position") + + // Verify balance increased again + details = getPositionDetails(pid: pid, beFailed: false) + flowCredit = getCreditBalanceForType(details: details, vaultType: CompositeType(FLOW_TOKEN_IDENTIFIER)!) + Test.assert(flowCredit >= 2_500.0 - 0.01, message: "FLOW credit should be ~2500") + log("✓ FLOW credit balance after third deposit: ".concat(flowCredit.toString())) + + log("=== Test Passed: Multiple Deposits of Same Collateral Type Succeed ===\n") +} + +/// Test that multiple borrows of the SAME debt type work fine +access(all) +fun testMultipleBorrowsOfSameDebtTypeSucceed() { + Test.reset(to: snapshot) + log("=== Test: Multiple Borrows of Same Debt Type Succeed ===") + + // Create user with FLOW collateral + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + transferFlowTokens(to: user, amount: 5_000.0) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) + + // Create position with FLOW collateral + let createPosRes = executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [3_000.0, FLOW_VAULT_STORAGE_PATH, false], + user + ) + Test.expect(createPosRes, Test.beSucceeded()) + log("✓ Created position with 3000 FLOW collateral") + + let pid: UInt64 = 0 + + // Borrow MOET (first time) + borrowFromPosition( + signer: user, + positionId: pid, + tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + amount: 300.0, + beFailed: false + ) + log("✓ Borrowed 300 MOET (first borrow)") + + // Get debt balance + var details = getPositionDetails(pid: pid, beFailed: false) + var moetDebt = getDebitBalanceForType(details: details, vaultType: CompositeType(MOET_TOKEN_IDENTIFIER)!) + log("MOET debt after first borrow: ".concat(moetDebt.toString())) + + // Borrow more MOET (second time) - should SUCCEED + borrowFromPosition( + signer: user, + positionId: pid, + tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + amount: 200.0, + beFailed: false + ) + log("✓ Borrowed additional 200 MOET (second borrow)") + + // Verify debt increased + details = getPositionDetails(pid: pid, beFailed: false) + moetDebt = getDebitBalanceForType(details: details, vaultType: CompositeType(MOET_TOKEN_IDENTIFIER)!) + Test.assert(moetDebt >= 500.0 - 0.01, message: "MOET debt should be ~500") + log("✓ MOET debt after second borrow: ".concat(moetDebt.toString())) + + // Borrow even more MOET (third time) - should SUCCEED + borrowFromPosition( + signer: user, + positionId: pid, + tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + amount: 100.0, + beFailed: false + ) + log("✓ Borrowed additional 100 MOET (third borrow)") + + // Verify debt increased again + details = getPositionDetails(pid: pid, beFailed: false) + moetDebt = getDebitBalanceForType(details: details, vaultType: CompositeType(MOET_TOKEN_IDENTIFIER)!) + Test.assert(moetDebt >= 600.0 - 0.01, message: "MOET debt should be ~600") + log("✓ MOET debt after third borrow: ".concat(moetDebt.toString())) + + log("=== Test Passed: Multiple Borrows of Same Debt Type Succeed ===\n") +} diff --git a/cadence/tests/transactions/position-manager/withdraw_from_position.cdc b/cadence/tests/transactions/position-manager/withdraw_from_position.cdc index fc9f54e3..d7214db5 100644 --- a/cadence/tests/transactions/position-manager/withdraw_from_position.cdc +++ b/cadence/tests/transactions/position-manager/withdraw_from_position.cdc @@ -1,6 +1,7 @@ import "FungibleToken" import "FlowToken" import "MOET" +import "DummyToken" import "FlowALPv0" import "FlowALPModels" @@ -47,6 +48,9 @@ transaction( } else if tokenTypeIdentifier == "A.0000000000000007.MOET.Vault" { // For MOET, use the MOET vault path receiverRef = signer.storage.borrow<&{FungibleToken.Receiver}>(from: MOET.VaultStoragePath) + } else if tokenTypeIdentifier == "A.0000000000000007.DummyToken.Vault" { + // For DummyToken, use the DummyToken vault path + receiverRef = signer.storage.borrow<&{FungibleToken.Receiver}>(from: DummyToken.VaultStoragePath) } self.receiverVault = receiverRef ?? panic("Could not borrow vault receiver for token type: \(tokenTypeIdentifier). Ensure vault is set up.") From 7187d1e74cfa5da711ea01076760d17f578928bf Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Fri, 27 Feb 2026 23:54:41 -0500 Subject: [PATCH 09/26] fix test --- .../tests/single_token_constraint_test.cdc | 57 ++++++++++++------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/cadence/tests/single_token_constraint_test.cdc b/cadence/tests/single_token_constraint_test.cdc index f56978ad..7ded39ca 100644 --- a/cadence/tests/single_token_constraint_test.cdc +++ b/cadence/tests/single_token_constraint_test.cdc @@ -83,11 +83,18 @@ fun testCannotAddSecondCollateralType() { log("=== Test Passed: Cannot Add Second Collateral Type ===\n") } -/// Test that a position with MOET debt cannot borrow FLOW +/// Test that with MOET collateral and FLOW debt, borrowing MOET draws from Credit (not Debit). +/// +/// In the two-token case (FLOW + MOET), the single-debt-type constraint cannot be directly +/// triggered via a healthy position because health checks always prevent the Credit-to-Debit +/// flip scenario. When a user has MOET collateral (Credit) and FLOW debt (Debit), "borrowing" +/// MOET simply reduces the existing MOET Credit — it does NOT create a second Debit entry. +/// The 3-token test (debt_type_constraint_three_token_test.cdc) covers the full constraint +/// using a neutral DummyToken collateral. access(all) fun testCannotAddSecondDebtType() { Test.reset(to: snapshot) - log("=== Test: Cannot Add Second Debt Type ===") + log("=== Test: Borrowing Collateral Token Reduces Credit, Not Creates Second Debit ===") // Create user1 with FLOW to provide reserves let user1 = Test.createAccount() @@ -131,24 +138,35 @@ fun testCannotAddSecondDebtType() { ) log("✓ User2 borrowed 300 FLOW (first debt type, from reserves)") - // Verify position has FLOW debt - let details = getPositionDetails(pid: pid, beFailed: false) + // Capture MOET Credit before the next operation + var details = getPositionDetails(pid: pid, beFailed: false) let flowDebt = getDebitBalanceForType(details: details, vaultType: CompositeType(FLOW_TOKEN_IDENTIFIER)!) + let moetCreditBefore = getCreditBalanceForType(details: details, vaultType: CompositeType(MOET_TOKEN_IDENTIFIER)!) Test.assert(flowDebt > 0.0, message: "Position should have FLOW debt") log("✓ Position has FLOW debt: ".concat(flowDebt.toString())) + log("✓ MOET Credit before: ".concat(moetCreditBefore.toString())) - // Try to borrow MOET (second debt type) - should FAIL - // This creates MOET debt via minting, different from FLOW debt + // Borrowing MOET when user already has MOET collateral (Credit) draws from that Credit — + // it does NOT create MOET Debit and therefore does NOT violate the single-debt-type rule. borrowFromPosition( signer: user2, positionId: pid, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, amount: 200.0, - beFailed: true // Expect failure + beFailed: false // SUCCEEDS: draws from existing MOET Credit, no new Debit created ) - log("✓ Borrowing MOET after FLOW correctly failed") + log("✓ Borrowing MOET drew from MOET Credit (no second Debit created)") - log("=== Test Passed: Cannot Add Second Debt Type ===\n") + // Verify: MOET Credit decreased, and no MOET Debit was created + details = getPositionDetails(pid: pid, beFailed: false) + let moetCreditAfter = getCreditBalanceForType(details: details, vaultType: CompositeType(MOET_TOKEN_IDENTIFIER)!) + let moetDebt = getDebitBalanceForType(details: details, vaultType: CompositeType(MOET_TOKEN_IDENTIFIER)!) + Test.assert(moetCreditAfter < moetCreditBefore, message: "MOET Credit should have decreased") + Test.assert(moetDebt == 0.0, message: "No MOET Debit should have been created") + log("✓ MOET Credit after: ".concat(moetCreditAfter.toString()).concat(" (decreased by 200)")) + log("✓ MOET Debit: ".concat(moetDebt.toString()).concat(" (no second debt type created)")) + + log("=== Test Passed: Borrowing Collateral Token Reduces Credit, Not Creates Second Debit ===\n") } /// Test that a position with MOET collateral cannot add FLOW collateral @@ -327,20 +345,17 @@ fun testCannotWithdrawCollateralBeyondBalanceWithDifferentDebt() { log("✓ Position has MOET collateral: ".concat(moetCredit.toString())) log("✓ Position has FLOW debt: ".concat(flowDebt.toString())) - // Now try to withdraw 200 MOET (more than the 100 collateral) - // This would flip MOET from Credit to Debit, creating MOET debt - // Should FAIL because we already have FLOW debt - withdrawFromPosition( - signer: user2, - positionId: pid, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, - amount: 200.0, - pullFromTopUpSource: false + // Now try to withdraw 200 MOET (more than the 100 collateral). + // The contract prevents withdrawals beyond the Credit balance with + // "Insufficient funds for withdrawal" — you simply cannot over-withdraw. + let overWithdrawRes = executeTransaction( + "./transactions/position-manager/withdraw_from_position.cdc", + [pid, MOET_TOKEN_IDENTIFIER, 200.0, false], + user2 ) - // Note: This might succeed or fail depending on health constraints - // The actual constraint validation happens when Credit flips to Debit + Test.expect(overWithdrawRes, Test.beFailed()) + log("✓ Over-withdrawal correctly failed (Insufficient funds — cannot withdraw beyond Credit balance)") - log("✓ Withdrawal attempt completed") log("=== Test Passed: Cannot Create Second Debt Type by Over-Withdrawing Collateral ===\n") } From 1a6b4ec4f8526ece428e86278f56752345c82104 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Mon, 2 Mar 2026 15:15:11 -0500 Subject: [PATCH 10/26] method names for multi types --- cadence/contracts/FlowALPModels.cdc | 70 +++++++++++++++++++---------- 1 file changed, 46 insertions(+), 24 deletions(-) diff --git a/cadence/contracts/FlowALPModels.cdc b/cadence/contracts/FlowALPModels.cdc index a9c04ab2..9b778dec 100644 --- a/cadence/contracts/FlowALPModels.cdc +++ b/cadence/contracts/FlowALPModels.cdc @@ -1904,13 +1904,13 @@ access(all) contract FlowALPModels { /// If nil, the Pool will not pull underflown value, and liquidation may occur. access(EImplementation) fun setTopUpSource(_ source: {DeFiActions.Source}?) - /// Returns the current collateral type for this position based on existing Credit balances - /// Returns nil if no Credit balance exists yet (allows any collateral type for first deposit) - access(all) fun getCollateralType(): Type? + /// Returns the current collateral types for this position based on existing Credit balances + /// Returns empty array if no Credit balance exists yet (allows any collateral type for first deposit) + access(all) fun getCollateralTypes(): [Type] - /// Returns the current debt type for this position based on existing Debit balances - /// Returns nil if no Debit balance exists yet (allows any debt type for first borrow) - access(all) fun getDebtType(): Type? + /// Returns the current debt types for this position based on existing Debit balances + /// Returns empty array if no Debit balance exists yet (allows any debt type for first borrow) + access(all) fun getDebtTypes(): [Type] /// Validates that the given token type can be used as collateral for this position /// Panics if position already has a different collateral type @@ -2088,54 +2088,76 @@ access(all) contract FlowALPModels { self.topUpSource = source } - /// Returns the current collateral type for this position based on existing Credit balances - /// Returns nil if no Credit balance exists yet (allows any collateral type for first deposit) - access(all) fun getCollateralType(): Type? { + /// Returns the current collateral types for this position based on existing Credit balances + /// Returns empty array if no Credit balance exists yet (allows any collateral type for first deposit) + access(all) fun getCollateralTypes(): [Type] { + let types: [Type] = [] for type in self.balances.keys { if self.balances[type]!.direction == BalanceDirection.Credit { - return type + types.append(type) } } - return nil + return types } - /// Returns the current debt type for this position based on existing Debit balances - /// Returns nil if no Debit balance exists yet (allows any debt type for first borrow) - access(all) fun getDebtType(): Type? { + /// Returns the current debt types for this position based on existing Debit balances + /// Returns empty array if no Debit balance exists yet (allows any debt type for first borrow) + access(all) fun getDebtTypes(): [Type] { + let types: [Type] = [] for type in self.balances.keys { if self.balances[type]!.direction == BalanceDirection.Debit { - return type + types.append(type) } } - return nil + return types } /// Validates that the given token type can be used as collateral for this position /// Panics if position already has a different collateral type access(EImplementation) fun validateCollateralType(_ type: Type) { - let existingType = self.getCollateralType() - if existingType == nil { + let existingTypes = self.getCollateralTypes() + + // Constraint: For now, only one collateral type is allowed per position + // This assertion ensures the invariant is maintained + assert(existingTypes.length <= 1, message: "Internal error: Position has multiple collateral types") + + if existingTypes.length == 0 { // No collateral yet, allow any type return } - if existingType! != type { - panic("Position already has collateral type ".concat(existingType!.identifier).concat(". Cannot deposit ").concat(type.identifier).concat(". Only one collateral type allowed per position.")) + // Check if type already exists (idempotent) + if existingTypes.contains(type) { + return } + + // For now, only one collateral type is allowed per position + // This restriction can be removed in the future to support multiple collateral types + panic("Position already has collateral type ".concat(existingTypes[0].identifier).concat(". Cannot deposit ").concat(type.identifier).concat(". Only one collateral type allowed per position.")) } /// Validates that the given token type can be used as debt for this position /// Panics if position already has a different debt type access(EImplementation) fun validateDebtType(_ type: Type) { - let existingType = self.getDebtType() - if existingType == nil { + let existingTypes = self.getDebtTypes() + + // Constraint: For now, only one debt type is allowed per position + // This assertion ensures the invariant is maintained + assert(existingTypes.length <= 1, message: "Internal error: Position has multiple debt types") + + if existingTypes.length == 0 { // No debt yet, allow any type return } - if existingType! != type { - panic("Position already has debt type ".concat(existingType!.identifier).concat(". Cannot borrow ").concat(type.identifier).concat(". Only one debt type allowed per position.")) + // Check if type already exists (idempotent) + if existingTypes.contains(type) { + return } + + // For now, only one debt type is allowed per position + // This restriction can be removed in the future to support multiple debt types + panic("Position already has debt type ".concat(existingTypes[0].identifier).concat(". Cannot borrow ").concat(type.identifier).concat(". Only one debt type allowed per position.")) } } From 093d823cbc0cf20704e32967bb6f9e5b2946a312 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 4 Mar 2026 13:54:52 -0500 Subject: [PATCH 11/26] reserve handler --- cadence/contracts/FlowALPModels.cdc | 159 ++++++++++++++++++++++++++++ cadence/contracts/FlowALPv0.cdc | 80 ++++++++------ 2 files changed, 204 insertions(+), 35 deletions(-) diff --git a/cadence/contracts/FlowALPModels.cdc b/cadence/contracts/FlowALPModels.cdc index 9b778dec..312b8a86 100644 --- a/cadence/contracts/FlowALPModels.cdc +++ b/cadence/contracts/FlowALPModels.cdc @@ -2,6 +2,7 @@ import "FungibleToken" import "DeFiActions" import "DeFiActionsUtils" import "MOET" +import "Burner" import "FlowALPMath" import "FlowALPInterestRates" import "FlowALPEvents" @@ -244,6 +245,147 @@ access(all) contract FlowALPModels { } } + /// TokenReserveHandler + /// + /// Interface for handling token reserve operations. Different token types may require + /// different handling for deposits and withdrawals. For example, MOET tokens are minted + /// on debt withdrawals and burned on repayments, while standard tokens use reserve vaults. + access(all) struct interface TokenReserveHandler { + /// Returns the token type this handler manages + access(all) view fun getTokenType(): Type + + /// Deposits collateral (always to reserves, including MOET) + access(all) fun depositCollateral( + state: auth(EImplementation) &{PoolState}, + from: @{FungibleToken.Vault} + ): UFix64 + + /// Deposits repayment (to reserves for most tokens, BURNS for MOET) + access(all) fun depositRepayment( + state: auth(EImplementation) &{PoolState}, + from: @{FungibleToken.Vault} + ): UFix64 + + /// Withdraws debt (from reserves for most tokens, MINTS for MOET) + access(all) fun withdrawDebt( + state: auth(EImplementation) &{PoolState}, + amount: UFix64, + minterRef: &MOET.Minter? + ): @{FungibleToken.Vault} + + /// Withdraws collateral (always from reserves, including MOET) + access(all) fun withdrawCollateral( + state: auth(EImplementation) &{PoolState}, + amount: UFix64 + ): @{FungibleToken.Vault} + } + + /// StandardTokenReserveHandler + /// + /// Standard implementation of TokenReserveHandler that interacts with reserve vaults + /// for all four operations (deposit/withdraw collateral and debt). + access(all) struct StandardTokenReserveHandler: TokenReserveHandler { + access(self) let tokenType: Type + + init(tokenType: Type) { + self.tokenType = tokenType + } + + access(all) view fun getTokenType(): Type { + return self.tokenType + } + + access(all) fun depositCollateral( + state: auth(EImplementation) &{PoolState}, + from: @{FungibleToken.Vault} + ): UFix64 { + let amount = from.balance + let reserveVault = state.borrowOrCreateReserve(self.tokenType) + reserveVault.deposit(from: <-from) + return amount + } + + access(all) fun depositRepayment( + state: auth(EImplementation) &{PoolState}, + from: @{FungibleToken.Vault} + ): UFix64 { + let amount = from.balance + let reserveVault = state.borrowOrCreateReserve(self.tokenType) + reserveVault.deposit(from: <-from) + return amount + } + + access(all) fun withdrawDebt( + state: auth(EImplementation) &{PoolState}, + amount: UFix64, + minterRef: &MOET.Minter? + ): @{FungibleToken.Vault} { + let reserveVault = state.borrowOrCreateReserve(self.tokenType) + return <- reserveVault.withdraw(amount: amount) + } + + access(all) fun withdrawCollateral( + state: auth(EImplementation) &{PoolState}, + amount: UFix64 + ): @{FungibleToken.Vault} { + let reserveVault = state.borrowOrCreateReserve(self.tokenType) + return <- reserveVault.withdraw(amount: amount) + } + } + + /// MoetTokenReserveHandler + /// + /// Special implementation of TokenReserveHandler for MOET tokens. + /// - Collateral deposits/withdrawals use reserve vaults (standard behavior) + /// - Debt repayments BURN the MOET tokens (reducing supply) + /// - Debt withdrawals MINT new MOET tokens (increasing supply) + access(all) struct MoetTokenReserveHandler: TokenReserveHandler { + + init() {} + + access(all) view fun getTokenType(): Type { + return Type<@MOET.Vault>() + } + + access(all) fun depositCollateral( + state: auth(EImplementation) &{PoolState}, + from: @{FungibleToken.Vault} + ): UFix64 { + let amount = from.balance + let reserveVault = state.borrowOrCreateReserve(Type<@MOET.Vault>()) + reserveVault.deposit(from: <-from) + return amount + } + + access(all) fun depositRepayment( + state: auth(EImplementation) &{PoolState}, + from: @{FungibleToken.Vault} + ): UFix64 { + // Repayments burn MOET tokens to reduce supply + let amount = from.balance + Burner.burn(<-from) + return amount + } + + access(all) fun withdrawDebt( + state: auth(EImplementation) &{PoolState}, + amount: UFix64, + minterRef: &MOET.Minter? + ): @{FungibleToken.Vault} { + // Debt withdrawals mint new MOET tokens + assert(minterRef != nil, message: "MOET Minter reference required for debt withdrawal") + return <- minterRef!.mintTokens(amount: amount) + } + + access(all) fun withdrawCollateral( + state: auth(EImplementation) &{PoolState}, + amount: UFix64 + ): @{FungibleToken.Vault} { + let reserveVault = state.borrowOrCreateReserve(Type<@MOET.Vault>()) + return <- reserveVault.withdraw(amount: amount) + } + } + /// Risk parameters for a token used in effective collateral/debt computations. /// The collateral and borrow factors are fractional values which represent a discount to the "true/market" value of the token. /// The size of this discount indicates a subjective assessment of risk for the token. @@ -1070,6 +1212,10 @@ access(all) contract FlowALPModels { access(EImplementation) fun increaseDebitBalance(by amount: UFix128) /// Decreases total debit balance (floored at 0) and recalculates interest rates. access(EImplementation) fun decreaseDebitBalance(by amount: UFix128) + + /// Returns the reserve operations handler for this token type. + /// Different tokens may have different reserve behaviors (e.g., MOET burns on repayment, others use reserves). + access(all) view fun getReserveOperations(): {TokenReserveHandler} } /// TokenStateImplv1 is the concrete implementation of TokenState. @@ -1141,6 +1287,8 @@ access(all) contract FlowALPModels { /// - A credit balance greater than or equal to M /// - A debit balance greater than or equal to M access(self) var minimumTokenBalancePerPosition: UFix64 + /// The reserve operations handler for this token type + access(self) let reserveHandler: {TokenReserveHandler} init( tokenType: Type, @@ -1169,6 +1317,12 @@ access(all) contract FlowALPModels { self.depositUsage = {} self.lastDepositCapacityUpdate = getCurrentBlock().timestamp self.minimumTokenBalancePerPosition = 1.0 + // Initialize reserve handler based on token type + if tokenType == Type<@MOET.Vault>() { + self.reserveHandler = MoetTokenReserveHandler() + } else { + self.reserveHandler = StandardTokenReserveHandler(tokenType: tokenType) + } } // --- Getters --- @@ -1178,6 +1332,11 @@ access(all) contract FlowALPModels { return self.tokenType } + /// Returns the reserve operations handler for this token type. + access(all) view fun getReserveOperations(): {TokenReserveHandler} { + return self.reserveHandler + } + /// Returns the timestamp at which the TokenState was last updated. access(all) view fun getLastUpdate(): UFix64 { return self.lastUpdate diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 9fc88cec..9e11397e 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -568,8 +568,10 @@ access(all) contract FlowALPv0 { let repayAmount = repayment.balance assert(repayment.getType() == debtType, message: "Vault type mismatch for repay. Repayment type is \(repayment.getType().identifier) but debt type is \(debtType.identifier)") - let debtReserveRef = self.state.borrowOrCreateReserve(debtType) - debtReserveRef.deposit(from: <-repayment) + // Use reserve handler to deposit repayment (burns MOET, deposits to reserves for other tokens) + let repayReserveOps = self.state.getTokenState(debtType)!.getReserveOperations() + let repayStateRef = &self.state as auth(FlowALPModels.EImplementation) &{FlowALPModels.PoolState} + repayReserveOps.depositRepayment(state: repayStateRef, from: <-repayment) // Reduce borrower's debt position by repayAmount let position = self._borrowPosition(pid: pid) @@ -1331,6 +1333,10 @@ access(all) contract FlowALPv0 { } let positionBalance = position.getBalance(type) + // Determine if this is a repayment or collateral deposit + // based on the current balance state + let isRepayment = positionBalance != nil && positionBalance!.direction == FlowALPModels.BalanceDirection.Debit + // If this position doesn't currently have an entry for this token, create one. if positionBalance == nil { // Validate single collateral type constraint @@ -1342,12 +1348,6 @@ access(all) contract FlowALPv0 { )) } - // Create vault if it doesn't exist yet - if !self.state.hasReserve(type) { - self.state.initReserve(type, <-from.createEmptyVault()) - } - let reserveVault = self.state.borrowReserve(type)! - // Reflect the deposit in the position's balance. // // This only records the portion of the deposit that was accepted, not any queued portions, @@ -1364,8 +1364,14 @@ access(all) contract FlowALPv0 { // Only the accepted amount consumes capacity; queued portions will consume capacity when processed later tokenState.consumeDepositCapacity(acceptedAmount, pid: pid) - // Add the money to the reserves - reserveVault.deposit(from: <-from) + // Use reserve handler to deposit (burns MOET repayments, deposits to reserves for collateral/other tokens) + let depositReserveOps = self.state.getTokenState(type)!.getReserveOperations() + let depositStateRef = &self.state as auth(FlowALPModels.EImplementation) &{FlowALPModels.PoolState} + if isRepayment { + depositReserveOps.depositRepayment(state: depositStateRef, from: <-from) + } else { + depositReserveOps.depositCollateral(state: depositStateRef, from: <-from) + } self._queuePositionForUpdateIfNecessary(pid: pid) @@ -1539,6 +1545,9 @@ access(all) contract FlowALPv0 { } var positionBalance = position.getBalance(type) + // Determine if this is a debt withdrawal or collateral withdrawal + // based on the balance state BEFORE recording the withdrawal + let isDebtWithdrawal = positionBalance == nil || positionBalance!.direction == FlowALPModels.BalanceDirection.Debit // If this position doesn't currently have an entry for this token, create one. if positionBalance == nil { @@ -1593,30 +1602,34 @@ access(all) contract FlowALPv0 { // Queue for update if necessary self._queuePositionForUpdateIfNecessary(pid: pid) - // Get tokens either by minting (MOET) or from reserves (other tokens) - var withdrawn: @{FungibleToken.Vault}? <- nil - if type == Type<@MOET.Vault>() { - // For MOET, mint new tokens - withdrawn <-! FlowALPv0._borrowMOETMinter().mintTokens(amount: amount) + // Withdraw via reserve handler (handles MOET mint/burn vs standard reserve operations) + let reserveOps = self.state.getTokenState(type)!.getReserveOperations() + let stateRef = &self.state as auth(FlowALPModels.EImplementation) &{FlowALPModels.PoolState} + var vault: @{FungibleToken.Vault}? <- nil + if isDebtWithdrawal { + vault <-! reserveOps.withdrawDebt( + state: stateRef, + amount: amount, + minterRef: FlowALPv0._borrowMOETMinter() + ) } else { - // For other tokens, withdraw from reserves - assert(self.state.borrowReserve(type) != nil, message: "Cannot withdraw \(type.identifier) - no reserves available") - let reserveVault = self.state.borrowReserve(type)! - assert(reserveVault.balance >= amount, message: "Insufficient reserves for \(type.identifier): need \(amount), have \(reserveVault.balance)") - withdrawn <-! reserveVault.withdraw(amount: amount) + vault <-! reserveOps.withdrawCollateral( + state: stateRef, + amount: amount + ) } - let unwrappedWithdrawn <- withdrawn! + let unwrappedVault <- vault! FlowALPEvents.emitWithdrawn( pid: pid, poolUUID: self.uuid, vaultType: type, - amount: unwrappedWithdrawn.balance, - withdrawnUUID: unwrappedWithdrawn.uuid + amount: unwrappedVault.balance, + withdrawnUUID: unwrappedVault.uuid ) self.unlockPosition(pid) - return <- unwrappedWithdrawn + return <- unwrappedVault } /////////////////////// @@ -2010,18 +2023,15 @@ access(all) contract FlowALPv0 { tokenState: tokenState ) - // Get tokens either by minting (MOET) or from reserves (other tokens) + // Withdraw debt via reserve handler (handles MOET mint vs standard reserve withdrawal) + let sinkReserveOps = self.state.getTokenState(sinkType)!.getReserveOperations() + let sinkStateRef = &self.state as auth(FlowALPModels.EImplementation) &{FlowALPModels.PoolState} var sinkVault: @{FungibleToken.Vault}? <- nil - if sinkType == Type<@MOET.Vault>() { - // For MOET, mint new tokens - sinkVault <-! FlowALPv0._borrowMOETMinter().mintTokens(amount: sinkAmount) - } else { - // For other tokens, withdraw from reserves - assert(self.state.borrowReserve(sinkType) != nil, message: "Cannot withdraw \(sinkAmount) of \(sinkType.identifier) - token not in reserves") - let reserveVault = self.state.borrowReserve(sinkType)! - assert(reserveVault.balance >= sinkAmount, message: "Insufficient reserves for \(sinkType.identifier): available \(reserveVault.balance), needed \(sinkAmount)") - sinkVault <-! reserveVault.withdraw(amount: sinkAmount) - } + sinkVault <-! sinkReserveOps.withdrawDebt( + state: sinkStateRef, + amount: sinkAmount, + minterRef: FlowALPv0._borrowMOETMinter() + ) let unwrappedSinkVault <- sinkVault! FlowALPEvents.emitRebalanced( From e3dcb844a1461efb6804391bdadda58d878a3548 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:37:16 -0500 Subject: [PATCH 12/26] Apply suggestion from @jordanschalm Co-authored-by: Jordan Schalm --- cadence/contracts/FlowALPv0.cdc | 6 ------ 1 file changed, 6 deletions(-) diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 9e11397e..f4b781b7 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -577,12 +577,6 @@ access(all) contract FlowALPv0 { let position = self._borrowPosition(pid: pid) let debtState = self._borrowUpdatedTokenState(type: debtType) - if position.getBalance(debtType) == nil { - // Liquidation is repaying debt - validate single debt type - position.validateDebtType(debtType) - - position.setBalance(debtType, FlowALPModels.InternalBalance(direction: FlowALPModels.BalanceDirection.Debit, scaledBalance: 0.0)) - } position.borrowBalance(debtType)!.recordDeposit(amount: UFix128(repayAmount), tokenState: debtState) // Withdraw seized collateral from position and send to liquidator From 8153647d19ad6339ed8751bb4b96b03860d88f73 Mon Sep 17 00:00:00 2001 From: liobrasil Date: Wed, 4 Mar 2026 18:15:10 -0400 Subject: [PATCH 13/26] Fix phantom debt/collateral types after exact zero balances --- cadence/contracts/FlowALPModels.cdc | 10 +- .../debt_type_constraint_three_token_test.cdc | 119 ++++++++++++++++++ 2 files changed, 127 insertions(+), 2 deletions(-) diff --git a/cadence/contracts/FlowALPModels.cdc b/cadence/contracts/FlowALPModels.cdc index 312b8a86..fa600400 100644 --- a/cadence/contracts/FlowALPModels.cdc +++ b/cadence/contracts/FlowALPModels.cdc @@ -2252,7 +2252,10 @@ access(all) contract FlowALPModels { access(all) fun getCollateralTypes(): [Type] { let types: [Type] = [] for type in self.balances.keys { - if self.balances[type]!.direction == BalanceDirection.Credit { + let balance = self.balances[type]! + // Ignore zero balances so exact repay/withdraw operations do not leave + // phantom token-type constraints. + if balance.direction == BalanceDirection.Credit && balance.scaledBalance > 0.0 { types.append(type) } } @@ -2264,7 +2267,10 @@ access(all) contract FlowALPModels { access(all) fun getDebtTypes(): [Type] { let types: [Type] = [] for type in self.balances.keys { - if self.balances[type]!.direction == BalanceDirection.Debit { + let balance = self.balances[type]! + // Ignore zero balances so exact repay/withdraw operations do not leave + // phantom token-type constraints. + if balance.direction == BalanceDirection.Debit && balance.scaledBalance > 0.0 { types.append(type) } } diff --git a/cadence/tests/debt_type_constraint_three_token_test.cdc b/cadence/tests/debt_type_constraint_three_token_test.cdc index 4058e000..18e2c5b3 100644 --- a/cadence/tests/debt_type_constraint_three_token_test.cdc +++ b/cadence/tests/debt_type_constraint_three_token_test.cdc @@ -298,6 +298,125 @@ fun testCanWithdrawCollateralWithDebt() { log("\n=== Test Complete: Collateral Withdrawal Works ===") } +/// Regression: exact debt repayment should clear debt-type constraints. +/// After repaying FLOW debt to exactly zero, borrowing MOET as a new debt type should succeed. +access(all) +fun testExactRepayClearsDebtTypeConstraint() { + Test.reset(to: snapshot) + log("\n=== Test: Exact Repay Clears Debt Type Constraint ===\n") + + // Provide FLOW reserves for initial FLOW borrow. + let flowProvider = Test.createAccount() + setupMoetVault(flowProvider, beFailed: false) + transferFlowTokens(to: flowProvider, amount: 10_000.0) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, flowProvider) + + let createFlowPos = executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [5_000.0, FLOW_VAULT_STORAGE_PATH, false], + flowProvider + ) + Test.expect(createFlowPos, Test.beSucceeded()) + + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + setupDummyTokenVault(user) + mintDummyToken(to: user, amount: 10_000.0) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) + + let createPosRes = executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [5_000.0, DummyToken.VaultStoragePath, false], + user + ) + Test.expect(createPosRes, Test.beSucceeded()) + + let pid: UInt64 = 1 + + // Create FLOW debt, then repay exactly to zero. + borrowFromPosition( + signer: user, + positionId: pid, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + amount: 300.0, + beFailed: false + ) + depositToPosition( + signer: user, + positionID: pid, + amount: 300.0, + vaultStoragePath: FLOW_VAULT_STORAGE_PATH, + pushToDrawDownSink: false + ) + + // If exact repay leaves a phantom FLOW debt type, this borrow would fail. + borrowFromPosition( + signer: user, + positionId: pid, + tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + amount: 100.0, + beFailed: false + ) + + let details = getPositionDetails(pid: pid, beFailed: false) + let flowDebt = getDebitBalanceForType(details: details, vaultType: CompositeType(FLOW_TOKEN_IDENTIFIER)!) + let moetDebt = getDebitBalanceForType(details: details, vaultType: CompositeType(MOET_TOKEN_IDENTIFIER)!) + Test.assert(flowDebt == 0.0, message: "FLOW debt should be zero after exact repay") + Test.assert(moetDebt >= 100.0 - 0.01, message: "MOET debt should be ~100 after new borrow") + + log("\n=== Test Complete: Exact Repay Clears Debt Type Constraint ===") +} + +/// Regression: exact full collateral withdrawal should clear collateral-type constraints. +/// After withdrawing FLOW collateral to exactly zero, depositing Dummy collateral should succeed. +access(all) +fun testExactFullWithdrawClearsCollateralTypeConstraint() { + Test.reset(to: snapshot) + log("\n=== Test: Exact Full Withdraw Clears Collateral Type Constraint ===\n") + + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + setupDummyTokenVault(user) + transferFlowTokens(to: user, amount: 2_000.0) + mintDummyToken(to: user, amount: 2_000.0) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) + + let createPosRes = executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [1_000.0, FLOW_VAULT_STORAGE_PATH, false], + user + ) + Test.expect(createPosRes, Test.beSucceeded()) + + let pid: UInt64 = 0 + + // Withdraw collateral exactly to zero. + withdrawFromPosition( + signer: user, + positionId: pid, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + amount: 1_000.0, + pullFromTopUpSource: false + ) + + // If exact full withdraw leaves a phantom FLOW collateral type, this deposit would fail. + depositToPosition( + signer: user, + positionID: pid, + amount: 500.0, + vaultStoragePath: DummyToken.VaultStoragePath, + pushToDrawDownSink: false + ) + + let details = getPositionDetails(pid: pid, beFailed: false) + let flowCredit = getCreditBalanceForType(details: details, vaultType: CompositeType(FLOW_TOKEN_IDENTIFIER)!) + let dummyCredit = getCreditBalanceForType(details: details, vaultType: CompositeType(DUMMY_TOKEN_IDENTIFIER)!) + Test.assert(flowCredit == 0.0, message: "FLOW collateral should be zero after full withdrawal") + Test.assert(dummyCredit >= 500.0 - 0.01, message: "Dummy collateral should be ~500 after deposit") + + log("\n=== Test Complete: Exact Full Withdraw Clears Collateral Type Constraint ===") +} + // Helper functions access(all) From 368dbaa3424ca090f22d09220118f6fd7ce5e16b Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 4 Mar 2026 17:35:48 -0500 Subject: [PATCH 14/26] trim tests --- .../debt_type_constraint_simple_test.cdc | 113 -------------- .../debt_type_constraint_three_token_test.cdc | 143 ------------------ .../tests/single_token_constraint_test.cdc | 36 ----- 3 files changed, 292 deletions(-) delete mode 100644 cadence/tests/debt_type_constraint_simple_test.cdc diff --git a/cadence/tests/debt_type_constraint_simple_test.cdc b/cadence/tests/debt_type_constraint_simple_test.cdc deleted file mode 100644 index ff0175c5..00000000 --- a/cadence/tests/debt_type_constraint_simple_test.cdc +++ /dev/null @@ -1,113 +0,0 @@ -import Test -import BlockchainHelpers - -import "MOET" -import "FlowToken" -import "FlowALPv0" -import "test_helpers.cdc" - -/// Simple test to verify debt type constraint is enforced - -access(all) -fun setup() { - deployContracts() - - // Setup oracle prices - setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0) - setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: MOET_TOKEN_IDENTIFIER, price: 1.0) - - // Create pool with MOET as default token - createAndStorePool(signer: PROTOCOL_ACCOUNT, defaultTokenIdentifier: MOET_TOKEN_IDENTIFIER, beFailed: false) - - // Add FLOW as supported token - addSupportedTokenZeroRateCurve( - signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, - collateralFactor: 0.8, - borrowFactor: 0.77, - depositRate: 1_000_000.0, - depositCapacityCap: 1_000_000.0 - ) -} - -/// Test that a position with FLOW debt cannot borrow MOET (second debt type) -access(all) -fun testDebtTypeConstraint() { - log("=== Test: Debt Type Constraint ===") - - // Create user1 to provide FLOW reserves - let user1 = Test.createAccount() - setupMoetVault(user1, beFailed: false) - transferFlowTokens(to: user1, amount: 10_000.0) - grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user1) - - // User1 deposits FLOW (creates reserves) - let createPos1 = executeTransaction( - "../transactions/flow-alp/position/create_position.cdc", - [5_000.0, FLOW_VAULT_STORAGE_PATH, false], - user1 - ) - Test.expect(createPos1, Test.beSucceeded()) - log("✓ User1 deposited 5000 FLOW to create reserves") - - // Create user2 with MOET collateral - let user2 = Test.createAccount() - setupMoetVault(user2, beFailed: false) - mintMoet(signer: PROTOCOL_ACCOUNT, to: user2.address, amount: 10_000.0, beFailed: false) - grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user2) - - // User2 creates position with MOET collateral - let createPos2 = executeTransaction( - "../transactions/flow-alp/position/create_position.cdc", - [5_000.0, MOET.VaultStoragePath, false], - user2 - ) - Test.expect(createPos2, Test.beSucceeded()) - log("✓ User2 created position with 5000 MOET collateral") - - let pid: UInt64 = 1 // User2's position - - // User2 borrows FLOW (first debt type, from reserves) - borrowFromPosition( - signer: user2, - positionId: pid, - tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, - amount: 1_000.0, - beFailed: false - ) - log("✓ User2 borrowed 1000 FLOW (first debt type)") - - // Verify User2 has FLOW debt - let details = getPositionDetails(pid: pid, beFailed: false) - let flowDebt = getDebitBalanceForType(details: details, vaultType: CompositeType(FLOW_TOKEN_IDENTIFIER)!) - Test.assert(flowDebt >= 1_000.0 - 0.01, message: "User2 should have ~1000 FLOW debt") - log("✓ User2 has FLOW debt: ".concat(flowDebt.toString())) - - // User2 tries to borrow MOET (second debt type, via minting) - // Need to withdraw MORE than collateral amount to flip MOET from Credit to Debit - // This should FAIL with debt type constraint error - let borrowMoet = executeTransaction( - "./transactions/position-manager/borrow_from_position.cdc", - [pid, MOET_TOKEN_IDENTIFIER, 6_000.0], // More than 5000 collateral - user2 - ) - - // Check if it failed - if borrowMoet.status == Test.ResultStatus.succeeded { - log("❌ ERROR: Borrowing MOET should have failed but succeeded!") - log("❌ Debt type constraint is NOT enforced") - Test.assert(false, message: "Debt type constraint should prevent borrowing MOET after FLOW") - } else { - log("✓ Borrowing MOET correctly failed") - - // Check error message - let errorMsg = borrowMoet.error?.message ?? "" - if errorMsg.contains("debt type") || errorMsg.contains("Only one debt type") { - log("✓ Error message mentions debt type constraint: ".concat(errorMsg)) - } else { - log("⚠ Warning: Error message doesn't mention debt type: ".concat(errorMsg)) - } - } - - log("=== Test Complete ===\n") -} diff --git a/cadence/tests/debt_type_constraint_three_token_test.cdc b/cadence/tests/debt_type_constraint_three_token_test.cdc index 4058e000..ff82baf9 100644 --- a/cadence/tests/debt_type_constraint_three_token_test.cdc +++ b/cadence/tests/debt_type_constraint_three_token_test.cdc @@ -155,149 +155,6 @@ fun testCannotBorrowSecondDebtType() { log("\n=== Test Complete: Debt Type Constraint Verified ===") } -/// Test that multiple borrows of the SAME debt type still work -access(all) -fun testCanBorrowSameDebtTypeMultipleTimes() { - Test.reset(to: snapshot) - log("\n=== Test: Can Borrow Same Debt Type Multiple Times (3 Tokens) ===\n") - - // Setup FLOW reserves - let flowProvider = Test.createAccount() - setupMoetVault(flowProvider, beFailed: false) - transferFlowTokens(to: flowProvider, amount: 10_000.0) - grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, flowProvider) - - let createFlowPos = executeTransaction( - "../transactions/flow-alp/position/create_position.cdc", - [5_000.0, FLOW_VAULT_STORAGE_PATH, false], - flowProvider - ) - Test.expect(createFlowPos, Test.beSucceeded()) - log("✓ FlowProvider deposited 5000 FLOW (creates reserves)") - - // Main user - let user = Test.createAccount() - setupMoetVault(user, beFailed: false) - setupDummyTokenVault(user) - mintDummyToken(to: user, amount: 10_000.0) - grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) - - // Create position with DummyToken collateral - let createPosRes = executeTransaction( - "../transactions/flow-alp/position/create_position.cdc", - [5_000.0, DummyToken.VaultStoragePath, false], - user - ) - Test.expect(createPosRes, Test.beSucceeded()) - log("✓ User created position with 5000 DummyToken collateral") - - let pid: UInt64 = 1 - - // Borrow FLOW (first time) - borrowFromPosition( - signer: user, - positionId: pid, - tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, - amount: 500.0, - beFailed: false - ) - log("✓ Borrowed 500 FLOW (first borrow)") - - // Borrow FLOW (second time) - should SUCCEED - borrowFromPosition( - signer: user, - positionId: pid, - tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, - amount: 300.0, - beFailed: false - ) - log("✓ Borrowed 300 more FLOW (second borrow - same type)") - - // Borrow FLOW (third time) - should SUCCEED - borrowFromPosition( - signer: user, - positionId: pid, - tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, - amount: 200.0, - beFailed: false - ) - log("✓ Borrowed 200 more FLOW (third borrow - same type)") - - // Verify total FLOW debt - let details = getPositionDetails(pid: pid, beFailed: false) - let flowDebt = getDebitBalanceForType(details: details, vaultType: CompositeType(FLOW_TOKEN_IDENTIFIER)!) - Test.assert(flowDebt >= 1_000.0 - 0.01, message: "Should have ~1000 total FLOW debt") - log("✓ Total FLOW debt: ".concat(flowDebt.toString())) - - log("\n=== Test Complete: Same Debt Type Borrowing Works ===") -} - -/// Test that withdrawing collateral while having debt works -access(all) -fun testCanWithdrawCollateralWithDebt() { - Test.reset(to: snapshot) - log("\n=== Test: Can Withdraw Collateral While Having Debt (3 Tokens) ===\n") - - // Setup FLOW reserves - let flowProvider = Test.createAccount() - setupMoetVault(flowProvider, beFailed: false) - transferFlowTokens(to: flowProvider, amount: 10_000.0) - grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, flowProvider) - - let createFlowPos = executeTransaction( - "../transactions/flow-alp/position/create_position.cdc", - [5_000.0, FLOW_VAULT_STORAGE_PATH, false], - flowProvider - ) - Test.expect(createFlowPos, Test.beSucceeded()) - log("✓ FlowProvider deposited 5000 FLOW") - - // Main user - let user = Test.createAccount() - setupMoetVault(user, beFailed: false) - setupDummyTokenVault(user) - mintDummyToken(to: user, amount: 10_000.0) - grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) - - // Create position with DummyToken collateral - let createPosRes = executeTransaction( - "../transactions/flow-alp/position/create_position.cdc", - [5_000.0, DummyToken.VaultStoragePath, false], - user - ) - Test.expect(createPosRes, Test.beSucceeded()) - log("✓ User created position with 5000 DummyToken collateral") - - let pid: UInt64 = 1 - - // Borrow FLOW - borrowFromPosition( - signer: user, - positionId: pid, - tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, - amount: 1_000.0, - beFailed: false - ) - log("✓ Borrowed 1000 FLOW") - - // Withdraw some DummyToken collateral while debt exists - withdrawFromPosition( - signer: user, - positionId: pid, - tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER, - amount: 1_000.0, - pullFromTopUpSource: false - ) - log("✓ Withdrew 1000 DummyToken collateral (while having FLOW debt)") - - // Verify position is still healthy - let health = getPositionHealth(pid: pid, beFailed: false) - log("✓ Position health after withdrawal: ".concat(health.toString())) - Test.assert(health >= UFix128(1.1), message: "Position should still be healthy") - - log("\n=== Test Complete: Collateral Withdrawal Works ===") -} - // Helper functions access(all) diff --git a/cadence/tests/single_token_constraint_test.cdc b/cadence/tests/single_token_constraint_test.cdc index 7ded39ca..fe50d83c 100644 --- a/cadence/tests/single_token_constraint_test.cdc +++ b/cadence/tests/single_token_constraint_test.cdc @@ -169,42 +169,6 @@ fun testCannotAddSecondDebtType() { log("=== Test Passed: Borrowing Collateral Token Reduces Credit, Not Creates Second Debit ===\n") } -/// Test that a position with MOET collateral cannot add FLOW collateral -access(all) -fun testCannotAddFlowToMoetCollateral() { - Test.reset(to: snapshot) - log("=== Test: Cannot Add FLOW to MOET Collateral ===") - - // Create user with both tokens - let user = Test.createAccount() - setupMoetVault(user, beFailed: false) - transferFlowTokens(to: user, amount: 2_000.0) - mintMoet(signer: PROTOCOL_ACCOUNT, to: user.address, amount: 2_000.0, beFailed: false) - grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) - - // Create position with MOET collateral - let createPosRes = executeTransaction( - "../transactions/flow-alp/position/create_position.cdc", - [1_000.0, MOET.VaultStoragePath, false], - user - ) - Test.expect(createPosRes, Test.beSucceeded()) - log("✓ Created position with MOET collateral") - - let pid: UInt64 = 0 - - // Try to deposit FLOW to the same position - should FAIL - let depositFlowRes = executeTransaction( - "./transactions/position-manager/deposit_to_position.cdc", - [pid, 500.0, FLOW_VAULT_STORAGE_PATH, false], - user - ) - Test.expect(depositFlowRes, Test.beFailed()) - log("✓ Depositing FLOW to MOET-collateral position correctly failed") - - log("=== Test Passed: Cannot Add FLOW to MOET Collateral ===\n") -} - /// Test that a position with FLOW collateral and MOET debt can withdraw both token types /// (Withdraw FLOW = reduce collateral, Withdraw MOET = borrow more debt) access(all) From a414472b4a12ef44c73f7e75b631737bea9b29fe Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 4 Mar 2026 17:39:32 -0500 Subject: [PATCH 15/26] fix test --- .../debt_type_constraint_three_token_test.cdc | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/cadence/tests/debt_type_constraint_three_token_test.cdc b/cadence/tests/debt_type_constraint_three_token_test.cdc index ff82baf9..678bc6c5 100644 --- a/cadence/tests/debt_type_constraint_three_token_test.cdc +++ b/cadence/tests/debt_type_constraint_three_token_test.cdc @@ -135,22 +135,8 @@ fun testCannotBorrowSecondDebtType() { user ) - // Verify it FAILED - if borrowMoetRes.status == Test.ResultStatus.succeeded { - log("❌ ERROR: Borrowing MOET should have failed but succeeded!") - log("❌ Debt type constraint is NOT enforced!") - Test.assert(false, message: "Should not be able to borrow MOET after already having FLOW debt") - } else { - log("✅ Borrowing MOET correctly FAILED") - - // Check error message - let errorMsg = borrowMoetRes.error?.message ?? "" - if errorMsg.contains("debt type") || errorMsg.contains("Only one debt type") { - log("✅ Error message mentions debt type constraint") - } else { - log("⚠️ Warning: Error message doesn't clearly mention debt type constraint") - } - } + Test.expect(borrowMoetRes, Test.beFailed()) + Test.assertError(borrowMoetRes, errorMessage: "debt type") log("\n=== Test Complete: Debt Type Constraint Verified ===") } From ebffab08b5624c59a44cac73a33ebce7ced8fd52 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 4 Mar 2026 21:06:22 -0500 Subject: [PATCH 16/26] move around --- cadence/contracts/FlowALPModels.cdc | 66 ++++++----------------------- cadence/contracts/FlowALPv0.cdc | 41 +++++++++--------- 2 files changed, 34 insertions(+), 73 deletions(-) diff --git a/cadence/contracts/FlowALPModels.cdc b/cadence/contracts/FlowALPModels.cdc index fa600400..58521002 100644 --- a/cadence/contracts/FlowALPModels.cdc +++ b/cadence/contracts/FlowALPModels.cdc @@ -2071,13 +2071,10 @@ access(all) contract FlowALPModels { /// Returns empty array if no Debit balance exists yet (allows any debt type for first borrow) access(all) fun getDebtTypes(): [Type] - /// Validates that the given token type can be used as collateral for this position - /// Panics if position already has a different collateral type - access(EImplementation) fun validateCollateralType(_ type: Type) - - /// Validates that the given token type can be used as debt for this position - /// Panics if position already has a different debt type - access(EImplementation) fun validateDebtType(_ type: Type) + /// Temporary constraint: one collateral type and one debt type per position. + /// Used as a post-condition on all functions that mutate position balances. + /// This restriction will be lifted in a future protocol version. + access(all) view fun satisfiesTemporaryBalanceConstraint(): Bool } /// InternalPositionImplv1 is the concrete implementation of InternalPosition. @@ -2277,52 +2274,17 @@ access(all) contract FlowALPModels { return types } - /// Validates that the given token type can be used as collateral for this position - /// Panics if position already has a different collateral type - access(EImplementation) fun validateCollateralType(_ type: Type) { - let existingTypes = self.getCollateralTypes() - - // Constraint: For now, only one collateral type is allowed per position - // This assertion ensures the invariant is maintained - assert(existingTypes.length <= 1, message: "Internal error: Position has multiple collateral types") - - if existingTypes.length == 0 { - // No collateral yet, allow any type - return - } - - // Check if type already exists (idempotent) - if existingTypes.contains(type) { - return - } - - // For now, only one collateral type is allowed per position - // This restriction can be removed in the future to support multiple collateral types - panic("Position already has collateral type ".concat(existingTypes[0].identifier).concat(". Cannot deposit ").concat(type.identifier).concat(". Only one collateral type allowed per position.")) - } - - /// Validates that the given token type can be used as debt for this position - /// Panics if position already has a different debt type - access(EImplementation) fun validateDebtType(_ type: Type) { - let existingTypes = self.getDebtTypes() - - // Constraint: For now, only one debt type is allowed per position - // This assertion ensures the invariant is maintained - assert(existingTypes.length <= 1, message: "Internal error: Position has multiple debt types") - - if existingTypes.length == 0 { - // No debt yet, allow any type - return - } - - // Check if type already exists (idempotent) - if existingTypes.contains(type) { - return + access(all) view fun satisfiesTemporaryBalanceConstraint(): Bool { + var creditCount: Int = 0 + var debitCount: Int = 0 + for key in self.balances.keys { + if self.balances[key]!.direction == BalanceDirection.Credit { + creditCount = creditCount + 1 + } else { + debitCount = debitCount + 1 + } } - - // For now, only one debt type is allowed per position - // This restriction can be removed in the future to support multiple debt types - panic("Position already has debt type ".concat(existingTypes[0].identifier).concat(". Cannot borrow ").concat(type.identifier).concat(". Only one debt type allowed per position.")) + return creditCount <= 1 && debitCount <= 1 } } diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index f4b781b7..0903be33 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -563,7 +563,10 @@ 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 + } + post { + self._borrowPosition(pid: pid).satisfiesTemporaryBalanceConstraint(): "Position \(pid) violates temporary balance constraint: only one collateral type and one debt type are allowed per position" } let repayAmount = repayment.balance @@ -583,9 +586,6 @@ access(all) contract FlowALPv0 { let seizeState = self._borrowUpdatedTokenState(type: seizeType) let positionBalance = position.getBalance(seizeType) if positionBalance == nil { - // Liquidation is seizing collateral - validate single collateral type - position.validateCollateralType(seizeType) - position.setBalance(seizeType, FlowALPModels.InternalBalance(direction: FlowALPModels.BalanceDirection.Credit, scaledBalance: 0.0)) } @@ -1286,6 +1286,13 @@ access(all) contract FlowALPv0 { pre { !self.isPaused(): "Withdrawal, deposits, and liquidations are paused by governance" } + post { + // Only enforce when a new balance entry is being created. + // Overpayment that flips an existing Debit→Credit on the same token is allowed. + before(self._borrowPosition(pid: pid).getBalance(from.getType())) != nil + || self._borrowPosition(pid: pid).satisfiesTemporaryBalanceConstraint(): + "Position \(pid) violates temporary balance constraint: only one collateral type and one debt type are allowed per position" + } // NOTE: caller must have already validated pid + token support let amount = from.balance if amount == 0.0 { @@ -1333,9 +1340,6 @@ access(all) contract FlowALPv0 { // If this position doesn't currently have an entry for this token, create one. if positionBalance == nil { - // Validate single collateral type constraint - position.validateCollateralType(type) - position.setBalance(type, FlowALPModels.InternalBalance( direction: FlowALPModels.BalanceDirection.Credit, scaledBalance: 0.0 @@ -1452,6 +1456,12 @@ access(all) contract FlowALPv0 { } post { !self.state.isPositionLocked(pid): "Position is not unlocked" + // Allow the withdrawal if the constraint was already violated before it ran + // (e.g. debt overpayment flipped a Debit→Credit in a prior call within the same tx). + // Only reject withdrawals that introduce a NEW constraint violation. + !before(self._borrowPosition(pid: pid).satisfiesTemporaryBalanceConstraint()) + || self._borrowPosition(pid: pid).satisfiesTemporaryBalanceConstraint(): + "Position \(pid) violates temporary balance constraint: only one collateral type and one debt type are allowed per position" } self.lockPosition(pid) if self.config.isDebugLogging() { @@ -1545,10 +1555,6 @@ access(all) contract FlowALPv0 { // If this position doesn't currently have an entry for this token, create one. if positionBalance == nil { - // When withdrawing a token that doesn't exist in position yet, - // we'll be creating a debit (debt). Validate single debt type. - position.validateDebtType(type) - position.setBalance(type, FlowALPModels.InternalBalance( direction: FlowALPModels.BalanceDirection.Credit, scaledBalance: 0.0 @@ -1559,19 +1565,12 @@ access(all) contract FlowALPv0 { } // Reflect the withdrawal in the position's balance - let wasCredit = positionBalance!.direction == FlowALPModels.BalanceDirection.Credit let uintAmount = UFix128(amount) position.borrowBalance(type)!.recordWithdrawal( amount: uintAmount, tokenState: tokenState ) - // If we flipped from Credit to Debit, validate debt type constraint - // Re-fetch balance to check if direction changed - positionBalance = position.getBalance(type) - if wasCredit && positionBalance!.direction == FlowALPModels.BalanceDirection.Debit { - position.validateDebtType(type) - } // Attempt to pull additional collateral from the top-up source (if configured) // to keep the position above minHealth after the withdrawal. // Regardless of whether a top-up occurs, the position must be healthy post-withdrawal. @@ -1935,6 +1934,9 @@ access(all) contract FlowALPv0 { pre { !self.isPaused(): "Withdrawal, deposits, and liquidations are paused by governance" } + post { + self._borrowPosition(pid: pid).satisfiesTemporaryBalanceConstraint(): "Position \(pid) violates temporary balance constraint: only one collateral type and one debt type are allowed per position" + } if self.config.isDebugLogging() { log(" [CONTRACT] rebalancePosition(pid: \(pid), force: \(force))") } @@ -2002,9 +2004,6 @@ access(all) contract FlowALPv0 { if sinkAmount > 0.0 { let tokenState = self._borrowUpdatedTokenState(type: sinkType) if position.getBalance(sinkType) == nil { - // Rebalancing is borrowing/withdrawing to push to sink - validate single debt type - position.validateDebtType(sinkType) - position.setBalance(sinkType, FlowALPModels.InternalBalance( direction: FlowALPModels.BalanceDirection.Credit, scaledBalance: 0.0 From 5d9b84c9ea8fccfa8fcd983eccef28859b91212d Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 4 Mar 2026 22:15:22 -0500 Subject: [PATCH 17/26] address comments --- cadence/contracts/FlowALPModels.cdc | 45 ++--------------------------- cadence/contracts/FlowALPv0.cdc | 3 -- 2 files changed, 3 insertions(+), 45 deletions(-) diff --git a/cadence/contracts/FlowALPModels.cdc b/cadence/contracts/FlowALPModels.cdc index 58521002..cd9b1a24 100644 --- a/cadence/contracts/FlowALPModels.cdc +++ b/cadence/contracts/FlowALPModels.cdc @@ -341,8 +341,6 @@ access(all) contract FlowALPModels { /// - Debt withdrawals MINT new MOET tokens (increasing supply) access(all) struct MoetTokenReserveHandler: TokenReserveHandler { - init() {} - access(all) view fun getTokenType(): Type { return Type<@MOET.Vault>() } @@ -2063,14 +2061,6 @@ access(all) contract FlowALPModels { /// If nil, the Pool will not pull underflown value, and liquidation may occur. access(EImplementation) fun setTopUpSource(_ source: {DeFiActions.Source}?) - /// Returns the current collateral types for this position based on existing Credit balances - /// Returns empty array if no Credit balance exists yet (allows any collateral type for first deposit) - access(all) fun getCollateralTypes(): [Type] - - /// Returns the current debt types for this position based on existing Debit balances - /// Returns empty array if no Debit balance exists yet (allows any debt type for first borrow) - access(all) fun getDebtTypes(): [Type] - /// Temporary constraint: one collateral type and one debt type per position. /// Used as a post-condition on all functions that mutate position balances. /// This restriction will be lifted in a future protocol version. @@ -2244,43 +2234,14 @@ access(all) contract FlowALPModels { self.topUpSource = source } - /// Returns the current collateral types for this position based on existing Credit balances - /// Returns empty array if no Credit balance exists yet (allows any collateral type for first deposit) - access(all) fun getCollateralTypes(): [Type] { - let types: [Type] = [] - for type in self.balances.keys { - let balance = self.balances[type]! - // Ignore zero balances so exact repay/withdraw operations do not leave - // phantom token-type constraints. - if balance.direction == BalanceDirection.Credit && balance.scaledBalance > 0.0 { - types.append(type) - } - } - return types - } - - /// Returns the current debt types for this position based on existing Debit balances - /// Returns empty array if no Debit balance exists yet (allows any debt type for first borrow) - access(all) fun getDebtTypes(): [Type] { - let types: [Type] = [] - for type in self.balances.keys { - let balance = self.balances[type]! - // Ignore zero balances so exact repay/withdraw operations do not leave - // phantom token-type constraints. - if balance.direction == BalanceDirection.Debit && balance.scaledBalance > 0.0 { - types.append(type) - } - } - return types - } - access(all) view fun satisfiesTemporaryBalanceConstraint(): Bool { var creditCount: Int = 0 var debitCount: Int = 0 for key in self.balances.keys { - if self.balances[key]!.direction == BalanceDirection.Credit { + let balance = self.balances[key]! + if balance.direction == BalanceDirection.Credit && balance.scaledBalance > UFix128(0) { creditCount = creditCount + 1 - } else { + } else if balance.direction == BalanceDirection.Debit { debitCount = debitCount + 1 } } diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 0903be33..763045f2 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -585,9 +585,6 @@ access(all) contract FlowALPv0 { // Withdraw seized collateral from position and send to liquidator let seizeState = self._borrowUpdatedTokenState(type: seizeType) let positionBalance = position.getBalance(seizeType) - if positionBalance == nil { - position.setBalance(seizeType, FlowALPModels.InternalBalance(direction: FlowALPModels.BalanceDirection.Credit, scaledBalance: 0.0)) - } position.borrowBalance(seizeType)!.recordWithdrawal(amount: UFix128(seizeAmount), tokenState: seizeState) let seizeReserveRef = self.state.borrowReserve(seizeType)! From af6b9d786df64f483e4497b1d4aace49aa037cdb Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 4 Mar 2026 23:41:07 -0500 Subject: [PATCH 18/26] fix test --- cadence/tests/debt_type_constraint_three_token_test.cdc | 5 ++++- cadence/tests/multi_token_reserve_borrowing_test.cdc | 2 ++ cadence/tests/single_token_constraint_test.cdc | 8 ++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/cadence/tests/debt_type_constraint_three_token_test.cdc b/cadence/tests/debt_type_constraint_three_token_test.cdc index ef46af22..8d0dabd8 100644 --- a/cadence/tests/debt_type_constraint_three_token_test.cdc +++ b/cadence/tests/debt_type_constraint_three_token_test.cdc @@ -106,6 +106,7 @@ fun testCannotBorrowSecondDebtType() { signer: user, positionId: pid, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: 1_000.0, beFailed: false ) @@ -131,7 +132,7 @@ fun testCannotBorrowSecondDebtType() { let borrowMoetRes = executeTransaction( "./transactions/position-manager/borrow_from_position.cdc", - [pid, MOET_TOKEN_IDENTIFIER, 500.0], + [pid, MOET_TOKEN_IDENTIFIER, MOET.VaultStoragePath, 500.0], user ) @@ -181,6 +182,7 @@ fun testExactRepayClearsDebtTypeConstraint() { signer: user, positionId: pid, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: 300.0, beFailed: false ) @@ -197,6 +199,7 @@ fun testExactRepayClearsDebtTypeConstraint() { signer: user, positionId: pid, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + vaultStoragePath: MOET.VaultStoragePath, amount: 100.0, beFailed: false ) diff --git a/cadence/tests/multi_token_reserve_borrowing_test.cdc b/cadence/tests/multi_token_reserve_borrowing_test.cdc index b4c093af..96be9313 100644 --- a/cadence/tests/multi_token_reserve_borrowing_test.cdc +++ b/cadence/tests/multi_token_reserve_borrowing_test.cdc @@ -78,6 +78,7 @@ fun testMultiTokenReserveBorrowing() { signer: user1, positionId: pid1, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + vaultStoragePath: MOET.VaultStoragePath, amount: user1MoetBorrowAmount, beFailed: false ) @@ -132,6 +133,7 @@ fun testMultiTokenReserveBorrowing() { signer: user2, positionId: pid2, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: user2FlowBorrowAmount, beFailed: false ) diff --git a/cadence/tests/single_token_constraint_test.cdc b/cadence/tests/single_token_constraint_test.cdc index fe50d83c..f9939757 100644 --- a/cadence/tests/single_token_constraint_test.cdc +++ b/cadence/tests/single_token_constraint_test.cdc @@ -133,6 +133,7 @@ fun testCannotAddSecondDebtType() { signer: user2, positionId: pid, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: 300.0, beFailed: false ) @@ -152,6 +153,7 @@ fun testCannotAddSecondDebtType() { signer: user2, positionId: pid, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + vaultStoragePath: MOET.VaultStoragePath, amount: 200.0, beFailed: false // SUCCEEDS: draws from existing MOET Credit, no new Debit created ) @@ -198,6 +200,7 @@ fun testCanWithdrawBothCollateralAndDebtTokens() { signer: user, positionId: pid, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + vaultStoragePath: MOET.VaultStoragePath, amount: 500.0, beFailed: false ) @@ -233,6 +236,7 @@ fun testCanWithdrawBothCollateralAndDebtTokens() { signer: user, positionId: pid, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + vaultStoragePath: MOET.VaultStoragePath, amount: 200.0, beFailed: false ) @@ -295,6 +299,7 @@ fun testCannotWithdrawCollateralBeyondBalanceWithDifferentDebt() { signer: user2, positionId: pid, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: 30.0, beFailed: false ) @@ -414,6 +419,7 @@ fun testMultipleBorrowsOfSameDebtTypeSucceed() { signer: user, positionId: pid, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + vaultStoragePath: MOET.VaultStoragePath, amount: 300.0, beFailed: false ) @@ -429,6 +435,7 @@ fun testMultipleBorrowsOfSameDebtTypeSucceed() { signer: user, positionId: pid, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + vaultStoragePath: MOET.VaultStoragePath, amount: 200.0, beFailed: false ) @@ -445,6 +452,7 @@ fun testMultipleBorrowsOfSameDebtTypeSucceed() { signer: user, positionId: pid, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + vaultStoragePath: MOET.VaultStoragePath, amount: 100.0, beFailed: false ) From 50584882260cd4e3250465e1562bcf7c0e0c77dd Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Thu, 5 Mar 2026 09:33:27 -0500 Subject: [PATCH 19/26] skip multi collateral tests --- flow.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/flow.json b/flow.json index b0ebccae..c27bccc2 100644 --- a/flow.json +++ b/flow.json @@ -52,7 +52,8 @@ "DummyToken": { "source": "./cadence/tests/contracts/DummyToken.cdc", "aliases": { - "testing": "0000000000000007" + "testing": "0000000000000007", + "mainnet-fork": "6d888f175c158410" } }, "FlowALPEvents": { @@ -73,6 +74,7 @@ "source": "./cadence/lib/FlowALPMath.cdc", "aliases": { "testing": "0000000000000007", + "mainnet": "6b00ff876c299c61", "mainnet-fork": "6b00ff876c299c61" } }, @@ -80,7 +82,7 @@ "source": "./cadence/contracts/FlowALPModels.cdc", "aliases": { "testing": "0000000000000007", - "mainnet": "6d888f175c158410" + "mainnet-fork": "6b00ff876c299c61" } }, "FlowALPRebalancerPaidv1": { @@ -462,4 +464,4 @@ ] } } -} \ No newline at end of file +} From 9fbd813db4a4eae7790de497fb17491c8f70feda Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Thu, 5 Mar 2026 10:07:49 -0500 Subject: [PATCH 20/26] skip test --- ...tion_test.cdc => fork_multi_collateral_position_test_skip.cdc} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename cadence/tests/{fork_multi_collateral_position_test.cdc => fork_multi_collateral_position_test_skip.cdc} (100%) diff --git a/cadence/tests/fork_multi_collateral_position_test.cdc b/cadence/tests/fork_multi_collateral_position_test_skip.cdc similarity index 100% rename from cadence/tests/fork_multi_collateral_position_test.cdc rename to cadence/tests/fork_multi_collateral_position_test_skip.cdc From b65f0ff4cf91fc59d6cdc797ba712adff1da33c2 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Thu, 5 Mar 2026 10:32:37 -0500 Subject: [PATCH 21/26] fix zero balance check --- cadence/contracts/FlowALPModels.cdc | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cadence/contracts/FlowALPModels.cdc b/cadence/contracts/FlowALPModels.cdc index cd9b1a24..d90727bf 100644 --- a/cadence/contracts/FlowALPModels.cdc +++ b/cadence/contracts/FlowALPModels.cdc @@ -2239,10 +2239,12 @@ access(all) contract FlowALPModels { var debitCount: Int = 0 for key in self.balances.keys { let balance = self.balances[key]! - if balance.direction == BalanceDirection.Credit && balance.scaledBalance > UFix128(0) { - creditCount = creditCount + 1 - } else if balance.direction == BalanceDirection.Debit { - debitCount = debitCount + 1 + if balance.scaledBalance > UFix128(0) { + if balance.direction == BalanceDirection.Credit { + creditCount = creditCount + 1 + } else { + debitCount = debitCount + 1 + } } } return creditCount <= 1 && debitCount <= 1 From 416a9d2bc65624b78bfb62300a9c3b20bc987ba7 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Fri, 6 Mar 2026 12:14:19 -0500 Subject: [PATCH 22/26] clean-up --- cadence/contracts/FlowALPModels.cdc | 21 - cadence/contracts/FlowALPv0.cdc | 19 - .../debt_type_constraint_three_token_test.cdc | 286 ----------- ...> fork_multi_collateral_position_test.cdc} | 0 .../tests/single_token_constraint_test.cdc | 468 ------------------ 5 files changed, 794 deletions(-) delete mode 100644 cadence/tests/debt_type_constraint_three_token_test.cdc rename cadence/tests/{fork_multi_collateral_position_test_skip.cdc => fork_multi_collateral_position_test.cdc} (100%) delete mode 100644 cadence/tests/single_token_constraint_test.cdc diff --git a/cadence/contracts/FlowALPModels.cdc b/cadence/contracts/FlowALPModels.cdc index d90727bf..df733938 100644 --- a/cadence/contracts/FlowALPModels.cdc +++ b/cadence/contracts/FlowALPModels.cdc @@ -2060,11 +2060,6 @@ access(all) contract FlowALPModels { /// Sets the top-up source. See borrowTopUpSource for additional details. /// If nil, the Pool will not pull underflown value, and liquidation may occur. access(EImplementation) fun setTopUpSource(_ source: {DeFiActions.Source}?) - - /// Temporary constraint: one collateral type and one debt type per position. - /// Used as a post-condition on all functions that mutate position balances. - /// This restriction will be lifted in a future protocol version. - access(all) view fun satisfiesTemporaryBalanceConstraint(): Bool } /// InternalPositionImplv1 is the concrete implementation of InternalPosition. @@ -2233,22 +2228,6 @@ access(all) contract FlowALPModels { access(EImplementation) fun setTopUpSource(_ source: {DeFiActions.Source}?) { self.topUpSource = source } - - access(all) view fun satisfiesTemporaryBalanceConstraint(): Bool { - var creditCount: Int = 0 - var debitCount: Int = 0 - for key in self.balances.keys { - let balance = self.balances[key]! - if balance.scaledBalance > UFix128(0) { - if balance.direction == BalanceDirection.Credit { - creditCount = creditCount + 1 - } else { - debitCount = debitCount + 1 - } - } - } - return creditCount <= 1 && debitCount <= 1 - } } /// Factory function to create a new InternalPositionImplv1 resource. diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 763045f2..d0b2f30b 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -565,9 +565,6 @@ access(all) contract FlowALPv0 { !self.isPausedOrWarmup(): "Liquidations are paused by governance" // position must have debt and collateral balance } - post { - self._borrowPosition(pid: pid).satisfiesTemporaryBalanceConstraint(): "Position \(pid) violates temporary balance constraint: only one collateral type and one debt type are allowed per position" - } let repayAmount = repayment.balance assert(repayment.getType() == debtType, message: "Vault type mismatch for repay. Repayment type is \(repayment.getType().identifier) but debt type is \(debtType.identifier)") @@ -1283,13 +1280,6 @@ access(all) contract FlowALPv0 { pre { !self.isPaused(): "Withdrawal, deposits, and liquidations are paused by governance" } - post { - // Only enforce when a new balance entry is being created. - // Overpayment that flips an existing Debit→Credit on the same token is allowed. - before(self._borrowPosition(pid: pid).getBalance(from.getType())) != nil - || self._borrowPosition(pid: pid).satisfiesTemporaryBalanceConstraint(): - "Position \(pid) violates temporary balance constraint: only one collateral type and one debt type are allowed per position" - } // NOTE: caller must have already validated pid + token support let amount = from.balance if amount == 0.0 { @@ -1453,12 +1443,6 @@ access(all) contract FlowALPv0 { } post { !self.state.isPositionLocked(pid): "Position is not unlocked" - // Allow the withdrawal if the constraint was already violated before it ran - // (e.g. debt overpayment flipped a Debit→Credit in a prior call within the same tx). - // Only reject withdrawals that introduce a NEW constraint violation. - !before(self._borrowPosition(pid: pid).satisfiesTemporaryBalanceConstraint()) - || self._borrowPosition(pid: pid).satisfiesTemporaryBalanceConstraint(): - "Position \(pid) violates temporary balance constraint: only one collateral type and one debt type are allowed per position" } self.lockPosition(pid) if self.config.isDebugLogging() { @@ -1931,9 +1915,6 @@ access(all) contract FlowALPv0 { pre { !self.isPaused(): "Withdrawal, deposits, and liquidations are paused by governance" } - post { - self._borrowPosition(pid: pid).satisfiesTemporaryBalanceConstraint(): "Position \(pid) violates temporary balance constraint: only one collateral type and one debt type are allowed per position" - } if self.config.isDebugLogging() { log(" [CONTRACT] rebalancePosition(pid: \(pid), force: \(force))") } diff --git a/cadence/tests/debt_type_constraint_three_token_test.cdc b/cadence/tests/debt_type_constraint_three_token_test.cdc deleted file mode 100644 index 8d0dabd8..00000000 --- a/cadence/tests/debt_type_constraint_three_token_test.cdc +++ /dev/null @@ -1,286 +0,0 @@ -import Test -import BlockchainHelpers - -import "MOET" -import "FlowToken" -import "DummyToken" -import "FlowALPv0" -import "test_helpers.cdc" - -/// Three-token test to properly verify debt type constraint enforcement -/// Uses DummyToken, FLOW, and MOET to test that a position cannot have multiple debt types - -access(all) let DUMMY_TOKEN_IDENTIFIER = "A.0000000000000007.DummyToken.Vault" - -access(all) var snapshot: UInt64 = 0 - -access(all) -fun setup() { - deployContracts() - - // Setup oracle prices for all three tokens - setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0) - setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: MOET_TOKEN_IDENTIFIER, price: 1.0) - setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: DUMMY_TOKEN_IDENTIFIER, price: 1.0) - - // Create pool with MOET as default token - createAndStorePool(signer: PROTOCOL_ACCOUNT, defaultTokenIdentifier: MOET_TOKEN_IDENTIFIER, beFailed: false) - - // Add FLOW as supported token - addSupportedTokenZeroRateCurve( - signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, - collateralFactor: 0.8, - borrowFactor: 0.77, - depositRate: 1_000_000.0, - depositCapacityCap: 1_000_000.0 - ) - - // Add DummyToken as supported token - addSupportedTokenZeroRateCurve( - signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER, - collateralFactor: 0.8, - borrowFactor: 0.77, - depositRate: 1_000_000.0, - depositCapacityCap: 1_000_000.0 - ) - - snapshot = getCurrentBlockHeight() -} - -/// Test that a position with DummyToken collateral and FLOW debt cannot borrow MOET (second debt type) -access(all) -fun testCannotBorrowSecondDebtType() { - log("=== Test: Cannot Borrow Second Debt Type (3 Tokens) ===\n") - - // ===== Setup: Create reserves for FLOW and MOET ===== - - // User to provide FLOW reserves - let flowProvider = Test.createAccount() - setupMoetVault(flowProvider, beFailed: false) - transferFlowTokens(to: flowProvider, amount: 10_000.0) - grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, flowProvider) - - let createFlowPos = executeTransaction( - "../transactions/flow-alp/position/create_position.cdc", - [5_000.0, FLOW_VAULT_STORAGE_PATH, false], - flowProvider - ) - Test.expect(createFlowPos, Test.beSucceeded()) - log("✓ FlowProvider deposited 5000 FLOW (creates FLOW reserves)") - - // ===== Main Test User ===== - - let user = Test.createAccount() - setupMoetVault(user, beFailed: false) - setupDummyTokenVault(user) - mintDummyToken(to: user, amount: 10_000.0) - grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) - - log("✓ User created with DummyToken") - - // User creates position with DummyToken collateral - let createPosRes = executeTransaction( - "../transactions/flow-alp/position/create_position.cdc", - [5_000.0, DummyToken.VaultStoragePath, false], - user - ) - Test.expect(createPosRes, Test.beSucceeded()) - log("✓ User created position with 5000 DummyToken collateral\n") - - let pid: UInt64 = 1 // User's position ID - - // Verify position has DummyToken collateral - var details = getPositionDetails(pid: pid, beFailed: false) - let dummyCredit = getCreditBalanceForType(details: details, vaultType: CompositeType(DUMMY_TOKEN_IDENTIFIER)!) - Test.assert(dummyCredit >= 5_000.0 - 0.01, message: "Position should have ~5000 DummyToken collateral") - log("Position state:") - log(" - DummyToken collateral: ".concat(dummyCredit.toString())) - - // ===== Step 1: Borrow FLOW (first debt type) ===== - - log("\n--- Step 1: Borrow FLOW (first debt type) ---") - - borrowFromPosition( - signer: user, - positionId: pid, - tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, - vaultStoragePath: FLOW_VAULT_STORAGE_PATH, - amount: 1_000.0, - beFailed: false - ) - log("✓ User borrowed 1000 FLOW (first debt type, from reserves)") - - // Verify position now has FLOW debt - details = getPositionDetails(pid: pid, beFailed: false) - let flowDebt = getDebitBalanceForType(details: details, vaultType: CompositeType(FLOW_TOKEN_IDENTIFIER)!) - Test.assert(flowDebt >= 1_000.0 - 0.01, message: "Position should have ~1000 FLOW debt") - - log("\nPosition state after borrowing FLOW:") - log(" - DummyToken collateral: ".concat(dummyCredit.toString())) - log(" - FLOW debt: ".concat(flowDebt.toString())) - - // Check position health - let health = getPositionHealth(pid: pid, beFailed: false) - log(" - Health: ".concat(health.toString())) - Test.assert(health >= UFix128(1.1), message: "Position should be healthy") - - // ===== Step 2: Try to borrow MOET (second debt type) - SHOULD FAIL ===== - - log("\n--- Step 2: Try to borrow MOET (second debt type) ---") - - let borrowMoetRes = executeTransaction( - "./transactions/position-manager/borrow_from_position.cdc", - [pid, MOET_TOKEN_IDENTIFIER, MOET.VaultStoragePath, 500.0], - user - ) - - Test.expect(borrowMoetRes, Test.beFailed()) - Test.assertError(borrowMoetRes, errorMessage: "debt type") - - log("\n=== Test Complete: Debt Type Constraint Verified ===") -} - -/// Regression: exact debt repayment should clear debt-type constraints. -/// After repaying FLOW debt to exactly zero, borrowing MOET as a new debt type should succeed. -access(all) -fun testExactRepayClearsDebtTypeConstraint() { - Test.reset(to: snapshot) - log("\n=== Test: Exact Repay Clears Debt Type Constraint ===\n") - - // Provide FLOW reserves for initial FLOW borrow. - let flowProvider = Test.createAccount() - setupMoetVault(flowProvider, beFailed: false) - transferFlowTokens(to: flowProvider, amount: 10_000.0) - grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, flowProvider) - - let createFlowPos = executeTransaction( - "../transactions/flow-alp/position/create_position.cdc", - [5_000.0, FLOW_VAULT_STORAGE_PATH, false], - flowProvider - ) - Test.expect(createFlowPos, Test.beSucceeded()) - - let user = Test.createAccount() - setupMoetVault(user, beFailed: false) - setupDummyTokenVault(user) - mintDummyToken(to: user, amount: 10_000.0) - grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) - - let createPosRes = executeTransaction( - "../transactions/flow-alp/position/create_position.cdc", - [5_000.0, DummyToken.VaultStoragePath, false], - user - ) - Test.expect(createPosRes, Test.beSucceeded()) - - let pid: UInt64 = 1 - - // Create FLOW debt, then repay exactly to zero. - borrowFromPosition( - signer: user, - positionId: pid, - tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, - vaultStoragePath: FLOW_VAULT_STORAGE_PATH, - amount: 300.0, - beFailed: false - ) - depositToPosition( - signer: user, - positionID: pid, - amount: 300.0, - vaultStoragePath: FLOW_VAULT_STORAGE_PATH, - pushToDrawDownSink: false - ) - - // If exact repay leaves a phantom FLOW debt type, this borrow would fail. - borrowFromPosition( - signer: user, - positionId: pid, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, - vaultStoragePath: MOET.VaultStoragePath, - amount: 100.0, - beFailed: false - ) - - let details = getPositionDetails(pid: pid, beFailed: false) - let flowDebt = getDebitBalanceForType(details: details, vaultType: CompositeType(FLOW_TOKEN_IDENTIFIER)!) - let moetDebt = getDebitBalanceForType(details: details, vaultType: CompositeType(MOET_TOKEN_IDENTIFIER)!) - Test.assert(flowDebt == 0.0, message: "FLOW debt should be zero after exact repay") - Test.assert(moetDebt >= 100.0 - 0.01, message: "MOET debt should be ~100 after new borrow") - - log("\n=== Test Complete: Exact Repay Clears Debt Type Constraint ===") -} - -/// Regression: exact full collateral withdrawal should clear collateral-type constraints. -/// After withdrawing FLOW collateral to exactly zero, depositing Dummy collateral should succeed. -access(all) -fun testExactFullWithdrawClearsCollateralTypeConstraint() { - Test.reset(to: snapshot) - log("\n=== Test: Exact Full Withdraw Clears Collateral Type Constraint ===\n") - - let user = Test.createAccount() - setupMoetVault(user, beFailed: false) - setupDummyTokenVault(user) - transferFlowTokens(to: user, amount: 2_000.0) - mintDummyToken(to: user, amount: 2_000.0) - grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) - - let createPosRes = executeTransaction( - "../transactions/flow-alp/position/create_position.cdc", - [1_000.0, FLOW_VAULT_STORAGE_PATH, false], - user - ) - Test.expect(createPosRes, Test.beSucceeded()) - - let pid: UInt64 = 0 - - // Withdraw collateral exactly to zero. - withdrawFromPosition( - signer: user, - positionId: pid, - tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, - amount: 1_000.0, - pullFromTopUpSource: false - ) - - // If exact full withdraw leaves a phantom FLOW collateral type, this deposit would fail. - depositToPosition( - signer: user, - positionID: pid, - amount: 500.0, - vaultStoragePath: DummyToken.VaultStoragePath, - pushToDrawDownSink: false - ) - - let details = getPositionDetails(pid: pid, beFailed: false) - let flowCredit = getCreditBalanceForType(details: details, vaultType: CompositeType(FLOW_TOKEN_IDENTIFIER)!) - let dummyCredit = getCreditBalanceForType(details: details, vaultType: CompositeType(DUMMY_TOKEN_IDENTIFIER)!) - Test.assert(flowCredit == 0.0, message: "FLOW collateral should be zero after full withdrawal") - Test.assert(dummyCredit >= 500.0 - 0.01, message: "Dummy collateral should be ~500 after deposit") - - log("\n=== Test Complete: Exact Full Withdraw Clears Collateral Type Constraint ===") -} - -// Helper functions - -access(all) -fun setupDummyTokenVault(_ account: Test.TestAccount) { - let result = executeTransaction( - "./transactions/dummy_token/setup_vault.cdc", - [], - account - ) - Test.expect(result, Test.beSucceeded()) -} - -access(all) -fun mintDummyToken(to: Test.TestAccount, amount: UFix64) { - let result = executeTransaction( - "./transactions/dummy_token/mint.cdc", - [amount, to.address], - PROTOCOL_ACCOUNT - ) - Test.expect(result, Test.beSucceeded()) -} diff --git a/cadence/tests/fork_multi_collateral_position_test_skip.cdc b/cadence/tests/fork_multi_collateral_position_test.cdc similarity index 100% rename from cadence/tests/fork_multi_collateral_position_test_skip.cdc rename to cadence/tests/fork_multi_collateral_position_test.cdc diff --git a/cadence/tests/single_token_constraint_test.cdc b/cadence/tests/single_token_constraint_test.cdc deleted file mode 100644 index f9939757..00000000 --- a/cadence/tests/single_token_constraint_test.cdc +++ /dev/null @@ -1,468 +0,0 @@ -import Test -import BlockchainHelpers - -import "MOET" -import "FlowToken" -import "FlowALPv0" -import "test_helpers.cdc" - -/// Tests that verify single collateral and single debt token type constraints per position -/// -/// Each position should only allow: -/// - ONE collateral token type (Credit balance) -/// - ONE debt token type (Debit balance) - -access(all) var snapshot: UInt64 = 0 - -access(all) -fun setup() { - deployContracts() - - // Setup oracle prices - setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0) - setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: MOET_TOKEN_IDENTIFIER, price: 1.0) - - // Create pool with MOET as default token (borrowable via minting) - createAndStorePool(signer: PROTOCOL_ACCOUNT, defaultTokenIdentifier: MOET_TOKEN_IDENTIFIER, beFailed: false) - - // Add FLOW as supported token (can be both collateral and debt) - addSupportedTokenZeroRateCurve( - signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, - collateralFactor: 0.8, - borrowFactor: 0.77, - depositRate: 1_000_000.0, - depositCapacityCap: 1_000_000.0 - ) - - // MOET is already added as the default token when pool was created - - snapshot = getCurrentBlockHeight() -} - -/// Test that a position with FLOW collateral cannot add MOET collateral -access(all) -fun testCannotAddSecondCollateralType() { - log("=== Test: Cannot Add Second Collateral Type ===") - - // Create user with both FLOW and MOET - let user = Test.createAccount() - setupMoetVault(user, beFailed: false) - transferFlowTokens(to: user, amount: 2_000.0) - mintMoet(signer: PROTOCOL_ACCOUNT, to: user.address, amount: 2_000.0, beFailed: false) - grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) - - // Create position with FLOW collateral - let createPosRes = executeTransaction( - "../transactions/flow-alp/position/create_position.cdc", - [1_000.0, FLOW_VAULT_STORAGE_PATH, false], - user - ) - Test.expect(createPosRes, Test.beSucceeded()) - log("✓ Created position with FLOW collateral") - - let pid: UInt64 = 0 - - // Try to deposit MOET to the same position - should FAIL - let depositMoetRes = executeTransaction( - "./transactions/position-manager/deposit_to_position.cdc", - [pid, 500.0, MOET.VaultStoragePath, false], - user - ) - Test.expect(depositMoetRes, Test.beFailed()) - log("✓ Depositing MOET to FLOW-collateral position correctly failed") - - // Verify error message mentions collateral type constraint - let errorMsg = depositMoetRes.error?.message ?? "" - Test.assert( - errorMsg.contains("collateral") || errorMsg.contains("drawDownSink"), - message: "Error should mention collateral type constraint. Got: ".concat(errorMsg) - ) - log("✓ Error message mentions collateral type constraint") - - log("=== Test Passed: Cannot Add Second Collateral Type ===\n") -} - -/// Test that with MOET collateral and FLOW debt, borrowing MOET draws from Credit (not Debit). -/// -/// In the two-token case (FLOW + MOET), the single-debt-type constraint cannot be directly -/// triggered via a healthy position because health checks always prevent the Credit-to-Debit -/// flip scenario. When a user has MOET collateral (Credit) and FLOW debt (Debit), "borrowing" -/// MOET simply reduces the existing MOET Credit — it does NOT create a second Debit entry. -/// The 3-token test (debt_type_constraint_three_token_test.cdc) covers the full constraint -/// using a neutral DummyToken collateral. -access(all) -fun testCannotAddSecondDebtType() { - Test.reset(to: snapshot) - log("=== Test: Borrowing Collateral Token Reduces Credit, Not Creates Second Debit ===") - - // Create user1 with FLOW to provide reserves - let user1 = Test.createAccount() - setupMoetVault(user1, beFailed: false) - transferFlowTokens(to: user1, amount: 5_000.0) - grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user1) - - // User1 deposits FLOW to create reserves - let createPos1Res = executeTransaction( - "../transactions/flow-alp/position/create_position.cdc", - [3_000.0, FLOW_VAULT_STORAGE_PATH, false], - user1 - ) - Test.expect(createPos1Res, Test.beSucceeded()) - log("✓ User1 deposited 3000 FLOW (creates FLOW reserves)") - - // Create user2 with MOET collateral (NOT FLOW) - let user2 = Test.createAccount() - setupMoetVault(user2, beFailed: false) - mintMoet(signer: PROTOCOL_ACCOUNT, to: user2.address, amount: 3_000.0, beFailed: false) - grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user2) - - // User2 creates position with MOET collateral - let createPos2Res = executeTransaction( - "../transactions/flow-alp/position/create_position.cdc", - [2_000.0, MOET.VaultStoragePath, false], - user2 - ) - Test.expect(createPos2Res, Test.beSucceeded()) - log("✓ User2 created position with 2000 MOET collateral") - - let pid: UInt64 = 1 // User2's position ID - - // User2 borrows FLOW (first debt type) - borrows from reserves created by User1 - borrowFromPosition( - signer: user2, - positionId: pid, - tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, - vaultStoragePath: FLOW_VAULT_STORAGE_PATH, - amount: 300.0, - beFailed: false - ) - log("✓ User2 borrowed 300 FLOW (first debt type, from reserves)") - - // Capture MOET Credit before the next operation - var details = getPositionDetails(pid: pid, beFailed: false) - let flowDebt = getDebitBalanceForType(details: details, vaultType: CompositeType(FLOW_TOKEN_IDENTIFIER)!) - let moetCreditBefore = getCreditBalanceForType(details: details, vaultType: CompositeType(MOET_TOKEN_IDENTIFIER)!) - Test.assert(flowDebt > 0.0, message: "Position should have FLOW debt") - log("✓ Position has FLOW debt: ".concat(flowDebt.toString())) - log("✓ MOET Credit before: ".concat(moetCreditBefore.toString())) - - // Borrowing MOET when user already has MOET collateral (Credit) draws from that Credit — - // it does NOT create MOET Debit and therefore does NOT violate the single-debt-type rule. - borrowFromPosition( - signer: user2, - positionId: pid, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, - vaultStoragePath: MOET.VaultStoragePath, - amount: 200.0, - beFailed: false // SUCCEEDS: draws from existing MOET Credit, no new Debit created - ) - log("✓ Borrowing MOET drew from MOET Credit (no second Debit created)") - - // Verify: MOET Credit decreased, and no MOET Debit was created - details = getPositionDetails(pid: pid, beFailed: false) - let moetCreditAfter = getCreditBalanceForType(details: details, vaultType: CompositeType(MOET_TOKEN_IDENTIFIER)!) - let moetDebt = getDebitBalanceForType(details: details, vaultType: CompositeType(MOET_TOKEN_IDENTIFIER)!) - Test.assert(moetCreditAfter < moetCreditBefore, message: "MOET Credit should have decreased") - Test.assert(moetDebt == 0.0, message: "No MOET Debit should have been created") - log("✓ MOET Credit after: ".concat(moetCreditAfter.toString()).concat(" (decreased by 200)")) - log("✓ MOET Debit: ".concat(moetDebt.toString()).concat(" (no second debt type created)")) - - log("=== Test Passed: Borrowing Collateral Token Reduces Credit, Not Creates Second Debit ===\n") -} - -/// Test that a position with FLOW collateral and MOET debt can withdraw both token types -/// (Withdraw FLOW = reduce collateral, Withdraw MOET = borrow more debt) -access(all) -fun testCanWithdrawBothCollateralAndDebtTokens() { - Test.reset(to: snapshot) - log("=== Test: Can Withdraw Both Collateral and Debt Tokens ===") - - // Create user with FLOW collateral - let user = Test.createAccount() - setupMoetVault(user, beFailed: false) - transferFlowTokens(to: user, amount: 5_000.0) - grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) - - // Create position with FLOW collateral - let createPosRes = executeTransaction( - "../transactions/flow-alp/position/create_position.cdc", - [3_000.0, FLOW_VAULT_STORAGE_PATH, false], - user - ) - Test.expect(createPosRes, Test.beSucceeded()) - log("✓ Created position with 3000 FLOW collateral") - - let pid: UInt64 = 0 - - // Borrow MOET (create debt) - borrowFromPosition( - signer: user, - positionId: pid, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, - vaultStoragePath: MOET.VaultStoragePath, - amount: 500.0, - beFailed: false - ) - log("✓ Borrowed 500 MOET (created debt)") - - // Verify position has both FLOW collateral and MOET debt - var details = getPositionDetails(pid: pid, beFailed: false) - let flowCredit = getCreditBalanceForType(details: details, vaultType: CompositeType(FLOW_TOKEN_IDENTIFIER)!) - let moetDebt = getDebitBalanceForType(details: details, vaultType: CompositeType(MOET_TOKEN_IDENTIFIER)!) - Test.assert(flowCredit > 0.0, message: "Position should have FLOW collateral") - Test.assert(moetDebt > 0.0, message: "Position should have MOET debt") - log("✓ Position has FLOW collateral: ".concat(flowCredit.toString())) - log("✓ Position has MOET debt: ".concat(moetDebt.toString())) - - // Withdraw FLOW (reduce collateral) - should SUCCEED - withdrawFromPosition( - signer: user, - positionId: pid, - tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, - amount: 500.0, - pullFromTopUpSource: false - ) - log("✓ Withdrew 500 FLOW from collateral reserves") - - // Verify FLOW collateral decreased - details = getPositionDetails(pid: pid, beFailed: false) - let flowCreditAfter = getCreditBalanceForType(details: details, vaultType: CompositeType(FLOW_TOKEN_IDENTIFIER)!) - Test.assert(flowCreditAfter < flowCredit, message: "FLOW collateral should have decreased") - log("✓ FLOW collateral after withdrawal: ".concat(flowCreditAfter.toString())) - - // Withdraw MOET (borrow more debt) - should SUCCEED - borrowFromPosition( - signer: user, - positionId: pid, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, - vaultStoragePath: MOET.VaultStoragePath, - amount: 200.0, - beFailed: false - ) - log("✓ Borrowed additional 200 MOET (increased debt)") - - // Verify MOET debt increased - details = getPositionDetails(pid: pid, beFailed: false) - let moetDebtAfter = getDebitBalanceForType(details: details, vaultType: CompositeType(MOET_TOKEN_IDENTIFIER)!) - Test.assert(moetDebtAfter > moetDebt, message: "MOET debt should have increased") - log("✓ MOET debt after additional borrow: ".concat(moetDebtAfter.toString())) - - // Verify position is still healthy - let health = getPositionHealth(pid: pid, beFailed: false) - Test.assert(health >= UFix128(1.1), message: "Position should maintain healthy ratio") - log("✓ Position health: ".concat(health.toString())) - - log("=== Test Passed: Can Withdraw Both Collateral and Debt Tokens ===\n") -} - -/// Test that withdrawing collateral token beyond balance (creating debt) with different type fails -access(all) -fun testCannotWithdrawCollateralBeyondBalanceWithDifferentDebt() { - Test.reset(to: snapshot) - log("=== Test: Cannot Create Second Debt Type by Over-Withdrawing Collateral ===") - - // Create user1 with FLOW to provide reserves - let user1 = Test.createAccount() - setupMoetVault(user1, beFailed: false) - transferFlowTokens(to: user1, amount: 5_000.0) - grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user1) - - // User1 deposits FLOW to create reserves - let createPos1Res = executeTransaction( - "../transactions/flow-alp/position/create_position.cdc", - [3_000.0, FLOW_VAULT_STORAGE_PATH, false], - user1 - ) - Test.expect(createPos1Res, Test.beSucceeded()) - log("✓ User1 deposited 3000 FLOW (creates FLOW reserves)") - - // Create user2 with small MOET collateral - let user2 = Test.createAccount() - setupMoetVault(user2, beFailed: false) - mintMoet(signer: PROTOCOL_ACCOUNT, to: user2.address, amount: 3_000.0, beFailed: false) - grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user2) - - // User2 creates position with 100 MOET collateral (small amount) - let createPos2Res = executeTransaction( - "../transactions/flow-alp/position/create_position.cdc", - [100.0, MOET.VaultStoragePath, false], - user2 - ) - Test.expect(createPos2Res, Test.beSucceeded()) - log("✓ User2 created position with 100 MOET collateral") - - let pid: UInt64 = 1 // User2's position ID - - // User2 borrows FLOW (first debt type, from reserves) - borrowFromPosition( - signer: user2, - positionId: pid, - tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, - vaultStoragePath: FLOW_VAULT_STORAGE_PATH, - amount: 30.0, - beFailed: false - ) - log("✓ User2 borrowed 30 FLOW (first debt type, from reserves)") - - // Verify position has: MOET collateral (100), FLOW debt (30) - let details = getPositionDetails(pid: pid, beFailed: false) - let moetCredit = getCreditBalanceForType(details: details, vaultType: CompositeType(MOET_TOKEN_IDENTIFIER)!) - let flowDebt = getDebitBalanceForType(details: details, vaultType: CompositeType(FLOW_TOKEN_IDENTIFIER)!) - Test.assert(moetCredit >= 100.0 - 0.01, message: "Should have ~100 MOET collateral") - Test.assert(flowDebt >= 30.0 - 0.01, message: "Should have ~30 FLOW debt") - log("✓ Position has MOET collateral: ".concat(moetCredit.toString())) - log("✓ Position has FLOW debt: ".concat(flowDebt.toString())) - - // Now try to withdraw 200 MOET (more than the 100 collateral). - // The contract prevents withdrawals beyond the Credit balance with - // "Insufficient funds for withdrawal" — you simply cannot over-withdraw. - let overWithdrawRes = executeTransaction( - "./transactions/position-manager/withdraw_from_position.cdc", - [pid, MOET_TOKEN_IDENTIFIER, 200.0, false], - user2 - ) - Test.expect(overWithdrawRes, Test.beFailed()) - log("✓ Over-withdrawal correctly failed (Insufficient funds — cannot withdraw beyond Credit balance)") - - log("=== Test Passed: Cannot Create Second Debt Type by Over-Withdrawing Collateral ===\n") -} - -/// Test that multiple deposits of the SAME collateral type work fine -access(all) -fun testMultipleDepositsOfSameCollateralTypeSucceed() { - Test.reset(to: snapshot) - log("=== Test: Multiple Deposits of Same Collateral Type Succeed ===") - - // Create user with FLOW - let user = Test.createAccount() - setupMoetVault(user, beFailed: false) - transferFlowTokens(to: user, amount: 5_000.0) - grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) - - // Create position with FLOW collateral - let createPosRes = executeTransaction( - "../transactions/flow-alp/position/create_position.cdc", - [1_000.0, FLOW_VAULT_STORAGE_PATH, false], - user - ) - Test.expect(createPosRes, Test.beSucceeded()) - log("✓ Created position with 1000 FLOW collateral") - - let pid: UInt64 = 0 - - // Get initial balance - var details = getPositionDetails(pid: pid, beFailed: false) - var flowCredit = getCreditBalanceForType(details: details, vaultType: CompositeType(FLOW_TOKEN_IDENTIFIER)!) - log("Initial FLOW credit balance: ".concat(flowCredit.toString())) - - // Deposit more FLOW to the same position - should SUCCEED - depositToPosition( - signer: user, - positionID: pid, - amount: 500.0, - vaultStoragePath: FLOW_VAULT_STORAGE_PATH, - pushToDrawDownSink: false - ) - log("✓ Deposited additional 500 FLOW to same position") - - // Verify balance increased - details = getPositionDetails(pid: pid, beFailed: false) - flowCredit = getCreditBalanceForType(details: details, vaultType: CompositeType(FLOW_TOKEN_IDENTIFIER)!) - Test.assert(flowCredit >= 1_500.0 - 0.01, message: "FLOW credit should be ~1500") - log("✓ FLOW credit balance after second deposit: ".concat(flowCredit.toString())) - - // Deposit even more FLOW - should SUCCEED - depositToPosition( - signer: user, - positionID: pid, - amount: 1_000.0, - vaultStoragePath: FLOW_VAULT_STORAGE_PATH, - pushToDrawDownSink: false - ) - log("✓ Deposited additional 1000 FLOW to same position") - - // Verify balance increased again - details = getPositionDetails(pid: pid, beFailed: false) - flowCredit = getCreditBalanceForType(details: details, vaultType: CompositeType(FLOW_TOKEN_IDENTIFIER)!) - Test.assert(flowCredit >= 2_500.0 - 0.01, message: "FLOW credit should be ~2500") - log("✓ FLOW credit balance after third deposit: ".concat(flowCredit.toString())) - - log("=== Test Passed: Multiple Deposits of Same Collateral Type Succeed ===\n") -} - -/// Test that multiple borrows of the SAME debt type work fine -access(all) -fun testMultipleBorrowsOfSameDebtTypeSucceed() { - Test.reset(to: snapshot) - log("=== Test: Multiple Borrows of Same Debt Type Succeed ===") - - // Create user with FLOW collateral - let user = Test.createAccount() - setupMoetVault(user, beFailed: false) - transferFlowTokens(to: user, amount: 5_000.0) - grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) - - // Create position with FLOW collateral - let createPosRes = executeTransaction( - "../transactions/flow-alp/position/create_position.cdc", - [3_000.0, FLOW_VAULT_STORAGE_PATH, false], - user - ) - Test.expect(createPosRes, Test.beSucceeded()) - log("✓ Created position with 3000 FLOW collateral") - - let pid: UInt64 = 0 - - // Borrow MOET (first time) - borrowFromPosition( - signer: user, - positionId: pid, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, - vaultStoragePath: MOET.VaultStoragePath, - amount: 300.0, - beFailed: false - ) - log("✓ Borrowed 300 MOET (first borrow)") - - // Get debt balance - var details = getPositionDetails(pid: pid, beFailed: false) - var moetDebt = getDebitBalanceForType(details: details, vaultType: CompositeType(MOET_TOKEN_IDENTIFIER)!) - log("MOET debt after first borrow: ".concat(moetDebt.toString())) - - // Borrow more MOET (second time) - should SUCCEED - borrowFromPosition( - signer: user, - positionId: pid, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, - vaultStoragePath: MOET.VaultStoragePath, - amount: 200.0, - beFailed: false - ) - log("✓ Borrowed additional 200 MOET (second borrow)") - - // Verify debt increased - details = getPositionDetails(pid: pid, beFailed: false) - moetDebt = getDebitBalanceForType(details: details, vaultType: CompositeType(MOET_TOKEN_IDENTIFIER)!) - Test.assert(moetDebt >= 500.0 - 0.01, message: "MOET debt should be ~500") - log("✓ MOET debt after second borrow: ".concat(moetDebt.toString())) - - // Borrow even more MOET (third time) - should SUCCEED - borrowFromPosition( - signer: user, - positionId: pid, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, - vaultStoragePath: MOET.VaultStoragePath, - amount: 100.0, - beFailed: false - ) - log("✓ Borrowed additional 100 MOET (third borrow)") - - // Verify debt increased again - details = getPositionDetails(pid: pid, beFailed: false) - moetDebt = getDebitBalanceForType(details: details, vaultType: CompositeType(MOET_TOKEN_IDENTIFIER)!) - Test.assert(moetDebt >= 600.0 - 0.01, message: "MOET debt should be ~600") - log("✓ MOET debt after third borrow: ".concat(moetDebt.toString())) - - log("=== Test Passed: Multiple Borrows of Same Debt Type Succeed ===\n") -} From 23603b4184ff4ebc545481bd7140b937f9b0173f Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Fri, 6 Mar 2026 12:46:19 -0500 Subject: [PATCH 23/26] simplify handler --- cadence/contracts/FlowALPModels.cdc | 81 ++++++----------------------- cadence/contracts/FlowALPv0.cdc | 37 ++++--------- 2 files changed, 27 insertions(+), 91 deletions(-) diff --git a/cadence/contracts/FlowALPModels.cdc b/cadence/contracts/FlowALPModels.cdc index df733938..1f45ef8c 100644 --- a/cadence/contracts/FlowALPModels.cdc +++ b/cadence/contracts/FlowALPModels.cdc @@ -248,42 +248,30 @@ access(all) contract FlowALPModels { /// TokenReserveHandler /// /// Interface for handling token reserve operations. Different token types may require - /// different handling for deposits and withdrawals. For example, MOET tokens are minted - /// on debt withdrawals and burned on repayments, while standard tokens use reserve vaults. + /// different handling for deposits and withdrawals. For example, MOET tokens are always + /// burned on deposits and minted on withdrawals, while standard tokens use reserve vaults. access(all) struct interface TokenReserveHandler { /// Returns the token type this handler manages access(all) view fun getTokenType(): Type - /// Deposits collateral (always to reserves, including MOET) - access(all) fun depositCollateral( + /// Deposits tokens (BURNS for MOET, deposits to reserves for other tokens) + access(all) fun deposit( state: auth(EImplementation) &{PoolState}, from: @{FungibleToken.Vault} ): UFix64 - /// Deposits repayment (to reserves for most tokens, BURNS for MOET) - access(all) fun depositRepayment( - state: auth(EImplementation) &{PoolState}, - from: @{FungibleToken.Vault} - ): UFix64 - - /// Withdraws debt (from reserves for most tokens, MINTS for MOET) - access(all) fun withdrawDebt( + /// Withdraws tokens (MINTS for MOET, withdraws from reserves for other tokens) + access(all) fun withdraw( state: auth(EImplementation) &{PoolState}, amount: UFix64, minterRef: &MOET.Minter? ): @{FungibleToken.Vault} - - /// Withdraws collateral (always from reserves, including MOET) - access(all) fun withdrawCollateral( - state: auth(EImplementation) &{PoolState}, - amount: UFix64 - ): @{FungibleToken.Vault} } /// StandardTokenReserveHandler /// /// Standard implementation of TokenReserveHandler that interacts with reserve vaults - /// for all four operations (deposit/withdraw collateral and debt). + /// for both deposit and withdraw operations. access(all) struct StandardTokenReserveHandler: TokenReserveHandler { access(self) let tokenType: Type @@ -295,17 +283,7 @@ access(all) contract FlowALPModels { return self.tokenType } - access(all) fun depositCollateral( - state: auth(EImplementation) &{PoolState}, - from: @{FungibleToken.Vault} - ): UFix64 { - let amount = from.balance - let reserveVault = state.borrowOrCreateReserve(self.tokenType) - reserveVault.deposit(from: <-from) - return amount - } - - access(all) fun depositRepayment( + access(all) fun deposit( state: auth(EImplementation) &{PoolState}, from: @{FungibleToken.Vault} ): UFix64 { @@ -315,7 +293,7 @@ access(all) contract FlowALPModels { return amount } - access(all) fun withdrawDebt( + access(all) fun withdraw( state: auth(EImplementation) &{PoolState}, amount: UFix64, minterRef: &MOET.Minter? @@ -323,65 +301,38 @@ access(all) contract FlowALPModels { let reserveVault = state.borrowOrCreateReserve(self.tokenType) return <- reserveVault.withdraw(amount: amount) } - - access(all) fun withdrawCollateral( - state: auth(EImplementation) &{PoolState}, - amount: UFix64 - ): @{FungibleToken.Vault} { - let reserveVault = state.borrowOrCreateReserve(self.tokenType) - return <- reserveVault.withdraw(amount: amount) - } } /// MoetTokenReserveHandler /// /// Special implementation of TokenReserveHandler for MOET tokens. - /// - Collateral deposits/withdrawals use reserve vaults (standard behavior) - /// - Debt repayments BURN the MOET tokens (reducing supply) - /// - Debt withdrawals MINT new MOET tokens (increasing supply) + /// - Deposits always BURN the MOET tokens (reducing supply) + /// - Withdrawals always MINT new MOET tokens (increasing supply) access(all) struct MoetTokenReserveHandler: TokenReserveHandler { access(all) view fun getTokenType(): Type { return Type<@MOET.Vault>() } - access(all) fun depositCollateral( + access(all) fun deposit( state: auth(EImplementation) &{PoolState}, from: @{FungibleToken.Vault} ): UFix64 { - let amount = from.balance - let reserveVault = state.borrowOrCreateReserve(Type<@MOET.Vault>()) - reserveVault.deposit(from: <-from) - return amount - } - - access(all) fun depositRepayment( - state: auth(EImplementation) &{PoolState}, - from: @{FungibleToken.Vault} - ): UFix64 { - // Repayments burn MOET tokens to reduce supply + // All deposits burn MOET tokens to reduce supply let amount = from.balance Burner.burn(<-from) return amount } - access(all) fun withdrawDebt( + access(all) fun withdraw( state: auth(EImplementation) &{PoolState}, amount: UFix64, minterRef: &MOET.Minter? ): @{FungibleToken.Vault} { - // Debt withdrawals mint new MOET tokens - assert(minterRef != nil, message: "MOET Minter reference required for debt withdrawal") + // All withdrawals mint new MOET tokens + assert(minterRef != nil, message: "MOET Minter reference required for withdrawal") return <- minterRef!.mintTokens(amount: amount) } - - access(all) fun withdrawCollateral( - state: auth(EImplementation) &{PoolState}, - amount: UFix64 - ): @{FungibleToken.Vault} { - let reserveVault = state.borrowOrCreateReserve(Type<@MOET.Vault>()) - return <- reserveVault.withdraw(amount: amount) - } } /// Risk parameters for a token used in effective collateral/debt computations. diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index d0b2f30b..f10230d8 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -571,7 +571,7 @@ access(all) contract FlowALPv0 { // Use reserve handler to deposit repayment (burns MOET, deposits to reserves for other tokens) let repayReserveOps = self.state.getTokenState(debtType)!.getReserveOperations() let repayStateRef = &self.state as auth(FlowALPModels.EImplementation) &{FlowALPModels.PoolState} - repayReserveOps.depositRepayment(state: repayStateRef, from: <-repayment) + repayReserveOps.deposit(state: repayStateRef, from: <-repayment) // Reduce borrower's debt position by repayAmount let position = self._borrowPosition(pid: pid) @@ -1349,14 +1349,10 @@ access(all) contract FlowALPv0 { // Only the accepted amount consumes capacity; queued portions will consume capacity when processed later tokenState.consumeDepositCapacity(acceptedAmount, pid: pid) - // Use reserve handler to deposit (burns MOET repayments, deposits to reserves for collateral/other tokens) + // Use reserve handler to deposit (burns MOET, deposits to reserves for other tokens) let depositReserveOps = self.state.getTokenState(type)!.getReserveOperations() let depositStateRef = &self.state as auth(FlowALPModels.EImplementation) &{FlowALPModels.PoolState} - if isRepayment { - depositReserveOps.depositRepayment(state: depositStateRef, from: <-from) - } else { - depositReserveOps.depositCollateral(state: depositStateRef, from: <-from) - } + depositReserveOps.deposit(state: depositStateRef, from: <-from) self._queuePositionForUpdateIfNecessary(pid: pid) @@ -1576,23 +1572,14 @@ access(all) contract FlowALPv0 { // Queue for update if necessary self._queuePositionForUpdateIfNecessary(pid: pid) - // Withdraw via reserve handler (handles MOET mint/burn vs standard reserve operations) + // Withdraw via reserve handler (mints MOET, withdraws from reserves for other tokens) let reserveOps = self.state.getTokenState(type)!.getReserveOperations() let stateRef = &self.state as auth(FlowALPModels.EImplementation) &{FlowALPModels.PoolState} - var vault: @{FungibleToken.Vault}? <- nil - if isDebtWithdrawal { - vault <-! reserveOps.withdrawDebt( - state: stateRef, - amount: amount, - minterRef: FlowALPv0._borrowMOETMinter() - ) - } else { - vault <-! reserveOps.withdrawCollateral( - state: stateRef, - amount: amount - ) - } - let unwrappedVault <- vault! + let unwrappedVault <- reserveOps.withdraw( + state: stateRef, + amount: amount, + minterRef: FlowALPv0._borrowMOETMinter() + ) FlowALPEvents.emitWithdrawn( pid: pid, @@ -1994,16 +1981,14 @@ access(all) contract FlowALPv0 { tokenState: tokenState ) - // Withdraw debt via reserve handler (handles MOET mint vs standard reserve withdrawal) + // Withdraw via reserve handler (mints MOET, withdraws from reserves for other tokens) let sinkReserveOps = self.state.getTokenState(sinkType)!.getReserveOperations() let sinkStateRef = &self.state as auth(FlowALPModels.EImplementation) &{FlowALPModels.PoolState} - var sinkVault: @{FungibleToken.Vault}? <- nil - sinkVault <-! sinkReserveOps.withdrawDebt( + let unwrappedSinkVault <- sinkReserveOps.withdraw( state: sinkStateRef, amount: sinkAmount, minterRef: FlowALPv0._borrowMOETMinter() ) - let unwrappedSinkVault <- sinkVault! FlowALPEvents.emitRebalanced( pid: pid, From ee601c1f97d13a0970d652de1a0ba14f6fb14652 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Fri, 6 Mar 2026 17:41:19 -0500 Subject: [PATCH 24/26] tweak handler --- cadence/contracts/FlowALPModels.cdc | 67 +++++++++++++++-- cadence/contracts/FlowALPv0.cdc | 113 +++++++++++++++++++--------- 2 files changed, 135 insertions(+), 45 deletions(-) diff --git a/cadence/contracts/FlowALPModels.cdc b/cadence/contracts/FlowALPModels.cdc index 1f45ef8c..6782cc56 100644 --- a/cadence/contracts/FlowALPModels.cdc +++ b/cadence/contracts/FlowALPModels.cdc @@ -248,19 +248,38 @@ access(all) contract FlowALPModels { /// TokenReserveHandler /// /// Interface for handling token reserve operations. Different token types may require - /// different handling for deposits and withdrawals. For example, MOET tokens are always - /// burned on deposits and minted on withdrawals, while standard tokens use reserve vaults. + /// different handling for deposits and withdrawals. For MOET: deposits always burn tokens, + /// withdrawals always mint tokens. For other tokens: standard reserve vault operations. access(all) struct interface TokenReserveHandler { /// Returns the token type this handler manages access(all) view fun getTokenType(): Type - /// Deposits tokens (BURNS for MOET, deposits to reserves for other tokens) + /// Checks if reserves need to exist for this token type + /// For MOET: returns false (MOET uses mint/burn, not reserves) + /// For other tokens: checks if reserves exist in the pool state + access(all) view fun hasReserve( + state: &{PoolState} + ): Bool + + /// Returns the maximum amount that can be withdrawn given the requested amount + /// For MOET: returns requestedAmount (can mint unlimited) + /// For other tokens: returns min(requestedAmount, reserveBalance) or 0 if no reserves + access(all) fun getMaxWithdrawableAmount( + state: auth(EImplementation) &{PoolState}, + requestedAmount: UFix64 + ): UFix64 + + /// Deposits tokens + /// For MOET: always burns tokens + /// For other tokens: deposits to reserves access(all) fun deposit( state: auth(EImplementation) &{PoolState}, from: @{FungibleToken.Vault} ): UFix64 - /// Withdraws tokens (MINTS for MOET, withdraws from reserves for other tokens) + /// Withdraws tokens + /// For MOET: always mints new tokens + /// For other tokens: withdraws from reserves access(all) fun withdraw( state: auth(EImplementation) &{PoolState}, amount: UFix64, @@ -283,6 +302,23 @@ access(all) contract FlowALPModels { return self.tokenType } + access(all) view fun hasReserve( + state: &{PoolState} + ): Bool { + return state.hasReserve(self.tokenType) + } + + access(all) fun getMaxWithdrawableAmount( + state: auth(EImplementation) &{PoolState}, + requestedAmount: UFix64 + ): UFix64 { + if let reserveVault = state.borrowReserve(self.tokenType) { + let balance = reserveVault.balance + return requestedAmount > balance ? balance : requestedAmount + } + return 0.0 + } + access(all) fun deposit( state: auth(EImplementation) &{PoolState}, from: @{FungibleToken.Vault} @@ -306,19 +342,34 @@ access(all) contract FlowALPModels { /// MoetTokenReserveHandler /// /// Special implementation of TokenReserveHandler for MOET tokens. - /// - Deposits always BURN the MOET tokens (reducing supply) - /// - Withdrawals always MINT new MOET tokens (increasing supply) + /// - All deposits BURN tokens (reducing supply, never stored in reserves) + /// - All withdrawals MINT new tokens (increasing supply, never from reserves) access(all) struct MoetTokenReserveHandler: TokenReserveHandler { access(all) view fun getTokenType(): Type { return Type<@MOET.Vault>() } + access(all) view fun hasReserve( + state: &{PoolState} + ): Bool { + // MOET doesn't use reserves (always mints/burns) + return true + } + + access(all) fun getMaxWithdrawableAmount( + state: auth(EImplementation) &{PoolState}, + requestedAmount: UFix64 + ): UFix64 { + // MOET can mint unlimited amounts + return requestedAmount + } + access(all) fun deposit( state: auth(EImplementation) &{PoolState}, from: @{FungibleToken.Vault} ): UFix64 { - // All deposits burn MOET tokens to reduce supply + // Always burn MOET deposits (never store in reserves) let amount = from.balance Burner.burn(<-from) return amount @@ -329,7 +380,7 @@ access(all) contract FlowALPModels { amount: UFix64, minterRef: &MOET.Minter? ): @{FungibleToken.Vault} { - // All withdrawals mint new MOET tokens + // Always mint MOET withdrawals (never withdraw from reserves) assert(minterRef != nil, message: "MOET Minter reference required for withdrawal") return <- minterRef!.mintTokens(amount: amount) } diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index f10230d8..869f5631 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -584,8 +584,14 @@ access(all) contract FlowALPv0 { let positionBalance = position.getBalance(seizeType) position.borrowBalance(seizeType)!.recordWithdrawal(amount: UFix128(seizeAmount), tokenState: seizeState) - let seizeReserveRef = self.state.borrowReserve(seizeType)! - let seizedCollateral <- seizeReserveRef.withdraw(amount: seizeAmount) + + // Use handler to withdraw seized collateral (mints MOET or withdraws from reserves) + let seizeReserveOps = seizeState.getReserveOperations() + let seizedCollateral <- seizeReserveOps.withdraw( + state: &self.state as auth(FlowALPModels.EImplementation) &{FlowALPModels.PoolState}, + amount: seizeAmount, + minterRef: FlowALPv0._borrowMOETMinter() + ) let newHealth = self.positionHealth(pid: pid) // TODO: sanity check health here? for auto-liquidating, we may need to perform a bounded search which could result in unbounded error in the final health @@ -2081,16 +2087,15 @@ access(all) contract FlowALPv0 { let tokenState = self._borrowUpdatedTokenState(type: tokenType) tokenState.updateInterestRates() - // Ensure reserves exist for this token type - if !self.state.hasReserve(tokenType) { + // Check if reserves are available using the handler + let reserveOps = tokenState.getReserveOperations() + + if !reserveOps.hasReserve(state: &self.state as &{FlowALPModels.PoolState}) { return } - // Get reference to reserves - let reserveRef = self.state.borrowReserve(tokenType)! - - // Collect stability and get token vault - if let collectedVault <- self._collectStability(tokenState: tokenState, reserveVault: reserveRef) { + // Collect stability using the pool state + if let collectedVault <- self._collectStability(tokenState: tokenState, poolState: &self.state as auth(FlowALPModels.EImplementation) &{FlowALPModels.PoolState}) { let collectedBalance = collectedVault.balance // Deposit collected token into stability fund if !self.state.hasStabilityFund(tokenType) { @@ -2109,10 +2114,10 @@ access(all) contract FlowALPv0 { } } - /// Collects insurance by withdrawing from reserves and swapping to MOET. + /// Collects insurance by using the reserve handler (mints MOET or withdraws from reserves), then swaps to MOET if needed. access(self) fun _collectInsurance( tokenState: auth(FlowALPModels.EImplementation) &{FlowALPModels.TokenState}, - reserveVault: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}, + poolState: auth(FlowALPModels.EImplementation) &{FlowALPModels.PoolState}, oraclePrice: UFix64, maxDeviationBps: UInt16 ): @MOET.Vault? { @@ -2137,17 +2142,39 @@ access(all) contract FlowALPv0 { return nil } - if reserveVault.balance == 0.0 { + let tokenType = tokenState.getTokenType() + let reserveOps = tokenState.getReserveOperations() + + // Check if reserves are available using the handler + if !reserveOps.hasReserve(state: poolState) { tokenState.setLastInsuranceCollectionTime(currentTime) return nil } - let amountToCollect = insuranceAmountUFix64 > reserveVault.balance ? reserveVault.balance : insuranceAmountUFix64 - var insuranceVault <- reserveVault.withdraw(amount: amountToCollect) + // Get max withdrawable amount (unlimited for MOET, capped by reserves for others) + let amountToCollect = reserveOps.getMaxWithdrawableAmount(state: poolState, requestedAmount: insuranceAmountUFix64) + if amountToCollect == 0.0 { + tokenState.setLastInsuranceCollectionTime(currentTime) + return nil + } + + // Use handler to withdraw (mints MOET or withdraws from reserves) + var collectedVault <- reserveOps.withdraw( + state: poolState, + amount: amountToCollect, + minterRef: FlowALPv0._borrowMOETMinter() + ) + + // If MOET, we're done - return the minted vault + if tokenType == Type<@MOET.Vault>() { + tokenState.setLastInsuranceCollectionTime(currentTime) + return <-collectedVault as! @MOET.Vault + } + // For other tokens, swap to MOET let insuranceSwapper = tokenState.getInsuranceSwapper() ?? panic("missing insurance swapper") - assert(insuranceSwapper.inType() == reserveVault.getType(), message: "Insurance swapper input type must be same as reserveVault") + assert(insuranceSwapper.inType() == collectedVault.getType(), message: "Insurance swapper input type must match collected vault type") assert(insuranceSwapper.outType() == Type<@MOET.Vault>(), message: "Insurance swapper must output MOET") let quote = insuranceSwapper.quoteOut(forProvided: amountToCollect, reverse: false) @@ -2155,16 +2182,16 @@ access(all) contract FlowALPv0 { assert( FlowALPMath.dexOraclePriceDeviationInRange(dexPrice: dexPrice, oraclePrice: oraclePrice, maxDeviationBps: maxDeviationBps), message: "DEX/oracle price deviation too large. Dex price: \(dexPrice), Oracle price: \(oraclePrice)") - var moetVault <- insuranceSwapper.swap(quote: quote, inVault: <-insuranceVault) as! @MOET.Vault + var moetVault <- insuranceSwapper.swap(quote: quote, inVault: <-collectedVault) as! @MOET.Vault tokenState.setLastInsuranceCollectionTime(currentTime) return <-moetVault } - /// Collects stability funds by withdrawing from reserves. + /// Collects stability funds by using the reserve handler (mints MOET or withdraws from reserves). access(self) fun _collectStability( tokenState: auth(FlowALPModels.EImplementation) &{FlowALPModels.TokenState}, - reserveVault: auth(FungibleToken.Withdraw) &{FungibleToken.Vault} + poolState: auth(FlowALPModels.EImplementation) &{FlowALPModels.PoolState} ): @{FungibleToken.Vault}? { let currentTime = getCurrentBlock().timestamp @@ -2188,14 +2215,28 @@ access(all) contract FlowALPv0 { return nil } - if reserveVault.balance == 0.0 { + let tokenType = tokenState.getTokenType() + let reserveOps = tokenState.getReserveOperations() + + // Check if reserves are available using the handler + if !reserveOps.hasReserve(state: poolState) { tokenState.setLastStabilityFeeCollectionTime(currentTime) return nil } - let reserveVaultBalance = reserveVault.balance - let amountToCollect = stabilityAmountUFix64 > reserveVaultBalance ? reserveVaultBalance : stabilityAmountUFix64 - let stabilityVault <- reserveVault.withdraw(amount: amountToCollect) + // Get max withdrawable amount (unlimited for MOET, capped by reserves for others) + let amountToCollect = reserveOps.getMaxWithdrawableAmount(state: poolState, requestedAmount: stabilityAmountUFix64) + if amountToCollect == 0.0 { + tokenState.setLastStabilityFeeCollectionTime(currentTime) + return nil + } + + // Use handler to withdraw (mints MOET or withdraws from reserves) + let stabilityVault <- reserveOps.withdraw( + state: poolState, + amount: amountToCollect, + minterRef: FlowALPv0._borrowMOETMinter() + ) tokenState.setLastStabilityFeeCollectionTime(currentTime) return <-stabilityVault @@ -2293,23 +2334,22 @@ 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) { + + // Check if reserves are available using the handler + let reserveOps = tokenState.getReserveOperations() + + if !reserveOps.hasReserve(state: &self.state as &{FlowALPModels.PoolState}) { return } - // Get reference to reserves - if let reserveRef = self.state.borrowReserve(tokenType) { - // Collect insurance and get MOET vault - let oraclePrice = self.config.getPriceOracle().price(ofToken: tokenType)! - if let collectedMOET <- self._collectInsurance( - tokenState: tokenState, - reserveVault: reserveRef, - oraclePrice: oraclePrice, - maxDeviationBps: self.config.getDexOracleDeviationBps() - ) { + // Collect insurance using the pool state + let oraclePrice = self.config.getPriceOracle().price(ofToken: tokenType)! + if let collectedMOET <- self._collectInsurance( + tokenState: tokenState, + poolState: &self.state as auth(FlowALPModels.EImplementation) &{FlowALPModels.PoolState}, + oraclePrice: oraclePrice, + maxDeviationBps: self.config.getDexOracleDeviationBps() + ) { let collectedMOETBalance = collectedMOET.balance // Deposit collected MOET into insurance fund self.state.depositToInsuranceFund(from: <-collectedMOET) @@ -2321,7 +2361,6 @@ access(all) contract FlowALPv0 { collectionTime: tokenState.getLastInsuranceCollectionTime() ) } - } } /// Returns an authorized reference to the requested InternalPosition or `nil` if the position does not exist From 08fd6fdf7075dfdb798389430c45a5a693b2e50a Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Fri, 6 Mar 2026 18:37:42 -0500 Subject: [PATCH 25/26] switch tests to use dummy token --- .../insurance_collection_formula_test.cdc | 90 +++++-- cadence/tests/insurance_collection_test.cdc | 237 +++++++++++------- .../stability_collection_formula_test.cdc | 88 +++++-- cadence/tests/stability_collection_test.cdc | 176 ++++++++----- 4 files changed, 397 insertions(+), 194 deletions(-) diff --git a/cadence/tests/insurance_collection_formula_test.cdc b/cadence/tests/insurance_collection_formula_test.cdc index 27c7cc39..b6bcdf78 100644 --- a/cadence/tests/insurance_collection_formula_test.cdc +++ b/cadence/tests/insurance_collection_formula_test.cdc @@ -3,8 +3,32 @@ import BlockchainHelpers import "MOET" import "FlowToken" +import "DummyToken" import "test_helpers.cdc" +access(all) let DUMMY_TOKEN_IDENTIFIER = "A.0000000000000007.DummyToken.Vault" + +// Helper function to setup DummyToken vault +access(all) +fun setupDummyTokenVault(_ account: Test.TestAccount) { + let result = executeTransaction( + "./transactions/dummy_token/setup_vault.cdc", + [], + account + ) + Test.expect(result, Test.beSucceeded()) +} + +// Helper function to mint DummyToken +access(all) +fun mintDummyToken(to: Test.TestAccount, amount: UFix64) { + let result = executeTransaction( + "./transactions/dummy_token/mint.cdc", + [amount, to.address], + PROTOCOL_ACCOUNT + ) + Test.expect(result, Test.beSucceeded()) +} access(all) fun setup() { @@ -21,74 +45,90 @@ fun setup() { depositRate: 1_000_000.0, depositCapacityCap: 1_000_000.0 ) + + // Add DummyToken as a supported token for borrowing + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: DUMMY_TOKEN_IDENTIFIER, price: 1.0) + addSupportedTokenZeroRateCurve( + signer: PROTOCOL_ACCOUNT, + tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER, + collateralFactor: 0.8, + borrowFactor: 1.0, + depositRate: 1_000_000.0, + depositCapacityCap: 1_000_000.0 + ) } // ----------------------------------------------------------------------------- // Test: collectInsurance full success flow with formula verification // Full flow: LP deposits to create credit → borrower borrows to create debit -// → advance time → collect insurance → verify MOET returned, reserves reduced, +// → advance time → collect insurance → verify tokens returned, reserves reduced, // timestamp updated, and formula -// Formula: insuranceAmount = totalDebitBalance * insuranceRate * (timeElapsed / secondsPerYear) +// Formula: insuranceAmount = debitIncome * insuranceRate +// where debitIncome = totalDebitBalance * (currentDebitRate^timeElapsed - 1.0) // // This test runs in isolation (separate file) to ensure totalDebitBalance // equals exactly the borrowed amount without interference from other tests. +// Uses DummyToken (not MOET) to test reserve behavior. // ----------------------------------------------------------------------------- access(all) fun test_collectInsurance_success_fullAmount() { - // setup LP to provide MOET liquidity for borrowing + // setup LP to provide DummyToken liquidity for borrowing let lp = Test.createAccount() - setupMoetVault(lp, beFailed: false) - mintMoet(signer: PROTOCOL_ACCOUNT, to: lp.address, amount: 10000.0, beFailed: false) + setupDummyTokenVault(lp) + mintDummyToken(to: lp, amount: 10000.0) - // LP deposits MOET (creates credit balance, provides borrowing liquidity) - createPosition(admin: PROTOCOL_ACCOUNT, signer: lp, amount: 10000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) + // LP deposits DummyToken (creates credit balance, provides borrowing liquidity) + createPosition(admin: PROTOCOL_ACCOUNT, signer: lp, amount: 10000.0, vaultStoragePath: DummyToken.VaultStoragePath, pushToDrawDownSink: false) // setup borrower with FLOW collateral - // With 0.8 CF and 1.3 target health: 1000 FLOW collateral allows borrowing ~615 MOET + // With 0.8 CF and 1.3 target health: 1000 FLOW collateral allows borrowing ~615 DummyToken // borrow = (collateral * price * CF) / targetHealth = (1000 * 1.0 * 0.8) / 1.3 ≈ 615.38 let borrower = Test.createAccount() - setupMoetVault(borrower, beFailed: false) + setupDummyTokenVault(borrower) transferFlowTokens(to: borrower, amount: 1000.0) - // borrower deposits FLOW and auto-borrows MOET (creates debit balance ~615 MOET) - createPosition(admin: PROTOCOL_ACCOUNT, signer: borrower, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: true) + // borrower deposits FLOW collateral + createPosition(admin: PROTOCOL_ACCOUNT, signer: borrower, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + // Then borrows DummyToken to create DummyToken debit balance (~615 DummyToken) + borrowFromPosition(signer: borrower, positionId: 1, tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER, vaultStoragePath: DummyToken.VaultStoragePath, amount: 615.38, beFailed: false) // setup protocol account with MOET vault for the swapper setupMoetVault(PROTOCOL_ACCOUNT, beFailed: false) mintMoet(signer: PROTOCOL_ACCOUNT, to: PROTOCOL_ACCOUNT.address, amount: 10000.0, beFailed: false) - // configure insurance swapper (1:1 ratio) - let swapperResult = setInsuranceSwapper(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0) + // configure insurance swapper for DummyToken (1:1 ratio) + let swapperResult = setInsuranceSwapper(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER, priceRatio: 1.0) Test.expect(swapperResult, Test.beSucceeded()) - // set 10% annual debit rate + // set 10% annual debit rate for DummyToken // insurance is calculated on debit income, not debit balance - setInterestCurveFixed(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, yearlyRate: 0.1) + setInterestCurveFixed(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER, yearlyRate: 0.1) // set insurance rate (10% of debit income) - let rateResult = setInsuranceRate(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, insuranceRate: 0.1) + let rateResult = setInsuranceRate(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER, insuranceRate: 0.1) Test.expect(rateResult, Test.beSucceeded()) // collect insurance to reset last insurance collection timestamp, // this accounts for timing variation between pool creation and this point // (each transaction/script execution advances the block timestamp slightly) - collectInsurance(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, beFailed: false) + collectInsurance(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER, beFailed: false) // record balances after resetting the timestamp let initialInsuranceBalance = getInsuranceFundBalance() - let reserveBalanceBefore = getReserveBalance(vaultIdentifier: MOET_TOKEN_IDENTIFIER) - Test.assert(reserveBalanceBefore > 0.0, message: "Reserves should exist after deposit") + let reserveBalanceBefore = getReserveBalance(vaultIdentifier: DUMMY_TOKEN_IDENTIFIER) + // DummyToken reserves should exist (DummyToken uses reserves, not mint/burn) + Test.assert(reserveBalanceBefore > 0.0, message: "DummyToken reserves should exist after deposit") // record timestamp before advancing time let timestampBefore = getBlockTimestamp() Test.moveTime(by: ONE_YEAR) - collectInsurance(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, beFailed: false) + collectInsurance(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER, beFailed: false) // verify insurance was collected, reserves decreased let finalInsuranceBalance = getInsuranceFundBalance() - let reserveBalanceAfter = getReserveBalance(vaultIdentifier: MOET_TOKEN_IDENTIFIER) - Test.assert(reserveBalanceAfter < reserveBalanceBefore, message: "Reserves should have decreased after collection") + let reserveBalanceAfter = getReserveBalance(vaultIdentifier: DUMMY_TOKEN_IDENTIFIER) + Test.assert(reserveBalanceAfter < reserveBalanceBefore, message: "DummyToken reserves should have decreased after collection") let collectedAmount = finalInsuranceBalance - initialInsuranceBalance @@ -98,15 +138,15 @@ fun test_collectInsurance_success_fullAmount() { // verify last insurance collection time was updated to current block timestamp let currentTimestamp = getBlockTimestamp() - let lastInsuranceCollectionTime = getLastInsuranceCollectionTime(tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER) + let lastInsuranceCollectionTime = getLastInsuranceCollectionTime(tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER) Test.assertEqual(currentTimestamp, lastInsuranceCollectionTime!) // verify formula: insuranceAmount = debitIncome * insuranceRate // where debitIncome = totalDebitBalance * (currentDebitRate^timeElapsed - 1.0) // = (1.0 + 0.1 / 31_557_600)^31_557_600 = 1.10517091665 - // debitBalance ≈ 615.38 MOET + // debitBalance ≈ 615.38 DummyToken // With 10% annual debit rate over 1 year: debitIncome ≈ 615.38 * (1.10517091665 - 1) ≈ 64.72 - // Insurance = debitIncome * 0.1 ≈ 6.472 MOET + // Insurance = debitIncome * 0.1 ≈ 6.472 DummyToken (swapped to MOET at 1:1) // NOTE: // We intentionally do not use `equalWithinVariance` with `defaultUFixVariance` here. diff --git a/cadence/tests/insurance_collection_test.cdc b/cadence/tests/insurance_collection_test.cdc index 87aa8d96..a6111a49 100644 --- a/cadence/tests/insurance_collection_test.cdc +++ b/cadence/tests/insurance_collection_test.cdc @@ -3,10 +3,35 @@ import BlockchainHelpers import "MOET" import "FlowToken" +import "DummyToken" import "test_helpers.cdc" +access(all) let DUMMY_TOKEN_IDENTIFIER = "A.0000000000000007.DummyToken.Vault" + access(all) var snapshot: UInt64 = 0 +// Helper function to setup DummyToken vault +access(all) +fun setupDummyTokenVault(_ account: Test.TestAccount) { + let result = executeTransaction( + "./transactions/dummy_token/setup_vault.cdc", + [], + account + ) + Test.expect(result, Test.beSucceeded()) +} + +// Helper function to mint DummyToken +access(all) +fun mintDummyToken(to: Test.TestAccount, amount: UFix64) { + let result = executeTransaction( + "./transactions/dummy_token/mint.cdc", + [amount, to.address], + PROTOCOL_ACCOUNT + ) + Test.expect(result, Test.beSucceeded()) +} + access(all) fun setup() { deployContracts() @@ -102,61 +127,79 @@ fun test_collectInsurance_zeroDebitBalance_returnsNil() { // When calculated insurance amount exceeds reserve balance, it collects // only what is available. Verify exact amount withdrawn from reserves. // Note: Insurance is calculated on debit income (interest accrued on debit balance) +// Uses DummyToken (not MOET) to test reserve-capping behavior. // ----------------------------------------------------------------------------- access(all) fun test_collectInsurance_partialReserves_collectsAvailable() { - // setup LP to provide MOET liquidity for borrowing (small amount to create limited reserves) + // Add DummyToken as a supported token for borrowing + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: DUMMY_TOKEN_IDENTIFIER, price: 1.0) + addSupportedTokenZeroRateCurve( + signer: PROTOCOL_ACCOUNT, + tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER, + collateralFactor: 0.8, + borrowFactor: 1.0, + depositRate: 1_000_000.0, + depositCapacityCap: 1_000_000.0 + ) + + // setup LP to provide DummyToken liquidity for borrowing (small amount to create limited reserves) let lp = Test.createAccount() - setupMoetVault(lp, beFailed: false) - mintMoet(signer: PROTOCOL_ACCOUNT, to: lp.address, amount: 1000.0, beFailed: false) + setupDummyTokenVault(lp) + mintDummyToken(to: lp, amount: 1000.0) - // LP deposits 1000 MOET (creates credit balance, provides borrowing liquidity) - createPosition(admin: PROTOCOL_ACCOUNT, signer: lp, amount: 1000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) + // LP deposits 1000 DummyToken (creates credit balance, provides borrowing liquidity) + createPosition(admin: PROTOCOL_ACCOUNT, signer: lp, amount: 1000.0, vaultStoragePath: DummyToken.VaultStoragePath, pushToDrawDownSink: false) - // setup borrower with large FLOW collateral to borrow most of the MOET + // setup borrower with large FLOW collateral to borrow most of the DummyToken let borrower = Test.createAccount() - setupMoetVault(borrower, beFailed: false) + setupDummyTokenVault(borrower) transferFlowTokens(to: borrower, amount: 10000.0) - // borrower deposits 10000 FLOW and auto-borrows MOET - // With 0.8 CF and 1.3 target health: 10000 FLOW allows borrowing ~6153 MOET - // But pool only has 1000 MOET, so borrower gets ~1000 MOET (limited by liquidity) - // This leaves reserves very low (close to 0) - createPosition(admin: PROTOCOL_ACCOUNT, signer: borrower, amount: 10000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: true) + // borrower deposits 10000 FLOW collateral + createPosition(admin: PROTOCOL_ACCOUNT, signer: borrower, amount: 10000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + // Then borrows 900 DummyToken to create DummyToken debit balance + borrowFromPosition(signer: borrower, positionId: 1, tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER, vaultStoragePath: DummyToken.VaultStoragePath, amount: 900.0, beFailed: false) // setup protocol account with MOET vault for the swapper setupMoetVault(PROTOCOL_ACCOUNT, beFailed: false) mintMoet(signer: PROTOCOL_ACCOUNT, to: PROTOCOL_ACCOUNT.address, amount: 10000.0, beFailed: false) - // configure insurance swapper (1:1 ratio) - let swapperResult = setInsuranceSwapper(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0) + // configure insurance swapper for DummyToken (1:1 ratio) + let swapperResult = setInsuranceSwapper(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER, priceRatio: 1.0) Test.expect(swapperResult, Test.beSucceeded()) - // set 90% annual debit rate - setInterestCurveFixed(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, yearlyRate: 0.9) + // set 90% annual debit rate for DummyToken + setInterestCurveFixed(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER, yearlyRate: 0.9) // set a high insurance rate (90% of debit income goes to insurance) - // Note: default stabilityFeeRate is 0.05, so insuranceRate + stabilityFeeRate = 0.9 + 0.05 = 0.95 < 1.0 - let rateResult = setInsuranceRate(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, insuranceRate: 0.9) + let rateResult = setInsuranceRate(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER, insuranceRate: 0.9) Test.expect(rateResult, Test.beSucceeded()) let initialInsuranceBalance = getInsuranceFundBalance() Test.assertEqual(0.0, initialInsuranceBalance) + // DummyToken reserves should exist after deposit + let reserveBalanceBefore = getReserveBalance(vaultIdentifier: DUMMY_TOKEN_IDENTIFIER) + Test.assert(reserveBalanceBefore > 0.0, message: "DummyToken reserves should exist after deposit") + Test.moveTime(by: ONE_YEAR + DAY * 30.0) // year + month // collect insurance - should collect up to available reserve balance - collectInsurance(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, beFailed: false) + collectInsurance(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER, beFailed: false) let finalInsuranceBalance = getInsuranceFundBalance() - let reserveBalanceAfter = getReserveBalance(vaultIdentifier: MOET_TOKEN_IDENTIFIER) + let reserveBalanceAfter = getReserveBalance(vaultIdentifier: DUMMY_TOKEN_IDENTIFIER) + + // verify reserves were used (decreased) + Test.assert(reserveBalanceAfter < reserveBalanceBefore, message: "DummyToken reserves should have decreased") - // with 1:1 swap ratio, insurance fund balance should equal amount withdrawn from reserves - Test.assertEqual(0.0, reserveBalanceAfter) + // For DummyToken, insurance is withdrawn from reserves and swapped to MOET + // The amount collected should be limited by available reserves + let amountWithdrawn = reserveBalanceBefore - reserveBalanceAfter + Test.assert(amountWithdrawn > 0.0, message: "Should have withdrawn from reserves") - // verify collection was limited by reserves - // Formula: 90% debit income -> 90% insurance rate -> large insurance amount, but limited by available reserves - Test.assertEqual(1000.0, finalInsuranceBalance) + // Insurance fund should have MOET (swapped from DummyToken) + Test.assert(finalInsuranceBalance > 0.0, message: "Insurance fund should have received MOET") } // ----------------------------------------------------------------------------- @@ -206,58 +249,72 @@ fun test_collectInsurance_tinyAmount_roundsToZero_returnsNil() { // ----------------------------------------------------------------------------- // Test: collectInsurance full success flow // Full flow: LP deposits to create credit → borrower borrows to create debit -// → advance time → collect insurance → verify MOET returned, reserves reduced, timestamp updated +// → advance time → collect insurance → verify tokens returned, reserves reduced, timestamp updated // Note: Formula verification is in insurance_collection_formula_test.cdc (isolated test) +// Uses DummyToken (not MOET) to test reserve behavior. // ----------------------------------------------------------------------------- access(all) fun test_collectInsurance_success_fullAmount() { - // setup LP to provide MOET liquidity for borrowing + // Add DummyToken as a supported token for borrowing + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: DUMMY_TOKEN_IDENTIFIER, price: 1.0) + addSupportedTokenZeroRateCurve( + signer: PROTOCOL_ACCOUNT, + tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER, + collateralFactor: 0.8, + borrowFactor: 1.0, + depositRate: 1_000_000.0, + depositCapacityCap: 1_000_000.0 + ) + + // setup LP to provide DummyToken liquidity for borrowing let lp = Test.createAccount() - setupMoetVault(lp, beFailed: false) - mintMoet(signer: PROTOCOL_ACCOUNT, to: lp.address, amount: 10000.0, beFailed: false) + setupDummyTokenVault(lp) + mintDummyToken(to: lp, amount: 10000.0) - // LP deposits MOET (creates credit balance, provides borrowing liquidity) - createPosition(admin: PROTOCOL_ACCOUNT, signer: lp, amount: 10000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) + // LP deposits DummyToken (creates credit balance, provides borrowing liquidity) + createPosition(admin: PROTOCOL_ACCOUNT, signer: lp, amount: 10000.0, vaultStoragePath: DummyToken.VaultStoragePath, pushToDrawDownSink: false) // setup borrower with FLOW collateral let borrower = Test.createAccount() - setupMoetVault(borrower, beFailed: false) + setupDummyTokenVault(borrower) transferFlowTokens(to: borrower, amount: 1000.0) - // borrower deposits FLOW and auto-borrows MOET (creates debit balance) - createPosition(admin: PROTOCOL_ACCOUNT, signer: borrower, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: true) + // borrower deposits FLOW collateral + createPosition(admin: PROTOCOL_ACCOUNT, signer: borrower, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + // Then borrows DummyToken to create DummyToken debit balance + borrowFromPosition(signer: borrower, positionId: 1, tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER, vaultStoragePath: DummyToken.VaultStoragePath, amount: 615.38, beFailed: false) // setup protocol account with MOET vault for the swapper setupMoetVault(PROTOCOL_ACCOUNT, beFailed: false) mintMoet(signer: PROTOCOL_ACCOUNT, to: PROTOCOL_ACCOUNT.address, amount: 10000.0, beFailed: false) - // configure insurance swapper (1:1 ratio) - let swapperResult = setInsuranceSwapper(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0) + // configure insurance swapper for DummyToken (1:1 ratio) + let swapperResult = setInsuranceSwapper(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER, priceRatio: 1.0) Test.expect(swapperResult, Test.beSucceeded()) - // set 10% annual debit rate + // set 10% annual debit rate for DummyToken // Insurance is calculated on debit income, not debit balance directly - setInterestCurveFixed(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, yearlyRate: 0.1) + setInterestCurveFixed(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER, yearlyRate: 0.1) // set insurance rate (10% of debit income) - let rateResult = setInsuranceRate(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, insuranceRate: 0.1) + let rateResult = setInsuranceRate(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER, insuranceRate: 0.1) Test.expect(rateResult, Test.beSucceeded()) // initial insurance and reserves let initialInsuranceBalance = getInsuranceFundBalance() Test.assertEqual(0.0, initialInsuranceBalance) - let reserveBalanceBefore = getReserveBalance(vaultIdentifier: MOET_TOKEN_IDENTIFIER) - Test.assert(reserveBalanceBefore > 0.0, message: "Reserves should exist after deposit") + let reserveBalanceBefore = getReserveBalance(vaultIdentifier: DUMMY_TOKEN_IDENTIFIER) + Test.assert(reserveBalanceBefore > 0.0, message: "DummyToken reserves should exist after deposit") Test.moveTime(by: ONE_YEAR) - collectInsurance(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, beFailed: false) + collectInsurance(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER, beFailed: false) // verify insurance was collected, reserves decreased let finalInsuranceBalance = getInsuranceFundBalance() Test.assert(finalInsuranceBalance > 0.0, message: "Insurance fund should have received MOET") - let reserveBalanceAfter = getReserveBalance(vaultIdentifier: MOET_TOKEN_IDENTIFIER) - Test.assert(reserveBalanceAfter < reserveBalanceBefore, message: "Reserves should have decreased after collection") + let reserveBalanceAfter = getReserveBalance(vaultIdentifier: DUMMY_TOKEN_IDENTIFIER) + Test.assert(reserveBalanceAfter < reserveBalanceBefore, message: "DummyToken reserves should have decreased after collection") // verify the amount withdrawn from reserves equals the insurance fund balance (1:1 swap ratio) let amountWithdrawnFromReserves = reserveBalanceBefore - reserveBalanceAfter @@ -265,7 +322,7 @@ fun test_collectInsurance_success_fullAmount() { // verify last insurance collection time was updated to current block timestamp let currentTimestamp = getBlockTimestamp() - let lastCollectionTime = getLastInsuranceCollectionTime(tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER) + let lastCollectionTime = getLastInsuranceCollectionTime(tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER) Test.assertEqual(currentTimestamp, lastCollectionTime!) } @@ -274,34 +331,46 @@ fun test_collectInsurance_success_fullAmount() { // Verifies that insurance collection works independently for different tokens // Each token type has its own last insurance collection timestamp and rate // Note: Insurance is calculated on totalDebitBalance, so we need borrowing activity for each token +// Uses DummyToken and FlowToken (both reserve-based) to test multi-token reserve behavior. // ----------------------------------------------------------------------------- access(all) fun test_collectInsurance_multipleTokens() { - // Note: FlowToken is already added in setup() + // Add DummyToken support + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: DUMMY_TOKEN_IDENTIFIER, price: 1.0) + addSupportedTokenZeroRateCurve( + signer: PROTOCOL_ACCOUNT, + tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER, + collateralFactor: 0.8, + borrowFactor: 1.0, + depositRate: 1_000_000.0, + depositCapacityCap: 1_000_000.0 + ) - // setup MOET LP to provide MOET liquidity for borrowing - let moetLp = Test.createAccount() - setupMoetVault(moetLp, beFailed: false) - mintMoet(signer: PROTOCOL_ACCOUNT, to: moetLp.address, amount: 10000.0, beFailed: false) + // setup DummyToken LP to provide DummyToken liquidity for borrowing + let dummyLp = Test.createAccount() + setupDummyTokenVault(dummyLp) + mintDummyToken(to: dummyLp, amount: 10000.0) - // MOET LP deposits MOET (creates MOET credit balance) - createPosition(admin: PROTOCOL_ACCOUNT, signer: moetLp, amount: 10000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) + // DummyToken LP deposits DummyToken (creates DummyToken credit balance) + createPosition(admin: PROTOCOL_ACCOUNT, signer: dummyLp, amount: 10000.0, vaultStoragePath: DummyToken.VaultStoragePath, pushToDrawDownSink: false) // setup FLOW LP to provide FLOW liquidity for borrowing let flowLp = Test.createAccount() - setupMoetVault(flowLp, beFailed: false) transferFlowTokens(to: flowLp, amount: 10000.0) - // FLOW LP deposits FLOW (creates FLOW debit balance) + // FLOW LP deposits FLOW (creates FLOW credit balance) createPosition(admin: PROTOCOL_ACCOUNT, signer: flowLp, amount: 10000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) - // setup MOET borrower with FLOW collateral (creates MOET debit) - let moetBorrower = Test.createAccount() - setupMoetVault(moetBorrower, beFailed: false) - transferFlowTokens(to: moetBorrower, amount: 1000.0) + // setup DummyToken borrower with MOET collateral (creates DummyToken debit) + let dummyBorrower = Test.createAccount() + setupMoetVault(dummyBorrower, beFailed: false) + setupDummyTokenVault(dummyBorrower) + mintMoet(signer: PROTOCOL_ACCOUNT, to: dummyBorrower.address, amount: 1000.0, beFailed: false) - // MOET borrower deposits FLOW and auto-borrows MOET (creates MOET debit balance) - createPosition(admin: PROTOCOL_ACCOUNT, signer: moetBorrower, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: true) + // DummyToken borrower deposits MOET collateral + createPosition(admin: PROTOCOL_ACCOUNT, signer: dummyBorrower, amount: 1000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) + // Then borrows DummyToken (creates DummyToken debit balance) + borrowFromPosition(signer: dummyBorrower, positionId: 2, tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER, vaultStoragePath: DummyToken.VaultStoragePath, amount: 615.0, beFailed: false) // setup FLOW borrower with MOET collateral (creates FLOW debit) let flowBorrower = Test.createAccount() @@ -318,20 +387,20 @@ fun test_collectInsurance_multipleTokens() { mintMoet(signer: PROTOCOL_ACCOUNT, to: PROTOCOL_ACCOUNT.address, amount: 20000.0, beFailed: false) // configure insurance swappers for both tokens (both swap to MOET at 1:1) - let moetSwapperResult = setInsuranceSwapper(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0) - Test.expect(moetSwapperResult, Test.beSucceeded()) + let dummySwapperResult = setInsuranceSwapper(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER, priceRatio: 1.0) + Test.expect(dummySwapperResult, Test.beSucceeded()) let flowSwapperResult = setInsuranceSwapper(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, priceRatio: 1.0) Test.expect(flowSwapperResult, Test.beSucceeded()) // set 10% annual debit rates // Insurance is calculated on debit income, not debit balance directly - setInterestCurveFixed(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, yearlyRate: 0.1) + setInterestCurveFixed(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER, yearlyRate: 0.1) setInterestCurveFixed(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, yearlyRate: 0.1) // set different insurance rates for each token type (percentage of debit income) - let moetRateResult = setInsuranceRate(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, insuranceRate: 0.1) // 10% - Test.expect(moetRateResult, Test.beSucceeded()) + let dummyRateResult = setInsuranceRate(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER, insuranceRate: 0.1) // 10% + Test.expect(dummyRateResult, Test.beSucceeded()) let flowRateResult = setInsuranceRate(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, insuranceRate: 0.05) // 5% Test.expect(flowRateResult, Test.beSucceeded()) @@ -340,54 +409,54 @@ fun test_collectInsurance_multipleTokens() { let initialInsuranceBalance = getInsuranceFundBalance() Test.assertEqual(0.0, initialInsuranceBalance) - let moetReservesBefore = getReserveBalance(vaultIdentifier: MOET_TOKEN_IDENTIFIER) + let dummyReservesBefore = getReserveBalance(vaultIdentifier: DUMMY_TOKEN_IDENTIFIER) let flowReservesBefore = getReserveBalance(vaultIdentifier: FLOW_TOKEN_IDENTIFIER) - Test.assert(moetReservesBefore > 0.0, message: "MOET reserves should exist after deposit") + Test.assert(dummyReservesBefore > 0.0, message: "DummyToken reserves should exist after deposit") Test.assert(flowReservesBefore > 0.0, message: "Flow reserves should exist after deposit") // advance time Test.moveTime(by: ONE_YEAR) - // collect insurance for MOET only - collectInsurance(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, beFailed: false) + // collect insurance for DummyToken only + collectInsurance(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER, beFailed: false) - let balanceAfterMoetCollection = getInsuranceFundBalance() - Test.assert(balanceAfterMoetCollection > 0.0, message: "Insurance fund should have received MOET after MOET collection") + let balanceAfterDummyCollection = getInsuranceFundBalance() + Test.assert(balanceAfterDummyCollection > 0.0, message: "Insurance fund should have received MOET after DummyToken collection") - // verify the amount withdrawn from MOET reserves equals the insurance fund balance increase (1:1 swap ratio) - let moetAmountWithdrawn = moetReservesBefore - getReserveBalance(vaultIdentifier: MOET_TOKEN_IDENTIFIER) - Test.assertEqual(moetAmountWithdrawn, balanceAfterMoetCollection) + // verify the amount withdrawn from DummyToken reserves equals the insurance fund balance increase (1:1 swap ratio) + let dummyAmountWithdrawn = dummyReservesBefore - getReserveBalance(vaultIdentifier: DUMMY_TOKEN_IDENTIFIER) + Test.assertEqual(dummyAmountWithdrawn, balanceAfterDummyCollection) - let moetLastCollectionTime = getLastInsuranceCollectionTime(tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER) + let dummyLastCollectionTime = getLastInsuranceCollectionTime(tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER) let flowLastCollectionTimeBeforeFlowCollection = getLastInsuranceCollectionTime(tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER) - // MOET timestamp should be updated, Flow timestamp should still be at pool creation time - Test.assert(moetLastCollectionTime != nil, message: "MOET lastInsuranceCollectionTime should be set") + // DummyToken timestamp should be updated, Flow timestamp should still be at pool creation time + Test.assert(dummyLastCollectionTime != nil, message: "DummyToken lastInsuranceCollectionTime should be set") Test.assert(flowLastCollectionTimeBeforeFlowCollection != nil, message: "Flow lastInsuranceCollectionTime should be set") - Test.assert(moetLastCollectionTime! > flowLastCollectionTimeBeforeFlowCollection!, message: "MOET timestamp should be newer than Flow timestamp") + Test.assert(dummyLastCollectionTime! > flowLastCollectionTimeBeforeFlowCollection!, message: "DummyToken timestamp should be newer than Flow timestamp") // collect insurance for Flow collectInsurance(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, beFailed: false) let balanceAfterFlowCollection = getInsuranceFundBalance() - Test.assert(balanceAfterFlowCollection > balanceAfterMoetCollection, message: "Insurance fund should increase after Flow collection") + Test.assert(balanceAfterFlowCollection > balanceAfterDummyCollection, message: "Insurance fund should increase after Flow collection") let flowLastCollectionTimeAfter = getLastInsuranceCollectionTime(tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER) Test.assert(flowLastCollectionTimeAfter != nil, message: "Flow lastInsuranceCollectionTime should be set after collection") // verify reserves decreased for both token types - let moetReservesAfter = getReserveBalance(vaultIdentifier: MOET_TOKEN_IDENTIFIER) + let dummyReservesAfter = getReserveBalance(vaultIdentifier: DUMMY_TOKEN_IDENTIFIER) let flowReservesAfter = getReserveBalance(vaultIdentifier: FLOW_TOKEN_IDENTIFIER) - Test.assert(moetReservesAfter < moetReservesBefore, message: "MOET reserves should have decreased") + Test.assert(dummyReservesAfter < dummyReservesBefore, message: "DummyToken reserves should have decreased") Test.assert(flowReservesAfter < flowReservesBefore, message: "Flow reserves should have decreased") // verify the amount withdrawn from Flow reserves equals the insurance fund balance increase (1:1 swap ratio) let flowAmountWithdrawn = flowReservesBefore - flowReservesAfter - let flowInsuranceIncrease = balanceAfterFlowCollection - balanceAfterMoetCollection + let flowInsuranceIncrease = balanceAfterFlowCollection - balanceAfterDummyCollection Test.assertEqual(flowAmountWithdrawn, flowInsuranceIncrease) - // verify Flow timestamp is now updated (should be >= MOET timestamp since it was collected after) - Test.assert(flowLastCollectionTimeAfter! >= moetLastCollectionTime!, message: "Flow timestamp should be >= MOET timestamp") + // verify Flow timestamp is now updated (should be >= DummyToken timestamp since it was collected after) + Test.assert(flowLastCollectionTimeAfter! >= dummyLastCollectionTime!, message: "Flow timestamp should be >= DummyToken timestamp") } // ----------------------------------------------------------------------------- diff --git a/cadence/tests/stability_collection_formula_test.cdc b/cadence/tests/stability_collection_formula_test.cdc index 00e8a0d6..37007fa9 100644 --- a/cadence/tests/stability_collection_formula_test.cdc +++ b/cadence/tests/stability_collection_formula_test.cdc @@ -3,8 +3,33 @@ import BlockchainHelpers import "MOET" import "FlowToken" +import "DummyToken" import "test_helpers.cdc" +access(all) let DUMMY_TOKEN_IDENTIFIER = "A.0000000000000007.DummyToken.Vault" + +// Helper function to setup DummyToken vault +access(all) +fun setupDummyTokenVault(_ account: Test.TestAccount) { + let result = executeTransaction( + "./transactions/dummy_token/setup_vault.cdc", + [], + account + ) + Test.expect(result, Test.beSucceeded()) +} + +// Helper function to mint DummyToken +access(all) +fun mintDummyToken(to: Test.TestAccount, amount: UFix64) { + let result = executeTransaction( + "./transactions/dummy_token/mint.cdc", + [amount, to.address], + PROTOCOL_ACCOUNT + ) + Test.expect(result, Test.beSucceeded()) +} + access(all) fun setup() { deployContracts() @@ -20,6 +45,17 @@ fun setup() { depositRate: 1_000_000.0, depositCapacityCap: 1_000_000.0 ) + + // Add DummyToken as a supported token for borrowing + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: DUMMY_TOKEN_IDENTIFIER, price: 1.0) + addSupportedTokenZeroRateCurve( + signer: PROTOCOL_ACCOUNT, + tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER, + collateralFactor: 0.8, + borrowFactor: 1.0, + depositRate: 1_000_000.0, + depositCapacityCap: 1_000_000.0 + ) } // ----------------------------------------------------------------------------- @@ -32,57 +68,61 @@ fun setup() { // // This test runs in isolation (separate file) to ensure totalDebitBalance // equals exactly the borrowed amount without interference from other tests. +// Uses DummyToken (not MOET) to test reserve behavior. // ----------------------------------------------------------------------------- access(all) fun test_collectStability_success_fullAmount() { - // setup LP to provide MOET liquidity for borrowing + // setup LP to provide DummyToken liquidity for borrowing let lp = Test.createAccount() - setupMoetVault(lp, beFailed: false) - mintMoet(signer: PROTOCOL_ACCOUNT, to: lp.address, amount: 10000.0, beFailed: false) + setupDummyTokenVault(lp) + mintDummyToken(to: lp, amount: 10000.0) - // LP deposits MOET (creates credit balance, provides borrowing liquidity) - createPosition(admin: PROTOCOL_ACCOUNT, signer: lp, amount: 10000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) + // LP deposits DummyToken (creates credit balance, provides borrowing liquidity) + createPosition(admin: PROTOCOL_ACCOUNT, signer: lp, amount: 10000.0, vaultStoragePath: DummyToken.VaultStoragePath, pushToDrawDownSink: false) // setup borrower with FLOW collateral - // With 0.8 CF and 1.3 target health: 1000 FLOW collateral allows borrowing ~615 MOET + // With 0.8 CF and 1.3 target health: 1000 FLOW collateral allows borrowing ~615 DummyToken // borrow = (collateral * price * CF) / targetHealth = (1000 * 1.0 * 0.8) / 1.3 ≈ 615.38 let borrower = Test.createAccount() - setupMoetVault(borrower, beFailed: false) + setupDummyTokenVault(borrower) transferFlowTokens(to: borrower, amount: 1000.0) - // borrower deposits FLOW and auto-borrows MOET (creates debit balance ~615 MOET) - createPosition(admin: PROTOCOL_ACCOUNT, signer: borrower, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: true) + // borrower deposits FLOW collateral + createPosition(admin: PROTOCOL_ACCOUNT, signer: borrower, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + // Then borrows DummyToken to create DummyToken debit balance (~615 DummyToken) + borrowFromPosition(signer: borrower, positionId: 1, tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER, vaultStoragePath: DummyToken.VaultStoragePath, amount: 615.38, beFailed: false) - // set 10% annual debit rate + // set 10% annual debit rate for DummyToken // stability is calculated on interest income, not debit balance directly - setInterestCurveFixed(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, yearlyRate: 0.1) + setInterestCurveFixed(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER, yearlyRate: 0.1) // set stability fee rate (10% of interest income) - let rateResult = setStabilityFeeRate(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, stabilityFeeRate: 0.1) + let rateResult = setStabilityFeeRate(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER, stabilityFeeRate: 0.1) Test.expect(rateResult, Test.beSucceeded()) // collect stability to reset last stability collection timestamp, // this accounts for timing variation between pool creation and this point // (each transaction/script execution advances the block timestamp slightly) - var res = collectStability(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER) + var res = collectStability(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER) Test.expect(res, Test.beSucceeded()) // record balances after resetting the timestamp - let initialStabilityBalance = getStabilityFundBalance(tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER) - let reserveBalanceBefore = getReserveBalance(vaultIdentifier: MOET_TOKEN_IDENTIFIER) - Test.assert(reserveBalanceBefore > 0.0, message: "Reserves should exist after deposit") + let initialStabilityBalance = getStabilityFundBalance(tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER) + let reserveBalanceBefore = getReserveBalance(vaultIdentifier: DUMMY_TOKEN_IDENTIFIER) + // DummyToken reserves should exist (DummyToken uses reserves, not mint/burn) + Test.assert(reserveBalanceBefore > 0.0, message: "DummyToken reserves should exist after deposit") // record timestamp before advancing time let timestampBefore = getBlockTimestamp() Test.moveTime(by: ONE_YEAR) - res = collectStability(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER) + res = collectStability(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER) Test.expect(res, Test.beSucceeded()) // verify stability was collected, reserves decreased - let finalStabilityBalance = getStabilityFundBalance(tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER) - let reserveBalanceAfter = getReserveBalance(vaultIdentifier: MOET_TOKEN_IDENTIFIER) - Test.assert(reserveBalanceAfter < reserveBalanceBefore, message: "Reserves should have decreased after collection") + let finalStabilityBalance = getStabilityFundBalance(tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER) + let reserveBalanceAfter = getReserveBalance(vaultIdentifier: DUMMY_TOKEN_IDENTIFIER) + Test.assert(reserveBalanceAfter < reserveBalanceBefore, message: "DummyToken reserves should have decreased after collection") let collectedAmount = finalStabilityBalance! - initialStabilityBalance! @@ -92,15 +132,15 @@ fun test_collectStability_success_fullAmount() { // verify last stability collection time was updated to current block timestamp let currentTimestamp = getBlockTimestamp() - let lastStabilityCollectionTime = getLastStabilityCollectionTime(tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER) + let lastStabilityCollectionTime = getLastStabilityCollectionTime(tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER) Test.assertEqual(currentTimestamp, lastStabilityCollectionTime!) // verify formula: stabilityAmount = interestIncome * stabilityFeeRate - // where interestIncome = totalDebitBalance * (currentDebitRate^timeElapsed - 1.0) + // where interestIncome = totalDebitBalance * (currentDebitRate^timeElapsed - 1.0) // = (1.0 + 0.1 / 31_557_600)^31_557_600 = 1.10517091665 - // debitBalance ≈ 615.38 MOET + // debitBalance ≈ 615.38 DummyToken // With 10% annual debit rate over 1 year: interestIncome ≈ 615.38 * (1.10517091665 - 1) ≈ 64.72 - // Stability = interestIncome * 0.1 ≈ 6.472 MOET + // Stability = interestIncome * 0.1 ≈ 6.472 DummyToken // NOTE: // We intentionally do not use `equalWithinVariance` with `defaultUFixVariance` here. diff --git a/cadence/tests/stability_collection_test.cdc b/cadence/tests/stability_collection_test.cdc index 7d0eb36e..7385d418 100644 --- a/cadence/tests/stability_collection_test.cdc +++ b/cadence/tests/stability_collection_test.cdc @@ -3,10 +3,35 @@ import BlockchainHelpers import "MOET" import "FlowToken" +import "DummyToken" import "test_helpers.cdc" +access(all) let DUMMY_TOKEN_IDENTIFIER = "A.0000000000000007.DummyToken.Vault" + access(all) var snapshot: UInt64 = 0 +// Helper function to setup DummyToken vault +access(all) +fun setupDummyTokenVault(_ account: Test.TestAccount) { + let result = executeTransaction( + "./transactions/dummy_token/setup_vault.cdc", + [], + account + ) + Test.expect(result, Test.beSucceeded()) +} + +// Helper function to mint DummyToken +access(all) +fun mintDummyToken(to: Test.TestAccount, amount: UFix64) { + let result = executeTransaction( + "./transactions/dummy_token/mint.cdc", + [amount, to.address], + PROTOCOL_ACCOUNT + ) + Test.expect(result, Test.beSucceeded()) +} + access(all) fun setup() { deployContracts() @@ -70,57 +95,74 @@ fun test_collectStability_zeroDebitBalance_returnsNil() { // Test: collectStability only collects up to available reserve balance // When calculated stability amount exceeds reserve balance, it collects // only what is available. Verify exact amount withdrawn from reserves. +// Uses DummyToken (not MOET) to test reserve-capping behavior. // ----------------------------------------------------------------------------- access(all) fun test_collectStability_partialReserves_collectsAvailable() { - // setup LP to provide MOET liquidity for borrowing (small amount to create limited reserves) + // Add DummyToken as a supported token for borrowing + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: DUMMY_TOKEN_IDENTIFIER, price: 1.0) + addSupportedTokenZeroRateCurve( + signer: PROTOCOL_ACCOUNT, + tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER, + collateralFactor: 0.8, + borrowFactor: 1.0, + depositRate: 1_000_000.0, + depositCapacityCap: 1_000_000.0 + ) + + // setup LP to provide DummyToken liquidity for borrowing (small amount to create limited reserves) let lp = Test.createAccount() - setupMoetVault(lp, beFailed: false) - mintMoet(signer: PROTOCOL_ACCOUNT, to: lp.address, amount: 1000.0, beFailed: false) + setupDummyTokenVault(lp) + mintDummyToken(to: lp, amount: 1000.0) - // LP deposits 1000 MOET (creates credit balance, provides borrowing liquidity) - createPosition(admin: PROTOCOL_ACCOUNT, signer: lp, amount: 1000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) + // LP deposits 1000 DummyToken (creates credit balance, provides borrowing liquidity) + createPosition(admin: PROTOCOL_ACCOUNT, signer: lp, amount: 1000.0, vaultStoragePath: DummyToken.VaultStoragePath, pushToDrawDownSink: false) - // setup borrower with large FLOW collateral to borrow most of the MOET + // setup borrower with large FLOW collateral to borrow most of the DummyToken let borrower = Test.createAccount() - setupMoetVault(borrower, beFailed: false) + setupDummyTokenVault(borrower) transferFlowTokens(to: borrower, amount: 10000.0) - // borrower deposits 10000 FLOW and auto-borrows MOET - // With 0.8 CF and 1.3 target health: 10000 FLOW allows borrowing ~6153 MOET - // But pool only has 1000 MOET, so borrower gets ~1000 MOET (limited by liquidity) - // This leaves reserves very low (close to 0) - createPosition(admin: PROTOCOL_ACCOUNT, signer: borrower, amount: 10000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: true) + // borrower deposits 10000 FLOW collateral + createPosition(admin: PROTOCOL_ACCOUNT, signer: borrower, amount: 10000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + // Then borrows 900 DummyToken to create DummyToken debit balance + borrowFromPosition(signer: borrower, positionId: 1, tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER, vaultStoragePath: DummyToken.VaultStoragePath, amount: 900.0, beFailed: false) setupMoetVault(PROTOCOL_ACCOUNT, beFailed: false) mintMoet(signer: PROTOCOL_ACCOUNT, to: PROTOCOL_ACCOUNT.address, amount: 10000.0, beFailed: false) - // set 90% annual debit rate - setInterestCurveFixed(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, yearlyRate: 0.9) + // set 90% annual debit rate for DummyToken + setInterestCurveFixed(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER, yearlyRate: 0.9) // set a high stability fee rate so calculated amount would exceed reserves - // Note: stabilityFeeRate must be < 1.0, using 0.9 which combined with default insuranceRate (0.0) = 0.9 < 1.0 - let rateResult = setStabilityFeeRate(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, stabilityFeeRate: 0.9) + let rateResult = setStabilityFeeRate(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER, stabilityFeeRate: 0.9) Test.expect(rateResult, Test.beSucceeded()) - let initialStabilityBalance = getStabilityFundBalance(tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER) + let initialStabilityBalance = getStabilityFundBalance(tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER) Test.assertEqual(nil, initialStabilityBalance) + // DummyToken reserves should exist after deposit + let reserveBalanceBefore = getReserveBalance(vaultIdentifier: DUMMY_TOKEN_IDENTIFIER) + Test.assert(reserveBalanceBefore > 0.0, message: "DummyToken reserves should exist after deposit") + Test.moveTime(by: ONE_YEAR + DAY * 30.0) // 1 year + 1 month // collect stability - should collect up to available reserve balance - let res = collectStability(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER) + let res = collectStability(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER) Test.expect(res, Test.beSucceeded()) - let finalStabilityBalance = getStabilityFundBalance(tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER) - let reserveBalanceAfter = getReserveBalance(vaultIdentifier: MOET_TOKEN_IDENTIFIER) + let finalStabilityBalance = getStabilityFundBalance(tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER) + let reserveBalanceAfter = getReserveBalance(vaultIdentifier: DUMMY_TOKEN_IDENTIFIER) + + // verify reserves were used (decreased) + Test.assert(reserveBalanceAfter < reserveBalanceBefore, message: "DummyToken reserves should have decreased") - // stability fund balance should equal amount withdrawn from reserves - Test.assertEqual(0.0, reserveBalanceAfter) + // stability fund balance should be positive (collected from reserves) + Test.assert(finalStabilityBalance! > 0.0, message: "Stability fund should have received DummyToken") - // verify collection was limited by reserves - // Formula: 90% debit income -> 90% stability rate -> large amount, but limited by available reserves - Test.assertEqual(1000.0, finalStabilityBalance!) + // verify collection was limited by available reserves + let amountWithdrawn = reserveBalanceBefore - reserveBalanceAfter + Test.assertEqual(amountWithdrawn, finalStabilityBalance!) } // ----------------------------------------------------------------------------- @@ -169,34 +211,46 @@ fun test_collectStability_tinyAmount_roundsToZero_returnsNil() { // Test: collectStability with multiple token types // Verifies that stability collection works independently for different tokens // Each token type has its own last stability collection timestamp and rate +// Uses DummyToken and FlowToken (both reserve-based) to test multi-token reserve behavior. // ----------------------------------------------------------------------------- access(all) fun test_collectStability_multipleTokens() { - // Note: FlowToken is already added in setup() + // Add DummyToken support + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: DUMMY_TOKEN_IDENTIFIER, price: 1.0) + addSupportedTokenZeroRateCurve( + signer: PROTOCOL_ACCOUNT, + tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER, + collateralFactor: 0.8, + borrowFactor: 1.0, + depositRate: 1_000_000.0, + depositCapacityCap: 1_000_000.0 + ) - // setup MOET LP to provide MOET liquidity for borrowing - let moetLp = Test.createAccount() - setupMoetVault(moetLp, beFailed: false) - mintMoet(signer: PROTOCOL_ACCOUNT, to: moetLp.address, amount: 10000.0, beFailed: false) + // setup DummyToken LP to provide DummyToken liquidity for borrowing + let dummyLp = Test.createAccount() + setupDummyTokenVault(dummyLp) + mintDummyToken(to: dummyLp, amount: 10000.0) - // MOET LP deposits MOET (creates MOET credit balance) - createPosition(admin: PROTOCOL_ACCOUNT, signer: moetLp, amount: 10000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) + // DummyToken LP deposits DummyToken (creates DummyToken credit balance) + createPosition(admin: PROTOCOL_ACCOUNT, signer: dummyLp, amount: 10000.0, vaultStoragePath: DummyToken.VaultStoragePath, pushToDrawDownSink: false) // setup FLOW LP to provide FLOW liquidity for borrowing let flowLp = Test.createAccount() - setupMoetVault(flowLp, beFailed: false) transferFlowTokens(to: flowLp, amount: 10000.0) // FLOW LP deposits FLOW (creates FLOW credit balance) createPosition(admin: PROTOCOL_ACCOUNT, signer: flowLp, amount: 10000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) - // setup MOET borrower with FLOW collateral (creates MOET debit) - let moetBorrower = Test.createAccount() - setupMoetVault(moetBorrower, beFailed: false) - transferFlowTokens(to: moetBorrower, amount: 1000.0) + // setup DummyToken borrower with MOET collateral (creates DummyToken debit) + let dummyBorrower = Test.createAccount() + setupMoetVault(dummyBorrower, beFailed: false) + setupDummyTokenVault(dummyBorrower) + mintMoet(signer: PROTOCOL_ACCOUNT, to: dummyBorrower.address, amount: 1000.0, beFailed: false) - // MOET borrower deposits FLOW and auto-borrows MOET (creates MOET debit balance) - createPosition(admin: PROTOCOL_ACCOUNT, signer: moetBorrower, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: true) + // DummyToken borrower deposits MOET collateral + createPosition(admin: PROTOCOL_ACCOUNT, signer: dummyBorrower, amount: 1000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) + // Then borrows DummyToken (creates DummyToken debit balance) + borrowFromPosition(signer: dummyBorrower, positionId: 2, tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER, vaultStoragePath: DummyToken.VaultStoragePath, amount: 615.0, beFailed: false) // setup FLOW borrower with MOET collateral (creates FLOW debit) let flowBorrower = Test.createAccount() @@ -210,48 +264,48 @@ fun test_collectStability_multipleTokens() { // set 10% annual debit rates // Stability is calculated on interest income, not debit balance directly - setInterestCurveFixed(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, yearlyRate: 0.1) + setInterestCurveFixed(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER, yearlyRate: 0.1) setInterestCurveFixed(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, yearlyRate: 0.1) // set different stability fee rates for each token type (percentage of interest income) - let moetRateResult = setStabilityFeeRate(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, stabilityFeeRate: 0.1) // 10% - Test.expect(moetRateResult, Test.beSucceeded()) + let dummyRateResult = setStabilityFeeRate(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER, stabilityFeeRate: 0.1) // 10% + Test.expect(dummyRateResult, Test.beSucceeded()) let flowRateResult = setStabilityFeeRate(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, stabilityFeeRate: 0.05) // 5% Test.expect(flowRateResult, Test.beSucceeded()) // verify initial state - let initialMoetStabilityBalance = getStabilityFundBalance(tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER) - Test.assertEqual(nil, initialMoetStabilityBalance) + let initialDummyStabilityBalance = getStabilityFundBalance(tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER) + Test.assertEqual(nil, initialDummyStabilityBalance) let initialFlowStabilityBalance = getStabilityFundBalance(tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER) Test.assertEqual(nil, initialFlowStabilityBalance) - let moetReservesBefore = getReserveBalance(vaultIdentifier: MOET_TOKEN_IDENTIFIER) + let dummyReservesBefore = getReserveBalance(vaultIdentifier: DUMMY_TOKEN_IDENTIFIER) let flowReservesBefore = getReserveBalance(vaultIdentifier: FLOW_TOKEN_IDENTIFIER) - Test.assert(moetReservesBefore > 0.0, message: "MOET reserves should exist after deposit") + Test.assert(dummyReservesBefore > 0.0, message: "DummyToken reserves should exist after deposit") Test.assert(flowReservesBefore > 0.0, message: "Flow reserves should exist after deposit") // advance time Test.moveTime(by: ONE_YEAR) - // collect stability for MOET only - var res = collectStability(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER) + // collect stability for DummyToken only + var res = collectStability(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER) Test.expect(res, Test.beSucceeded()) - let balanceAfterMoetCollection = getStabilityFundBalance(tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER) - Test.assert(balanceAfterMoetCollection! > 0.0, message: "MOET stability fund should have received tokens after MOET collection") + let balanceAfterDummyCollection = getStabilityFundBalance(tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER) + Test.assert(balanceAfterDummyCollection! > 0.0, message: "DummyToken stability fund should have received tokens after DummyToken collection") - // verify the amount withdrawn from MOET reserves equals the stability fund balance increase - let moetAmountWithdrawn = moetReservesBefore - getReserveBalance(vaultIdentifier: MOET_TOKEN_IDENTIFIER) - Test.assertEqual(moetAmountWithdrawn, balanceAfterMoetCollection!) + // verify the amount withdrawn from DummyToken reserves equals the stability fund balance increase + let dummyAmountWithdrawn = dummyReservesBefore - getReserveBalance(vaultIdentifier: DUMMY_TOKEN_IDENTIFIER) + Test.assertEqual(dummyAmountWithdrawn, balanceAfterDummyCollection!) - let moetLastCollectionTime = getLastStabilityCollectionTime(tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER) + let dummyLastCollectionTime = getLastStabilityCollectionTime(tokenTypeIdentifier: DUMMY_TOKEN_IDENTIFIER) let flowLastCollectionTimeBeforeFlowCollection = getLastStabilityCollectionTime(tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER) - // MOET timestamp should be updated, Flow timestamp should still be at pool creation time - Test.assert(moetLastCollectionTime != nil, message: "MOET lastStabilityCollectionTime should be set") + // DummyToken timestamp should be updated, Flow timestamp should still be at pool creation time + Test.assert(dummyLastCollectionTime != nil, message: "DummyToken lastStabilityCollectionTime should be set") Test.assert(flowLastCollectionTimeBeforeFlowCollection != nil, message: "Flow lastStabilityCollectionTime should be set") - Test.assert(moetLastCollectionTime! > flowLastCollectionTimeBeforeFlowCollection!, message: "MOET timestamp should be newer than Flow timestamp") + Test.assert(dummyLastCollectionTime! > flowLastCollectionTimeBeforeFlowCollection!, message: "DummyToken timestamp should be newer than Flow timestamp") // collect stability for Flow res = collectStability(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER) @@ -264,17 +318,17 @@ fun test_collectStability_multipleTokens() { Test.assert(flowLastCollectionTimeAfter != nil, message: "Flow lastStabilityCollectionTime should be set after collection") // verify reserves decreased for both token types - let moetReservesAfter = getReserveBalance(vaultIdentifier: MOET_TOKEN_IDENTIFIER) + let dummyReservesAfter = getReserveBalance(vaultIdentifier: DUMMY_TOKEN_IDENTIFIER) let flowReservesAfter = getReserveBalance(vaultIdentifier: FLOW_TOKEN_IDENTIFIER) - Test.assert(moetReservesAfter < moetReservesBefore, message: "MOET reserves should have decreased") + Test.assert(dummyReservesAfter < dummyReservesBefore, message: "DummyToken reserves should have decreased") Test.assert(flowReservesAfter < flowReservesBefore, message: "Flow reserves should have decreased") // verify the amount withdrawn from Flow reserves equals the Flow stability fund balance let flowAmountWithdrawn = flowReservesBefore - flowReservesAfter Test.assertEqual(flowAmountWithdrawn, flowBalanceAfterCollection!) - // verify Flow timestamp is now updated (should be >= MOET timestamp since it was collected after) - Test.assert(flowLastCollectionTimeAfter! >= moetLastCollectionTime!, message: "Flow timestamp should be >= MOET timestamp") + // verify Flow timestamp is now updated (should be >= DummyToken timestamp since it was collected after) + Test.assert(flowLastCollectionTimeAfter! >= dummyLastCollectionTime!, message: "Flow timestamp should be >= DummyToken timestamp") } // ----------------------------------------------------------------------------- From 8d480454034581c599c7bda6fd39646f57a18ad8 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Fri, 6 Mar 2026 19:23:10 -0500 Subject: [PATCH 26/26] revert unnecessary changes --- .../transactions/position-manager/borrow_from_position.cdc | 5 ----- 1 file changed, 5 deletions(-) diff --git a/cadence/tests/transactions/position-manager/borrow_from_position.cdc b/cadence/tests/transactions/position-manager/borrow_from_position.cdc index bffa787a..589281c1 100644 --- a/cadence/tests/transactions/position-manager/borrow_from_position.cdc +++ b/cadence/tests/transactions/position-manager/borrow_from_position.cdc @@ -1,6 +1,5 @@ import "FungibleToken" import "FlowToken" -import "MOET" import "FlowALPv0" import "FlowALPModels" @@ -33,10 +32,6 @@ transaction( self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier: \(tokenTypeIdentifier)") - if signer.storage.type(at: tokenVaultStoragePath) == nil { - signer.storage.save(<-FlowToken.createEmptyVault(vaultType: self.tokenType), to: tokenVaultStoragePath) - } - self.receiverVault = signer.storage.borrow<&{FungibleToken.Receiver}>(from: tokenVaultStoragePath) ?? panic("Could not borrow receiver vault") }