@agoric/fast-usdc 0.2.0-u18a.0 → 0.2.0-u19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,13 +16,14 @@ import { makeFeeTools } from '../utils/fees.js';
16
16
 
17
17
  /**
18
18
  * @import {HostInterface} from '@agoric/async-flow';
19
+ * @import {Amount, Brand} from '@agoric/ertp';
19
20
  * @import {TypedPattern} from '@agoric/internal'
20
21
  * @import {NatAmount} from '@agoric/ertp';
21
22
  * @import {ChainAddress, ChainHub, Denom, OrchestrationAccount} from '@agoric/orchestration';
22
23
  * @import {ZoeTools} from '@agoric/orchestration/src/utils/zoe-tools.js';
23
24
  * @import {VowTools} from '@agoric/vow';
24
25
  * @import {Zone} from '@agoric/zone';
25
- * @import {CctpTxEvidence, AddressHook, EvmHash, FeeConfig, LogFn, NobleAddress, EvidenceWithRisk} from '../types.js';
26
+ * @import {AddressHook, EvmHash, FeeConfig, LogFn, NobleAddress, EvidenceWithRisk} from '../types.js';
26
27
  * @import {StatusManager} from './status-manager.js';
27
28
  * @import {LiquidityPoolKit} from './liquidity-pool.js';
28
29
  */
@@ -31,12 +32,12 @@ import { makeFeeTools } from '../utils/fees.js';
31
32
  * @typedef {{
32
33
  * chainHub: ChainHub;
33
34
  * feeConfig: FeeConfig;
34
- * localTransfer: ZoeTools['localTransfer'];
35
35
  * log?: LogFn;
36
36
  * statusManager: StatusManager;
37
37
  * usdc: { brand: Brand<'nat'>; denom: Denom; };
38
38
  * vowTools: VowTools;
39
39
  * zcf: ZCF;
40
+ * zoeTools: ZoeTools;
40
41
  * }} AdvancerKitPowers
41
42
  */
42
43
 
@@ -63,12 +64,21 @@ const AdvancerKitI = harden({
63
64
  onRejected: M.call(M.error(), AdvancerVowCtxShape).returns(),
64
65
  }),
65
66
  transferHandler: M.interface('TransferHandlerI', {
66
- // TODO confirm undefined, and not bigint (sequence)
67
67
  onFulfilled: M.call(M.undefined(), AdvancerVowCtxShape).returns(
68
68
  M.undefined(),
69
69
  ),
70
70
  onRejected: M.call(M.error(), AdvancerVowCtxShape).returns(M.undefined()),
71
71
  }),
72
+ withdrawHandler: M.interface('WithdrawHandlerI', {
73
+ onFulfilled: M.call(M.undefined(), {
74
+ advanceAmount: AnyNatAmountShape,
75
+ tmpReturnSeat: M.remotable(),
76
+ }).returns(M.undefined()),
77
+ onRejected: M.call(M.error(), {
78
+ advanceAmount: AnyNatAmountShape,
79
+ tmpReturnSeat: M.remotable(),
80
+ }).returns(M.undefined()),
81
+ }),
72
82
  });
73
83
 
74
84
  /**
@@ -81,6 +91,14 @@ const AdvancerKitI = harden({
81
91
  * }} AdvancerVowCtx
82
92
  */
83
93
 
