From 23f85ed5bc0928af6c4e2333bed81f6f01e57cda Mon Sep 17 00:00:00 2001 From: Daniel Simon Date: Mon, 24 May 2021 13:55:15 +0700 Subject: [PATCH] feat: wip --- ...arams.borrowingfeedecaytoleranceminutes.md | 13 + ...perationoptionalparams.maxborrowingrate.md | 13 + ...ethers.borrowingoperationoptionalparams.md | 21 ++ .../lib-ethers.ethersliquity.adjusttrove.md | 4 +- docs/sdk/lib-ethers.ethersliquity.md | 4 +- .../sdk/lib-ethers.ethersliquity.opentrove.md | 4 +- docs/sdk/lib-ethers.md | 1 + ...rs.populatableethersliquity.adjusttrove.md | 4 +- .../lib-ethers.populatableethersliquity.md | 4 +- ...hers.populatableethersliquity.opentrove.md | 4 +- ...tedethersliquitytransaction.gasheadroom.md | 20 ++ ...thers.populatedethersliquitytransaction.md | 1 + ...thers.sendableethersliquity.adjusttrove.md | 4 +- docs/sdk/lib-ethers.sendableethersliquity.md | 4 +- ...-ethers.sendableethersliquity.opentrove.md | 4 +- packages/lib-ethers/etc/lib-ethers.api.md | 27 +- packages/lib-ethers/package.json | 5 +- packages/lib-ethers/scripts/spam-troves.ts | 104 +++++++ .../lib-ethers/src/BlockPolledLiquityStore.ts | 41 +-- packages/lib-ethers/src/EthersLiquity.ts | 25 +- .../lib-ethers/src/EthersLiquityConnection.ts | 8 +- .../src/PopulatableEthersLiquity.ts | 267 +++++++++++++++--- .../lib-ethers/src/ReadableEthersLiquity.ts | 36 ++- .../lib-ethers/src/SendableEthersLiquity.ts | 15 +- packages/lib-ethers/src/_utils.ts | 23 ++ packages/lib-ethers/src/contracts.ts | 12 + packages/lib-ethers/test/Liquity.test.ts | 13 +- yarn.lock | 12 + 28 files changed, 562 insertions(+), 131 deletions(-) create mode 100644 docs/sdk/lib-ethers.borrowingoperationoptionalparams.borrowingfeedecaytoleranceminutes.md create mode 100644 docs/sdk/lib-ethers.borrowingoperationoptionalparams.maxborrowingrate.md create mode 100644 docs/sdk/lib-ethers.borrowingoperationoptionalparams.md create mode 100644 docs/sdk/lib-ethers.populatedethersliquitytransaction.gasheadroom.md create mode 100644 packages/lib-ethers/scripts/spam-troves.ts create mode 100644 packages/lib-ethers/src/_utils.ts diff --git a/docs/sdk/lib-ethers.borrowingoperationoptionalparams.borrowingfeedecaytoleranceminutes.md b/docs/sdk/lib-ethers.borrowingoperationoptionalparams.borrowingfeedecaytoleranceminutes.md new file mode 100644 index 0000000000..90bf9ab4ad --- /dev/null +++ b/docs/sdk/lib-ethers.borrowingoperationoptionalparams.borrowingfeedecaytoleranceminutes.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@liquity/lib-ethers](./lib-ethers.md) > [BorrowingOperationOptionalParams](./lib-ethers.borrowingoperationoptionalparams.md) > [borrowingFeeDecayToleranceMinutes](./lib-ethers.borrowingoperationoptionalparams.borrowingfeedecaytoleranceminutes.md) + +## BorrowingOperationOptionalParams.borrowingFeeDecayToleranceMinutes property + +TODO + +Signature: + +```typescript +borrowingFeeDecayToleranceMinutes?: number; +``` diff --git a/docs/sdk/lib-ethers.borrowingoperationoptionalparams.maxborrowingrate.md b/docs/sdk/lib-ethers.borrowingoperationoptionalparams.maxborrowingrate.md new file mode 100644 index 0000000000..911318ebaf --- /dev/null +++ b/docs/sdk/lib-ethers.borrowingoperationoptionalparams.maxborrowingrate.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@liquity/lib-ethers](./lib-ethers.md) > [BorrowingOperationOptionalParams](./lib-ethers.borrowingoperationoptionalparams.md) > [maxBorrowingRate](./lib-ethers.borrowingoperationoptionalparams.maxborrowingrate.md) + +## BorrowingOperationOptionalParams.maxBorrowingRate property + +Maximum acceptable [borrowing rate](./lib-base.fees.borrowingrate.md) (default: current borrowing rate plus 0.5%). + +Signature: + +```typescript +maxBorrowingRate?: Decimalish; +``` diff --git a/docs/sdk/lib-ethers.borrowingoperationoptionalparams.md b/docs/sdk/lib-ethers.borrowingoperationoptionalparams.md new file mode 100644 index 0000000000..03972f83ab --- /dev/null +++ b/docs/sdk/lib-ethers.borrowingoperationoptionalparams.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [@liquity/lib-ethers](./lib-ethers.md) > [BorrowingOperationOptionalParams](./lib-ethers.borrowingoperationoptionalparams.md) + +## BorrowingOperationOptionalParams interface + +Optional parameters of a transaction that borrows LUSD. + +Signature: + +```typescript +export interface BorrowingOperationOptionalParams +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [borrowingFeeDecayToleranceMinutes?](./lib-ethers.borrowingoperationoptionalparams.borrowingfeedecaytoleranceminutes.md) | number | (Optional) TODO | +| [maxBorrowingRate?](./lib-ethers.borrowingoperationoptionalparams.maxborrowingrate.md) | [Decimalish](./lib-base.decimalish.md) | (Optional) Maximum acceptable [borrowing rate](./lib-base.fees.borrowingrate.md) (default: current borrowing rate plus 0.5%). | + diff --git a/docs/sdk/lib-ethers.ethersliquity.adjusttrove.md b/docs/sdk/lib-ethers.ethersliquity.adjusttrove.md index b9fb38566f..c3eae04b62 100644 --- a/docs/sdk/lib-ethers.ethersliquity.adjusttrove.md +++ b/docs/sdk/lib-ethers.ethersliquity.adjusttrove.md @@ -9,7 +9,7 @@ Adjust existing Trove by changing its collateral, debt, or both. Signature: ```typescript -adjustTrove(params: TroveAdjustmentParams, maxBorrowingRate?: Decimalish, overrides?: EthersTransactionOverrides): Promise; +adjustTrove(params: TroveAdjustmentParams, maxBorrowingRateOrOptionalParams?: Decimalish | BorrowingOperationOptionalParams, overrides?: EthersTransactionOverrides): Promise; ``` ## Parameters @@ -17,7 +17,7 @@ adjustTrove(params: TroveAdjustmentParams, maxBorrowingRate?: Decima | Parameter | Type | Description | | --- | --- | --- | | params | [TroveAdjustmentParams](./lib-base.troveadjustmentparams.md)<[Decimalish](./lib-base.decimalish.md)> | Parameters of the adjustment. | -| maxBorrowingRate | [Decimalish](./lib-base.decimalish.md) | Maximum acceptable [borrowing rate](./lib-base.fees.borrowingrate.md) if params includes borrowLUSD. | +| maxBorrowingRateOrOptionalParams | [Decimalish](./lib-base.decimalish.md) \| [BorrowingOperationOptionalParams](./lib-ethers.borrowingoperationoptionalparams.md) | | | overrides | [EthersTransactionOverrides](./lib-ethers.etherstransactionoverrides.md) | | Returns: diff --git a/docs/sdk/lib-ethers.ethersliquity.md b/docs/sdk/lib-ethers.ethersliquity.md index 12e57edcf9..c8a45432b0 100644 --- a/docs/sdk/lib-ethers.ethersliquity.md +++ b/docs/sdk/lib-ethers.ethersliquity.md @@ -29,7 +29,7 @@ The constructor for this class is marked as internal. Third-party code should no | Method | Modifiers | Description | | --- | --- | --- | -| [adjustTrove(params, maxBorrowingRate, overrides)](./lib-ethers.ethersliquity.adjusttrove.md) | | Adjust existing Trove by changing its collateral, debt, or both. | +| [adjustTrove(params, maxBorrowingRateOrOptionalParams, overrides)](./lib-ethers.ethersliquity.adjusttrove.md) | | Adjust existing Trove by changing its collateral, debt, or both. | | [approveUniTokens(allowance, overrides)](./lib-ethers.ethersliquity.approveunitokens.md) | | Allow the liquidity mining contract to use Uniswap ETH/LUSD LP tokens for [staking](./lib-base.transactableliquity.stakeunitokens.md). | | [borrowLUSD(amount, maxBorrowingRate, overrides)](./lib-ethers.ethersliquity.borrowlusd.md) | | Adjust existing Trove by borrowing more LUSD. | | [claimCollateralSurplus(overrides)](./lib-ethers.ethersliquity.claimcollateralsurplus.md) | | Claim leftover collateral after a liquidation or redemption. | @@ -65,7 +65,7 @@ The constructor for this class is marked as internal. Third-party code should no | [hasStore(store)](./lib-ethers.ethersliquity.hasstore_1.md) | | Check whether this EthersLiquity is an [EthersLiquityWithStore](./lib-ethers.ethersliquitywithstore.md)<[BlockPolledLiquityStore](./lib-ethers.blockpolledliquitystore.md)>. | | [liquidate(address, overrides)](./lib-ethers.ethersliquity.liquidate.md) | | Liquidate one or more undercollateralized Troves. | | [liquidateUpTo(maximumNumberOfTrovesToLiquidate, overrides)](./lib-ethers.ethersliquity.liquidateupto.md) | | Liquidate the least collateralized Troves up to a maximum number. | -| [openTrove(params, maxBorrowingRate, overrides)](./lib-ethers.ethersliquity.opentrove.md) | | Open a new Trove by depositing collateral and borrowing LUSD. | +| [openTrove(params, maxBorrowingRateOrOptionalParams, overrides)](./lib-ethers.ethersliquity.opentrove.md) | | Open a new Trove by depositing collateral and borrowing LUSD. | | [redeemLUSD(amount, maxRedemptionRate, overrides)](./lib-ethers.ethersliquity.redeemlusd.md) | | Redeem LUSD to native currency (e.g. Ether) at face value. | | [registerFrontend(kickbackRate, overrides)](./lib-ethers.ethersliquity.registerfrontend.md) | | Register current wallet address as a Liquity frontend. | | [repayLUSD(amount, overrides)](./lib-ethers.ethersliquity.repaylusd.md) | | Adjust existing Trove by repaying some of its debt. | diff --git a/docs/sdk/lib-ethers.ethersliquity.opentrove.md b/docs/sdk/lib-ethers.ethersliquity.opentrove.md index c0eefd6acb..d37bbc40a1 100644 --- a/docs/sdk/lib-ethers.ethersliquity.opentrove.md +++ b/docs/sdk/lib-ethers.ethersliquity.opentrove.md @@ -9,7 +9,7 @@ Open a new Trove by depositing collateral and borrowing LUSD. Signature: ```typescript -openTrove(params: TroveCreationParams, maxBorrowingRate?: Decimalish, overrides?: EthersTransactionOverrides): Promise; +openTrove(params: TroveCreationParams, maxBorrowingRateOrOptionalParams?: Decimalish | BorrowingOperationOptionalParams, overrides?: EthersTransactionOverrides): Promise; ``` ## Parameters @@ -17,7 +17,7 @@ openTrove(params: TroveCreationParams, maxBorrowingRate?: Decimalish | Parameter | Type | Description | | --- | --- | --- | | params | [TroveCreationParams](./lib-base.trovecreationparams.md)<[Decimalish](./lib-base.decimalish.md)> | How much to deposit and borrow. | -| maxBorrowingRate | [Decimalish](./lib-base.decimalish.md) | Maximum acceptable [borrowing rate](./lib-base.fees.borrowingrate.md). | +| maxBorrowingRateOrOptionalParams | [Decimalish](./lib-base.decimalish.md) \| [BorrowingOperationOptionalParams](./lib-ethers.borrowingoperationoptionalparams.md) | | | overrides | [EthersTransactionOverrides](./lib-ethers.etherstransactionoverrides.md) | | Returns: diff --git a/docs/sdk/lib-ethers.md b/docs/sdk/lib-ethers.md index 1acac6a5d0..55ecab4ff0 100644 --- a/docs/sdk/lib-ethers.md +++ b/docs/sdk/lib-ethers.md @@ -25,6 +25,7 @@ | Interface | Description | | --- | --- | | [BlockPolledLiquityStoreExtraState](./lib-ethers.blockpolledliquitystoreextrastate.md) | Extra state added to [LiquityStoreState](./lib-base.liquitystorestate.md) by [BlockPolledLiquityStore](./lib-ethers.blockpolledliquitystore.md). | +| [BorrowingOperationOptionalParams](./lib-ethers.borrowingoperationoptionalparams.md) | Optional parameters of a transaction that borrows LUSD. | | [EthersCallOverrides](./lib-ethers.etherscalloverrides.md) | Optional parameters taken by [ReadableEthersLiquity](./lib-ethers.readableethersliquity.md) functions. | | [EthersLiquityConnection](./lib-ethers.ethersliquityconnection.md) | Information about a connection to the Liquity protocol. | | [EthersLiquityConnectionOptionalParams](./lib-ethers.ethersliquityconnectionoptionalparams.md) | Optional parameters of [ReadableEthersLiquity.connect()](./lib-ethers.readableethersliquity.connect_1.md) and [EthersLiquity.connect()](./lib-ethers.ethersliquity.connect_1.md). | diff --git a/docs/sdk/lib-ethers.populatableethersliquity.adjusttrove.md b/docs/sdk/lib-ethers.populatableethersliquity.adjusttrove.md index 0d385c1b5d..4765a66d9a 100644 --- a/docs/sdk/lib-ethers.populatableethersliquity.adjusttrove.md +++ b/docs/sdk/lib-ethers.populatableethersliquity.adjusttrove.md @@ -9,7 +9,7 @@ Adjust existing Trove by changing its collateral, debt, or both. Signature: ```typescript -adjustTrove(params: TroveAdjustmentParams, maxBorrowingRate?: Decimalish, overrides?: EthersTransactionOverrides): Promise>; +adjustTrove(params: TroveAdjustmentParams, maxBorrowingRateOrOptionalParams?: Decimalish | BorrowingOperationOptionalParams, overrides?: EthersTransactionOverrides): Promise>; ``` ## Parameters @@ -17,7 +17,7 @@ adjustTrove(params: TroveAdjustmentParams, maxBorrowingRate?: Decima | Parameter | Type | Description | | --- | --- | --- | | params | [TroveAdjustmentParams](./lib-base.troveadjustmentparams.md)<[Decimalish](./lib-base.decimalish.md)> | Parameters of the adjustment. | -| maxBorrowingRate | [Decimalish](./lib-base.decimalish.md) | Maximum acceptable [borrowing rate](./lib-base.fees.borrowingrate.md) if params includes borrowLUSD. | +| maxBorrowingRateOrOptionalParams | [Decimalish](./lib-base.decimalish.md) \| [BorrowingOperationOptionalParams](./lib-ethers.borrowingoperationoptionalparams.md) | | | overrides | [EthersTransactionOverrides](./lib-ethers.etherstransactionoverrides.md) | | Returns: diff --git a/docs/sdk/lib-ethers.populatableethersliquity.md b/docs/sdk/lib-ethers.populatableethersliquity.md index facbdd17c7..f2b1c08f0e 100644 --- a/docs/sdk/lib-ethers.populatableethersliquity.md +++ b/docs/sdk/lib-ethers.populatableethersliquity.md @@ -23,7 +23,7 @@ export declare class PopulatableEthersLiquity implements PopulatableLiquity. | | [borrowLUSD(amount, maxBorrowingRate, overrides)](./lib-ethers.populatableethersliquity.borrowlusd.md) | | Adjust existing Trove by borrowing more LUSD. | | [claimCollateralSurplus(overrides)](./lib-ethers.populatableethersliquity.claimcollateralsurplus.md) | | Claim leftover collateral after a liquidation or redemption. | @@ -33,7 +33,7 @@ export declare class PopulatableEthersLiquity implements PopulatableLiquitySignature: ```typescript -openTrove(params: TroveCreationParams, maxBorrowingRate?: Decimalish, overrides?: EthersTransactionOverrides): Promise>; +openTrove(params: TroveCreationParams, maxBorrowingRateOrOptionalParams?: Decimalish | BorrowingOperationOptionalParams, overrides?: EthersTransactionOverrides): Promise>; ``` ## Parameters @@ -17,7 +17,7 @@ openTrove(params: TroveCreationParams, maxBorrowingRate?: Decimalish | Parameter | Type | Description | | --- | --- | --- | | params | [TroveCreationParams](./lib-base.trovecreationparams.md)<[Decimalish](./lib-base.decimalish.md)> | How much to deposit and borrow. | -| maxBorrowingRate | [Decimalish](./lib-base.decimalish.md) | Maximum acceptable [borrowing rate](./lib-base.fees.borrowingrate.md). | +| maxBorrowingRateOrOptionalParams | [Decimalish](./lib-base.decimalish.md) \| [BorrowingOperationOptionalParams](./lib-ethers.borrowingoperationoptionalparams.md) | | | overrides | [EthersTransactionOverrides](./lib-ethers.etherstransactionoverrides.md) | | Returns: diff --git a/docs/sdk/lib-ethers.populatedethersliquitytransaction.gasheadroom.md b/docs/sdk/lib-ethers.populatedethersliquitytransaction.gasheadroom.md new file mode 100644 index 0000000000..ec4ba0952e --- /dev/null +++ b/docs/sdk/lib-ethers.populatedethersliquitytransaction.gasheadroom.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [@liquity/lib-ethers](./lib-ethers.md) > [PopulatedEthersLiquityTransaction](./lib-ethers.populatedethersliquitytransaction.md) > [gasHeadroom](./lib-ethers.populatedethersliquitytransaction.gasheadroom.md) + +## PopulatedEthersLiquityTransaction.gasHeadroom property + +Extra gas added to the transaction's `gasLimit` on top of the estimated minimum requirement. + +Signature: + +```typescript +readonly gasHeadroom?: number; +``` + +## Remarks + +Gas estimation is based on blockchain state at the latest block. However, most transactions stay in pending state for several blocks before being included in a block. This may increase the actual gas requirements of certain Liquity transactions by the time they are eventually mined, therefore the Liquity SDK increases these transactions' `gasLimit` by default (unless `gasLimit` is [overridden](./lib-ethers.etherstransactionoverrides.md)). + +Note: even though the SDK includes gas headroom for many transaction types, currently this property is only implemented for [openTrove()](./lib-ethers.populatableethersliquity.opentrove.md), [adjustTrove()](./lib-ethers.populatableethersliquity.adjusttrove.md) and its aliases. + diff --git a/docs/sdk/lib-ethers.populatedethersliquitytransaction.md b/docs/sdk/lib-ethers.populatedethersliquitytransaction.md index 0406599c26..0a8b6a264a 100644 --- a/docs/sdk/lib-ethers.populatedethersliquitytransaction.md +++ b/docs/sdk/lib-ethers.populatedethersliquitytransaction.md @@ -23,6 +23,7 @@ The constructor for this class is marked as internal. Third-party code should no | Property | Modifiers | Type | Description | | --- | --- | --- | --- | +| [gasHeadroom?](./lib-ethers.populatedethersliquitytransaction.gasheadroom.md) | | number | (Optional) Extra gas added to the transaction's gasLimit on top of the estimated minimum requirement. | | [rawPopulatedTransaction](./lib-ethers.populatedethersliquitytransaction.rawpopulatedtransaction.md) | | [EthersPopulatedTransaction](./lib-ethers.etherspopulatedtransaction.md) | Unsigned transaction object populated by Ethers. | ## Methods diff --git a/docs/sdk/lib-ethers.sendableethersliquity.adjusttrove.md b/docs/sdk/lib-ethers.sendableethersliquity.adjusttrove.md index 68e6049c5a..cb54ae15a2 100644 --- a/docs/sdk/lib-ethers.sendableethersliquity.adjusttrove.md +++ b/docs/sdk/lib-ethers.sendableethersliquity.adjusttrove.md @@ -9,7 +9,7 @@ Adjust existing Trove by changing its collateral, debt, or both. Signature: ```typescript -adjustTrove(params: TroveAdjustmentParams, maxBorrowingRate?: Decimalish, overrides?: EthersTransactionOverrides): Promise>; +adjustTrove(params: TroveAdjustmentParams, maxBorrowingRateOrOptionalParams?: Decimalish | BorrowingOperationOptionalParams, overrides?: EthersTransactionOverrides): Promise>; ``` ## Parameters @@ -17,7 +17,7 @@ adjustTrove(params: TroveAdjustmentParams, maxBorrowingRate?: Decima | Parameter | Type | Description | | --- | --- | --- | | params | [TroveAdjustmentParams](./lib-base.troveadjustmentparams.md)<[Decimalish](./lib-base.decimalish.md)> | Parameters of the adjustment. | -| maxBorrowingRate | [Decimalish](./lib-base.decimalish.md) | Maximum acceptable [borrowing rate](./lib-base.fees.borrowingrate.md) if params includes borrowLUSD. | +| maxBorrowingRateOrOptionalParams | [Decimalish](./lib-base.decimalish.md) \| [BorrowingOperationOptionalParams](./lib-ethers.borrowingoperationoptionalparams.md) | | | overrides | [EthersTransactionOverrides](./lib-ethers.etherstransactionoverrides.md) | | Returns: diff --git a/docs/sdk/lib-ethers.sendableethersliquity.md b/docs/sdk/lib-ethers.sendableethersliquity.md index e73ca0010d..4cbc0118e8 100644 --- a/docs/sdk/lib-ethers.sendableethersliquity.md +++ b/docs/sdk/lib-ethers.sendableethersliquity.md @@ -23,7 +23,7 @@ export declare class SendableEthersLiquity implements SendableLiquity. | | [borrowLUSD(amount, maxBorrowingRate, overrides)](./lib-ethers.sendableethersliquity.borrowlusd.md) | | Adjust existing Trove by borrowing more LUSD. | | [claimCollateralSurplus(overrides)](./lib-ethers.sendableethersliquity.claimcollateralsurplus.md) | | Claim leftover collateral after a liquidation or redemption. | @@ -33,7 +33,7 @@ export declare class SendableEthersLiquity implements SendableLiquitySignature: ```typescript -openTrove(params: TroveCreationParams, maxBorrowingRate?: Decimalish, overrides?: EthersTransactionOverrides): Promise>; +openTrove(params: TroveCreationParams, maxBorrowingRateOrOptionalParams?: Decimalish | BorrowingOperationOptionalParams, overrides?: EthersTransactionOverrides): Promise>; ``` ## Parameters @@ -17,7 +17,7 @@ openTrove(params: TroveCreationParams, maxBorrowingRate?: Decimalish | Parameter | Type | Description | | --- | --- | --- | | params | [TroveCreationParams](./lib-base.trovecreationparams.md)<[Decimalish](./lib-base.decimalish.md)> | How much to deposit and borrow. | -| maxBorrowingRate | [Decimalish](./lib-base.decimalish.md) | Maximum acceptable [borrowing rate](./lib-base.fees.borrowingrate.md). | +| maxBorrowingRateOrOptionalParams | [Decimalish](./lib-base.decimalish.md) \| [BorrowingOperationOptionalParams](./lib-ethers.borrowingoperationoptionalparams.md) | | | overrides | [EthersTransactionOverrides](./lib-ethers.etherstransactionoverrides.md) | | Returns: diff --git a/packages/lib-ethers/etc/lib-ethers.api.md b/packages/lib-ethers/etc/lib-ethers.api.md index f158003580..a4a313dec9 100644 --- a/packages/lib-ethers/etc/lib-ethers.api.md +++ b/packages/lib-ethers/etc/lib-ethers.api.md @@ -62,11 +62,19 @@ export class BlockPolledLiquityStore extends LiquityStore Fees; } // @public export type BlockPolledLiquityStoreState = LiquityStoreState; +// @public +export interface BorrowingOperationOptionalParams { + borrowingFeeDecayToleranceMinutes?: number; + maxBorrowingRate?: Decimalish; +} + // @internal (undocumented) export function _connectByChainId(provider: EthersProvider, signer: EthersSigner | undefined, chainId: number, optionalParams: EthersLiquityConnectionOptionalParams & { useStore: T; @@ -88,7 +96,7 @@ export class EthersLiquity implements ReadableEthersLiquity, TransactableLiquity // @internal constructor(readable: ReadableEthersLiquity); // (undocumented) - adjustTrove(params: TroveAdjustmentParams, maxBorrowingRate?: Decimalish, overrides?: EthersTransactionOverrides): Promise; + adjustTrove(params: TroveAdjustmentParams, maxBorrowingRateOrOptionalParams?: Decimalish | BorrowingOperationOptionalParams, overrides?: EthersTransactionOverrides): Promise; // (undocumented) approveUniTokens(allowance?: Decimalish, overrides?: EthersTransactionOverrides): Promise; // (undocumented) @@ -117,6 +125,8 @@ export class EthersLiquity implements ReadableEthersLiquity, TransactableLiquity static _from(connection: EthersLiquityConnection): EthersLiquity; // @internal (undocumented) _getActivePool(overrides?: EthersCallOverrides): Promise; + // @internal (undocumented) + _getBlockTimestamp(blockTag?: BlockTag): Promise; // (undocumented) getCollateralSurplusBalance(address?: string, overrides?: EthersCallOverrides): Promise; // @internal (undocumented) @@ -182,7 +192,7 @@ export class EthersLiquity implements ReadableEthersLiquity, TransactableLiquity // @internal (undocumented) _mintUniToken(amount: Decimalish, address?: string, overrides?: EthersTransactionOverrides): Promise; // (undocumented) - openTrove(params: TroveCreationParams, maxBorrowingRate?: Decimalish, overrides?: EthersTransactionOverrides): Promise; + openTrove(params: TroveCreationParams, maxBorrowingRateOrOptionalParams?: Decimalish | BorrowingOperationOptionalParams, overrides?: EthersTransactionOverrides): Promise; readonly populate: PopulatableEthersLiquity; // (undocumented) redeemLUSD(amount: Decimalish, maxRedemptionRate?: Decimalish, overrides?: EthersTransactionOverrides): Promise; @@ -321,7 +331,7 @@ export class ObservableEthersLiquity implements ObservableLiquity { export class PopulatableEthersLiquity implements PopulatableLiquity { constructor(readable: ReadableEthersLiquity); // (undocumented) - adjustTrove(params: TroveAdjustmentParams, maxBorrowingRate?: Decimalish, overrides?: EthersTransactionOverrides): Promise>; + adjustTrove(params: TroveAdjustmentParams, maxBorrowingRateOrOptionalParams?: Decimalish | BorrowingOperationOptionalParams, overrides?: EthersTransactionOverrides): Promise>; // (undocumented) approveUniTokens(allowance?: Decimalish, overrides?: EthersTransactionOverrides): Promise>; // (undocumented) @@ -343,7 +353,7 @@ export class PopulatableEthersLiquity implements PopulatableLiquity>; // (undocumented) - openTrove(params: TroveCreationParams, maxBorrowingRate?: Decimalish, overrides?: EthersTransactionOverrides): Promise>; + openTrove(params: TroveCreationParams, maxBorrowingRateOrOptionalParams?: Decimalish | BorrowingOperationOptionalParams, overrides?: EthersTransactionOverrides): Promise>; // (undocumented) redeemLUSD(amount: Decimalish, maxRedemptionRate?: Decimalish, overrides?: EthersTransactionOverrides): Promise; // (undocumented) @@ -381,7 +391,8 @@ export class PopulatableEthersLiquity implements PopulatableLiquity implements PopulatedLiquityTransaction> { // @internal - constructor(rawPopulatedTransaction: EthersPopulatedTransaction, connection: EthersLiquityConnection, parse: (rawReceipt: EthersTransactionReceipt) => T); + constructor(rawPopulatedTransaction: EthersPopulatedTransaction, connection: EthersLiquityConnection, parse: (rawReceipt: EthersTransactionReceipt) => T, gasHeadroom?: number); + readonly gasHeadroom?: number; readonly rawPopulatedTransaction: EthersPopulatedTransaction; // (undocumented) send(): Promise>; @@ -449,6 +460,8 @@ export class ReadableEthersLiquity implements ReadableLiquity { static _from(connection: EthersLiquityConnection): ReadableEthersLiquity; // @internal (undocumented) _getActivePool(overrides?: EthersCallOverrides): Promise; + // @internal (undocumented) + _getBlockTimestamp(blockTag?: BlockTag): Promise; // (undocumented) getCollateralSurplusBalance(address?: string, overrides?: EthersCallOverrides): Promise; // @internal (undocumented) @@ -521,7 +534,7 @@ export const _redeemMaxIterations = 70; export class SendableEthersLiquity implements SendableLiquity { constructor(populatable: PopulatableEthersLiquity); // (undocumented) - adjustTrove(params: TroveAdjustmentParams, maxBorrowingRate?: Decimalish, overrides?: EthersTransactionOverrides): Promise>; + adjustTrove(params: TroveAdjustmentParams, maxBorrowingRateOrOptionalParams?: Decimalish | BorrowingOperationOptionalParams, overrides?: EthersTransactionOverrides): Promise>; // (undocumented) approveUniTokens(allowance?: Decimalish, overrides?: EthersTransactionOverrides): Promise>; // (undocumented) @@ -543,7 +556,7 @@ export class SendableEthersLiquity implements SendableLiquity>; // (undocumented) - openTrove(params: TroveCreationParams, maxBorrowingRate?: Decimalish, overrides?: EthersTransactionOverrides): Promise>; + openTrove(params: TroveCreationParams, maxBorrowingRateOrOptionalParams?: Decimalish | BorrowingOperationOptionalParams, overrides?: EthersTransactionOverrides): Promise>; // (undocumented) redeemLUSD(amount: Decimalish, maxRedemptionRate?: Decimalish, overrides?: EthersTransactionOverrides): Promise>; // (undocumented) diff --git a/packages/lib-ethers/package.json b/packages/lib-ethers/package.json index ebbf24570b..44ea6746a1 100644 --- a/packages/lib-ethers/package.json +++ b/packages/lib-ethers/package.json @@ -28,6 +28,7 @@ "save-live-version:run": "ts-node scripts/save-live-version.ts", "save-live-version:check": "run-s check-live-version", "scrape-eth-usd": "ts-node scripts/scrape-eth-usd.ts", + "spam-troves": "ts-node scripts/spam-troves.ts", "test": "hardhat test", "test-live": "run-s test-live:*", "test-live:check-version": "run-s check-live-version", @@ -47,6 +48,7 @@ "@types/mocha": "8.2.1", "@types/node": "14.14.34", "@types/sinon-chai": "3.2.5", + "@types/ws": "7.4.4", "@typescript-eslint/eslint-plugin": "4.17.0", "@typescript-eslint/parser": "4.18.0", "chai": "4.3.4", @@ -61,6 +63,7 @@ "hardhat": "2.1.1", "npm-run-all": "4.1.5", "ts-node": "9.1.1", - "typescript": "4.1.5" + "typescript": "4.1.5", + "ws": "7.4.6" } } diff --git a/packages/lib-ethers/scripts/spam-troves.ts b/packages/lib-ethers/scripts/spam-troves.ts new file mode 100644 index 0000000000..afbda677a7 --- /dev/null +++ b/packages/lib-ethers/scripts/spam-troves.ts @@ -0,0 +1,104 @@ +import WebSocket from "ws"; +import { TransactionResponse } from "@ethersproject/abstract-provider"; +import { JsonRpcProvider } from "@ethersproject/providers"; +import { Wallet } from "@ethersproject/wallet"; + +import { Decimal, LUSD_MINIMUM_DEBT, Trove } from "@liquity/lib-base"; +import { EthersLiquity, EthersLiquityWithStore, BlockPolledLiquityStore } from "@liquity/lib-ethers"; + +import { + Batched, + BatchedProvider, + WebSocketAugmented, + WebSocketAugmentedProvider +} from "@liquity/providers"; + +const BatchedWebSocketAugmentedJsonRpcProvider = Batched(WebSocketAugmented(JsonRpcProvider)); + +Object.assign(globalThis, { WebSocket }); + +const numberOfTrovesToCreate = 1000; +const collateralRatioStart = Decimal.from(2); +const collateralRatioStep = Decimal.from(1e-6); +const funderKey = "0x4d5db4107d237df6a3d58ee5f70ae63d73d7658d4026f2eefd2f204c81682cb7"; + +let provider: BatchedProvider & WebSocketAugmentedProvider & JsonRpcProvider; +let funder: Wallet; +let liquity: EthersLiquityWithStore; + +const waitForSuccess = (tx: TransactionResponse) => + tx.wait().then(receipt => { + if (!receipt.status) { + throw new Error("Transaction failed"); + } + return receipt; + }); + +const createTrove = async (nominalCollateralRatio: Decimal) => { + const randomWallet = Wallet.createRandom().connect(provider); + + const debt = LUSD_MINIMUM_DEBT.mul(2); + const collateral = debt.mul(nominalCollateralRatio); + + await funder + .sendTransaction({ + to: randomWallet.address, + value: collateral.hex + }) + .then(waitForSuccess); + + await liquity.populate + .openTrove( + Trove.recreate(new Trove(collateral, debt), liquity.store.state.borrowingRate), + {}, + { from: randomWallet.address } + ) + .then(tx => randomWallet.signTransaction(tx.rawPopulatedTransaction)) + .then(tx => provider.sendTransaction(tx)) + .then(waitForSuccess); +}; + +const runLoop = async () => { + for (let i = 0; i < numberOfTrovesToCreate; ++i) { + const collateralRatio = collateralRatioStep.mul(i).add(collateralRatioStart); + const nominalCollateralRatio = collateralRatio.div(liquity.store.state.price); + + await createTrove(nominalCollateralRatio); + + if ((i + 1) % 10 == 0) { + console.log(`Created ${i + 1} Troves.`); + } + } +}; + +const main = async () => { + provider = new BatchedWebSocketAugmentedJsonRpcProvider(); + funder = new Wallet(funderKey, provider); + + const network = await provider.getNetwork(); + + provider.chainId = network.chainId; + provider.openWebSocket( + provider.connection.url.replace(/^http/i, "ws").replace("8545", "8546"), + network + ); + + liquity = await EthersLiquity.connect(provider, { useStore: "blockPolled" }); + + let stopStore: () => void; + + return new Promise(resolve => { + liquity.store.onLoaded = resolve; + stopStore = liquity.store.start(); + }) + .then(runLoop) + .then(() => { + stopStore(); + provider.closeWebSocket(); + }); +}; + +main().catch(err => { + console.error(err); + process.exit(1); +}); diff --git a/packages/lib-ethers/src/BlockPolledLiquityStore.ts b/packages/lib-ethers/src/BlockPolledLiquityStore.ts index 265eb56d11..33aa03cd33 100644 --- a/packages/lib-ethers/src/BlockPolledLiquityStore.ts +++ b/packages/lib-ethers/src/BlockPolledLiquityStore.ts @@ -1,4 +1,3 @@ -import { BigNumber } from "@ethersproject/bignumber"; import { AddressZero } from "@ethersproject/constants"; import { @@ -8,15 +7,13 @@ import { TroveWithPendingRedistribution, StabilityDeposit, LQTYStake, - LiquityStore + LiquityStore, + Fees } from "@liquity/lib-base"; +import { decimalify, promiseAllValues } from "./_utils"; import { ReadableEthersLiquity } from "./ReadableEthersLiquity"; -import { - EthersLiquityConnection, - _getBlockTimestamp, - _getProvider -} from "./EthersLiquityConnection"; +import { EthersLiquityConnection, _getProvider } from "./EthersLiquityConnection"; import { EthersCallOverrides, EthersProvider } from "./types"; /** @@ -38,6 +35,9 @@ export interface BlockPolledLiquityStoreExtraState { * Timestamp of latest block (number of seconds since epoch). */ blockTimestamp: number; + + /** @internal */ + _feesFactory: (blockTimestamp: number, recoveryMode: boolean) => Fees; } /** @@ -48,19 +48,6 @@ export interface BlockPolledLiquityStoreExtraState { */ export type BlockPolledLiquityStoreState = LiquityStoreState; -type Resolved = T extends Promise ? U : T; -type ResolvedValues = { [P in keyof T]: Resolved }; - -const promiseAllValues = (object: T) => { - const keys = Object.keys(object); - - return Promise.all(Object.values(object)).then(values => - Object.fromEntries(values.map((value, i) => [keys[i], value])) - ) as Promise>; -}; - -const decimalify = (bigNumber: BigNumber) => Decimal.fromBigNumberString(bigNumber.toHexString()); - /** * Ethers-based {@link @liquity/lib-base#LiquityStore} that updates state whenever there's a new * block. @@ -103,12 +90,12 @@ export class BlockPolledLiquityStore extends LiquityStore { + return this._readable._getBlockTimestamp(blockTag); + } + /** @internal */ _getFeesFactory( overrides?: EthersCallOverrides @@ -310,10 +321,12 @@ export class EthersLiquity implements ReadableEthersLiquity, TransactableLiquity */ openTrove( params: TroveCreationParams, - maxBorrowingRate?: Decimalish, + maxBorrowingRateOrOptionalParams?: Decimalish | BorrowingOperationOptionalParams, overrides?: EthersTransactionOverrides ): Promise { - return this.send.openTrove(params, maxBorrowingRate, overrides).then(waitForSuccess); + return this.send + .openTrove(params, maxBorrowingRateOrOptionalParams, overrides) + .then(waitForSuccess); } /** @@ -336,10 +349,12 @@ export class EthersLiquity implements ReadableEthersLiquity, TransactableLiquity */ adjustTrove( params: TroveAdjustmentParams, - maxBorrowingRate?: Decimalish, + maxBorrowingRateOrOptionalParams?: Decimalish | BorrowingOperationOptionalParams, overrides?: EthersTransactionOverrides ): Promise { - return this.send.adjustTrove(params, maxBorrowingRate, overrides).then(waitForSuccess); + return this.send + .adjustTrove(params, maxBorrowingRateOrOptionalParams, overrides) + .then(waitForSuccess); } /** diff --git a/packages/lib-ethers/src/EthersLiquityConnection.ts b/packages/lib-ethers/src/EthersLiquityConnection.ts index 641dc50e36..d6cf89cf0a 100644 --- a/packages/lib-ethers/src/EthersLiquityConnection.ts +++ b/packages/lib-ethers/src/EthersLiquityConnection.ts @@ -1,4 +1,3 @@ -import { BigNumber } from "@ethersproject/bignumber"; import { Block, BlockTag } from "@ethersproject/abstract-provider"; import { Signer } from "@ethersproject/abstract-signer"; @@ -11,6 +10,7 @@ import rinkeby from "../deployments/rinkeby.json"; import ropsten from "../deployments/ropsten.json"; import mainnet from "../deployments/mainnet.json"; +import { numberify, panic } from "./_utils"; import { EthersProvider, EthersSigner } from "./types"; import { @@ -139,8 +139,6 @@ export const _getContracts = (connection: EthersLiquityConnection): _LiquityCont const getMulticall = (connection: EthersLiquityConnection): _Multicall | undefined => (connection as _InternalEthersLiquityConnection)._multicall; -const numberify = (bigNumber: BigNumber) => bigNumber.toNumber(); - const getTimestampFromBlock = ({ timestamp }: Block) => timestamp; /** @internal */ @@ -152,10 +150,6 @@ export const _getBlockTimestamp = ( getMulticall(connection)?.getCurrentBlockTimestamp({ blockTag }).then(numberify) ?? _getProvider(connection).getBlock(blockTag).then(getTimestampFromBlock); -const panic = (e: unknown): T => { - throw e; -}; - /** @internal */ export const _requireSigner = (connection: EthersLiquityConnection): EthersSigner => connection.signer ?? panic(new Error("Must be connected through a Signer")); diff --git a/packages/lib-ethers/src/PopulatableEthersLiquity.ts b/packages/lib-ethers/src/PopulatableEthersLiquity.ts index 9df9c596ee..3e311536b4 100644 --- a/packages/lib-ethers/src/PopulatableEthersLiquity.ts +++ b/packages/lib-ethers/src/PopulatableEthersLiquity.ts @@ -12,6 +12,7 @@ import { Decimalish, LiquidationDetails, LiquityReceipt, + LUSD_MINIMUM_DEBT, LUSD_MINIMUM_NET_DEBT, MinedReceipt, PopulatableLiquity, @@ -49,11 +50,12 @@ import { _requireSigner } from "./EthersLiquityConnection"; +import { decimalify, promiseAllValues } from "./_utils"; import { _priceFeedIsTestnet, _uniTokenIsMock } from "./contracts"; import { logsToString } from "./parseLogs"; import { ReadableEthersLiquity } from "./ReadableEthersLiquity"; -const decimalify = (bigNumber: BigNumber) => Decimal.fromBigNumberString(bigNumber.toHexString()); +const bigNumberMax = (a: BigNumber, b?: BigNumber) => (b?.gt(a) ? b : a); // With 70 iterations redemption costs about ~10M gas, and each iteration accounts for ~138k more /** @internal */ @@ -61,6 +63,7 @@ export const _redeemMaxIterations = 70; const defaultBorrowingRateSlippageTolerance = Decimal.from(0.005); // 0.5% const defaultRedemptionRateSlippageTolerance = Decimal.from(0.001); // 0.1% +const defaultBorrowingFeeDecayToleranceMinutes = 10; const noDetails = () => undefined; @@ -261,6 +264,59 @@ export class SentEthersLiquityTransaction } } +/** + * Optional parameters of a transaction that borrows LUSD. + * + * @public + */ +export interface BorrowingOperationOptionalParams { + /** + * Maximum acceptable {@link @liquity/lib-base#Fees.borrowingRate | borrowing rate} + * (default: current borrowing rate plus 0.5%). + */ + maxBorrowingRate?: Decimalish; + + /** TODO */ + borrowingFeeDecayToleranceMinutes?: number; +} + +const normalizeBorrowingOperationOptionalParams = ( + maxBorrowingRateOrOptionalParams: Decimalish | BorrowingOperationOptionalParams | undefined, + currentBorrowingRate: Decimal | undefined +): { + maxBorrowingRate: Decimal; + borrowingFeeDecayToleranceMinutes: number; +} => { + if (maxBorrowingRateOrOptionalParams === undefined) { + return { + maxBorrowingRate: + currentBorrowingRate?.add(defaultBorrowingRateSlippageTolerance) ?? Decimal.ZERO, + borrowingFeeDecayToleranceMinutes: defaultBorrowingFeeDecayToleranceMinutes + }; + } else if ( + typeof maxBorrowingRateOrOptionalParams === "number" || + typeof maxBorrowingRateOrOptionalParams === "string" || + maxBorrowingRateOrOptionalParams instanceof Decimal + ) { + return { + maxBorrowingRate: Decimal.from(maxBorrowingRateOrOptionalParams), + borrowingFeeDecayToleranceMinutes: defaultBorrowingFeeDecayToleranceMinutes + }; + } else { + const { maxBorrowingRate, borrowingFeeDecayToleranceMinutes } = maxBorrowingRateOrOptionalParams; + + return { + maxBorrowingRate: + maxBorrowingRate !== undefined + ? Decimal.from(maxBorrowingRate) + : currentBorrowingRate?.add(defaultBorrowingRateSlippageTolerance) ?? Decimal.ZERO, + + borrowingFeeDecayToleranceMinutes: + borrowingFeeDecayToleranceMinutes ?? defaultBorrowingFeeDecayToleranceMinutes + }; + } +}; + /** * A transaction that has been prepared for sending. * @@ -275,6 +331,22 @@ export class PopulatedEthersLiquityTransaction /** Unsigned transaction object populated by Ethers. */ readonly rawPopulatedTransaction: EthersPopulatedTransaction; + /** + * Extra gas added to the transaction's `gasLimit` on top of the estimated minimum requirement. + * + * @remarks + * Gas estimation is based on blockchain state at the latest block. However, most transactions + * stay in pending state for several blocks before being included in a block. This may increase + * the actual gas requirements of certain Liquity transactions by the time they are eventually + * mined, therefore the Liquity SDK increases these transactions' `gasLimit` by default (unless + * `gasLimit` is {@link EthersTransactionOverrides | overridden}). + * + * Note: even though the SDK includes gas headroom for many transaction types, currently this + * property is only implemented for {@link PopulatableEthersLiquity.openTrove | openTrove()}, + * {@link PopulatableEthersLiquity.adjustTrove | adjustTrove()} and its aliases. + */ + readonly gasHeadroom?: number; + private readonly _connection: EthersLiquityConnection; private readonly _parse: (rawReceipt: EthersTransactionReceipt) => T; @@ -282,11 +354,16 @@ export class PopulatedEthersLiquityTransaction constructor( rawPopulatedTransaction: EthersPopulatedTransaction, connection: EthersLiquityConnection, - parse: (rawReceipt: EthersTransactionReceipt) => T + parse: (rawReceipt: EthersTransactionReceipt) => T, + gasHeadroom?: number ) { this.rawPopulatedTransaction = rawPopulatedTransaction; this._connection = connection; this._parse = parse; + + if (gasHeadroom !== undefined) { + this.gasHeadroom = gasHeadroom; + } } /** {@inheritDoc @liquity/lib-base#PopulatedLiquityTransaction.send} */ @@ -410,7 +487,8 @@ export class PopulatableEthersLiquity private _wrapTroveChangeWithFees( params: T, - rawPopulatedTransaction: EthersPopulatedTransaction + rawPopulatedTransaction: EthersPopulatedTransaction, + gasHeadroom?: number ): PopulatedEthersLiquityTransaction<_TroveChangeWithFees> { const { borrowerOperations } = _getContracts(this._readable.connection); @@ -432,7 +510,9 @@ export class PopulatableEthersLiquity newTrove, fee }; - } + }, + + gasHeadroom ); } @@ -685,32 +765,79 @@ export class PopulatableEthersLiquity /** {@inheritDoc @liquity/lib-base#PopulatableLiquity.openTrove} */ async openTrove( params: TroveCreationParams, - maxBorrowingRate?: Decimalish, + maxBorrowingRateOrOptionalParams?: Decimalish | BorrowingOperationOptionalParams, overrides?: EthersTransactionOverrides ): Promise> { const { borrowerOperations } = _getContracts(this._readable.connection); - const normalized = _normalizeTroveCreation(params); - const { depositCollateral, borrowLUSD } = normalized; + const normalizedParams = _normalizeTroveCreation(params); + const { depositCollateral, borrowLUSD } = normalizedParams; + + const [fees, blockTimestamp, total, price] = await Promise.all([ + this._readable._getFeesFactory(), + this._readable._getBlockTimestamp(), + this._readable.getTotal(), + this._readable.getPrice() + ]); + + const recoveryMode = total.collateralRatioIsBelowCritical(price); + + const decayBorrowingRate = (seconds: number) => + fees(blockTimestamp + seconds, recoveryMode).borrowingRate(); + + const currentBorrowingRate = decayBorrowingRate(0); + const newTrove = Trove.create(normalizedParams, currentBorrowingRate); + const hints = await this._findHints(newTrove); + + const { + maxBorrowingRate, + borrowingFeeDecayToleranceMinutes + } = normalizeBorrowingOperationOptionalParams( + maxBorrowingRateOrOptionalParams, + currentBorrowingRate + ); + + const txParams = (borrowLUSD: Decimal): Parameters => [ + maxBorrowingRate.hex, + borrowLUSD.hex, + ...hints, + { value: depositCollateral.hex, ...overrides } + ]; - const fees = await this._readable.getFees(); - const borrowingRate = fees.borrowingRate(); - const newTrove = Trove.create(normalized, borrowingRate); + let gasHeadroom: number | undefined; - maxBorrowingRate = - maxBorrowingRate !== undefined - ? Decimal.from(maxBorrowingRate) - : borrowingRate.add(defaultBorrowingRateSlippageTolerance); + if (overrides?.gasLimit === undefined) { + const decayedBorrowingRate = decayBorrowingRate(60 * borrowingFeeDecayToleranceMinutes); + const decayedTrove = Trove.create(normalizedParams, decayedBorrowingRate); + const { borrowLUSD: borrowLUSDSimulatingDecay } = Trove.recreate( + decayedTrove, + currentBorrowingRate + ); + + if (decayedTrove.debt.lt(LUSD_MINIMUM_DEBT)) { + throw new Error( + `Trove's debt might fall below ${LUSD_MINIMUM_DEBT} ` + + `within ${borrowingFeeDecayToleranceMinutes} minutes` + ); + } + + const [gasNow, gasLater] = await Promise.all([ + borrowerOperations.estimateGas.openTrove(...txParams(borrowLUSD)), + borrowerOperations.estimateGas.openTrove(...txParams(borrowLUSDSimulatingDecay)) + ]); + + const gasLimit = addGasForPotentialLastFeeOperationTimeUpdate( + bigNumberMax(addGasForPotentialListTraversal(gasNow), gasLater) + ); + + gasHeadroom = gasLimit.sub(gasNow).toNumber(); + overrides = { ...overrides, gasLimit }; + } return this._wrapTroveChangeWithFees( - normalized, - await borrowerOperations.estimateAndPopulate.openTrove( - { value: depositCollateral.hex, ...overrides }, - compose(addGasForPotentialLastFeeOperationTimeUpdate, addGasForPotentialListTraversal), - maxBorrowingRate.hex, - borrowLUSD.hex, - ...(await this._findHints(newTrove)) - ) + normalizedParams, + await borrowerOperations.populateTransaction.openTrove(...txParams(borrowLUSD)), + gasHeadroom ); } @@ -761,42 +888,90 @@ export class PopulatableEthersLiquity /** {@inheritDoc @liquity/lib-base#PopulatableLiquity.adjustTrove} */ async adjustTrove( params: TroveAdjustmentParams, - maxBorrowingRate?: Decimalish, + maxBorrowingRateOrOptionalParams?: Decimalish | BorrowingOperationOptionalParams, overrides?: EthersTransactionOverrides ): Promise> { const address = _requireAddress(this._readable.connection, overrides); const { borrowerOperations } = _getContracts(this._readable.connection); - const normalized = _normalizeTroveAdjustment(params); - const { depositCollateral, withdrawCollateral, borrowLUSD, repayLUSD } = normalized; + const normalizedParams = _normalizeTroveAdjustment(params); + const { depositCollateral, withdrawCollateral, borrowLUSD, repayLUSD } = normalizedParams; - const [trove, fees] = await Promise.all([ + const [trove, feeVars] = await Promise.all([ this._readable.getTrove(address), - borrowLUSD && this._readable.getFees() + borrowLUSD && + promiseAllValues({ + fees: this._readable._getFeesFactory(), + blockTimestamp: this._readable._getBlockTimestamp(), + total: this._readable.getTotal(), + price: this._readable.getPrice() + }) ]); - const borrowingRate = fees?.borrowingRate(); - const finalTrove = trove.adjust(normalized, borrowingRate); + const decayBorrowingRate = (seconds: number) => + feeVars + ?.fees( + feeVars.blockTimestamp + seconds, + feeVars.total.collateralRatioIsBelowCritical(feeVars.price) + ) + .borrowingRate(); - maxBorrowingRate = - maxBorrowingRate !== undefined - ? Decimal.from(maxBorrowingRate) - : borrowingRate?.add(defaultBorrowingRateSlippageTolerance) ?? Decimal.ZERO; + const currentBorrowingRate = decayBorrowingRate(0); + const adjustedTrove = trove.adjust(normalizedParams, currentBorrowingRate); + const hints = await this._findHints(adjustedTrove); + + const { + maxBorrowingRate, + borrowingFeeDecayToleranceMinutes + } = normalizeBorrowingOperationOptionalParams( + maxBorrowingRateOrOptionalParams, + currentBorrowingRate + ); + + const txParams = (borrowLUSD?: Decimal): Parameters => [ + maxBorrowingRate.hex, + (withdrawCollateral ?? Decimal.ZERO).hex, + (borrowLUSD ?? repayLUSD ?? Decimal.ZERO).hex, + !!borrowLUSD, + ...hints, + { value: depositCollateral?.hex, ...overrides } + ]; + + let gasHeadroom: number | undefined; + + if (overrides?.gasLimit === undefined) { + const decayedBorrowingRate = decayBorrowingRate(60 * borrowingFeeDecayToleranceMinutes); + const decayedTrove = trove.adjust(normalizedParams, decayedBorrowingRate); + const { borrowLUSD: borrowLUSDSimulatingDecay } = trove.adjustTo( + decayedTrove, + currentBorrowingRate + ); + + if (decayedTrove.debt.lt(LUSD_MINIMUM_DEBT)) { + throw new Error( + `Trove's debt might fall below ${LUSD_MINIMUM_DEBT} ` + + `within ${borrowingFeeDecayToleranceMinutes} minutes` + ); + } + + const [gasNow, gasLater] = await Promise.all([ + borrowerOperations.estimateGas.adjustTrove(...txParams(borrowLUSD)), + borrowLUSD && + borrowerOperations.estimateGas.adjustTrove(...txParams(borrowLUSDSimulatingDecay)) + ]); + + const gasLimit = (borrowLUSD ? addGasForPotentialLastFeeOperationTimeUpdate : id)( + bigNumberMax(addGasForPotentialListTraversal(gasNow), gasLater) + ); + + gasHeadroom = gasLimit.sub(gasNow).toNumber(); + overrides = { ...overrides, gasLimit }; + } return this._wrapTroveChangeWithFees( - normalized, - await borrowerOperations.estimateAndPopulate.adjustTrove( - { value: depositCollateral?.hex, ...overrides }, - compose( - borrowLUSD ? addGasForPotentialLastFeeOperationTimeUpdate : id, - addGasForPotentialListTraversal - ), - maxBorrowingRate.hex, - (withdrawCollateral ?? Decimal.ZERO).hex, - (borrowLUSD ?? repayLUSD ?? Decimal.ZERO).hex, - !!borrowLUSD, - ...(await this._findHints(finalTrove)) - ) + normalizedParams, + await borrowerOperations.populateTransaction.adjustTrove(...txParams(borrowLUSD)), + gasHeadroom ); } diff --git a/packages/lib-ethers/src/ReadableEthersLiquity.ts b/packages/lib-ethers/src/ReadableEthersLiquity.ts index 7aa6e8f6d6..4d1a927f2c 100644 --- a/packages/lib-ethers/src/ReadableEthersLiquity.ts +++ b/packages/lib-ethers/src/ReadableEthersLiquity.ts @@ -1,4 +1,4 @@ -import { BigNumber } from "@ethersproject/bignumber"; +import { BlockTag } from "@ethersproject/abstract-provider"; import { Decimal, @@ -17,6 +17,7 @@ import { import { MultiTroveGetter } from "../types"; +import { decimalify, numberify, panic } from "./_utils"; import { EthersCallOverrides, EthersProvider, EthersSigner } from "./types"; import { @@ -46,10 +47,6 @@ enum BackendTroveStatus { closedByRedemption } -const panic = (error: Error): T => { - throw error; -}; - const userTroveStatusFrom = (backendStatus: BackendTroveStatus): UserTroveStatus => backendStatus === BackendTroveStatus.nonExistent ? "nonExistent" @@ -63,8 +60,6 @@ const userTroveStatusFrom = (backendStatus: BackendTroveStatus): UserTroveStatus ? "closedByRedemption" : panic(new Error(`invalid backendStatus ${backendStatus}`)); -const decimalify = (bigNumber: BigNumber) => Decimal.fromBigNumberString(bigNumber.toHexString()); -const numberify = (bigNumber: BigNumber) => bigNumber.toNumber(); const convertToDate = (timestamp: number) => new Date(timestamp * 1000); const validSortingOptions = ["ascendingCollateralRatio", "descendingCollateralRatio"]; @@ -354,7 +349,7 @@ export class ReadableEthersLiquity implements ReadableLiquity { async getRemainingLiquidityMiningLQTYReward(overrides?: EthersCallOverrides): Promise { const [calculateRemainingLQTY, blockTimestamp] = await Promise.all([ this._getRemainingLiquidityMiningLQTYRewardCalculator(overrides), - _getBlockTimestamp(this.connection, overrides?.blockTag) + this._getBlockTimestamp(overrides?.blockTag) ]); return calculateRemainingLQTY(blockTimestamp); @@ -435,6 +430,11 @@ export class ReadableEthersLiquity implements ReadableLiquity { } } + /** @internal */ + _getBlockTimestamp(blockTag?: BlockTag): Promise { + return _getBlockTimestamp(this.connection, blockTag); + } + /** @internal */ async _getFeesFactory( overrides?: EthersCallOverrides @@ -463,7 +463,7 @@ export class ReadableEthersLiquity implements ReadableLiquity { this._getFeesFactory(overrides), this.getTotal(overrides), this.getPrice(overrides), - _getBlockTimestamp(this.connection, overrides?.blockTag) + this._getBlockTimestamp(overrides?.blockTag) ]); return createFees(blockTimestamp, total.collateralRatioIsBelowCritical(price)); @@ -695,6 +695,20 @@ class _BlockPolledReadableEthersLiquity : this._readable.getCollateralSurplusBalance(address, overrides); } + async _getBlockTimestamp(blockTag?: BlockTag): Promise { + return this._blockHit({ blockTag }) + ? this.store.state.blockTimestamp + : this._readable._getBlockTimestamp(blockTag); + } + + async _getFeesFactory( + overrides?: EthersCallOverrides + ): Promise<(blockTimestamp: number, recoveryMode: boolean) => Fees> { + return this._blockHit(overrides) + ? this.store.state._feesFactory + : this._readable._getFeesFactory(overrides); + } + async getFees(overrides?: EthersCallOverrides): Promise { return this._blockHit(overrides) ? this.store.state.fees : this._readable.getFees(overrides); } @@ -739,10 +753,6 @@ class _BlockPolledReadableEthersLiquity throw new Error("Method not implemented."); } - _getFeesFactory(): Promise<(blockTimestamp: number, recoveryMode: boolean) => Fees> { - throw new Error("Method not implemented."); - } - _getRemainingLiquidityMiningLQTYRewardCalculator(): Promise<(blockTimestamp: number) => Decimal> { throw new Error("Method not implemented."); } diff --git a/packages/lib-ethers/src/SendableEthersLiquity.ts b/packages/lib-ethers/src/SendableEthersLiquity.ts index 1cc0d65221..2256326321 100644 --- a/packages/lib-ethers/src/SendableEthersLiquity.ts +++ b/packages/lib-ethers/src/SendableEthersLiquity.ts @@ -20,6 +20,7 @@ import { } from "./types"; import { + BorrowingOperationOptionalParams, PopulatableEthersLiquity, PopulatedEthersLiquityTransaction, SentEthersLiquityTransaction @@ -41,12 +42,14 @@ export class SendableEthersLiquity } /** {@inheritDoc @liquity/lib-base#SendableLiquity.openTrove} */ - openTrove( + async openTrove( params: TroveCreationParams, - maxBorrowingRate?: Decimalish, + maxBorrowingRateOrOptionalParams?: Decimalish | BorrowingOperationOptionalParams, overrides?: EthersTransactionOverrides ): Promise> { - return this._populate.openTrove(params, maxBorrowingRate, overrides).then(sendTransaction); + return this._populate + .openTrove(params, maxBorrowingRateOrOptionalParams, overrides) + .then(sendTransaction); } /** {@inheritDoc @liquity/lib-base#SendableLiquity.closeTrove} */ @@ -59,10 +62,12 @@ export class SendableEthersLiquity /** {@inheritDoc @liquity/lib-base#SendableLiquity.adjustTrove} */ adjustTrove( params: TroveAdjustmentParams, - maxBorrowingRate?: Decimalish, + maxBorrowingRateOrOptionalParams?: Decimalish | BorrowingOperationOptionalParams, overrides?: EthersTransactionOverrides ): Promise> { - return this._populate.adjustTrove(params, maxBorrowingRate, overrides).then(sendTransaction); + return this._populate + .adjustTrove(params, maxBorrowingRateOrOptionalParams, overrides) + .then(sendTransaction); } /** {@inheritDoc @liquity/lib-base#SendableLiquity.depositCollateral} */ diff --git a/packages/lib-ethers/src/_utils.ts b/packages/lib-ethers/src/_utils.ts new file mode 100644 index 0000000000..4e4cbf8c07 --- /dev/null +++ b/packages/lib-ethers/src/_utils.ts @@ -0,0 +1,23 @@ +import { BigNumber } from "@ethersproject/bignumber"; + +import { Decimal } from "@liquity/lib-base"; + +export const numberify = (bigNumber: BigNumber): number => bigNumber.toNumber(); + +export const decimalify = (bigNumber: BigNumber): Decimal => + Decimal.fromBigNumberString(bigNumber.toHexString()); + +export const panic = (e: unknown): T => { + throw e; +}; + +export type Resolved = T extends Promise ? U : T; +export type ResolvedValues = { [P in keyof T]: Resolved }; + +export const promiseAllValues = (object: T): Promise> => { + const keys = Object.keys(object); + + return Promise.all(Object.values(object)).then(values => + Object.fromEntries(values.map((value, i) => [keys[i], value])) + ) as Promise>; +}; diff --git a/packages/lib-ethers/src/contracts.ts b/packages/lib-ethers/src/contracts.ts index 55cf63231e..25a23685bb 100644 --- a/packages/lib-ethers/src/contracts.ts +++ b/packages/lib-ethers/src/contracts.ts @@ -97,6 +97,18 @@ type TypedContract = _TypeSafeContract & : never; }; + readonly estimateGas: { + [P in keyof V]: V[P] extends (...args: infer A) => unknown + ? (...args: A) => Promise + : never; + }; + + readonly populateTransaction: { + [P in keyof V]: V[P] extends (...args: infer A) => unknown + ? (...args: A) => Promise + : never; + }; + readonly estimateAndPopulate: { [P in keyof V]: V[P] extends (...args: [...infer A, infer O | undefined]) => unknown ? EstimatedContractFunction diff --git a/packages/lib-ethers/test/Liquity.test.ts b/packages/lib-ethers/test/Liquity.test.ts index cd6412ff14..262fcbc601 100644 --- a/packages/lib-ethers/test/Liquity.test.ts +++ b/packages/lib-ethers/test/Liquity.test.ts @@ -175,8 +175,11 @@ describe("EthersLiquity", () => { ]; const borrowerOperations = { - estimateAndPopulate: { - openTrove: () => ({}) + estimateGas: { + openTrove: () => Promise.resolve(BigNumber.from(1)) + }, + populateTransaction: { + openTrove: () => Promise.resolve({}) } }; @@ -190,7 +193,11 @@ describe("EthersLiquity", () => { const fakeLiquity = new PopulatableEthersLiquity(({ getNumberOfTroves: () => Promise.resolve(1000000), - getFees: () => Promise.resolve(new Fees(0, 0.99, 1, new Date(), new Date(), false)), + getTotal: () => Promise.resolve(new Trove(Decimal.from(10), Decimal.ONE)), + getPrice: () => Promise.resolve(Decimal.ONE), + _getBlockTimestamp: () => Promise.resolve(0), + _getFeesFactory: () => + Promise.resolve(() => new Fees(0, 0.99, 1, new Date(), new Date(), false)), connection: { signerOrProvider: user, diff --git a/yarn.lock b/yarn.lock index c09d9d5f21..a4cacb6462 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4156,6 +4156,13 @@ "@types/webpack-sources" "*" source-map "^0.6.0" +"@types/ws@7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.4.tgz#93e1e00824c1de2608c30e6de4303ab3b4c0c9bc" + integrity sha512-d/7W23JAXPodQNbOZNXvl2K+bqAQrCMwlh/nuQsPSQk6Fq0opHoPrUw43aHsvSbIiQPr8Of2hkFbnz1XBFVyZQ== + dependencies: + "@types/node" "*" + "@types/yargs-parser@*": version "20.2.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.0.tgz#dd3e6699ba3237f0348cd085e4698780204842f9" @@ -21812,6 +21819,11 @@ ws@7.4.6: resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== +ws@7.4.6: + version "7.4.6" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" + integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== + ws@^3.0.0: version "3.3.3" resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2"