Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions .github/workflows/cadence_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ on:
push:
branches:
- main
- v0
pull_request:
branches:
- main
- v0

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

Large diffs are not rendered by default.

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

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

// -----------------------------------------------------------------------------
// Close Position Precision Test Suite
//
// Tests close position functionality with focus on:
// 1. Balance increases (collateral appreciation)
// 2. Balance falls (collateral depreciation)
// 3. Rounding precision and shortfall tolerance
// -----------------------------------------------------------------------------

access(all) var snapshot: UInt64 = 0

access(all)
fun setup() {
deployContracts()
snapshot = getCurrentBlockHeight()
}

// =============================================================================
// Test 1: Close position with no debt
// =============================================================================
access(all)
fun test_closePosition_noDebt() {
log("\n=== Test: Close Position with No Debt ===")

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

// Create pool & enable token
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)

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

// Open position with pushToDrawDownSink = false (no debt)
let openRes = _executeTransaction(
"../transactions/flow-alp/position/create_position.cdc",
[100.0, FLOW_VAULT_STORAGE_PATH, false],
user
)
Test.expect(openRes, Test.beSucceeded())

// Verify no MOET was borrowed
let moetBalance = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)!
Test.assertEqual(0.0, moetBalance)

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

log("✅ Successfully closed position with no debt")
}

// =============================================================================
// Test 2: Close position with debt
// =============================================================================
access(all)
fun test_closePosition_withDebt() {
log("\n=== Test: Close Position with Debt ===")

// Reset price to 1.0 for this test
setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0)

// Reuse existing pool from previous test
let user = Test.createAccount()
setupMoetVault(user, beFailed: false)
mintFlow(to: user, amount: 1_000.0)
grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user)

// Open position with pushToDrawDownSink = true (creates debt)
let openRes = _executeTransaction(
"../transactions/flow-alp/position/create_position.cdc",
[100.0, FLOW_VAULT_STORAGE_PATH, true],
user
)
Test.expect(openRes, Test.beSucceeded())

// Verify MOET was borrowed
let moetBalance = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)!
log("Borrowed MOET: \(moetBalance)")
Test.assert(moetBalance > 0.0)

// Verify FLOW collateral was deposited
let flowBalanceAfterDeposit = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)!
log("FLOW balance after deposit: \(flowBalanceAfterDeposit)")
Test.assert(flowBalanceAfterDeposit < 1_000.0)

// Close position (ID 1 since test 1 created position 0)
let closeRes = _executeTransaction(
"../transactions/flow-alp/position/repay_and_close_position.cdc",
[UInt64(1)],
user
)
Test.expect(closeRes, Test.beSucceeded())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we verify in the test that the debt was actually repaid and collateral returned to the user's account?


// Verify FLOW collateral was returned
let flowBalanceAfterPositionClosed = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)!
log("FLOW balance after position closed: \(flowBalanceAfterPositionClosed)")
Test.assert(flowBalanceAfterPositionClosed == 1_000.0)

log("✅ Successfully closed position with debt: \(moetBalance) MOET")
}

// =============================================================================
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about test cases with multiple debt/collateral balances?

  • I have N>1 debt balances but only provide M<N sources
  • I have multiple collateral positions

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

multi collateral/debt test case will be invalidated by #184
I'll implement it when we figure out how to treat multi collateral/debt position

// Test 3: Close with precision shortfall after multiple rebalances
// =============================================================================
access(all)
fun test_closePosition_precisionShortfall_multipleRebalances() {
log("\n=== Test: Close with Precision Shortfall (Multiple Rebalances) ===")

// Reset price to 1.0 for this test
setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0)

// Reuse existing pool from previous test
let user = Test.createAccount()
setupMoetVault(user, beFailed: false)
mintFlow(to: user, amount: 1_000.0)
grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user)

// Open position
let openRes = _executeTransaction(
"../transactions/flow-alp/position/create_position.cdc",
[100.0, FLOW_VAULT_STORAGE_PATH, true],
user
)
Test.expect(openRes, Test.beSucceeded())

// Perform rebalances with varying prices to accumulate rounding errors
log("\nRebalance 1: FLOW price = $1.2")
setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.2)
let reb1 = _executeTransaction("../transactions/flow-alp/pool-management/rebalance_position.cdc", [UInt64(2), true], PROTOCOL_ACCOUNT)
Test.expect(reb1, Test.beSucceeded())

