Skip to content

Commit 3117eef

Browse files
committed
feat: liquidity pool borrower and repayer facets
1 parent 65b9ec6 commit 3117eef

10 files changed

+803
-78
lines changed

packages/fast-usdc/src/exos/liquidity-pool.js

+181-55
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,76 @@
1-
import {
2-
AmountMath,
3-
AmountShape,
4-
PaymentShape,
5-
RatioShape,
6-
} from '@agoric/ertp';
1+
import { AmountMath, AmountShape } from '@agoric/ertp';
72
import {
83
makeRecorderTopic,
94
TopicsRecordShape,
105
} from '@agoric/zoe/src/contractSupport/topics.js';
11-
import { depositToSeat } from '@agoric/zoe/src/contractSupport/zoeHelpers.js';
126
import { SeatShape } from '@agoric/zoe/src/typeGuards.js';
137
import { M } from '@endo/patterns';
148
import { Fail, q } from '@endo/errors';
159
import {
10+
borrowCalc,
1611
depositCalc,
1712
makeParity,
13+
repayCalc,
1814
withdrawCalc,
19-
withFees,
2015
} from '../pool-share-math.js';
21-
import { makeProposalShapes } from '../type-guards.js';
16+
import {
17+
makeNatAmountShape,
18+
makeProposalShapes,
19+
PoolMetricsShape,
20+
} from '../type-guards.js';
2221

2322
/**
2423
* @import {Zone} from '@agoric/zone';
25-
* @import {Remote, TypedPattern} from '@agoric/internal'
24+
* @import {Remote} from '@agoric/internal'
2625
* @import {StorageNode} from '@agoric/internal/src/lib-chainStorage.js'
27-
* @import {MakeRecorderKit, RecorderKit} from '@agoric/zoe/src/contractSupport/recorder.js'
26+
* @import {MakeRecorderKit} from '@agoric/zoe/src/contractSupport/recorder.js'
2827
* @import {USDCProposalShapes, ShareWorth} from '../pool-share-math.js'
28+
* @import {PoolStats} from '../types.js';
2929
*/
3030

31-
const { add, isEqual } = AmountMath;
31+
const { add, isEqual, makeEmpty } = AmountMath;
3232

3333
/** @param {Brand} brand */
3434
const makeDust = brand => AmountMath.make(brand, 1n);
3535

