Skip to content

Commit ee6fb77

Browse files
authored
Merge pull request #188 from onflow/nialexsan/close-position
Nialexsan/close position
2 parents c979d63 + 3f464d7 commit ee6fb77

11 files changed

Lines changed: 996 additions & 59 deletions

File tree

.github/workflows/cadence_tests.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ on:
55
push:
66
branches:
77
- main
8+
- v0
89
pull_request:
910
branches:
1011
- main
12+
- v0
1113

1214
jobs:
1315
tests:

cadence/contracts/FlowALPv0.cdc

Lines changed: 400 additions & 6 deletions
Large diffs are not rendered by default.
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
import Test
2+
import BlockchainHelpers
3+
4+
import "MOET"
5+
import "FlowALPv0"
6+
import "FlowALPMath"
7+
import "test_helpers.cdc"
8+
9+
// -----------------------------------------------------------------------------
10+
// Close Position Precision Test Suite
11+
//
12+
// Tests close position functionality with focus on:
13+
// 1. Balance increases (collateral appreciation)
14+
// 2. Balance falls (collateral depreciation)
15+
// 3. Rounding precision and shortfall tolerance
16+
// -----------------------------------------------------------------------------
17+
18+
access(all) var snapshot: UInt64 = 0
19+
20+
access(all)
21+
fun setup() {
22+
deployContracts()
23+
snapshot = getCurrentBlockHeight()
24+
}
25+
26+
// =============================================================================
27+
// Test 1: Close position with no debt
28+
// =============================================================================
29+
access(all)
30+
fun test_closePosition_noDebt() {
31+
log("\n=== Test: Close Position with No Debt ===")
32+
33+
// Setup: price = 1.0
34+
setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0)
35+
36+
// Create pool & enable token
37+
createAndStorePool(signer: PROTOCOL_ACCOUNT, defaultTokenIdentifier: MOET_TOKEN_IDENTIFIER, beFailed: false)
38+
addSupportedTokenZeroRateCurve(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, collateralFactor: 0.8, borrowFactor: 1.0, depositRate: 1_000_000.0, depositCapacityCap: 1_000_000.0)
39+
40+
let user = Test.createAccount()
41+
setupMoetVault(user, beFailed: false)
42+
mintFlow(to: user, amount: 1_000.0)
43+
grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user)
44+
45+
// Open position with pushToDrawDownSink = false (no debt)
46+
let openRes = _executeTransaction(
47+
"../transactions/flow-alp/position/create_position.cdc",
48+
[100.0, FLOW_VAULT_STORAGE_PATH, false],
49+
user
50+
)
51+
Test.expect(openRes, Test.beSucceeded())
52+
53+
// Verify no MOET was borrowed
54+
let moetBalance = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)!
55+
Test.assertEqual(0.0, moetBalance)
56+
57+
// Close position (ID 0)
58+
let closeRes = _executeTransaction(
59+
"../transactions/flow-alp/position/repay_and_close_position.cdc",
60+
[UInt64(0)],
61+
user
62+
)
63+
Test.expect(closeRes, Test.beSucceeded())
64+
65+
log("✅ Successfully closed position with no debt")
66+
}
67+
68+
// =============================================================================
69+
// Test 2: Close position with debt
70+
// =============================================================================
71+
access(all)
72+
fun test_closePosition_withDebt() {
73+
log("\n=== Test: Close Position with Debt ===")
74+
75+
// Reset price to 1.0 for this test
76+
setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0)
77+
78+
// Reuse existing pool from previous test
79+
let user = Test.createAccount()
80+
setupMoetVault(user, beFailed: false)
81+
mintFlow(to: user, amount: 1_000.0)
82+
grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user)
83+
84+
// Open position with pushToDrawDownSink = true (creates debt)
85+
let openRes = _executeTransaction(
86+
"../transactions/flow-alp/position/create_position.cdc",
87+
[100.0, FLOW_VAULT_STORAGE_PATH, true],
88+
user
89+
)
90+
Test.expect(openRes, Test.beSucceeded())
91+
92+
// Verify MOET was borrowed
93+
let moetBalance = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)!
94+
log("Borrowed MOET: \(moetBalance)")
95+
Test.assert(moetBalance > 0.0)
96+
97+
// Verify FLOW collateral was deposited
98+
let flowBalanceAfterDeposit = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)!
99+
log("FLOW balance after deposit: \(flowBalanceAfterDeposit)")
100+
Test.assert(flowBalanceAfterDeposit < 1_000.0)
101+
102+
// Close position (ID 1 since test 1 created position 0)
103+
let closeRes = _executeTransaction(
104+
"../transactions/flow-alp/position/repay_and_close_position.cdc",
105+
[UInt64(1)],
106+
user
107+
)
108+
Test.expect(closeRes, Test.beSucceeded())
109+
110+
// Verify FLOW collateral was returned
111+
let flowBalanceAfterPositionClosed = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)!
112+
log("FLOW balance after position closed: \(flowBalanceAfterPositionClosed)")
113+
Test.assert(flowBalanceAfterPositionClosed == 1_000.0)
114+
115+
log("✅ Successfully closed position with debt: \(moetBalance) MOET")
116+
}
117+
118+
// =============================================================================
119+
// Test 3: Close with precision shortfall after multiple rebalances
120+
// =============================================================================
121+
access(all)
122+
fun test_closePosition_precisionShortfall_multipleRebalances() {
123+
log("\n=== Test: Close with Precision Shortfall (Multiple Rebalances) ===")
124+
125+
// Reset price to 1.0 for this test
126+
setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0)
127+
128+
// Reuse existing pool from previous test
129+
let user = Test.createAccount()
130+
setupMoetVault(user, beFailed: false)
131+
mintFlow(to: user, amount: 1_000.0)
132+
grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user)
133+
134+
// Open position
135+
let openRes = _executeTransaction(
136+
"../transactions/flow-alp/position/create_position.cdc",
137+
[100.0, FLOW_VAULT_STORAGE_PATH, true],
138+
user
139+
)
140+
Test.expect(openRes, Test.beSucceeded())
141+
142+
// Perform rebalances with varying prices to accumulate rounding errors
143+
log("\nRebalance 1: FLOW price = $1.2")
144+
setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.2)
145+
let reb1 = _executeTransaction("../transactions/flow-alp/pool-management/rebalance_position.cdc", [UInt64(2), true], PROTOCOL_ACCOUNT)
146+
Test.expect(reb1, Test.beSucceeded())
147+
148+
log("\nRebalance 2: FLOW price = $1.9")
149+
setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 0.9)
150+
let reb2 = _executeTransaction("../transactions/flow-alp/pool-management/rebalance_position.cdc", [UInt64(2), true], PROTOCOL_ACCOUNT)
151+
Test.expect(reb2, Test.beSucceeded())
152+
153+
log("\nRebalance 3: FLOW price = $1.5")
154+
setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.5)
155+
let reb3 = _executeTransaction("../transactions/flow-alp/pool-management/rebalance_position.cdc", [UInt64(2), true], PROTOCOL_ACCOUNT)
156+
Test.expect(reb3, Test.beSucceeded())
157+
158+
// Get final position state
159+
let finalDetails = getPositionDetails(pid: 2, beFailed: false)
160+
log("\n--- Final State ---")
161+
log("Health: \(finalDetails.health)")
162+
logBalances(finalDetails.balances)
163+
164+
// Close position - may have tiny shortfall due to accumulated rounding
165+
let closeRes = _executeTransaction(
166+
"../transactions/flow-alp/position/repay_and_close_position.cdc",
167+
[UInt64(2)],
168+
user
169+
)
170+
Test.expect(closeRes, Test.beSucceeded())
171+
172+
log("✅ Successfully closed after 3 rebalances (precision shortfall automatically handled)")
173+
}
174+
175+
// =============================================================================
176+
// Test 4: Demonstrate precision with extreme volatility
177+
// =============================================================================
178+
access(all)
179+
fun test_closePosition_extremeVolatility() {
180+
log("\n=== Test: Close After Extreme Price Volatility ===")
181+
182+
// Reset price to 1.0 for this test
183+
setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0)
184+
185+
// Reuse existing pool from previous test
186+
let user = Test.createAccount()
187+
setupMoetVault(user, beFailed: false)
188+
mintFlow(to: user, amount: 1_000.0)
189+
grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user)
190+
191+
// Open position
192+
let openRes = _executeTransaction(
193+
"../transactions/flow-alp/position/create_position.cdc",
194+
[100.0, FLOW_VAULT_STORAGE_PATH, true],
195+
user
196+
)
197+
Test.expect(openRes, Test.beSucceeded())
198+
199+
// Simulate extreme volatility: 5x gains, 90% drops
200+
let extremePrices: [UFix64] = [5.0, 0.5, 3.0, 0.2, 4.0, 0.1, 2.0]
201+
202+
var volCount = 1
203+
for price in extremePrices {
204+
log("\nExtreme volatility \(volCount): FLOW = $\(price)")
205+
setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: price)
206+
207+
let rebalanceRes = _executeTransaction(
208+
"../transactions/flow-alp/pool-management/rebalance_position.cdc",
209+
[UInt64(3), true],
210+
PROTOCOL_ACCOUNT
211+
)
212+
Test.expect(rebalanceRes, Test.beSucceeded())
213+
214+
let details = getPositionDetails(pid: 3, beFailed: false)
215+
log("Health: \(details.health)")
216+
volCount = volCount + 1
217+
}
218+
219+
log("\n--- Closing after extreme volatility ---")
220+
221+
// Close position
222+
let closeRes = _executeTransaction(
223+
"../transactions/flow-alp/position/repay_and_close_position.cdc",
224+
[UInt64(3)],
225+
user
226+
)
227+
Test.expect(closeRes, Test.beSucceeded())
228+
229+
log("✅ Successfully closed after extreme volatility (balance increased/fell dramatically)")
230+
}
231+
232+
// =============================================================================
233+
// Test 5: Close position with insufficient debt repayment
234+
// =============================================================================
235+
access(all)
236+
fun test_closePosition_insufficientRepayment() {
237+
log("\n=== Test: Close Position with Insufficient Debt Repayment ===")
238+
239+
setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0)
240+
241+
let user = Test.createAccount()
242+
setupMoetVault(user, beFailed: false)
243+
mintFlow(to: user, amount: 1_000.0)
244+
grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user)
245+
246+
// Open position with debt — borrowed MOET is pushed to user's MOET vault (position 7)
247+
let openRes = _executeTransaction(
248+
"../transactions/flow-alp/position/create_position.cdc",
249+
[100.0, FLOW_VAULT_STORAGE_PATH, true],
250+
user
251+
)
252+
Test.expect(openRes, Test.beSucceeded())
253+
254+
let debt = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)!
255+
log("Borrowed MOET (= debt): \(debt)")
256+
Test.assert(debt > 0.0)
257+
258+
let shortfall = 0.00000001
259+
260+
// Transfer a tiny amount away so user has (debt - 1 satoshi), one short of what's needed
261+
let other = Test.createAccount()
262+
setupMoetVault(other, beFailed: false)
263+
let transferTx = Test.Transaction(
264+
code: Test.readFile("../transactions/moet/transfer_moet.cdc"),
265+
authorizers: [user.address],
266+
signers: [user],
267+
arguments: [other.address, shortfall]
268+
)
269+
let transferRes = Test.executeTransaction(transferTx)
270+
Test.expect(transferRes, Test.beSucceeded())
271+
272+
let remainingMoet = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)!
273+
log("MOET remaining after transfer: \(remainingMoet)")
274+
Test.assertEqual(debt - shortfall, remainingMoet)
275+
276+
// Attempt to close — source has 0 MOET but debt requires repayment
277+
let closeRes = _executeTransaction(
278+
"../transactions/flow-alp/position/repay_and_close_position.cdc",
279+
[UInt64(4)],
280+
user
281+
)
282+
Test.expect(closeRes, Test.beFailed())
283+
Test.assertError(closeRes, errorMessage: "Insufficient funds from source")
284+
log("✅ Close correctly failed with insufficient repayment")
285+
}
286+

0 commit comments

Comments
 (0)