log("\nRebalance 2: FLOW price = $1.9")
setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 0.9)
let reb2 = _executeTransaction("../transactions/flow-alp/pool-management/rebalance_position.cdc", [UInt64(2), true], PROTOCOL_ACCOUNT)
Test.expect(reb2, Test.beSucceeded())

log("\nRebalance 3: FLOW price = $1.5")
setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.5)
let reb3 = _executeTransaction("../transactions/flow-alp/pool-management/rebalance_position.cdc", [UInt64(2), true], PROTOCOL_ACCOUNT)
Test.expect(reb3, Test.beSucceeded())

// Get final position state
let finalDetails = getPositionDetails(pid: 2, beFailed: false)
log("\n--- Final State ---")
log("Health: \(finalDetails.health)")
logBalances(finalDetails.balances)

// Close position - may have tiny shortfall due to accumulated rounding
let closeRes = _executeTransaction(
"../transactions/flow-alp/position/repay_and_close_position.cdc",
[UInt64(2)],
user
)
Test.expect(closeRes, Test.beSucceeded())

log("✅ Successfully closed after 3 rebalances (precision shortfall automatically handled)")
}

// =============================================================================
// Test 4: Demonstrate precision with extreme volatility
// =============================================================================
access(all)
fun test_closePosition_extremeVolatility() {
log("\n=== Test: Close After Extreme Price Volatility ===")

// Reset price to 1.0 for this test
setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0)

// Reuse existing pool from previous test
let user = Test.createAccount()
setupMoetVault(user, beFailed: false)
mintFlow(to: user, amount: 1_000.0)
grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user)

// Open position
let openRes = _executeTransaction(
"../transactions/flow-alp/position/create_position.cdc",
[100.0, FLOW_VAULT_STORAGE_PATH, true],
user
)
Test.expect(openRes, Test.beSucceeded())

// Simulate extreme volatility: 5x gains, 90% drops
let extremePrices: [UFix64] = [5.0, 0.5, 3.0, 0.2, 4.0, 0.1, 2.0]

var volCount = 1
for price in extremePrices {
log("\nExtreme volatility \(volCount): FLOW = $\(price)")
setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: price)

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

let details = getPositionDetails(pid: 3, beFailed: false)
log("Health: \(details.health)")
volCount = volCount + 1
}

log("\n--- Closing after extreme volatility ---")

// Close position
let closeRes = _executeTransaction(
"../transactions/flow-alp/position/repay_and_close_position.cdc",
[UInt64(3)],
user
)
Test.expect(closeRes, Test.beSucceeded())

log("✅ Successfully closed after extreme volatility (balance increased/fell dramatically)")
}

// =============================================================================
// Test 5: Close position with insufficient debt repayment
// =============================================================================
access(all)
fun test_closePosition_insufficientRepayment() {
log("\n=== Test: Close Position with Insufficient Debt Repayment ===")

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

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

// Open position with debt — borrowed MOET is pushed to user's MOET vault (position 7)
let openRes = _executeTransaction(
"../transactions/flow-alp/position/create_position.cdc",
[100.0, FLOW_VAULT_STORAGE_PATH, true],
user
)
Test.expect(openRes, Test.beSucceeded())

let debt = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)!
log("Borrowed MOET (= debt): \(debt)")
Test.assert(debt > 0.0)

let shortfall = 0.00000001

// Transfer a tiny amount away so user has (debt - 1 satoshi), one short of what's needed
let other = Test.createAccount()
setupMoetVault(other, beFailed: false)
let transferTx = Test.Transaction(
code: Test.readFile("../transactions/moet/transfer_moet.cdc"),
authorizers: [user.address],
signers: [user],
arguments: [other.address, shortfall]
)
let transferRes = Test.executeTransaction(transferTx)
Test.expect(transferRes, Test.beSucceeded())

let remainingMoet = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)!
log("MOET remaining after transfer: \(remainingMoet)")
Test.assertEqual(debt - shortfall, remainingMoet)

// Attempt to close — source has 0 MOET but debt requires repayment
let closeRes = _executeTransaction(
"../transactions/flow-alp/position/repay_and_close_position.cdc",
[UInt64(4)],
user
)
Test.expect(closeRes, Test.beFailed())
Test.assertError(closeRes, errorMessage: "Insufficient funds from source")
log("✅ Close correctly failed with insufficient repayment")
}

Loading
Loading