Skip to content

Commit

Permalink
feat(run-protocol): charge penalty for liquidation (#4996)
Browse files Browse the repository at this point in the history
* refactor(run-protocol): broaden scope of collateral reserve to assets generally

* types

* LiquidationPenalty param on vault manager

* charge liquidation penalty

* test for liquidation math

* review

* Revert "refactor(run-protocol): broaden scope of collateral reserve to assets generally"

This reverts commit 34a45e3.
  • Loading branch information
turadg authored Apr 6, 2022
1 parent cc73834 commit 5467be4
Show file tree
Hide file tree
Showing 15 changed files with 175 additions and 75 deletions.
2 changes: 1 addition & 1 deletion packages/governance/src/contractGovernor.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ const validateQuestionFromCounter = async (zoe, electorate, voteCounter) => {
* governed: {
* issuerKeywordRecord: IssuerKeywordRecord,
* terms: {governedParams: {[CONTRACT_ELECTORATE]: Amount<'set'>}},
* privateArgs: unknown,
* privateArgs: Record<string, unknown>,
* }
* }>}
*/
Expand Down
3 changes: 2 additions & 1 deletion packages/run-protocol/src/collect.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ export const mapValues = (obj, f) =>
/** @type { <X, Y>(xs: X[], ys: Y[]) => [X, Y][]} */
export const zip = (xs, ys) => harden(xs.map((x, i) => [x, ys[+i]]));

/** @type { <K extends string, V>(obj: Record<K, ERef<V>>) => Promise<Record<string, V>> } */
/** @type { <K extends string, V>(obj: Record<K, ERef<V>>) => Promise<Record<K, V>> } */
export const allValues = async obj => {
const resolved = await Promise.all(values(obj));
// @ts-expect-error cast
return harden(fromEntries(zip(keys(obj), resolved)));
};
10 changes: 10 additions & 0 deletions packages/run-protocol/src/econ-behaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ const BASIS_POINTS = 10_000n;

const CENTRAL_DENOM_NAME = 'urun';

/**
* @file A collection of productions, each of which declares inputs and outputs.
* Each function is passed a set of powers for reading from and writing to the vat config.
*
* Each of the things they produce they're responsible for resolving or setting.
*
* In production called by @agoric/vats to bootstrap.
*/

