Skip to content

Commit aae39c4

Browse files
fix!: correct borrowing rate in recovery mode
BREAKING CHANGE: `fees` has been moved from `LiquityStoreBaseState` to `LiquityStoreDerivedState`. Most users won't need to make a code change. However, care should be taken to upgrade both SDK packages at the same time, to ensure compatibility. Fixes liquity#300.
1 parent 55755e7 commit aae39c4

18 files changed

+120
-33
lines changed

docs/sdk/lib-base.liquitystorebasestate.md

-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ export interface LiquityStoreBaseState
1818
| --- | --- | --- |
1919
| [accountBalance](./lib-base.liquitystorebasestate.accountbalance.md) | [Decimal](./lib-base.decimal.md) | User's native currency balance (e.g. Ether). |
2020
| [collateralSurplusBalance](./lib-base.liquitystorebasestate.collateralsurplusbalance.md) | [Decimal](./lib-base.decimal.md) | Amount of leftover collateral available for withdrawal to the user. |
21-
| [fees](./lib-base.liquitystorebasestate.fees.md) | [Fees](./lib-base.fees.md) | Calculator for current fees. |
2221
| [frontend](./lib-base.liquitystorebasestate.frontend.md) | [FrontendStatus](./lib-base.frontendstatus.md) | Status of currently used frontend. |
2322
| [lqtyBalance](./lib-base.liquitystorebasestate.lqtybalance.md) | [Decimal](./lib-base.decimal.md) | User's LQTY token balance. |
2423
| [lqtyStake](./lib-base.liquitystorebasestate.lqtystake.md) | [LQTYStake](./lib-base.lqtystake.md) | User's LQTY stake. |

docs/sdk/lib-base.liquitystorebasestate.fees.md renamed to docs/sdk/lib-base.liquitystorederivedstate.fees.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
22

3-
[Home](./index.md) &gt; [@liquity/lib-base](./lib-base.md) &gt; [LiquityStoreBaseState](./lib-base.liquitystorebasestate.md) &gt; [fees](./lib-base.liquitystorebasestate.fees.md)
3+
[Home](./index.md) &gt; [@liquity/lib-base](./lib-base.md) &gt; [LiquityStoreDerivedState](./lib-base.liquitystorederivedstate.md) &gt; [fees](./lib-base.liquitystorederivedstate.fees.md)
44

5-
## LiquityStoreBaseState.fees property
5+
## LiquityStoreDerivedState.fees property
66

77
Calculator for current fees.
88

docs/sdk/lib-base.liquitystorederivedstate.md

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export interface LiquityStoreDerivedState
1717
| Property | Type | Description |
1818
| --- | --- | --- |
1919
| [borrowingRate](./lib-base.liquitystorederivedstate.borrowingrate.md) | [Decimal](./lib-base.decimal.md) | Current borrowing rate. |
20+
| [fees](./lib-base.liquitystorederivedstate.fees.md) | [Fees](./lib-base.fees.md) | Calculator for current fees. |
2021
| [redemptionRate](./lib-base.liquitystorederivedstate.redemptionrate.md) | [Decimal](./lib-base.decimal.md) | Current redemption rate. |
2122
| [trove](./lib-base.liquitystorederivedstate.trove.md) | [Trove](./lib-base.trove.md) | Current state of user's Trove |
2223

docs/sdk/lib-base.minimum_borrowing_rate.md

+5
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,8 @@ Value that the [borrowing rate](./lib-base.fees.borrowingrate.md) will never dec
1111
```typescript
1212
MINIMUM_BORROWING_RATE: Decimal
1313
```
14+
15+
## Remarks
16+
17+
Note that the borrowing rate can still be lower than this during recovery mode, when it's overridden by zero.
18+

packages/lib-base/etc/lib-base.api.md

