Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
2ed0198
add todos while reviewing existing code
jordanschalm Dec 17, 2025
aa982a7
more todos and documentation
jordanschalm Dec 17, 2025
1821da4
skeleton of manual liquidation
jordanschalm Dec 19, 2025
5b53c1f
add health check: must be <=target after liquidation
jordanschalm Dec 19, 2025
9457d0d
implement internal liquidation function + bound checks on seize/repay
jordanschalm Dec 20, 2025
f95b620
Merge branch 'main' into jord/liquidation
jordanschalm Jan 5, 2026
095e24c
fix dex/oravle inequality assertion
jordanschalm Jan 7, 2026
05dbf8b
fix inconsistent calculation of implied collateral price
jordanschalm Jan 7, 2026
16b6e8d
rm extraneous comments
jordanschalm Jan 7, 2026
0e0f783
fix true balance calculation to account for direction
jordanschalm Jan 7, 2026
18bfffa
make dex input optional (so contract compiles, for now)
jordanschalm Jan 7, 2026
96a5731
update dependencies
jordanschalm Jan 7, 2026
4dab0e7
remove LiquidationResult
jordanschalm Jan 7, 2026
35be7ef
remove old liquidation logic methods
jordanschalm Jan 7, 2026
9f8d070
remove protocol fee
jordanschalm Jan 7, 2026
6178e58
remove liqudiation quote, update docs
jordanschalm Jan 7, 2026
b03dfad
update mock dex swapper to support SwapperProvider
jordanschalm Jan 8, 2026
cd7c2e3
add tx for manual liquidation
jordanschalm Jan 8, 2026
af33646
fix health assertion
jordanschalm Jan 8, 2026
2c690ed
minimal working test
jordanschalm Jan 9, 2026
f5c77c8
add tests
jordanschalm Jan 9, 2026
d01c659
add collateral overage test
jordanschalm Jan 9, 2026
9cc5038
check specific error messages
jordanschalm Jan 9, 2026
f440c14
add test stubs
jordanschalm Jan 9, 2026
87ddbc2
clarify credit/debit rate fields
jordanschalm Jan 9, 2026
c9c3545
re-add unused field (upgradability)
jordanschalm Jan 9, 2026
d5d5518
remove existing dex test
jordanschalm Jan 9, 2026
0fdb73c
remove quote liquidation tx
jordanschalm Jan 9, 2026
f194d1a
remove provisional dex (to be added in separate issue)
jordanschalm Jan 9, 2026
376610a
Merge branch 'main' into jord/liquidation
jordanschalm Jan 9, 2026
c3bf506
add tx+test for vault mismatch test
jordanschalm Jan 12, 2026
8a9b523
Merge branch 'jord/liquidation' of github.com:onflow/FlowCreditMarket…
jordanschalm Jan 12, 2026
8e751cc
add tests
jordanschalm Jan 12, 2026
908b705
fix unsupported collateral type test
jordanschalm Jan 12, 2026
daa3e52
Merge branch 'main' into jord/liquidation
jordanschalm Jan 13, 2026
6947c89
improve "supported, not in position" tests
jordanschalm Jan 13, 2026
1b34a81
remove unneeded logs
jordanschalm Jan 14, 2026
dc77506
Update cadence/contracts/FlowCreditMarket.cdc
jordanschalm Jan 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
849 changes: 178 additions & 671 deletions cadence/contracts/FlowCreditMarket.cdc

Large diffs are not rendered by default.

2 changes: 0 additions & 2 deletions cadence/contracts/mocks/MockDexSwapper.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -87,5 +87,3 @@ access(all) contract MockDexSwapper {
access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) { self.uniqueID = id }
}
}


13 changes: 0 additions & 13 deletions cadence/scripts/flow-credit-market/quote_liquidation.cdc

This file was deleted.

667 changes: 462 additions & 205 deletions cadence/tests/liquidation_phase1_test.cdc

Large diffs are not rendered by default.

86 changes: 0 additions & 86 deletions cadence/tests/liquidation_phase2_dex_test.cdc

This file was deleted.

23 changes: 23 additions & 0 deletions cadence/tests/test_helpers.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,17 @@ fun getPositionDetails(pid: UInt64, beFailed: Bool): FlowCreditMarket.PositionDe
return res.returnValue as! FlowCreditMarket.PositionDetails
}

access(all)
fun getPositionBalance(pid: UInt64, vaultID: String): FlowCreditMarket.PositionBalance {
let positionDetails = getPositionDetails(pid: pid, beFailed: false)
for bal in positionDetails.balances {
if bal.vaultType == CompositeType(vaultID) {
return bal
}
}
panic("expected to find balance for \(vaultID) in position\(pid)")
}