3636
/**
37-
* Use of pool-share-math in offer handlers below assumes that
38-
* the pool balance represented by the USDC allocation in poolSeat
39-
* is the same as the pool balance represented by the numerator
40-
* of shareWorth.
37+
* Verifies that the total pool balance (unencumbered + encumbered) matches the
38+
* shareWorth numerator. The total pool balance consists of:
39+
* 1. unencumbered balance - USDC available in the pool for borrowing
40+
* 2. encumbered balance - USDC currently lent out
4141
*
42-
* Well, almost: they're the same modulo the dust used
43-
* to initialize shareWorth with a non-zero denominator.
42+
* A negligible `dust` amount is used to initialize shareWorth with a non-zero
43+
* denominator. It must remain in the pool at all times.
4444
*
4545
* @param {ZCFSeat} poolSeat
4646
* @param {ShareWorth} shareWorth
4747
* @param {Brand} USDC
48+
* @param {Amount<'nat'>} encumberedBalance
4849
*/
49-
const checkPoolBalance = (poolSeat, shareWorth, USDC) => {
50-
const available = poolSeat.getAmountAllocated('USDC', USDC);
50+
const checkPoolBalance = (poolSeat, shareWorth, USDC, encumberedBalance) => {
51+
const unencumberedBalance = poolSeat.getAmountAllocated('USDC', USDC);
5152
const dust = makeDust(USDC);
52-
isEqual(add(available, dust), shareWorth.numerator) ||
53-
Fail`🚨 pool balance ${q(available)} inconsistent with shareWorth ${q(shareWorth)}`;
53+
const grossBalance = add(add(unencumberedBalance, dust), encumberedBalance);
54+
isEqual(grossBalance, shareWorth.numerator) ||
55+
Fail`🚨 pool balance ${q(unencumberedBalance)} and encumbered balance ${q(encumberedBalance)} inconsistent with shareWorth ${q(shareWorth)}`;
5456
};
5557

58+
/**
59+
* @typedef {{
60+
* Principal: Amount<'nat'>;
61+
* PoolFee: Amount<'nat'>;
62+
* ContractFee: Amount<'nat'>;
63+
* }} RepayAmountKWR
64+
*/
65+
66+
/**
67+
* @typedef {{
68+
* Principal: Payment<'nat'>;
69+
* PoolFee: Payment<'nat'>;
70+
* ContractFee: Payment<'nat'>;
71+
* }} RepayPaymentKWR
72+
*/
73+
5674
/**
5775
* @param {Zone} zone
5876
* @param {ZCF} zcf
@@ -65,11 +83,25 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
6583
return zone.exoClassKit(
6684
'Liquidity Pool',
6785
{
68-
feeSink: M.interface('feeSink', {
69-
receive: M.call(AmountShape, PaymentShape).returns(M.promise()),
86+
borrower: M.interface('borrower', {
87+
getBalance: M.call().returns(AmountShape),
88+
borrow: M.call(
89+
SeatShape,
90+
harden({ USDC: makeNatAmountShape(USDC, 1n) }),
91+
).returns(),
92+
}),
93+
repayer: M.interface('repayer', {
94+
repay: M.call(
95+
SeatShape,
96+
harden({
97+
Principal: makeNatAmountShape(USDC, 1n),
98+
PoolFee: makeNatAmountShape(USDC, 0n),
99+
ContractFee: makeNatAmountShape(USDC, 0n),
100+
}),
101+
).returns(),
70102
}),
71103
external: M.interface('external', {
72-
publishShareWorth: M.call().returns(),
104+
publishPoolMetrics: M.call().returns(),
73105
}),
74106
depositHandler: M.interface('depositHandler', {
75107
handle: M.call(SeatShape, M.any()).returns(M.promise()),
@@ -92,57 +124,143 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
92124
const proposalShapes = makeProposalShapes({ USDC, PoolShares });
93125
const shareWorth = makeParity(makeDust(USDC), PoolShares);
94126
const { zcfSeat: poolSeat } = zcf.makeEmptySeatKit();
95-
const shareWorthRecorderKit = tools.makeRecorderKit(node, RatioShape);
127+
const { zcfSeat: feeSeat } = zcf.makeEmptySeatKit();
128+
const poolMetricsRecorderKit = tools.makeRecorderKit(
129+
node,
130+
PoolMetricsShape,
131+
);
132+
const encumberedBalance = makeEmpty(USDC);
133+
/** @type {PoolStats} */
134+
const poolStats = harden({
135+
totalBorrows: makeEmpty(USDC),
136+
totalContractFees: makeEmpty(USDC),
137+
totalPoolFees: makeEmpty(USDC),
138+
totalRepays: makeEmpty(USDC),
139+
});
96140
return {
97-
shareMint,
98-
shareWorth,
141+
/** used for `checkPoolBalance` invariant. aka 'outstanding borrows' */
142+
encumberedBalance,
143+
feeSeat,
144+
poolStats,
145+
poolMetricsRecorderKit,
99146
poolSeat,
100147
PoolShares,
101148
proposalShapes,
102-
shareWorthRecorderKit,
149+
shareMint,
150+
shareWorth,
103151
};
104152
},
105153
{
106-
feeSink: {
154+
borrower: {
155+
getBalance() {
156+
const { poolSeat } = this.state;
157+
return poolSeat.getAmountAllocated('USDC', USDC);
158+
},
107159
/**
108-
* @param {Amount<'nat'>} amount
109-
* @param {Payment<'nat'>} payment
160+
* @param {ZCFSeat} toSeat
161+
* @param {{ USDC: Amount<'nat'>}} amountKWR
110162
*/
111-
async receive(amount, payment) {
112-
const { poolSeat, shareWorth } = this.state;
113-
const { external } = this.facets;
114-
await depositToSeat(
115-
zcf,
116-
poolSeat,
117-
harden({ USDC: amount }),
118-
harden({ USDC: payment }),
163+
borrow(toSeat, amountKWR) {
164+
const { encumberedBalance, poolSeat, poolStats } = this.state;
165+
166+
// Validate amount is available in pool
167+
const post = borrowCalc(
168+
amountKWR.USDC,
169+
poolSeat.getAmountAllocated('USDC', USDC),
170+
encumberedBalance,
171+
poolStats,
119172
);
120-
this.state.shareWorth = withFees(shareWorth, amount);
121-
external.publishShareWorth();
173+
174+
// COMMIT POINT
175+
try {
176+
zcf.atomicRearrange(harden([[poolSeat, toSeat, amountKWR]]));
177+
} catch (cause) {
178+
const reason = Error('🚨 cannot commit borrow', { cause });
179+
console.error(reason.message, cause);
180+
zcf.shutdownWithFailure(reason);
181+
}
182+
183+
Object.assign(this.state, post);
184+
this.facets.external.publishPoolMetrics();
122185
},
186+
// TODO method to repay failed `LOA.deposit()`
123187
},
188+
repayer: {
189+
/**
190+
* @param {ZCFSeat} fromSeat
191+
* @param {RepayAmountKWR} amounts
192+
*/
193+
repay(fromSeat, amounts) {
194+
const {
195+
encumberedBalance,
196+
feeSeat,
197+
poolSeat,
198+
poolStats,
199+
shareWorth,
200+
} = this.state;
201+
checkPoolBalance(poolSeat, shareWorth, USDC, encumberedBalance);
202+
203+
const fromSeatAllocation = fromSeat.getCurrentAllocation();
204+
// Validate allocation equals amounts and Principal <= encumberedBalance
205+
const post = repayCalc(
206+
shareWorth,
207+
fromSeatAllocation,
208+
amounts,
209+
encumberedBalance,
210+
poolStats,
211+
);
124212

213+
const { ContractFee, ...rest } = amounts;
214+
215+
// COMMIT POINT
216+
try {
217+
zcf.atomicRearrange(
218+
harden([
219+
[
220+
fromSeat,
221+
poolSeat,
222+
rest,
223+
{ USDC: add(amounts.PoolFee, amounts.Principal) },
224+
],
225+
[fromSeat, feeSeat, { ContractFee }, { USDC: ContractFee }],
226+
]),
227+
);
228+
} catch (cause) {
229+
const reason = Error('🚨 cannot commit repay', { cause });
230+
console.error(reason.message, cause);
231+
zcf.shutdownWithFailure(reason);
232+
}
233+
234+
Object.assign(this.state, post);
235+
this.facets.external.publishPoolMetrics();
236+
},
237+
},
125238
external: {
126-
publishShareWorth() {
127-
const { shareWorth } = this.state;
128-
const { recorder } = this.state.shareWorthRecorderKit;
239+
publishPoolMetrics() {
240+
const { poolStats, shareWorth, encumberedBalance } = this.state;
241+
const { recorder } = this.state.poolMetricsRecorderKit;
129242
// Consumers of this .write() are off-chain / outside the VM.
130243
// And there's no way to recover from a failed write.
131244
// So don't await.
132-
void recorder.write(shareWorth);
245+
void recorder.write({
246+
encumberedBalance,
247+
shareWorth,
248+
...poolStats,
249+
});
133250
},
134251
},
135252

136253
depositHandler: {
137254
/** @param {ZCFSeat} lp */
138255
async handle(lp) {
139-
const { shareWorth, shareMint, poolSeat } = this.state;
256+
const { shareWorth, shareMint, poolSeat, encumberedBalance } =
257+
this.state;
140258
const { external } = this.facets;
141259

142260
/** @type {USDCProposalShapes['deposit']} */
143261
// @ts-expect-error ensured by proposalShape
144262
const proposal = lp.getProposal();
145-
checkPoolBalance(poolSeat, shareWorth, USDC);
263+
checkPoolBalance(poolSeat, shareWorth, USDC, encumberedBalance);
146264
const post = depositCalc(shareWorth, proposal);
147265

148266
// COMMIT POINT
@@ -165,20 +283,21 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
165283
console.error(reason.message, cause);
166284
zcf.shutdownWithFailure(reason);
167285
}
168-
external.publishShareWorth();
286+
external.publishPoolMetrics();
169287
},
170288
},
171289
withdrawHandler: {
172290
/** @param {ZCFSeat} lp */
173291
async handle(lp) {
174-
const { shareWorth, shareMint, poolSeat } = this.state;
292+
const { shareWorth, shareMint, poolSeat, encumberedBalance } =
293+
this.state;
175294
const { external } = this.facets;
176295

177296
/** @type {USDCProposalShapes['withdraw']} */
178297
// @ts-expect-error ensured by proposalShape
179298
const proposal = lp.getProposal();
180299
const { zcfSeat: burn } = zcf.makeEmptySeatKit();
181-
checkPoolBalance(poolSeat, shareWorth, USDC);
300+
checkPoolBalance(poolSeat, shareWorth, USDC, encumberedBalance);
182301
const post = withdrawCalc(shareWorth, proposal);
183302

184303
// COMMIT POINT
@@ -201,7 +320,7 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
201320
console.error(reason.message, cause);
202321
zcf.shutdownWithFailure(reason);
203322
}
204-
external.publishShareWorth();
323+
external.publishPoolMetrics();
205324
},
206325
},
207326
public: {
@@ -222,18 +341,25 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
222341
);
223342
},
224343
getPublicTopics() {
225-
const { shareWorthRecorderKit } = this.state;
344+
const { poolMetricsRecorderKit } = this.state;
226345
return {
227-
shareWorth: makeRecorderTopic('shareWorth', shareWorthRecorderKit),
346+
poolMetrics: makeRecorderTopic(
347+
'poolMetrics',
348+
poolMetricsRecorderKit,
349+
),
228350
};
229351
},
230352
},
231353
},
232354
{
233355
finish: ({ facets: { external } }) => {
234-
void external.publishShareWorth();
356+
void external.publishPoolMetrics();
235357
},
236358
},
237359
);
238360
};
239361
harden(prepareLiquidityPoolKit);
362+
363+
/**
364+
* @typedef {ReturnType<ReturnType<typeof prepareLiquidityPoolKit>>} LiquidityPoolKit
365+
*/

0 commit comments

Comments
 (0)