Skip to content
Merged
Show file tree
Hide file tree
Changes from 49 commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
fdc137d
relax dust balance
nialexsan Feb 13, 2026
20f9d81
Merge branch 'nialexsan/update-ref-20260211' into nialexsan/relax-rem…
nialexsan Feb 13, 2026
0a464a4
Merge branch 'main' into nialexsan/relax-remaining-balance
nialexsan Feb 13, 2026
ebe265a
restore relax
nialexsan Feb 13, 2026
8fd49d3
fix typo
nialexsan Feb 13, 2026
760b53d
update ref
nialexsan Feb 17, 2026
1833914
Merge remote-tracking branch 'origin/main' into nialexsan/relax-remai…
nialexsan Feb 19, 2026
8bcc4e2
Merge remote-tracking branch 'origin/main' into nialexsan/relax-remai…
nialexsan Feb 19, 2026
015a9d8
ref bridge exact
nialexsan Feb 19, 2026
972ac4c
update ref
nialexsan Feb 19, 2026
d9970e3
update ref
nialexsan Feb 20, 2026
3417621
Merge branch 'main' into nialexsan/relax-remaining-balance
nialexsan Feb 24, 2026
f158d12
close position method
nialexsan Feb 24, 2026
2a2e552
fix assertion
nialexsan Feb 24, 2026
8e0f6f7
close position
nialexsan Feb 25, 2026
824c388
Apply suggestion from @nialexsan
nialexsan Feb 25, 2026
47a9e67
Apply suggestion from @nialexsan
nialexsan Feb 25, 2026
94df1fc
fix lock
nialexsan Feb 25, 2026
76d6b6c
round up debt
nialexsan Feb 25, 2026
94ae8ce
repayment balance check
nialexsan Feb 25, 2026
77ab388
update deps
nialexsan Feb 25, 2026
ed16a72
revert unnecessary changes
nialexsan Feb 25, 2026
ad92e44
Apply suggestion from @nialexsan
nialexsan Feb 25, 2026
ebf1c8c
remove buffer
nialexsan Feb 25, 2026
253be6f
close position test
nialexsan Feb 25, 2026
ca37d21
add safe rounding
nialexsan Feb 26, 2026
b8366f5
Merge branch 'main' into nialexsan/relax-remaining-balance
nialexsan Feb 26, 2026
edf96dc
fix merge
nialexsan Feb 26, 2026
0b1831c
use vaults array
nialexsan Feb 28, 2026
cd7c16c
Apply suggestion from @nialexsan
nialexsan Feb 28, 2026
f5d99da
fix event
nialexsan Feb 28, 2026
cbd9f10
Merge remote-tracking branch 'origin/nialexsan/pre-refactor' into nia…
nialexsan Feb 28, 2026
182a5ff
run ci/cd
nialexsan Feb 28, 2026
aed49a1
fully repay debt
nialexsan Feb 28, 2026
355e1c6
tweaks
nialexsan Feb 28, 2026
d9f0b6c
tweaks
nialexsan Feb 28, 2026
6c722af
tweaks
nialexsan Feb 28, 2026
1b42f8a
tweaks
nialexsan Feb 28, 2026
f2b7859
fix position direction
nialexsan Mar 2, 2026
c008602
address PR comments
nialexsan Mar 3, 2026
a11028e
remove redundent seen check
nialexsan Mar 3, 2026
c864297
split into helper functions
nialexsan Mar 3, 2026
0ed237b
remove unnecessary struct
nialexsan Mar 3, 2026
252e658
address comments
nialexsan Mar 3, 2026
7a769c8
handle queued deposits
nialexsan Mar 3, 2026
0117b9c
Apply suggestions from code review
nialexsan Mar 3, 2026
5e19226
address PR comments, add queued deposits test
nialexsan Mar 3, 2026
cb98c6b
fix looping
nialexsan Mar 3, 2026
ae8e3b9
one source per debt
nialexsan Mar 4, 2026
530f342
address comments, remove unnecessary tests
nialexsan Mar 4, 2026
901a226
tweak tests
nialexsan Mar 4, 2026
f5de92d
address comments
nialexsan Mar 4, 2026
114fa5c
tweak ci/cd
nialexsan Mar 4, 2026
1b3c6fa
rename test
nialexsan Mar 4, 2026
f43e2e0
remove unused test
nialexsan Mar 4, 2026
2531367
Apply suggestions from code review
nialexsan Mar 4, 2026
918c314
Apply suggestions from code review
nialexsan Mar 4, 2026
434e177
Fix stale async queue entries on close and add regression test
liobrasil Mar 4, 2026
cb17a21
Optimize closePosition queue removal to linear scan
liobrasil Mar 4, 2026
a1fbfb1
Apply suggestions from code review
nialexsan Mar 4, 2026
79bdb58
Merge branch 'nialexsan/close-position' into lionel/fix-close-positio…
liobrasil Mar 4, 2026
fa07368
Merge branch 'v0' into nialexsan/close-position
liobrasil Mar 4, 2026
4a93edd
Merge branch 'nialexsan/close-position' into lionel/fix-close-positio…
liobrasil Mar 4, 2026
047a6a7
Merge pull request #198 from onflow/lionel/fix-close-position-stale-a…
nialexsan Mar 4, 2026
6dafb4e
Remove closed Position resource from manager after close
liobrasil Mar 4, 2026
ce96de4
address comments
nialexsan Mar 5, 2026
3f464d7
Update cadence/transactions/flow-alp/position/repay_and_close_positio…
nialexsan Mar 5, 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
1 change: 1 addition & 0 deletions .github/workflows/cadence_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:
pull_request:
branches:
- main
- nialexsan/pre-refactor

