Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
37 changes: 23 additions & 14 deletions cadence/contracts/FlowALPv0.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -1908,6 +1908,13 @@ access(all) contract FlowALPv0 {
}

/// Collects insurance by withdrawing from reserves and swapping to MOET.
///
/// NOTE: If reserves are insufficient to cover the full calculated fee, collection is skipped
/// entirely and the timestamp is not updated. This interacts with the rate-change
/// collection: if a rate change occurs while reserves are insufficient, the pre-change
/// fees will not be settled under the old rate. When reserves eventually recover, the entire
/// elapsed window — including the period before the rate change — will be collected under the
/// new rate, causing over- or under-collection for that period.
access(self) fun _collectInsurance(
tokenState: auth(FlowALPModels.EImplementation) &{FlowALPModels.TokenState},
reserveVault: auth(FungibleToken.Withdraw) &{FungibleToken.Vault},
Expand Down Expand Up @@ -1935,31 +1942,36 @@ access(all) contract FlowALPv0 {
return nil
}

if reserveVault.balance == 0.0 {
tokenState.setLastInsuranceCollectionTime(currentTime)
if insuranceAmountUFix64 > reserveVault.balance {
// do not collect the insurance fee if the reserve doesn't have enough tokens to cover the full amount
return nil
}

let amountToCollect = insuranceAmountUFix64 > reserveVault.balance ? reserveVault.balance : insuranceAmountUFix64
var insuranceVault <- reserveVault.withdraw(amount: amountToCollect)

let insuranceVault <- reserveVault.withdraw(amount: insuranceAmountUFix64)
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.outType() == Type<@MOET.Vault>(), message: "Insurance swapper must output MOET")

let quote = insuranceSwapper.quoteOut(forProvided: amountToCollect, reverse: false)
let quote = insuranceSwapper.quoteOut(forProvided: insuranceAmountUFix64, reverse: false)
let dexPrice = quote.outAmount / quote.inAmount
assert(
FlowALPMath.dexOraclePriceDeviationInRange(dexPrice: dexPrice, oraclePrice: oraclePrice, maxDeviationBps: maxDeviationBps),
message: "DEX/oracle price deviation too large. Dex price: \(dexPrice), Oracle price: \(oraclePrice)")
message: "DEX/oracle price deviation exceeds \(maxDeviationBps)bps. Dex price: \(dexPrice), Oracle price: \(oraclePrice)",
)
var moetVault <- insuranceSwapper.swap(quote: quote, inVault: <-insuranceVault) as! @MOET.Vault

tokenState.setLastInsuranceCollectionTime(currentTime)
return <-moetVault
}

/// Collects stability funds by withdrawing from reserves.
///
/// NOTE: If reserves are insufficient to cover the full calculated fee, collection is skipped
/// entirely and the timestamp is not updated. This interacts with the rate-change
/// collection: if a rate change occurs while reserves are insufficient, the pre-change
/// fees will not be settled under the old rate. When reserves eventually recover, the entire
/// elapsed window — including the period before the rate change — will be collected under the
/// new rate, causing over- or under-collection for that period.
access(self) fun _collectStability(
tokenState: auth(FlowALPModels.EImplementation) &{FlowALPModels.TokenState},
reserveVault: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}
Expand All @@ -1986,15 +1998,12 @@ access(all) contract FlowALPv0 {
return nil
}

if reserveVault.balance == 0.0 {
tokenState.setLastStabilityFeeCollectionTime(currentTime)
if stabilityAmountUFix64 > reserveVault.balance {
// do not collect the stability fee if the reserve doesn't have enough tokens to cover the full amount
return nil
}

let reserveVaultBalance = reserveVault.balance
let amountToCollect = stabilityAmountUFix64 > reserveVaultBalance ? reserveVaultBalance : stabilityAmountUFix64
let stabilityVault <- reserveVault.withdraw(amount: amountToCollect)

let stabilityVault <- reserveVault.withdraw(amount: stabilityAmountUFix64)
tokenState.setLastStabilityFeeCollectionTime(currentTime)
return <-stabilityVault
}
Expand Down
25 changes: 14 additions & 11 deletions cadence/tests/insurance_collection_test.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,12 @@ fun test_collectInsurance_zeroDebitBalance_returnsNil() {
}