+10-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ export class _CachedReadableLiquity<T extends unknown[]> implements _ReadableLiq
1212
// (undocumented)
1313
getFees(...extraParams: T): Promise<Fees>;
1414
// (undocumented)
15+
_getFeesInNormalMode(...extraParams: T): Promise<Fees>;
16+
// (undocumented)
1517
getFrontendStatus(address?: string, ...extraParams: T): Promise<FrontendStatus>;
1618
// (undocumented)
1719
getLQTYBalance(address?: string, ...extraParams: T): Promise<Decimal>;
@@ -175,13 +177,15 @@ export const _failedReceipt: <R>(rawReceipt: R) => FailedReceipt<R>;
175177
// @public
176178
export class Fees {
177179
// @internal
178-
constructor(lastFeeOperation: Date, baseRateWithoutDecay: Decimalish, minuteDecayFactor: Decimalish, beta: Decimalish);
180+
constructor(lastFeeOperation: Date, baseRateWithoutDecay: Decimalish, minuteDecayFactor: Decimalish, beta: Decimalish, recoveryMode?: boolean);
179181
// @internal (undocumented)
180182
baseRate(when: Date): Decimal;
181183
borrowingRate(): Decimal;
182184
equals(that: Fees): boolean;
183185
redemptionRate(redeemedFractionOfSupply?: Decimalish): Decimal;
184186
// @internal (undocumented)
187+
_setRecoveryMode(recoveryMode: boolean): Fees;
188+
// @internal (undocumented)
185189
toString(): string;
186190
}
187191

