1
- import {
2
- AmountMath ,
3
- AmountShape ,
4
- PaymentShape ,
5
- RatioShape ,
6
- } from '@agoric/ertp' ;
1
+ import { AmountMath , AmountShape } from '@agoric/ertp' ;
7
2
import {
8
3
makeRecorderTopic ,
9
4
TopicsRecordShape ,
10
5
} from '@agoric/zoe/src/contractSupport/topics.js' ;
11
- import { depositToSeat } from '@agoric/zoe/src/contractSupport/zoeHelpers.js' ;
12
6
import { SeatShape } from '@agoric/zoe/src/typeGuards.js' ;
13
7
import { M } from '@endo/patterns' ;
14
8
import { Fail , q } from '@endo/errors' ;
15
9
import {
10
+ borrowCalc ,
16
11
depositCalc ,
17
12
makeParity ,
13
+ repayCalc ,
18
14
withdrawCalc ,
19
- withFees ,
20
15
} 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' ;
22
21
23
22
/**
24
23
* @import {Zone} from '@agoric/zone';
25
- * @import {Remote, TypedPattern } from '@agoric/internal'
24
+ * @import {Remote} from '@agoric/internal'
26
25
* @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'
28
27
* @import {USDCProposalShapes, ShareWorth} from '../pool-share-math.js'
28
+ * @import {PoolStats} from '../types.js';
29
29
*/
30
30
31
- const { add, isEqual } = AmountMath ;
31
+ const { add, isEqual, makeEmpty } = AmountMath ;
32
32
33
33
/** @param {Brand } brand */
34
34
const makeDust = brand => AmountMath . make ( brand , 1n ) ;
35
35
36
36
/**
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
41
41
*
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 .
44
44
*
45
45
* @param {ZCFSeat } poolSeat
46
46
* @param {ShareWorth } shareWorth
47
47
* @param {Brand } USDC
48
+ * @param {Amount<'nat'> } encumberedBalance
48
49
*/
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 ) ;
51
52
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 ) } ` ;
54
56
} ;
55
57
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
+
56
74
/**
57
75
* @param {Zone } zone
58
76
* @param {ZCF } zcf
@@ -65,11 +83,25 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
65
83
return zone . exoClassKit (
66
84
'Liquidity Pool' ,
67
85
{
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 ( ) ,
70
102
} ) ,
71
103
external : M . interface ( 'external' , {
72
- publishShareWorth : M . call ( ) . returns ( ) ,
104
+ publishPoolMetrics : M . call ( ) . returns ( ) ,
73
105
} ) ,
74
106
depositHandler : M . interface ( 'depositHandler' , {
75
107
handle : M . call ( SeatShape , M . any ( ) ) . returns ( M . promise ( ) ) ,
@@ -92,57 +124,143 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
92
124
const proposalShapes = makeProposalShapes ( { USDC , PoolShares } ) ;
93
125
const shareWorth = makeParity ( makeDust ( USDC ) , PoolShares ) ;
94
126
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
+ } ) ;
96
140
return {
97
- shareMint,
98
- shareWorth,
141
+ /** used for `checkPoolBalance` invariant. aka 'outstanding borrows' */
142
+ encumberedBalance,
143
+ feeSeat,
144
+ poolStats,
145
+ poolMetricsRecorderKit,
99
146
poolSeat,
100
147
PoolShares,
101
148
proposalShapes,
102
- shareWorthRecorderKit,
149
+ shareMint,
150
+ shareWorth,
103
151
} ;
104
152
} ,
105
153
{
106
- feeSink : {
154
+ borrower : {
155
+ getBalance ( ) {
156
+ const { poolSeat } = this . state ;
157
+ return poolSeat . getAmountAllocated ( 'USDC' , USDC ) ;
158
+ } ,
107
159
/**
108
- * @param {Amount<'nat'> } amount
109
- * @param {Payment <'nat'> } payment
160
+ * @param {ZCFSeat } toSeat
161
+ * @param {{ USDC: Amount <'nat'>}} amountKWR
110
162
*/
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 ,
119
172
) ;
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 ( ) ;
122
185
} ,
186
+ // TODO method to repay failed `LOA.deposit()`
123
187
} ,
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
+ ) ;
124
212
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
+ } ,
125
238
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 ;
129
242
// Consumers of this .write() are off-chain / outside the VM.
130
243
// And there's no way to recover from a failed write.
131
244
// So don't await.
132
- void recorder . write ( shareWorth ) ;
245
+ void recorder . write ( {
246
+ encumberedBalance,
247
+ shareWorth,
248
+ ...poolStats ,
249
+ } ) ;
133
250
} ,
134
251
} ,
135
252
136
253
depositHandler : {
137
254
/** @param {ZCFSeat } lp */
138
255
async handle ( lp ) {
139
- const { shareWorth, shareMint, poolSeat } = this . state ;
256
+ const { shareWorth, shareMint, poolSeat, encumberedBalance } =
257
+ this . state ;
140
258
const { external } = this . facets ;
141
259
142
260
/** @type {USDCProposalShapes['deposit'] } */
143
261
// @ts -expect-error ensured by proposalShape
144
262
const proposal = lp . getProposal ( ) ;
145
- checkPoolBalance ( poolSeat , shareWorth , USDC ) ;
263
+ checkPoolBalance ( poolSeat , shareWorth , USDC , encumberedBalance ) ;
146
264
const post = depositCalc ( shareWorth , proposal ) ;
147
265
148
266
// COMMIT POINT
@@ -165,20 +283,21 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
165
283
console . error ( reason . message , cause ) ;
166
284
zcf . shutdownWithFailure ( reason ) ;
167
285
}
168
- external . publishShareWorth ( ) ;
286
+ external . publishPoolMetrics ( ) ;
169
287
} ,
170
288
} ,
171
289
withdrawHandler : {
172
290
/** @param {ZCFSeat } lp */
173
291
async handle ( lp ) {
174
- const { shareWorth, shareMint, poolSeat } = this . state ;
292
+ const { shareWorth, shareMint, poolSeat, encumberedBalance } =
293
+ this . state ;
175
294
const { external } = this . facets ;
176
295
177
296
/** @type {USDCProposalShapes['withdraw'] } */
178
297
// @ts -expect-error ensured by proposalShape
179
298
const proposal = lp . getProposal ( ) ;
180
299
const { zcfSeat : burn } = zcf . makeEmptySeatKit ( ) ;
181
- checkPoolBalance ( poolSeat , shareWorth , USDC ) ;
300
+ checkPoolBalance ( poolSeat , shareWorth , USDC , encumberedBalance ) ;
182
301
const post = withdrawCalc ( shareWorth , proposal ) ;
183
302
184
303
// COMMIT POINT
@@ -201,7 +320,7 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
201
320
console . error ( reason . message , cause ) ;
202
321
zcf . shutdownWithFailure ( reason ) ;
203
322
}
204
- external . publishShareWorth ( ) ;
323
+ external . publishPoolMetrics ( ) ;
205
324
} ,
206
325
} ,
207
326
public : {
@@ -222,18 +341,25 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
222
341
) ;
223
342
} ,
224
343
getPublicTopics ( ) {
225
- const { shareWorthRecorderKit } = this . state ;
344
+ const { poolMetricsRecorderKit } = this . state ;
226
345
return {
227
- shareWorth : makeRecorderTopic ( 'shareWorth' , shareWorthRecorderKit ) ,
346
+ poolMetrics : makeRecorderTopic (
347
+ 'poolMetrics' ,
348
+ poolMetricsRecorderKit ,
349
+ ) ,
228
350
} ;
229
351
} ,
230
352
} ,
231
353
} ,
232
354
{
233
355
finish : ( { facets : { external } } ) => {
234
- void external . publishShareWorth ( ) ;
356
+ void external . publishPoolMetrics ( ) ;
235
357
} ,
236
358
} ,
237
359
) ;
238
360
} ;
239
361
harden ( prepareLiquidityPoolKit ) ;
362
+
363
+ /**
364
+ * @typedef {ReturnType<ReturnType<typeof prepareLiquidityPoolKit>> } LiquidityPoolKit
365
+ */
0 commit comments