diff --git a/docs/sdk/lib-ethers.borrowingoperationoptionalparams.borrowingfeedecaytoleranceminutes.md b/docs/sdk/lib-ethers.borrowingoperationoptionalparams.borrowingfeedecaytoleranceminutes.md new file mode 100644 index 000000000..da35f6f1d --- /dev/null +++ b/docs/sdk/lib-ethers.borrowingoperationoptionalparams.borrowingfeedecaytoleranceminutes.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [@liquity/lib-ethers](./lib-ethers.md) > [BorrowingOperationOptionalParams](./lib-ethers.borrowingoperationoptionalparams.md) > [borrowingFeeDecayToleranceMinutes](./lib-ethers.borrowingoperationoptionalparams.borrowingfeedecaytoleranceminutes.md) + +## BorrowingOperationOptionalParams.borrowingFeeDecayToleranceMinutes property + +Control the amount of extra gas included attached to the transaction. + +Signature: + +```typescript +borrowingFeeDecayToleranceMinutes?: number; +``` + +## Remarks + +Transactions that borrow LUSD must pay a variable borrowing fee, which is added to the Trove's debt. This fee increases whenever a redemption occurs, and otherwise decays exponentially. Due to this decay, a Trove's collateral ratio can end up being higher than initially calculated if the transaction is pending for a long time. When this happens, the backend has to iterate over the sorted list of Troves to find a new position for the Trove, which costs extra gas. + +The SDK can estimate how much the gas costs of the transaction may increase due to this decay, and can include additional gas to ensure that it will still succeed, even if it ends up pending for a relatively long time. This parameter specifies the length of time that should be covered by the extra gas. + +Default: 60 minutes. + diff --git a/docs/sdk/lib-ethers.borrowingoperationoptionalparams.maxborrowingrate.md b/docs/sdk/lib-ethers.borrowingoperationoptionalparams.maxborrowingrate.md new file mode 100644 index 000000000..911318eba --- /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 000000000..16d4ae44a --- /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) Control the amount of extra gas included attached to the transaction. | +| [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 aa9182922..c3eae04b6 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: @@ -26,7 +26,7 @@ Promise<[TroveAdjustmentDetails](./lib-base.troveadjustmentdetails.md)> ## Exceptions -Throws [EthersTransactionFailedError](./lib-ethers.etherstransactionfailederror.md) in case of transaction failure. +Throws [EthersTransactionFailedError](./lib-ethers.etherstransactionfailederror.md) in case of transaction failure. Throws [EthersTransactionCancelledError](./lib-ethers.etherstransactioncancellederror.md) if the transaction is cancelled or replaced. diff --git a/docs/sdk/lib-ethers.ethersliquity.depositcollateral.md b/docs/sdk/lib-ethers.ethersliquity.depositcollateral.md index f3812131e..5a2485eb5 100644 --- a/docs/sdk/lib-ethers.ethersliquity.depositcollateral.md +++ b/docs/sdk/lib-ethers.ethersliquity.depositcollateral.md @@ -25,7 +25,7 @@ Promise<[TroveAdjustmentDetails](./lib-base.troveadjustmentdetails.md)> ## Exceptions -Throws [EthersTransactionFailedError](./lib-ethers.etherstransactionfailederror.md) in case of transaction failure. +Throws [EthersTransactionFailedError](./lib-ethers.etherstransactionfailederror.md) in case of transaction failure. Throws [EthersTransactionCancelledError](./lib-ethers.etherstransactioncancellederror.md) if the transaction is cancelled or replaced. diff --git a/docs/sdk/lib-ethers.ethersliquity.liquidateupto.md b/docs/sdk/lib-ethers.ethersliquity.liquidateupto.md index fa73e8288..2cdbc2059 100644 --- a/docs/sdk/lib-ethers.ethersliquity.liquidateupto.md +++ b/docs/sdk/lib-ethers.ethersliquity.liquidateupto.md @@ -25,5 +25,5 @@ Promise<[LiquidationDetails](./lib-base.liquidationdetails.md)> ## Exceptions -Throws [EthersTransactionFailedError](./lib-ethers.etherstransactionfailederror.md) in case of transaction failure. +Throws [EthersTransactionFailedError](./lib-ethers.etherstransactionfailederror.md) in case of transaction failure. Throws [EthersTransactionCancelledError](./lib-ethers.etherstransactioncancellederror.md) if the transaction is cancelled or replaced. diff --git a/docs/sdk/lib-ethers.ethersliquity.md b/docs/sdk/lib-ethers.ethersliquity.md index 12e57edcf..c8a45432b 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 d4de3c92f..d37bbc40a 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: @@ -26,7 +26,7 @@ Promise<[TroveCreationDetails](./lib-base.trovecreationdetails.md)> ## Exceptions -Throws [EthersTransactionFailedError](./lib-ethers.etherstransactionfailederror.md) in case of transaction failure. +Throws [EthersTransactionFailedError](./lib-ethers.etherstransactionfailederror.md) in case of transaction failure. Throws [EthersTransactionCancelledError](./lib-ethers.etherstransactioncancellederror.md) if the transaction is cancelled or replaced. ## Remarks diff --git a/docs/sdk/lib-ethers.ethersliquity.redeemlusd.md b/docs/sdk/lib-ethers.ethersliquity.redeemlusd.md index 6671bf7c9..22679df53 100644 --- a/docs/sdk/lib-ethers.ethersliquity.redeemlusd.md +++ b/docs/sdk/lib-ethers.ethersliquity.redeemlusd.md @@ -26,7 +26,7 @@ Promise<[RedemptionDetails](./lib-base.redemptiondetails.md)> ## Exceptions -Throws [EthersTransactionFailedError](./lib-ethers.etherstransactionfailederror.md) in case of transaction failure. +Throws [EthersTransactionFailedError](./lib-ethers.etherstransactionfailederror.md) in case of transaction failure. Throws [EthersTransactionCancelledError](./lib-ethers.etherstransactioncancellederror.md) if the transaction is cancelled or replaced. ## Remarks diff --git a/docs/sdk/lib-ethers.ethersliquity.registerfrontend.md b/docs/sdk/lib-ethers.ethersliquity.registerfrontend.md index 7b4b79e67..54c10b1da 100644 --- a/docs/sdk/lib-ethers.ethersliquity.registerfrontend.md +++ b/docs/sdk/lib-ethers.ethersliquity.registerfrontend.md @@ -25,5 +25,5 @@ Promise<void> ## Exceptions -Throws [EthersTransactionFailedError](./lib-ethers.etherstransactionfailederror.md) in case of transaction failure. +Throws [EthersTransactionFailedError](./lib-ethers.etherstransactionfailederror.md) in case of transaction failure. Throws [EthersTransactionCancelledError](./lib-ethers.etherstransactioncancellederror.md) if the transaction is cancelled or replaced. diff --git a/docs/sdk/lib-ethers.ethersliquity.repaylusd.md b/docs/sdk/lib-ethers.ethersliquity.repaylusd.md index 6f2cca0c7..9380c76a3 100644 --- a/docs/sdk/lib-ethers.ethersliquity.repaylusd.md +++ b/docs/sdk/lib-ethers.ethersliquity.repaylusd.md @@ -25,7 +25,7 @@ Promise<[TroveAdjustmentDetails](./lib-base.troveadjustmentdetails.md) + +[Home](./index.md) > [@liquity/lib-ethers](./lib-ethers.md) > [EthersLiquityConnection](./lib-ethers.ethersliquityconnection.md) > [startBlock](./lib-ethers.ethersliquityconnection.startblock.md) + +## EthersLiquityConnection.startBlock property + +Number of block in which the first Liquity contract was deployed. + +Signature: + +```typescript +readonly startBlock: number; +``` diff --git a/docs/sdk/lib-ethers.etherstransactioncancellederror.md b/docs/sdk/lib-ethers.etherstransactioncancellederror.md new file mode 100644 index 000000000..06b6add36 --- /dev/null +++ b/docs/sdk/lib-ethers.etherstransactioncancellederror.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [@liquity/lib-ethers](./lib-ethers.md) > [EthersTransactionCancelledError](./lib-ethers.etherstransactioncancellederror.md) + +## EthersTransactionCancelledError class + +Thrown when a transaction is cancelled or replaced by a different transaction. + +Signature: + +```typescript +export declare class EthersTransactionCancelledError extends Error +``` +Extends: Error + +## Remarks + +The constructor for this class is marked as internal. Third-party code should not call the constructor directly or create subclasses that extend the `EthersTransactionCancelledError` class. + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [rawError](./lib-ethers.etherstransactioncancellederror.rawerror.md) | | Error | | +| [rawReplacementReceipt](./lib-ethers.etherstransactioncancellederror.rawreplacementreceipt.md) | | [EthersTransactionReceipt](./lib-ethers.etherstransactionreceipt.md) | | + diff --git a/docs/sdk/lib-ethers.etherstransactioncancellederror.rawerror.md b/docs/sdk/lib-ethers.etherstransactioncancellederror.rawerror.md new file mode 100644 index 000000000..1e4c44805 --- /dev/null +++ b/docs/sdk/lib-ethers.etherstransactioncancellederror.rawerror.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [@liquity/lib-ethers](./lib-ethers.md) > [EthersTransactionCancelledError](./lib-ethers.etherstransactioncancellederror.md) > [rawError](./lib-ethers.etherstransactioncancellederror.rawerror.md) + +## EthersTransactionCancelledError.rawError property + +Signature: + +```typescript +readonly rawError: Error; +``` diff --git a/docs/sdk/lib-ethers.etherstransactioncancellederror.rawreplacementreceipt.md b/docs/sdk/lib-ethers.etherstransactioncancellederror.rawreplacementreceipt.md new file mode 100644 index 000000000..c151c7277 --- /dev/null +++ b/docs/sdk/lib-ethers.etherstransactioncancellederror.rawreplacementreceipt.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [@liquity/lib-ethers](./lib-ethers.md) > [EthersTransactionCancelledError](./lib-ethers.etherstransactioncancellederror.md) > [rawReplacementReceipt](./lib-ethers.etherstransactioncancellederror.rawreplacementreceipt.md) + +## EthersTransactionCancelledError.rawReplacementReceipt property + +Signature: + +```typescript +readonly rawReplacementReceipt: EthersTransactionReceipt; +``` diff --git a/docs/sdk/lib-ethers.md b/docs/sdk/lib-ethers.md index 0f90cf3cc..55ecab4ff 100644 --- a/docs/sdk/lib-ethers.md +++ b/docs/sdk/lib-ethers.md @@ -10,6 +10,7 @@ | --- | --- | | [BlockPolledLiquityStore](./lib-ethers.blockpolledliquitystore.md) | Ethers-based [LiquityStore](./lib-base.liquitystore.md) that updates state whenever there's a new block. | | [EthersLiquity](./lib-ethers.ethersliquity.md) | Convenience class that combines multiple interfaces of the library in one object. | +| [EthersTransactionCancelledError](./lib-ethers.etherstransactioncancellederror.md) | Thrown when a transaction is cancelled or replaced by a different transaction. | | [EthersTransactionFailedError](./lib-ethers.etherstransactionfailederror.md) | Thrown by [EthersLiquity](./lib-ethers.ethersliquity.md) in case of transaction failure. | | [PopulatableEthersLiquity](./lib-ethers.populatableethersliquity.md) | Ethers-based implementation of [PopulatableLiquity](./lib-base.populatableliquity.md). | | [PopulatedEthersLiquityTransaction](./lib-ethers.populatedethersliquitytransaction.md) | A transaction that has been prepared for sending. | @@ -24,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 0d385c1b5..4765a66d9 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 facbdd17c..f2b1c08f0 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 000000000..ec4ba0952 --- /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 0406599c2..0a8b6a264 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 68e6049c5..cb54ae15a 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 e73ca0010..4cbc0118e 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/docs/sdk/lib-ethers.sentethersliquitytransaction.waitforreceipt.md b/docs/sdk/lib-ethers.sentethersliquitytransaction.waitforreceipt.md index f62443785..8834e17d0 100644 --- a/docs/sdk/lib-ethers.sentethersliquitytransaction.waitforreceipt.md +++ b/docs/sdk/lib-ethers.sentethersliquitytransaction.waitforreceipt.md @@ -17,3 +17,7 @@ Promise<[MinedReceipt](./lib-base.minedreceipt.md)<[EthersTransact Either a [FailedReceipt](./lib-base.failedreceipt.md) or a [SuccessfulReceipt](./lib-base.successfulreceipt.md). +## Exceptions + +Throws [EthersTransactionCancelledError](./lib-ethers.etherstransactioncancellederror.md) if the transaction is cancelled or replaced. + diff --git a/packages/contracts/.gitignore b/packages/contracts/.gitignore index 3ce651b0f..8529439c1 100644 --- a/packages/contracts/.gitignore +++ b/packages/contracts/.gitignore @@ -4,6 +4,8 @@ # Truffle contract JSONs /build/contracts +/bin + # Buidler compilation cache /cache @@ -15,8 +17,9 @@ /secrets.js -/.vscode +.vscode +yarn-error.log # solidity-coverage /.coverage_artifacts @@ -34,6 +37,7 @@ # brownie __pycache__ +.pytest_cache .history .hypothesis/ build/ diff --git a/packages/contracts/.npmrc b/packages/contracts/.npmrc new file mode 100644 index 000000000..94a06c218 --- /dev/null +++ b/packages/contracts/.npmrc @@ -0,0 +1 @@ +access=public diff --git a/packages/contracts/brownie-config.yaml b/packages/contracts/brownie-config.yaml index 8a118381b..7ddfef7d6 100644 --- a/packages/contracts/brownie-config.yaml +++ b/packages/contracts/brownie-config.yaml @@ -7,7 +7,7 @@ networks: default_contract_owner: true cmd_settings: port: 8545 - gas_limit: 6721975 + gas_limit: 8000000 accounts: 1000 chain_id: 1337 network_id: 1588949648 diff --git a/packages/contracts/contracts/TestContracts/PriceFeedTestnet.sol b/packages/contracts/contracts/TestContracts/PriceFeedTestnet.sol index 0cd8f477a..ffdb76d9f 100644 --- a/packages/contracts/contracts/TestContracts/PriceFeedTestnet.sol +++ b/packages/contracts/contracts/TestContracts/PriceFeedTestnet.sol @@ -20,6 +20,9 @@ contract PriceFeedTestnet is IPriceFeed { } function fetchPrice() external override returns (uint256) { + // Fire an event just like the mainnet version would. + // This lets the subgraph rely on events to get the latest price even when developing locally. + emit LastGoodPriceUpdated(_price); return _price; } diff --git a/packages/contracts/hardhat.config.js b/packages/contracts/hardhat.config.js index a8a748055..8f31df2f2 100644 --- a/packages/contracts/hardhat.config.js +++ b/packages/contracts/hardhat.config.js @@ -1,6 +1,6 @@ require("@nomiclabs/hardhat-truffle5"); require("@nomiclabs/hardhat-ethers"); -require("@nomiclabs/hardhat-etherscan"); +// require("@nomiclabs/hardhat-etherscan"); require("solidity-coverage"); require("hardhat-gas-reporter"); diff --git a/packages/contracts/hardhat.config.mainnet-fork.js b/packages/contracts/hardhat.config.mainnet-fork.js index 606618433..48c3ec6be 100644 --- a/packages/contracts/hardhat.config.mainnet-fork.js +++ b/packages/contracts/hardhat.config.mainnet-fork.js @@ -58,10 +58,10 @@ module.exports = { accounts: accountsList, gas: 10000000, // tx gas limit blockGasLimit: 12500000, - gasPrice: 20000000000, + gasPrice: process.env.GAS_PRICE ? parseInt(process.env.GAS_PRICE) : 20000000000, forking: { url: alchemyUrl(), - blockNumber: 12152522 + blockNumber: process.env.BLOCK_NUMBER ? parseInt(process.env.BLOCK_NUMBER) : 12152522 } } }, diff --git a/packages/contracts/mainnetDeployment/ABIs/ERC20.js b/packages/contracts/mainnetDeployment/ABIs/ERC20.js index 3ade5b3d5..4c605dc5d 100644 --- a/packages/contracts/mainnetDeployment/ABIs/ERC20.js +++ b/packages/contracts/mainnetDeployment/ABIs/ERC20.js @@ -1,226 +1,228 @@ -const ERC20Abi = [ +const ERC20 = { + "abi": [ { - "constant": true, - "inputs": [], - "name": "name", - "outputs": [ - { - "name": "", - "type": "string" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" + "constant": true, + "inputs": [], + "name": "name", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" }, { - "constant": false, - "inputs": [ - { - "name": "_spender", - "type": "address" - }, - { - "name": "_value", - "type": "uint256" - } - ], - "name": "approve", - "outputs": [ - { - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" + "constant": false, + "inputs": [ + { + "name": "_spender", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" }, { - "constant": true, - "inputs": [], - "name": "totalSupply", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" }, { - "constant": false, - "inputs": [ - { - "name": "_from", - "type": "address" - }, - { - "name": "_to", - "type": "address" - }, - { - "name": "_value", - "type": "uint256" - } - ], - "name": "transferFrom", - "outputs": [ - { - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" + "constant": false, + "inputs": [ + { + "name": "_from", + "type": "address" + }, + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" }, { - "constant": true, - "inputs": [], - "name": "decimals", - "outputs": [ - { - "name": "", - "type": "uint8" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [ + { + "name": "", + "type": "uint8" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" }, { - "constant": true, - "inputs": [ - { - "name": "_owner", - "type": "address" - } - ], - "name": "balanceOf", - "outputs": [ - { - "name": "balance", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" + "constant": true, + "inputs": [ + { + "name": "_owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "name": "balance", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" }, { - "constant": true, - "inputs": [], - "name": "symbol", - "outputs": [ - { - "name": "", - "type": "string" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" }, { - "constant": false, - "inputs": [ - { - "name": "_to", - "type": "address" - }, - { - "name": "_value", - "type": "uint256" - } - ], - "name": "transfer", - "outputs": [ - { - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" + "constant": false, + "inputs": [ + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" }, { - "constant": true, - "inputs": [ - { - "name": "_owner", - "type": "address" - }, - { - "name": "_spender", - "type": "address" - } - ], - "name": "allowance", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" + "constant": true, + "inputs": [ + { + "name": "_owner", + "type": "address" + }, + { + "name": "_spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" }, { - "payable": true, - "stateMutability": "payable", - "type": "fallback" + "payable": true, + "stateMutability": "payable", + "type": "fallback" }, { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "name": "owner", - "type": "address" - }, - { - "indexed": true, - "name": "spender", - "type": "address" - }, - { - "indexed": false, - "name": "value", - "type": "uint256" - } - ], - "name": "Approval", - "type": "event" + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" }, { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "name": "from", - "type": "address" - }, - { - "indexed": true, - "name": "to", - "type": "address" - }, - { - "indexed": false, - "name": "value", - "type": "uint256" - } - ], - "name": "Transfer", - "type": "event" + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "from", + "type": "address" + }, + { + "indexed": true, + "name": "to", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" } -] + ] +} module.exports = { - ERC20Abi: ERC20Abi -} \ No newline at end of file + ERC20: ERC20 +} diff --git a/packages/contracts/mainnetDeployment/ABIs/IVault.js b/packages/contracts/mainnetDeployment/ABIs/IVault.js new file mode 100644 index 000000000..4425f61c0 --- /dev/null +++ b/packages/contracts/mainnetDeployment/ABIs/IVault.js @@ -0,0 +1,1136 @@ +const IVault = { + "abi": [ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract IAuthorizer", + "name": "newAuthorizer", + "type": "address" + } + ], + "name": "AuthorizerChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "ExternalBalanceTransfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract IFlashLoanRecipient", + "name": "recipient", + "type": "address" + }, + { + "indexed": true, + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "feeAmount", + "type": "uint256" + } + ], + "name": "FlashLoan", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": true, + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "int256", + "name": "delta", + "type": "int256" + } + ], + "name": "InternalBalanceChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bool", + "name": "paused", + "type": "bool" + } + ], + "name": "PausedStateChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "poolId", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "liquidityProvider", + "type": "address" + }, + { + "indexed": false, + "internalType": "contract IERC20[]", + "name": "tokens", + "type": "address[]" + }, + { + "indexed": false, + "internalType": "int256[]", + "name": "deltas", + "type": "int256[]" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "protocolFeeAmounts", + "type": "uint256[]" + } + ], + "name": "PoolBalanceChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "poolId", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "assetManager", + "type": "address" + }, + { + "indexed": true, + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "int256", + "name": "cashDelta", + "type": "int256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "managedDelta", + "type": "int256" + } + ], + "name": "PoolBalanceManaged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "poolId", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "poolAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "enum IVault.PoolSpecialization", + "name": "specialization", + "type": "uint8" + } + ], + "name": "PoolRegistered", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "relayer", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "RelayerApprovalChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "poolId", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "contract IERC20", + "name": "tokenIn", + "type": "address" + }, + { + "indexed": true, + "internalType": "contract IERC20", + "name": "tokenOut", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + } + ], + "name": "Swap", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "poolId", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "contract IERC20[]", + "name": "tokens", + "type": "address[]" + } + ], + "name": "TokensDeregistered", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "poolId", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "contract IERC20[]", + "name": "tokens", + "type": "address[]" + }, + { + "indexed": false, + "internalType": "address[]", + "name": "assetManagers", + "type": "address[]" + } + ], + "name": "TokensRegistered", + "type": "event" + }, + { + "inputs": [], + "name": "WETH", + "outputs": [ + { + "internalType": "contract IWETH", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "enum IVault.SwapKind", + "name": "kind", + "type": "uint8" + }, + { + "components": [ + { + "internalType": "bytes32", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "assetInIndex", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "assetOutIndex", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "internalType": "struct IVault.BatchSwapStep[]", + "name": "swaps", + "type": "tuple[]" + }, + { + "internalType": "contract IAsset[]", + "name": "assets", + "type": "address[]" + }, + { + "components": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "bool", + "name": "fromInternalBalance", + "type": "bool" + }, + { + "internalType": "address payable", + "name": "recipient", + "type": "address" + }, + { + "internalType": "bool", + "name": "toInternalBalance", + "type": "bool" + } + ], + "internalType": "struct IVault.FundManagement", + "name": "funds", + "type": "tuple" + }, + { + "internalType": "int256[]", + "name": "limits", + "type": "int256[]" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "batchSwap", + "outputs": [ + { + "internalType": "int256[]", + "name": "", + "type": "int256[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "contract IERC20[]", + "name": "tokens", + "type": "address[]" + } + ], + "name": "deregisterTokens", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address payable", + "name": "recipient", + "type": "address" + }, + { + "components": [ + { + "internalType": "contract IAsset[]", + "name": "assets", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "minAmountsOut", + "type": "uint256[]" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + }, + { + "internalType": "bool", + "name": "toInternalBalance", + "type": "bool" + } + ], + "internalType": "struct IVault.ExitPoolRequest", + "name": "request", + "type": "tuple" + } + ], + "name": "exitPool", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IFlashLoanRecipient", + "name": "recipient", + "type": "address" + }, + { + "internalType": "contract IERC20[]", + "name": "tokens", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "name": "flashLoan", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "getAuthorizer", + "outputs": [ + { + "internalType": "contract IAuthorizer", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getDomainSeparator", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "internalType": "contract IERC20[]", + "name": "tokens", + "type": "address[]" + } + ], + "name": "getInternalBalance", + "outputs": [ + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "getNextNonce", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getPausedState", + "outputs": [ + { + "internalType": "bool", + "name": "paused", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "pauseWindowEndTime", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "bufferPeriodEndTime", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "poolId", + "type": "bytes32" + } + ], + "name": "getPool", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "enum IVault.PoolSpecialization", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + } + ], + "name": "getPoolTokenInfo", + "outputs": [ + { + "internalType": "uint256", + "name": "cash", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "managed", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "lastChangeBlock", + "type": "uint256" + }, + { + "internalType": "address", + "name": "assetManager", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "poolId", + "type": "bytes32" + } + ], + "name": "getPoolTokens", + "outputs": [ + { + "internalType": "contract IERC20[]", + "name": "tokens", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "balances", + "type": "uint256[]" + }, + { + "internalType": "uint256", + "name": "lastChangeBlock", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getProtocolFeesCollector", + "outputs": [ + { + "internalType": "contract IProtocolFeesCollector", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "internalType": "address", + "name": "relayer", + "type": "address" + } + ], + "name": "hasApprovedRelayer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "components": [ + { + "internalType": "contract IAsset[]", + "name": "assets", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "maxAmountsIn", + "type": "uint256[]" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + }, + { + "internalType": "bool", + "name": "fromInternalBalance", + "type": "bool" + } + ], + "internalType": "struct IVault.JoinPoolRequest", + "name": "request", + "type": "tuple" + } + ], + "name": "joinPool", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "enum IVault.PoolBalanceOpKind", + "name": "kind", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "internalType": "struct IVault.PoolBalanceOp[]", + "name": "ops", + "type": "tuple[]" + } + ], + "name": "managePoolBalance", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "enum IVault.UserBalanceOpKind", + "name": "kind", + "type": "uint8" + }, + { + "internalType": "contract IAsset", + "name": "asset", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address payable", + "name": "recipient", + "type": "address" + } + ], + "internalType": "struct IVault.UserBalanceOp[]", + "name": "ops", + "type": "tuple[]" + } + ], + "name": "manageUserBalance", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "enum IVault.SwapKind", + "name": "kind", + "type": "uint8" + }, + { + "components": [ + { + "internalType": "bytes32", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "assetInIndex", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "assetOutIndex", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "internalType": "struct IVault.BatchSwapStep[]", + "name": "swaps", + "type": "tuple[]" + }, + { + "internalType": "contract IAsset[]", + "name": "assets", + "type": "address[]" + }, + { + "components": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "bool", + "name": "fromInternalBalance", + "type": "bool" + }, + { + "internalType": "address payable", + "name": "recipient", + "type": "address" + }, + { + "internalType": "bool", + "name": "toInternalBalance", + "type": "bool" + } + ], + "internalType": "struct IVault.FundManagement", + "name": "funds", + "type": "tuple" + } + ], + "name": "queryBatchSwap", + "outputs": [ + { + "internalType": "int256[]", + "name": "assetDeltas", + "type": "int256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "enum IVault.PoolSpecialization", + "name": "specialization", + "type": "uint8" + } + ], + "name": "registerPool", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "contract IERC20[]", + "name": "tokens", + "type": "address[]" + }, + { + "internalType": "address[]", + "name": "assetManagers", + "type": "address[]" + } + ], + "name": "registerTokens", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IAuthorizer", + "name": "newAuthorizer", + "type": "address" + } + ], + "name": "setAuthorizer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "paused", + "type": "bool" + } + ], + "name": "setPaused", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "relayer", + "type": "address" + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "setRelayerApproval", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "bytes32", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "enum IVault.SwapKind", + "name": "kind", + "type": "uint8" + }, + { + "internalType": "contract IAsset", + "name": "assetIn", + "type": "address" + }, + { + "internalType": "contract IAsset", + "name": "assetOut", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "internalType": "struct IVault.SingleSwap", + "name": "singleSwap", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "bool", + "name": "fromInternalBalance", + "type": "bool" + }, + { + "internalType": "address payable", + "name": "recipient", + "type": "address" + }, + { + "internalType": "bool", + "name": "toInternalBalance", + "type": "bool" + } + ], + "internalType": "struct IVault.FundManagement", + "name": "funds", + "type": "tuple" + }, + { + "internalType": "uint256", + "name": "limit", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swap", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + } + ] +} + +module.exports = { + IVault: IVault +} diff --git a/packages/contracts/mainnetDeployment/ABIs/WETH.js b/packages/contracts/mainnetDeployment/ABIs/WETH.js new file mode 100644 index 000000000..d609e4722 --- /dev/null +++ b/packages/contracts/mainnetDeployment/ABIs/WETH.js @@ -0,0 +1,8 @@ +// https://etherscan.io/address/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2#code +const WETH = { + "abi": [{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"guy","type":"address"},{"name":"wad","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"src","type":"address"},{"name":"dst","type":"address"},{"name":"wad","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"wad","type":"uint256"}],"name":"withdraw","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"dst","type":"address"},{"name":"wad","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"deposit","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"},{"name":"","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"payable":true,"stateMutability":"payable","type":"fallback"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"address"},{"indexed":true,"name":"guy","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"address"},{"indexed":true,"name":"dst","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"dst","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Deposit","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Withdrawal","type":"event"}] +} + +module.exports = { + WETH: WETH +} diff --git a/packages/contracts/mainnetDeployment/ABIs/WeightedPool2Tokens.js b/packages/contracts/mainnetDeployment/ABIs/WeightedPool2Tokens.js new file mode 100644 index 000000000..f662270b9 --- /dev/null +++ b/packages/contracts/mainnetDeployment/ABIs/WeightedPool2Tokens.js @@ -0,0 +1,1148 @@ +const WeightedPool2Tokens = { + "abi": [ + { + "inputs": [ + { + "components": [ + { + "internalType": "contract IVault", + "name": "vault", + "type": "address" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "symbol", + "type": "string" + }, + { + "internalType": "contract IERC20", + "name": "token0", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "token1", + "type": "address" + }, + { + "internalType": "uint256", + "name": "normalizedWeight0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "normalizedWeight1", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "swapFeePercentage", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "pauseWindowDuration", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "bufferPeriodDuration", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "oracleEnabled", + "type": "bool" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "internalType": "struct WeightedPool2Tokens.NewPoolParams", + "name": "params", + "type": "tuple" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bool", + "name": "enabled", + "type": "bool" + } + ], + "name": "OracleEnabledChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bool", + "name": "paused", + "type": "bool" + } + ], + "name": "PausedStateChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "swapFeePercentage", + "type": "uint256" + } + ], + "name": "SwapFeePercentageChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [], + "name": "DOMAIN_SEPARATOR", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "decreaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "enableOracle", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "selector", + "type": "bytes4" + } + ], + "name": "getActionId", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getAuthorizer", + "outputs": [ + { + "internalType": "contract IAuthorizer", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getInvariant", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getLargestSafeQueryWindow", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "getLastInvariant", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "enum IPriceOracle.Variable", + "name": "variable", + "type": "uint8" + } + ], + "name": "getLatest", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getMiscData", + "outputs": [ + { + "internalType": "int256", + "name": "logInvariant", + "type": "int256" + }, + { + "internalType": "int256", + "name": "logTotalSupply", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "oracleSampleCreationTimestamp", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "oracleIndex", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "oracleEnabled", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "swapFeePercentage", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getNormalizedWeights", + "outputs": [ + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "enum IPriceOracle.Variable", + "name": "variable", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "ago", + "type": "uint256" + } + ], + "internalType": "struct IPriceOracle.OracleAccumulatorQuery[]", + "name": "queries", + "type": "tuple[]" + } + ], + "name": "getPastAccumulators", + "outputs": [ + { + "internalType": "int256[]", + "name": "results", + "type": "int256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getPausedState", + "outputs": [ + { + "internalType": "bool", + "name": "paused", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "pauseWindowEndTime", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "bufferPeriodEndTime", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getPoolId", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getRate", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "getSample", + "outputs": [ + { + "internalType": "int256", + "name": "logPairPrice", + "type": "int256" + }, + { + "internalType": "int256", + "name": "accLogPairPrice", + "type": "int256" + }, + { + "internalType": "int256", + "name": "logBptPrice", + "type": "int256" + }, + { + "internalType": "int256", + "name": "accLogBptPrice", + "type": "int256" + }, + { + "internalType": "int256", + "name": "logInvariant", + "type": "int256" + }, + { + "internalType": "int256", + "name": "accLogInvariant", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getSwapFeePercentage", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "enum IPriceOracle.Variable", + "name": "variable", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "secs", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "ago", + "type": "uint256" + } + ], + "internalType": "struct IPriceOracle.OracleAverageQuery[]", + "name": "queries", + "type": "tuple[]" + } + ], + "name": "getTimeWeightedAverage", + "outputs": [ + { + "internalType": "uint256[]", + "name": "results", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getTotalSamples", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "getVault", + "outputs": [ + { + "internalType": "contract IVault", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "addedValue", + "type": "uint256" + } + ], + "name": "increaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "nonces", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "balances", + "type": "uint256[]" + }, + { + "internalType": "uint256", + "name": "lastChangeBlock", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "protocolSwapFeePercentage", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "name": "onExitPool", + "outputs": [ + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "balances", + "type": "uint256[]" + }, + { + "internalType": "uint256", + "name": "lastChangeBlock", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "protocolSwapFeePercentage", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "name": "onJoinPool", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amountsIn", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "dueProtocolFeeAmounts", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "enum IVault.SwapKind", + "name": "kind", + "type": "uint8" + }, + { + "internalType": "contract IERC20", + "name": "tokenIn", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "tokenOut", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "lastChangeBlock", + "type": "uint256" + }, + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "internalType": "struct IPoolSwapStructs.SwapRequest", + "name": "request", + "type": "tuple" + }, + { + "internalType": "uint256", + "name": "balanceTokenIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "balanceTokenOut", + "type": "uint256" + } + ], + "name": "onSwap", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "permit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "balances", + "type": "uint256[]" + }, + { + "internalType": "uint256", + "name": "lastChangeBlock", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "protocolSwapFeePercentage", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "name": "queryExit", + "outputs": [ + { + "internalType": "uint256", + "name": "bptIn", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "amountsOut", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "balances", + "type": "uint256[]" + }, + { + "internalType": "uint256", + "name": "lastChangeBlock", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "protocolSwapFeePercentage", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "name": "queryJoin", + "outputs": [ + { + "internalType": "uint256", + "name": "bptOut", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "amountsIn", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "paused", + "type": "bool" + } + ], + "name": "setPaused", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "swapFeePercentage", + "type": "uint256" + } + ], + "name": "setSwapFeePercentage", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } + ] +} + +module.exports = { + WeightedPool2Tokens: WeightedPool2Tokens +} diff --git a/packages/contracts/mainnetDeployment/ABIs/WeightedPool2TokensFactory.js b/packages/contracts/mainnetDeployment/ABIs/WeightedPool2TokensFactory.js new file mode 100644 index 000000000..eee12af5f --- /dev/null +++ b/packages/contracts/mainnetDeployment/ABIs/WeightedPool2TokensFactory.js @@ -0,0 +1,131 @@ +const WeightedPool2TokensFactory = { + "abi": [ + { + "inputs": [ + { + "internalType": "contract IVault", + "name": "vault", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "name": "PoolCreated", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "symbol", + "type": "string" + }, + { + "internalType": "contract IERC20[]", + "name": "tokens", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "weights", + "type": "uint256[]" + }, + { + "internalType": "uint256", + "name": "swapFeePercentage", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "oracleEnabled", + "type": "bool" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "create", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "getPauseConfiguration", + "outputs": [ + { + "internalType": "uint256", + "name": "pauseWindowDuration", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "bufferPeriodDuration", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getVault", + "outputs": [ + { + "internalType": "contract IVault", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "name": "isPoolFromFactory", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + } + ] +} + +module.exports = { + WeightedPool2TokensFactory: WeightedPool2TokensFactory +} diff --git a/packages/contracts/mainnetDeployment/balancerPoolDeployment.js b/packages/contracts/mainnetDeployment/balancerPoolDeployment.js new file mode 100644 index 000000000..7e864020e --- /dev/null +++ b/packages/contracts/mainnetDeployment/balancerPoolDeployment.js @@ -0,0 +1,163 @@ +// Test with: +// GAS_PRICE=0 BLOCK_NUMBER=12612689 npx hardhat run mainnetDeployment/balancerPoolDeployment.js --config hardhat.config.mainnet-fork.js + +const { WeightedPool2TokensFactory } = require("./ABIs/WeightedPool2TokensFactory.js") +const { WeightedPool2Tokens } = require("./ABIs/WeightedPool2Tokens.js") +const { IVault } = require("./ABIs/IVault.js") +const { ERC20 } = require("./ABIs/ERC20.js") +const { WETH: WETH_ABI } = require("./ABIs/WETH.js") +const { ChainlinkAggregatorV3Interface } = require("./ABIs/ChainlinkAggregatorV3Interface.js") +const toBigNum = ethers.BigNumber.from +const { TestHelper: th, TimeValues: timeVals } = require("../utils/testHelpers.js") +const { dec } = th +// Addresses are the same on all networks + +const VAULT = '0xBA12222222228d8Ba445958a75a0704d566BF2C8'; + +const WEIGHTED_POOL_FACTORY = '0x8E9aa87E45e92bad84D5F8DD1bff34Fb92637dE9'; +const ORACLE_POOL_FACTORY = '0xA5bf2ddF098bb0Ef6d120C98217dD6B141c74EE0'; + +const DELEGATE_OWNER = '0xBA1BA1ba1BA1bA1bA1Ba1BA1ba1BA1bA1ba1ba1B'; + +// Mainnet addresses; adjust for testnets + +const WETH = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; +const LUSD = '0x5f98805A4E8be255a32880FDeC7F6728C6568bA0'; +const CHAINLINK_ETHUSD_PROXY = '0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419'; + +const tokens = [LUSD, WETH]; +const weights = [toBigNum(dec(4, 17)), toBigNum(dec(6, 17))]; + +const NAME = 'WETH/LUSD Pool'; +const SYMBOL = '60WETH-40LUSD'; +const swapFeePercentage = toBigNum(dec(5, 15)); // 0.5% + +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; + +const INITIAL_FUNDING = toBigNum(dec(5, 22)); // $50k + +async function main() { + // Uncomment for testing: + /* + const impersonateAddress = "0x787EfF01c9FdC1918d1AA6eeFf089B191e2922E4" + await hre.network.provider.request({ + method: "hardhat_impersonateAccount", + params: [ impersonateAddress ] + }) + const deployerWallet = await ethers.provider.getSigner(impersonateAddress) + const deployerWalletAddress = impersonateAddress + */ + + const deployerWallet = (await ethers.getSigners())[0] + const deployerWalletAddress = deployerWallet.address + + const factory = new ethers.Contract( + ORACLE_POOL_FACTORY, + WeightedPool2TokensFactory.abi, + deployerWallet + ); + const vault = new ethers.Contract( + VAULT, + IVault.abi, + deployerWallet + ); + + const chainlinkProxy = new ethers.Contract( + CHAINLINK_ETHUSD_PROXY, + ChainlinkAggregatorV3Interface, + deployerWallet + ) + + // ZERO_ADDRESS owner means fixed swap fees + // DELEGATE_OWNER grants permission to governance for dynamic fee management + // Any other address lets that address directly set the fees + const oracleEnabled = true; + const tx1 = await factory.create( + NAME, SYMBOL, tokens, weights, + swapFeePercentage, oracleEnabled, + DELEGATE_OWNER + ); + const receipt1 = await tx1.wait(); + + // We need to get the new pool address out of the PoolCreated event + // (Or just grab it from Etherscan) + const events = receipt1.events.filter((e) => e.event === 'PoolCreated'); + const poolAddress = events[0].args.pool; + + // We're going to need the PoolId later, so ask the contract for it + const pool = new ethers.Contract( + poolAddress, + WeightedPool2Tokens.abi, + deployerWallet + ); + const poolId = await pool.getPoolId(); + + // Get latest price + const chainlinkPrice = await chainlinkProxy.latestAnswer(); + // chainlink price has only 8 decimals + const eth_price = chainlinkPrice.mul(toBigNum(dec(1, 10))); + th.logBN('ETH price', eth_price) + // Tokens must be in the same order + // Values must be decimal-normalized! + const weth_balance = INITIAL_FUNDING.mul(weights[1]).div(eth_price); + const lusd_balance = INITIAL_FUNDING.mul(weights[0]).div(toBigNum(dec(1, 18))); + const initialBalances = [lusd_balance, weth_balance]; + th.logBN('Initial LUSD', lusd_balance) + th.logBN('Initial WETH', weth_balance) + + const JOIN_KIND_INIT = 0; + + // Construct magic userData + const initUserData = + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint256[]'], + [JOIN_KIND_INIT, initialBalances]); + const joinPoolRequest = { + assets: tokens, + maxAmountsIn: initialBalances, + userData: initUserData, + fromInternalBalance: false + } + + // Caller is "you". joinPool takes a sender (source of initialBalances) + // And a receiver (where BPT are sent). Normally, both are the caller. + // If you have a User Balance of any of these tokens, you can set + // fromInternalBalance to true, and fund a pool with no token transfers + // (well, except for the BPT out) + + // Need to approve the Vault to transfer the tokens! + const weth = new ethers.Contract( + WETH, + WETH_ABI.abi, + deployerWallet + ) + th.logBN('weth balance: ', await weth.balanceOf(deployerWalletAddress)) + const currentWethBalance = await weth.balanceOf(deployerWalletAddress) + if (currentWethBalance.lt(weth_balance)) { + const txDepositWeth = await weth.deposit({ value: weth_balance.sub(currentWethBalance) }); + await txDepositWeth.wait() + } + th.logBN('weth balance: ', await weth.balanceOf(deployerWalletAddress)) + const txApproveWeth = await weth.approve(VAULT, weth_balance); + await txApproveWeth.wait() + const lusd = new ethers.Contract( + LUSD, + ERC20.abi, + deployerWallet + ) + const txApproveLusd = await lusd.approve(VAULT, lusd_balance); + await txApproveLusd.wait() + + // joins and exits are done on the Vault, not the pool + const tx2 = await vault.joinPool(poolId, deployerWalletAddress, deployerWalletAddress, joinPoolRequest); + // You can wait for it like this, or just print the tx hash and monitor + const receipt2 = await tx2.wait(); + console.log('Final tx status:', receipt2.status) + th.logBN('Pool BPT tokens', await pool.totalSupply()) +} + +main() + .then(() => process.exit(0)) + .catch(error => { + console.error(error); + process.exit(1); + }); diff --git a/packages/contracts/mainnetDeployment/deploymentParams.localFork.js b/packages/contracts/mainnetDeployment/deploymentParams.localFork.js index 352e00e49..f4f17d0c0 100644 --- a/packages/contracts/mainnetDeployment/deploymentParams.localFork.js +++ b/packages/contracts/mainnetDeployment/deploymentParams.localFork.js @@ -5,7 +5,7 @@ const externalAddrs = { TELLOR_MASTER:"0x88dF592F8eb5D7Bd38bFeF7dEb0fBc02cf3778a0", // https://uniswap.org/docs/v2/smart-contracts/factory/ UNISWAP_V2_FACTORY: "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f", - UNIWAP_V2_ROUTER02: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + UNISWAP_V2_ROUTER02: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", // https://etherscan.io/token/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 WETH_ERC20: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", } diff --git a/packages/contracts/mainnetDeployment/deploymentParams.mainnet.js b/packages/contracts/mainnetDeployment/deploymentParams.mainnet.js index b291fc8ad..9653a2506 100644 --- a/packages/contracts/mainnetDeployment/deploymentParams.mainnet.js +++ b/packages/contracts/mainnetDeployment/deploymentParams.mainnet.js @@ -5,7 +5,7 @@ const externalAddrs = { TELLOR_MASTER:"0x88dF592F8eb5D7Bd38bFeF7dEb0fBc02cf3778a0", // https://uniswap.org/docs/v2/smart-contracts/factory/ UNISWAP_V2_FACTORY: "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f", - UNIWAP_V2_ROUTER02: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + UNISWAP_V2_ROUTER02: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", // https://etherscan.io/token/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 WETH_ERC20: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", } diff --git a/packages/contracts/mainnetDeployment/deploymentParams.rinkeby.js b/packages/contracts/mainnetDeployment/deploymentParams.rinkeby.js index 30ec26d8b..90c9dd258 100644 --- a/packages/contracts/mainnetDeployment/deploymentParams.rinkeby.js +++ b/packages/contracts/mainnetDeployment/deploymentParams.rinkeby.js @@ -5,7 +5,7 @@ const externalAddrs = { TELLOR_MASTER:"0x20374E579832859f180536A69093A126Db1c8aE9", // https://uniswap.org/docs/v2/smart-contracts/factory/ UNISWAP_V2_FACTORY: "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f", - UNIWAP_V2_ROUTER02: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + UNISWAP_V2_ROUTER02: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", WETH_ERC20: "0xc778417e063141139fce010982780140aa0cd5ab", } diff --git a/packages/contracts/mainnetDeployment/mainnetDeployment.js b/packages/contracts/mainnetDeployment/mainnetDeployment.js index 84a30fe87..f2a676f4e 100644 --- a/packages/contracts/mainnetDeployment/mainnetDeployment.js +++ b/packages/contracts/mainnetDeployment/mainnetDeployment.js @@ -23,7 +23,7 @@ async function mainnetDeploy(configParams) { let deployerETHBalance = await ethers.provider.getBalance(deployerWallet.address) console.log(`deployerETHBalance before: ${deployerETHBalance}`) - // Get UniswaV2Factory instance at its deployed address + // Get UniswapV2Factory instance at its deployed address const uniswapV2Factory = new ethers.Contract( configParams.externalAddrs.UNISWAP_V2_FACTORY, UniswapV2Factory.abi, @@ -312,7 +312,7 @@ async function mainnetDeploy(configParams) { // // Get the UniswapV2Router contract // const uniswapV2Router02 = new ethers.Contract( - // configParams.externalAddrs.UNIWAP_V2_ROUTER02, + // configParams.externalAddrs.UNISWAP_V2_ROUTER02, // UniswapV2Router02.abi, // deployerWallet // ) diff --git a/packages/contracts/package.json b/packages/contracts/package.json index dc46554b7..af59bc71f 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -1,7 +1,6 @@ { "name": "@liquity/contracts", "version": "1.0.0", - "private": true, "description": "", "main": "truffle-config.js", "directories": { diff --git a/packages/contracts/tests/simulation_test.py b/packages/contracts/tests/simulation_test.py index 7047c2dee..958bcf811 100644 --- a/packages/contracts/tests/simulation_test.py +++ b/packages/contracts/tests/simulation_test.py @@ -137,6 +137,7 @@ def contracts(): contracts.lockupContractFactory.address, accounts[0], # bountyAddress accounts[0], # lpRewardsAddress + accounts[0], # multisigAddress { 'from': accounts[0] } ) diff --git a/packages/contracts/utils/hintExamples.js b/packages/contracts/utils/hintExamples.js new file mode 100644 index 000000000..7bcfbbe7f --- /dev/null +++ b/packages/contracts/utils/hintExamples.js @@ -0,0 +1,125 @@ + +const { TestHelper: th } = require("../utils/testHelpers.js") +const dh = require("./deploymentHelpers.js") + +// const [borrower, A, B, C] = (() => Array.from(Array(4), x => web3.eth.accounts.create().address))() + +async function main() { + const accounts = await web3.eth.getAccounts() + const [borrower, A, B] = accounts + + const coreContracts = await dh.deployLiquityCoreHardhat() + const ARBITRARY_ADDRESS = "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419" + const LQTYContracts = await dh.deployLQTYContractsHardhat( + ARBITRARY_ADDRESS, + ARBITRARY_ADDRESS, + ARBITRARY_ADDRESS + ) + + const { troveManager, borrowerOperations, hintHelpers, sortedTroves, priceFeedTestnet } = coreContracts + + await dh.connectCoreContracts(coreContracts, LQTYContracts) + await dh.connectLQTYContracts(LQTYContracts) + await dh.connectLQTYContractsToCore(LQTYContracts, coreContracts) + + // Examples of off-chain hint calculation for Open Trove + + const toWei = web3.utils.toWei + const toBN = web3.utils.toBN + + const price = toBN(toWei('2500')) + await priceFeedTestnet.setPrice(toBN(toWei('2500'))) + + const LUSDAmount = toBN(toWei('2500')) // borrower wants to withdraw 2500 LUSD + const ETHColl = toBN(toWei('5')) // borrower wants to lock 5 ETH collateral + + // Call deployed TroveManager contract to read the liquidation reserve and latest borrowing fee + const liquidationReserve = await troveManager.LUSD_GAS_COMPENSATION() + const expectedFee = await troveManager.getBorrowingFeeWithDecay(LUSDAmount) + + // Total debt of the new trove = LUSD amount drawn, plus fee, plus the liquidation reserve + const expectedDebt = LUSDAmount.add(expectedFee).add(liquidationReserve) + + // Get the nominal NICR of the new trove + const _1e20 = toBN(toWei('100')) + let NICR = ETHColl.mul(_1e20).div(expectedDebt) + + // Get an approximate address hint from the deployed HintHelper contract. Use (15 * number of troves) trials + // to get an approx. hint that is close to the right position. + let numTroves = await sortedTroves.getSize() + let numTrials = numTroves.mul(toBN('15')) + let { 0: approxHint } = await hintHelpers.getApproxHint(NICR, numTrials, 42) // random seed of 42 + + // Use the approximate hint to get the exact upper and lower hints from the deployed SortedTroves contract + let { 0: upperHint, 1: lowerHint } = await sortedTroves.findInsertPosition(NICR, approxHint, approxHint) + + // Finally, call openTrove with the exact upperHint and lowerHint + const maxFee = '5'.concat('0'.repeat(16)) // Slippage protection: 5% + await borrowerOperations.openTrove(maxFee, LUSDAmount, upperHint, lowerHint, { value: ETHColl }) + + // --- adjust trove --- + + const collIncrease = toBN(toWei('1')) // borrower wants to add 1 ETH + const LUSDRepayment = toBN(toWei('230')) // borrower wants to repay 230 LUSD + + // Get trove's current debt and coll + const {0: debt, 1: coll} = await troveManager.getEntireDebtAndColl(borrower) + + const newDebt = debt.sub(LUSDRepayment) + const newColl = coll.add(collIncrease) + + NICR = newColl.mul(_1e20).div(newDebt) + + // Get an approximate address hint from the deployed HintHelper contract. Use (15 * number of troves) trials + // to get an approx. hint that is close to the right position. + numTroves = await sortedTroves.getSize() + numTrials = numTroves.mul(toBN('15')) + ({0: approxHint} = await hintHelpers.getApproxHint(NICR, numTrials, 42)) + + // Use the approximate hint to get the exact upper and lower hints from the deployed SortedTroves contract + ({ 0: upperHint, 1: lowerHint } = await sortedTroves.findInsertPosition(NICR, approxHint, approxHint)) + + // Call adjustTrove with the exact upperHint and lowerHint + await borrowerOperations.adjustTrove(maxFee, 0, LUSDRepayment, false, upperHint, lowerHint, {value: collIncrease}) + + + // --- RedeemCollateral --- + + // Get the redemptions hints from the deployed HintHelpers contract + const redemptionhint = await hintHelpers.getRedemptionHints(LUSDAmount, price, 50) + + const {0: firstRedemptionHint, 1: partialRedemptionNewICR, 2: truncatedLUSDAmount} = redemptionhint + + // Get the approximate partial redemption hint + const { + hintAddress: approxPartialRedemptionHint, + latestRandomSeed + } = await contracts.hintHelpers.getApproxHint(partialRedemptionNewICR, numTrials, 42) + + /* Use the approximate partial redemption hint to get the exact partial redemption hint from the + * deployed SortedTroves contract + */ + const exactPartialRedemptionHint = (await sortedTroves.findInsertPosition(partialRedemptionNewICR, + approxPartialRedemptionHint, + approxPartialRedemptionHint)) + + /* Finally, perform the on-chain redemption, passing the truncated LUSD amount, the correct hints, and the expected + * ICR of the final partially redeemed trove in the sequence. + */ + await troveManager.redeemCollateral(truncatedLUSDAmount, + firstRedemptionHint, + exactPartialRedemptionHint[0], + exactPartialRedemptionHint[1], + partialRedemptionNewICR, + 0, maxFee, + { from: redeemer }, + ) +} + +main() + .then(() => process.exit(0)) + .catch(error => { + console.error(error); + process.exit(1); + }); + diff --git a/packages/dev-frontend/package.json b/packages/dev-frontend/package.json index 96b315c69..f91ac0404 100644 --- a/packages/dev-frontend/package.json +++ b/packages/dev-frontend/package.json @@ -4,7 +4,7 @@ "private": true, "homepage": ".", "dependencies": { - "@ethersproject/abi": "5.0.13", + "@ethersproject/abi": "5.3.1", "@fortawesome/fontawesome-svg-core": "1.2.34", "@fortawesome/free-regular-svg-icons": "5.15.2", "@fortawesome/free-solid-svg-icons": "5.15.2", @@ -27,7 +27,7 @@ "@web3-react/injected-connector": "6.0.7", "@web3-react/types": "6.0.7", "cross-env": "7.0.3", - "ethers": "5.0.32", + "ethers": "5.3.1", "npm-run-all": "4.1.5", "react": "17.0.1", "react-circular-progressbar": "2.0.3", diff --git a/packages/dev-frontend/src/App.test.tsx b/packages/dev-frontend/src/App.test.tsx index 3b06dce92..64caefd6e 100644 --- a/packages/dev-frontend/src/App.test.tsx +++ b/packages/dev-frontend/src/App.test.tsx @@ -14,7 +14,7 @@ console.log(`${trove}`); * Just a quick and dirty testcase to prove that the approach can work in our CI pipeline. */ test("there's no smoke", async () => { - const { getByText, getAllByText, getByLabelText, findByText } = render(); + const { getByText, getByLabelText, findByText } = render(); expect(await findByText(/you can borrow lusd by opening a trove/i)).toBeInTheDocument(); @@ -24,7 +24,7 @@ test("there's no smoke", async () => { fireEvent.click(getByLabelText(/^borrow$/i)); fireEvent.change(getByLabelText(/^borrow$/i), { target: { value: `${trove.debt}` } }); - const confirmButton = getAllByText(/confirm/i)[0]; + const confirmButton = await findByText(/confirm/i); fireEvent.click(confirmButton); expect(await findByText(/adjust/i)).toBeInTheDocument(); diff --git a/packages/dev-frontend/src/components/Stability/Yield.tsx b/packages/dev-frontend/src/components/Stability/Yield.tsx index 4f6c9775f..3dd531c85 100644 --- a/packages/dev-frontend/src/components/Stability/Yield.tsx +++ b/packages/dev-frontend/src/components/Stability/Yield.tsx @@ -39,8 +39,9 @@ export const Yield: React.FC = () => { const yearlyHalvingSchedule = 0.5; // 50% see LQTY distribution schedule for more info const remainingLqtyOneYear = remainingStabilityPoolLQTYReward.mul(yearlyHalvingSchedule); - const remainingLqtyInUSD = remainingLqtyOneYear.mul(lqtyPrice); - const aprPercentage = remainingLqtyInUSD.div(lusdInStabilityPool).mul(100); + const remainingLqtyOneYearInUSD = remainingLqtyOneYear.mul(lqtyPrice); + const aprPercentage = remainingLqtyOneYearInUSD.div(lusdInStabilityPool).mul(100); + const remainingLqtyInUSD = remainingStabilityPoolLQTYReward.mul(lqtyPrice); if (aprPercentage.isZero) return null; diff --git a/packages/dev-frontend/src/components/Transaction.tsx b/packages/dev-frontend/src/components/Transaction.tsx index 6c3460115..b09a82398 100644 --- a/packages/dev-frontend/src/components/Transaction.tsx +++ b/packages/dev-frontend/src/components/Transaction.tsx @@ -7,7 +7,7 @@ import { defaultAbiCoder } from "@ethersproject/abi"; import { buildStyles, CircularProgressbarWithChildren } from "react-circular-progressbar"; import "react-circular-progressbar/dist/styles.css"; -import { EthersTransactionOverrides } from "@liquity/lib-ethers"; +import { EthersTransactionOverrides, EthersTransactionCancelledError } from "@liquity/lib-ethers"; import { SentLiquityTransaction, LiquityReceipt } from "@liquity/lib-base"; import { useLiquity } from "../hooks/LiquityContext"; @@ -237,9 +237,8 @@ export function Transaction { +const tryToGetRevertReason = async (provider: Provider, tx: TransactionReceipt) => { try { - const tx = await provider.getTransaction(hash); const result = await provider.call(tx, tx.blockNumber); if (hexDataLength(result) % 32 === 4 && hexDataSlice(result, 0, 4) === "0x08c379a0") { @@ -322,7 +321,7 @@ export const TransactionMonitor: React.FC = () => { id }); } else { - const reason = await tryToGetRevertReason(provider, txHash); + const reason = await tryToGetRevertReason(provider, receipt.rawReceipt); if (cancelled) { return; @@ -344,14 +343,21 @@ export const TransactionMonitor: React.FC = () => { return; } - console.error(`Failed to get receipt for tx ${txHash}`); - console.error(rawError); + finished = true; + + if (rawError instanceof EthersTransactionCancelledError) { + console.log(`Cancelled tx ${txHash}`); + setTransactionState({ type: "cancelled", id }); + } else { + console.error(`Failed to get receipt for tx ${txHash}`); + console.error(rawError); - setTransactionState({ - type: "failed", - id, - error: new Error("Failed") - }); + setTransactionState({ + type: "failed", + id, + error: new Error("Failed") + }); + } } }; diff --git a/packages/dev-frontend/src/components/Trove/Adjusting.tsx b/packages/dev-frontend/src/components/Trove/Adjusting.tsx index c6ac78fb0..9fcaa1d90 100644 --- a/packages/dev-frontend/src/components/Trove/Adjusting.tsx +++ b/packages/dev-frontend/src/components/Trove/Adjusting.tsx @@ -9,6 +9,8 @@ import { Difference } from "@liquity/lib-base"; import { useLiquitySelector } from "@liquity/lib-react"; + +import { useStableTroveChange } from "../../hooks/useStableTroveChange"; import { ActionDescription } from "../ActionDescription"; import { useMyTransactionState } from "../Transaction"; import { TroveAction } from "./TroveAction"; @@ -19,6 +21,7 @@ import { InfoIcon } from "../InfoIcon"; import { LoadingOverlay } from "../LoadingOverlay"; import { CollateralRatio } from "./CollateralRatio"; import { EditableRow, StaticRow } from "./Editor"; +import { ExpensiveTroveChangeWarning, GasEstimationState } from "./ExpensiveTroveChangeWarning"; import { selectForTroveChangeValidation, validateTroveChange @@ -118,10 +121,6 @@ export const Adjusting: React.FC = () => { setNetDebt(trove.netDebt); }, [trove.collateral, trove.netDebt]); - if (trove.status !== "open") { - return null; - } - const isDirty = !collateral.eq(trove.collateral) || !netDebt.eq(trove.netDebt); const isDebtIncrease = netDebt.gt(trove.netDebt); const debtIncreaseAmount = isDebtIncrease ? netDebt.sub(trove.netDebt) : Decimal.ZERO; @@ -133,8 +132,10 @@ export const Adjusting: React.FC = () => { const maxBorrowingRate = borrowingRate.add(0.005); const updatedTrove = isDirty ? new Trove(collateral, totalDebt) : trove; const feePct = new Percent(borrowingRate); - const maxEth = accountBalance.gt(GAS_ROOM_ETH) ? accountBalance.sub(GAS_ROOM_ETH) : Decimal.ZERO; - const maxCollateral = collateral.add(maxEth); + const availableEth = accountBalance.gt(GAS_ROOM_ETH) + ? accountBalance.sub(GAS_ROOM_ETH) + : Decimal.ZERO; + const maxCollateral = trove.collateral.add(availableEth); const collateralMaxedOut = collateral.eq(maxCollateral); const collateralRatio = !collateral.isZero && !netDebt.isZero ? updatedTrove.collateralRatio(price) : undefined; @@ -147,10 +148,17 @@ export const Adjusting: React.FC = () => { validationContext ); + const stableTroveChange = useStableTroveChange(troveChange); + const [gasEstimationState, setGasEstimationState] = useState({ type: "idle" }); + const isTransactionPending = transactionState.type === "waitingForApproval" || transactionState.type === "waitingForConfirmation"; + if (trove.status !== "open") { + return null; + } + return ( @@ -252,16 +260,25 @@ export const Adjusting: React.FC = () => { )} + + - {troveChange ? ( + {stableTroveChange ? ( Confirm diff --git a/packages/dev-frontend/src/components/Trove/ExpensiveTroveChangeWarning.tsx b/packages/dev-frontend/src/components/Trove/ExpensiveTroveChangeWarning.tsx new file mode 100644 index 000000000..9c439385a --- /dev/null +++ b/packages/dev-frontend/src/components/Trove/ExpensiveTroveChangeWarning.tsx @@ -0,0 +1,86 @@ +import React, { useEffect } from "react"; + +import { Decimal, TroveChange } from "@liquity/lib-base"; +import { PopulatedEthersLiquityTransaction } from "@liquity/lib-ethers"; + +import { useLiquity } from "../../hooks/LiquityContext"; +import { Warning } from "../Warning"; + +export type GasEstimationState = + | { type: "idle" | "inProgress" } + | { type: "complete"; populatedTx: PopulatedEthersLiquityTransaction }; + +type ExpensiveTroveChangeWarningParams = { + troveChange?: Exclude, { type: "invalidCreation" }>; + maxBorrowingRate: Decimal; + borrowingFeeDecayToleranceMinutes: number; + gasEstimationState: GasEstimationState; + setGasEstimationState: (newState: GasEstimationState) => void; +}; + +export const ExpensiveTroveChangeWarning: React.FC = ({ + troveChange, + maxBorrowingRate, + borrowingFeeDecayToleranceMinutes, + gasEstimationState, + setGasEstimationState +}) => { + const { liquity } = useLiquity(); + + useEffect(() => { + if (troveChange && troveChange.type !== "closure") { + setGasEstimationState({ type: "inProgress" }); + + let cancelled = false; + + const timeoutId = setTimeout(async () => { + const populatedTx = await (troveChange.type === "creation" + ? liquity.populate.openTrove(troveChange.params, { + maxBorrowingRate, + borrowingFeeDecayToleranceMinutes + }) + : liquity.populate.adjustTrove(troveChange.params, { + maxBorrowingRate, + borrowingFeeDecayToleranceMinutes + })); + + if (!cancelled) { + setGasEstimationState({ type: "complete", populatedTx }); + console.log( + "Estimated TX cost: " + + Decimal.from(`${populatedTx.rawPopulatedTransaction.gasLimit}`).prettify(0) + ); + } + }, 333); + + return () => { + clearTimeout(timeoutId); + cancelled = true; + }; + } else { + setGasEstimationState({ type: "idle" }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [troveChange]); + + if ( + troveChange && + gasEstimationState.type === "complete" && + gasEstimationState.populatedTx.gasHeadroom !== undefined && + gasEstimationState.populatedTx.gasHeadroom >= 200000 + ) { + return troveChange.type === "creation" ? ( + + The cost of opening a Trove in this collateral ratio range is rather high. To lower it, + choose a slightly different collateral ratio. + + ) : ( + + The cost of adjusting a Trove into this collateral ratio range is rather high. To lower it, + choose a slightly different collateral ratio. + + ); + } + + return null; +}; diff --git a/packages/dev-frontend/src/components/Trove/Opening.tsx b/packages/dev-frontend/src/components/Trove/Opening.tsx index b96ebf811..e69d8475d 100644 --- a/packages/dev-frontend/src/components/Trove/Opening.tsx +++ b/packages/dev-frontend/src/components/Trove/Opening.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useState } from "react"; -import { Flex, Button, Box, Card, Heading } from "theme-ui"; +import { Flex, Button, Box, Card, Heading, Spinner } from "theme-ui"; import { LiquityStoreState, Decimal, @@ -9,6 +9,8 @@ import { Percent } from "@liquity/lib-base"; import { useLiquitySelector } from "@liquity/lib-react"; + +import { useStableTroveChange } from "../../hooks/useStableTroveChange"; import { ActionDescription } from "../ActionDescription"; import { useMyTransactionState } from "../Transaction"; import { TroveAction } from "./TroveAction"; @@ -19,6 +21,7 @@ import { InfoIcon } from "../InfoIcon"; import { LoadingOverlay } from "../LoadingOverlay"; import { CollateralRatio } from "./CollateralRatio"; import { EditableRow, StaticRow } from "./Editor"; +import { ExpensiveTroveChangeWarning, GasEstimationState } from "./ExpensiveTroveChangeWarning"; import { selectForTroveChangeValidation, validateTroveChange @@ -54,8 +57,9 @@ export const Opening: React.FC = () => { const totalDebt = borrowAmount.add(LUSD_LIQUIDATION_RESERVE).add(fee); const isDirty = !collateral.isZero || !borrowAmount.isZero; const trove = isDirty ? new Trove(collateral, totalDebt) : EMPTY_TROVE; - const maxEth = accountBalance.gt(GAS_ROOM_ETH) ? accountBalance.sub(GAS_ROOM_ETH) : Decimal.ZERO; - const maxCollateral = collateral.add(maxEth); + const maxCollateral = accountBalance.gt(GAS_ROOM_ETH) + ? accountBalance.sub(GAS_ROOM_ETH) + : Decimal.ZERO; const collateralMaxedOut = collateral.eq(maxCollateral); const collateralRatio = !collateral.isZero && !borrowAmount.isZero ? trove.collateralRatio(price) : undefined; @@ -67,6 +71,9 @@ export const Opening: React.FC = () => { validationContext ); + const stableTroveChange = useStableTroveChange(troveChange); + const [gasEstimationState, setGasEstimationState] = useState({ type: "idle" }); + const transactionState = useMyTransactionState(TRANSACTION_ID); const isTransactionPending = transactionState.type === "waitingForApproval" || @@ -188,16 +195,29 @@ export const Opening: React.FC = () => { )} + + - {troveChange ? ( + {gasEstimationState.type === "inProgress" ? ( + + ) : stableTroveChange ? ( Confirm diff --git a/packages/dev-frontend/src/components/Trove/TroveAction.tsx b/packages/dev-frontend/src/components/Trove/TroveAction.tsx index d8ba7bc5d..2e9567809 100644 --- a/packages/dev-frontend/src/components/Trove/TroveAction.tsx +++ b/packages/dev-frontend/src/components/Trove/TroveAction.tsx @@ -9,23 +9,31 @@ type TroveActionProps = { transactionId: string; change: Exclude, { type: "invalidCreation" }>; maxBorrowingRate: Decimal; + borrowingFeeDecayToleranceMinutes: number; }; export const TroveAction: React.FC = ({ children, transactionId, change, - maxBorrowingRate + maxBorrowingRate, + borrowingFeeDecayToleranceMinutes }) => { const { liquity } = useLiquity(); const [sendTransaction] = useTransactionFunction( transactionId, change.type === "creation" - ? liquity.send.openTrove.bind(liquity.send, change.params, maxBorrowingRate) + ? liquity.send.openTrove.bind(liquity.send, change.params, { + maxBorrowingRate, + borrowingFeeDecayToleranceMinutes + }) : change.type === "closure" ? liquity.send.closeTrove.bind(liquity.send) - : liquity.send.adjustTrove.bind(liquity.send, change.params, maxBorrowingRate) + : liquity.send.adjustTrove.bind(liquity.send, change.params, { + maxBorrowingRate, + borrowingFeeDecayToleranceMinutes + }) ); return ; diff --git a/packages/dev-frontend/src/components/Trove/TroveManager.tsx b/packages/dev-frontend/src/components/Trove/TroveManager.tsx index 43c91d9fc..3c5d238f8 100644 --- a/packages/dev-frontend/src/components/Trove/TroveManager.tsx +++ b/packages/dev-frontend/src/components/Trove/TroveManager.tsx @@ -235,6 +235,7 @@ export const TroveManager: React.FC = ({ collateral, debt }) transactionId={`${transactionIdPrefix}${validChange.type}`} change={validChange} maxBorrowingRate={maxBorrowingRate} + borrowingFeeDecayToleranceMinutes={60} > Confirm diff --git a/packages/dev-frontend/src/components/Trove/validation/validateTroveChange.tsx b/packages/dev-frontend/src/components/Trove/validation/validateTroveChange.tsx index ec012d7d3..bc67624aa 100644 --- a/packages/dev-frontend/src/components/Trove/validation/validateTroveChange.tsx +++ b/packages/dev-frontend/src/components/Trove/validation/validateTroveChange.tsx @@ -1,6 +1,7 @@ import { Decimal, LUSD_MINIMUM_DEBT, + LUSD_MINIMUM_NET_DEBT, Trove, TroveAdjustmentParams, TroveChange, @@ -161,7 +162,7 @@ export const validateTroveChange = ( }; const validateTroveCreation = ( - { depositCollateral }: TroveCreationParams, + { depositCollateral, borrowLUSD }: TroveCreationParams, { resultingTrove, recoveryMode, @@ -170,12 +171,12 @@ const validateTroveCreation = ( price }: TroveChangeValidationContext ): JSX.Element | null => { - if (resultingTrove.debt.lt(LUSD_MINIMUM_DEBT)) { + if (borrowLUSD.lt(LUSD_MINIMUM_NET_DEBT)) { return ( - Total debt must be at least{" "} + You must borrow at least{" "} - {LUSD_MINIMUM_DEBT.toString()} {COIN} + {LUSD_MINIMUM_NET_DEBT.toString()} {COIN} . diff --git a/packages/dev-frontend/src/components/Warning.tsx b/packages/dev-frontend/src/components/Warning.tsx new file mode 100644 index 000000000..f1f683666 --- /dev/null +++ b/packages/dev-frontend/src/components/Warning.tsx @@ -0,0 +1,27 @@ +import { Box, Flex, Text } from "theme-ui"; + +import { Icon } from "./Icon"; + +export const Warning: React.FC = ({ children }) => ( + + + + {children} + + +); diff --git a/packages/dev-frontend/src/hooks/useStableTroveChange.ts b/packages/dev-frontend/src/hooks/useStableTroveChange.ts new file mode 100644 index 000000000..17ef8f3c4 --- /dev/null +++ b/packages/dev-frontend/src/hooks/useStableTroveChange.ts @@ -0,0 +1,32 @@ +import { useEffect, useState } from "react"; +import { Decimal, TroveChange } from "@liquity/lib-base"; + +type ValidTroveChange = Exclude, { type: "invalidCreation" }>; + +const paramsEq = (a?: Decimal, b?: Decimal) => (a && b ? a.eq(b) : !a && !b); + +const equals = (a: ValidTroveChange, b: ValidTroveChange): boolean => { + return ( + a.type === b.type && + paramsEq(a.params.borrowLUSD, b.params.borrowLUSD) && + paramsEq(a.params.repayLUSD, b.params.repayLUSD) && + paramsEq(a.params.depositCollateral, b.params.depositCollateral) && + paramsEq(a.params.withdrawCollateral, b.params.withdrawCollateral) + ); +}; + +export const useStableTroveChange = ( + troveChange: ValidTroveChange | undefined +): ValidTroveChange | undefined => { + const [stableTroveChange, setStableTroveChange] = useState(troveChange); + + useEffect(() => { + if (!!stableTroveChange !== !!troveChange) { + setStableTroveChange(troveChange); + } else if (stableTroveChange && troveChange && !equals(stableTroveChange, troveChange)) { + setStableTroveChange(troveChange); + } + }, [stableTroveChange, troveChange]); + + return stableTroveChange; +}; diff --git a/packages/examples/package.json b/packages/examples/package.json index d91da912b..a4e76280d 100644 --- a/packages/examples/package.json +++ b/packages/examples/package.json @@ -4,7 +4,7 @@ "private": true, "devDependencies": { "chalk": "^4.1.0", - "ethers": "^5.0.0" + "ethers": "^5.3.0" }, "scripts": { "liqbot": "node src/liqbot.js" diff --git a/packages/fuzzer/package.json b/packages/fuzzer/package.json index 2b7ca864d..679648a22 100644 --- a/packages/fuzzer/package.json +++ b/packages/fuzzer/package.json @@ -9,7 +9,7 @@ "@types/yargs": "^16.0.0", "colors": "^1.4.0", "dotenv": "^8.2.0", - "ethers": "^5.0.0", + "ethers": "^5.3.0", "ts-node": "^9.0.0", "typescript": "~4.1.0", "yargs": "^16.0.3" diff --git a/packages/lib-base/package.json b/packages/lib-base/package.json index a22c5f22e..08add7412 100644 --- a/packages/lib-base/package.json +++ b/packages/lib-base/package.json @@ -22,7 +22,7 @@ "test": "mocha --require ts-node/register" }, "dependencies": { - "@ethersproject/bignumber": "5.0.15" + "@ethersproject/bignumber": "5.3.0" }, "devDependencies": { "@microsoft/api-extractor": "7.13.2", diff --git a/packages/lib-ethers/deployments/default/goerli.json b/packages/lib-ethers/deployments/default/goerli.json index 76e9a1552..c55eb16a1 100644 --- a/packages/lib-ethers/deployments/default/goerli.json +++ b/packages/lib-ethers/deployments/default/goerli.json @@ -8,6 +8,7 @@ "_priceFeedIsTestnet": true, "_uniTokenIsMock": false, "_isDev": false, + "startBlock": 4547952, "addresses": { "activePool": "0x5948018DEeC14642E6127c5a3AC4081798bB73d8", "borrowerOperations": "0xa36bA16411AF139737E8E345Cd9422a47856bECa", diff --git a/packages/lib-ethers/deployments/default/kovan.json b/packages/lib-ethers/deployments/default/kovan.json index b5731fb86..903df9714 100644 --- a/packages/lib-ethers/deployments/default/kovan.json +++ b/packages/lib-ethers/deployments/default/kovan.json @@ -8,6 +8,7 @@ "_priceFeedIsTestnet": false, "_uniTokenIsMock": false, "_isDev": false, + "startBlock": 24134777, "addresses": { "activePool": "0x2FEB8CC8eD32117D1F39168543f29c30fdf10105", "borrowerOperations": "0xA289111CC4b306E3F5F15c654D4c318B3dA51813", diff --git a/packages/lib-ethers/deployments/default/mainnet.json b/packages/lib-ethers/deployments/default/mainnet.json index eb4c5d9fb..8cc61c129 100644 --- a/packages/lib-ethers/deployments/default/mainnet.json +++ b/packages/lib-ethers/deployments/default/mainnet.json @@ -8,6 +8,7 @@ "_priceFeedIsTestnet": false, "_uniTokenIsMock": false, "_isDev": false, + "startBlock": 12178551, "addresses": { "activePool": "0xDf9Eb223bAFBE5c5271415C75aeCD68C21fE3D7F", "borrowerOperations": "0x24179CD81c9e782A4096035f7eC97fB8B783e007", diff --git a/packages/lib-ethers/deployments/default/rinkeby.json b/packages/lib-ethers/deployments/default/rinkeby.json index e4104e803..d62dff24c 100644 --- a/packages/lib-ethers/deployments/default/rinkeby.json +++ b/packages/lib-ethers/deployments/default/rinkeby.json @@ -8,6 +8,7 @@ "_priceFeedIsTestnet": false, "_uniTokenIsMock": false, "_isDev": false, + "startBlock": 8341450, "addresses": { "activePool": "0xbA49275F8F890E7296F64b3e81F1Ada656030150", "borrowerOperations": "0x91656701b33eca6425A239930FccAA842D0E2031", diff --git a/packages/lib-ethers/deployments/default/ropsten.json b/packages/lib-ethers/deployments/default/ropsten.json index d72e2baba..648d60126 100644 --- a/packages/lib-ethers/deployments/default/ropsten.json +++ b/packages/lib-ethers/deployments/default/ropsten.json @@ -8,6 +8,7 @@ "_priceFeedIsTestnet": true, "_uniTokenIsMock": false, "_isDev": false, + "startBlock": 9961294, "addresses": { "activePool": "0x8bE79B54Bff7754B57294077c2B5017AF9f57dC2", "borrowerOperations": "0xfe9049E677C5773dd72ac7E19c38c68aB0891744", diff --git a/packages/lib-ethers/etc/lib-ethers.api.md b/packages/lib-ethers/etc/lib-ethers.api.md index 0219d74ed..a4a313dec 100644 --- a/packages/lib-ethers/etc/lib-ethers.api.md +++ b/packages/lib-ethers/etc/lib-ethers.api.md @@ -9,6 +9,7 @@ import { BlockTag } from '@ethersproject/abstract-provider'; import { CollateralGainTransferDetails } from '@liquity/lib-base'; import { Decimal } from '@liquity/lib-base'; import { Decimalish } from '@liquity/lib-base'; +import { ErrorCode } from '@ethersproject/logger'; import { FailedReceipt } from '@liquity/lib-base'; import { Fees } from '@liquity/lib-base'; import { FrontendStatus } from '@liquity/lib-base'; @@ -61,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; @@ -87,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) @@ -116,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) @@ -181,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; @@ -233,6 +244,7 @@ export interface EthersLiquityConnection extends EthersLiquityConnectionOptional readonly _priceFeedIsTestnet: boolean; readonly provider: EthersProvider; readonly signer?: EthersSigner; + readonly startBlock: number; readonly totalStabilityPoolLQTYReward: Decimal; readonly version: string; } @@ -261,6 +273,16 @@ export type EthersProvider = Provider; // @public export type EthersSigner = Signer; +// @public +export class EthersTransactionCancelledError extends Error { + // @internal + constructor(rawError: _RawTransactionReplacedError); + // (undocumented) + readonly rawError: Error; + // (undocumented) + readonly rawReplacementReceipt: EthersTransactionReceipt; +} + // @public export class EthersTransactionFailedError extends TransactionFailedError> { constructor(message: string, failedReceipt: FailedReceipt); @@ -309,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) @@ -331,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) @@ -369,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>; @@ -389,6 +412,34 @@ export class PopulatedEthersRedemption extends PopulatedEthersLiquityTransaction readonly redeemableLUSDAmount: Decimal; } +// @internal (undocumented) +export enum _RawErrorReason { + // (undocumented) + TRANSACTION_CANCELLED = "cancelled", + // (undocumented) + TRANSACTION_FAILED = "transaction failed", + // (undocumented) + TRANSACTION_REPLACED = "replaced", + // (undocumented) + TRANSACTION_REPRICED = "repriced" +} + +// @internal (undocumented) +export interface _RawTransactionReplacedError extends Error { + // (undocumented) + cancelled: boolean; + // (undocumented) + code: ErrorCode.TRANSACTION_REPLACED; + // (undocumented) + hash: string; + // (undocumented) + reason: _RawErrorReason.TRANSACTION_CANCELLED | _RawErrorReason.TRANSACTION_REPLACED | _RawErrorReason.TRANSACTION_REPRICED; + // (undocumented) + receipt: EthersTransactionReceipt; + // (undocumented) + replacement: EthersTransactionResponse; +} + // @public export class ReadableEthersLiquity implements ReadableLiquity { // @internal @@ -409,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) @@ -481,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) @@ -503,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 ff2c2477e..44ea6746a 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", @@ -35,7 +36,7 @@ }, "peerDependencies": { "@liquity/lib-base": "^3.0.0", - "ethers": "^5.0.0" + "ethers": "^5.3.0" }, "devDependencies": { "@microsoft/api-extractor": "7.13.2", @@ -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", @@ -56,11 +58,12 @@ "dotenv": "8.2.0", "eslint": "7.22.0", "eslint-plugin-tsdoc": "0.2.11", - "ethers": "5.0.32", + "ethers": "5.3.1", "fs-extra": "9.1.0", "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 000000000..afbda677a --- /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 265eb56d1..33aa03cd3 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 @@ -306,13 +317,16 @@ export class EthersLiquity implements ReadableEthersLiquity, TransactableLiquity * * @throws * Throws {@link EthersTransactionFailedError} in case of transaction failure. + * Throws {@link EthersTransactionCancelledError} if the transaction is cancelled or replaced. */ 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); } /** @@ -320,6 +334,7 @@ export class EthersLiquity implements ReadableEthersLiquity, TransactableLiquity * * @throws * Throws {@link EthersTransactionFailedError} in case of transaction failure. + * Throws {@link EthersTransactionCancelledError} if the transaction is cancelled or replaced. */ closeTrove(overrides?: EthersTransactionOverrides): Promise { return this.send.closeTrove(overrides).then(waitForSuccess); @@ -330,13 +345,16 @@ export class EthersLiquity implements ReadableEthersLiquity, TransactableLiquity * * @throws * Throws {@link EthersTransactionFailedError} in case of transaction failure. + * Throws {@link EthersTransactionCancelledError} if the transaction is cancelled or replaced. */ 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); } /** @@ -344,6 +362,7 @@ export class EthersLiquity implements ReadableEthersLiquity, TransactableLiquity * * @throws * Throws {@link EthersTransactionFailedError} in case of transaction failure. + * Throws {@link EthersTransactionCancelledError} if the transaction is cancelled or replaced. */ depositCollateral( amount: Decimalish, @@ -357,6 +376,7 @@ export class EthersLiquity implements ReadableEthersLiquity, TransactableLiquity * * @throws * Throws {@link EthersTransactionFailedError} in case of transaction failure. + * Throws {@link EthersTransactionCancelledError} if the transaction is cancelled or replaced. */ withdrawCollateral( amount: Decimalish, @@ -370,6 +390,7 @@ export class EthersLiquity implements ReadableEthersLiquity, TransactableLiquity * * @throws * Throws {@link EthersTransactionFailedError} in case of transaction failure. + * Throws {@link EthersTransactionCancelledError} if the transaction is cancelled or replaced. */ borrowLUSD( amount: Decimalish, @@ -384,6 +405,7 @@ export class EthersLiquity implements ReadableEthersLiquity, TransactableLiquity * * @throws * Throws {@link EthersTransactionFailedError} in case of transaction failure. + * Throws {@link EthersTransactionCancelledError} if the transaction is cancelled or replaced. */ repayLUSD( amount: Decimalish, @@ -402,6 +424,7 @@ export class EthersLiquity implements ReadableEthersLiquity, TransactableLiquity * * @throws * Throws {@link EthersTransactionFailedError} in case of transaction failure. + * Throws {@link EthersTransactionCancelledError} if the transaction is cancelled or replaced. */ liquidate( address: string | string[], @@ -415,6 +438,7 @@ export class EthersLiquity implements ReadableEthersLiquity, TransactableLiquity * * @throws * Throws {@link EthersTransactionFailedError} in case of transaction failure. + * Throws {@link EthersTransactionCancelledError} if the transaction is cancelled or replaced. */ liquidateUpTo( maximumNumberOfTrovesToLiquidate: number, @@ -428,6 +452,7 @@ export class EthersLiquity implements ReadableEthersLiquity, TransactableLiquity * * @throws * Throws {@link EthersTransactionFailedError} in case of transaction failure. + * Throws {@link EthersTransactionCancelledError} if the transaction is cancelled or replaced. */ depositLUSDInStabilityPool( amount: Decimalish, @@ -442,6 +467,7 @@ export class EthersLiquity implements ReadableEthersLiquity, TransactableLiquity * * @throws * Throws {@link EthersTransactionFailedError} in case of transaction failure. + * Throws {@link EthersTransactionCancelledError} if the transaction is cancelled or replaced. */ withdrawLUSDFromStabilityPool( amount: Decimalish, @@ -455,6 +481,7 @@ export class EthersLiquity implements ReadableEthersLiquity, TransactableLiquity * * @throws * Throws {@link EthersTransactionFailedError} in case of transaction failure. + * Throws {@link EthersTransactionCancelledError} if the transaction is cancelled or replaced. */ withdrawGainsFromStabilityPool( overrides?: EthersTransactionOverrides @@ -467,6 +494,7 @@ export class EthersLiquity implements ReadableEthersLiquity, TransactableLiquity * * @throws * Throws {@link EthersTransactionFailedError} in case of transaction failure. + * Throws {@link EthersTransactionCancelledError} if the transaction is cancelled or replaced. */ transferCollateralGainToTrove( overrides?: EthersTransactionOverrides @@ -479,6 +507,7 @@ export class EthersLiquity implements ReadableEthersLiquity, TransactableLiquity * * @throws * Throws {@link EthersTransactionFailedError} in case of transaction failure. + * Throws {@link EthersTransactionCancelledError} if the transaction is cancelled or replaced. */ sendLUSD( toAddress: string, @@ -493,6 +522,7 @@ export class EthersLiquity implements ReadableEthersLiquity, TransactableLiquity * * @throws * Throws {@link EthersTransactionFailedError} in case of transaction failure. + * Throws {@link EthersTransactionCancelledError} if the transaction is cancelled or replaced. */ sendLQTY( toAddress: string, @@ -507,6 +537,7 @@ export class EthersLiquity implements ReadableEthersLiquity, TransactableLiquity * * @throws * Throws {@link EthersTransactionFailedError} in case of transaction failure. + * Throws {@link EthersTransactionCancelledError} if the transaction is cancelled or replaced. */ redeemLUSD( amount: Decimalish, @@ -521,6 +552,7 @@ export class EthersLiquity implements ReadableEthersLiquity, TransactableLiquity * * @throws * Throws {@link EthersTransactionFailedError} in case of transaction failure. + * Throws {@link EthersTransactionCancelledError} if the transaction is cancelled or replaced. */ claimCollateralSurplus(overrides?: EthersTransactionOverrides): Promise { return this.send.claimCollateralSurplus(overrides).then(waitForSuccess); @@ -531,6 +563,7 @@ export class EthersLiquity implements ReadableEthersLiquity, TransactableLiquity * * @throws * Throws {@link EthersTransactionFailedError} in case of transaction failure. + * Throws {@link EthersTransactionCancelledError} if the transaction is cancelled or replaced. */ stakeLQTY(amount: Decimalish, overrides?: EthersTransactionOverrides): Promise { return this.send.stakeLQTY(amount, overrides).then(waitForSuccess); @@ -541,6 +574,7 @@ export class EthersLiquity implements ReadableEthersLiquity, TransactableLiquity * * @throws * Throws {@link EthersTransactionFailedError} in case of transaction failure. + * Throws {@link EthersTransactionCancelledError} if the transaction is cancelled or replaced. */ unstakeLQTY(amount: Decimalish, overrides?: EthersTransactionOverrides): Promise { return this.send.unstakeLQTY(amount, overrides).then(waitForSuccess); @@ -551,6 +585,7 @@ export class EthersLiquity implements ReadableEthersLiquity, TransactableLiquity * * @throws * Throws {@link EthersTransactionFailedError} in case of transaction failure. + * Throws {@link EthersTransactionCancelledError} if the transaction is cancelled or replaced. */ withdrawGainsFromStaking(overrides?: EthersTransactionOverrides): Promise { return this.send.withdrawGainsFromStaking(overrides).then(waitForSuccess); @@ -561,6 +596,7 @@ export class EthersLiquity implements ReadableEthersLiquity, TransactableLiquity * * @throws * Throws {@link EthersTransactionFailedError} in case of transaction failure. + * Throws {@link EthersTransactionCancelledError} if the transaction is cancelled or replaced. */ registerFrontend(kickbackRate: Decimalish, overrides?: EthersTransactionOverrides): Promise { return this.send.registerFrontend(kickbackRate, overrides).then(waitForSuccess); @@ -580,6 +616,7 @@ export class EthersLiquity implements ReadableEthersLiquity, TransactableLiquity * * @throws * Throws {@link EthersTransactionFailedError} in case of transaction failure. + * Throws {@link EthersTransactionCancelledError} if the transaction is cancelled or replaced. */ approveUniTokens(allowance?: Decimalish, overrides?: EthersTransactionOverrides): Promise { return this.send.approveUniTokens(allowance, overrides).then(waitForSuccess); @@ -590,6 +627,7 @@ export class EthersLiquity implements ReadableEthersLiquity, TransactableLiquity * * @throws * Throws {@link EthersTransactionFailedError} in case of transaction failure. + * Throws {@link EthersTransactionCancelledError} if the transaction is cancelled or replaced. */ stakeUniTokens(amount: Decimalish, overrides?: EthersTransactionOverrides): Promise { return this.send.stakeUniTokens(amount, overrides).then(waitForSuccess); @@ -600,6 +638,7 @@ export class EthersLiquity implements ReadableEthersLiquity, TransactableLiquity * * @throws * Throws {@link EthersTransactionFailedError} in case of transaction failure. + * Throws {@link EthersTransactionCancelledError} if the transaction is cancelled or replaced. */ unstakeUniTokens(amount: Decimalish, overrides?: EthersTransactionOverrides): Promise { return this.send.unstakeUniTokens(amount, overrides).then(waitForSuccess); @@ -610,6 +649,7 @@ export class EthersLiquity implements ReadableEthersLiquity, TransactableLiquity * * @throws * Throws {@link EthersTransactionFailedError} in case of transaction failure. + * Throws {@link EthersTransactionCancelledError} if the transaction is cancelled or replaced. */ withdrawLQTYRewardFromLiquidityMining(overrides?: EthersTransactionOverrides): Promise { return this.send.withdrawLQTYRewardFromLiquidityMining(overrides).then(waitForSuccess); @@ -620,6 +660,7 @@ export class EthersLiquity implements ReadableEthersLiquity, TransactableLiquity * * @throws * Throws {@link EthersTransactionFailedError} in case of transaction failure. + * Throws {@link EthersTransactionCancelledError} if the transaction is cancelled or replaced. */ exitLiquidityMining(overrides?: EthersTransactionOverrides): Promise { return this.send.exitLiquidityMining(overrides).then(waitForSuccess); diff --git a/packages/lib-ethers/src/EthersLiquityConnection.ts b/packages/lib-ethers/src/EthersLiquityConnection.ts index 828588125..d6cf89cf0 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 { @@ -66,6 +66,9 @@ export interface EthersLiquityConnection extends EthersLiquityConnectionOptional /** Date when the Liquity contracts were deployed. */ readonly deploymentDate: Date; + /** Number of block in which the first Liquity contract was deployed. */ + readonly startBlock: number; + /** Time period (in seconds) after `deploymentDate` during which redemptions are disabled. */ readonly bootstrapPeriod: number; @@ -136,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 */ @@ -149,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 317be6e21..cc8459105 100644 --- a/packages/lib-ethers/src/PopulatableEthersLiquity.ts +++ b/packages/lib-ethers/src/PopulatableEthersLiquity.ts @@ -3,6 +3,8 @@ import assert from "assert"; import { BigNumber, BigNumberish } from "@ethersproject/bignumber"; import { AddressZero } from "@ethersproject/constants"; import { Log } from "@ethersproject/abstract-provider"; +import { ErrorCode } from "@ethersproject/logger"; +import { Transaction } from "@ethersproject/transactions"; import { CollateralGainTransferDetails, @@ -10,6 +12,7 @@ import { Decimalish, LiquidationDetails, LiquityReceipt, + LUSD_MINIMUM_DEBT, LUSD_MINIMUM_NET_DEBT, MinedReceipt, PopulatableLiquity, @@ -43,16 +46,16 @@ import { import { EthersLiquityConnection, _getContracts, - _getProvider, _requireAddress, _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 */ @@ -60,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; @@ -67,8 +71,10 @@ const compose = (f: (_: U) => V, g: (_: T) => U) => (_: T) => f(g(_)); const id = (t: T) => t; -// Takes ~6-7K to update lastFeeOperationTime. Let's be on the safe side. -const addGasForPotentialLastFeeOperationTimeUpdate = (gas: BigNumber) => gas.add(10000); +// Takes ~6-7K (use 10K to be safe) to update lastFeeOperationTime, but the cost of calculating the +// decayed baseRate increases logarithmically with time elapsed since the last update. +const addGasForBaseRateUpdate = (maxMinutesSinceLastUpdate = 10) => (gas: BigNumber) => + gas.add(10000 + 1414 * Math.ceil(Math.log2(maxMinutesSinceLastUpdate + 1))); // First traversal in ascending direction takes ~50K, then ~13.5K per extra step. // 80K should be enough for 3 steps, plus some extra to be safe. @@ -112,6 +118,75 @@ function* generateTrials(totalNumberOfTrials: number) { } } +/** @internal */ +export enum _RawErrorReason { + TRANSACTION_FAILED = "transaction failed", + TRANSACTION_CANCELLED = "cancelled", + TRANSACTION_REPLACED = "replaced", + TRANSACTION_REPRICED = "repriced" +} + +const transactionReplacementReasons: unknown[] = [ + _RawErrorReason.TRANSACTION_CANCELLED, + _RawErrorReason.TRANSACTION_REPLACED, + _RawErrorReason.TRANSACTION_REPRICED +]; + +interface RawTransactionFailedError extends Error { + code: ErrorCode.CALL_EXCEPTION; + reason: _RawErrorReason.TRANSACTION_FAILED; + transactionHash: string; + transaction: Transaction; + receipt: EthersTransactionReceipt; +} + +/** @internal */ +export interface _RawTransactionReplacedError extends Error { + code: ErrorCode.TRANSACTION_REPLACED; + reason: + | _RawErrorReason.TRANSACTION_CANCELLED + | _RawErrorReason.TRANSACTION_REPLACED + | _RawErrorReason.TRANSACTION_REPRICED; + cancelled: boolean; + hash: string; + replacement: EthersTransactionResponse; + receipt: EthersTransactionReceipt; +} + +const hasProp = (o: T, p: P): o is T & { [_ in P]: unknown } => p in o; + +const isTransactionFailedError = (error: Error): error is RawTransactionFailedError => + hasProp(error, "code") && + error.code === ErrorCode.CALL_EXCEPTION && + hasProp(error, "reason") && + error.reason === _RawErrorReason.TRANSACTION_FAILED; + +const isTransactionReplacedError = (error: Error): error is _RawTransactionReplacedError => + hasProp(error, "code") && + error.code === ErrorCode.TRANSACTION_REPLACED && + hasProp(error, "reason") && + transactionReplacementReasons.includes(error.reason); + +/** + * Thrown when a transaction is cancelled or replaced by a different transaction. + * + * @public + */ +export class EthersTransactionCancelledError extends Error { + readonly rawReplacementReceipt: EthersTransactionReceipt; + readonly rawError: Error; + + /** @internal */ + constructor(rawError: _RawTransactionReplacedError) { + assert(rawError.reason !== _RawErrorReason.TRANSACTION_REPRICED); + + super(`Transaction ${rawError.reason}`); + this.name = "TransactionCancelledError"; + this.rawReplacementReceipt = rawError.receipt; + this.rawError = rawError; + } +} + /** * A transaction that has already been sent. * @@ -150,24 +225,116 @@ export class SentEthersLiquityTransaction : _pendingReceipt; } + private async _waitForRawReceipt(confirmations?: number) { + try { + return await this.rawSentTransaction.wait(confirmations); + } catch (error: unknown) { + if (error instanceof Error) { + if (isTransactionFailedError(error)) { + return error.receipt; + } + + if (isTransactionReplacedError(error)) { + if (error.cancelled) { + throw new EthersTransactionCancelledError(error); + } else { + return error.receipt; + } + } + } + + throw error; + } + } + /** {@inheritDoc @liquity/lib-base#SentLiquityTransaction.getReceipt} */ async getReceipt(): Promise> { - return this._receiptFrom( - await _getProvider(this._connection).getTransactionReceipt(this.rawSentTransaction.hash) - ); + return this._receiptFrom(await this._waitForRawReceipt(0)); } - /** {@inheritDoc @liquity/lib-base#SentLiquityTransaction.waitForReceipt} */ + /** + * {@inheritDoc @liquity/lib-base#SentLiquityTransaction.waitForReceipt} + * + * @throws + * Throws {@link EthersTransactionCancelledError} if the transaction is cancelled or replaced. + */ async waitForReceipt(): Promise> { - const receipt = this._receiptFrom( - await _getProvider(this._connection).waitForTransaction(this.rawSentTransaction.hash) - ); + const receipt = this._receiptFrom(await this._waitForRawReceipt()); assert(receipt.status !== "pending"); return receipt; } } +/** + * 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; + + /** + * Control the amount of extra gas included attached to the transaction. + * + * @remarks + * Transactions that borrow LUSD must pay a variable borrowing fee, which is added to the Trove's + * debt. This fee increases whenever a redemption occurs, and otherwise decays exponentially. + * Due to this decay, a Trove's collateral ratio can end up being higher than initially calculated + * if the transaction is pending for a long time. When this happens, the backend has to iterate + * over the sorted list of Troves to find a new position for the Trove, which costs extra gas. + * + * The SDK can estimate how much the gas costs of the transaction may increase due to this decay, + * and can include additional gas to ensure that it will still succeed, even if it ends up pending + * for a relatively long time. This parameter specifies the length of time that should be covered + * by the extra gas. + * + * Default: 60 minutes. + */ + 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. * @@ -182,6 +349,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; @@ -189,11 +372,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} */ @@ -317,7 +505,8 @@ export class PopulatableEthersLiquity private _wrapTroveChangeWithFees( params: T, - rawPopulatedTransaction: EthersPopulatedTransaction + rawPopulatedTransaction: EthersPopulatedTransaction, + gasHeadroom?: number ): PopulatedEthersLiquityTransaction<_TroveChangeWithFees> { const { borrowerOperations } = _getContracts(this._readable.connection); @@ -339,7 +528,9 @@ export class PopulatableEthersLiquity newTrove, fee }; - } + }, + + gasHeadroom ); } @@ -542,7 +733,13 @@ export class PopulatableEthersLiquity const { hintAddress } = results.reduce((a, b) => (a.diff.lt(b.diff) ? a : b)); - return sortedTroves.findInsertPosition(nominalCollateralRatio.hex, hintAddress, hintAddress); + const [prev, next] = await sortedTroves.findInsertPosition( + nominalCollateralRatio.hex, + hintAddress, + hintAddress + ); + + return prev === AddressZero ? [next, next] : next === AddressZero ? [prev, prev] : [prev, next]; } private async _findHints(trove: Trove): Promise<[string, string]> { @@ -592,32 +789,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 fees = await this._readable.getFees(); - const borrowingRate = fees.borrowingRate(); - const newTrove = Trove.create(normalized, borrowingRate); + const currentBorrowingRate = decayBorrowingRate(0); + const newTrove = Trove.create(normalizedParams, currentBorrowingRate); + const hints = await this._findHints(newTrove); - maxBorrowingRate = - maxBorrowingRate !== undefined - ? Decimal.from(maxBorrowingRate) - : borrowingRate.add(defaultBorrowingRateSlippageTolerance); + const { + maxBorrowingRate, + borrowingFeeDecayToleranceMinutes + } = normalizeBorrowingOperationOptionalParams( + maxBorrowingRateOrOptionalParams, + currentBorrowingRate + ); + + const txParams = (borrowLUSD: Decimal): Parameters => [ + maxBorrowingRate.hex, + borrowLUSD.hex, + ...hints, + { value: depositCollateral.hex, ...overrides } + ]; + + let gasHeadroom: number | undefined; + + 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 = addGasForBaseRateUpdate(borrowingFeeDecayToleranceMinutes)( + 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 ); } @@ -668,42 +912,92 @@ 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(); + + 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; - maxBorrowingRate = - maxBorrowingRate !== undefined - ? Decimal.from(maxBorrowingRate) - : borrowingRate?.add(defaultBorrowingRateSlippageTolerance) ?? Decimal.ZERO; + 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)) + ]); + + let gasLimit = bigNumberMax(addGasForPotentialListTraversal(gasNow), gasLater); + + if (borrowLUSD) { + gasLimit = addGasForBaseRateUpdate(borrowingFeeDecayToleranceMinutes)(gasLimit); + } + + 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 ); } @@ -931,7 +1225,7 @@ export class PopulatableEthersLiquity return new PopulatedEthersRedemption( await troveManager.estimateAndPopulate.redeemCollateral( { ...overrides }, - addGasForPotentialLastFeeOperationTimeUpdate, + addGasForBaseRateUpdate(), truncatedAmount.hex, firstRedemptionHint, ...partialHints, diff --git a/packages/lib-ethers/src/ReadableEthersLiquity.ts b/packages/lib-ethers/src/ReadableEthersLiquity.ts index af9d4f4d2..4d1a927f2 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, @@ -12,13 +12,12 @@ import { TroveListingParams, TroveWithPendingRedistribution, UserTrove, - UserTroveStatus, - _CachedReadableLiquity, - _LiquityReadCache + UserTroveStatus } from "@liquity/lib-base"; import { MultiTroveGetter } from "../types"; +import { decimalify, numberify, panic } from "./_utils"; import { EthersCallOverrides, EthersProvider, EthersSigner } from "./types"; import { @@ -48,10 +47,6 @@ enum BackendTroveStatus { closedByRedemption } -const panic = (error: Error): T => { - throw error; -}; - const userTroveStatusFrom = (backendStatus: BackendTroveStatus): UserTroveStatus => backendStatus === BackendTroveStatus.nonExistent ? "nonExistent" @@ -65,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"]; @@ -356,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); @@ -437,6 +430,11 @@ export class ReadableEthersLiquity implements ReadableLiquity { } } + /** @internal */ + _getBlockTimestamp(blockTag?: BlockTag): Promise { + return _getBlockTimestamp(this.connection, blockTag); + } + /** @internal */ async _getFeesFactory( overrides?: EthersCallOverrides @@ -465,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)); @@ -537,205 +535,214 @@ export interface ReadableEthersLiquityWithStore { - private _store: BlockPolledLiquityStore; +class _BlockPolledReadableEthersLiquity + implements ReadableEthersLiquityWithStore { + readonly connection: EthersLiquityConnection; + readonly store: BlockPolledLiquityStore; + + private readonly _readable: ReadableEthersLiquity; + + constructor(readable: ReadableEthersLiquity) { + const store = new BlockPolledLiquityStore(readable); - constructor(store: BlockPolledLiquityStore) { - this._store = store; + this.store = store; + this.connection = readable.connection; + this._readable = readable; } private _blockHit(overrides?: EthersCallOverrides): boolean { return ( !overrides || overrides.blockTag === undefined || - overrides.blockTag === this._store.state.blockTag + overrides.blockTag === this.store.state.blockTag ); } private _userHit(address?: string, overrides?: EthersCallOverrides): boolean { return ( this._blockHit(overrides) && - (address === undefined || address === this._store.connection.userAddress) + (address === undefined || address === this.store.connection.userAddress) ); } private _frontendHit(address?: string, overrides?: EthersCallOverrides): boolean { return ( this._blockHit(overrides) && - (address === undefined || address === this._store.connection.frontendTag) + (address === undefined || address === this.store.connection.frontendTag) ); } - getTotalRedistributed(overrides?: EthersCallOverrides): Trove | undefined { - if (this._blockHit(overrides)) { - return this._store.state.totalRedistributed; - } + hasStore(store?: EthersLiquityStoreOption): boolean { + return store === undefined || store === "blockPolled"; } - getTroveBeforeRedistribution( + async getTotalRedistributed(overrides?: EthersCallOverrides): Promise { + return this._blockHit(overrides) + ? this.store.state.totalRedistributed + : this._readable.getTotalRedistributed(overrides); + } + + async getTroveBeforeRedistribution( address?: string, overrides?: EthersCallOverrides - ): TroveWithPendingRedistribution | undefined { - if (this._userHit(address, overrides)) { - return this._store.state.troveBeforeRedistribution; - } + ): Promise { + return this._userHit(address, overrides) + ? this.store.state.troveBeforeRedistribution + : this._readable.getTroveBeforeRedistribution(address, overrides); } - getTrove(address?: string, overrides?: EthersCallOverrides): UserTrove | undefined { - if (this._userHit(address, overrides)) { - return this._store.state.trove; - } + async getTrove(address?: string, overrides?: EthersCallOverrides): Promise { + return this._userHit(address, overrides) + ? this.store.state.trove + : this._readable.getTrove(address, overrides); } - getNumberOfTroves(overrides?: EthersCallOverrides): number | undefined { - if (this._blockHit(overrides)) { - return this._store.state.numberOfTroves; - } + async getNumberOfTroves(overrides?: EthersCallOverrides): Promise { + return this._blockHit(overrides) + ? this.store.state.numberOfTroves + : this._readable.getNumberOfTroves(overrides); } - getPrice(overrides?: EthersCallOverrides): Decimal | undefined { - if (this._blockHit(overrides)) { - return this._store.state.price; - } + async getPrice(overrides?: EthersCallOverrides): Promise { + return this._blockHit(overrides) ? this.store.state.price : this._readable.getPrice(overrides); } - getTotal(overrides?: EthersCallOverrides): Trove | undefined { - if (this._blockHit(overrides)) { - return this._store.state.total; - } + async getTotal(overrides?: EthersCallOverrides): Promise { + return this._blockHit(overrides) ? this.store.state.total : this._readable.getTotal(overrides); } - getStabilityDeposit( + async getStabilityDeposit( address?: string, overrides?: EthersCallOverrides - ): StabilityDeposit | undefined { - if (this._userHit(address, overrides)) { - return this._store.state.stabilityDeposit; - } + ): Promise { + return this._userHit(address, overrides) + ? this.store.state.stabilityDeposit + : this._readable.getStabilityDeposit(address, overrides); } - getRemainingStabilityPoolLQTYReward(overrides?: EthersCallOverrides): Decimal | undefined { - if (this._blockHit(overrides)) { - return this._store.state.remainingStabilityPoolLQTYReward; - } + async getRemainingStabilityPoolLQTYReward(overrides?: EthersCallOverrides): Promise { + return this._blockHit(overrides) + ? this.store.state.remainingStabilityPoolLQTYReward + : this._readable.getRemainingStabilityPoolLQTYReward(overrides); } - getLUSDInStabilityPool(overrides?: EthersCallOverrides): Decimal | undefined { - if (this._blockHit(overrides)) { - return this._store.state.lusdInStabilityPool; - } + async getLUSDInStabilityPool(overrides?: EthersCallOverrides): Promise { + return this._blockHit(overrides) + ? this.store.state.lusdInStabilityPool + : this._readable.getLUSDInStabilityPool(overrides); } - getLUSDBalance(address?: string, overrides?: EthersCallOverrides): Decimal | undefined { - if (this._userHit(address, overrides)) { - return this._store.state.lusdBalance; - } + async getLUSDBalance(address?: string, overrides?: EthersCallOverrides): Promise { + return this._userHit(address, overrides) + ? this.store.state.lusdBalance + : this._readable.getLUSDBalance(address, overrides); } - getLQTYBalance(address?: string, overrides?: EthersCallOverrides): Decimal | undefined { - if (this._userHit(address, overrides)) { - return this._store.state.lqtyBalance; - } + async getLQTYBalance(address?: string, overrides?: EthersCallOverrides): Promise { + return this._userHit(address, overrides) + ? this.store.state.lqtyBalance + : this._readable.getLQTYBalance(address, overrides); } - getUniTokenBalance(address?: string, overrides?: EthersCallOverrides): Decimal | undefined { - if (this._userHit(address, overrides)) { - return this._store.state.uniTokenBalance; - } + async getUniTokenBalance(address?: string, overrides?: EthersCallOverrides): Promise { + return this._userHit(address, overrides) + ? this.store.state.uniTokenBalance + : this._readable.getUniTokenBalance(address, overrides); } - getUniTokenAllowance(address?: string, overrides?: EthersCallOverrides): Decimal | undefined { - if (this._userHit(address, overrides)) { - return this._store.state.uniTokenAllowance; - } + async getUniTokenAllowance(address?: string, overrides?: EthersCallOverrides): Promise { + return this._userHit(address, overrides) + ? this.store.state.uniTokenAllowance + : this._readable.getUniTokenAllowance(address, overrides); } - getRemainingLiquidityMiningLQTYReward(overrides?: EthersCallOverrides): Decimal | undefined { - if (this._blockHit(overrides)) { - return this._store.state.remainingLiquidityMiningLQTYReward; - } + async getRemainingLiquidityMiningLQTYReward(overrides?: EthersCallOverrides): Promise { + return this._blockHit(overrides) + ? this.store.state.remainingLiquidityMiningLQTYReward + : this._readable.getRemainingLiquidityMiningLQTYReward(overrides); } - getLiquidityMiningStake(address?: string, overrides?: EthersCallOverrides): Decimal | undefined { - if (this._userHit(address, overrides)) { - return this._store.state.liquidityMiningStake; - } + async getLiquidityMiningStake( + address?: string, + overrides?: EthersCallOverrides + ): Promise { + return this._userHit(address, overrides) + ? this.store.state.liquidityMiningStake + : this._readable.getLiquidityMiningStake(address, overrides); } - getTotalStakedUniTokens(overrides?: EthersCallOverrides): Decimal | undefined { - if (this._blockHit(overrides)) { - return this._store.state.totalStakedUniTokens; - } + async getTotalStakedUniTokens(overrides?: EthersCallOverrides): Promise { + return this._blockHit(overrides) + ? this.store.state.totalStakedUniTokens + : this._readable.getTotalStakedUniTokens(overrides); } - getLiquidityMiningLQTYReward( + async getLiquidityMiningLQTYReward( address?: string, overrides?: EthersCallOverrides - ): Decimal | undefined { - if (this._userHit(address, overrides)) { - return this._store.state.liquidityMiningLQTYReward; - } + ): Promise { + return this._userHit(address, overrides) + ? this.store.state.liquidityMiningLQTYReward + : this._readable.getLiquidityMiningLQTYReward(address, overrides); } - getCollateralSurplusBalance( + async getCollateralSurplusBalance( address?: string, overrides?: EthersCallOverrides - ): Decimal | undefined { - if (this._userHit(address, overrides)) { - return this._store.state.collateralSurplusBalance; - } + ): Promise { + return this._userHit(address, overrides) + ? this.store.state.collateralSurplusBalance + : this._readable.getCollateralSurplusBalance(address, overrides); } - getFees(overrides?: EthersCallOverrides): Fees | undefined { - if (this._blockHit(overrides)) { - return this._store.state.fees; - } + async _getBlockTimestamp(blockTag?: BlockTag): Promise { + return this._blockHit({ blockTag }) + ? this.store.state.blockTimestamp + : this._readable._getBlockTimestamp(blockTag); } - getLQTYStake(address?: string, overrides?: EthersCallOverrides): LQTYStake | undefined { - if (this._userHit(address, overrides)) { - return this._store.state.lqtyStake; - } + async _getFeesFactory( + overrides?: EthersCallOverrides + ): Promise<(blockTimestamp: number, recoveryMode: boolean) => Fees> { + return this._blockHit(overrides) + ? this.store.state._feesFactory + : this._readable._getFeesFactory(overrides); } - getTotalStakedLQTY(overrides?: EthersCallOverrides): Decimal | undefined { - if (this._blockHit(overrides)) { - return this._store.state.totalStakedLQTY; - } + async getFees(overrides?: EthersCallOverrides): Promise { + return this._blockHit(overrides) ? this.store.state.fees : this._readable.getFees(overrides); } - getFrontendStatus( - address?: string, - overrides?: EthersCallOverrides - ): { status: "unregistered" } | { status: "registered"; kickbackRate: Decimal } | undefined { - if (this._frontendHit(address, overrides)) { - return this._store.state.frontend; - } + async getLQTYStake(address?: string, overrides?: EthersCallOverrides): Promise { + return this._userHit(address, overrides) + ? this.store.state.lqtyStake + : this._readable.getLQTYStake(address, overrides); } - getTroves() { - return undefined; + async getTotalStakedLQTY(overrides?: EthersCallOverrides): Promise { + return this._blockHit(overrides) + ? this.store.state.totalStakedLQTY + : this._readable.getTotalStakedLQTY(overrides); } -} -class _BlockPolledReadableEthersLiquity - extends _CachedReadableLiquity<[overrides?: EthersCallOverrides]> - implements ReadableEthersLiquityWithStore { - readonly connection: EthersLiquityConnection; - readonly store: BlockPolledLiquityStore; - - constructor(readable: ReadableEthersLiquity) { - const store = new BlockPolledLiquityStore(readable); + async getFrontendStatus( + address?: string, + overrides?: EthersCallOverrides + ): Promise { + return this._frontendHit(address, overrides) + ? this.store.state.frontend + : this._readable.getFrontendStatus(address, overrides); + } - super(readable, new BlockPolledLiquityStoreBasedCache(store)); + getTroves( + params: TroveListingParams & { beforeRedistribution: true }, + overrides?: EthersCallOverrides + ): Promise; - this.store = store; - this.connection = readable.connection; - } + getTroves(params: TroveListingParams, overrides?: EthersCallOverrides): Promise; - hasStore(store?: EthersLiquityStoreOption): boolean { - return store === undefined || store === "blockPolled"; + getTroves(params: TroveListingParams, overrides?: EthersCallOverrides): Promise { + return this._readable.getTroves(params, overrides); } _getActivePool(): Promise { @@ -746,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 1cc0d6522..225632632 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 000000000..4e4cbf8c0 --- /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 5ec874d96..25a23685b 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 @@ -224,6 +236,7 @@ export interface _LiquityDeploymentJSON { readonly addresses: _LiquityContractAddresses; readonly version: string; readonly deploymentDate: number; + readonly startBlock: number; readonly bootstrapPeriod: number; readonly totalStabilityPoolLQTYReward: string; readonly liquidityMiningLQTYRewardRate: string; diff --git a/packages/lib-ethers/test/Liquity.test.ts b/packages/lib-ethers/test/Liquity.test.ts index cd6412ff1..eb6c7e453 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, @@ -662,7 +669,7 @@ describe("EthersLiquity", () => { it("should redeem some LUSD after the bootstrap phase", async () => { // Fast-forward 15 days - increaseTime(60 * 60 * 24 * 15); + await increaseTime(60 * 60 * 24 * 15); expect(`${await otherLiquities[0].getCollateralSurplusBalance()}`).to.equal("0"); expect(`${await otherLiquities[1].getCollateralSurplusBalance()}`).to.equal("0"); @@ -777,7 +784,7 @@ describe("EthersLiquity", () => { await otherLiquities[1].openTrove(troveCreationParams); await otherLiquities[2].openTrove(troveCreationParams); - increaseTime(60 * 60 * 24 * 15); + await increaseTime(60 * 60 * 24 * 15); }); it("should truncate the amount if it would put the last Trove below the min debt", async () => { @@ -864,7 +871,7 @@ describe("EthersLiquity", () => { ); } - increaseTime(60 * 60 * 24 * 15); + await increaseTime(60 * 60 * 24 * 15); }); it("should redeem using the maximum iterations and almost all gas", async () => { @@ -1012,7 +1019,7 @@ describe("EthersLiquity", () => { { depositCollateral: 20, borrowLUSD: 2080 } ]); - increaseTime(60 * 60 * 24 * 15); + await increaseTime(60 * 60 * 24 * 15); }); it("should include enough gas for updating lastFeeOperationTime", async () => { @@ -1165,4 +1172,102 @@ describe("EthersLiquity", () => { await waitForSuccess(liquidateMultiple.send()); }); }); + + describe("Gas estimation (fee decay)", () => { + before(async function () { + if (network.name !== "hardhat") { + this.skip(); + } + + this.timeout("1m"); + + deployment = await deployLiquity(deployer); + const [redeemedUser, ...someMoreUsers] = otherUsers.slice(0, 21); + [liquity, ...otherLiquities] = await connectUsers([user, ...someMoreUsers]); + + // Create a "slope" of Troves with similar, but slightly decreasing ICRs + await openTroves( + someMoreUsers, + someMoreUsers.map((_, i) => ({ + depositCollateral: 20, + borrowLUSD: LUSD_MINIMUM_NET_DEBT.add(i / 10) + })) + ); + + // Sweep LUSD + await Promise.all( + otherLiquities.map(async otherLiquity => + otherLiquity.sendLUSD(await user.getAddress(), await otherLiquity.getLUSDBalance(), { + gasPrice: 0 + }) + ) + ); + + const price = await liquity.getPrice(); + + // Create a "designated victim" Trove that'll be redeemed + const redeemedTroveDebt = await liquity + .getLUSDBalance() + .then(x => x.div(10).add(LUSD_LIQUIDATION_RESERVE)); + const redeemedTroveCollateral = redeemedTroveDebt.mulDiv(1.1, price); + const redeemedTrove = new Trove(redeemedTroveCollateral, redeemedTroveDebt); + + await openTroves([redeemedUser], [Trove.recreate(redeemedTrove)]); + + // Jump past bootstrap period + await increaseTime(60 * 60 * 24 * 15); + + // Increase the borrowing rate by redeeming + const { actualLUSDAmount } = await liquity.redeemLUSD(redeemedTrove.netDebt, undefined, { + gasPrice: 0 + }); + + expect(`${actualLUSDAmount}`).to.equal(`${redeemedTrove.netDebt}`); + + const borrowingRate = await liquity.getFees().then(fees => Number(fees.borrowingRate())); + expect(borrowingRate).to.be.within(0.04, 0.049); // make sure it's high, but not clamped to 5% + }); + + it("should predict the gas increase due to fee decay", async function () { + this.timeout("1m"); + + const [bottomTrove] = await liquity.getTroves({ + first: 1, + sortedBy: "ascendingCollateralRatio" + }); + + const borrowingRate = await liquity.getFees().then(fees => fees.borrowingRate()); + + for (const [borrowingFeeDecayToleranceMinutes, roughGasHeadroom] of [ + [10, 102000], + [20, 184000], + [30, 241000] + ]) { + const tx = await liquity.populate.openTrove(Trove.recreate(bottomTrove, borrowingRate), { + borrowingFeeDecayToleranceMinutes + }); + + expect(tx.gasHeadroom).to.be.within(roughGasHeadroom - 1000, roughGasHeadroom + 1000); + } + }); + + it("should include enough gas for the TX to succeed after pending", async function () { + this.timeout("1m"); + + const [bottomTrove] = await liquity.getTroves({ + first: 1, + sortedBy: "ascendingCollateralRatio" + }); + + const borrowingRate = await liquity.getFees().then(fees => fees.borrowingRate()); + + const tx = await liquity.populate.openTrove( + Trove.recreate(bottomTrove.multiply(2), borrowingRate), + { borrowingFeeDecayToleranceMinutes: 60 } + ); + + await increaseTime(60 * 60); + await waitForSuccess(tx.send()); + }); + }); }); diff --git a/packages/lib-ethers/utils/deploy.ts b/packages/lib-ethers/utils/deploy.ts index 3619b63f5..74d0708c6 100644 --- a/packages/lib-ethers/utils/deploy.ts +++ b/packages/lib-ethers/utils/deploy.ts @@ -25,12 +25,12 @@ export const setSilent = (s: boolean): void => { silent = s; }; -const deployContract = async ( +const deployContractAndGetBlockNumber = async ( deployer: Signer, getContractFactory: (name: string, signer: Signer) => Promise, contractName: string, ...args: unknown[] -) => { +): Promise<[address: string, blockNumber: number]> => { log(`Deploying ${contractName} ...`); const contract = await (await getContractFactory(contractName, deployer)).deploy(...args); @@ -45,17 +45,28 @@ const deployContract = async ( log(); - return contract.address; + return [contract.address, receipt.blockNumber]; }; +const deployContract: ( + ...p: Parameters +) => Promise = (...p) => deployContractAndGetBlockNumber(...p).then(([a]) => a); + const deployContracts = async ( deployer: Signer, getContractFactory: (name: string, signer: Signer) => Promise, priceFeedIsTestnet = true, overrides?: Overrides -): Promise> => { +): Promise<[addresses: Omit<_LiquityContractAddresses, "uniToken">, startBlock: number]> => { + const [activePoolAddress, startBlock] = await deployContractAndGetBlockNumber( + deployer, + getContractFactory, + "ActivePool", + { ...overrides } + ); + const addresses = { - activePool: await deployContract(deployer, getContractFactory, "ActivePool", { ...overrides }), + activePool: activePoolAddress, borrowerOperations: await deployContract(deployer, getContractFactory, "BorrowerOperations", { ...overrides }), @@ -95,40 +106,44 @@ const deployContracts = async ( unipool: await deployContract(deployer, getContractFactory, "Unipool", { ...overrides }) }; - return { - ...addresses, - lusdToken: await deployContract( - deployer, - getContractFactory, - "LUSDToken", - addresses.troveManager, - addresses.stabilityPool, - addresses.borrowerOperations, - { ...overrides } - ), + return [ + { + ...addresses, + lusdToken: await deployContract( + deployer, + getContractFactory, + "LUSDToken", + addresses.troveManager, + addresses.stabilityPool, + addresses.borrowerOperations, + { ...overrides } + ), - lqtyToken: await deployContract( - deployer, - getContractFactory, - "LQTYToken", - addresses.communityIssuance, - addresses.lqtyStaking, - addresses.lockupContractFactory, - Wallet.createRandom().address, // _bountyAddress (TODO: parameterize this) - addresses.unipool, // _lpRewardsAddress - Wallet.createRandom().address, // _multisigAddress (TODO: parameterize this) - { ...overrides } - ), + lqtyToken: await deployContract( + deployer, + getContractFactory, + "LQTYToken", + addresses.communityIssuance, + addresses.lqtyStaking, + addresses.lockupContractFactory, + Wallet.createRandom().address, // _bountyAddress (TODO: parameterize this) + addresses.unipool, // _lpRewardsAddress + Wallet.createRandom().address, // _multisigAddress (TODO: parameterize this) + { ...overrides } + ), - multiTroveGetter: await deployContract( - deployer, - getContractFactory, - "MultiTroveGetter", - addresses.troveManager, - addresses.sortedTroves, - { ...overrides } - ) - }; + multiTroveGetter: await deployContract( + deployer, + getContractFactory, + "MultiTroveGetter", + addresses.troveManager, + addresses.sortedTroves, + { ...overrides } + ) + }, + + startBlock + ]; }; export const deployTellorCaller = ( @@ -324,18 +339,19 @@ export const deployAndSetupContracts = async ( _uniTokenIsMock: !wethAddress, _isDev, - addresses: await deployContracts( - deployer, - getContractFactory, - _priceFeedIsTestnet, - overrides - ).then(async addresses => ({ - ...addresses, + ...(await deployContracts(deployer, getContractFactory, _priceFeedIsTestnet, overrides).then( + async ([addresses, startBlock]) => ({ + startBlock, + + addresses: { + ...addresses, - uniToken: await (wethAddress - ? createUniswapV2Pair(deployer, wethAddress, addresses.lusdToken, overrides) - : deployMockUniToken(deployer, getContractFactory, overrides)) - })) + uniToken: await (wethAddress + ? createUniswapV2Pair(deployer, wethAddress, addresses.lusdToken, overrides) + : deployMockUniToken(deployer, getContractFactory, overrides)) + } + }) + )) }; const contracts = _connectToContracts(deployer, deployment); diff --git a/packages/lib-react/package.json b/packages/lib-react/package.json index 5dc753cdd..349c80973 100644 --- a/packages/lib-react/package.json +++ b/packages/lib-react/package.json @@ -15,7 +15,7 @@ "react": "^16.13.1" }, "devDependencies": { - "@ethersproject/bignumber": "5.0.15", + "@ethersproject/bignumber": "5.3.0", "@types/react": "17.0.3", "@typescript-eslint/eslint-plugin": "4.17.0", "@typescript-eslint/parser": "4.18.0", diff --git a/packages/lib-subgraph/apollo.config.js b/packages/lib-subgraph/apollo.config.js index e21426ad5..85688bd51 100644 --- a/packages/lib-subgraph/apollo.config.js +++ b/packages/lib-subgraph/apollo.config.js @@ -2,7 +2,7 @@ module.exports = { client: { service: { name: "liquity-subgraph", - url: "http://localhost:8000/subgraphs/name/liquity/subgraph" + url: "http://localhost:8000/subgraphs/name/liquity/liquity" } } }; diff --git a/packages/lib-subgraph/package.json b/packages/lib-subgraph/package.json index bba1b7e04..32c6801fa 100644 --- a/packages/lib-subgraph/package.json +++ b/packages/lib-subgraph/package.json @@ -26,4 +26,4 @@ "cross-fetch": "~3.0.6", "graphql": "^15.3.0" } -} \ No newline at end of file +} diff --git a/packages/lib-subgraph/src/SubgraphLiquity.ts b/packages/lib-subgraph/src/SubgraphLiquity.ts index 400c9bcd4..b5d416491 100644 --- a/packages/lib-subgraph/src/SubgraphLiquity.ts +++ b/packages/lib-subgraph/src/SubgraphLiquity.ts @@ -136,7 +136,7 @@ const troveBeforeRedistribution = new Query< query TroveWithoutRewards($address: ID!) { user(id: $address) { id - currentTrove { + trove { id ...TroveRawFields } @@ -145,8 +145,8 @@ const troveBeforeRedistribution = new Query< ${troveRawFields} `, ({ data: { user } }, { address }) => { - if (user?.currentTrove) { - return troveFromRawFields(user.currentTrove); + if (user?.trove) { + return troveFromRawFields(user.trove); } else { return new TroveWithPendingRedistribution(address, "nonExistent"); } diff --git a/packages/lib-subgraph/types/Global.ts b/packages/lib-subgraph/types/Global.ts index b019522da..33df2f629 100644 --- a/packages/lib-subgraph/types/Global.ts +++ b/packages/lib-subgraph/types/Global.ts @@ -13,7 +13,7 @@ export interface Global_global_currentSystemState { * Sequence number as an ID (string) */ id: string; - price: any; + price: any | null; totalCollateral: any; totalDebt: any; tokensInStabilityPool: any; diff --git a/packages/lib-subgraph/types/TroveWithoutRewards.ts b/packages/lib-subgraph/types/TroveWithoutRewards.ts index 45a1fc47b..44cb658e8 100644 --- a/packages/lib-subgraph/types/TroveWithoutRewards.ts +++ b/packages/lib-subgraph/types/TroveWithoutRewards.ts @@ -9,7 +9,7 @@ import { TroveStatus } from "./globalTypes"; // GraphQL query operation: TroveWithoutRewards // ==================================================== -export interface TroveWithoutRewards_user_currentTrove_owner { +export interface TroveWithoutRewards_user_trove_owner { __typename: "User"; /** * User's Ethereum address as a hex-string @@ -17,13 +17,13 @@ export interface TroveWithoutRewards_user_currentTrove_owner { id: string; } -export interface TroveWithoutRewards_user_currentTrove { +export interface TroveWithoutRewards_user_trove { __typename: "Trove"; /** * Owner's ID + '-' + an incremented integer */ id: string; - owner: TroveWithoutRewards_user_currentTrove_owner; + owner: TroveWithoutRewards_user_trove_owner; status: TroveStatus; rawCollateral: any; rawDebt: any; @@ -44,7 +44,7 @@ export interface TroveWithoutRewards_user { * User's Ethereum address as a hex-string */ id: string; - currentTrove: TroveWithoutRewards_user_currentTrove | null; + trove: TroveWithoutRewards_user_trove | null; } export interface TroveWithoutRewards { diff --git a/packages/providers/package.json b/packages/providers/package.json index a4e733bd5..492d847c5 100644 --- a/packages/providers/package.json +++ b/packages/providers/package.json @@ -8,11 +8,11 @@ "prepare": "tsc --project tsconfig.dist.json" }, "devDependencies": { - "ethers": "^5.0.0", + "ethers": "^5.3.0", "ts-node": "^9.1.1", "typescript": "~4.1.0" }, "peerDependencies": { - "ethers": "^5.0.0" + "ethers": "^5.3.0" } } diff --git a/packages/providers/src/WebSocketAugmentedProvider.ts b/packages/providers/src/WebSocketAugmentedProvider.ts index d252150df..d5c214215 100644 --- a/packages/providers/src/WebSocketAugmentedProvider.ts +++ b/packages/providers/src/WebSocketAugmentedProvider.ts @@ -1,6 +1,7 @@ import { TransactionRequest, TransactionReceipt, + TransactionResponse, BlockTag, EventType, Listener, @@ -10,6 +11,7 @@ import { import { BaseProvider, Web3Provider } from "@ethersproject/providers"; import { Networkish } from "@ethersproject/networks"; import { Deferrable } from "@ethersproject/properties"; +import { hexDataLength } from "@ethersproject/bytes"; import { WebSocketProvider } from "./WebSocketProvider"; @@ -30,11 +32,59 @@ export const isWebSocketAugmentedProvider = ( const isHeaderNotFoundError = (error: any) => typeof error === "object" && typeof error.message === "string" && - error.message.includes("header not found"); + (error.message.includes( + // geth + "header not found" + ) || + error.message.includes( + // openethereum + "request is not supported because your node is running with state pruning" + )); + +const isTransactionHash = (eventName: EventType): eventName is string => + typeof eventName === "string" && hexDataLength(eventName) === 32; const loadBalancingGlitchRetryIntervalMs = 200; const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); +type BlockListenerContext = { + isActive: () => boolean; + removeMe: () => void; +}; + +const waitFor = (f: (t: T) => Promise) => (g: (u: U) => void) => ( + t: T, + { isActive }: BlockListenerContext +) => { + f(t).then(u => { + if (u !== null && isActive()) { + g(u); + } + }); +}; + +const pass = (f: (t: T) => void) => (t: T, _: BlockListenerContext) => { + f(t); +}; + +const passOnce = (f: (t: T) => void) => (t: T, { removeMe }: BlockListenerContext) => { + f(t); + removeMe(); +}; + +const sequence = ( + f: (_: (_: U) => void) => (_: T, context: BlockListenerContext) => void, + g: (_: (_: V) => void) => (_: U, context: BlockListenerContext) => void +) => (h: (_: V) => void) => (t: T, context: BlockListenerContext) => { + f(u => g(h)(u, context))(t, context); +}; + +const defer = (f: (t: T) => void) => (t: T) => { + setTimeout(() => { + f(t); + }, 0); +}; + export const WebSocketAugmented = BaseProvider>(Base: T) => { let webSocketAugmentedProvider = class extends Base implements WebSocketAugmentedProvider { _wsProvider?: WebSocketProvider; @@ -44,7 +94,7 @@ export const WebSocketAugmented = BaseProvide _seenBlock = 0; _blockListenerScheduled = false; - readonly _blockListeners = new Set<(blockNumber: number) => void>(); + readonly _blockListeners = new Map<(_: never) => void, (blockNumber: number) => void>(); readonly _blockListener = this._onBlock.bind(this); openWebSocket(url: string, network: Networkish) { @@ -91,7 +141,7 @@ export const WebSocketAugmented = BaseProvide setTimeout(() => { this._blockListenerScheduled = false; - [...this._blockListeners].forEach(listener => listener(this._seenBlock)); + [...this._blockListeners].forEach(([, listener]) => listener(this._seenBlock)); }, 50); } } @@ -165,17 +215,39 @@ export const WebSocketAugmented = BaseProvide } } + _wrap( + f: (t: T) => void, + g: (f: (t: T) => void) => (u: U, { removeMe }: BlockListenerContext) => void + ): [(t: T) => void, (u: U) => void] { + return [ + f, + (u: U) => + g(defer(f))(u, { + isActive: () => this._blockListeners.has(f), + removeMe: () => this._blockListeners.delete(f) + }) + ]; + } + on(eventName: EventType, listener: Listener) { - if (eventName === "block") { - return this._addBlockListener(listener); + if (isTransactionHash(eventName)) { + const fetchReceipt = this._getTransactionReceiptFromLatest.bind(this, eventName); + const [, passReceipt] = this._wrap(listener, waitFor(fetchReceipt)); + + passReceipt(undefined); + + return this._addBlockListener(listener, passReceipt); + } else if (eventName === "block") { + return this._addBlockListener(...this._wrap(listener, pass)); } else { return super.on(eventName, listener); } } - _addBlockListener(listener: (blockNumber: number) => void) { - if (!this._blockListeners.has(listener)) { - this._blockListeners.add(listener); + _addBlockListener(key: (_: never) => void, blockListener: (blockNumber: number) => void) { + if (!this._blockListeners.has(key)) { + this._blockListeners.set(key, blockListener); + if (this._blockListeners.size === 1) { this._startBlockEvents(); } @@ -184,28 +256,31 @@ export const WebSocketAugmented = BaseProvide } once(eventName: EventType, listener: Listener) { - if (eventName === "block") { - const listenOnce = (blockNumber: number) => { - listener(blockNumber); - this._removeBlockListener(listenOnce); - }; - return this._addBlockListener(listenOnce); + if (isTransactionHash(eventName)) { + const fetchReceipt = this._getTransactionReceiptFromLatest.bind(this, eventName); + const [, passReceiptOnce] = this._wrap(listener, sequence(waitFor(fetchReceipt), passOnce)); + + passReceiptOnce(undefined); + + return this._addBlockListener(listener, passReceiptOnce); + } else if (eventName === "block") { + return this._addBlockListener(...this._wrap(listener, passOnce)); } else { return super.once(eventName, listener); } } off(eventName: EventType, listener: Listener) { - if (eventName === "block") { + if (isTransactionHash(eventName) || eventName === "block") { return this._removeBlockListener(listener); } else { return super.off(eventName, listener); } } - _removeBlockListener(listener: (blockNumber: number) => void) { - if (this._blockListeners.has(listener)) { - this._blockListeners.delete(listener); + _removeBlockListener(key: (_: never) => void) { + if (this._blockListeners.has(key)) { + this._blockListeners.delete(key); if (this._blockListeners.size === 0) { this._stopBlockEvents(); } @@ -213,12 +288,45 @@ export const WebSocketAugmented = BaseProvide return this; } + async getTransaction(transactionHash: string | Promise) { + const txPromises: Promise[] = [ + super.getTransaction(transactionHash), + ...(this._wsProvider?.isReady ? [this._wsProvider.getTransaction(transactionHash)] : []) + ]; + + const first = await Promise.race(txPromises); + const tx = first ?? (await Promise.all(txPromises)).find(tx => tx !== null) ?? null; + + return tx as TransactionResponse; + } + getTransactionReceipt(transactionHash: string | Promise) { - return this._wsProvider?.ready + return this._wsProvider?.isReady ? this._wsProvider.getTransactionReceipt(transactionHash) : super.getTransactionReceipt(transactionHash); } + getTransactionCount( + addressOrName: string | Promise, + blockTag?: BlockTag | Promise + ) { + return this._wsProvider?.isReady + ? this._wsProvider.getTransactionCount(addressOrName, blockTag) + : super.getTransactionCount(addressOrName, blockTag); + } + + getBlock(blockHashOrBlockTag: BlockTag | string | Promise) { + return this._wsProvider?.isReady + ? this._wsProvider.getBlock(blockHashOrBlockTag) + : super.getBlock(blockHashOrBlockTag); + } + + getBlockWithTransactions(blockHashOrBlockTag: BlockTag | string | Promise) { + return this._wsProvider?.isReady + ? this._wsProvider.getBlockWithTransactions(blockHashOrBlockTag) + : super.getBlockWithTransactions(blockHashOrBlockTag); + } + async _blockContainsTx(blockNumber: number, txHash: string) { let block: Block | null; diff --git a/packages/subgraph/.gitignore b/packages/subgraph/.gitignore index 874209833..32f59b741 100644 --- a/packages/subgraph/.gitignore +++ b/packages/subgraph/.gitignore @@ -1,3 +1,4 @@ node_modules /build /generated +/subgraph.yaml diff --git a/packages/subgraph/README.md b/packages/subgraph/README.md new file mode 100644 index 000000000..91cf1da69 --- /dev/null +++ b/packages/subgraph/README.md @@ -0,0 +1,30 @@ +# Liquity Subgraph + +Contains the entities and dependencies to populate a subgraph for Liquity protocol. + + +# Development quickstart +You need to run a Graph Node locally. + +1. Clone Graph Node: `git clone https://github.com/graphprotocol/graph-node` +2. Move into the docker directory: `cd graph-node/docker` +3. Start Graph Node docker instance: `docker-compose up -d` +4. Read the logs from your Graph Node: `docker ps | grep graph-node | cut -f 1 -d ' ' | xargs docker logs -f` +5. Start your local Liquity dev chain: `cd your_liquity_repo_path && yarn start-dev-chain` +6. Compile Liquity subgraph: `yarn prepare:subgraph && yarn build:subgraph` +7. Deploy Liquity subgraph to your Graph Node: `cd packages/subgraph && yarn prepare-local && yarn create-local && yarn deploy-local` +8. Open Graph Node graphql API instance in your browser: `http://127.0.0.1:8000/subgraphs/name/liquity/liquity` +9. Open Liquity in your browser: `http://localhost:3000` + +# Making subgraph code changes +Having done all of the above, if you make subgraph code changes you'll need to run the following: +1. Recompile local changes: `yarn prepare:subgraph && yarn build:subgraph` +2. Redeploy local changes: `cd packages/subgraph && yarn prepare-local && yarn create-local && yarn deploy-local` + +# Gotchas + +## Stopping and starting dev chain +If you stop and start your local dev chain you need to redeploy your subgraph because the contract addresses will have changed. + +## Unregistered frontends +Local instance runs with a frontend ID of Ethereum zero address (`0x0000000000000000000000000000000000000000`) to register the local frontend run `liquity.registerFrontend(0.9).then(console.log)` in your browser console and update `packages/dev-frontend/src/config`'s `ADDRESS_ZERO` to the address returned in your console and refresh the page. \ No newline at end of file diff --git a/packages/subgraph/abi/ERC20.json b/packages/subgraph/abi/ERC20.json new file mode 100644 index 000000000..2059dc436 --- /dev/null +++ b/packages/subgraph/abi/ERC20.json @@ -0,0 +1,334 @@ +[ + { + "inputs": [ + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "symbol", + "type": "string" + }, + { + "internalType": "address", + "name": "initialAccount", + "type": "address" + }, + { + "internalType": "uint256", + "name": "initialBalance", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "burn", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "subtractedValue", + "type": "uint256" + } + ], + "name": "decreaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "addedValue", + "type": "uint256" + } + ], + "name": "increaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/packages/subgraph/package.json b/packages/subgraph/package.json index 2d66ffbcd..1e3944176 100644 --- a/packages/subgraph/package.json +++ b/packages/subgraph/package.json @@ -3,16 +3,21 @@ "version": "0.1.0", "private": true, "scripts": { - "create": "graph create liquity/liquity-protocol --node https://api.thegraph.com/deploy/", - "create-local": "graph create liquity/liquity-protocol --node http://127.0.0.1:8020", - "prepare": "graph codegen", + "create-local": "graph create liquity/liquity --node http://127.0.0.1:8020", + "prepare": "run-s prepare:*", + "prepare:manifest": "node subgraph.yaml.js", + "prepare:codegen": "graph codegen", + "prepare-local": "run-s prepare-local:*", + "prepare-local:manifest": "node subgraph.yaml.js dev", + "prepare-local:codegen": "graph codegen", "build": "graph build", - "deploy": "graph deploy liquity/liquity-protocol --ipfs https://api.thegraph.com/ipfs/ --node https://api.thegraph.com/deploy/", - "deploy-local": "graph deploy liquity/liquity-protocol --ipfs http://localhost:5001 --node http://127.0.0.1:8020", + "deploy": "graph deploy liquity/liquity --debug --ipfs https://api.thegraph.com/ipfs/ --node https://api.thegraph.com/deploy/", + "deploy-local": "graph deploy liquity/liquity --ipfs http://localhost:5001 --node http://127.0.0.1:8020", "graph": "graph" }, "devDependencies": { "@graphprotocol/graph-cli": "^0.20.0", - "@graphprotocol/graph-ts": "^0.20.0" + "@graphprotocol/graph-ts": "^0.20.0", + "npm-run-all": "^4.1.5" } } diff --git a/packages/subgraph/schema.graphql b/packages/subgraph/schema.graphql index af033de9a..218fe2136 100644 --- a/packages/subgraph/schema.graphql +++ b/packages/subgraph/schema.graphql @@ -2,8 +2,6 @@ type Global @entity { "There should be only one System entity with an ID of 'only'" id: ID! - priceFeedAddress: Bytes - systemStateCount: Int! transactionCount: Int! changeCount: Int! @@ -20,6 +18,11 @@ type Global @entity { numberOfActiveLQTYStakes: Int! totalLQTYTokensStaked: BigDecimal! + "Total amount of LUSD paid as borrowing fees" + totalBorrowingFeesPaid: BigDecimal! + "Total amount of ETH paid as redemption fees" + totalRedemptionFeesPaid: BigDecimal! + "Total redistributed per-stake collateral" rawTotalRedistributedCollateral: BigInt! @@ -33,6 +36,9 @@ type Global @entity { "Only used internally as temporary storage. Will always be null in queries" currentRedemption: Redemption + + "Only used internally as temporary storage. Will always be null in queries" + tmpDepositUpdate: BigInt } type SystemState @entity { @@ -42,7 +48,7 @@ type SystemState @entity { "Can be used to chronologically sort SystemStates" sequenceNumber: Int! - price: BigDecimal! + price: BigDecimal totalCollateral: BigDecimal! totalDebt: BigDecimal! @@ -61,12 +67,16 @@ type User @entity { trove: Trove stabilityDeposit: StabilityDeposit stake: LqtyStake + frontend: Frontend collSurplus: BigDecimal! collSurplusChanges: [CollSurplusChange!]! @derivedFrom(field: "user") liquidations: [Liquidation!]! @derivedFrom(field: "liquidator") redemptions: [Redemption!]! @derivedFrom(field: "redeemer") + + balances: [TokenBalance!] @derivedFrom(field: "owner") + allowances: [TokenAllowance!] @derivedFrom(field: "owner") } enum TroveStatus { @@ -110,6 +120,8 @@ type StabilityDeposit @entity { depositedAmount: BigDecimal! + frontend: Frontend + changes: [StabilityDepositChange!]! @derivedFrom(field: "stabilityDeposit") } @@ -170,6 +182,8 @@ type TroveChange implements Change @entity { debtChange: BigDecimal! debtAfter: BigDecimal! + borrowingFee: BigDecimal + collateralRatioBefore: BigDecimal collateralRatioAfter: BigDecimal @@ -235,6 +249,8 @@ type Redemption @entity { collateralRedeemed: BigDecimal! partial: Boolean! + fee: BigDecimal! + troveChanges: [TroveChange!]! @derivedFrom(field: "redemption") } @@ -281,6 +297,37 @@ type LqtyStakeChange implements Change @entity { amountChange: BigDecimal! amountAfter: BigDecimal! - issuanceGain: BigDecimal - redemptionGain: BigDecimal + issuanceGain: BigDecimal! + redemptionGain: BigDecimal! +} + +type Token @entity { + id: ID! + name: String! + symbol: String! + totalSupply: BigInt! + balances: [TokenBalance!] @derivedFrom(field: "token") + allowances: [TokenAllowance!] @derivedFrom(field: "token") +} + +type TokenBalance @entity { + id: ID! + token: Token! + owner: User! + balance: BigInt! +} + +type TokenAllowance @entity { + id: ID! + token: Token! + owner: User! + spender: User! + value: BigInt! +} + +type Frontend @entity { + id: ID! + owner: User! + kickbackRate: BigDecimal! + deposits: [StabilityDeposit!]! @derivedFrom(field: "frontend") } diff --git a/packages/subgraph/src/calls/PriceFeed.ts b/packages/subgraph/src/calls/PriceFeed.ts deleted file mode 100644 index 8bbbf94ad..000000000 --- a/packages/subgraph/src/calls/PriceFeed.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Address, BigInt } from "@graphprotocol/graph-ts"; - -import { PriceFeed } from "../../generated/TroveManager/PriceFeed"; - -export function getPrice(priceFeedAddress: Address): BigInt { - let priceFeed = PriceFeed.bind(priceFeedAddress); - - return priceFeed.fetchPrice(); -} diff --git a/packages/subgraph/src/entities/Change.ts b/packages/subgraph/src/entities/Change.ts index f6f5ef34e..c69881eed 100644 --- a/packages/subgraph/src/entities/Change.ts +++ b/packages/subgraph/src/entities/Change.ts @@ -4,14 +4,7 @@ import { getChangeSequenceNumber } from "./Global"; import { getTransaction } from "./Transaction"; import { getCurrentSystemState } from "./SystemState"; -export function beginChange(event: ethereum.Event): i32 { - // Pre-create the Transaction entity that this change will eventually refer to (if it doesn't - // exist yet). - - // This is needed because creating a new Transaction may have the side effect of increasing the - // change sequence number through creating a PriceChange. - getTransaction(event); - +export function beginChange(): i32 { return getChangeSequenceNumber(); } diff --git a/packages/subgraph/src/entities/Frontend.ts b/packages/subgraph/src/entities/Frontend.ts new file mode 100644 index 000000000..35c89ec9d --- /dev/null +++ b/packages/subgraph/src/entities/Frontend.ts @@ -0,0 +1,27 @@ +import { Address, BigInt, log } from "@graphprotocol/graph-ts"; +import { Frontend } from "../../generated/schema"; +import { decimalize } from "../utils/bignumbers"; +import { getUser } from "./User"; + +export function registerFrontend(ownerAddress: Address, kickbackRate: BigInt): void { + let owner = getUser(ownerAddress); + let frontend = new Frontend(owner.id); + + frontend.owner = owner.id; + frontend.kickbackRate = decimalize(kickbackRate); + + frontend.save(); +} + +export function assignFrontendToDepositor( + depositorAddress: Address, + frontendAddress: Address +): void { + let frontend = Frontend.load(frontendAddress.toHexString()); + + if (frontend != null) { + let depositor = getUser(depositorAddress); + depositor.frontend = frontend.id; + depositor.save(); + } +} diff --git a/packages/subgraph/src/entities/Global.ts b/packages/subgraph/src/entities/Global.ts index d649a5908..d3c99b14d 100644 --- a/packages/subgraph/src/entities/Global.ts +++ b/packages/subgraph/src/entities/Global.ts @@ -1,8 +1,8 @@ -import { Value, BigInt, Address } from "@graphprotocol/graph-ts"; +import { Value, BigInt } from "@graphprotocol/graph-ts"; import { Global, LqtyStakeChange } from "../../generated/schema"; -import { BIGINT_ZERO, DECIMAL_ZERO } from "../utils/bignumbers"; +import { BIGINT_ZERO, DECIMAL_ZERO, decimalize } from "../utils/bignumbers"; const onlyGlobalId = "only"; @@ -29,6 +29,8 @@ export function getGlobal(): Global { newGlobal.totalNumberOfLQTYStakes = 0; newGlobal.numberOfActiveLQTYStakes = 0; newGlobal.totalLQTYTokensStaked = DECIMAL_ZERO; + newGlobal.totalBorrowingFeesPaid = DECIMAL_ZERO; + newGlobal.totalRedemptionFeesPaid = DECIMAL_ZERO; return newGlobal; } @@ -56,6 +58,12 @@ export function getChangeSequenceNumber(): i32 { return increaseCounter("changeCount"); } +export function getLastChangeSequenceNumber(): i32 { + let global = getGlobal(); + + return global.changeCount - 1; +} + export function getLiquidationSequenceNumber(): i32 { return increaseCounter("liquidationCount"); } @@ -64,17 +72,6 @@ export function getRedemptionSequenceNumber(): i32 { return increaseCounter("redemptionCount"); } -export function updatePriceFeedAddress(priceFeedAddress: Address): void { - let global = getGlobal(); - - global.priceFeedAddress = priceFeedAddress; - global.save(); -} - -export function getPriceFeedAddress(): Address { - return getGlobal().priceFeedAddress as Address; -} - export function updateTotalRedistributed(L_ETH: BigInt, L_LUSDDebt: BigInt): void { let global = getGlobal(); @@ -150,15 +147,16 @@ export function handleLQTYStakeChange( global.totalNumberOfLQTYStakes++; } global.numberOfActiveLQTYStakes++; - global.totalLQTYTokensStaked = global.totalLQTYTokensStaked.plus(stakeChange.amountChange); - } else if (stakeChange.stakeOperation == "stakeIncreased") { - global.totalLQTYTokensStaked = global.totalLQTYTokensStaked.plus(stakeChange.amountChange); - } else if (stakeChange.stakeOperation == "stakeDecreased") { - global.totalLQTYTokensStaked = global.totalLQTYTokensStaked.minus(stakeChange.amountChange); } else if (stakeChange.stakeOperation == "stakeRemoved") { global.numberOfActiveLQTYStakes--; - global.totalLQTYTokensStaked = global.totalLQTYTokensStaked.minus(stakeChange.amountChange); } + global.totalLQTYTokensStaked = global.totalLQTYTokensStaked.plus(stakeChange.amountChange); + global.save(); +} + +export function increaseTotalBorrowingFeesPaid(_LUSDFee: BigInt): void { + let global = getGlobal(); + global.totalBorrowingFeesPaid = global.totalBorrowingFeesPaid.plus(decimalize(_LUSDFee)); global.save(); } diff --git a/packages/subgraph/src/entities/LqtyStake.ts b/packages/subgraph/src/entities/LqtyStake.ts index fedeafaa6..07012427b 100644 --- a/packages/subgraph/src/entities/LqtyStake.ts +++ b/packages/subgraph/src/entities/LqtyStake.ts @@ -9,7 +9,7 @@ import { getUser } from "./User"; import { handleLQTYStakeChange } from "./Global"; function startLQTYStakeChange(event: ethereum.Event): LqtyStakeChange { - let sequenceNumber = beginChange(event); + let sequenceNumber = beginChange(); let stakeChange = new LqtyStakeChange(sequenceNumber.toString()); stakeChange.issuanceGain = DECIMAL_ZERO; stakeChange.redemptionGain = DECIMAL_ZERO; diff --git a/packages/subgraph/src/entities/Redemption.ts b/packages/subgraph/src/entities/Redemption.ts index d62a4e39d..65fff0903 100644 --- a/packages/subgraph/src/entities/Redemption.ts +++ b/packages/subgraph/src/entities/Redemption.ts @@ -23,6 +23,7 @@ export function getCurrentRedemption(event: ethereum.Event): Redemption { newRedemption.tokensActuallyRedeemed = DECIMAL_ZERO; newRedemption.collateralRedeemed = DECIMAL_ZERO; newRedemption.partial = false; + newRedemption.fee = DECIMAL_ZERO; newRedemption.save(); let global = getGlobal(); @@ -39,16 +40,21 @@ export function finishCurrentRedemption( event: ethereum.Event, _attemptedLUSDAmount: BigInt, _actualLUSDAmount: BigInt, - _ETHSent: BigInt + _ETHSent: BigInt, + _ETHFee: BigInt ): void { + let fee = decimalize(_ETHFee); + let currentRedemption = getCurrentRedemption(event); currentRedemption.tokensAttemptedToRedeem = decimalize(_attemptedLUSDAmount); currentRedemption.tokensActuallyRedeemed = decimalize(_actualLUSDAmount); currentRedemption.collateralRedeemed = decimalize(_ETHSent); currentRedemption.partial = _actualLUSDAmount < _attemptedLUSDAmount; + currentRedemption.fee = fee; currentRedemption.save(); let global = getGlobal(); global.currentRedemption = null; + global.totalRedemptionFeesPaid = global.totalRedemptionFeesPaid.plus(fee); global.save(); } diff --git a/packages/subgraph/src/entities/StabilityDeposit.ts b/packages/subgraph/src/entities/StabilityDeposit.ts index 83d0acb1f..9b401685f 100644 --- a/packages/subgraph/src/entities/StabilityDeposit.ts +++ b/packages/subgraph/src/entities/StabilityDeposit.ts @@ -20,7 +20,6 @@ function getStabilityDeposit(_user: Address): StabilityDeposit { newStabilityDeposit.owner = owner.id; newStabilityDeposit.depositedAmount = DECIMAL_ZERO; - owner.stabilityDeposit = newStabilityDeposit.id; owner.save(); @@ -29,7 +28,7 @@ function getStabilityDeposit(_user: Address): StabilityDeposit { } function createStabilityDepositChange(event: ethereum.Event): StabilityDepositChange { - let sequenceNumber = beginChange(event); + let sequenceNumber = beginChange(); let stabilityDepositChange = new StabilityDepositChange(sequenceNumber.toString()); initChange(stabilityDepositChange, event, sequenceNumber); @@ -76,6 +75,7 @@ export function updateStabilityDeposit( ): void { let stabilityDeposit = getStabilityDeposit(_user); let newDepositedAmount = decimalize(_amount); + let owner = getUser(_user); if (newDepositedAmount == stabilityDeposit.depositedAmount) { // Don't create a StabilityDepositChange when there's no change... duh. @@ -83,6 +83,12 @@ export function updateStabilityDeposit( return; } + if (owner.frontend != stabilityDeposit.frontend) { + // FrontEndTagSet is emitted just before UserDepositChanged event + // FrontEndTagSet sets the owner.frontend, so we can use that + stabilityDeposit.frontend = owner.frontend; + } + updateStabilityDepositByOperation( event, stabilityDeposit, @@ -106,7 +112,7 @@ export function withdrawCollateralGainFromStabilityDeposit( let stabilityDeposit = getStabilityDeposit(_user) as StabilityDeposit; let depositLoss = decimalize(_LUSDLoss); - let newDepositedAmount = stabilityDeposit.depositedAmount - depositLoss; + let newDepositedAmount = stabilityDeposit.depositedAmount.minus(depositLoss); updateStabilityDepositByOperation( event, diff --git a/packages/subgraph/src/entities/SystemState.ts b/packages/subgraph/src/entities/SystemState.ts index 78b8bea59..b31b87406 100644 --- a/packages/subgraph/src/entities/SystemState.ts +++ b/packages/subgraph/src/entities/SystemState.ts @@ -1,4 +1,4 @@ -import { ethereum, BigDecimal } from "@graphprotocol/graph-ts"; +import { ethereum, BigDecimal, BigInt } from "@graphprotocol/graph-ts"; import { SystemState, @@ -8,7 +8,13 @@ import { CollSurplusChange } from "../../generated/schema"; -import { decimalize, DECIMAL_INITIAL_PRICE, DECIMAL_ZERO, DECIMAL_ONE } from "../utils/bignumbers"; +import { + decimalize, + DECIMAL_ZERO, + DECIMAL_ONE, + DECIMAL_COLLATERAL_GAS_COMPENSATION_DIVISOR, + DECIMAL_PRECISION +} from "../utils/bignumbers"; import { calculateCollateralRatio } from "../utils/collateralRatio"; import { @@ -18,9 +24,7 @@ import { isRecoveryModeLiquidation } from "../types/TroveOperation"; -import { getPrice } from "../calls/PriceFeed"; - -import { getGlobal, getSystemStateSequenceNumber, getPriceFeedAddress } from "./Global"; +import { getGlobal, getSystemStateSequenceNumber } from "./Global"; import { beginChange, initChange, finishChange } from "./Change"; export function getCurrentSystemState(): SystemState { @@ -32,7 +36,6 @@ export function getCurrentSystemState(): SystemState { let newSystemState = new SystemState(sequenceNumber.toString()); newSystemState.sequenceNumber = sequenceNumber; - newSystemState.price = DECIMAL_INITIAL_PRICE; newSystemState.totalCollateral = DECIMAL_ZERO; newSystemState.totalDebt = DECIMAL_ZERO; newSystemState.tokensInStabilityPool = DECIMAL_ZERO; @@ -60,16 +63,17 @@ export function bumpSystemState(systemState: SystemState): void { global.save(); } -// To make sure this returns the latest price, a Transaction entity should be created for the -// triggering event beforehand, either directly or indirectly through creating a Change entity export function getCurrentPrice(): BigDecimal { let currentSystemState = getCurrentSystemState(); - return currentSystemState.price; + // The backend always starts with fetching the latest price, so LastGoodPriceUpdated will be + // the first event emitted. We can be sure that by the time we need the price, it will have been + // initialized. + return currentSystemState.price!; } function createPriceChange(event: ethereum.Event): PriceChange { - let sequenceNumber = beginChange(event); + let sequenceNumber = beginChange(); let priceChange = new PriceChange(sequenceNumber.toString()); initChange(priceChange, event, sequenceNumber); @@ -82,13 +86,23 @@ function finishPriceChange(priceChange: PriceChange): void { } /* - * Call the PriceFeed to get the latest price, and update the SystemState through a PriceChange - * if it has changed. + * Update SystemState through a PriceChange if _lastGoodPrice is different from the last recorded + * price. */ -export function checkPrice(event: ethereum.Event): void { +export function updatePrice(event: ethereum.Event, _lastGoodPrice: BigInt): void { let systemState = getCurrentSystemState(); - let oldPrice = systemState.price; - let newPrice = decimalize(getPrice(getPriceFeedAddress())); + let oldPriceOrNull = systemState.price; + let newPrice = decimalize(_lastGoodPrice); + + if (oldPriceOrNull == null) { + // On first price event, just initialize price in the current system state without creating + // a price change. + systemState.price = newPrice; + systemState.save(); + return; + } + + let oldPrice = oldPriceOrNull!; if (newPrice != oldPrice) { let priceChange = createPriceChange(event); @@ -96,7 +110,7 @@ export function checkPrice(event: ethereum.Event): void { systemState.price = newPrice; bumpSystemState(systemState); - priceChange.priceChange = newPrice - oldPrice; + priceChange.priceChange = newPrice.minus(oldPrice); finishPriceChange(priceChange); } } @@ -108,14 +122,18 @@ function tryToOffsetWithTokensFromStabilityPool( ): void { if (debtToLiquidate <= systemState.tokensInStabilityPool) { // Completely offset - systemState.totalCollateral -= collateralToLiquidate; - systemState.totalDebt -= debtToLiquidate; - systemState.tokensInStabilityPool -= debtToLiquidate; + systemState.totalCollateral = systemState.totalCollateral.minus(collateralToLiquidate); + systemState.totalDebt = systemState.totalDebt.minus(debtToLiquidate); + systemState.tokensInStabilityPool = systemState.tokensInStabilityPool.minus(debtToLiquidate); } else if (systemState.tokensInStabilityPool > DECIMAL_ZERO) { // Partially offset, emptying the pool - systemState.totalCollateral -= - (collateralToLiquidate * systemState.tokensInStabilityPool) / debtToLiquidate; - systemState.totalDebt -= systemState.tokensInStabilityPool; + systemState.totalCollateral = systemState.totalCollateral.minus( + collateralToLiquidate + .times(systemState.tokensInStabilityPool) + .div(debtToLiquidate) + .truncate(DECIMAL_PRECISION) + ); + systemState.totalDebt = systemState.totalDebt.minus(systemState.tokensInStabilityPool); systemState.tokensInStabilityPool = DECIMAL_ZERO; } else { // Empty pool @@ -127,15 +145,22 @@ export function updateSystemStateByTroveChange(troveChange: TroveChange): void { let operation = troveChange.troveOperation; if (isBorrowerOperation(operation) || isRedemption(operation)) { - systemState.totalCollateral += troveChange.collateralChange; - systemState.totalDebt += troveChange.debtChange; + systemState.totalCollateral = systemState.totalCollateral.plus(troveChange.collateralChange); + systemState.totalDebt = systemState.totalDebt.plus(troveChange.debtChange); } else if (isLiquidation(operation)) { - // TODO gas compensation + let collateral = troveChange.collateralBefore; + let debt = troveChange.debtBefore; + let collateralGasCompensation = collateral + .div(DECIMAL_COLLATERAL_GAS_COMPENSATION_DIVISOR) + .truncate(DECIMAL_PRECISION); + + systemState.totalCollateral = systemState.totalCollateral.minus(collateralGasCompensation); + if (!isRecoveryModeLiquidation(operation) || troveChange.collateralRatioBefore > DECIMAL_ONE) { tryToOffsetWithTokensFromStabilityPool( systemState, - -troveChange.collateralChange, - -troveChange.debtChange + collateral.minus(collateralGasCompensation), + debt ); } } @@ -143,7 +168,7 @@ export function updateSystemStateByTroveChange(troveChange: TroveChange): void { systemState.totalCollateralRatio = calculateCollateralRatio( systemState.totalCollateral, systemState.totalDebt, - systemState.price + systemState.price! // A trove change is guaranteed to be preceeded by a price update ); bumpSystemState(systemState); @@ -156,7 +181,9 @@ export function updateSystemStateByStabilityDepositChange( let operation = stabilityDepositChange.stabilityDepositOperation; if (operation == "depositTokens" || operation == "withdrawTokens") { - systemState.tokensInStabilityPool += stabilityDepositChange.depositedAmountChange; + systemState.tokensInStabilityPool = systemState.tokensInStabilityPool.plus( + stabilityDepositChange.depositedAmountChange + ); } bumpSystemState(systemState); @@ -165,7 +192,9 @@ export function updateSystemStateByStabilityDepositChange( export function updateSystemStateByCollSurplusChange(collSurplusChange: CollSurplusChange): void { let systemState = getCurrentSystemState(); - systemState.collSurplusPoolBalance += collSurplusChange.collSurplusChange; + systemState.collSurplusPoolBalance = systemState.collSurplusPoolBalance.plus( + collSurplusChange.collSurplusChange + ); bumpSystemState(systemState); } diff --git a/packages/subgraph/src/entities/Token.ts b/packages/subgraph/src/entities/Token.ts new file mode 100644 index 000000000..9b0e81bc7 --- /dev/null +++ b/packages/subgraph/src/entities/Token.ts @@ -0,0 +1,33 @@ +import { Address } from "@graphprotocol/graph-ts"; + +import { Token } from "../../generated/schema"; +import { ERC20 } from "../../generated/LUSDToken/ERC20"; +import { BIGINT_ZERO } from "../utils/bignumbers"; + +function createToken(address: Address, name: string, symbol: string): Token { + let id = address.toHexString(); + let token = new Token(id); + token.name = name; + token.symbol = symbol; + token.totalSupply = BIGINT_ZERO; + token.save(); + + return token; +} + +export function getToken(_token: Address): Token { + let id = _token.toHexString(); + let tokenOrNull = Token.load(id); + + if (tokenOrNull != null) { + return tokenOrNull as Token; + } else { + // Bind the contract to the address that emitted the event + let contract = ERC20.bind(_token); + + // Access state variables and functions by calling them + let name = contract.name(); + let symbol = contract.symbol(); + return createToken(_token, name, symbol); + } +} diff --git a/packages/subgraph/src/entities/TokenAllowance.ts b/packages/subgraph/src/entities/TokenAllowance.ts new file mode 100644 index 000000000..4edbb7515 --- /dev/null +++ b/packages/subgraph/src/entities/TokenAllowance.ts @@ -0,0 +1,45 @@ +import { ethereum, Address, BigInt } from "@graphprotocol/graph-ts"; + +import { BIGINT_ZERO } from "../utils/bignumbers"; + +import { TokenAllowance } from "../../generated/schema"; + +import { getUser } from "./User"; + +export function getTokenAllowance( + _token: Address, + _owner: Address, + _spender: Address +): TokenAllowance { + let id = _token.toHexString() + "-" + _owner.toHexString() + "-" + _spender.toHexString(); + let owner = getUser(_owner); + let spender = getUser(_spender); + let allowanceOrNull = TokenAllowance.load(id); + + if (allowanceOrNull != null) { + return allowanceOrNull as TokenAllowance; + } else { + let newAllowance = new TokenAllowance(id); + + newAllowance.token = _token.toHexString(); + newAllowance.owner = owner.id; + newAllowance.spender = spender.id; + newAllowance.value = BIGINT_ZERO; + newAllowance.save(); + + return newAllowance; + } +} + +export function updateAllowance( + _event: ethereum.Event, + _owner: Address, + _spender: Address, + _value: BigInt +): void { + let tokenAddress = _event.address; + + let tokenAllowance = getTokenAllowance(tokenAddress, _owner, _spender); + tokenAllowance.value = _value; + tokenAllowance.save(); +} diff --git a/packages/subgraph/src/entities/TokenBalance.ts b/packages/subgraph/src/entities/TokenBalance.ts new file mode 100644 index 000000000..46d559689 --- /dev/null +++ b/packages/subgraph/src/entities/TokenBalance.ts @@ -0,0 +1,61 @@ +import { ethereum, Address, BigInt } from "@graphprotocol/graph-ts"; + +import { BIGINT_ZERO } from "../utils/bignumbers"; +import { ZERO_ADDRESS } from "../utils/constants"; + +import { TokenBalance } from "../../generated/schema"; + +import { getUser } from "./User"; +import { getToken } from "./Token"; + +export function getTokenBalance(_token: Address, _owner: Address): TokenBalance { + let id = _token.toHexString() + "-" + _owner.toHexString(); + let user = getUser(_owner); + let balanceOrNull = TokenBalance.load(id); + + if (balanceOrNull != null) { + return balanceOrNull as TokenBalance; + } else { + let newBalance = new TokenBalance(id); + + newBalance.token = _token.toHexString(); + newBalance.owner = user.id; + newBalance.balance = BIGINT_ZERO; + newBalance.save(); + + return newBalance; + } +} + +export function updateBalance( + _event: ethereum.Event, + _from: Address, + _to: Address, + _value: BigInt +): void { + let tokenAddress = _event.address; + let token = getToken(tokenAddress); + + if (_from.toHexString() == ZERO_ADDRESS) { + // mint + // increase total supply + token.totalSupply = token.totalSupply.plus(_value); + token.save(); + } else { + // decrease from balance + let tokenBalanceFrom = getTokenBalance(tokenAddress, _from); + tokenBalanceFrom.balance = tokenBalanceFrom.balance.minus(_value); + tokenBalanceFrom.save(); + } + if (_to.toHexString() == ZERO_ADDRESS) { + // burn + // decrease total supply + token.totalSupply = token.totalSupply.minus(_value); + token.save(); + } else { + // increase to balance + let tokenBalanceTo = getTokenBalance(tokenAddress, _to); + tokenBalanceTo.balance = tokenBalanceTo.balance.plus(_value); + tokenBalanceTo.save(); + } +} diff --git a/packages/subgraph/src/entities/Transaction.ts b/packages/subgraph/src/entities/Transaction.ts index 90fb807a0..3817c1a9e 100644 --- a/packages/subgraph/src/entities/Transaction.ts +++ b/packages/subgraph/src/entities/Transaction.ts @@ -3,14 +3,10 @@ import { ethereum } from "@graphprotocol/graph-ts"; import { Transaction } from "../../generated/schema"; import { getTransactionSequenceNumber } from "./Global"; -import { checkPrice } from "./SystemState"; /* * Return existing entity for the transaction that emitted this event, or create and return a new * one if none exists yet. - * - * When creating a new Transaction, it checks if the price has changed. This may have the side - * effect of creating new SystemState and PriceChange entities. */ export function getTransaction(event: ethereum.Event): Transaction { let transactionId = event.transaction.hash.toHex(); @@ -26,8 +22,6 @@ export function getTransaction(event: ethereum.Event): Transaction { newTransaction.timestamp = event.block.timestamp.toI32(); newTransaction.save(); - checkPrice(event); - return newTransaction; } } diff --git a/packages/subgraph/src/entities/Trove.ts b/packages/subgraph/src/entities/Trove.ts index fcee97902..916b88dc5 100644 --- a/packages/subgraph/src/entities/Trove.ts +++ b/packages/subgraph/src/entities/Trove.ts @@ -14,7 +14,9 @@ import { increaseNumberOfLiquidatedTroves, increaseNumberOfRedeemedTroves, increaseNumberOfOpenTroves, - increaseNumberOfTrovesClosedByOwner + increaseNumberOfTrovesClosedByOwner, + getLastChangeSequenceNumber, + getGlobal } from "./Global"; import { beginChange, initChange, finishChange } from "./Change"; import { getCurrentPrice, updateSystemStateByTroveChange } from "./SystemState"; @@ -72,7 +74,7 @@ function setTroveStatus(trove: Trove, status: string): void { } function createTroveChange(event: ethereum.Event): TroveChange { - let sequenceNumber = beginChange(event); + let sequenceNumber = beginChange(); let troveChange = new TroveChange(sequenceNumber.toString()); initChange(troveChange, event, sequenceNumber); @@ -90,10 +92,9 @@ export function updateTrove( _borrower: Address, _coll: BigInt, _debt: BigInt, - stake: BigInt, - snapshotETH: BigInt, - snapshotLUSDDebt: BigInt + stake: BigInt ): void { + let global = getGlobal(); let trove = getTrove(_borrower); let newCollateral = decimalize(_coll); let newDebt = decimalize(_debt); @@ -119,8 +120,8 @@ export function updateTrove( troveChange.debtAfter = trove.debt; troveChange.collateralRatioAfter = calculateCollateralRatio(trove.collateral, trove.debt, price); - troveChange.collateralChange = troveChange.collateralAfter - troveChange.collateralBefore; - troveChange.debtChange = troveChange.debtAfter - troveChange.debtBefore; + troveChange.collateralChange = troveChange.collateralAfter.minus(troveChange.collateralBefore); + troveChange.debtChange = troveChange.debtAfter.minus(troveChange.debtBefore); if (isLiquidation(operation)) { let currentLiquidation = getCurrentLiquidation(event); @@ -138,12 +139,18 @@ export function updateTrove( trove.rawCollateral = _coll; trove.rawDebt = _debt; trove.rawStake = stake; - trove.rawSnapshotOfTotalRedistributedCollateral = snapshotETH; - trove.rawSnapshotOfTotalRedistributedDebt = snapshotLUSDDebt; if (stake != BIGINT_ZERO) { - trove.collateralRatioSortKey = (_debt * BIGINT_SCALING_FACTOR) / stake - snapshotLUSDDebt; + trove.rawSnapshotOfTotalRedistributedCollateral = global.rawTotalRedistributedCollateral; + trove.rawSnapshotOfTotalRedistributedDebt = global.rawTotalRedistributedDebt; + + trove.collateralRatioSortKey = _debt + .times(BIGINT_SCALING_FACTOR) + .div(stake) + .minus(global.rawTotalRedistributedDebt); } else { + trove.rawSnapshotOfTotalRedistributedCollateral = BIGINT_ZERO; + trove.rawSnapshotOfTotalRedistributedDebt = BIGINT_ZERO; trove.collateralRatioSortKey = null; } @@ -161,3 +168,38 @@ export function updateTrove( trove.save(); } + +export function setBorrowingFeeOfLastTroveChange(_LUSDFee: BigInt): void { + let lastChangeSequenceNumber = getLastChangeSequenceNumber(); + + let lastTroveChange = TroveChange.load(lastChangeSequenceNumber.toString()); + lastTroveChange.borrowingFee = decimalize(_LUSDFee); + lastTroveChange.save(); +} + +export function applyRedistributionToTroveBeforeLiquidation( + event: ethereum.Event, + _borrower: Address +): void { + let global = getGlobal(); + let trove = getTrove(_borrower); + + let redistributedCollateral = global.rawTotalRedistributedCollateral + .minus(trove.rawSnapshotOfTotalRedistributedCollateral) + .times(trove.rawStake) + .div(BIGINT_SCALING_FACTOR); + + let redistributedDebt = global.rawTotalRedistributedDebt + .minus(trove.rawSnapshotOfTotalRedistributedDebt) + .times(trove.rawStake) + .div(BIGINT_SCALING_FACTOR); + + updateTrove( + event, + "accrueRewards", + _borrower, + trove.rawCollateral.plus(redistributedCollateral), + trove.rawDebt.plus(redistributedDebt), + BIGINT_ZERO // No need to calculate new stake, because we know the Trove is being liquidated + ); +} diff --git a/packages/subgraph/src/entities/User.ts b/packages/subgraph/src/entities/User.ts index 7fc7c3e25..007aad5b5 100644 --- a/packages/subgraph/src/entities/User.ts +++ b/packages/subgraph/src/entities/User.ts @@ -23,7 +23,7 @@ export function getUser(_user: Address): User { } function createCollSurplusChange(event: ethereum.Event): CollSurplusChange { - let sequenceNumber = beginChange(event); + let sequenceNumber = beginChange(); let collSurplusChange = new CollSurplusChange(sequenceNumber.toString()); initChange(collSurplusChange, event, sequenceNumber); @@ -52,8 +52,9 @@ export function updateUserClaimColl( collSurplusChange.collSurplusBefore = user.collSurplus; collSurplusChange.collSurplusAfter = newCollSurplus; - collSurplusChange.collSurplusChange = - collSurplusChange.collSurplusAfter - collSurplusChange.collSurplusBefore; + collSurplusChange.collSurplusChange = collSurplusChange.collSurplusAfter.minus( + collSurplusChange.collSurplusBefore + ); updateSystemStateByCollSurplusChange(collSurplusChange); finishCollSurplusChange(collSurplusChange); diff --git a/packages/subgraph/src/mappings/BorrowerOperations.ts b/packages/subgraph/src/mappings/BorrowerOperations.ts index 9c7938f96..256f4e4b2 100644 --- a/packages/subgraph/src/mappings/BorrowerOperations.ts +++ b/packages/subgraph/src/mappings/BorrowerOperations.ts @@ -1,27 +1,25 @@ -import { TroveManager } from "../../generated/TroveManager/TroveManager"; import { - BorrowerOperations, - TroveUpdated -} from "../../generated/templates/BorrowerOperations/BorrowerOperations"; + TroveUpdated, + LUSDBorrowingFeePaid +} from "../../generated/BorrowerOperations/BorrowerOperations"; import { getTroveOperationFromBorrowerOperation } from "../types/TroveOperation"; -import { updateTrove } from "../entities/Trove"; +import { setBorrowingFeeOfLastTroveChange, updateTrove } from "../entities/Trove"; +import { increaseTotalBorrowingFeesPaid } from "../entities/Global"; export function handleTroveUpdated(event: TroveUpdated): void { - let borrowerOperations = BorrowerOperations.bind(event.address); - let troveManagerAddress = borrowerOperations.troveManager(); - let troveManager = TroveManager.bind(troveManagerAddress); - let snapshots = troveManager.rewardSnapshots(event.params._borrower); - updateTrove( event, getTroveOperationFromBorrowerOperation(event.params.operation), event.params._borrower, event.params._coll, event.params._debt, - event.params.stake, - snapshots.value0, - snapshots.value1 + event.params.stake ); } + +export function handleLUSDBorrowingFeePaid(event: LUSDBorrowingFeePaid): void { + setBorrowingFeeOfLastTroveChange(event.params._LUSDFee); + increaseTotalBorrowingFeesPaid(event.params._LUSDFee); +} diff --git a/packages/subgraph/src/mappings/CollSurplusPool.ts b/packages/subgraph/src/mappings/CollSurplusPool.ts index b9e77d75d..3571eda98 100644 --- a/packages/subgraph/src/mappings/CollSurplusPool.ts +++ b/packages/subgraph/src/mappings/CollSurplusPool.ts @@ -1,6 +1,4 @@ -import { - CollBalanceUpdated -} from "../../generated/templates/CollSurplusPool/CollSurplusPool"; +import { CollBalanceUpdated } from "../../generated/CollSurplusPool/CollSurplusPool"; import { updateUserClaimColl } from "../entities/User"; diff --git a/packages/subgraph/src/mappings/LqtyStake.ts b/packages/subgraph/src/mappings/LqtyStake.ts index 1b73443a4..f10402ea9 100644 --- a/packages/subgraph/src/mappings/LqtyStake.ts +++ b/packages/subgraph/src/mappings/LqtyStake.ts @@ -1,7 +1,4 @@ -import { - StakeChanged, - StakingGainsWithdrawn -} from "../../generated/templates/LQTYStaking/LQTYStaking"; +import { StakeChanged, StakingGainsWithdrawn } from "../../generated/LQTYStaking/LQTYStaking"; import { updateStake, withdrawStakeGains } from "../entities/LqtyStake"; diff --git a/packages/subgraph/src/mappings/PriceFeed.ts b/packages/subgraph/src/mappings/PriceFeed.ts new file mode 100644 index 000000000..f1a73a921 --- /dev/null +++ b/packages/subgraph/src/mappings/PriceFeed.ts @@ -0,0 +1,7 @@ +import { LastGoodPriceUpdated } from "../../generated/PriceFeed/PriceFeed"; + +import { updatePrice } from "../entities/SystemState"; + +export function handleLastGoodPriceUpdated(event: LastGoodPriceUpdated): void { + updatePrice(event, event.params._lastGoodPrice); +} diff --git a/packages/subgraph/src/mappings/StabilityPool.ts b/packages/subgraph/src/mappings/StabilityPool.ts index 07b5a1814..0a422603a 100644 --- a/packages/subgraph/src/mappings/StabilityPool.ts +++ b/packages/subgraph/src/mappings/StabilityPool.ts @@ -1,22 +1,66 @@ +import { BigInt } from "@graphprotocol/graph-ts"; + import { UserDepositChanged, - ETHGainWithdrawn -} from "../../generated/templates/StabilityPool/StabilityPool"; + ETHGainWithdrawn, + FrontEndRegistered, + FrontEndTagSet +} from "../../generated/StabilityPool/StabilityPool"; + +import { BIGINT_ZERO } from "../utils/bignumbers"; + +import { getGlobal } from "../entities/Global"; import { updateStabilityDeposit, withdrawCollateralGainFromStabilityDeposit } from "../entities/StabilityDeposit"; +import { registerFrontend, assignFrontendToDepositor } from "../entities/Frontend"; + +// Read the value of tmpDepositUpdate from the Global entity, and replace it with: +// - null, if it wasn't null +// - valueToSetIfNull if it was null +// +// Returns the value of tmpDepositUpdate before the swap. +function swapTmpDepositUpdate(valueToSetIfNull: BigInt): BigInt | null { + let global = getGlobal(); + + let tmpDepositUpdate = global.tmpDepositUpdate; + global.tmpDepositUpdate = tmpDepositUpdate == null ? valueToSetIfNull : null; + global.save(); + + return tmpDepositUpdate; +} export function handleUserDepositChanged(event: UserDepositChanged): void { - updateStabilityDeposit(event, event.params._depositor, event.params._newDeposit); + let ethGainWithdrawn = swapTmpDepositUpdate(event.params._newDeposit); + + if (ethGainWithdrawn != null) { + updateStabilityDeposit(event, event.params._depositor, event.params._newDeposit); + } } export function handleETHGainWithdrawn(event: ETHGainWithdrawn): void { + // Leave a non-null dummy value to signal to handleUserDepositChanged() + // that ETH gains have been withdrawn + let depositUpdate = swapTmpDepositUpdate(BIGINT_ZERO); + withdrawCollateralGainFromStabilityDeposit( event, event.params._depositor, event.params._ETH, event.params._LUSDLoss ); + + if (depositUpdate != null) { + updateStabilityDeposit(event, event.params._depositor, depositUpdate as BigInt); + } +} + +export function handleFrontendRegistered(event: FrontEndRegistered): void { + registerFrontend(event.params._frontEnd, event.params._kickbackRate); +} + +export function handleFrontendTagSet(event: FrontEndTagSet): void { + assignFrontendToDepositor(event.params._depositor, event.params._frontEnd); } diff --git a/packages/subgraph/src/mappings/Token.ts b/packages/subgraph/src/mappings/Token.ts new file mode 100644 index 000000000..5b081f32e --- /dev/null +++ b/packages/subgraph/src/mappings/Token.ts @@ -0,0 +1,12 @@ +import { Transfer, Approval } from "../../generated/LUSDToken/ERC20"; + +import { updateBalance } from "../entities/TokenBalance"; +import { updateAllowance } from "../entities/TokenAllowance"; + +export function handleTokenTransfer(event: Transfer): void { + updateBalance(event, event.params.from, event.params.to, event.params.value); +} + +export function handleTokenApproval(event: Approval): void { + updateAllowance(event, event.params.owner, event.params.spender, event.params.value); +} diff --git a/packages/subgraph/src/mappings/TroveManager.ts b/packages/subgraph/src/mappings/TroveManager.ts index ba6c8550f..7683e55a3 100644 --- a/packages/subgraph/src/mappings/TroveManager.ts +++ b/packages/subgraph/src/mappings/TroveManager.ts @@ -1,96 +1,36 @@ import { - TroveManager, TroveUpdated, TroveLiquidated, Liquidation, Redemption, - BorrowerOperationsAddressChanged, - StabilityPoolAddressChanged, - CollSurplusPoolAddressChanged, - PriceFeedAddressChanged, - LQTYStakingAddressChanged + LTermsUpdated } from "../../generated/TroveManager/TroveManager"; -import { - BorrowerOperations, - StabilityPool, - CollSurplusPool, - LQTYStaking -} from "../../generated/templates"; - -import { BIGINT_ZERO } from "../utils/bignumbers"; import { getTroveOperationFromTroveManagerOperation } from "../types/TroveOperation"; import { finishCurrentLiquidation } from "../entities/Liquidation"; import { finishCurrentRedemption } from "../entities/Redemption"; -import { updateTrove } from "../entities/Trove"; -import { updatePriceFeedAddress, updateTotalRedistributed } from "../entities/Global"; - -export function handleBorrowerOperationsAddressChanged( - event: BorrowerOperationsAddressChanged -): void { - BorrowerOperations.create(event.params._newBorrowerOperationsAddress); -} - -export function handleStabilityPoolAddressChanged(event: StabilityPoolAddressChanged): void { - StabilityPool.create(event.params._stabilityPoolAddress); -} - -export function handleCollSurplusPoolAddressChanged(event: CollSurplusPoolAddressChanged): void { - CollSurplusPool.create(event.params._collSurplusPoolAddress); -} - -export function handleLQTYStakingAddressChanged(event: LQTYStakingAddressChanged): void { - LQTYStaking.create(event.params._lqtyStakingAddress); -} - -export function handlePriceFeedAddressChanged(event: PriceFeedAddressChanged): void { - updatePriceFeedAddress(event.params._newPriceFeedAddress); -} +import { applyRedistributionToTroveBeforeLiquidation, updateTrove } from "../entities/Trove"; +import { updateTotalRedistributed } from "../entities/Global"; export function handleTroveUpdated(event: TroveUpdated): void { - let troveManager = TroveManager.bind(event.address); - let snapshots = troveManager.rewardSnapshots(event.params._borrower); - updateTrove( event, getTroveOperationFromTroveManagerOperation(event.params._operation), event.params._borrower, event.params._coll, event.params._debt, - event.params._stake, - snapshots.value0, - snapshots.value1 + event.params._stake ); } export function handleTroveLiquidated(event: TroveLiquidated): void { - updateTrove( - event, - "accrueRewards", - event.params._borrower, - event.params._coll, - event.params._debt, - BIGINT_ZERO, - BIGINT_ZERO, - BIGINT_ZERO - ); - - updateTrove( - event, - getTroveOperationFromTroveManagerOperation(event.params._operation), - event.params._borrower, - BIGINT_ZERO, - BIGINT_ZERO, - BIGINT_ZERO, - BIGINT_ZERO, - BIGINT_ZERO - ); + applyRedistributionToTroveBeforeLiquidation(event, event.params._borrower); + // No need to close the Trove yet, as TroveLiquidated will be followed by a TroveUpdated event + // that sets collateral and debt to 0. } export function handleLiquidation(event: Liquidation): void { - let troveManager = TroveManager.bind(event.address); - finishCurrentLiquidation( event, event.params._liquidatedColl, @@ -98,8 +38,6 @@ export function handleLiquidation(event: Liquidation): void { event.params._collGasCompensation, event.params._LUSDGasCompensation ); - - updateTotalRedistributed(troveManager.L_ETH(), troveManager.L_LUSDDebt()); } export function handleRedemption(event: Redemption): void { @@ -107,6 +45,11 @@ export function handleRedemption(event: Redemption): void { event, event.params._attemptedLUSDAmount, event.params._actualLUSDAmount, - event.params._ETHSent + event.params._ETHSent, + event.params._ETHFee ); } + +export function handleLTermsUpdated(event: LTermsUpdated): void { + updateTotalRedistributed(event.params._L_ETH, event.params._L_LUSDDebt); +} diff --git a/packages/subgraph/src/utils/bignumbers.ts b/packages/subgraph/src/utils/bignumbers.ts index 3042e1751..14cab324c 100644 --- a/packages/subgraph/src/utils/bignumbers.ts +++ b/packages/subgraph/src/utils/bignumbers.ts @@ -1,5 +1,7 @@ import { Bytes, BigInt, BigDecimal } from "@graphprotocol/graph-ts"; +export let DECIMAL_PRECISION = 18; + // E.g. 1.5 is represented as 1.5 * 10^18, where 10^18 is called the scaling factor export let DECIMAL_SCALING_FACTOR = BigDecimal.fromString("1000000000000000000"); export let BIGINT_SCALING_FACTOR = BigInt.fromI32(10).pow(18); @@ -7,13 +9,13 @@ export let BIGINT_SCALING_FACTOR = BigInt.fromI32(10).pow(18); export let DECIMAL_ZERO = BigDecimal.fromString("0"); export let DECIMAL_ONE = BigDecimal.fromString("1"); +export let DECIMAL_COLLATERAL_GAS_COMPENSATION_DIVISOR = BigDecimal.fromString("200"); + export let BIGINT_ZERO = BigInt.fromI32(0); export let BIGINT_MAX_UINT256 = BigInt.fromUnsignedBytes( Bytes.fromHexString("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") as Bytes ); -export let DECIMAL_INITIAL_PRICE = BigDecimal.fromString("200"); - export function decimalize(bigInt: BigInt): BigDecimal { return bigInt.divDecimal(DECIMAL_SCALING_FACTOR); } diff --git a/packages/subgraph/src/utils/collateralRatio.ts b/packages/subgraph/src/utils/collateralRatio.ts index 94b540ebf..b82eb0419 100644 --- a/packages/subgraph/src/utils/collateralRatio.ts +++ b/packages/subgraph/src/utils/collateralRatio.ts @@ -1,6 +1,6 @@ import { BigDecimal } from "@graphprotocol/graph-ts"; -import { DECIMAL_ZERO } from "./bignumbers"; +import { DECIMAL_PRECISION, DECIMAL_ZERO } from "./bignumbers"; export function calculateCollateralRatio( collateral: BigDecimal, @@ -11,5 +11,5 @@ export function calculateCollateralRatio( return null; } - return (collateral * price) / debt; + return collateral.times(price).div(debt).truncate(DECIMAL_PRECISION); } diff --git a/packages/subgraph/src/utils/constants.ts b/packages/subgraph/src/utils/constants.ts new file mode 100644 index 000000000..f78f4aa03 --- /dev/null +++ b/packages/subgraph/src/utils/constants.ts @@ -0,0 +1 @@ +export let ZERO_ADDRESS = '0x' + '0'.repeat(40); diff --git a/packages/subgraph/subgraph.yaml b/packages/subgraph/subgraph.yaml.js similarity index 57% rename from packages/subgraph/subgraph.yaml rename to packages/subgraph/subgraph.yaml.js index 88a5c1c6d..672b9f0dc 100644 --- a/packages/subgraph/subgraph.yaml +++ b/packages/subgraph/subgraph.yaml.js @@ -1,6 +1,20 @@ +const fs = require("fs"); + +const network = process.argv[2] || "mainnet"; +const { addresses, startBlock } = require(`@liquity/lib-ethers/deployments/${network}.json`); + +console.log(`Preparing subgraph manifest for network "${network}"`); + +const yaml = (strings, ...keys) => + strings + .flatMap((string, i) => [string, Array.isArray(keys[i]) ? keys[i].join("") : keys[i]]) + .join("") + .substring(1); // Skip initial newline + +const manifest = yaml` specVersion: 0.0.2 -description: Liquity Decentralized Borrowing Protocol -repository: https://github.com/liquity/subgraph +description: Liquity is a decentralized borrowing protocol offering interest-free liquidity against collateral in Ether. +repository: https://github.com/liquity/dev/tree/main/packages/subgraph schema: file: ./schema.graphql dataSources: @@ -8,10 +22,9 @@ dataSources: kind: ethereum/contract network: mainnet source: - #address: "0x56fcdA0436E5C7a33ee5bfe292f11AC66429Eb5c" - address: "0x6645E03DA2a711f780af7cCE1019Cb9a9135C898" abi: TroveManager - # startBlock: 8110721 + address: "${addresses.troveManager}" + startBlock: ${startBlock} mapping: file: ./src/mappings/TroveManager.ts language: wasm/assemblyscript @@ -21,7 +34,6 @@ dataSources: - Global - User - Transaction - - PriceChange - Trove - TroveChange - Redemption @@ -30,19 +42,7 @@ dataSources: abis: - name: TroveManager file: ../lib-ethers/abi/TroveManager.json - - name: PriceFeed - file: ../lib-ethers/abi/PriceFeed.json eventHandlers: - - event: BorrowerOperationsAddressChanged(address) - handler: handleBorrowerOperationsAddressChanged - - event: StabilityPoolAddressChanged(address) - handler: handleStabilityPoolAddressChanged - - event: CollSurplusPoolAddressChanged(address) - handler: handleCollSurplusPoolAddressChanged - - event: PriceFeedAddressChanged(address) - handler: handlePriceFeedAddressChanged - - event: LQTYStakingAddressChanged(address) - handler: handleLQTYStakingAddressChanged - event: TroveUpdated(indexed address,uint256,uint256,uint256,uint8) handler: handleTroveUpdated - event: TroveLiquidated(indexed address,uint256,uint256,uint8) @@ -51,12 +51,15 @@ dataSources: handler: handleLiquidation - event: Redemption(uint256,uint256,uint256,uint256) handler: handleRedemption -templates: + - event: LTermsUpdated(uint256,uint256) + handler: handleLTermsUpdated - name: BorrowerOperations kind: ethereum/contract network: mainnet source: abi: BorrowerOperations + address: "${addresses.borrowerOperations}" + startBlock: ${startBlock} mapping: file: ./src/mappings/BorrowerOperations.ts language: wasm/assemblyscript @@ -66,25 +69,47 @@ templates: - Global - User - Transaction - - PriceChange - Trove - TroveChange - SystemState abis: - name: BorrowerOperations file: ../lib-ethers/abi/BorrowerOperations.json - - name: TroveManager - file: ../lib-ethers/abi/TroveManager.json - - name: PriceFeed - file: ../lib-ethers/abi/PriceFeed.json eventHandlers: - event: TroveUpdated(indexed address,uint256,uint256,uint256,uint8) handler: handleTroveUpdated + - event: LUSDBorrowingFeePaid(indexed address,uint256) + handler: handleLUSDBorrowingFeePaid + - name: PriceFeed + kind: ethereum/contract + network: mainnet + source: + abi: PriceFeed + address: "${addresses.priceFeed}" + startBlock: ${startBlock} + mapping: + file: ./src/mappings/PriceFeed.ts + language: wasm/assemblyscript + kind: ethereum/events + apiVersion: 0.0.4 + entities: + - Global + - Transaction + - PriceChange + - SystemState + abis: + - name: PriceFeed + file: ../lib-ethers/abi/PriceFeed.json + eventHandlers: + - event: LastGoodPriceUpdated(uint256) + handler: handleLastGoodPriceUpdated - name: StabilityPool kind: ethereum/contract network: mainnet source: abi: StabilityPool + address: "${addresses.stabilityPool}" + startBlock: ${startBlock} mapping: file: ./src/mappings/StabilityPool.ts language: wasm/assemblyscript @@ -94,25 +119,29 @@ templates: - Global - User - Transaction - - PriceChange - StabilityDeposit - StabilityDepositChange - SystemState + - Frontend abis: - name: StabilityPool file: ../lib-ethers/abi/StabilityPool.json - - name: PriceFeed - file: ../lib-ethers/abi/PriceFeed.json eventHandlers: - event: UserDepositChanged(indexed address,uint256) handler: handleUserDepositChanged - event: ETHGainWithdrawn(indexed address,uint256,uint256) handler: handleETHGainWithdrawn + - event: FrontEndRegistered(indexed address,uint256) + handler: handleFrontendRegistered + - event: FrontEndTagSet(indexed address,indexed address) + handler: handleFrontendTagSet - name: CollSurplusPool kind: ethereum/contract network: mainnet source: abi: CollSurplusPool + address: "${addresses.collSurplusPool}" + startBlock: ${startBlock} mapping: file: ./src/mappings/CollSurplusPool.ts language: wasm/assemblyscript @@ -128,8 +157,6 @@ templates: abis: - name: CollSurplusPool file: ../lib-ethers/abi/CollSurplusPool.json - - name: PriceFeed - file: ../lib-ethers/abi/PriceFeed.json eventHandlers: - event: CollBalanceUpdated(indexed address,uint256) handler: handleCollSurplusBalanceUpdated @@ -138,6 +165,8 @@ templates: network: mainnet source: abi: LQTYStaking + address: "${addresses.lqtyStaking}" + startBlock: ${startBlock} mapping: file: ./src/mappings/LqtyStake.ts language: wasm/assemblyscript @@ -152,10 +181,42 @@ templates: abis: - name: LQTYStaking file: ../lib-ethers/abi/LQTYStaking.json - - name: PriceFeed - file: ../lib-ethers/abi/PriceFeed.json eventHandlers: - event: StakeChanged(indexed address,uint256) handler: handleStakeChanged - event: StakingGainsWithdrawn(indexed address,uint256,uint256) handler: handleStakeGainsWithdrawn +${[ + ["LUSDToken", addresses.lusdToken], + ["LQTYToken", addresses.lqtyToken] +].map( + ([name, address]) => yaml` + - name: ${name} + kind: ethereum/contract + network: mainnet + source: + abi: ERC20 + address: "${address}" + startBlock: ${startBlock} + mapping: + file: ./src/mappings/Token.ts + language: wasm/assemblyscript + kind: ethereum/events + apiVersion: 0.0.4 + entities: + - Global + - User + - Transaction + - Token + abis: + - name: ERC20 + file: ./abi/ERC20.json + eventHandlers: + - event: Transfer(indexed address,indexed address,uint256) + handler: handleTokenTransfer + - event: Approval(indexed address,indexed address,uint256) + handler: handleTokenApproval +` +)}`; + +fs.writeFileSync("subgraph.yaml", manifest); diff --git a/papers/whitepaper/Liquity Whitepaper rev. 0.2.tex b/papers/whitepaper/Liquity Whitepaper rev. 0.3.tex similarity index 98% rename from papers/whitepaper/Liquity Whitepaper rev. 0.2.tex rename to papers/whitepaper/Liquity Whitepaper rev. 0.3.tex index f0b0ecb68..7b0302031 100644 --- a/papers/whitepaper/Liquity Whitepaper rev. 0.2.tex +++ b/papers/whitepaper/Liquity Whitepaper rev. 0.3.tex @@ -12,6 +12,7 @@ \usepackage{float} \usepackage{tabularx} \usepackage[table]{colortbl} +\usepackage{tablefootnote} \renewcommand\theadalign{bc} \renewcommand\theadfont{\bfseries} @@ -112,7 +113,7 @@ \subsection{Borrower operations } \textit{Liquidation Reserve}. When a borrower opens a new Trove, an amount of 200 LUSD is reserved and held back by the protocol as a compensation for the gas costs if the Trove needs to be liquidated at some point. The 200 LUSD is added to the Trove's debt, impacting its collateral ratio. When a borrower closes their Trove, the Liquidation Reserve is refunded, i.e. the corresponding 200 LUSD debt on the Trove is cancelled. The borrower thus needs to pay back 200 LUSD less to fully pay off their debt. \\ -\textit{Borrowing Fee}. The protocol charges a one-time Borrowing Fee for the borrowed liquidity. The fee is added to the Trove's debt and is given by a \textbf{base rate} (see 3.3 Redemption mechanism “Redemption fee and base rate”) multiplied by the amount of liquidity drawn by the borrower. The minimum Borrowing Fee is 0.5\%, and the maximum is 5\%. \\ +\textit{Borrowing Fee}. The protocol charges a one-time Borrowing Fee for the borrowed liquidity. The fee is added to the Trove's debt and is given by a \textbf{base rate} + 0.5\% (see 3.3 Redemption mechanism “Redemption fee and base rate”) multiplied by the amount of liquidity drawn by the borrower. The minimum Borrowing Fee is 0.5\%, and the maximum is 5\%. \\ \begin{tcolorbox} \textbf{Example}\\ @@ -163,7 +164,7 @@ \subsection{Redemption mechanism} The system first applies the decay rate to the current base rate: $$b(t):=b(t-1)\times\delta^{\triangle t}=0.014\times0.94^2=0.01237$$ -It then increases the base rate given the redeemed amount ($\alpha= 0.5$): +It then increases the base rate in proportion to the fraction of total supply redeemed ($\alpha= 0.5$): $$b(t):=b(t-1)+0.5\times\frac{m}{n}=0.01237+0.5\times\frac{150000}{10000000}=0.01987$$ \\ @@ -233,7 +234,7 @@ \subsection{Redistribute undercollateralized Troves to other borrowers} \hline \textcolor{red}{D} & \textcolor{red}{$8000$} & \textcolor{red}{$4.3$} & \textcolor{red}{$108\%$} & \textcolor{red}{$-8000.00$} & \textcolor{red}{$-4.30$} & \textcolor{red}{$0.00$} & \textcolor{red}{$0.00$} & \textcolor{red}{n/a} & \textcolor{red}{$-600$} \\ \hlineB{2.5} - Total & $20000$ & $16.8$ & $17\%$ & $0.00$ & $0.00$ & $20000.00$ & $16.80$ & $17\%$ & $0.00$ \\ + Total & $20000$ & $16.8$ & $168\%$ & $0.00$ & $0.00$ & $20000.00$ & $16.80$ & $168\%$ \tablefootnote{For simplicity reason we are disregarding the Gas Compensation in the calculation} & $0.00$ \\ \hline \end{tabular} \end{center} diff --git a/yarn.lock b/yarn.lock index 848dfff14..5736ad9f4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1547,21 +1547,6 @@ "@ethersproject/properties" ">=5.0.0-beta.131" "@ethersproject/strings" ">=5.0.0-beta.130" -"@ethersproject/abi@5.0.13", "@ethersproject/abi@^5.0.0-beta.146", "@ethersproject/abi@^5.0.10": - version "5.0.13" - resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.0.13.tgz#600a559c3730467716595658beaa2894b4352bcc" - integrity sha512-2coOH3D7ra1lwamKEH0HVc+Jbcsw5yfeCgmY8ekhCDualEiyyovD2qDcMBBcY3+kjoLHVTmo7ost6MNClxdOrg== - dependencies: - "@ethersproject/address" "^5.0.9" - "@ethersproject/bignumber" "^5.0.13" - "@ethersproject/bytes" "^5.0.9" - "@ethersproject/constants" "^5.0.8" - "@ethersproject/hash" "^5.0.10" - "@ethersproject/keccak256" "^5.0.7" - "@ethersproject/logger" "^5.0.8" - "@ethersproject/properties" "^5.0.7" - "@ethersproject/strings" "^5.0.8" - "@ethersproject/abi@5.0.7": version "5.0.7" resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.0.7.tgz#79e52452bd3ca2956d0e1c964207a58ad1a0ee7b" @@ -1577,6 +1562,36 @@ "@ethersproject/properties" "^5.0.3" "@ethersproject/strings" "^5.0.4" +"@ethersproject/abi@5.3.1", "@ethersproject/abi@^5.3.0": + version "5.3.1" + resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.3.1.tgz#69a1a496729d3a83521675a57cbe21f3cc27241c" + integrity sha512-F98FWTJG7nWWAQ4DcV6R0cSlrj67MWK3ylahuFbzkumem5cLWg1p7fZ3vIdRoS1c7TEf55Lvyx0w7ICR47IImw== + dependencies: + "@ethersproject/address" "^5.3.0" + "@ethersproject/bignumber" "^5.3.0" + "@ethersproject/bytes" "^5.3.0" + "@ethersproject/constants" "^5.3.0" + "@ethersproject/hash" "^5.3.0" + "@ethersproject/keccak256" "^5.3.0" + "@ethersproject/logger" "^5.3.0" + "@ethersproject/properties" "^5.3.0" + "@ethersproject/strings" "^5.3.0" + +"@ethersproject/abi@^5.0.0-beta.146": + version "5.0.13" + resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.0.13.tgz#600a559c3730467716595658beaa2894b4352bcc" + integrity sha512-2coOH3D7ra1lwamKEH0HVc+Jbcsw5yfeCgmY8ekhCDualEiyyovD2qDcMBBcY3+kjoLHVTmo7ost6MNClxdOrg== + dependencies: + "@ethersproject/address" "^5.0.9" + "@ethersproject/bignumber" "^5.0.13" + "@ethersproject/bytes" "^5.0.9" + "@ethersproject/constants" "^5.0.8" + "@ethersproject/hash" "^5.0.10" + "@ethersproject/keccak256" "^5.0.7" + "@ethersproject/logger" "^5.0.8" + "@ethersproject/properties" "^5.0.7" + "@ethersproject/strings" "^5.0.8" + "@ethersproject/abi@^5.0.2": version "5.1.0" resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.1.0.tgz#d582c9f6a8e8192778b5f2c991ce19d7b336b0c5" @@ -1592,18 +1607,18 @@ "@ethersproject/properties" "^5.1.0" "@ethersproject/strings" "^5.1.0" -"@ethersproject/abstract-provider@5.0.10", "@ethersproject/abstract-provider@^5.0.8": - version "5.0.10" - resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.0.10.tgz#a533aed39a5f27312745c8c4c40fa25fc884831c" - integrity sha512-OSReY5iz94iIaPlRvLiJP8YVIvQLx4aUvMMnHWSaA/vTU8QHZmgNlt4OBdYV1+aFY8Xl+VRYiWBHq72ZDKXXCQ== +"@ethersproject/abstract-provider@5.3.0", "@ethersproject/abstract-provider@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.3.0.tgz#f4c0ae4a4cef9f204d7781de805fd44b72756c81" + integrity sha512-1+MLhGP1GwxBDBNwMWVmhCsvKwh4gK7oIfOrmlmePNeskg1NhIrYssraJBieaFNHUYfKEd/1DjiVZMw8Qu5Cxw== dependencies: - "@ethersproject/bignumber" "^5.0.13" - "@ethersproject/bytes" "^5.0.9" - "@ethersproject/logger" "^5.0.8" - "@ethersproject/networks" "^5.0.7" - "@ethersproject/properties" "^5.0.7" - "@ethersproject/transactions" "^5.0.9" - "@ethersproject/web" "^5.0.12" + "@ethersproject/bignumber" "^5.3.0" + "@ethersproject/bytes" "^5.3.0" + "@ethersproject/logger" "^5.3.0" + "@ethersproject/networks" "^5.3.0" + "@ethersproject/properties" "^5.3.0" + "@ethersproject/transactions" "^5.3.0" + "@ethersproject/web" "^5.3.0" "@ethersproject/abstract-provider@^5.1.0": version "5.1.0" @@ -1618,16 +1633,16 @@ "@ethersproject/transactions" "^5.1.0" "@ethersproject/web" "^5.1.0" -"@ethersproject/abstract-signer@5.0.14", "@ethersproject/abstract-signer@^5.0.10": - version "5.0.14" - resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.0.14.tgz#30ef912b0f86599d90fdffc65c110452e7b55cf1" - integrity sha512-JztBwVO7o5OHLh2vyjordlS4/1EjRyaECtc8vPdXTF1i4dXN+J0coeRoPN6ZFbBvi/YbaB6br2fvqhst1VQD/g== +"@ethersproject/abstract-signer@5.3.0", "@ethersproject/abstract-signer@^5.0.10", "@ethersproject/abstract-signer@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.3.0.tgz#05172b653e15b535ed5854ef5f6a72f4b441052d" + integrity sha512-w8IFwOYqiPrtvosPuArZ3+QPR2nmdVTRrVY8uJYL3NNfMmQfTy3V3l2wbzX47UUlNbPJY+gKvzJAyvK1onZxJg== dependencies: - "@ethersproject/abstract-provider" "^5.0.8" - "@ethersproject/bignumber" "^5.0.13" - "@ethersproject/bytes" "^5.0.9" - "@ethersproject/logger" "^5.0.8" - "@ethersproject/properties" "^5.0.7" + "@ethersproject/abstract-provider" "^5.3.0" + "@ethersproject/bignumber" "^5.3.0" + "@ethersproject/bytes" "^5.3.0" + "@ethersproject/logger" "^5.3.0" + "@ethersproject/properties" "^5.3.0" "@ethersproject/abstract-signer@^5.1.0": version "5.1.0" @@ -1640,7 +1655,18 @@ "@ethersproject/logger" "^5.1.0" "@ethersproject/properties" "^5.1.0" -"@ethersproject/address@5.0.11", "@ethersproject/address@>=5.0.0-beta.128", "@ethersproject/address@^5.0.0", "@ethersproject/address@^5.0.4", "@ethersproject/address@^5.0.9": +"@ethersproject/address@5.3.0", "@ethersproject/address@^5.0.0", "@ethersproject/address@^5.0.9", "@ethersproject/address@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.3.0.tgz#e53b69eacebf332e8175de814c5e6507d6932518" + integrity sha512-29TgjzEBK+gUEUAOfWCG7s9IxLNLCqvr+oDSk6L9TXD0VLvZJKhJV479tKQqheVA81OeGxfpdxYtUVH8hqlCvA== + dependencies: + "@ethersproject/bignumber" "^5.3.0" + "@ethersproject/bytes" "^5.3.0" + "@ethersproject/keccak256" "^5.3.0" + "@ethersproject/logger" "^5.3.0" + "@ethersproject/rlp" "^5.3.0" + +"@ethersproject/address@>=5.0.0-beta.128", "@ethersproject/address@^5.0.4": version "5.0.11" resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.0.11.tgz#12022e8c590c33939beb5ab18b401ecf585eac59" integrity sha512-Et4GBdD8/tsBGjCEOKee9upN29qjL5kbRcmJifb4Penmiuh9GARXL2/xpXvEp5EW+EIW/rfCHFJrkYBgoQFQBw== @@ -1662,12 +1688,12 @@ "@ethersproject/logger" "^5.1.0" "@ethersproject/rlp" "^5.1.0" -"@ethersproject/base64@5.0.9", "@ethersproject/base64@^5.0.7": - version "5.0.9" - resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.0.9.tgz#bb1f35d3dba92082a574d5e2418f9202a0a1a7e6" - integrity sha512-37RBz5LEZ9SlTNGiWCYFttnIN9J7qVs9Xo2EbqGqDH5LfW9EIji66S+YDMpXVo1zWDax1FkEldAoatxHK2gfgA== +"@ethersproject/base64@5.3.0", "@ethersproject/base64@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.3.0.tgz#b831fb35418b42ad24d943c557259062b8640824" + integrity sha512-JIqgtOmgKcbc2sjGWTXyXktqUhvFUDte8fPVsAaOrcPiJf6YotNF+nsrOYGC9pbHBEGSuSBp3QR0varkO8JHEw== dependencies: - "@ethersproject/bytes" "^5.0.9" + "@ethersproject/bytes" "^5.3.0" "@ethersproject/base64@^5.1.0": version "5.1.0" @@ -1676,15 +1702,24 @@ dependencies: "@ethersproject/bytes" "^5.1.0" -"@ethersproject/basex@5.0.9", "@ethersproject/basex@^5.0.7": - version "5.0.9" - resolved "https://registry.yarnpkg.com/@ethersproject/basex/-/basex-5.0.9.tgz#00d727a031bac563cb8bb900955206f1bf3cf1fc" - integrity sha512-FANswl1IN3PS0eltQxH2aM2+utPrkLUVG4XVFi6SafRG9EpAqXCgycxC8PU90mPGhigYTpg9cnTB5mCZ6ejQjw== +"@ethersproject/basex@5.3.0", "@ethersproject/basex@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@ethersproject/basex/-/basex-5.3.0.tgz#02dea3ab8559ae625c6d548bc11773432255c916" + integrity sha512-8J4nS6t/SOnoCgr3DF5WCSRLC5YwTKYpZWJqeyYQLX+86TwPhtzvHXacODzcDII9tWKhVg6g0Bka8JCBWXsCiQ== dependencies: - "@ethersproject/bytes" "^5.0.9" - "@ethersproject/properties" "^5.0.7" + "@ethersproject/bytes" "^5.3.0" + "@ethersproject/properties" "^5.3.0" + +"@ethersproject/bignumber@5.3.0", "@ethersproject/bignumber@^5.0.13", "@ethersproject/bignumber@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.3.0.tgz#74ab2ec9c3bda4e344920565720a6ee9c794e9db" + integrity sha512-5xguJ+Q1/zRMgHgDCaqAexx/8DwDVLRemw2i6uR8KyGjwGdXI8f32QZZ1cKGucBN6ekJvpUpHy6XAuQnTv0mPA== + dependencies: + "@ethersproject/bytes" "^5.3.0" + "@ethersproject/logger" "^5.3.0" + bn.js "^4.11.9" -"@ethersproject/bignumber@5.0.15", "@ethersproject/bignumber@>=5.0.0-beta.130", "@ethersproject/bignumber@^5.0.13", "@ethersproject/bignumber@^5.0.7": +"@ethersproject/bignumber@>=5.0.0-beta.130", "@ethersproject/bignumber@^5.0.7": version "5.0.15" resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.0.15.tgz#b089b3f1e0381338d764ac1c10512f0c93b184ed" integrity sha512-MTADqnyacvdRwtKh7o9ujwNDSM1SDJjYDMYAzjIgjoi9rh6TY4suMbhCa3i2vh3SUXiXSICyTI8ui+NPdrZ9Lw== @@ -1702,7 +1737,14 @@ "@ethersproject/logger" "^5.1.0" bn.js "^4.4.0" -"@ethersproject/bytes@5.0.11", "@ethersproject/bytes@>=5.0.0-beta.129", "@ethersproject/bytes@^5.0.4", "@ethersproject/bytes@^5.0.9": +"@ethersproject/bytes@5.3.0", "@ethersproject/bytes@^5.0.9", "@ethersproject/bytes@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.3.0.tgz#473e0da7f831d535b2002be05e6f4ca3729a1bc9" + integrity sha512-rqLJjdVqCcn7glPer7Fxh87PRqlnRScVAoxcIP3PmOUNApMWJ6yRdOFfo2KvPAdO7Le3yEI1o0YW+Yvr7XCYvw== + dependencies: + "@ethersproject/logger" "^5.3.0" + +"@ethersproject/bytes@>=5.0.0-beta.129", "@ethersproject/bytes@^5.0.4": version "5.0.11" resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.0.11.tgz#21118e75b1d00db068984c15530e316021101276" integrity sha512-D51plLYY5qF05AsoVQwIZVLqlBkaTPVHVP/1WmmBIWyHB0cRW0C9kh0kx5Exo51rB63Hk8PfHxc7SmpoaQFEyg== @@ -1716,7 +1758,14 @@ dependencies: "@ethersproject/logger" "^5.1.0" -"@ethersproject/constants@5.0.10", "@ethersproject/constants@>=5.0.0-beta.128", "@ethersproject/constants@^5.0.4", "@ethersproject/constants@^5.0.8": +"@ethersproject/constants@5.3.0", "@ethersproject/constants@^5.0.8", "@ethersproject/constants@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.3.0.tgz#a5d6d86c0eec2c64c3024479609493b9afb3fc77" + integrity sha512-4y1feNOwEpgjAfiCFWOHznvv6qUF/H6uI0UKp8xdhftb+H+FbKflXg1pOgH5qs4Sr7EYBL+zPyPb+YD5g1aEyw== + dependencies: + "@ethersproject/bignumber" "^5.3.0" + +"@ethersproject/constants@>=5.0.0-beta.128", "@ethersproject/constants@^5.0.4": version "5.0.10" resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.0.10.tgz#eb0c604fbc44c53ba9641eed31a1d0c9e1ebcadc" integrity sha512-OSo8jxkHLDXieCy8bgOFR7lMfgPxEzKvSDdP+WAWHCDM8+orwch0B6wzkTmiQFgryAtIctrBt5glAdJikZ3hGw== @@ -1730,22 +1779,37 @@ dependencies: "@ethersproject/bignumber" "^5.1.0" -"@ethersproject/contracts@5.0.12": - version "5.0.12" - resolved "https://registry.yarnpkg.com/@ethersproject/contracts/-/contracts-5.0.12.tgz#6d488db46221258399dfe80b89bf849b3afd7897" - integrity sha512-srijy31idjz8bE+gL1I6IRj2H4I9dUwfQ+QroLrIgNdGArqY8y2iFUKa3QTy+JBX26fJsdYiCQi1kKkaNpnMpQ== - dependencies: - "@ethersproject/abi" "^5.0.10" - "@ethersproject/abstract-provider" "^5.0.8" - "@ethersproject/abstract-signer" "^5.0.10" - "@ethersproject/address" "^5.0.9" - "@ethersproject/bignumber" "^5.0.13" - "@ethersproject/bytes" "^5.0.9" - "@ethersproject/constants" "^5.0.8" - "@ethersproject/logger" "^5.0.8" - "@ethersproject/properties" "^5.0.7" - -"@ethersproject/hash@5.0.12", "@ethersproject/hash@>=5.0.0-beta.128", "@ethersproject/hash@^5.0.10", "@ethersproject/hash@^5.0.4": +"@ethersproject/contracts@5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@ethersproject/contracts/-/contracts-5.3.0.tgz#ad699a3abaae30bfb6422cf31813a663b2d4099c" + integrity sha512-eDyQ8ltykvyQqnGZxb/c1e0OnEtzqXhNNC4BX8nhYBCaoBrYYuK/1fLmyEvc5+XUMoxNhwpYkoSSwvPLci7/Zg== + dependencies: + "@ethersproject/abi" "^5.3.0" + "@ethersproject/abstract-provider" "^5.3.0" + "@ethersproject/abstract-signer" "^5.3.0" + "@ethersproject/address" "^5.3.0" + "@ethersproject/bignumber" "^5.3.0" + "@ethersproject/bytes" "^5.3.0" + "@ethersproject/constants" "^5.3.0" + "@ethersproject/logger" "^5.3.0" + "@ethersproject/properties" "^5.3.0" + "@ethersproject/transactions" "^5.3.0" + +"@ethersproject/hash@5.3.0", "@ethersproject/hash@^5.0.10", "@ethersproject/hash@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.3.0.tgz#f65e3bf3db3282df4da676db6cfa049535dd3643" + integrity sha512-gAFZSjUPQ32CIfoKSMtMEQ+IO0kQxqhwz9fCIFt2DtAq2u4pWt8mL9Z5P0r6KkLcQU8LE9FmuPPyd+JvBzmr1w== + dependencies: + "@ethersproject/abstract-signer" "^5.3.0" + "@ethersproject/address" "^5.3.0" + "@ethersproject/bignumber" "^5.3.0" + "@ethersproject/bytes" "^5.3.0" + "@ethersproject/keccak256" "^5.3.0" + "@ethersproject/logger" "^5.3.0" + "@ethersproject/properties" "^5.3.0" + "@ethersproject/strings" "^5.3.0" + +"@ethersproject/hash@>=5.0.0-beta.128", "@ethersproject/hash@^5.0.4": version "5.0.12" resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.0.12.tgz#1074599f7509e2ca2bb7a3d4f4e39ab3a796da42" integrity sha512-kn4QN+fhNFbUgX3XZTZUaQixi0oyfIEY+hfW+KtkHu+rq7dV76oAIvaLEEynu1/4npOL38E4X4YI42gGZk+C0Q== @@ -1773,44 +1837,52 @@ "@ethersproject/properties" "^5.1.0" "@ethersproject/strings" "^5.1.0" -"@ethersproject/hdnode@5.0.10", "@ethersproject/hdnode@^5.0.8": - version "5.0.10" - resolved "https://registry.yarnpkg.com/@ethersproject/hdnode/-/hdnode-5.0.10.tgz#f7cdf154bf5d104c76dce2940745fc71d9e7eb1b" - integrity sha512-ZLwMtIcXK7xz2lSITDCl40W04CtRq4K9NwBxhCzdzPdaz6XnoJMwGz2YMVLg+8ksseq+RYtTwIIXtlK6vyvQyg== - dependencies: - "@ethersproject/abstract-signer" "^5.0.10" - "@ethersproject/basex" "^5.0.7" - "@ethersproject/bignumber" "^5.0.13" - "@ethersproject/bytes" "^5.0.9" - "@ethersproject/logger" "^5.0.8" - "@ethersproject/pbkdf2" "^5.0.7" - "@ethersproject/properties" "^5.0.7" - "@ethersproject/sha2" "^5.0.7" - "@ethersproject/signing-key" "^5.0.8" - "@ethersproject/strings" "^5.0.8" - "@ethersproject/transactions" "^5.0.9" - "@ethersproject/wordlists" "^5.0.8" - -"@ethersproject/json-wallets@5.0.12", "@ethersproject/json-wallets@^5.0.10": - version "5.0.12" - resolved "https://registry.yarnpkg.com/@ethersproject/json-wallets/-/json-wallets-5.0.12.tgz#8946a0fcce1634b636313a50330b7d30a24996e8" - integrity sha512-nac553zGZnOewpjlqbfy7WBl8m3y7qudzRsI2dCxrediYtPIVIs9f6Pbnou8vDmmp8X4/U4W788d+Ma88o+Gbg== - dependencies: - "@ethersproject/abstract-signer" "^5.0.10" - "@ethersproject/address" "^5.0.9" - "@ethersproject/bytes" "^5.0.9" - "@ethersproject/hdnode" "^5.0.8" - "@ethersproject/keccak256" "^5.0.7" - "@ethersproject/logger" "^5.0.8" - "@ethersproject/pbkdf2" "^5.0.7" - "@ethersproject/properties" "^5.0.7" - "@ethersproject/random" "^5.0.7" - "@ethersproject/strings" "^5.0.8" - "@ethersproject/transactions" "^5.0.9" +"@ethersproject/hdnode@5.3.0", "@ethersproject/hdnode@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@ethersproject/hdnode/-/hdnode-5.3.0.tgz#26fed65ffd5c25463fddff13f5fb4e5617553c94" + integrity sha512-zLmmtLNoDMGoYRdjOab01Zqkvp+TmZyCGDAMQF1Bs3yZyBs/kzTNi1qJjR1jVUcPP5CWGtjFwY8iNG8oNV9J8g== + dependencies: + "@ethersproject/abstract-signer" "^5.3.0" + "@ethersproject/basex" "^5.3.0" + "@ethersproject/bignumber" "^5.3.0" + "@ethersproject/bytes" "^5.3.0" + "@ethersproject/logger" "^5.3.0" + "@ethersproject/pbkdf2" "^5.3.0" + "@ethersproject/properties" "^5.3.0" + "@ethersproject/sha2" "^5.3.0" + "@ethersproject/signing-key" "^5.3.0" + "@ethersproject/strings" "^5.3.0" + "@ethersproject/transactions" "^5.3.0" + "@ethersproject/wordlists" "^5.3.0" + +"@ethersproject/json-wallets@5.3.0", "@ethersproject/json-wallets@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@ethersproject/json-wallets/-/json-wallets-5.3.0.tgz#7b1a5ff500c12aa8597ae82c8939837b0449376e" + integrity sha512-/xwbqaIb5grUIGNmeEaz8GdcpmDr++X8WT4Jqcclnxow8PXCUHFeDxjf3O+nSuoqOYG/Ds0+BI5xuQKbva6Xkw== + dependencies: + "@ethersproject/abstract-signer" "^5.3.0" + "@ethersproject/address" "^5.3.0" + "@ethersproject/bytes" "^5.3.0" + "@ethersproject/hdnode" "^5.3.0" + "@ethersproject/keccak256" "^5.3.0" + "@ethersproject/logger" "^5.3.0" + "@ethersproject/pbkdf2" "^5.3.0" + "@ethersproject/properties" "^5.3.0" + "@ethersproject/random" "^5.3.0" + "@ethersproject/strings" "^5.3.0" + "@ethersproject/transactions" "^5.3.0" aes-js "3.0.0" scrypt-js "3.0.1" -"@ethersproject/keccak256@5.0.9", "@ethersproject/keccak256@>=5.0.0-beta.127", "@ethersproject/keccak256@^5.0.0-beta.130", "@ethersproject/keccak256@^5.0.3", "@ethersproject/keccak256@^5.0.7": +"@ethersproject/keccak256@5.3.0", "@ethersproject/keccak256@^5.0.7", "@ethersproject/keccak256@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.3.0.tgz#fb5cd36bdfd6fa02e2ea84964078a9fc6bd731be" + integrity sha512-Gv2YqgIUmRbYVNIibafT0qGaeGYLIA/EdWHJ7JcVxVSs2vyxafGxOJ5VpSBHWeOIsE6OOaCelYowhuuTicgdFQ== + dependencies: + "@ethersproject/bytes" "^5.3.0" + js-sha3 "0.5.7" + +"@ethersproject/keccak256@>=5.0.0-beta.127", "@ethersproject/keccak256@^5.0.0-beta.130", "@ethersproject/keccak256@^5.0.3": version "5.0.9" resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.0.9.tgz#ca0d86e4af56c13b1ef25e533bde3e96d28f647d" integrity sha512-zhdUTj6RGtCJSgU+bDrWF6cGbvW453LoIC1DSNWrTlXzC7WuH4a+EiPrgc7/kNoRxerKuA/cxYlI8GwNtVtDlw== @@ -1826,7 +1898,12 @@ "@ethersproject/bytes" "^5.1.0" js-sha3 "0.5.7" -"@ethersproject/logger@5.0.10", "@ethersproject/logger@>=5.0.0-beta.129", "@ethersproject/logger@^5.0.5", "@ethersproject/logger@^5.0.8": +"@ethersproject/logger@5.3.0", "@ethersproject/logger@^5.0.8", "@ethersproject/logger@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.3.0.tgz#7a69fa1d4ca0d4b7138da1627eb152f763d84dd0" + integrity sha512-8bwJ2gxJGkZZnpQSq5uSiZSJjyVTWmlGft4oH8vxHdvO1Asy4TwVepAhPgxIQIMxXZFUNMych1YjIV4oQ4I7dA== + +"@ethersproject/logger@>=5.0.0-beta.129", "@ethersproject/logger@^5.0.5": version "5.0.10" resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.0.10.tgz#fd884688b3143253e0356ef92d5f22d109d2e026" integrity sha512-0y2T2NqykDrbPM3Zw9RSbPkDOxwChAL8detXaom76CfYoGxsOnRP/zTX8OUAV+x9LdwzgbWvWmeXrc0M7SuDZw== @@ -1836,12 +1913,12 @@ resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.1.0.tgz#4cdeeefac029373349d5818f39c31b82cc6d9bbf" integrity sha512-wtUaD1lBX10HBXjjKV9VHCBnTdUaKQnQ2XSET1ezglqLdPdllNOIlLfhyCRqXm5xwcjExVI5ETokOYfjPtaAlw== -"@ethersproject/networks@5.0.9", "@ethersproject/networks@^5.0.7": - version "5.0.9" - resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.0.9.tgz#ec5da11e4d4bfd69bec4eaebc9ace33eb9569279" - integrity sha512-L8+VCQwArBLGkxZb/5Ns/OH/OxP38AcaveXIxhUTq+VWpXYjrObG3E7RDQIKkUx1S1IcQl/UWTz5w4DK0UitJg== +"@ethersproject/networks@5.3.1", "@ethersproject/networks@^5.3.0": + version "5.3.1" + resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.3.1.tgz#78fe08324cee289ce239acf8c746121934b2ef61" + integrity sha512-6uQKHkYChlsfeiZhQ8IHIqGE/sQsf25o9ZxAYpMxi15dLPzz3IxOEF5KiSD32aHwsjXVBKBSlo+teAXLlYJybw== dependencies: - "@ethersproject/logger" "^5.0.8" + "@ethersproject/logger" "^5.3.0" "@ethersproject/networks@^5.1.0": version "5.1.0" @@ -1850,15 +1927,22 @@ dependencies: "@ethersproject/logger" "^5.1.0" -"@ethersproject/pbkdf2@5.0.9", "@ethersproject/pbkdf2@^5.0.7": - version "5.0.9" - resolved "https://registry.yarnpkg.com/@ethersproject/pbkdf2/-/pbkdf2-5.0.9.tgz#be39c7f0a66c0d3cb1ad1dbb12a78e9bcdf9b5ae" - integrity sha512-ItE/wQ/WVw/ajEHPUVgfu0aEvksPgOQc+278bke8sGKnGO3ppjmqp0MHh17tHc1EBTzJbSms5aLIqc56qZ/oiA== +"@ethersproject/pbkdf2@5.3.0", "@ethersproject/pbkdf2@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@ethersproject/pbkdf2/-/pbkdf2-5.3.0.tgz#8adbb41489c3c9f319cc44bc7d3e6095fd468dc8" + integrity sha512-Q9ChVU6gBFiex0FSdtzo4b0SAKz3ZYcYVFLrEWHL0FnHvNk3J3WgAtRNtBQGQYn/T5wkoTdZttMbfBkFlaiWcA== dependencies: - "@ethersproject/bytes" "^5.0.9" - "@ethersproject/sha2" "^5.0.7" + "@ethersproject/bytes" "^5.3.0" + "@ethersproject/sha2" "^5.3.0" + +"@ethersproject/properties@5.3.0", "@ethersproject/properties@^5.0.7", "@ethersproject/properties@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.3.0.tgz#feef4c4babeb7c10a6b3449575016f4ad2c092b2" + integrity sha512-PaHxJyM5/bfusk6vr3yP//JMnm4UEojpzuWGTmtL5X4uNhNnFNvlYilZLyDr4I9cTkIbipCMsAuIcXWsmdRnEw== + dependencies: + "@ethersproject/logger" "^5.3.0" -"@ethersproject/properties@5.0.9", "@ethersproject/properties@>=5.0.0-beta.131", "@ethersproject/properties@^5.0.3", "@ethersproject/properties@^5.0.7": +"@ethersproject/properties@>=5.0.0-beta.131", "@ethersproject/properties@^5.0.3": version "5.0.9" resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.0.9.tgz#d7aae634680760136ea522e25c3ef043ec15b5c2" integrity sha512-ZCjzbHYTw+rF1Pn8FDCEmx3gQttwIHcm/6Xee8g/M3Ga3SfW4tccNMbs5zqnBH0E4RoOPaeNgyg1O68TaF0tlg== @@ -1872,46 +1956,46 @@ dependencies: "@ethersproject/logger" "^5.1.0" -"@ethersproject/providers@5.0.24": - version "5.0.24" - resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.0.24.tgz#4c638a029482d052faa18364b5e0e2d3ddd9c0cb" - integrity sha512-M4Iw1r4gGJkt7ZUa++iREuviKL/DIpmIMsaUlVlXtV+ZrUXeN8xQ3zOTrbz7R4h9W9oljBZM7i4D3Kn1krJ30A== - dependencies: - "@ethersproject/abstract-provider" "^5.0.8" - "@ethersproject/abstract-signer" "^5.0.10" - "@ethersproject/address" "^5.0.9" - "@ethersproject/basex" "^5.0.7" - "@ethersproject/bignumber" "^5.0.13" - "@ethersproject/bytes" "^5.0.9" - "@ethersproject/constants" "^5.0.8" - "@ethersproject/hash" "^5.0.10" - "@ethersproject/logger" "^5.0.8" - "@ethersproject/networks" "^5.0.7" - "@ethersproject/properties" "^5.0.7" - "@ethersproject/random" "^5.0.7" - "@ethersproject/rlp" "^5.0.7" - "@ethersproject/sha2" "^5.0.7" - "@ethersproject/strings" "^5.0.8" - "@ethersproject/transactions" "^5.0.9" - "@ethersproject/web" "^5.0.12" +"@ethersproject/providers@5.3.1": + version "5.3.1" + resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.3.1.tgz#a12c6370e8cbc0968c9744641b8ef90b0dd5ec2b" + integrity sha512-HC63vENTrur6/JKEhcQbA8PRDj1FAesdpX98IW+xAAo3EAkf70ou5fMIA3KCGzJDLNTeYA4C2Bonz849tVLekg== + dependencies: + "@ethersproject/abstract-provider" "^5.3.0" + "@ethersproject/abstract-signer" "^5.3.0" + "@ethersproject/address" "^5.3.0" + "@ethersproject/basex" "^5.3.0" + "@ethersproject/bignumber" "^5.3.0" + "@ethersproject/bytes" "^5.3.0" + "@ethersproject/constants" "^5.3.0" + "@ethersproject/hash" "^5.3.0" + "@ethersproject/logger" "^5.3.0" + "@ethersproject/networks" "^5.3.0" + "@ethersproject/properties" "^5.3.0" + "@ethersproject/random" "^5.3.0" + "@ethersproject/rlp" "^5.3.0" + "@ethersproject/sha2" "^5.3.0" + "@ethersproject/strings" "^5.3.0" + "@ethersproject/transactions" "^5.3.0" + "@ethersproject/web" "^5.3.0" bech32 "1.1.4" - ws "7.2.3" + ws "7.4.6" -"@ethersproject/random@5.0.9", "@ethersproject/random@^5.0.7": - version "5.0.9" - resolved "https://registry.yarnpkg.com/@ethersproject/random/-/random-5.0.9.tgz#1903d4436ba66e4c8ac77968b16f756abea3a0d0" - integrity sha512-DANG8THsKqFbJOantrxumtG6gyETNE54VfbsWa+SQAT8WKpDo9W/X5Zhh73KuhClaey1UI32uVmISZeq/Zxn1A== +"@ethersproject/random@5.3.0", "@ethersproject/random@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@ethersproject/random/-/random-5.3.0.tgz#7c46bf36e50cb0d0550bc8c666af8e1d4496dc1a" + integrity sha512-A5SL/4inutSwt3Fh2OD0x2gz+x6GHmuUnIPkR7zAiTidMD2N8F6tZdMF1hlQKWVCcVMWhEQg8mWijhEzm6BBYw== dependencies: - "@ethersproject/bytes" "^5.0.9" - "@ethersproject/logger" "^5.0.8" + "@ethersproject/bytes" "^5.3.0" + "@ethersproject/logger" "^5.3.0" -"@ethersproject/rlp@5.0.9", "@ethersproject/rlp@^5.0.7": - version "5.0.9" - resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.0.9.tgz#da205bf8a34d3c3409eb73ddd237130a4b376aff" - integrity sha512-ns1U7ZMVeruUW6JXc4om+1w3w4ynHN/0fpwmeNTsAjwGKoF8SAUgue6ylKpHKWSti2idx7jDxbn8hNNFHk67CA== +"@ethersproject/rlp@5.3.0", "@ethersproject/rlp@^5.0.7", "@ethersproject/rlp@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.3.0.tgz#7cb93a7b5dfa69163894153c9d4b0d936f333188" + integrity sha512-oI0joYpsRanl9guDubaW+1NbcpK0vJ3F/6Wpcanzcnqq+oaW9O5E98liwkEDPcb16BUTLIJ+ZF8GPIHYxJ/5Pw== dependencies: - "@ethersproject/bytes" "^5.0.9" - "@ethersproject/logger" "^5.0.8" + "@ethersproject/bytes" "^5.3.0" + "@ethersproject/logger" "^5.3.0" "@ethersproject/rlp@^5.1.0": version "5.1.0" @@ -1921,24 +2005,26 @@ "@ethersproject/bytes" "^5.1.0" "@ethersproject/logger" "^5.1.0" -"@ethersproject/sha2@5.0.9", "@ethersproject/sha2@^5.0.7": - version "5.0.9" - resolved "https://registry.yarnpkg.com/@ethersproject/sha2/-/sha2-5.0.9.tgz#41275ee03e6e1660b3c997754005e089e936adc6" - integrity sha512-5FH4s47gM7N1fFAYQ1+m7aX0SbLg0Xr+6tvqndmNqc382/qBIbzXiGlUookrsjlPb6gLNurnTssCXjNM72J6lQ== +"@ethersproject/sha2@5.3.0", "@ethersproject/sha2@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@ethersproject/sha2/-/sha2-5.3.0.tgz#209f9a1649f7d2452dcd5e5b94af43b7f3f42366" + integrity sha512-r5ftlwKcocYEuFz2JbeKOT5SAsCV4m1RJDsTOEfQ5L67ZC7NFDK5i7maPdn1bx4nPhylF9VAwxSrQ1esmwzylg== dependencies: - "@ethersproject/bytes" "^5.0.9" - "@ethersproject/logger" "^5.0.8" - hash.js "1.1.3" + "@ethersproject/bytes" "^5.3.0" + "@ethersproject/logger" "^5.3.0" + hash.js "1.1.7" -"@ethersproject/signing-key@5.0.11", "@ethersproject/signing-key@^5.0.8": - version "5.0.11" - resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.0.11.tgz#19fc5c4597e18ad0a5efc6417ba5b74069fdd2af" - integrity sha512-Jfcru/BGwdkXhLxT+8WCZtFy7LL0TPFZw05FAb5asxB/MyVsEfNdNxGDtjVE9zXfmRSPe/EusXYY4K7wcygOyQ== +"@ethersproject/signing-key@5.3.0", "@ethersproject/signing-key@^5.0.8", "@ethersproject/signing-key@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.3.0.tgz#a96c88f8173e1abedfa35de32d3e5db7c48e5259" + integrity sha512-+DX/GwHAd0ok1bgedV1cKO0zfK7P/9aEyNoaYiRsGHpCecN7mhLqcdoUiUzE7Uz86LBsxm5ssK0qA1kBB47fbQ== dependencies: - "@ethersproject/bytes" "^5.0.9" - "@ethersproject/logger" "^5.0.8" - "@ethersproject/properties" "^5.0.7" + "@ethersproject/bytes" "^5.3.0" + "@ethersproject/logger" "^5.3.0" + "@ethersproject/properties" "^5.3.0" + bn.js "^4.11.9" elliptic "6.5.4" + hash.js "1.1.7" "@ethersproject/signing-key@^5.1.0": version "5.1.0" @@ -1951,18 +2037,27 @@ bn.js "^4.4.0" elliptic "6.5.4" -"@ethersproject/solidity@5.0.10": - version "5.0.10" - resolved "https://registry.yarnpkg.com/@ethersproject/solidity/-/solidity-5.0.10.tgz#128c9289761cf83d81ff62a1195d6079a924a86c" - integrity sha512-8OG3HLqynWXDA6mVIHuHfF/ojTTwBahON7hc9GAKCqglzXCkVA3OpyxOJXPzjHClRIAUUiU7r9oy9Z/nsjtT/g== +"@ethersproject/solidity@5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@ethersproject/solidity/-/solidity-5.3.0.tgz#2a0b00b4aaaef99a080ddea13acab1fa35cd4a93" + integrity sha512-uLRBaNUiISHbut94XKewJgQh6UmydWTBp71I7I21pkjVXfZO2dJ5EOo3jCnumJc01M4LOm79dlNNmF3oGIvweQ== dependencies: - "@ethersproject/bignumber" "^5.0.13" - "@ethersproject/bytes" "^5.0.9" - "@ethersproject/keccak256" "^5.0.7" - "@ethersproject/sha2" "^5.0.7" - "@ethersproject/strings" "^5.0.8" + "@ethersproject/bignumber" "^5.3.0" + "@ethersproject/bytes" "^5.3.0" + "@ethersproject/keccak256" "^5.3.0" + "@ethersproject/sha2" "^5.3.0" + "@ethersproject/strings" "^5.3.0" + +"@ethersproject/strings@5.3.0", "@ethersproject/strings@^5.0.8", "@ethersproject/strings@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.3.0.tgz#a6b640aab56a18e0909f657da798eef890968ff0" + integrity sha512-j/AzIGZ503cvhuF2ldRSjB0BrKzpsBMtCieDtn4TYMMZMQ9zScJn9wLzTQl/bRNvJbBE6TOspK0r8/Ngae/f2Q== + dependencies: + "@ethersproject/bytes" "^5.3.0" + "@ethersproject/constants" "^5.3.0" + "@ethersproject/logger" "^5.3.0" -"@ethersproject/strings@5.0.10", "@ethersproject/strings@>=5.0.0-beta.130", "@ethersproject/strings@^5.0.4", "@ethersproject/strings@^5.0.8": +"@ethersproject/strings@>=5.0.0-beta.130", "@ethersproject/strings@^5.0.4": version "5.0.10" resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.0.10.tgz#ddce1e9724f4ac4f3f67e0cac0b48748e964bfdb" integrity sha512-KAeoS1tZ9/5ECXiIZA6S6hywbD0so2VmuW+Wfyo5EDXeyZ6Na1nxTPhTnW7voQmjbeYJffCrOc0qLFJeylyg7w== @@ -1980,7 +2075,22 @@ "@ethersproject/constants" "^5.1.0" "@ethersproject/logger" "^5.1.0" -"@ethersproject/transactions@5.0.11", "@ethersproject/transactions@^5.0.0-beta.135", "@ethersproject/transactions@^5.0.9": +"@ethersproject/transactions@5.3.0", "@ethersproject/transactions@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.3.0.tgz#49b86f2bafa4d0bdf8e596578fc795ee47c50458" + integrity sha512-cdfK8VVyW2oEBCXhURG0WQ6AICL/r6Gmjh0e4Bvbv6MCn/GBd8FeBH3rtl7ho+AW50csMKeGv3m3K1HSHB2jMQ== + dependencies: + "@ethersproject/address" "^5.3.0" + "@ethersproject/bignumber" "^5.3.0" + "@ethersproject/bytes" "^5.3.0" + "@ethersproject/constants" "^5.3.0" + "@ethersproject/keccak256" "^5.3.0" + "@ethersproject/logger" "^5.3.0" + "@ethersproject/properties" "^5.3.0" + "@ethersproject/rlp" "^5.3.0" + "@ethersproject/signing-key" "^5.3.0" + +"@ethersproject/transactions@^5.0.0-beta.135": version "5.0.11" resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.0.11.tgz#b31df5292f47937136a45885d6ee6112477c13df" integrity sha512-ftsRvR9+gQp7L63F6+XmstvsZ4w8GtWvQB08e/zB+oB86Fnhq8+i/tkgpJplSHC8I/qgiCisva+M3u2GVhDFPA== @@ -2010,46 +2120,46 @@ "@ethersproject/rlp" "^5.1.0" "@ethersproject/signing-key" "^5.1.0" -"@ethersproject/units@5.0.11": - version "5.0.11" - resolved "https://registry.yarnpkg.com/@ethersproject/units/-/units-5.0.11.tgz#f82f6e353ac0d6fa43b17337790f1f9aa72cb4c8" - integrity sha512-nOSPmcCWyB/dwoBRhhTtPGCsTbiXqmc7Q0Adwvafc432AC7hy3Fj3IFZtnSXsbtJ/GdHCIUIoA8gtvxSsFuBJg== - dependencies: - "@ethersproject/bignumber" "^5.0.13" - "@ethersproject/constants" "^5.0.8" - "@ethersproject/logger" "^5.0.8" - -"@ethersproject/wallet@5.0.12": - version "5.0.12" - resolved "https://registry.yarnpkg.com/@ethersproject/wallet/-/wallet-5.0.12.tgz#bfb96f95e066b4b1b4591c4615207b87afedda8b" - integrity sha512-rboJebGf47/KPZrKZQdYg9BAYuXbc/OwcUyML1K1f2jnJeo1ObWV11U1PAWTjTbhhSy6/Fg+34GO2yMb5Dt1Rw== +"@ethersproject/units@5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@ethersproject/units/-/units-5.3.0.tgz#c4d1493532ad3d4ddf6e2bc4f8c94a2db933a8f5" + integrity sha512-BkfccZGwfJ6Ob+AelpIrgAzuNhrN2VLp3AILnkqTOv+yBdsc83V4AYf25XC/u0rHnWl6f4POaietPwlMqP2vUg== dependencies: - "@ethersproject/abstract-provider" "^5.0.8" - "@ethersproject/abstract-signer" "^5.0.10" - "@ethersproject/address" "^5.0.9" - "@ethersproject/bignumber" "^5.0.13" - "@ethersproject/bytes" "^5.0.9" - "@ethersproject/hash" "^5.0.10" - "@ethersproject/hdnode" "^5.0.8" - "@ethersproject/json-wallets" "^5.0.10" - "@ethersproject/keccak256" "^5.0.7" - "@ethersproject/logger" "^5.0.8" - "@ethersproject/properties" "^5.0.7" - "@ethersproject/random" "^5.0.7" - "@ethersproject/signing-key" "^5.0.8" - "@ethersproject/transactions" "^5.0.9" - "@ethersproject/wordlists" "^5.0.8" + "@ethersproject/bignumber" "^5.3.0" + "@ethersproject/constants" "^5.3.0" + "@ethersproject/logger" "^5.3.0" -"@ethersproject/web@5.0.14", "@ethersproject/web@^5.0.12": - version "5.0.14" - resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.0.14.tgz#6e7bebdd9fb967cb25ee60f44d9218dc0803bac4" - integrity sha512-QpTgplslwZ0Sp9oKNLoRuS6TKxnkwfaEk3gr7zd7XLF8XBsYejsrQO/03fNfnMx/TAT/RR6WEw/mbOwpRSeVRA== +"@ethersproject/wallet@5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@ethersproject/wallet/-/wallet-5.3.0.tgz#91946b470bd279e39ade58866f21f92749d062af" + integrity sha512-boYBLydG6671p9QoG6EinNnNzbm7DNOjVT20eV8J6HQEq4aUaGiA2CytF2vK+2rOEWbzhZqoNDt6AlkE1LlsTg== + dependencies: + "@ethersproject/abstract-provider" "^5.3.0" + "@ethersproject/abstract-signer" "^5.3.0" + "@ethersproject/address" "^5.3.0" + "@ethersproject/bignumber" "^5.3.0" + "@ethersproject/bytes" "^5.3.0" + "@ethersproject/hash" "^5.3.0" + "@ethersproject/hdnode" "^5.3.0" + "@ethersproject/json-wallets" "^5.3.0" + "@ethersproject/keccak256" "^5.3.0" + "@ethersproject/logger" "^5.3.0" + "@ethersproject/properties" "^5.3.0" + "@ethersproject/random" "^5.3.0" + "@ethersproject/signing-key" "^5.3.0" + "@ethersproject/transactions" "^5.3.0" + "@ethersproject/wordlists" "^5.3.0" + +"@ethersproject/web@5.3.0", "@ethersproject/web@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.3.0.tgz#7959c403f6476c61515008d8f92da51c553a8ee1" + integrity sha512-Ni6/DHnY6k/TD41LEkv0RQDx4jqWz5e/RZvrSecsxGYycF+MFy2z++T/yGc2peRunLOTIFwEksgEGGlbwfYmhQ== dependencies: - "@ethersproject/base64" "^5.0.7" - "@ethersproject/bytes" "^5.0.9" - "@ethersproject/logger" "^5.0.8" - "@ethersproject/properties" "^5.0.7" - "@ethersproject/strings" "^5.0.8" + "@ethersproject/base64" "^5.3.0" + "@ethersproject/bytes" "^5.3.0" + "@ethersproject/logger" "^5.3.0" + "@ethersproject/properties" "^5.3.0" + "@ethersproject/strings" "^5.3.0" "@ethersproject/web@^5.1.0": version "5.1.0" @@ -2062,16 +2172,16 @@ "@ethersproject/properties" "^5.1.0" "@ethersproject/strings" "^5.1.0" -"@ethersproject/wordlists@5.0.10", "@ethersproject/wordlists@^5.0.8": - version "5.0.10" - resolved "https://registry.yarnpkg.com/@ethersproject/wordlists/-/wordlists-5.0.10.tgz#177b9a0b4d72b9c4f304d08b36612d6c60e9b896" - integrity sha512-jWsEm1iJzpg9SCXnNfFz+tcp4Ofzv0TJb6mj+soCNcar9GcT0yGz62ZsHC3pLQWaF4LkCzGwRJHJTXKjHQfG1A== +"@ethersproject/wordlists@5.3.0", "@ethersproject/wordlists@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@ethersproject/wordlists/-/wordlists-5.3.0.tgz#45a0205f5178c1de33d316cb2ab7ed5eac3c06c5" + integrity sha512-JcwumCZcsUxgWpiFU/BRy6b4KlTRdOmYvOKZcAw/3sdF93/pZyPW5Od2hFkHS8oWp4xS06YQ+qHqQhdcxdHafQ== dependencies: - "@ethersproject/bytes" "^5.0.9" - "@ethersproject/hash" "^5.0.10" - "@ethersproject/logger" "^5.0.8" - "@ethersproject/properties" "^5.0.7" - "@ethersproject/strings" "^5.0.8" + "@ethersproject/bytes" "^5.3.0" + "@ethersproject/hash" "^5.3.0" + "@ethersproject/logger" "^5.3.0" + "@ethersproject/properties" "^5.3.0" + "@ethersproject/strings" "^5.3.0" "@fortawesome/fontawesome-common-types@^0.2.34": version "0.2.34" @@ -4046,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" @@ -9010,41 +9127,41 @@ ethereumjs-util@^7.0.2, ethereumjs-util@^7.0.9: ethjs-util "0.1.6" rlp "^2.2.4" -ethers@5.0.32, ethers@^5.0.0, ethers@^5.0.32: - version "5.0.32" - resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.0.32.tgz#f009970be31d96a589bf0ce597a39c10c7e297a6" - integrity sha512-rORfGWR0HsA4pjKMMcWZorw12DHsXqfIAuPVHJsXt+vI24jvXcVqx+rLsSvgOoLdaCMdxiN5qlIq2+4axKG31g== - dependencies: - "@ethersproject/abi" "5.0.13" - "@ethersproject/abstract-provider" "5.0.10" - "@ethersproject/abstract-signer" "5.0.14" - "@ethersproject/address" "5.0.11" - "@ethersproject/base64" "5.0.9" - "@ethersproject/basex" "5.0.9" - "@ethersproject/bignumber" "5.0.15" - "@ethersproject/bytes" "5.0.11" - "@ethersproject/constants" "5.0.10" - "@ethersproject/contracts" "5.0.12" - "@ethersproject/hash" "5.0.12" - "@ethersproject/hdnode" "5.0.10" - "@ethersproject/json-wallets" "5.0.12" - "@ethersproject/keccak256" "5.0.9" - "@ethersproject/logger" "5.0.10" - "@ethersproject/networks" "5.0.9" - "@ethersproject/pbkdf2" "5.0.9" - "@ethersproject/properties" "5.0.9" - "@ethersproject/providers" "5.0.24" - "@ethersproject/random" "5.0.9" - "@ethersproject/rlp" "5.0.9" - "@ethersproject/sha2" "5.0.9" - "@ethersproject/signing-key" "5.0.11" - "@ethersproject/solidity" "5.0.10" - "@ethersproject/strings" "5.0.10" - "@ethersproject/transactions" "5.0.11" - "@ethersproject/units" "5.0.11" - "@ethersproject/wallet" "5.0.12" - "@ethersproject/web" "5.0.14" - "@ethersproject/wordlists" "5.0.10" +ethers@5.3.1, ethers@^5.0.32, ethers@^5.3.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.3.1.tgz#1f018f0aeb651576cd84fd987a45f0b99646d761" + integrity sha512-xCKmC0gsZ9gks89ZfK3B1y6LlPdvX5fxDtu9SytnpdDJR1M7pmJI+4H0AxQPMgUYr7GtQdmECLR0gWdJQ+lZYw== + dependencies: + "@ethersproject/abi" "5.3.1" + "@ethersproject/abstract-provider" "5.3.0" + "@ethersproject/abstract-signer" "5.3.0" + "@ethersproject/address" "5.3.0" + "@ethersproject/base64" "5.3.0" + "@ethersproject/basex" "5.3.0" + "@ethersproject/bignumber" "5.3.0" + "@ethersproject/bytes" "5.3.0" + "@ethersproject/constants" "5.3.0" + "@ethersproject/contracts" "5.3.0" + "@ethersproject/hash" "5.3.0" + "@ethersproject/hdnode" "5.3.0" + "@ethersproject/json-wallets" "5.3.0" + "@ethersproject/keccak256" "5.3.0" + "@ethersproject/logger" "5.3.0" + "@ethersproject/networks" "5.3.1" + "@ethersproject/pbkdf2" "5.3.0" + "@ethersproject/properties" "5.3.0" + "@ethersproject/providers" "5.3.1" + "@ethersproject/random" "5.3.0" + "@ethersproject/rlp" "5.3.0" + "@ethersproject/sha2" "5.3.0" + "@ethersproject/signing-key" "5.3.0" + "@ethersproject/solidity" "5.3.0" + "@ethersproject/strings" "5.3.0" + "@ethersproject/transactions" "5.3.0" + "@ethersproject/units" "5.3.0" + "@ethersproject/wallet" "5.3.0" + "@ethersproject/web" "5.3.0" + "@ethersproject/wordlists" "5.3.0" ethers@^4.0.0-beta.1, ethers@^4.0.32, ethers@^4.0.40: version "4.0.48" @@ -10627,7 +10744,7 @@ hash.js@1.1.3: inherits "^2.0.3" minimalistic-assert "^1.0.0" -hash.js@^1.0.0, hash.js@^1.0.3, hash.js@^1.1.7: +hash.js@1.1.7, hash.js@^1.0.0, hash.js@^1.0.3, hash.js@^1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== @@ -21697,10 +21814,10 @@ write-file-atomic@^3.0.0: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" -ws@7.2.3: - version "7.2.3" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.2.3.tgz#a5411e1fb04d5ed0efee76d26d5c46d830c39b46" - integrity sha512-HTDl9G9hbkNDk98naoR/cHDws7+EyYMOdL1BmjsZXRUjf7d+MficC4B7HLUPlSiho0vg+CWKrGIt/VJBd1xunQ== +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"