Skip to content

Nialexsan/close position#188

Merged
nialexsan merged 67 commits intov0from
nialexsan/close-position
Mar 5, 2026
Merged

Nialexsan/close position#188
nialexsan merged 67 commits intov0from
nialexsan/close-position

Conversation

@nialexsan
Copy link
Collaborator

@nialexsan nialexsan commented Feb 28, 2026

duplicate of #163 to a pre-refactor branch

Description

This PR introduces a full closePosition implementation with multi-token support, improved rounding safety, and an extensive precision test suite.

Implemented pool-level closePosition

  • Repays all debts (multi-token)
  • Withdraws all collateral types
  • Uses position locking for safety

Added PositionClosed event with detailed repayment + withdrawal breakdown

Hardened rounding logic

  • Debits round up
  • Credits round down

Added getTotalDebt() returning grouped debt per token type

Added precision & volatility tests (8 scenarios)


For contributor use:

  • Targeted PR against master branch
  • Linked to Github issue with discussion and accepted design OR link to spec that describes this work.
  • Code follows the standards mentioned here.
  • Updated relevant documentation
  • Re-reviewed Files changed in the Github PR explorer
  • Added appropriate labels


// Step 11: Destroy InternalPosition and unlock
destroy self.positions.remove(key: pid)!
self._unlockPosition(pid)
Copy link
Contributor

@liobrasil liobrasil Mar 4, 2026

Choose a reason for hiding this comment

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

Follow-up fix for stale async queue entry after close:
Context: when closePosition destroys the internal position, its pid can still be present in positionsNeedingUpdates. A later asyncUpdate() callback can then hit a stale pid and panic. PR #198 adds queue-stale handling + regression test coverage.

var processed: UInt64 = 0
while self.positionsNeedingUpdates.length > 0 && processed < self.positionsProcessedPerCallback {
let pid = self.positionsNeedingUpdates.removeFirst()
if self.positions[pid] == nil {
Copy link
Member

Choose a reason for hiding this comment

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

Do we need both this and _removePositionFromUpdateQueue, or would one or the other be sufficient? (Mainly for my understanding, I'm fine to keep both and be defensive.)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

technically only one should suffice, but this check is a fallback to make sure that the processing position doesn't panic

access(self) fun _removePositionFromUpdateQueue(pid: UInt64) {
// Keep this operation linear-time:
// find first matching pid, then remove once while preserving queue order.
var i = 0
Copy link
Member

Choose a reason for hiding this comment

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

can use for loop?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

for-in loop doesn't work because we're mutating the container during iteration

// User should have ~9800 FLOW (10000 - 50 - 150)
let expectedAfterDeposit = 10_000.0 - 50.0 - 150.0
equalWithinVariance(flowBalanceAfterDeposit, expectedAfterDeposit)
// Test.assert(flowBalanceAfterDeposit >= expectedAfterDeposit - 1.0, message: "Should have withdrawn full deposit amount")
Copy link
Member

Choose a reason for hiding this comment

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

uncomment or remove

// Final: 10000
let expectedFinal = 10_000.0 // All deposits returned
equalWithinVariance(flowBalanceAfter, expectedFinal)
// Test.assert(flowBalanceAfter >= expectedFinal - 10.0, message: "Should return all deposits (processed + queued)")
Copy link
Member

Choose a reason for hiding this comment

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

uncomment or remove

)
Test.expect(closeRes, Test.beSucceeded())

// Post-conditions: zero debt, collateral redeemed, HF == ceiling
Copy link
Member

Choose a reason for hiding this comment

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

What's the reasoning for removing these assertions?

Copy link
Collaborator Author

@nialexsan nialexsan Mar 5, 2026

Choose a reason for hiding this comment

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

the position is destroyed, and there is no way to get the metrics after closure

)
Test.expect(asyncRes, Test.beSucceeded())

// Step 5: run one more callback to prove queue state remains clean.
Copy link
Member

Choose a reason for hiding this comment

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

Why is this step necessary for the test?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

removed this step

[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?

// Helper Functions
// =============================================================================

access(all) fun logBalances(_ balances: [FlowALPv0.PositionBalance]) {
Copy link
Member

Choose a reason for hiding this comment

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

maybe move this to test_helpers.cdc?

}

access(all)
fun test_closePosition_clearsQueuedAsyncUpdateEntry() {
Copy link
Member

Choose a reason for hiding this comment

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

I would move this test case to close_position_with_queued_deposits_test.cdc and remove this file

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

@nialexsan nialexsan requested a review from jordanschalm March 5, 2026 02:42
…n.cdc

Co-authored-by: Jordan Schalm <jordan.schalm@gmail.com>
@nialexsan nialexsan merged commit ee6fb77 into v0 Mar 5, 2026
1 check passed
@nialexsan nialexsan deleted the nialexsan/close-position branch March 5, 2026 17:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants