@sovryn-zero/lib-ethers
Version:
Sovryn Zero SDK Ethers-based implementation
632 lines (521 loc) • 20.7 kB
text/typescript
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.");
}
}