Skip to content

Commit

Permalink
feat(vault): liquidation penalty handled by liquidation contracts (#5343
Browse files Browse the repository at this point in the history
)

* handleFooOffer style per #5179

* chore(zoe): more typing offerTo and Invitation

* plan

* factored penalties into liquidation contracts

* rm obsolete partitionProceeds

* lint and comments

* named parameters for makeGovernedTerms()

* wire reservePublicFacet into vaultFactory terms

* rm obsolete penaltyPoolSeat

* fixup! wire reservePublicFacet into vaultFactory terms

* fixup! rm obsolete penaltyPoolSeat

* consume reserve instance instead of facet

* typecheck liquidator,vaultFactory tests

* fix test typo tsc caught

* fix invitation param and typecheck that would have caught it

* update comment docs

* work around type resolution shortcomings

* better comments

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
  • Loading branch information
turadg and mergify[bot] authored May 16, 2022
1 parent 9d27c7f commit ce1cfaf
Show file tree
Hide file tree
Showing 20 changed files with 267 additions and 220 deletions.
6 changes: 5 additions & 1 deletion packages/governance/src/contractGovernance/paramManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
} from './assertions.js';
import { CONTRACT_ELECTORATE } from './governParam.js';

const { details: X } = assert;
const { details: X, quote: q } = assert;