jobs:
tests:
Expand Down
386 changes: 380 additions & 6 deletions cadence/contracts/FlowALPv0.cdc

Large diffs are not rendered by default.

331 changes: 331 additions & 0 deletions cadence/tests/close_position_dust_return_test.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,331 @@
import Test
import BlockchainHelpers

import "MOET"
import "FlowToken"
import "FlowALPv0"
import "FlowALPMath"
import "test_helpers.cdc"

// -----------------------------------------------------------------------------
// Close Position: Dust Return from Rounding Error Test
//
// This test demonstrates that when the protocol withdraws more from a source
// than the actual internal debt (due to conservative rounding UP), the excess
// "dust" is correctly returned to the user as collateral.
//
// Strategy:
// 1. Create position with debt
// 2. Use oracle price changes to create complex internal debt values
// 3. The debt has high precision at UFix128 level (many decimal places)
// 4. When converted to UFix64 and rounded UP, there's a measurable difference
// 5. The excess withdrawn from source becomes credit and is returned
// -----------------------------------------------------------------------------

access(all) var snapshot: UInt64 = 0

access(all)
fun safeReset() {
let cur = getCurrentBlockHeight()
if cur > snapshot {
Test.reset(to: snapshot)
}
}

access(all)
fun setup() {
deployContracts()
createAndStorePool(signer: PROTOCOL_ACCOUNT, defaultTokenIdentifier: MOET_TOKEN_IDENTIFIER, beFailed: false)
snapshot = getCurrentBlockHeight()
}

// =============================================================================
// Test: Dust return via oracle price manipulation
// =============================================================================
access(all)
fun test_closePosition_dustReturnFromRounding() {
safeReset()
log("\n=== Test: Dust Return from Rounding Error (via Price Changes) ===")

// Start with price = 1.0
setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0)

// Configure token with high limits
addSupportedTokenZeroRateCurve(
signer: PROTOCOL_ACCOUNT,
tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER,
collateralFactor: 0.8,
borrowFactor: 1.0,
depositRate: 1_000_000.0,
depositCapacityCap: 1_000_000.0
)

let user = Test.createAccount()
setupMoetVault(user, beFailed: false)
mintFlow(to: user, amount: 10_000.0)
grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user)

// Step 1: Open position with 1000 FLOW and borrow MOET
log("\n📍 Step 1: Open position with 1000 FLOW")
let openRes = _executeTransaction(
"../transactions/flow-alp/position/create_position.cdc",
[1000.0, FLOW_VAULT_STORAGE_PATH, true], // pushToDrawDownSink = true to borrow
user
)
Test.expect(openRes, Test.beSucceeded())

let positionDetails1 = getPositionDetails(pid: UInt64(0), beFailed: false)
var initialDebt: UFix64 = 0.0
for balance in positionDetails1.balances {
if balance.vaultType == Type<@MOET.Vault>() && balance.direction == FlowALPv0.BalanceDirection.Debit {
initialDebt = balance.balance
}
}
log("Initial MOET debt: ".concat(initialDebt.toString()))

// Step 2: Change price to create complex internal state
// Price changes cause health calculations and potential rebalancing
log("\n📍 Step 2: Change Flow price to 1.12345678")
setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.12345678)

// Force rebalance to apply price change effects (must be signed by pool owner)
let rebalance1 = _executeTransaction(
"../transactions/flow-alp/pool-management/rebalance_position.cdc",
[UInt64(0), true],
PROTOCOL_ACCOUNT
)
Test.expect(rebalance1, Test.beSucceeded())