94
+ export const stateShape = harden({
95
+ notifier: M.remotable(),
96
+ borrower: M.remotable(),
97
+ poolAccount: M.remotable(),
98
+ intermediateRecipient: M.opt(ChainAddressShape),
99
+ settlementAddress: M.opt(ChainAddressShape),
100
+ });
101
+
84
102
  /**
85
103
  * @param {Zone} zone
86
104
  * @param {AdvancerKitPowers} caps
@@ -90,12 +108,12 @@ export const prepareAdvancerKit = (
90
108
  {
91
109
  chainHub,
92
110
  feeConfig,
93
- localTransfer,
94
111
  log = makeTracer('Advancer', true),
95
112
  statusManager,
96
113
  usdc,
97
114
  vowTools: { watch, when },
98
115
  zcf,
116
+ zoeTools: { localTransfer, withdrawToSeat },
99
117
  },
100
118
  ) => {
101
119
  assertAllDefined({
@@ -114,8 +132,8 @@ export const prepareAdvancerKit = (
114
132
  AdvancerKitI,
115
133
  /**
116
134
  * @param {{
117
- * notifyFacet: import('./settler.js').SettlerKit['notify'];
118
- * borrowerFacet: LiquidityPoolKit['borrower'];
135
+ * notifier: import('./settler.js').SettlerKit['notifier'];
136
+ * borrower: LiquidityPoolKit['borrower'];
119
137
  * poolAccount: HostInterface<OrchestrationAccount<{chainId: 'agoric'}>>;
120
138
  * settlementAddress: ChainAddress;
121
139
  * intermediateRecipient?: ChainAddress;
@@ -152,9 +170,7 @@ export const prepareAdvancerKit = (
152
170
  statusManager.skipAdvance(evidence, risk.risksIdentified);
153
171
  return;
154
172
  }
155
-
156
- const { borrowerFacet, poolAccount, settlementAddress } =
157
- this.state;
173
+ const { settlementAddress } = this.state;
158
174
  const { recipientAddress } = evidence.aux;
159
175
  const decoded = decodeAddressHook(recipientAddress);
160
176
  mustMatch(decoded, AddressHookShape);
@@ -167,12 +183,20 @@ export const prepareAdvancerKit = (
167
183
  const destination = chainHub.makeChainAddress(EUD);
168
184
 
169
185
  const fullAmount = toAmount(evidence.tx.amount);
186
+ const { borrower, notifier, poolAccount } = this.state;
187
+ // do not advance if we've already received a mint/settlement
188
+ const mintedEarly = notifier.checkMintedEarly(
189
+ evidence,
190
+ destination,
191
+ );
192
+ if (mintedEarly) return;
193
+
170
194
  // throws if requested does not exceed fees
171
195
  const advanceAmount = feeTools.calculateAdvance(fullAmount);
172
196
 
173
197
  const { zcfSeat: tmpSeat } = zcf.makeEmptySeatKit();
174
198
  // throws if the pool has insufficient funds
175
- borrowerFacet.borrow(tmpSeat, advanceAmount);
199
+ borrower.borrow(tmpSeat, advanceAmount);
176
200
 
177
201
  // this cannot throw since `.isSeen()` is called in the same turn
178
202
  statusManager.advance(evidence);
@@ -193,7 +217,7 @@ export const prepareAdvancerKit = (
193
217
  });
194
218
  } catch (error) {
195
219
  log('Advancer error:', error);
196
- statusManager.observe(evidence);
220
+ statusManager.skipAdvance(evidence, [error.message]);
197
221
  }
198
222
  },
199
223
  /** @param {ChainAddress} intermediateRecipient */
