-
Notifications
You must be signed in to change notification settings - Fork 2
Nialexsan/close position #188
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 49 commits
Commits
Show all changes
67 commits
Select commit
Hold shift + click to select a range
fdc137d
relax dust balance
nialexsan 20f9d81
Merge branch 'nialexsan/update-ref-20260211' into nialexsan/relax-rem…
nialexsan 0a464a4
Merge branch 'main' into nialexsan/relax-remaining-balance
nialexsan ebe265a
restore relax
nialexsan 8fd49d3
fix typo
nialexsan 760b53d
update ref
nialexsan 1833914
Merge remote-tracking branch 'origin/main' into nialexsan/relax-remai…
nialexsan 8bcc4e2
Merge remote-tracking branch 'origin/main' into nialexsan/relax-remai…
nialexsan 015a9d8
ref bridge exact
nialexsan 972ac4c
update ref
nialexsan d9970e3
update ref
nialexsan 3417621
Merge branch 'main' into nialexsan/relax-remaining-balance
nialexsan f158d12
close position method
nialexsan 2a2e552
fix assertion
nialexsan 8e0f6f7
close position
nialexsan 824c388
Apply suggestion from @nialexsan
nialexsan 47a9e67
Apply suggestion from @nialexsan
nialexsan 94df1fc
fix lock
nialexsan 76d6b6c
round up debt
nialexsan 94ae8ce
repayment balance check
nialexsan 77ab388
update deps
nialexsan ed16a72
revert unnecessary changes
nialexsan ad92e44
Apply suggestion from @nialexsan
nialexsan ebf1c8c
remove buffer
nialexsan 253be6f
close position test
nialexsan ca37d21
add safe rounding
nialexsan b8366f5
Merge branch 'main' into nialexsan/relax-remaining-balance
nialexsan edf96dc
fix merge
nialexsan 0b1831c
use vaults array
nialexsan cd7c16c
Apply suggestion from @nialexsan
nialexsan f5d99da
fix event
nialexsan cbd9f10
Merge remote-tracking branch 'origin/nialexsan/pre-refactor' into nia…
nialexsan 182a5ff
run ci/cd
nialexsan aed49a1
fully repay debt
nialexsan 355e1c6
tweaks
nialexsan d9f0b6c
tweaks
nialexsan 6c722af
tweaks
nialexsan 1b42f8a
tweaks
nialexsan f2b7859
fix position direction
nialexsan c008602
address PR comments
nialexsan a11028e
remove redundent seen check
nialexsan c864297
split into helper functions
nialexsan 0ed237b
remove unnecessary struct
nialexsan 252e658
address comments
nialexsan 7a769c8
handle queued deposits
nialexsan 0117b9c
Apply suggestions from code review
nialexsan 5e19226
address PR comments, add queued deposits test
nialexsan cb98c6b
fix looping
nialexsan ae8e3b9
one source per debt
nialexsan 530f342
address comments, remove unnecessary tests
nialexsan 901a226
tweak tests
nialexsan f5de92d
address comments
nialexsan 114fa5c
tweak ci/cd
nialexsan 1b3c6fa
rename test
nialexsan f43e2e0
remove unused test
nialexsan 2531367
Apply suggestions from code review
nialexsan 918c314
Apply suggestions from code review
nialexsan 434e177
Fix stale async queue entries on close and add regression test
liobrasil cb17a21
Optimize closePosition queue removal to linear scan
liobrasil a1fbfb1
Apply suggestions from code review
nialexsan 79bdb58
Merge branch 'nialexsan/close-position' into lionel/fix-close-positio…
liobrasil fa07368
Merge branch 'v0' into nialexsan/close-position
liobrasil 4a93edd
Merge branch 'nialexsan/close-position' into lionel/fix-close-positio…
liobrasil 047a6a7
Merge pull request #198 from onflow/lionel/fix-close-position-stale-a…
nialexsan 6dafb4e
Remove closed Position resource from manager after close
liobrasil ce96de4
address comments
nialexsan 3f464d7
Update cadence/transactions/flow-alp/position/repay_and_close_positio…
nialexsan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ on: | |
| pull_request: | ||
| branches: | ||
| - main | ||
| - nialexsan/pre-refactor | ||
|
|
||
| jobs: | ||
| tests: | ||
|
|
||
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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") | ||
nialexsan marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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") | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.