// Step 3: Change price again to accumulate more precision
log("\n📍 Step 3: Change Flow price to 0.98765432")
setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 0.98765432)

let rebalance2 = _executeTransaction(
"../transactions/flow-alp/pool-management/rebalance_position.cdc",
[UInt64(0), true],
PROTOCOL_ACCOUNT
)
Test.expect(rebalance2, Test.beSucceeded())

// Step 4: Change price to a value with many decimal places
log("\n📍 Step 4: Change Flow price to 1.11111111 (many decimals)")
setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.11111111)

let rebalance3 = _executeTransaction(
"../transactions/flow-alp/pool-management/rebalance_position.cdc",
[UInt64(0), true],
PROTOCOL_ACCOUNT
)
Test.expect(rebalance3, Test.beSucceeded())

// Step 5: Deposit a fractional amount to create more precision
log("\n📍 Step 5: Deposit fractional Flow to create precision")
let depositRes = _executeTransaction(
"./transactions/position/deposit_to_position_by_id.cdc",
[UInt64(0), 123.45678901, FLOW_VAULT_STORAGE_PATH, false],
user
)
Test.expect(depositRes, Test.beSucceeded())

// Step 6: Get debt details BEFORE closing
log("\n📍 Step 6: Check debt before closure")
let positionDetailsBefore = getPositionDetails(pid: UInt64(0), beFailed: false)

var moetDebtUFix64: UFix64 = 0.0
log("Position balances:")
for balance in positionDetailsBefore.balances {
log(" - ".concat(balance.vaultType.identifier)
.concat(": ")
.concat(balance.balance.toString())
.concat(" (")
.concat(balance.direction == FlowALPv0.BalanceDirection.Credit ? "Credit" : "Debit")
.concat(")"))

if balance.vaultType == Type<@MOET.Vault>() && balance.direction == FlowALPv0.BalanceDirection.Debit {
moetDebtUFix64 = balance.balance
}
}

log("\n🔍 MOET debt (rounded UP to UFix64): ".concat(moetDebtUFix64.toString()))
Test.assert(moetDebtUFix64 > 0.0, message: "Position should have MOET debt")

// Step 7: Get balances before close
let moetBalanceBefore = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)!
let flowBalanceBefore = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)!

log("\n💰 Balances before closure:")
log(" User MOET balance: ".concat(moetBalanceBefore.toString()))
log(" User Flow balance: ".concat(flowBalanceBefore.toString()))

// Step 8: Close position
// The protocol will:
// 1. Get debt as UFix64 (rounded UP from internal UFix128)
// 2. Withdraw that amount from VaultSource (exact amount)
// 3. Deposit to position - if rounded debt > actual debt, excess becomes credit
// 4. Return all credits including the dust overpayment
log("\n📍 Step 8: Close position")
let closeRes = _executeTransaction(
"../transactions/flow-alp/position/repay_and_close_position.cdc",
[UInt64(0)],
user
)
Test.expect(closeRes, Test.beSucceeded())

// Step 9: Check final balances
let moetBalanceAfter = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)!
let flowBalanceAfter = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)!

log("\n💰 Balances after closure:")
log(" User MOET balance: ".concat(moetBalanceAfter.toString()))
log(" User Flow balance: ".concat(flowBalanceAfter.toString()))

let flowChange = flowBalanceAfter - flowBalanceBefore

log("\n📊 Changes:")
if moetBalanceAfter >= moetBalanceBefore {
let moetGain = moetBalanceAfter - moetBalanceBefore
log(" MOET change: +".concat(moetGain.toString()).concat(" (DUST RETURNED!)"))
} else {
let moetUsed = moetBalanceBefore - moetBalanceAfter
log(" MOET change: -".concat(moetUsed.toString()).concat(" (used for debt repayment)"))
}
log(" Flow change: +".concat(flowChange.toString()).concat(" (collateral returned)"))

// Assertions
Test.assert(flowChange > 1000.0, message: "Should receive back collateral (1000+ Flow)")

// Key assertion: Check if there's measurable MOET dust returned
// Due to conservative rounding UP of debt, there may be a tiny overpayment
// that gets returned as MOET collateral
if moetBalanceAfter > 0.0 {
log("\n✨ DUST DETECTED! ✨")
log("🔬 MOET dust returned: ".concat(moetBalanceAfter.toString()))
log("📝 This is the overpayment from conservative rounding (UFix128 → UFix64)")
log("💡 The protocol withdrew more than the actual internal debt")
log(" and correctly returned the excess as collateral!")

// The dust should be very small (< 0.01 MOET)
Test.assert(moetBalanceAfter < 0.01, message: "Dust should be very small")
} else {
log("\n📝 No measurable MOET dust at UFix64 precision")
log(" (Overpayment may exist at UFix128 level but rounds to zero at UFix64)")
log(" Try with more extreme price changes or fractional operations")
}