access(all)
fun poolExists(address: Address): Bool {
let res = _executeScript("../scripts/flow-credit-market/pool_exists.cdc", [address])
Expand Down Expand Up @@ -429,6 +440,18 @@ fun mintMoet(signer: Test.TestAccount, to: Address, amount: UFix64, beFailed: Bo
Test.expect(mintRes, beFailed ? Test.beFailed() : Test.beSucceeded())
}

access(all)
fun setupMockYieldTokenVault(_ signer: Test.TestAccount, beFailed: Bool) {
let setupRes = _executeTransaction("../transactions/mocks/yieldtoken/setup_vault.cdc", [], signer)
Test.expect(setupRes, beFailed ? Test.beFailed() : Test.beSucceeded())
}

access(all)
fun mintMockYieldToken(signer: Test.TestAccount, to: Address, amount: UFix64, beFailed: Bool) {
let mintRes = _executeTransaction("../transactions/mocks/yieldtoken/mint.cdc", [to, amount], signer)
Test.expect(mintRes, beFailed ? Test.beFailed() : Test.beSucceeded())
}


// Transfer Flow tokens from service account to recipient
access(all)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import "FungibleToken"
import "FungibleTokenMetadataViews"
import "MetadataViews"

import "FlowCreditMarket"

/// Attempt to liquidation a position by repaying `repayAmount`.
/// This TESTING-ONLY transaction allows specifying a different repayment vault type.
///
/// debtVaultIdentifier: e.g., Type<@MOET.Vault>().identifier
/// seizeVaultIdentifier: e.g., Type<@FlowToken.Vault>().identifier
transaction(pid: UInt64, purportedDebtVaultIdentifier: String, actualDebtVaultIdentifier: String, seizeVaultIdentifier: String, seizeAmount: UFix64, repayAmount: UFix64) {
let pool: &FlowCreditMarket.Pool
let receiver: &{FungibleToken.Receiver}
let actualDebtType: Type
let purportedDebtType: Type
let seizeType: Type
let repay: @{FungibleToken.Vault}

prepare(signer: auth(BorrowValue, SaveValue, IssueStorageCapabilityController, PublishCapability, UnpublishCapability) &Account) {
let protocolAddress = Type<@FlowCreditMarket.Pool>().address!
self.pool = getAccount(protocolAddress).capabilities.borrow<&FlowCreditMarket.Pool>(FlowCreditMarket.PoolPublicPath)
?? panic("Could not borrow Pool at \(FlowCreditMarket.PoolPublicPath)")

// Resolve types
self.actualDebtType = CompositeType(actualDebtVaultIdentifier) ?? panic("Invalid actualDebtVaultIdentifier: \(actualDebtVaultIdentifier)")
self.purportedDebtType = CompositeType(purportedDebtVaultIdentifier) ?? panic("Invalid purportedDebtVaultIdentifier: \(purportedDebtVaultIdentifier)")
self.seizeType = CompositeType(seizeVaultIdentifier) ?? panic("Invalid seizeVaultIdentifier: \(seizeVaultIdentifier)")

// Get the path and type data for the provided token type identifier
let debtVaultData = MetadataViews.resolveContractViewFromTypeIdentifier(
resourceTypeIdentifier: actualDebtVaultIdentifier,
viewType: Type<FungibleTokenMetadataViews.FTVaultData>()
) as? FungibleTokenMetadataViews.FTVaultData
?? panic("Could not construct valid FT type and view from identifier \(actualDebtVaultIdentifier)")

let seizeVaultData = MetadataViews.resolveContractViewFromTypeIdentifier(
resourceTypeIdentifier: seizeVaultIdentifier,
viewType: Type<FungibleTokenMetadataViews.FTVaultData>()
) as? FungibleTokenMetadataViews.FTVaultData
?? panic("Could not construct valid FT type and view from identifier \(seizeVaultIdentifier)")

// Check if the service account has a vault for this token type at the correct storage path
let debtVaultRef = signer.storage.borrow<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>(from: debtVaultData.storagePath)
?? panic("no debt vault in storage at path \(debtVaultData.storagePath)")
assert(debtVaultRef.balance >= repayAmount, message: "Insufficient debt token \(debtVaultRef.getType().identifier) balance \(debtVaultRef.balance)<\(repayAmount)")
self.repay <- debtVaultRef.withdraw(amount: repayAmount)

let seizeVaultRef = signer.capabilities.borrow<&{FungibleToken.Receiver}>(seizeVaultData.receiverPath)
?? panic("no seize receiver in storage at path \(seizeVaultData.receiverPath)")
self.receiver = seizeVaultRef
}

execute {
let seizedVault <- self.pool.manualLiquidation(
pid: pid,
debtType: self.purportedDebtType,
seizeType: self.seizeType,
seizeAmount: seizeAmount,
repayment: <-self.repay
)

self.receiver.deposit(from: <-seizedVault)
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import "FungibleToken"
import "FungibleTokenMetadataViews"
import "MetadataViews"

import "FlowCreditMarket"

/// Attempt to liquidation a position by repaying `repayAmount`.
///
/// debtVaultIdentifier: e.g., Type<@MOET.Vault>().identifier
/// seizeVaultIdentifier: e.g., Type<@FlowToken.Vault>().identifier
transaction(pid: UInt64, debtVaultIdentifier: String, seizeVaultIdentifier: String, seizeAmount: UFix64, repayAmount: UFix64) {
let pool: &FlowCreditMarket.Pool
let receiver: &{FungibleToken.Receiver}
let debtType: Type
let seizeType: Type
let repay: @{FungibleToken.Vault}

prepare(signer: auth(BorrowValue, SaveValue, IssueStorageCapabilityController, PublishCapability, UnpublishCapability) &Account) {
let protocolAddress = Type<@FlowCreditMarket.Pool>().address!
self.pool = getAccount(protocolAddress).capabilities.borrow<&FlowCreditMarket.Pool>(FlowCreditMarket.PoolPublicPath)
?? panic("Could not borrow Pool at \(FlowCreditMarket.PoolPublicPath)")

// Resolve types
self.debtType = CompositeType(debtVaultIdentifier) ?? panic("Invalid debtVaultIdentifier: \(debtVaultIdentifier)")
self.seizeType = CompositeType(seizeVaultIdentifier) ?? panic("Invalid seizeVaultIdentifier: \(seizeVaultIdentifier)")

// Get the path and type data for the provided token type identifier
let debtVaultData = MetadataViews.resolveContractViewFromTypeIdentifier(
resourceTypeIdentifier: debtVaultIdentifier,
viewType: Type<FungibleTokenMetadataViews.FTVaultData>()
) as? FungibleTokenMetadataViews.FTVaultData
?? panic("Could not construct valid FT type and view from identifier \(debtVaultIdentifier)")

let seizeVaultData = MetadataViews.resolveContractViewFromTypeIdentifier(
resourceTypeIdentifier: seizeVaultIdentifier,
viewType: Type<FungibleTokenMetadataViews.FTVaultData>()
) as? FungibleTokenMetadataViews.FTVaultData
?? panic("Could not construct valid FT type and view from identifier \(seizeVaultIdentifier)")

// Check if the service account has a vault for this token type at the correct storage path
let debtVaultRef = signer.storage.borrow<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>(from: debtVaultData.storagePath)
?? panic("no debt vault in storage at path \(debtVaultData.storagePath)")
assert(debtVaultRef.balance >= repayAmount, message: "Insufficient debt token \(debtVaultRef.getType().identifier) balance \(debtVaultRef.balance)<\(repayAmount)")
self.repay <- debtVaultRef.withdraw(amount: repayAmount)

let seizeVaultRef = signer.capabilities.borrow<&{FungibleToken.Receiver}>(seizeVaultData.receiverPath)
?? panic("no seize receiver in storage at path \(seizeVaultData.receiverPath)")
self.receiver = seizeVaultRef
}

execute {
let seizedVault <- self.pool.manualLiquidation(
pid: pid,
debtType: self.debtType,
seizeType: self.seizeType,
seizeAmount: seizeAmount,
repayment: <-self.repay
)

self.receiver.deposit(from: <-seizedVault)
}
}
29 changes: 29 additions & 0 deletions cadence/transactions/mocks/yieldtoken/mint.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import "FungibleToken"

import "MockYieldToken"

/// Mints MockYieldToken using the Minter stored in the signer's account and deposits to the recipients MockYieldToken Vault. If the
/// recipient's MockYieldToken Vault is not configured with a public Capability or the signer does not have a MOET Minter
/// stored, the transaction will revert.
///
/// @param to: The recipient's Flow address
/// @param amount: How many MockYieldToken tokens to mint to the recipient's account
///
transaction(to: Address, amount: UFix64) {

let receiver: &{FungibleToken.Vault}
let minter: &MockYieldToken.Minter

prepare(signer: auth(BorrowValue) &Account) {
self.minter = signer.storage.borrow<&MockYieldToken.Minter>(from: MockYieldToken.AdminStoragePath)
?? panic("Could not borrow reference to MOET Minter from signer's account at path \(MockYieldToken.AdminStoragePath)")
self.receiver = getAccount(to).capabilities.borrow<&{FungibleToken.Vault}>(MockYieldToken.VaultPublicPath)
?? panic("Could not borrow reference to MOET Vault from recipient's account at path \(MockYieldToken.VaultPublicPath)")
}

execute {
self.receiver.deposit(
from: <-self.minter.mintTokens(amount: amount)
)
}
}
Loading
Loading