@@ -242,7 +246,8 @@ export abstract class LiquityStore<T = unknown> {
242246
export interface LiquityStoreBaseState {
243247
accountBalance: Decimal;
244248
collateralSurplusBalance: Decimal;
245-
fees: Fees;
249+
// @internal (undocumented)
250+
_feesInNormalMode: Fees;
246251
frontend: FrontendStatus;
247252
lqtyBalance: Decimal;
248253
lqtyStake: LQTYStake;
@@ -261,6 +266,7 @@ export interface LiquityStoreBaseState {
261266
// @public
262267
export interface LiquityStoreDerivedState {
263268
borrowingRate: Decimal;
269+
fees: Fees;
264270
redemptionRate: Decimal;
265271
trove: Trove;
266272
}
@@ -446,6 +452,8 @@ export interface PopulatedLiquityTransaction<P = unknown, T extends SentLiquityT
446452
export interface ReadableLiquity {
447453
getCollateralSurplusBalance(address?: string): Promise<Decimal>;
448454
getFees(): Promise<Fees>;
455+
// @internal (undocumented)
456+
_getFeesInNormalMode(): Promise<Fees>;
449457
getFrontendStatus(address?: string): Promise<FrontendStatus>;
450458
getLQTYBalance(address?: string): Promise<Decimal>;
451459
getLQTYStake(address?: string): Promise<LQTYStake>;

packages/lib-base/src/Fees.ts

+22-7
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,36 @@ export class Fees {
2121
private readonly _minuteDecayFactor: Decimal;
2222
private readonly _beta: Decimal;
2323
private readonly _lastFeeOperation: Date;
24+
private readonly _recoveryMode: boolean;
2425

2526
/** @internal */
2627
constructor(
2728
lastFeeOperation: Date,
2829
baseRateWithoutDecay: Decimalish,
2930
minuteDecayFactor: Decimalish,
30-
beta: Decimalish
31+
beta: Decimalish,
32+
recoveryMode = false
3133
) {
3234
this._lastFeeOperation = lastFeeOperation;
3335
this._baseRateWithoutDecay = Decimal.from(baseRateWithoutDecay);
3436
this._minuteDecayFactor = Decimal.from(minuteDecayFactor);
3537
this._beta = Decimal.from(beta);
38+
this._recoveryMode = recoveryMode;
3639

3740
assert(this._minuteDecayFactor.lt(1));
3841
}
3942

43+
/** @internal */
44+
_setRecoveryMode(recoveryMode: boolean): Fees {
45+
return new Fees(
46+
this._lastFeeOperation,
47+
this._baseRateWithoutDecay,
48+
this._minuteDecayFactor,
49+
this._beta,
50+
recoveryMode
51+
);
52+
}
53+
4054
/**
4155
* Compare to another instance of `Fees`.
4256
*/
@@ -45,15 +59,17 @@ export class Fees {
4559
this._baseRateWithoutDecay.eq(that._baseRateWithoutDecay) &&
4660
this._minuteDecayFactor.eq(that._minuteDecayFactor) &&
4761
this._beta.eq(that._beta) &&
48-
this._lastFeeOperation.getTime() === that._lastFeeOperation.getTime()
62+
this._lastFeeOperation.getTime() === that._lastFeeOperation.getTime() &&
63+
this._recoveryMode === that._recoveryMode
4964
);
5065
}
5166

5267
/** @internal */
5368
toString(): string {
5469
return (
5570
`{ baseRateWithoutDecay: ${this._baseRateWithoutDecay}` +
56-
`, lastFeeOperation: "${this._lastFeeOperation.toLocaleString()}" } `
71+
`, lastFeeOperation: "${this._lastFeeOperation.toLocaleString()}"` +
72+
`, recoveryMode: ${this._recoveryMode} } `
5773
);
5874
}
5975

@@ -86,10 +102,9 @@ export class Fees {
86102
* ```
87103
*/
88104
borrowingRate(): Decimal {
89-
return Decimal.min(
90-
MINIMUM_BORROWING_RATE.add(this.baseRate(new Date())),
91-
MAXIMUM_BORROWING_RATE
92-
);
105+
return this._recoveryMode
106+
? Decimal.ZERO
107+
: Decimal.min(MINIMUM_BORROWING_RATE.add(this.baseRate(new Date())), MAXIMUM_BORROWING_RATE);
93108
}
94109

95110
/**

packages/lib-base/src/LiquityStore.ts

+18-4
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ export interface LiquityStoreBaseState {
6969
/** User's stability deposit. */
7070
stabilityDeposit: StabilityDeposit;
7171

72-
/** Calculator for current fees. */
73-
fees: Fees;
72+
/** @internal */
73+
_feesInNormalMode: Fees;
7474

7575
/** User's LQTY stake. */
7676
lqtyStake: LQTYStake;
@@ -88,6 +88,9 @@ export interface LiquityStoreDerivedState {
8888
/** Current state of user's Trove */
8989
trove: Trove;
9090

91+
/** Calculator for current fees. */
92+
fees: Fees;
93+
9194
/**
9295
* Current borrowing rate.
9396
*
@@ -359,7 +362,11 @@ export abstract class LiquityStore<T = unknown> {
359362
baseStateUpdate.stabilityDeposit
360363
),
361364

362-
fees: this._updateIfChanged(equals, "fees", baseState.fees, baseStateUpdate.fees),
365+
_feesInNormalMode: this._silentlyUpdateIfChanged(
366+
equals,
367+
baseState._feesInNormalMode,
368+
baseStateUpdate._feesInNormalMode
369+
),
363370

364371
lqtyStake: this._updateIfChanged(
365372
equals,
@@ -380,10 +387,15 @@ export abstract class LiquityStore<T = unknown> {
380387
private _derive({
381388
troveBeforeRedistribution,
382389
totalRedistributed,
383-
fees
390+
_feesInNormalMode,
391+
total,
392+
price
384393
}: LiquityStoreBaseState): LiquityStoreDerivedState {
394+
const fees = _feesInNormalMode._setRecoveryMode(total.collateralRatioIsBelowCritical(price));
395+
385396
return {
386397
trove: troveBeforeRedistribution.applyRedistribution(totalRedistributed),
398+
fees,
387399
borrowingRate: fees.borrowingRate(),
388400
redemptionRate: fees.redemptionRate()
389401
};
@@ -396,6 +408,8 @@ export abstract class LiquityStore<T = unknown> {
396408
return {
397409
trove: this._updateIfChanged(equals, "trove", derivedState.trove, derivedStateUpdate.trove),
398410

411+
fees: this._updateIfChanged(equals, "fees", derivedState.fees, derivedStateUpdate.fees),
412+
399413
borrowingRate: this._silentlyUpdateIfChanged(
400414
eq,
401415
derivedState.borrowingRate,

packages/lib-base/src/ReadableLiquity.ts

+3
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,9 @@ export interface ReadableLiquity {
144144
*/
145145
getTroves(params: TroveListingParams): Promise<[address: string, trove: Trove][]>;
146146

147+
/** @internal */
148+
_getFeesInNormalMode(): Promise<Fees>;
149+
147150
/**
148151
* Get a calculator for current fees.
149152
*/

packages/lib-base/src/_CachedReadableLiquity.ts

+16-6
Original file line numberDiff line numberDiff line change
@@ -167,30 +167,40 @@ export class _CachedReadableLiquity<T extends unknown[]>
167167
}
168168
}
169169

170-
async getFees(...extraParams: T): Promise<Fees> {
170+
async _getFeesInNormalMode(...extraParams: T): Promise<Fees> {
171171
return (
172-
this._cache.getFees?.call(this._cache, ...extraParams) ??
173-
this._readable.getFees(...extraParams)
172+
this._cache._getFeesInNormalMode(...extraParams) ??
173+
this._readable._getFeesInNormalMode(...extraParams)
174174
);
175175
}
176176

177+
async getFees(...extraParams: T): Promise<Fees> {
178+
const [feesInNormalMode, total, price] = await Promise.all([
179+
this._getFeesInNormalMode(...extraParams),
180+
this.getTotal(...extraParams),
181+
this.getPrice(...extraParams)
182+
]);
183+
184+
return feesInNormalMode._setRecoveryMode(total.collateralRatioIsBelowCritical(price));
185+
}
186+
177187
async getLQTYStake(address?: string, ...extraParams: T): Promise<LQTYStake> {
178188
return (
179-
this._cache.getLQTYStake?.call(this._cache, address, ...extraParams) ??
189+
this._cache.getLQTYStake(address, ...extraParams) ??
180190
this._readable.getLQTYStake(address, ...extraParams)
181191
);
182192
}
183193

184194
async getTotalStakedLQTY(...extraParams: T): Promise<Decimal> {
185195
return (
186-
this._cache.getTotalStakedLQTY?.call(this._cache, ...extraParams) ??
196+
this._cache.getTotalStakedLQTY(...extraParams) ??
187197
this._readable.getTotalStakedLQTY(...extraParams)
188198
);
189199
}
190200

191201
async getFrontendStatus(address?: string, ...extraParams: T): Promise<FrontendStatus> {
192202
return (
193-
this._cache.getFrontendStatus?.call(this._cache, address, ...extraParams) ??
203+
this._cache.getFrontendStatus(address, ...extraParams) ??
194204
this._readable.getFrontendStatus(address, ...extraParams)
195205
);
196206
}

packages/lib-base/src/constants.ts

+4
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ export const LUSD_LIQUIDATION_RESERVE = Decimal.from(50);
2424
/**
2525
* Value that the {@link Fees.borrowingRate | borrowing rate} will never decay below.
2626
*
27+
* @remarks
28+
* Note that the borrowing rate can still be lower than this during recovery mode, when it's
29+
* overridden by zero.
30+
*
2731
* @public
2832
*/
2933
export const MINIMUM_BORROWING_RATE = Decimal.from(0.005);

packages/lib-ethers/etc/lib-ethers.api.md

+4
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ export class EthersLiquity implements ReadableEthersLiquity, TransactableLiquity
111111
getCollateralSurplusBalance(address?: string, overrides?: EthersCallOverrides): Promise<Decimal>;
112112
// (undocumented)
113113
getFees(overrides?: EthersCallOverrides): Promise<Fees>;
114+
// @internal (undocumented)
115+
_getFeesInNormalMode(overrides?: EthersCallOverrides): Promise<Fees>;
114116
// (undocumented)
115117
getFrontendStatus(address?: string, overrides?: EthersCallOverrides): Promise<FrontendStatus>;
116118
// (undocumented)
@@ -345,6 +347,8 @@ export class ReadableEthersLiquity implements ReadableLiquity {
345347
getCollateralSurplusBalance(address?: string, overrides?: EthersCallOverrides): Promise<Decimal>;
346348
// (undocumented)
347349
getFees(overrides?: EthersCallOverrides): Promise<Fees>;
350+
// @internal (undocumented)
351+
_getFeesInNormalMode(overrides?: EthersCallOverrides): Promise<Fees>;
348352
// (undocumented)
349353
getFrontendStatus(address?: string, overrides?: EthersCallOverrides): Promise<FrontendStatus>;
350354
// (undocumented)

packages/lib-ethers/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"test-live:run": "cross-env USE_LIVE_VERSION=true hardhat test"
3434
},
3535
"peerDependencies": {
36-
"@liquity/lib-base": "^1.0.0",
36+
"@liquity/lib-base": "^2.0.0",
3737
"ethers": "^5.0.0"
3838
},
3939
"devDependencies": {

packages/lib-ethers/src/BlockPolledLiquityStore.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export class BlockPolledLiquityStore extends LiquityStore<BlockPolledLiquityStor
8080
totalRedistributed: this._readable.getTotalRedistributed({ blockTag }),
8181
total: this._readable.getTotal({ blockTag }),
8282
lusdInStabilityPool: this._readable.getLUSDInStabilityPool({ blockTag }),
83-
fees: this._readable.getFees({ blockTag }),
83+
_feesInNormalMode: this._readable._getFeesInNormalMode({ blockTag }),
8484
totalStakedLQTY: this._readable.getTotalStakedLQTY({ blockTag }),
8585

8686
frontend: frontendTag

packages/lib-ethers/src/EthersLiquity.ts

+5
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,11 @@ export class EthersLiquity implements ReadableEthersLiquity, TransactableLiquity
227227
return this._readable.getTroves(params, overrides);
228228
}
229229

230+
/** @internal */
231+
_getFeesInNormalMode(overrides?: EthersCallOverrides): Promise<Fees> {
232+
return this._readable._getFeesInNormalMode(overrides);
233+
}
234+
230235
/** {@inheritDoc @liquity/lib-base#ReadableLiquity.getFees} */
231236
getFees(overrides?: EthersCallOverrides): Promise<Fees> {
232237
return this._readable.getFees(overrides);

packages/lib-ethers/src/ReadableEthersLiquity.ts

+22-7
Original file line numberDiff line numberDiff line change
@@ -168,12 +168,10 @@ export class ReadableEthersLiquity implements ReadableLiquity {
168168

169169
/** {@inheritDoc @liquity/lib-base#ReadableLiquity.getTrove} */
170170
async getTrove(address?: string, overrides?: EthersCallOverrides): Promise<Trove> {
171-
address ??= _requireAddress(this.connection);
172-
173171
const [trove, totalRedistributed] = await Promise.all([
174-
this.getTroveBeforeRedistribution(address, { ...overrides }),
175-
this.getTotalRedistributed({ ...overrides })
176-
] as const);
172+
this.getTroveBeforeRedistribution(address, overrides),
173+
this.getTotalRedistributed(overrides)
174+
]);
177175

178176
return trove.applyRedistribution(totalRedistributed);
179177
}
@@ -316,8 +314,8 @@ export class ReadableEthersLiquity implements ReadableLiquity {
316314
}
317315
}
318316

319-
/** {@inheritDoc @liquity/lib-base#ReadableLiquity.getFees} */
320-
async getFees(overrides?: EthersCallOverrides): Promise<Fees> {
317+
/** @internal */
318+
async _getFeesInNormalMode(overrides?: EthersCallOverrides): Promise<Fees> {
321319
const { troveManager } = _getContracts(this.connection);
322320

323321
const [lastFeeOperationTime, baseRateWithoutDecay] = await Promise.all([
@@ -330,6 +328,17 @@ export class ReadableEthersLiquity implements ReadableLiquity {
330328
return new Fees(lastFeeOperation, baseRateWithoutDecay, MINUTE_DECAY_FACTOR, BETA);
331329
}
332330

331+
/** {@inheritDoc @liquity/lib-base#ReadableLiquity.getFees} */
332+
async getFees(overrides?: EthersCallOverrides): Promise<Fees> {
333+
const [feesInNormalMode, total, price] = await Promise.all([
334+
this._getFeesInNormalMode(overrides),
335+
this.getTotal(overrides),
336+
this.getPrice(overrides)
337+
]);
338+
339+
return feesInNormalMode._setRecoveryMode(total.collateralRatioIsBelowCritical(price));
340+
}
341+
333342
/** {@inheritDoc @liquity/lib-base#ReadableLiquity.getLQTYStake} */
334343
async getLQTYStake(address?: string, overrides?: EthersCallOverrides): Promise<LQTYStake> {
335344
address ??= _requireAddress(this.connection);
@@ -500,6 +509,12 @@ class BlockPolledLiquityStoreBasedCache
500509
}
501510
}
502511

512+
_getFeesInNormalMode(overrides?: EthersCallOverrides): Fees | undefined {
513+
if (this._blockHit(overrides)) {
514+
return this._store.state._feesInNormalMode;
515+
}
516+
}
517+
503518
getFees(overrides?: EthersCallOverrides): Fees | undefined {
504519
if (this._blockHit(overrides)) {
505520
return this._store.state.fees;

0 commit comments

Comments
 (0)