log("\n✅ Position closed successfully")
log("✅ Debt was repaid with conservative rounding UP")
log("✅ Any overpayment dust was correctly returned as collateral")
}

// =============================================================================
// Test 2: Extreme price volatility to maximize rounding error
// =============================================================================
access(all)
fun test_closePosition_extremePriceVolatility() {
safeReset()
log("\n=== Test: Extreme Price Volatility for Maximum Rounding Error ===")

// Start with a non-round price
setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.33333333)

addSupportedTokenZeroRateCurve(
signer: PROTOCOL_ACCOUNT,
tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER,
collateralFactor: 0.75, // 0.75 creates more complex calculations
borrowFactor: 0.95, // Non-1.0 borrow factor adds complexity
depositRate: 1_000_000.0,
depositCapacityCap: 1_000_000.0
)

let user = Test.createAccount()
setupMoetVault(user, beFailed: false)
mintFlow(to: user, amount: 10_000.0)
grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user)

// Open position with a fractional amount
log("\n📍 Open position with 777.77777701 FLOW (fractional)")
let openRes = _executeTransaction(
"../transactions/flow-alp/position/create_position.cdc",
[777.77777701, FLOW_VAULT_STORAGE_PATH, true],
user
)
Test.expect(openRes, Test.beSucceeded())

// Extreme price swings with fractional values
let prices = [1.98765432, 0.54321098, 2.11111111, 0.77777777, 1.45678901]
var priceIndex = 0

while priceIndex < prices.length {
let price = prices[priceIndex]
log("\n🔄 Price change #".concat(priceIndex.toString()).concat(": ").concat(price.toString()))
setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: price)

let rebalanceRes = _executeTransaction(
"../transactions/flow-alp/pool-management/rebalance_position.cdc",
[UInt64(0), true],
PROTOCOL_ACCOUNT
)
Test.expect(rebalanceRes, Test.beSucceeded())

priceIndex = priceIndex + 1
}

// Multiple fractional deposits to accumulate precision
log("\n📍 Multiple fractional deposits")
let depositAmounts = [11.11111101, 22.22222202, 33.33333303]
var depositIndex = 0

while depositIndex < depositAmounts.length {
let amount = depositAmounts[depositIndex]
let depositRes = _executeTransaction(
"./transactions/position/deposit_to_position_by_id.cdc",
[UInt64(0), amount, FLOW_VAULT_STORAGE_PATH, false],
user
)
Test.expect(depositRes, Test.beSucceeded())
depositIndex = depositIndex + 1
}

// Check debt before closure
let positionDetails = getPositionDetails(pid: UInt64(0), beFailed: false)
var moetDebt: UFix64 = 0.0
for balance in positionDetails.balances {
if balance.vaultType == Type<@MOET.Vault>() && balance.direction == FlowALPv0.BalanceDirection.Debit {
moetDebt = balance.balance
log("\n💵 MOET debt (UFix64): ".concat(moetDebt.toString()))
}
}

let moetBefore = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)!
let flowBefore = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)!

// Close position
log("\n📍 Closing position...")
let closeRes = _executeTransaction(
"../transactions/flow-alp/position/repay_and_close_position.cdc",
[UInt64(0)],
user
)
Test.expect(closeRes, Test.beSucceeded())

let moetAfter = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)!
let flowAfter = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)!

log("\n📊 Final Results:")
log(" MOET before: ".concat(moetBefore.toString()).concat(" → after: ").concat(moetAfter.toString()))
log(" Flow before: ".concat(flowBefore.toString()).concat(" → after: ").concat(flowAfter.toString()))

if moetAfter > 0.0 {
log("\n✨✨✨ SUCCESS! DUST RETURNED! ✨✨✨")
log("🎯 MOET dust: ".concat(moetAfter.toString()))
log("🔬 This proves the protocol correctly returns overpayment dust")
log("📐 Rounding UFix128 debt UP to UFix64 created measurable excess")
log("✅ The excess was deposited, flipped to credit, and returned!")
} else {
log("\n📝 Even with extreme volatility, dust is below UFix64 precision")
log(" The mechanism is still working at UFix128 level internally")
}

Test.assert(flowAfter > flowBefore, message: "Should receive Flow collateral back")
log("\n✅ Test completed successfully")
}
Loading
Loading