@@ -207,14 +231,21 @@ export const prepareAdvancerKit = (
207
231
  * @param {AdvancerVowCtx & { tmpSeat: ZCFSeat }} ctx
208
232
  */
209
233
  onFulfilled(result, ctx) {
210
- const { poolAccount, intermediateRecipient } = this.state;
211
- const { destination, advanceAmount, ...detail } = ctx;
212
- const transferV = E(poolAccount).transfer(
213
- destination,
214
- { denom: usdc.denom, value: advanceAmount.value },
215
- { forwardOpts: { intermediateRecipient } },
216
- );
217
- return watch(transferV, this.facets.transferHandler, {
234
+ const { poolAccount, intermediateRecipient, settlementAddress } =
235
+ this.state;
236
+ const { destination, advanceAmount, tmpSeat, ...detail } = ctx;
237
+ tmpSeat.exit();
238
+ const amount = harden({
239
+ denom: usdc.denom,
240
+ value: advanceAmount.value,
241
+ });
242
+ const transferOrSendV =
243
+ destination.chainId === settlementAddress.chainId
244
+ ? E(poolAccount).send(destination, amount)
245
+ : E(poolAccount).transfer(destination, amount, {
246
+ forwardOpts: { intermediateRecipient },
247
+ });
248
+ return watch(transferOrSendV, this.facets.transferHandler, {
218
249
  destination,
219
250
  advanceAmount,
220
251
  ...detail,
@@ -237,9 +268,10 @@ export const prepareAdvancerKit = (
237
268
  error,
238
269
  );
239
270
  try {
240
- const { borrowerFacet, notifyFacet } = this.state;
241
- notifyFacet.notifyAdvancingResult(restCtx, false);
242
- borrowerFacet.returnToPool(tmpSeat, advanceAmount);
271
+ const { borrower, notifier } = this.state;
272
+ notifier.notifyAdvancingResult(restCtx, false);
273
+ borrower.returnToPool(tmpSeat, advanceAmount);
274
+ tmpSeat.exit();
243
275
  } catch (e) {
244
276
  log('🚨 deposit to localOrchAccount failure recovery failed', e);
245
277
  }
@@ -247,44 +279,80 @@ export const prepareAdvancerKit = (
247
279
  },
248
280
  transferHandler: {
249
281
  /**
250
- * @param {unknown} result TODO confirm this is not a bigint (sequence)
282
+ * @param {undefined} result
251
283
  * @param {AdvancerVowCtx} ctx
252
284
  */
253
285
  onFulfilled(result, ctx) {
254
- const { notifyFacet } = this.state;
286
+ const { notifier } = this.state;
255
287
  const { advanceAmount, destination, ...detail } = ctx;
256
- log('Advance transfer fulfilled', {
257
- advanceAmount,
258
- destination,
259
- result,
260
- });
288
+ log('Advance succeeded', { advanceAmount, destination });
261
289
  // During development, due to a bug, this call threw.
262
290
  // The failure was silent (no diagnostics) due to:
263
291
  // - #10576 Vows do not report unhandled rejections
264
292
  // For now, the advancer kit relies on consistency between
265
- // notifyFacet, statusManager, and callers of handleTransactionEvent().
293
+ // notify, statusManager, and callers of handleTransactionEvent().
266
294
  // TODO: revisit #10576 during #10510
267
- notifyFacet.notifyAdvancingResult({ destination, ...detail }, true);
295
+ notifier.notifyAdvancingResult({ destination, ...detail }, true);
268
296
  },
269
297
  /**
270
298
  * @param {Error} error
271
299
  * @param {AdvancerVowCtx} ctx
272
300
  */
273
301
  onRejected(error, ctx) {
274
- const { notifyFacet } = this.state;
275
- log('Advance transfer rejected', error);
276
- notifyFacet.notifyAdvancingResult(ctx, false);
302
+ const { notifier, poolAccount } = this.state;
303
+ log('Advance failed', error);
304
+ const { advanceAmount, ...restCtx } = ctx;
305
+ notifier.notifyAdvancingResult(restCtx, false);
306
+ const { zcfSeat: tmpReturnSeat } = zcf.makeEmptySeatKit();
307
+ const withdrawV = withdrawToSeat(
308
+ // @ts-expect-error LocalAccountMethods vs OrchestrationAccount
309
+ poolAccount,
310
+ tmpReturnSeat,
311
+ harden({ USDC: advanceAmount }),
312
+ );
313
+ void watch(withdrawV, this.facets.withdrawHandler, {
314
+ advanceAmount,
315
+ tmpReturnSeat,
316
+ });
317
+ },
318
+ },
319
+ withdrawHandler: {
320
+ /**
321
+ *
322
+ * @param {undefined} result
323
+ * @param {{ advanceAmount: Amount<'nat'>; tmpReturnSeat: ZCFSeat; }} ctx
324
+ */
325
+ onFulfilled(result, { advanceAmount, tmpReturnSeat }) {
326
+ const { borrower } = this.state;
327
+ try {
328
+ borrower.returnToPool(tmpReturnSeat, advanceAmount);
329
+ } catch (e) {
330
+ // If we reach here, the unused advance funds will remain in `tmpReturnSeat`
331
+ // and must be retrieved from recovery sets.
332
+ log(
333
+ `🚨 return ${q(advanceAmount)} to pool failed. funds remain on "tmpReturnSeat"`,
334
+ e,
335
+ );
336
+ }
337
+ tmpReturnSeat.exit();
338
+ },
339
+ /**
340
+ * @param {Error} error
341
+ * @param {{ advanceAmount: Amount<'nat'>; tmpReturnSeat: ZCFSeat; }} ctx
342
+ */
343
+ onRejected(error, { advanceAmount, tmpReturnSeat }) {
344
+ log(
345
+ `🚨 withdraw ${q(advanceAmount)} from "poolAccount" to return to pool failed`,
346
+ error,
347
+ );
348
+ // If we reach here, the unused advance funds will remain in the `poolAccount`.
349
+ // A contract update will be required to return them to the LiquidityPool.
350
+ tmpReturnSeat.exit();
277
351
  },
278
352
  },
279
353
  },
280
354
  {
281
- stateShape: harden({
282
- notifyFacet: M.remotable(),
283
- borrowerFacet: M.remotable(),
284
- poolAccount: M.remotable(),
285
- intermediateRecipient: M.opt(ChainAddressShape),
286
- settlementAddress: M.opt(ChainAddressShape),
287
- }),
355
+ stateShape,
288
356
  },
289
357
  );
290
358
  };
@@ -1,13 +1,15 @@
1
- import { AmountMath } from '@agoric/ertp';
1
+ import { AmountMath, AmountShape, RatioShape } from '@agoric/ertp';
2
2
  import {
3
3
  makeRecorderTopic,
4
+ RecorderKitShape,
4
5
  TopicsRecordShape,
5
- } from '@agoric/zoe/src/contractSupport/topics.js';
6
+ } from '@agoric/zoe/src/contractSupport/index.js';
6
7
  import { SeatShape } from '@agoric/zoe/src/typeGuards.js';
7
8
  import { M } from '@endo/patterns';
8
9
  import { Fail, q } from '@endo/errors';
9
10
  import {
10
11
  borrowCalc,
12
+ checkPoolBalance,
11
13
  depositCalc,
12
14
  makeParity,
13
15
  repayCalc,
@@ -20,6 +22,7 @@ import {
20
22
  } from '../type-guards.js';
21
23
 
22
24
  /**
25
+ * @import {Amount, Brand, Payment} from '@agoric/ertp';
23
26
  * @import {Zone} from '@agoric/zone';
24
27
  * @import {Remote} from '@agoric/internal'
25
28
  * @import {StorageNode} from '@agoric/internal/src/lib-chainStorage.js'
@@ -28,32 +31,7 @@ import {
28
31
  * @import {PoolStats} from '../types.js';
29
32
  */
30
33
 
31
- const { add, isEqual, isGTE, makeEmpty } = AmountMath;
32
-
33
- /** @param {Brand} brand */
34
- const makeDust = brand => AmountMath.make(brand, 1n);
35
-
36
- /**
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
- *
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
- *
45
- * @param {ZCFSeat} poolSeat
46
- * @param {ShareWorth} shareWorth
47
- * @param {Brand} USDC
48
- * @param {Amount<'nat'>} encumberedBalance
49
- */
50
- const checkPoolBalance = (poolSeat, shareWorth, USDC, encumberedBalance) => {
51
- const unencumberedBalance = poolSeat.getAmountAllocated('USDC', USDC);
52
- const dust = makeDust(USDC);
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)}`;
56
- };
34
+ const { add, isGTE, makeEmpty } = AmountMath;
57
35
 
58
36
  /**
59
37
  * @typedef {{
@@ -71,6 +49,22 @@ const checkPoolBalance = (poolSeat, shareWorth, USDC, encumberedBalance) => {
71
49
  * }} RepayPaymentKWR
72
50
  */
73
51
 
52
+ export const stateShape = harden({
53
+ encumberedBalance: AmountShape,
54
+ feeSeat: M.remotable(),
55
+ poolStats: M.record(),
56
+ poolMetricsRecorderKit: RecorderKitShape,
57
+ poolSeat: M.remotable(),
58
+ PoolShares: M.remotable(),
59
+ proposalShapes: {
60
+ deposit: M.pattern(),
61
+ withdraw: M.pattern(),
62
+ withdrawFees: M.pattern(),
63
+ },
64
+ shareMint: M.remotable(),
65
+ shareWorth: RatioShape,
66
+ });
67
+
74
68
  /**
75
69
  * @param {Zone} zone
76
70
  * @param {ZCF} zcf
@@ -106,11 +100,18 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
106
100
  withdrawHandler: M.interface('withdrawHandler', {
107
101
  handle: M.call(SeatShape, M.any()).returns(M.promise()),
108
102
  }),
103
+ withdrawFeesHandler: M.interface('withdrawFeesHandler', {
104
+ handle: M.call(SeatShape, M.any()).returns(M.promise()),
105
+ }),
109
106
  public: M.interface('public', {
110
107
  makeDepositInvitation: M.call().returns(M.promise()),
111
108
  makeWithdrawInvitation: M.call().returns(M.promise()),
112
109
  getPublicTopics: M.call().returns(TopicsRecordShape),
113
110
  }),
111
+ feeRecipient: M.interface('feeRecipient', {
112
+ getContractFeeBalance: M.call().returns(AmountShape),
113
+ makeWithdrawFeesInvitation: M.call().returns(M.promise()),
114
+ }),
114
115
  },
115
116
  /**
116
117
  * @param {ZCFMint<'nat'>} shareMint
@@ -119,7 +120,7 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
119
120
  (shareMint, node) => {
120
121
  const { brand: PoolShares } = shareMint.getIssuerRecord();
121
122
  const proposalShapes = makeProposalShapes({ USDC, PoolShares });
122
- const shareWorth = makeParity(makeDust(USDC), PoolShares);
123
+ const shareWorth = makeParity(USDC, PoolShares);
123
124
  const { zcfSeat: poolSeat } = zcf.makeEmptySeatKit();
124
125
  const { zcfSeat: feeSeat } = zcf.makeEmptySeatKit();
125
126
  const poolMetricsRecorderKit = tools.makeRecorderKit(
@@ -191,7 +192,9 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
191
192
  zcf.atomicRearrange(
192
193
  harden([[borrowSeat, repaySeat, { USDC: amount }, returnAmounts]]),
193
194
  );
194
- return this.facets.repayer.repay(repaySeat, returnAmounts);
195
+ this.facets.repayer.repay(repaySeat, returnAmounts);
196
+ borrowSeat.exit();
197
+ repaySeat.exit();
195
198
  },
196
199
  },
197
200
  repayer: {
@@ -207,7 +210,11 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
207
210
  poolStats,
208
211
  shareWorth,
209
212
  } = this.state;
210
- checkPoolBalance(poolSeat, shareWorth, USDC, encumberedBalance);
213
+ checkPoolBalance(
214
+ poolSeat.getCurrentAllocation(),
215
+ shareWorth,
216
+ encumberedBalance,
217
+ );
211
218
 
212
219
  const fromSeatAllocation = fromSeat.getCurrentAllocation();
213
220
  // Validate allocation equals amounts and Principal <= encumberedBalance
@@ -264,7 +271,11 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
264
271
  /** @type {USDCProposalShapes['deposit']} */
265
272
  // @ts-expect-error ensured by proposalShape
266
273
  const proposal = lp.getProposal();
267
- checkPoolBalance(poolSeat, shareWorth, USDC, encumberedBalance);
274
+ checkPoolBalance(
275
+ poolSeat.getCurrentAllocation(),
276
+ shareWorth,
277
+ encumberedBalance,
278
+ );
268
279
  const post = depositCalc(shareWorth, proposal);
269
280
 
270
281
  // COMMIT POINT
@@ -300,8 +311,12 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
300
311
  // @ts-expect-error ensured by proposalShape
301
312
  const proposal = lp.getProposal();
302
313
  const { zcfSeat: burn } = zcf.makeEmptySeatKit();
303
- checkPoolBalance(poolSeat, shareWorth, USDC, encumberedBalance);
304
- const post = withdrawCalc(shareWorth, proposal);
314
+ const post = withdrawCalc(
315
+ shareWorth,
316
+ proposal,
317
+ poolSeat.getCurrentAllocation(),
318
+ encumberedBalance,
319
+ );
305
320
 
306
321
  // COMMIT POINT
307
322
  try {
@@ -325,6 +340,21 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
325
340
  external.publishPoolMetrics();
326
341
  },
327
342
  },
343
+ withdrawFeesHandler: {
344
+ /** @param {ZCFSeat} seat */
345
+ async handle(seat) {
346
+ const { feeSeat } = this.state;
347
+
348
+ const { want } = seat.getProposal();
349
+ const available = feeSeat.getAmountAllocated('USDC', want.USDC.brand);
350
+ isGTE(available, want.USDC) ||
351
+ Fail`cannot withdraw ${want.USDC}; only ${available} available`;
352
+
353
+ // COMMIT POINT
354
+ zcf.atomicRearrange(harden([[feeSeat, seat, want]]));
355
+ seat.exit();
356
+ },
357
+ },
328
358
  public: {
329
359
  makeDepositInvitation() {
330
360
  return zcf.makeInvitation(
@@ -352,11 +382,28 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
352
382
  };
353
383
  },
354
384
  },
385
+ feeRecipient: {
386
+ getContractFeeBalance() {
387
+ const { feeSeat } = this.state;
388
+ /** @type {Amount<'nat'>} */
389
+ const balance = feeSeat.getCurrentAllocation().USDC;
390
+ return balance;
391
+ },
392
+ makeWithdrawFeesInvitation() {
393
+ return zcf.makeInvitation(
394
+ this.facets.withdrawFeesHandler,
395
+ 'Withdraw Fees',
396
+ undefined,
397
+ this.state.proposalShapes.withdrawFees,
398
+ );
399
+ },
400
+ },
355
401
  },
356
402
  {
357
403
  finish: ({ facets: { external } }) => {
358
404
  void external.publishPoolMetrics();
359
405
  },
406
+ stateShape,
360
407
  },
361
408
  );
362
409
  };
@@ -90,8 +90,6 @@ export const prepareOperatorKit = (zone, staticPowers) =>
90
90
  */
91
91
  async SubmitEvidence(evidence, riskAssessment) {
92
92
  const { operator } = this.facets;
93
- // TODO(bootstrap integration): cause this call to throw and confirm that it
94
- // shows up in the the smart-wallet UpdateRecord `error` property
95
93
  operator.submitEvidence(evidence, riskAssessment);
96
94
  return staticPowers.makeInertInvitation(
97
95
  'evidence was pushed in the invitation maker call',