import { BlockTag } from "@ethersproject/abstract-provider"; import { Decimal, Fees, FrontendStatus, ARTHStore, ReadableARTH, StabilityDeposit, Trove, TroveListingParams, TroveWithPendingRedistribution, UserTrove, UserTroveStatus } from "@mahadao/arth-base"; import { MultiTroveGetter } from "../types"; import { decimalify, panic } from "./_utils"; import { EthersCallOverrides, EthersProvider, EthersSigner } from "./types"; import { EthersARTHConnection, EthersARTHConnectionOptionalParams, EthersARTHStoreOption, _connect, _getBlockTimestamp, _getContracts, _requireAddress, _requireFrontendAddress } from "./EthersARTHConnection"; import { BlockPolledARTHStore } from "./BlockPolledARTHStore"; // TODO: these are constant in the contracts, so it doesn't make sense to make a call for them, // but to avoid having to update them here when we change them in the contracts, we could read // them once after deployment and save them to ARTHDeployment. const MINUTE_DECAY_FACTOR = Decimal.from("0.999037758833783000"); const BETA = Decimal.from(2); enum BackendTroveStatus { nonExistent, active, closedByOwner, closedByLiquidation, closedByRedemption } const userTroveStatusFrom = (backendStatus: BackendTroveStatus): UserTroveStatus => backendStatus === BackendTroveStatus.nonExistent ? "nonExistent" : backendStatus === BackendTroveStatus.active ? "open" : backendStatus === BackendTroveStatus.closedByOwner ? "closedByOwner" : backendStatus === BackendTroveStatus.closedByLiquidation ? "closedByLiquidation" : backendStatus === BackendTroveStatus.closedByRedemption ? "closedByRedemption" : panic(new Error(`invalid backendStatus ${backendStatus}`)); const convertToDate = (timestamp: number) => new Date(timestamp * 1000); const validSortingOptions = ["ascendingCollateralRatio", "descendingCollateralRatio"]; const expectPositiveInt = (obj: { [P in K]?: number }, key: K) => { if (obj[key] !== undefined) { if (!Number.isInteger(obj[key])) { throw new Error(`${key} must be an integer`); } if (obj[key] < 0) { throw new Error(`${key} must not be negative`); } } }; /** * Ethers-based implementation of {@link @mahadao/arth-base#ReadableARTH}. * * @public */ export class ReadableEthersARTH implements ReadableARTH { readonly connection: EthersARTHConnection; /** @internal */ constructor(connection: EthersARTHConnection) { this.connection = connection; } /** @internal */ static _from( connection: EthersARTHConnection & { useStore: "blockPolled" } ): ReadableEthersARTHWithStore; /** @internal */ static _from(connection: EthersARTHConnection): ReadableEthersARTH; /** @internal */ static _from(connection: EthersARTHConnection): ReadableEthersARTH { const readable = new ReadableEthersARTH(connection); return connection.useStore === "blockPolled" ? new _BlockPolledReadableEthersARTH(readable) : readable; } /** @internal */ static connect( signerOrProvider: EthersSigner | EthersProvider, optionalParams: EthersARTHConnectionOptionalParams & { useStore: "blockPolled" } ): Promise>; static connect( signerOrProvider: EthersSigner | EthersProvider, optionalParams?: EthersARTHConnectionOptionalParams ): Promise; /** * Connect to the ARTH protocol and create a `ReadableEthersARTH` object. * * @param signerOrProvider - Ethers `Signer` or `Provider` to use for connecting to the Ethereum * network. * @param optionalParams - Optional parameters that can be used to customize the connection. */ static async connect( signerOrProvider: EthersSigner | EthersProvider, optionalParams?: EthersARTHConnectionOptionalParams ): Promise { return ReadableEthersARTH._from(await _connect(signerOrProvider, optionalParams)); } /** * Check whether this `ReadableEthersARTH` is a {@link ReadableEthersARTHWithStore}. */ hasStore(): this is ReadableEthersARTHWithStore; /** * Check whether this `ReadableEthersARTH` is a * {@link ReadableEthersARTHWithStore}\<{@link BlockPolledARTHStore}\>. */ hasStore(store: "blockPolled"): this is ReadableEthersARTHWithStore; hasStore(): boolean { return false; } /** {@inheritDoc @mahadao/arth-base#ReadableARTH.getTotalRedistributed} */ async getTotalRedistributed(overrides?: EthersCallOverrides): Promise { const { troveManager } = _getContracts(this.connection); const [collateral, debt] = await Promise.all([ troveManager.L_ETH({ ...overrides }).then(decimalify), troveManager.L_ARTHDebt({ ...overrides }).then(decimalify) ]); return new Trove(collateral, debt); } /** {@inheritDoc @mahadao/arth-base#ReadableARTH.getTroveBeforeRedistribution} */ async getTroveBeforeRedistribution( address?: string, overrides?: EthersCallOverrides ): Promise { address ??= _requireAddress(this.connection); const { troveManager } = _getContracts(this.connection); const [trove, snapshot] = await Promise.all([ troveManager.Troves(address, { ...overrides }), troveManager.rewardSnapshots(address, { ...overrides }) ]); if (trove.status === BackendTroveStatus.active) { return new TroveWithPendingRedistribution( address, userTroveStatusFrom(trove.status), decimalify(trove.coll), decimalify(trove.debt), decimalify(trove.stake), new Trove(decimalify(snapshot.ETH), decimalify(snapshot.ARTHDebt)) ); } else { return new TroveWithPendingRedistribution(address, userTroveStatusFrom(trove.status)); } } /** {@inheritDoc @mahadao/arth-base#ReadableARTH.getTrove} */ async getTrove(address?: string, overrides?: EthersCallOverrides): Promise { const [trove, totalRedistributed] = await Promise.all([ this.getTroveBeforeRedistribution(address, overrides), this.getTotalRedistributed(overrides) ]); return trove.applyRedistribution(totalRedistributed); } /** {@inheritDoc @mahadao/arth-base#ReadableARTH.getNumberOfTroves} */ async getNumberOfTroves(overrides?: EthersCallOverrides): Promise { const { troveManager } = _getContracts(this.connection); return (await troveManager.getTroveOwnersCount({ ...overrides })).toNumber(); } /** {@inheritDoc @mahadao/arth-base#ReadableARTH.getPrice} */ getPrice(overrides?: EthersCallOverrides): Promise { const { priceFeed } = _getContracts(this.connection); return priceFeed.callStatic.fetchPrice({ ...overrides }).then(decimalify); } /** @internal */ async _getActivePool(overrides?: EthersCallOverrides): Promise { const { activePool } = _getContracts(this.connection); const [activeCollateral, activeDebt] = await Promise.all( [activePool.getETH({ ...overrides }), activePool.getARTHDebt({ ...overrides })].map( getBigNumber => getBigNumber.then(decimalify) ) ); return new Trove(activeCollateral, activeDebt); } /** @internal */ async _getDefaultPool(overrides?: EthersCallOverrides): Promise { const { defaultPool } = _getContracts(this.connection); const [liquidatedCollateral, closedDebt] = await Promise.all( [defaultPool.getETH({ ...overrides }), defaultPool.getARTHDebt({ ...overrides })].map( getBigNumber => getBigNumber.then(decimalify) ) ); return new Trove(liquidatedCollateral, closedDebt); } /** {@inheritDoc @mahadao/arth-base#ReadableARTH.getTotal} */ async getTotal(overrides?: EthersCallOverrides): Promise { const [activePool, defaultPool] = await Promise.all([ this._getActivePool(overrides), this._getDefaultPool(overrides) ]); return activePool.add(defaultPool); } /** {@inheritDoc @mahadao/arth-base#ReadableARTH.getStabilityDeposit} */ async getStabilityDeposit( address?: string, overrides?: EthersCallOverrides ): Promise { address ??= _requireAddress(this.connection); const { stabilityPool } = _getContracts(this.connection); const [{ frontEndTag, initialValue }, currentARTH, collateralGain, MAHAReward] = await Promise.all([ stabilityPool.deposits(address, { ...overrides }), stabilityPool.getCompoundedARTHDeposit(address, { ...overrides }), stabilityPool.getDepositorETHGain(address, { ...overrides }), stabilityPool.getDepositorMAHAGain(address, { ...overrides }) ]); return new StabilityDeposit( decimalify(initialValue), decimalify(currentARTH), decimalify(collateralGain), decimalify(MAHAReward), frontEndTag ); } /** {@inheritDoc @mahadao/arth-base#ReadableARTH.getRemainingStabilityPoolMAHAReward} */ async getRemainingStabilityPoolMAHAReward(overrides?: EthersCallOverrides): Promise { const { communityIssuance } = _getContracts(this.connection); const issuanceCap = this.connection.totalStabilityPoolMAHAReward; const totalMAHAIssued = decimalify(await communityIssuance.totalMAHAIssued({ ...overrides })); // totalMAHAIssued approaches but never reaches issuanceCap return issuanceCap.sub(totalMAHAIssued); } /** {@inheritDoc @mahadao/arth-base#ReadableARTH.getARTHInStabilityPool} */ getARTHInStabilityPool(overrides?: EthersCallOverrides): Promise { const { stabilityPool } = _getContracts(this.connection); return stabilityPool.getTotalARTHDeposits({ ...overrides }).then(decimalify); } /** {@inheritDoc @mahadao/arth-base#ReadableARTH.getARTHBalance} */ getARTHBalance(address?: string, overrides?: EthersCallOverrides): Promise { address ??= _requireAddress(this.connection); const { arthToken } = _getContracts(this.connection); return arthToken.balanceOf(address, { ...overrides }).then(decimalify); } /** {@inheritDoc @mahadao/arth-base#ReadableARTH.getMAHABalance} */ getMAHABalance(address?: string, overrides?: EthersCallOverrides): Promise { address ??= _requireAddress(this.connection); const { mahaToken } = _getContracts(this.connection); return mahaToken.balanceOf(address, { ...overrides }).then(decimalify); } /** {@inheritDoc @mahadao/arth-base#ReadableARTH.getCollateralSurplusBalance} */ getCollateralSurplusBalance(address?: string, overrides?: EthersCallOverrides): Promise { address ??= _requireAddress(this.connection); const { collSurplusPool } = _getContracts(this.connection); return collSurplusPool.getCollateral(address, { ...overrides }).then(decimalify); } /** @internal */ getTroves( params: TroveListingParams & { beforeRedistribution: true }, overrides?: EthersCallOverrides ): Promise; /** {@inheritDoc @mahadao/arth-base#ReadableARTH.(getTroves:2)} */ getTroves(params: TroveListingParams, overrides?: EthersCallOverrides): Promise; async getTroves( params: TroveListingParams, overrides?: EthersCallOverrides ): Promise { const { multiTroveGetter } = _getContracts(this.connection); expectPositiveInt(params, "first"); expectPositiveInt(params, "startingAt"); if (!validSortingOptions.includes(params.sortedBy)) { throw new Error( `sortedBy must be one of: ${validSortingOptions.map(x => `"${x}"`).join(", ")}` ); } const [totalRedistributed, backendTroves] = await Promise.all([ params.beforeRedistribution ? undefined : this.getTotalRedistributed({ ...overrides }), multiTroveGetter.getMultipleSortedTroves( params.sortedBy === "descendingCollateralRatio" ? params.startingAt ?? 0 : -((params.startingAt ?? 0) + 1), params.first, { ...overrides } ) ]); const troves = mapBackendTroves(backendTroves); if (totalRedistributed) { return troves.map(trove => trove.applyRedistribution(totalRedistributed)); } else { return troves; } } /** @internal */ _getBlockTimestamp(blockTag?: BlockTag): Promise { return _getBlockTimestamp(this.connection, blockTag); } /** @internal */ async _getFeesFactory( overrides?: EthersCallOverrides ): Promise<(blockTimestamp: number, recoveryMode: boolean) => Fees> { const { troveManager } = _getContracts(this.connection); const [lastFeeOperationTime, baseRateWithoutDecay] = await Promise.all([ troveManager.lastFeeOperationTime({ ...overrides }), troveManager.baseRate({ ...overrides }).then(decimalify) ]); return (blockTimestamp, recoveryMode) => new Fees( baseRateWithoutDecay, MINUTE_DECAY_FACTOR, BETA, convertToDate(lastFeeOperationTime.toNumber()), convertToDate(blockTimestamp), recoveryMode ); } /** {@inheritDoc @mahadao/arth-base#ReadableARTH.getFees} */ async getFees(overrides?: EthersCallOverrides): Promise { const [createFees, total, price, blockTimestamp] = await Promise.all([ this._getFeesFactory(overrides), this.getTotal(overrides), this.getPrice(overrides), this._getBlockTimestamp(overrides?.blockTag) ]); return createFees(blockTimestamp, total.collateralRatioIsBelowCritical(price)); } /** {@inheritDoc @mahadao/arth-base#ReadableARTH.getFrontendStatus} */ async getFrontendStatus( address?: string, overrides?: EthersCallOverrides ): Promise { address ??= _requireFrontendAddress(this.connection); const { stabilityPool } = _getContracts(this.connection); const { registered, kickbackRate } = await stabilityPool.frontEnds(address, { ...overrides }); return registered ? { status: "registered", kickbackRate: decimalify(kickbackRate) } : { status: "unregistered" }; } } type Resolved = T extends Promise ? U : T; type BackendTroves = Resolved>; const mapBackendTroves = (troves: BackendTroves): TroveWithPendingRedistribution[] => troves.map( trove => new TroveWithPendingRedistribution( trove.owner, "open", // These Troves are coming from the SortedTroves list, so they must be open decimalify(trove.coll), decimalify(trove.debt), decimalify(trove.stake), new Trove(decimalify(trove.snapshotETH), decimalify(trove.snapshotARTHDebt)) ) ); /** * Variant of {@link ReadableEthersARTH} that exposes a {@link @mahadao/arth-base#ARTHStore}. * * @public */ export interface ReadableEthersARTHWithStore extends ReadableEthersARTH { /** An object that implements ARTHStore. */ readonly store: T; } class _BlockPolledReadableEthersARTH implements ReadableEthersARTHWithStore { readonly connection: EthersARTHConnection; readonly store: BlockPolledARTHStore; private readonly _readable: ReadableEthersARTH; constructor(readable: ReadableEthersARTH) { const store = new BlockPolledARTHStore(readable); this.store = store; this.connection = readable.connection; this._readable = readable; } getMAHABalance( address?: string | undefined, overrides?: EthersCallOverrides | undefined ): Promise { throw new Error("Method not implemented."); } private _blockHit(overrides?: EthersCallOverrides): boolean { return ( !overrides || overrides.blockTag === undefined || overrides.blockTag === this.store.state.blockTag ); } private _userHit(address?: string, overrides?: EthersCallOverrides): boolean { return ( this._blockHit(overrides) && (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) ); } hasStore(store?: EthersARTHStoreOption): boolean { return store === undefined || store === "blockPolled"; } async getTotalRedistributed(overrides?: EthersCallOverrides): Promise { return this._blockHit(overrides) ? this.store.state.totalRedistributed : this._readable.getTotalRedistributed(overrides); } async getTroveBeforeRedistribution( address?: string, overrides?: EthersCallOverrides ): Promise { return this._userHit(address, overrides) ? this.store.state.troveBeforeRedistribution : this._readable.getTroveBeforeRedistribution(address, overrides); } async getTrove(address?: string, overrides?: EthersCallOverrides): Promise { return this._userHit(address, overrides) ? this.store.state.trove : this._readable.getTrove(address, overrides); } async getNumberOfTroves(overrides?: EthersCallOverrides): Promise { return this._blockHit(overrides) ? this.store.state.numberOfTroves : this._readable.getNumberOfTroves(overrides); } async getPrice(overrides?: EthersCallOverrides): Promise { return this._blockHit(overrides) ? this.store.state.price : this._readable.getPrice(overrides); } async getTotal(overrides?: EthersCallOverrides): Promise { return this._blockHit(overrides) ? this.store.state.total : this._readable.getTotal(overrides); } async getStabilityDeposit( address?: string, overrides?: EthersCallOverrides ): Promise { return this._userHit(address, overrides) ? this.store.state.stabilityDeposit : this._readable.getStabilityDeposit(address, overrides); } async getRemainingStabilityPoolMAHAReward(overrides?: EthersCallOverrides): Promise { return this._blockHit(overrides) ? this.store.state.remainingStabilityPoolMAHAReward : this._readable.getRemainingStabilityPoolMAHAReward(overrides); } async getARTHInStabilityPool(overrides?: EthersCallOverrides): Promise { return this._blockHit(overrides) ? this.store.state.arthInStabilityPool : this._readable.getARTHInStabilityPool(overrides); } async getARTHBalance(address?: string, overrides?: EthersCallOverrides): Promise { return this._userHit(address, overrides) ? this.store.state.arthBalance : this._readable.getARTHBalance(address, overrides); } async getCollateralSurplusBalance( address?: string, overrides?: EthersCallOverrides ): Promise { return this._userHit(address, overrides) ? this.store.state.collateralSurplusBalance : this._readable.getCollateralSurplusBalance(address, overrides); } async _getBlockTimestamp(blockTag?: BlockTag): Promise { return this._blockHit({ blockTag }) ? this.store.state.blockTimestamp : this._readable._getBlockTimestamp(blockTag); } async _getFeesFactory( overrides?: EthersCallOverrides ): Promise<(blockTimestamp: number, recoveryMode: boolean) => Fees> { return this._blockHit(overrides) ? this.store.state._feesFactory : this._readable._getFeesFactory(overrides); } async getFees(overrides?: EthersCallOverrides): Promise { return this._blockHit(overrides) ? this.store.state.fees : this._readable.getFees(overrides); } async getFrontendStatus( address?: string, overrides?: EthersCallOverrides ): Promise { return this._frontendHit(address, overrides) ? this.store.state.frontend : this._readable.getFrontendStatus(address, overrides); } getTroves( params: TroveListingParams & { beforeRedistribution: true }, overrides?: EthersCallOverrides ): Promise; getTroves(params: TroveListingParams, overrides?: EthersCallOverrides): Promise; getTroves(params: TroveListingParams, overrides?: EthersCallOverrides): Promise { return this._readable.getTroves(params, overrides); } _getActivePool(): Promise { throw new Error("Method not implemented."); } _getDefaultPool(): Promise { throw new Error("Method not implemented."); } _getRemainingLiquidityMiningMAHARewardCalculator(): Promise<(blockTimestamp: number) => Decimal> { throw new Error("Method not implemented."); } }