Skip to content

Commit 0b9d08c

Browse files
committed
Merge branch 'main' into taras/173-liquidation-edge-cases
2 parents ba48ea0 + 002e27d commit 0b9d08c

29 files changed

Lines changed: 285 additions & 240 deletions

AGENTS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,9 @@ flow test cadence/tests/interest_accrual_integration_test.cdc --name test_combin
5757
```bash
5858
grep "fun test" cadence/tests/interest_accrual_integration_test.cdc
5959
```
60+
61+
# Cadence Coding Guidelines
62+
63+
- Use string templating, not concatenation: `"Hello \(name)"` not `"Hello ".concat(name)`
64+
- Use `equalWithinVariance` from `test_helpers.cdc` for equality assertions where rounding errors are possible
65+
- Use red-green TDD for bug fixes and extensions to functionality. Tests must use assertions (eg. `Test.assert`) to verify expected behaviour, not logs.

cadence/contracts/FlowALPEvents.cdc

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,11 +141,11 @@ access(all) contract FlowALPEvents {
141141
)
142142

143143
/// Emitted when the insurance rate for a token is updated by governance.
144-
/// The insurance rate is an annual fraction of debit interest diverted to the insurance fund.
144+
/// The insurance rate is a fee of accrued debit interest diverted to the insurance fund.
145145
///
146146
/// @param poolUUID the UUID of the pool containing the token
147147
/// @param tokenType the type identifier string of the token whose rate changed
148-
/// @param insuranceRate the new annual insurance rate (e.g. 0.001 for 0.1%)
148+
/// @param insuranceRate the new insurance fee (e.g. 0.001 for 0.1%)
149149
access(all) event InsuranceRateUpdated(
150150
poolUUID: UInt64,
151151
tokenType: String,
@@ -167,11 +167,11 @@ access(all) contract FlowALPEvents {
167167
)
168168

169169
/// Emitted when the stability fee rate for a token is updated by governance.
170-
/// The stability fee rate is an annual fraction of debit interest diverted to the stability fund.
170+
/// The stability fee rate is a fee of accrued debit interest diverted to the stability fund.
171171
///
172172
/// @param poolUUID the UUID of the pool containing the token
173173
/// @param tokenType the type identifier string of the token whose rate changed
174-
/// @param stabilityFeeRate the new annual stability fee rate (e.g. 0.05 for 5%)
174+
/// @param stabilityFeeRate the new stability fee (e.g. 0.05 for 5%)
175175
access(all) event StabilityFeeRateUpdated(
176176
poolUUID: UInt64,
177177
tokenType: String,

cadence/contracts/FlowALPInterestRates.cdc

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ access(all) contract FlowALPInterestRates {
44
///
55
/// A simple interface to calculate interest rate for a token type.
66
access(all) struct interface InterestCurve {
7-
/// Returns the annual interest rate for the given credit and debit balance, for some token T.
7+
/// Returns the annual nominal interest rate for the given credit and debit balance, for some token T.
88
/// @param creditBalance The credit (deposit) balance of token T
99
/// @param debitBalance The debit (withdrawal) balance of token T
1010
access(all) fun interestRate(creditBalance: UFix128, debitBalance: UFix128): UFix128 {
@@ -19,10 +19,10 @@ access(all) contract FlowALPInterestRates {
1919

2020
/// FixedCurve
2121
///
22-
/// A fixed-rate interest curve implementation that returns a constant yearly interest rate
22+
/// A fixed-rate interest curve implementation that returns a constant nominal yearly interest rate
2323
/// regardless of utilization. This is suitable for stable assets like MOET where predictable
2424
/// rates are desired.
25-
/// @param yearlyRate The fixed yearly interest rate as a UFix128 (e.g., 0.05 for 5% APY)
25+
/// @param yearlyRate The fixed yearly nominal rate as a UFix128 (e.g., 0.05 for a 5% nominal yearly rate)
2626
access(all) struct FixedCurve: InterestCurve {
2727

2828
access(all) let yearlyRate: UFix128
@@ -64,7 +64,7 @@ access(all) contract FlowALPInterestRates {
6464
/// This matches the live TokenState accounting used by FlowALP.
6565
///
6666
/// @param optimalUtilization The target utilization ratio (e.g., 0.80 for 80%)
67-
/// @param baseRate The minimum yearly interest rate (e.g., 0.01 for 1% APY)
67+
/// @param baseRate The minimum yearly nominal rate (e.g., 0.01 for a 1% nominal yearly rate)
6868
/// @param slope1 The total rate increase from 0% to optimal utilization (e.g., 0.04 for 4%)
6969
/// @param slope2 The total rate increase from optimal to 100% utilization (e.g., 0.60 for 60%)
7070
access(all) struct KinkCurve: InterestCurve {

cadence/lib/FlowALPMath.cdc

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,11 @@ access(all) contract FlowALPMath {
9999
return diffBps <= maxDeviationBps
100100
}
101101

102-
/// Converts a yearly interest rate to a per-second multiplication factor (stored in a UFix128 as a fixed point
103-
/// number with 18 decimal places). The input to this function will be just the relative annual interest rate
104-
/// (e.g. 0.05 for 5% interest), and the result will be the per-second multiplier (e.g. 1.000000000001).
102+
/// Converts a nominal yearly interest rate to a per-second multiplication factor (stored in a UFix128 as a fixed
103+
/// point number with 18 decimal places). The input to this function is the relative nominal annual rate
104+
/// (e.g. 0.05 for a 5% nominal yearly rate), and the result is the per-second multiplier
105+
/// (e.g. 1.000000000001). For positive rates, the effective one-year growth will be slightly higher than the
106+
/// nominal rate because interest compounds over time.
105107
access(all) view fun perSecondInterestRate(yearlyRate: UFix128): UFix128 {
106108
let perSecondScaledValue = yearlyRate / 31_557_600.0 // 365.25 * 24.0 * 60.0 * 60.0
107109
assert(
@@ -111,6 +113,19 @@ access(all) contract FlowALPMath {
111113
return perSecondScaledValue + 1.0
112114
}
113115

116+
/// Returns the effective annual yield (EAY) for a given nominal yearly rate, assuming discrete per-second compounding.
117+
///
118+
/// Formula: EAY = (1 + nominalRate / secondsPerYear) ^ secondsPerYear - 1
119+
///
120+
/// For example, a nominal rate of 100% (1.0) produces an effective rate of about 171.8281776413%
121+
/// under discrete per-second compounding: (1 + 1 / 31_557_600) ^ 31_557_600 - 1.
122+
/// This is extremely close to the continuous-compounding limit of e - 1.
123+
access(all) view fun effectiveYearlyRate(nominalYearlyRate: UFix128): UFix128 {
124+
let perSecondRate = FlowALPMath.perSecondInterestRate(yearlyRate: nominalYearlyRate)
125+
let compounded = FlowALPMath.powUFix128(perSecondRate, 31_557_600.0)
126+
return compounded - 1.0
127+
}
128+
114129
/// Returns the compounded interest index reflecting the passage of time
115130
/// The result is: newIndex = oldIndex * perSecondRate ^ seconds
116131
access(all) view fun compoundInterestIndex(

cadence/tests/TEST_COVERAGE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ The `test_helpers.cdc` file provides:
204204
3. **FLOW Debit Interest**
205205
- KinkCurve-based interest rates
206206
- Variable rates based on utilization
207-
- Interest compounds continuously
207+
- Interest compounds via discrete per-second updates
208208

209209
4. **FLOW Credit Interest**
210210
- LP earnings with insurance spread

cadence/tests/auto_borrow_behavior_test.cdc

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,18 +75,24 @@ fun testAutoBorrowBehaviorWithTargetHealth() {
7575
message: "Expected MOET to be in Debit (borrowed) state")
7676

7777
// Verify the amount is approximately what we calculated (within 0.01 tolerance)
78-
Test.assert(moetBalance >= expectedDebt - 0.01 && moetBalance <= expectedDebt + 0.01,
79-
message: "Expected MOET debt to be approximately \(expectedDebt), but got \(moetBalance)")
78+
Test.assert(
79+
equalWithinVariance(expectedDebt, moetBalance, 0.01),
80+
message: "Expected MOET debt to be approximately \(expectedDebt), but got \(moetBalance)",
81+
)
8082

8183
// Verify position health is at target
8284
let health = getPositionHealth(pid: 0, beFailed: false)
83-
Test.assert(equalWithinVariance(INT_TARGET_HEALTH, health),
84-
message: "Expected health to be \(INT_TARGET_HEALTH), but got \(health)")
85+
Test.assert(
86+
equalWithinVariance(INT_TARGET_HEALTH, health, DEFAULT_UFIX128_VARIANCE),
87+
message: "Expected health to be \(INT_TARGET_HEALTH), but got \(health)",
88+
)
8589

8690
// Verify the user actually received the borrowed MOET in their Vault (draw-down sink)
8791
let userMoetBalance = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)!
88-
Test.assert(userMoetBalance >= expectedDebt - 0.01 && userMoetBalance <= expectedDebt + 0.01,
89-
message: "Expected user MOET Vault balance to be approximately \(expectedDebt), but got \(userMoetBalance)")
92+
Test.assert(
93+
equalWithinVariance(expectedDebt, userMoetBalance, 0.01),
94+
message: "Expected user MOET Vault balance to be approximately \(expectedDebt), but got \(userMoetBalance)",
95+
)
9096
}
9197

9298
access(all)

cadence/tests/compute_available_withdrawal_test.cdc

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ fun test_atTargetHealth_nothingAvailable() {
8686
depositAmount: 0.0,
8787
beFailed: false
8888
)
89-
Test.assert(equalWithinVariance(0.0, availableMOET),
89+
Test.assert(equalWithinVariance(0.0, availableMOET, DEFAULT_UFIX_VARIANCE),
9090
message: "Expected 0 MOET available at target health, got \(availableMOET)")
9191

9292
let availableFLOW = fundsAvailableAboveTargetHealthAfterDepositing(
@@ -97,7 +97,7 @@ fun test_atTargetHealth_nothingAvailable() {
9797
depositAmount: 0.0,
9898
beFailed: false
9999
)
100-
Test.assert(equalWithinVariance(0.0, availableFLOW),
100+
Test.assert(equalWithinVariance(0.0, availableFLOW, DEFAULT_UFIX_VARIANCE),
101101
message: "Expected 0 FLOW available at target health, got \(availableFLOW)")
102102
}
103103

@@ -130,7 +130,7 @@ fun test_noCreditInWithdrawToken_zerodebt_fullBorrowCapacity() {
130130
depositAmount: 0.0,
131131
beFailed: false
132132
)
133-
Test.assert(equalWithinVariance(expectedAvailable, actualAvailable),
133+
Test.assert(equalWithinVariance(expectedAvailable, actualAvailable, DEFAULT_UFIX_VARIANCE),
134134
message: "Expected \(expectedAvailable) MOET available (zero-debt full capacity), got \(actualAvailable)")
135135
}
136136

@@ -158,7 +158,7 @@ fun test_creditInWithdrawToken_partialCollateralOnly() {
158158

159159
// Confirm the borrow happened and health is at target
160160
let healthAtCreation = getPositionHealth(pid: pid, beFailed: false)
161-
Test.assert(equalWithinVariance(UFix64(INT_TARGET_HEALTH), UFix64(healthAtCreation)),
161+
Test.assert(equalWithinVariance(UFix64(INT_TARGET_HEALTH), UFix64(healthAtCreation), DEFAULT_UFIX_VARIANCE),
162162
message: "Expected health ≈ 1.3 after creation with push, got \(healthAtCreation)")
163163

164164
// Increase FLOW price to 2.0 → more headroom above target
@@ -180,7 +180,7 @@ fun test_creditInWithdrawToken_partialCollateralOnly() {
180180
depositAmount: 0.0,
181181
beFailed: false
182182
)
183-
Test.assert(equalWithinVariance(expectedAvailable, actualAvailable),
183+
Test.assert(equalWithinVariance(expectedAvailable, actualAvailable, DEFAULT_UFIX_VARIANCE),
184184
message: "Expected \(expectedAvailable) FLOW available (partial collateral withdrawal), got \(actualAvailable)")
185185
}
186186

@@ -242,7 +242,7 @@ fun test_creditFlipsIntoDebt_availabilityExceedsCreditBalance() {
242242
depositAmount: 0.0,
243243
beFailed: false
244244
)
245-
Test.assert(equalWithinVariance(expectedAvailable, actualAvailable),
245+
Test.assert(equalWithinVariance(expectedAvailable, actualAvailable, DEFAULT_UFIX_VARIANCE),
246246
message: "Expected \(expectedAvailable) FLOW available (credit→debt flip), got \(actualAvailable)")
247247
}
248248

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import Test
2+
import BlockchainHelpers
3+
4+
import "FlowALPMath"
5+
import "test_helpers.cdc"
6+
7+
access(all)
8+
fun setup() {
9+
let err = Test.deployContract(
10+
name: "FlowALPMath",
11+
path: "../lib/FlowALPMath.cdc",
12+
arguments: []
13+
)
14+
Test.expect(err, Test.beNil())
15+
}
16+
17+
access(all) struct TestCase {
18+
access(all) let nominal: UFix128
19+
access(all) let expected: UFix128
20+
21+
init(nominal: UFix128, expected: UFix128) {
22+
self.nominal = nominal
23+
self.expected = expected
24+
}
25+
}
26+
27+
access(all)
28+
fun test_effectiveYearlyRate() {
29+
let delta: UFix128 = 0.0001
30+
let testCases = [
31+
TestCase(nominal: 0.01, expected: 0.01005016708), // ≈ e^0.01 - 1
32+
TestCase(nominal: 0.02, expected: 0.02020134003), // ≈ e^0.02 - 1
33+
TestCase(nominal: 0.05, expected: 0.05127109638), // ≈ e^0.05 - 1
34+
TestCase(nominal: 0.50, expected: 0.6487212707), // ≈ e^0.5 - 1
35+
TestCase(nominal: 1.0, expected: 1.7182818285), // ≈ e^1 - 1
36+
TestCase(nominal: 4.0, expected: 53.5981500331) // ≈ e^4 - 1
37+
]
38+
for testCase in testCases {
39+
let effective = FlowALPMath.effectiveYearlyRate(nominalYearlyRate: testCase.nominal)
40+
let diff = effective > testCase.expected ? effective - testCase.expected : testCase.expected - effective
41+
Test.assert(
42+
diff <= delta,
43+
message: "effectiveYearlyRate(\(testCase.nominal.toString())) expected ~\(testCase.expected.toString()), got \(effective.toString()), diff \(diff.toString()) exceeds delta \(delta.toString())"
44+
)
45+
}
46+
}

cadence/tests/funds_available_above_target_health_test.cdc

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ fun testFundsAvailableAboveTargetHealthAfterDepositingWithPushFromHealthy() {
9292
// assert expected starting point
9393
let balanceAfterBorrow = getBalance(address: userAccount.address, vaultPublicPath: MOET.VaultPublicPath)!
9494
let expectedBorrowAmount = (positionFundingAmount * flowCollateralFactor * flowStartPrice) / TARGET_HEALTH
95-
Test.assert(equalWithinVariance(expectedBorrowAmount, balanceAfterBorrow),
95+
Test.assert(equalWithinVariance(expectedBorrowAmount, balanceAfterBorrow, DEFAULT_UFIX_VARIANCE),
9696
message: "Expected MOET balance to be ~\(expectedBorrowAmount), but got \(balanceAfterBorrow)")
9797

9898
let evts = Test.eventsOfType(Type<FlowALPEvents.Opened>())
@@ -113,10 +113,10 @@ fun testFundsAvailableAboveTargetHealthAfterDepositingWithPushFromHealthy() {
113113
}
114114
Test.assertEqual(positionFundingAmount, flowPositionBalance!.balance)
115115

116-
Test.assert(equalWithinVariance(expectedBorrowAmount, moetBalance!.balance),
116+
Test.assert(equalWithinVariance(expectedBorrowAmount, moetBalance!.balance, DEFAULT_UFIX_VARIANCE),
117117
message: "Expected borrow amount to be \(expectedBorrowAmount), but got \(moetBalance!.balance)")
118118

119-
Test.assert(equalWithinVariance(INT_TARGET_HEALTH, health),
119+
Test.assert(equalWithinVariance(INT_TARGET_HEALTH, health, DEFAULT_UFIX128_VARIANCE),
120120
message: "Expected health to be \(INT_TARGET_HEALTH), but got \(health)")
121121

122122
log("[TEST] FLOW price set to \(flowStartPrice)")
@@ -297,7 +297,7 @@ fun testFundsAvailableAboveTargetHealthAfterDepositingWithoutPushFromOvercollate
297297
log("[TEST] Depositing: \(depositAmount)")
298298
log("[TEST] Expected Available: \(expectedAvailable)")
299299
log("[TEST] Actual Available: \(actualAvailable)")
300-
Test.assert(equalWithinVariance(expectedAvailable, actualAvailable),
300+
Test.assert(equalWithinVariance(expectedAvailable, actualAvailable, DEFAULT_UFIX_VARIANCE),
301301
message: "Values are not equal within variance - expected: \(expectedAvailable), actual: \(actualAvailable)")
302302

303303
log("..............................")
@@ -315,7 +315,7 @@ fun testFundsAvailableAboveTargetHealthAfterDepositingWithoutPushFromOvercollate
315315
log("[TEST] Depositing: \(depositAmount)")
316316
log("[TEST] Expected Available: \(expectedAvailable)")
317317
log("[TEST] Actual Available: \(actualAvailable)")
318-
Test.assert(equalWithinVariance(expectedAvailable, actualAvailable),
318+
Test.assert(equalWithinVariance(expectedAvailable, actualAvailable, DEFAULT_UFIX_VARIANCE),
319319
message: "Values are not equal within variance - expected: \(expectedAvailable), actual: \(actualAvailable)")
320320

321321
log("==============================")
@@ -354,6 +354,6 @@ fun runFundsAvailableAboveTargetHealthAfterDepositing(
354354
log("[TEST] Depositing: \(depositAmount)")
355355
log("[TEST] Expected Available: \(expectedAvailable)")
356356
log("[TEST] Actual Available: \(actualAvailable)")
357-
Test.assert(equalWithinVariance(expectedAvailable, actualAvailable),
357+
Test.assert(equalWithinVariance(expectedAvailable, actualAvailable, DEFAULT_UFIX_VARIANCE),
358358
message: "Values are not equal within variance - expected: \(expectedAvailable), actual: \(actualAvailable)")
359359
}

cadence/tests/funds_required_for_target_health_test.cdc

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ fun testFundsRequiredForTargetHealthAfterWithdrawingWithPushFromHealthy() {
8787
// assert expected starting point
8888
startingDebt = getBalance(address: userAccount.address, vaultPublicPath: MOET.VaultPublicPath)!
8989
let expectedStartingDebt = (positionFundingAmount * flowCollateralFactor * flowStartPrice) / TARGET_HEALTH
90-
Test.assert(equalWithinVariance(expectedStartingDebt, startingDebt),
90+
Test.assert(equalWithinVariance(expectedStartingDebt, startingDebt, DEFAULT_UFIX_VARIANCE),
9191
message: "Expected MOET balance to be ~\(expectedStartingDebt), but got \(startingDebt)")
9292

9393
var evts = Test.eventsOfType(Type<FlowALPEvents.Opened>())
@@ -102,7 +102,7 @@ fun testFundsRequiredForTargetHealthAfterWithdrawingWithPushFromHealthy() {
102102
Test.assertEqual(rebalancedEvt.amount, startingDebt)
103103

104104
let health = getPositionHealth(pid: positionID, beFailed: false)
105-
Test.assert(equalWithinVariance(INT_TARGET_HEALTH, health),
105+
Test.assert(equalWithinVariance(INT_TARGET_HEALTH, health, DEFAULT_UFIX128_VARIANCE),
106106
message: "Expected health to be \(INT_TARGET_HEALTH), but got \(health)")
107107

108108
log("[TEST] FLOW price set to \(flowStartPrice)")
@@ -283,7 +283,7 @@ fun testFundsRequiredForTargetHealthAfterWithdrawingWithPushFromOvercollateraliz
283283
// assert expected starting point
284284
startingDebt = getBalance(address: userAccount.address, vaultPublicPath: MOET.VaultPublicPath)!
285285
let expectedStartingDebt = (positionFundingAmount * flowCollateralFactor * flowStartPrice) / TARGET_HEALTH
286-
Test.assert(equalWithinVariance(expectedStartingDebt, startingDebt),
286+
Test.assert(equalWithinVariance(expectedStartingDebt, startingDebt, DEFAULT_UFIX_VARIANCE),
287287
message: "Expected MOET balance to be ~\(expectedStartingDebt), but got \(startingDebt)")
288288

289289
var evts = Test.eventsOfType(Type<FlowALPEvents.Opened>())
@@ -298,7 +298,7 @@ fun testFundsRequiredForTargetHealthAfterWithdrawingWithPushFromOvercollateraliz
298298
Test.assertEqual(rebalancedEvt.amount, startingDebt)
299299

300300
let actualHealthBeforePriceIncrease = getPositionHealth(pid: positionID, beFailed: false)
301-
Test.assert(equalWithinVariance(INT_TARGET_HEALTH, actualHealthBeforePriceIncrease),
301+
Test.assert(equalWithinVariance(INT_TARGET_HEALTH, actualHealthBeforePriceIncrease, DEFAULT_UFIX128_VARIANCE),
302302
message: "Expected health to be \(INT_TARGET_HEALTH), but got \(actualHealthBeforePriceIncrease)")
303303

304304
let priceIncrease = 0.25
@@ -438,7 +438,7 @@ fun testFundsRequiredForTargetHealthAfterWithdrawingWithPushFromUndercollaterali
438438
// assert expected starting point
439439
startingDebt = getBalance(address: userAccount.address, vaultPublicPath: MOET.VaultPublicPath)!
440440
let expectedStartingDebt = (positionFundingAmount * flowCollateralFactor * flowStartPrice) / TARGET_HEALTH
441-
Test.assert(equalWithinVariance(expectedStartingDebt, startingDebt),
441+
Test.assert(equalWithinVariance(expectedStartingDebt, startingDebt, DEFAULT_UFIX_VARIANCE),
442442
message: "Expected MOET balance to be ~\(expectedStartingDebt), but got \(startingDebt)")
443443

444444
var evts = Test.eventsOfType(Type<FlowALPEvents.Opened>())
@@ -453,7 +453,7 @@ fun testFundsRequiredForTargetHealthAfterWithdrawingWithPushFromUndercollaterali
453453
Test.assertEqual(rebalancedEvt.amount, startingDebt)
454454

455455
let actualHealthBeforePriceIncrease = getPositionHealth(pid: positionID, beFailed: false)
456-
Test.assert(equalWithinVariance(INT_TARGET_HEALTH, actualHealthBeforePriceIncrease),
456+
Test.assert(equalWithinVariance(INT_TARGET_HEALTH, actualHealthBeforePriceIncrease, DEFAULT_UFIX128_VARIANCE),
457457
message: "Expected health to be \(INT_TARGET_HEALTH), but got \(actualHealthBeforePriceIncrease)")
458458

459459
let priceDecrease = 0.25
@@ -557,6 +557,6 @@ fun runFundsRequiredForTargetHealthAfterWithdrawing(
557557
log("[TEST] Withdrawing: \(withdrawAmount)")
558558
log("[TEST] Expected Required: \(ufixExpectedRequired)")
559559
log("[TEST] Actual Required: \(actualRequired)")
560-
Test.assert(equalWithinVariance(ufixExpectedRequired, actualRequired),
560+
Test.assert(equalWithinVariance(ufixExpectedRequired, actualRequired, DEFAULT_UFIX_VARIANCE),
561561
message: "Expected required funds to be \(ufixExpectedRequired), but got \(actualRequired)")
562562
}

0 commit comments

Comments
 (0)