UNPKG

@sovryn-zero/lib-ethers

Version:
632 lines (521 loc) 20.7 kB
import { BigNumber } from "@ethersproject/bignumber"; import { Decimal, Fees, FrontendStatus, LiquityStore, ZEROStake, ReadableLiquity, StabilityDeposit, Trove, TroveListingParams, TroveWithPendingRedistribution, UserTrove, UserTroveStatus, _CachedReadableLiquity, _LiquityReadCache } from "@sovryn-zero/lib-base"; import { MultiTroveGetter } from "../types"; import { EthersCallOverrides, EthersProvider, EthersSigner } from "./types"; import { EthersLiquityConnection, EthersLiquityConnectionOptionalParams, EthersLiquityStoreOption, _connect, _getBlockTimestamp, _getContracts, _requireAddress, _requireFrontendAddress } from "./EthersLiquityConnection"; import { BlockPolledLiquityStore } from "./BlockPolledLiquityStore"; // 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 LiquityDeployment. const MINUTE_DECAY_FACTOR = Decimal.from("0.999037758833783000"); const BETA = Decimal.from(2); enum BackendTroveStatus { nonExistent, active, closedByOwner, closedByLiquidation, closedByRedemption } const panic = <T>(error: Error): T => { throw error; }; 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 decimalify = (bigNumber: BigNumber) => Decimal.fromBigNumberString(bigNumber.toHexString()); const convertToDate = (timestamp: number) => new Date(timestamp * 1000); const validSortingOptions = ["ascendingCollateralRatio", "descendingCollateralRatio"]; const expectPositiveInt = <K extends string>(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 @sovryn-zero/lib-base#ReadableLiquity}. * * @public */ export class ReadableEthersLiquity implements ReadableLiquity { readonly connection: EthersLiquityConnection; /** @internal */ constructor(connection: EthersLiquityConnection) { this.connection = connection; } /** @internal */ static _from( connection: EthersLiquityConnection & { useStore: "blockPolled" } ): ReadableEthersLiquityWithStore<BlockPolledLiquityStore>; /** @internal */ static _from(connection: EthersLiquityConnection): ReadableEthersLiquity; /** @internal */ static _from(connection: EthersLiquityConnection): ReadableEthersLiquity { const readable = new ReadableEthersLiquity(connection); return connection.useStore === "blockPolled" ? new _BlockPolledReadableEthersLiquity(readable) : readable; } /** @internal */ static connect( signerOrProvider: EthersSigner | EthersProvider, optionalParams: EthersLiquityConnectionOptionalParams & { useStore: "blockPolled" } ): Promise<ReadableEthersLiquityWithStore<BlockPolledLiquityStore>>; static connect( signerOrProvider: EthersSigner | EthersProvider, optionalParams?: EthersLiquityConnectionOptionalParams ): Promise<ReadableEthersLiquity>; /** * Connect to the Zero protocol and create a `ReadableEthersLiquity` 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?: EthersLiquityConnectionOptionalParams ): Promise<ReadableEthersLiquity> { return ReadableEthersLiquity._from(await _connect(signerOrProvider, optionalParams)); } /** * Check whether this `ReadableEthersLiquity` is a {@link ReadableEthersLiquityWithStore}. */ hasStore(): this is ReadableEthersLiquityWithStore; /** * Check whether this `ReadableEthersLiquity` is a * {@link ReadableEthersLiquityWithStore}\<{@link BlockPolledLiquityStore}\>. */ hasStore(store: "blockPolled"): this is ReadableEthersLiquityWithStore<BlockPolledLiquityStore>; hasStore(): boolean { return false; } /** {@inheritDoc @sovryn-zero/lib-base#ReadableLiquity.getTotalRedistributed} */ async getTotalRedistributed(overrides?: EthersCallOverrides): Promise<Trove> { const { troveManager } = _getContracts(this.connection); const [collateral, debt] = await Promise.all([ troveManager.L_ETH({ ...overrides }).then(decimalify), troveManager.L_ZUSDDebt({ ...overrides }).then(decimalify) ]); return new Trove(collateral, debt); } /** {@inheritDoc @sovryn-zero/lib-base#ReadableLiquity.getTroveBeforeRedistribution} */ async getTroveBeforeRedistribution( address?: string, overrides?: EthersCallOverrides ): Promise<TroveWithPendingRedistribution> { 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.ZUSDDebt)) ); } else { return new TroveWithPendingRedistribution(address, userTroveStatusFrom(trove.status)); } } /** {@inheritDoc @sovryn-zero/lib-base#ReadableLiquity.getTrove} */ async getTrove(address?: string, overrides?: EthersCallOverrides): Promise<UserTrove> { const [trove, totalRedistributed] = await Promise.all([ this.getTroveBeforeRedistribution(address, overrides), this.getTotalRedistributed(overrides) ]); return trove.applyRedistribution(totalRedistributed); } /** {@inheritDoc @sovryn-zero/lib-base#ReadableLiquity.getNumberOfTroves} */ async getNumberOfTroves(overrides?: EthersCallOverrides): Promise<number> { const { troveManager } = _getContracts(this.connection); return (await troveManager.getTroveOwnersCount({ ...overrides })).toNumber(); } /** {@inheritDoc @sovryn-zero/lib-base#ReadableLiquity.getPrice} */ getPrice(overrides?: EthersCallOverrides): Promise<Decimal> { const { priceFeed } = _getContracts(this.connection); return priceFeed.callStatic.fetchPrice({ ...overrides }).then(decimalify); } /** @internal */ async _getActivePool(overrides?: EthersCallOverrides): Promise<Trove> { const { activePool } = _getContracts(this.connection); const [activeCollateral, activeDebt] = await Promise.all( [ activePool.getETH({ ...overrides }), activePool.getZUSDDebt({ ...overrides }) ].map(getBigNumber => getBigNumber.then(decimalify)) ); return new Trove(activeCollateral, activeDebt); } /** @internal */ async _getDefaultPool(overrides?: EthersCallOverrides): Promise<Trove> { const { defaultPool } = _getContracts(this.connection); const [liquidatedCollateral, closedDebt] = await Promise.all( [ defaultPool.getETH({ ...overrides }), defaultPool.getZUSDDebt({ ...overrides }) ].map(getBigNumber => getBigNumber.then(decimalify)) ); return new Trove(liquidatedCollateral, closedDebt); } /** {@inheritDoc @sovryn-zero/lib-base#ReadableLiquity.getTotal} */ async getTotal(overrides?: EthersCallOverrides): Promise<Trove> { const [activePool, defaultPool] = await Promise.all([ this._getActivePool(overrides), this._getDefaultPool(overrides) ]); return activePool.add(defaultPool); } /** {@inheritDoc @sovryn-zero/lib-base#ReadableLiquity.getStabilityDeposit} */ async getStabilityDeposit( address?: string, overrides?: EthersCallOverrides ): Promise<StabilityDeposit> { address ??= _requireAddress(this.connection); const { stabilityPool } = _getContracts(this.connection); const [ { frontEndTag, initialValue }, currentZUSD, collateralGain, zeroReward ] = await Promise.all([ stabilityPool.deposits(address, { ...overrides }), stabilityPool.getCompoundedZUSDDeposit(address, { ...overrides }), stabilityPool.getDepositorETHGain(address, { ...overrides }), stabilityPool.getDepositorSOVGain(address, { ...overrides }) ]); return new StabilityDeposit( decimalify(initialValue), decimalify(currentZUSD), decimalify(collateralGain), decimalify(zeroReward), frontEndTag ); } /** {@inheritDoc @sovryn-zero/lib-base#ReadableLiquity.getZUSDInStabilityPool} */ getZUSDInStabilityPool(overrides?: EthersCallOverrides): Promise<Decimal> { const { stabilityPool } = _getContracts(this.connection); return stabilityPool.getTotalZUSDDeposits({ ...overrides }).then(decimalify); } /** {@inheritDoc @sovryn-zero/lib-base#ReadableLiquity.getZUSDBalance} */ getZUSDBalance(address?: string, overrides?: EthersCallOverrides): Promise<Decimal> { address ??= _requireAddress(this.connection); const { zusdToken } = _getContracts(this.connection); return zusdToken.balanceOf(address, { ...overrides }).then(decimalify); } /** {@inheritDoc @sovryn-zero/lib-base#ReadableLiquity.getZEROBalance} */ getZEROBalance(address?: string, overrides?: EthersCallOverrides): Promise<Decimal> { address ??= _requireAddress(this.connection); const { zeroToken } = _getContracts(this.connection); return zeroToken.balanceOf(address, { ...overrides }).then(decimalify); } /** {@inheritDoc @sovryn-zero/lib-base#ReadableLiquity.getCollateralSurplusBalance} */ getCollateralSurplusBalance(address?: string, overrides?: EthersCallOverrides): Promise<Decimal> { 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<TroveWithPendingRedistribution[]>; /** {@inheritDoc @sovryn-zero/lib-base#ReadableLiquity.(getTroves:2)} */ getTroves(params: TroveListingParams, overrides?: EthersCallOverrides): Promise<UserTrove[]>; async getTroves( params: TroveListingParams, overrides?: EthersCallOverrides ): Promise<UserTrove[]> { 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 */ 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 @sovryn-zero/lib-base#ReadableLiquity.getFees} */ async getFees(overrides?: EthersCallOverrides): Promise<Fees> { const [createFees, total, price, blockTimestamp] = await Promise.all([ this._getFeesFactory(overrides), this.getTotal(overrides), this.getPrice(overrides), _getBlockTimestamp(this.connection, overrides?.blockTag) ]); return createFees(blockTimestamp, total.collateralRatioIsBelowCritical(price)); } /** {@inheritDoc @sovryn-zero/lib-base#ReadableLiquity.getZEROStake} */ async getZEROStake(address?: string, overrides?: EthersCallOverrides): Promise<ZEROStake> { address ??= _requireAddress(this.connection); const { zeroStaking } = _getContracts(this.connection); const [stakedZERO, collateralGain, zusdGain] = await Promise.all( [ zeroStaking.stakes(address, { ...overrides }), zeroStaking.getPendingETHGain(address, { ...overrides }), zeroStaking.getPendingZUSDGain(address, { ...overrides }) ].map(getBigNumber => getBigNumber.then(decimalify)) ); return new ZEROStake(stakedZERO, collateralGain, zusdGain); } /** {@inheritDoc @sovryn-zero/lib-base#ReadableLiquity.getTotalStakedZERO} */ async getTotalStakedZERO(overrides?: EthersCallOverrides): Promise<Decimal> { const { zeroStaking } = _getContracts(this.connection); return zeroStaking.totalZEROStaked({ ...overrides }).then(decimalify); } /** {@inheritDoc @sovryn-zero/lib-base#ReadableLiquity.getFrontendStatus} */ async getFrontendStatus( address?: string, overrides?: EthersCallOverrides ): Promise<FrontendStatus> { 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> = T extends Promise<infer U> ? U : T; type BackendTroves = Resolved<ReturnType<MultiTroveGetter["getMultipleSortedTroves"]>>; 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.snapshotZUSDDebt)) ) ); /** * Variant of {@link ReadableEthersLiquity} that exposes a {@link @sovryn-zero/lib-base#LiquityStore}. * * @public */ export interface ReadableEthersLiquityWithStore<T extends LiquityStore = LiquityStore> extends ReadableEthersLiquity { /** An object that implements LiquityStore. */ readonly store: T; } class BlockPolledLiquityStoreBasedCache implements _LiquityReadCache<[overrides?: EthersCallOverrides]> { private _store: BlockPolledLiquityStore; constructor(store: BlockPolledLiquityStore) { this._store = store; } 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) ); } getTotalRedistributed(overrides?: EthersCallOverrides): Trove | undefined { if (this._blockHit(overrides)) { return this._store.state.totalRedistributed; } } getTroveBeforeRedistribution( address?: string, overrides?: EthersCallOverrides ): TroveWithPendingRedistribution | undefined { if (this._userHit(address, overrides)) { return this._store.state.troveBeforeRedistribution; } } getTrove(address?: string, overrides?: EthersCallOverrides): UserTrove | undefined { if (this._userHit(address, overrides)) { return this._store.state.trove; } } getNumberOfTroves(overrides?: EthersCallOverrides): number | undefined { if (this._blockHit(overrides)) { return this._store.state.numberOfTroves; } } getPrice(overrides?: EthersCallOverrides): Decimal | undefined { if (this._blockHit(overrides)) { return this._store.state.price; } } getTotal(overrides?: EthersCallOverrides): Trove | undefined { if (this._blockHit(overrides)) { return this._store.state.total; } } getStabilityDeposit( address?: string, overrides?: EthersCallOverrides ): StabilityDeposit | undefined { if (this._userHit(address, overrides)) { return this._store.state.stabilityDeposit; } } getZUSDInStabilityPool(overrides?: EthersCallOverrides): Decimal | undefined { if (this._blockHit(overrides)) { return this._store.state.zusdInStabilityPool; } } getZUSDBalance(address?: string, overrides?: EthersCallOverrides): Decimal | undefined { if (this._userHit(address, overrides)) { return this._store.state.zusdBalance; } } getZEROBalance(address?: string, overrides?: EthersCallOverrides): Decimal | undefined { if (this._userHit(address, overrides)) { return this._store.state.zeroBalance; } } getCollateralSurplusBalance( address?: string, overrides?: EthersCallOverrides ): Decimal | undefined { if (this._userHit(address, overrides)) { return this._store.state.collateralSurplusBalance; } } getFees(overrides?: EthersCallOverrides): Fees | undefined { if (this._blockHit(overrides)) { return this._store.state.fees; } } getZEROStake(address?: string, overrides?: EthersCallOverrides): ZEROStake | undefined { if (this._userHit(address, overrides)) { return this._store.state.zeroStake; } } getTotalStakedZERO(overrides?: EthersCallOverrides): Decimal | undefined { if (this._blockHit(overrides)) { return this._store.state.totalStakedZERO; } } getFrontendStatus( address?: string, overrides?: EthersCallOverrides ): { status: "unregistered" } | { status: "registered"; kickbackRate: Decimal } | undefined { if (this._frontendHit(address, overrides)) { return this._store.state.frontend; } } getTroves() { return undefined; } } class _BlockPolledReadableEthersLiquity extends _CachedReadableLiquity<[overrides?: EthersCallOverrides]> implements ReadableEthersLiquityWithStore<BlockPolledLiquityStore> { readonly connection: EthersLiquityConnection; readonly store: BlockPolledLiquityStore; constructor(readable: ReadableEthersLiquity) { const store = new BlockPolledLiquityStore(readable); super(readable, new BlockPolledLiquityStoreBasedCache(store)); this.store = store; this.connection = readable.connection; } hasStore(store?: EthersLiquityStoreOption): boolean { return store === undefined || store === "blockPolled"; } _getActivePool(): Promise<Trove> { throw new Error("Method not implemented."); } _getDefaultPool(): Promise<Trove> { throw new Error("Method not implemented."); } _getFeesFactory(): Promise<(blockTimestamp: number, recoveryMode: boolean) => Fees> { throw new Error("Method not implemented."); } _getRemainingLiquidityMiningZERORewardCalculator(): Promise<(blockTimestamp: number) => Decimal> { throw new Error("Method not implemented."); } }