/**
* @param {ParamManagerBase} paramManager
Expand All @@ -29,6 +29,10 @@ const assertElectorateMatches = (paramManager, governedParams) => {
const {
[CONTRACT_ELECTORATE]: { value: paramElectorate },
} = governedParams;
assert(
paramElectorate,
X`Missing ${q(CONTRACT_ELECTORATE)} term in ${q(governedParams)}`,
);
assert(
keyEQ(managerElectorate, paramElectorate),
X`Electorate in manager (${managerElectorate})} incompatible with terms (${paramElectorate}`,
Expand Down
6 changes: 5 additions & 1 deletion packages/run-protocol/src/proposals/core-proposal.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,11 @@ const SHARED_MAIN_MANIFEST = harden({
},
},
instance: {
consume: { amm: 'amm', economicCommittee: 'economicCommittee' },
consume: {
amm: 'amm',
economicCommittee: 'economicCommittee',
reserve: 'reserve',
},
produce: {
VaultFactory: 'VaultFactory',
Treasury: 'VaultFactory',
Expand Down
38 changes: 23 additions & 15 deletions packages/run-protocol/src/proposals/econ-behaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ export const startVaultFactory = async (
{
consume: {
chainTimerService,
priceAuthority,
priceAuthority: priceAuthorityP,
zoe,
feeMintAccess: feeMintAccessP, // ISSUE: why doeszn't Zoe await this?
economicCommitteeCreatorFacet: electorateCreatorFacet,
Expand Down Expand Up @@ -296,27 +296,35 @@ export const startVaultFactory = async (
loanFee: makeRatio(0n, centralBrand, BASIS_POINTS),
};

const [ammInstance, electorateInstance, contractGovernorInstall] =
await Promise.all([
instance.consume.amm,
instance.consume.economicCommittee,
contractGovernor,
]);
const [
ammInstance,
electorateInstance,
contractGovernorInstall,
reserveInstance,
] = await Promise.all([
instance.consume.amm,
instance.consume.economicCommittee,
contractGovernor,
instance.consume.reserve,
]);
const ammPublicFacet = await E(zoe).getPublicFacet(ammInstance);
const feeMintAccess = await feeMintAccessP;
const pa = await priceAuthority;
const priceAuthority = await priceAuthorityP;
const reservePublicFacet = await E(zoe).getPublicFacet(reserveInstance);
const timer = await chainTimerService;
const vaultFactoryTerms = makeGovernedTerms(
pa,
loanParams,
installations.liquidate,
const vaultFactoryTerms = makeGovernedTerms({
priceAuthority,
reservePublicFacet,
loanTiming: loanParams,
liquidationInstall: installations.liquidate,
timer,
invitationAmount,
vaultManagerParams,
ammPublicFacet,
liquidationDetailTerms(centralBrand),
AmountMath.make(centralBrand, minInitialDebt),
);
liquidationTerms: liquidationDetailTerms(centralBrand),
minInitialDebt: AmountMath.make(centralBrand, minInitialDebt),
bootstrapPaymentValue: 0n,
});

const governorTerms = harden({
timer,
Expand Down
45 changes: 40 additions & 5 deletions packages/run-protocol/src/vaultFactory/liquidateIncrementally.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ const trace = makeTracer('LiqI', false);
* price impact on the AMM of any one sale. Each block it will compute
* a tranche of collateral to sell, where the size is a function of
* the amount of that collateral in the AMM pool and the desired price impact.
* It presently consults the AMM and Oracle for whether to sell.
*
* The next revision of this will work as follows...
*
* It then gets 3 prices for the current tranche:
* - AMM quote - compute XYK locally based on the pool sizes
* - Reserve quote - based on a low price at which the Reserve will purchase
Expand All @@ -47,6 +51,7 @@ const trace = makeTracer('LiqI', false);
* @typedef {{
* amm: XYKAMMPublicFacet,
* priceAuthority: PriceAuthority,
* reservePublicFacet: AssetReservePublicFacet,
* timerService: TimerService,
* debtBrand: Brand,
* MaxImpactBP: NatValue,
Expand All @@ -59,6 +64,7 @@ const start = async zcf => {
const {
amm,
priceAuthority,
reservePublicFacet,
timerService,
debtBrand,
MaxImpactBP,
Expand All @@ -74,6 +80,10 @@ const start = async zcf => {
const asFloat = (numerator, denominator) =>
Number(numerator) / Number(denominator);

// TODO(5467)) distribute penalties to the reserve
assert(reservePublicFacet, 'Missing reservePublicFacet');
const { zcfSeat: penaltyPoolSeat } = zcf.makeEmptySeatKit();

/**
* Compute the tranche size whose sale on the AMM would have
* a price impact of MAX_IMPACT_BP.
Expand Down Expand Up @@ -242,16 +252,28 @@ const start = async zcf => {
trace('offerResult', { amounts });
}

const debtorHook = async (debtorSeat, { debt: originalDebt }) => {
trace('LIQ', originalDebt);
/**
* @param {ZCFSeat} debtorSeat
* @param {object} options
* @param {Amount<'nat'>} options.debt Debt before penalties
* @param {Ratio} options.penaltyRate
*/
const handleLiquidateOffer = async (
debtorSeat,
{ debt: originalDebt, penaltyRate },
) => {
assertProposalShape(debtorSeat, {
give: { In: null },
});
assert(
originalDebt.brand === debtBrand,
X`Cannot liquidate to ${originalDebt.brand}`,
);
for await (const t of processTranches(debtorSeat, originalDebt)) {
const penalty = ceilMultiplyBy(originalDebt, penaltyRate);
const debtWithPenalty = AmountMath.add(originalDebt, penalty);
trace('LIQ', { originalDebt, debtWithPenalty });

for await (const t of processTranches(debtorSeat, debtWithPenalty)) {
const { collateral, oracleLimit, ammProceeds, debt } = t;
trace(`OFFER TO DEBT: `, {
collateral,
Expand All @@ -276,15 +298,28 @@ const start = async zcf => {
});
}
}

// Now we need to know how much was sold so we can pay off the debt.
// We can use this seat because only liquidation adds debt brand to it..
const debtPaid = debtorSeat.getAmountAllocated('Out', debtBrand);
const penaltyPaid = AmountMath.min(penalty, debtPaid);

// Allocate penalty portion of proceeds to a seat that will hold it for transfer to reserve
penaltyPoolSeat.incrementBy(
debtorSeat.decrementBy(harden({ Out: penaltyPaid })),
);
zcf.reallocate(penaltyPoolSeat, debtorSeat);

debtorSeat.exit();
trace('exit seat');
};

/**
* @type {ERef<Liquidator>}
*/
const creatorFacet = Far('debtorInvitationCreator', {
makeLiquidateInvitation: () => zcf.makeInvitation(debtorHook, 'Liquidate'),
const creatorFacet = Far('debtorInvitationCreator (incrementally)', {
makeLiquidateInvitation: () =>
zcf.makeInvitation(handleLiquidateOffer, 'Liquidate'),
});

return harden({ creatorFacet });
Expand Down
43 changes: 34 additions & 9 deletions packages/run-protocol/src/vaultFactory/liquidateMinimum.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
// @ts-check

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

Expand All @@ -26,10 +29,19 @@ const start = async zcf => {

/**
* @param {ZCFSeat} debtorSeat
* @param {{ debt: Amount<'nat'> }} options
* @param {object} options
* @param {Amount<'nat'>} options.debt Debt before penalties
* @param {Ratio} options.penaltyRate
*/
const debtorHook = async (debtorSeat, { debt }) => {
const debtBrand = debt.brand;
const handleLiquidationOffer = async (
debtorSeat,
{ debt: originalDebt, penaltyRate },
) => {
// XXX does not distribute penalties anywhere
const { zcfSeat: penaltyPoolSeat } = zcf.makeEmptySeatKit();
const penalty = ceilMultiplyBy(originalDebt, penaltyRate);
const debtWithPenalty = AmountMath.add(originalDebt, penalty);
const debtBrand = originalDebt.brand;
const {
give: { In: amountIn },
} = debtorSeat.getProposal();
Expand All @@ -39,31 +51,44 @@ const start = async zcf => {
give: { In: amountIn },
want: { Out: AmountMath.makeEmpty(debtBrand) },
});
trace(`OFFER TO DEBT: `, debt, amountIn);
trace(`OFFER TO DEBT: `, debtWithPenalty, amountIn);
const { deposited } = await offerTo(
zcf,
swapInvitation,
undefined, // The keywords were mapped already
liqProposal,
debtorSeat,
debtorSeat,
{ stopAfter: debt },
{ stopAfter: debtWithPenalty },
);
const amounts = await deposited;
trace(`Liq results`, {
debt,
debtWithPenalty,
amountIn,
paid: debtorSeat.getCurrentAllocation(),
amounts,
});

// Now we need to know how much was sold so we can pay off the debt.
// We can use this seat because only liquidation adds debt brand to it..
const debtPaid = debtorSeat.getAmountAllocated('Out', debtBrand);
const penaltyPaid = AmountMath.min(penalty, debtPaid);

// Allocate penalty portion of proceeds to a seat that will hold it for transfer to reserve
penaltyPoolSeat.incrementBy(
debtorSeat.decrementBy(harden({ Out: penaltyPaid })),
);
zcf.reallocate(penaltyPoolSeat, debtorSeat);

debtorSeat.exit();
};

/**
* @type {ERef<Liquidator>}
*/
const creatorFacet = Far('debtorInvitationCreator', {
makeLiquidateInvitation: () => zcf.makeInvitation(debtorHook, 'Liquidate'),
const creatorFacet = Far('debtorInvitationCreator (minimum)', {
makeLiquidateInvitation: () =>
zcf.makeInvitation(handleLiquidationOffer, 'Liquidate'),
});

return harden({ creatorFacet });
Expand Down
Loading

0 comments on commit ce1cfaf

Please sign in to comment.