/**
* @param {EconomyBootstrapPowers} powers
* @param {{ committeeName: string, committeeSize: number }} electorateTerms
Expand Down Expand Up @@ -261,6 +270,7 @@ export const startVaultFactory = async (
// XXX the values aren't used. May be addressed by https://github.com/Agoric/agoric-sdk/issues/4861
debtLimit: AmountMath.make(centralBrand, 0n),
liquidationMargin: makeRatio(0n, centralBrand),
liquidationPenalty: makeRatio(10n, centralBrand, 100n),
interestRate: makeRatio(0n, centralBrand, BASIS_POINTS),
loanFee: makeRatio(0n, centralBrand, BASIS_POINTS),
};
Expand Down
7 changes: 6 additions & 1 deletion packages/run-protocol/src/vaultFactory/liquidateMinimum.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ const trace = makeTracer('LM');
const start = async zcf => {
const { amm } = zcf.getTerms();

/**
* @param {Amount<'nat'>} runDebt
*/
const makeDebtorHook = runDebt => {
const runBrand = runDebt.brand;
return async debtorSeat => {
Expand Down Expand Up @@ -95,8 +98,10 @@ const start = async zcf => {
return harden({ creatorFacet });
};

/** @typedef {ContractOf<typeof start>} LiquidationContract */

/**
* @param {LiquidationCreatorFacet} creatorFacet
* @param {LiquidationContract['creatorFacet']} creatorFacet
*/
const makeLiquidationStrategy = creatorFacet => {
const makeInvitation = async runDebt =>
Expand Down
56 changes: 47 additions & 9 deletions packages/run-protocol/src/vaultFactory/liquidation.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,29 @@

import { E } from '@endo/eventual-send';
import { AmountMath } from '@agoric/ertp';
import { offerTo } from '@agoric/zoe/src/contractSupport/index.js';
import {
ceilMultiplyBy,
offerTo,
} from '@agoric/zoe/src/contractSupport/index.js';
import { makeTracer } from '../makeTracer.js';

const trace = makeTracer('LIQ');

/**
* @param {Amount<'nat'>} proceeds
* @param {Amount<'nat'>} debt - after incurring penalty
* @param {Amount<'nat'>} penaltyPortion
*/
const partitionProceeds = (proceeds, debt, penaltyPortion) => {
const debtPaid = AmountMath.min(proceeds, debt);

// Pay as much of the penalty as possible
const penaltyProceeds = AmountMath.min(penaltyPortion, debtPaid);
const runToBurn = AmountMath.subtract(debtPaid, penaltyProceeds);

return { debtPaid, penaltyProceeds, runToBurn };
};

/**
* Liquidates a Vault, using the strategy to parameterize the particular
* contract being used. The strategy provides a KeywordMapping and proposal
Expand All @@ -23,6 +41,8 @@ const trace = makeTracer('LIQ');
* ) => void} burnLosses
* @param {LiquidationStrategy} strategy
* @param {Brand} collateralBrand
* @param {ZCFSeat} penaltyPoolSeat
* @param {Ratio} penaltyRate
* @returns {Promise<InnerVault>}
*/
const liquidate = async (
Expand All @@ -31,10 +51,16 @@ const liquidate = async (
burnLosses,
strategy,
collateralBrand,
penaltyPoolSeat,
penaltyRate,
) => {
innerVault.liquidating();
const debt = innerVault.getCurrentDebt();
const { brand: runBrand } = debt;

const debtBeforePenalty = innerVault.getCurrentDebt();
const penalty = ceilMultiplyBy(debtBeforePenalty, penaltyRate);

const debt = AmountMath.add(debtBeforePenalty, penalty);

const vaultZcfSeat = innerVault.getVaultSeat();

const collateralToSell = vaultZcfSeat.getAmountAllocated(
Expand All @@ -57,13 +83,24 @@ const liquidate = async (

// Now we need to know how much was sold so we can pay off the debt.
// We can use this because only liquidation adds RUN to the vaultSeat.
const proceeds = vaultZcfSeat.getAmountAllocated('RUN', runBrand);
const { debtPaid, penaltyProceeds, runToBurn } = partitionProceeds(
vaultZcfSeat.getAmountAllocated('RUN', debt.brand),
debt,
penalty,
);

trace({ debt, debtPaid, penaltyProceeds, runToBurn });

// Allocate penalty portion of proceeds to a seat that will be transferred to reserve
penaltyPoolSeat.incrementBy(
vaultZcfSeat.decrementBy(harden({ RUN: penaltyProceeds })),
);
zcf.reallocate(penaltyPoolSeat, vaultZcfSeat);

const isShortfall = !AmountMath.isGTE(proceeds, debt);
const runToBurn = isShortfall ? proceeds : debt;
trace({ debt, isShortfall, runToBurn });
burnLosses(harden({ RUN: runToBurn }), vaultZcfSeat);
innerVault.liquidated(AmountMath.subtract(debt, runToBurn));

// Accounting complete. Update the vault state.
innerVault.liquidated(AmountMath.subtract(debt, debtPaid));

// remaining funds are left on the vault for the user to close and claim
return innerVault;
Expand Down Expand Up @@ -99,5 +136,6 @@ const makeDefaultLiquidationStrategy = amm => {

harden(makeDefaultLiquidationStrategy);
harden(liquidate);
harden(partitionProceeds);

export { makeDefaultLiquidationStrategy, liquidate };
export { makeDefaultLiquidationStrategy, liquidate, partitionProceeds };
2 changes: 2 additions & 0 deletions packages/run-protocol/src/vaultFactory/params.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const RECORDING_PERIOD_KEY = 'RecordingPeriod';

export const DEBT_LIMIT_KEY = 'DebtLimit';
export const LIQUIDATION_MARGIN_KEY = 'LiquidationMargin';
export const LIQUIDATION_PENALTY_KEY = 'LiquidationPenalty';
export const INTEREST_RATE_KEY = 'InterestRate';
export const LOAN_FEE_KEY = 'LoanFee';

Expand All @@ -36,6 +37,7 @@ const makeVaultParamManager = initial =>
makeParamManagerSync({
[DEBT_LIMIT_KEY]: [ParamTypes.AMOUNT, initial.debtLimit],
[LIQUIDATION_MARGIN_KEY]: [ParamTypes.RATIO, initial.liquidationMargin],
[LIQUIDATION_PENALTY_KEY]: [ParamTypes.RATIO, initial.liquidationPenalty],
[INTEREST_RATE_KEY]: [ParamTypes.RATIO, initial.interestRate],
[LOAN_FEE_KEY]: [ParamTypes.RATIO, initial.loanFee],
});
Expand Down
15 changes: 3 additions & 12 deletions packages/run-protocol/src/vaultFactory/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
* @typedef {Object} VaultManagerParamValues
* @property {Ratio} liquidationMargin - margin below which collateral will be
* liquidated to satisfy the debt.
* @property {Ratio} liquidationPenalty - penalty charged upon liquidation as proportion of debt
* @property {Ratio} interestRate - annual interest rate charged on loans
* @property {Ratio} loanFee - The fee (in BasisPoints) charged when opening
* or increasing a loan.
Expand All @@ -43,7 +44,8 @@
* @typedef {Object} VaultFactory - the creator facet
* @property {AddVaultType} addVaultType
* @property {() => Promise<Array<Collateral>>} getCollaterals
* @property {() => Allocation} getRewardAllocation,
* @property {() => Allocation} getRewardAllocation
* @property {() => Allocation} getPenaltyAllocation
* @property {() => Instance} getContractGovernor
* @property {() => Promise<Invitation>} makeCollectFeesInvitation
*/
Expand Down Expand Up @@ -108,17 +110,6 @@
* @property {(runDebt: Amount) => Promise<Invitation>} makeInvitation
*/

/**
* @typedef {Object} LiquidationCreatorFacet
* @property {(runDebt: Amount) => Promise<Invitation>} makeDebtorInvitation
*/

/**
* @callback MakeLiquidationStrategy
* @param {LiquidationCreatorFacet} creatorFacet
* @returns {LiquidationStrategy}
*/

/**
* @typedef {Object} DebtStatus
* @property {Timestamp} latestInterestUpdate
Expand Down
15 changes: 8 additions & 7 deletions packages/run-protocol/src/vaultFactory/vaultFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ const { details: X } = assert;

/**
* @param {ZCF<GovernanceTerms<{}> & {
* ammPublicFacet: unknown,
* liquidationInstall: unknown,
* ammPublicFacet: AutoswapPublicFacet,
* liquidationInstall: Installation<import('./liquidateMinimum.js').start>,
* loanTimingParams: {ChargingPeriod: ParamRecord<'nat'>, RecordingPeriod: ParamRecord<'nat'>},
* timerService: TimerService,
* priceAuthority: ERef<PriceAuthority>}>} zcf
Expand Down Expand Up @@ -80,6 +80,7 @@ export const start = async (zcf, privateArgs) => {
/** For temporary staging of newly minted tokens */
const { zcfSeat: mintSeat } = zcf.makeEmptySeatKit();
const { zcfSeat: rewardPoolSeat } = zcf.makeEmptySeatKit();
const { zcfSeat: penaltyPoolSeat } = zcf.makeEmptySeatKit();

/**
* We provide an easy way for the vaultManager to add rewards to
Expand Down Expand Up @@ -164,6 +165,7 @@ export const start = async (zcf, privateArgs) => {
burnDebt,
timerService,
liquidationStrategy,
penaltyPoolSeat,
startTimeStamp,
);
collateralTypes.init(collateralBrand, vm);
Expand Down Expand Up @@ -218,10 +220,6 @@ export const start = async (zcf, privateArgs) => {
);
};

// Eventually the reward pool will live elsewhere. For now it's here for
// bookkeeping. It's needed in tests.
const getRewardAllocation = () => rewardPoolSeat.getCurrentAllocation();

// TODO use named getters of TypedParamManager
const getGovernedParams = paramDesc => {
return vaultParamManagers.get(paramDesc.collateralBrand).getParams();
Expand Down Expand Up @@ -275,9 +273,12 @@ export const start = async (zcf, privateArgs) => {
// TODO move this under governance #3972
addVaultType,
getCollaterals,
getRewardAllocation,
makeCollectFeesInvitation,
getContractGovernor: () => electionManager,

// XXX accessors for tests
getRewardAllocation: rewardPoolSeat.getCurrentAllocation,
getPenaltyAllocation: penaltyPoolSeat.getCurrentAllocation,
});

const vaultFactoryWrapper = Far('powerful vaultFactory wrapper', {
Expand Down
5 changes: 5 additions & 0 deletions packages/run-protocol/src/vaultFactory/vaultManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,14 @@ const trace = makeTracer('VM');
* getDebtLimit: () => Amount<'nat'>,
* getInterestRate: () => Ratio,
* getLiquidationMargin: () => Ratio,
* getLiquidationPenalty: () => Ratio,
* getLoanFee: () => Ratio,
* }} loanParamGetters
* @param {MintAndReallocate} mintAndReallocateWithFee
* @param {BurnDebt} burnDebt
* @param {ERef<TimerService>} timerService
* @param {LiquidationStrategy} liquidationStrategy
* @param {ZCFSeat} penaltyPoolSeat
* @param {Timestamp} startTimeStamp
*/
export const makeVaultManager = (
Expand All @@ -74,6 +76,7 @@ export const makeVaultManager = (
burnDebt,
timerService,
liquidationStrategy,
penaltyPoolSeat,
startTimeStamp,
) => {
/** @type {{brand: Brand<'nat'>}} */
Expand Down Expand Up @@ -143,6 +146,8 @@ export const makeVaultManager = (
debtMint.burnLosses,
liquidationStrategy,
collateralBrand,
penaltyPoolSeat,
loanParamGetters.getLiquidationPenalty(),
)
.then(() => {
prioritizedVaults?.removeVault(key);
Expand Down
1 change: 1 addition & 0 deletions packages/run-protocol/test/swingsetTests/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const makeRates = debtBrand => {
return {
debtLimit: AmountMath.make(debtBrand, 1_000_000n),
liquidationMargin: makeRatio(105n, debtBrand),
liquidationPenalty: makeRatio(10n, debtBrand, 100n, debtBrand),
interestRate: makeRatio(250n, debtBrand, BASIS_POINTS),
loanFee: makeRatio(200n, debtBrand, BASIS_POINTS),
};
Expand Down
47 changes: 47 additions & 0 deletions packages/run-protocol/test/vaultFactory/test-liquidation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// @ts-check
// Must be first to set up globals
import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js';

import { AmountMath } from '@agoric/ertp';
import { Far } from '@endo/marshal';
import { partitionProceeds } from '../../src/vaultFactory/liquidation.js';

export const mockBrand = Far('brand');

const amount = n => AmountMath.make(mockBrand, BigInt(n));

for (const [
proceeds,
debt,
penaltyPortion,
debtPaid,
penaltyProceeds,
runToBurn,
] of [
// no proceeds
[0, 0, 0, 0, 0, 0],
[0, 100, 10, 0, 0, 0],
// proceeds gte debt
[100, 100, 10, 100, 10, 90],
[200, 100, 10, 100, 10, 90],
// proceeds less than debt
[100, 200, 10, 100, 10, 90],
[100, 200, 200, 100, 100, 0],
]) {
test(`partitionProceeds: (${proceeds} for ${debt} with ${penaltyPortion} penalty) => ${{
debtPaid,
penaltyProceeds,
runToBurn,
}}`, t => {
const result = partitionProceeds(
amount(proceeds),
amount(debt),
amount(penaltyPortion),
);
t.deepEqual(result, {
debtPaid: amount(debtPaid),
penaltyProceeds: amount(penaltyProceeds),
runToBurn: amount(runToBurn),
});
});
}
Loading

0 comments on commit 5467be4

Please sign in to comment.