// -----------------------------------------------------------------------------
// Test: collectInsurance only collects up to available reserve balance
// 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)
// Test: collectInsurance does not collect when reserves are insufficient
// If the calculated insurance fee exceeds the reserve balance,
// no insurance fee should be collected and reserves remain unchanged.
// -----------------------------------------------------------------------------
access(all)
fun test_collectInsurance_partialReserves_collectsAvailable() {
fun test_collectInsurance_insufficientReserves() {
// setup LP to provide MOET liquidity for borrowing (small amount to create limited reserves)
let lp = Test.createAccount()
setupMoetVault(lp, beFailed: false)
Expand Down Expand Up @@ -143,20 +142,24 @@ fun test_collectInsurance_partialReserves_collectsAvailable() {
let initialInsuranceBalance = getInsuranceFundBalance()
Test.assertEqual(0.0, initialInsuranceBalance)

let reserveBalanceBefore = getReserveBalance(vaultIdentifier: MOET_TOKEN_IDENTIFIER)
let lastCollectionTimeBefore = getLastInsuranceCollectionTime(tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER)

Test.moveTime(by: ONE_YEAR + DAY * 30.0) // year + month

// collect insurance - should collect up to available reserve balance
// should not collect because reserves are insufficient
collectInsurance(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, beFailed: false)

let finalInsuranceBalance = getInsuranceFundBalance()
let reserveBalanceAfter = getReserveBalance(vaultIdentifier: MOET_TOKEN_IDENTIFIER)
let lastCollectionTimeAfter = getLastStabilityCollectionTime(tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER)

Test.assertEqual(0.0, finalInsuranceBalance)
Test.assertEqual(reserveBalanceBefore, reserveBalanceAfter)

// with 1:1 swap ratio, insurance fund balance should equal amount withdrawn from reserves
Test.assertEqual(0.0, reserveBalanceAfter)
// time should not change
Test.assertEqual(lastCollectionTimeBefore, lastCollectionTimeAfter)

// 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)
}

// -----------------------------------------------------------------------------
Expand Down
23 changes: 13 additions & 10 deletions cadence/tests/stability_collection_test.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,12 @@ 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.
// Test: collectStability does not collect when reserves are insufficient
// If the calculated stability fee exceeds the reserve balance,
// no stability fee should be collected and reserves remain unchanged.
// -----------------------------------------------------------------------------
access(all)
fun test_collectStability_partialReserves_collectsAvailable() {
fun test_collectStability_insufficientReserves() {
// setup LP to provide MOET liquidity for borrowing (small amount to create limited reserves)
let lp = Test.createAccount()
setupMoetVault(lp, beFailed: false)
Expand Down Expand Up @@ -106,21 +106,24 @@ fun test_collectStability_partialReserves_collectsAvailable() {
let initialStabilityBalance = getStabilityFundBalance(tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER)
Test.assertEqual(nil, initialStabilityBalance)

let reserveBalanceBefore = getReserveBalance(vaultIdentifier: MOET_TOKEN_IDENTIFIER)
let lastCollectionTimeBefore = getLastStabilityCollectionTime(tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER)

Test.moveTime(by: ONE_YEAR + DAY * 30.0) // 1 year + 1 month

// collect stability - should collect up to available reserve balance
// should not collect because reserves are insufficient
let res = collectStability(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER)
Test.expect(res, Test.beSucceeded())

let finalStabilityBalance = getStabilityFundBalance(tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER)
let reserveBalanceAfter = getReserveBalance(vaultIdentifier: MOET_TOKEN_IDENTIFIER)
let lastCollectionTimeAfter = getLastStabilityCollectionTime(tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER)

// stability fund balance should equal amount withdrawn from reserves
Test.assertEqual(0.0, reserveBalanceAfter)
Test.assertEqual(nil, finalStabilityBalance)
Test.assertEqual(reserveBalanceBefore, reserveBalanceAfter)

// 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!)
// time should not change
Test.assertEqual(lastCollectionTimeBefore, lastCollectionTimeAfter)
}

// -----------------------------------------------------------------------------
Expand Down
Loading