Skip to content
Open
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
2 changes: 1 addition & 1 deletion cadence/contracts/FlowALPv0.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,7 @@ access(all) contract FlowALPv0 {
withdrawBal: withdrawBal,
targetHealth: view.minHealth
)
return FlowALPMath.toUFix64Round(uintMax)
return FlowALPMath.toUFix64RoundDown(uintMax)
}

/// Returns the health of the given position, which is the ratio of the position's effective collateral
Expand Down
128 changes: 128 additions & 0 deletions cadence/tests/available_balance_rounding_test.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import Test
import BlockchainHelpers

import "MOET"
import "FlowALPModels"
import "test_helpers.cdc"

// Tests that availableBalance rounds DOWN when converting from UFix128 to UFix64.
// Rounding up could return a value that, if withdrawn/borrowed, would violate the position's minimum health factor.
//
// Token setup:
// FLOW: collateralFactor=0.8, borrowFactor=1.0, price=1.0
// MOET: collateralFactor=1.0, borrowFactor=1.0, price=1.0 (default token)
//
// Scenario:
// Deposit 100 FLOW (no debt). Available MOET borrow = 80 / 1.1 = 72.72727272727...
// UFix64 round-down: 72.72727272
// UFix64 round-half-up: 72.72727273 (incorrect — would breach minHealth)

access(all) let user = Test.createAccount()
access(all) let lp = Test.createAccount()

access(all) var snapshot: UInt64 = 0

access(all)
fun setup() {
deployContracts()

setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0)
setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: MOET_TOKEN_IDENTIFIER, price: 1.0)

createAndStorePool(signer: PROTOCOL_ACCOUNT, defaultTokenIdentifier: MOET_TOKEN_IDENTIFIER, beFailed: false)
addSupportedTokenZeroRateCurve(
signer: PROTOCOL_ACCOUNT,
tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER,
collateralFactor: 0.8,
borrowFactor: 1.0,
depositRate: 1_000_000.0,
depositCapacityCap: 1_000_000.0
)

// Setup LP with MOET liquidity so the user can borrow
setupMoetVault(lp, beFailed: false)
mintMoet(signer: PROTOCOL_ACCOUNT, to: lp.address, amount: 10_000.0, beFailed: false)
grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, lp)
createPosition(admin: PROTOCOL_ACCOUNT, signer: lp, amount: 10_000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false)

// Setup user with FLOW
setupMoetVault(user, beFailed: false)
transferFlowTokens(to: user, amount: 1_000.0)
grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user)

snapshot = getCurrentBlockHeight()
}

access(all)
fun beforeEach() {
if getCurrentBlockHeight() > snapshot {
Test.reset(to: snapshot)
}
}

// ---------------------------------------------------------------------------
// availableBalance should round down, not up, so the returned amount is always
// safe to withdraw without breaching minHealth.
//
// 100 FLOW deposited, no debt:
// effectiveCollateral = 100 * 1.0 * 0.8 = 80
// maxWithdraw(MOET) = 80 / minHealth(1.1) = 72.727272727272... (UFix128)
//
// Round-down to UFix64: 72.72727272
// Round-half-up to UFix64: 72.72727273
//
// The test asserts the round-down value, which fails with toUFix64Round and
// passes with toUFix64RoundDown.
// ---------------------------------------------------------------------------
access(all)
fun test_availableBalance_rounds_down() {
// Open position: deposit 100 FLOW, no auto-borrow
createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 100.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false)
let pid = getLastPositionId()

// availableBalance for MOET (no MOET credit, so this is the pure borrow path)
let available = getAvailableBalance(
pid: pid,
vaultIdentifier: MOET_TOKEN_IDENTIFIER,
pullFromTopUpSource: false,
beFailed: false
)

// 80 / 1.1 = 72.72727272727... → round-down = 72.72727272
let expectedRoundDown: UFix64 = 72.72727272
Test.assert(available == expectedRoundDown,
message: "availableBalance should round down: expected \(expectedRoundDown), got \(available)")
}

// ---------------------------------------------------------------------------
// Verify the safety property: borrowing the full availableBalance amount must
// succeed without breaching minHealth.
// ---------------------------------------------------------------------------
access(all)
fun test_borrowing_full_availableBalance_succeeds() {
createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 100.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false)
let pid = getLastPositionId()

let available = getAvailableBalance(
pid: pid,
vaultIdentifier: MOET_TOKEN_IDENTIFIER,
pullFromTopUpSource: false,
beFailed: false
)

// Borrow the exact amount returned by availableBalance
borrowFromPosition(
signer: user,
positionId: pid,
tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER,
vaultStoragePath: MOET.VaultStoragePath,
amount: available,
beFailed: false
)

// Health should be >= minHealth (1.1)
let health = getPositionHealth(pid: pid, beFailed: false)
Test.assert(health >= 1.1,
message: "Health after borrowing full availableBalance should be >= 1.1, got \(health)")
}

4 changes: 2 additions & 2 deletions cadence/tests/fork_multi_collateral_position_test.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,8 @@ fun test_multi_collateral_position() {

// STEP 6: Test weighted collateral factors - calculate max borrowing
// Max borrow = (effectiveCollateral / minHealth) * borrowFactor / price
// MOET: maxBorrow = ($1325 / 1.1) * 1.0 / $1.00 = 1204.54545455 MOET
let expectedMaxMoet: UFix64 = 1204.54545455
// MOET: maxBorrow = ($1325 / 1.1) * 1.0 / $1.00 = 1204.54545454 MOET (rounded down)
let expectedMaxMoet: UFix64 = 1204.54545454
let availableMoet = getAvailableBalance(pid: pid, vaultIdentifier: MAINNET_MOET_TOKEN_ID, pullFromTopUpSource: false, beFailed: false)
Test.assertEqual(expectedMaxMoet, availableMoet)

Expand Down
Loading