@agoric/fast-usdc 0.1.1-dev-02967de.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.
@@ -0,0 +1,365 @@
1
+ import { AmountMath, AmountShape } from '@agoric/ertp';
2
+ import {
3
+ makeRecorderTopic,
4
+ TopicsRecordShape,
5
+ } from '@agoric/zoe/src/contractSupport/topics.js';
6
+ import { SeatShape } from '@agoric/zoe/src/typeGuards.js';
7
+ import { M } from '@endo/patterns';
8
+ import { Fail, q } from '@endo/errors';
9
+ import {
10
+ borrowCalc,
11
+ depositCalc,
12
+ makeParity,
13
+ repayCalc,
14
+ withdrawCalc,
15
+ } from '../pool-share-math.js';
16
+ import {
17
+ makeNatAmountShape,
18
+ makeProposalShapes,
19
+ PoolMetricsShape,
20
+ } from '../type-guards.js';
21
+
22
+ /**
23
+ * @import {Zone} from '@agoric/zone';
24
+ * @import {Remote} from '@agoric/internal'
25
+ * @import {StorageNode} from '@agoric/internal/src/lib-chainStorage.js'
26
+ * @import {MakeRecorderKit} from '@agoric/zoe/src/contractSupport/recorder.js'
27
+ * @import {USDCProposalShapes, ShareWorth} from '../pool-share-math.js'
28
+ * @import {PoolStats} from '../types.js';
29
+ */
30
+
31
+ const { add, isEqual, 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
+ };
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
+
74
+ /**
75
+ * @param {Zone} zone
76
+ * @param {ZCF} zcf
77
+ * @param {Brand<'nat'>} USDC
78
+ * @param {{
79
+ * makeRecorderKit: MakeRecorderKit;
80
+ * }} tools
81
+ */
82
+ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
83
+ return zone.exoClassKit(
84
+ 'Liquidity Pool',
85
+ {
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(),
102
+ }),
103
+ external: M.interface('external', {
104
+ publishPoolMetrics: M.call().returns(),
105
+ }),
106
+ depositHandler: M.interface('depositHandler', {
107
+ handle: M.call(SeatShape, M.any()).returns(M.promise()),
108
+ }),
109
+ withdrawHandler: M.interface('withdrawHandler', {
110
+ handle: M.call(SeatShape, M.any()).returns(M.promise()),
111
+ }),
112
+ public: M.interface('public', {
113
+ makeDepositInvitation: M.call().returns(M.promise()),
114
+ makeWithdrawInvitation: M.call().returns(M.promise()),
115
+ getPublicTopics: M.call().returns(TopicsRecordShape),
116
+ }),
117
+ },
118
+ /**
119
+ * @param {ZCFMint<'nat'>} shareMint
120
+ * @param {Remote<StorageNode>} node
121
+ */
122
+ (shareMint, node) => {
123
+ const { brand: PoolShares } = shareMint.getIssuerRecord();
124
+ const proposalShapes = makeProposalShapes({ USDC, PoolShares });
125
+ const shareWorth = makeParity(makeDust(USDC), PoolShares);
126
+ const { zcfSeat: poolSeat } = zcf.makeEmptySeatKit();
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
+ });
140
+ return {
141
+ /** used for `checkPoolBalance` invariant. aka 'outstanding borrows' */
142
+ encumberedBalance,
143
+ feeSeat,
144
+ poolStats,
145
+ poolMetricsRecorderKit,
146
+ poolSeat,
147
+ PoolShares,
148
+ proposalShapes,
149
+ shareMint,
150
+ shareWorth,
151
+ };
152
+ },
153
+ {
154
+ borrower: {
155
+ getBalance() {
156
+ const { poolSeat } = this.state;
157
+ return poolSeat.getAmountAllocated('USDC', USDC);
158
+ },
159
+ /**
160
+ * @param {ZCFSeat} toSeat
161
+ * @param {{ USDC: Amount<'nat'>}} amountKWR
162
+ */
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,
172
+ );
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();
185
+ },
186
+ // TODO method to repay failed `LOA.deposit()`
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
+ );
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
+ },
238
+ external: {
239
+ publishPoolMetrics() {
240
+ const { poolStats, shareWorth, encumberedBalance } = this.state;
241
+ const { recorder } = this.state.poolMetricsRecorderKit;
242
+ // Consumers of this .write() are off-chain / outside the VM.
243
+ // And there's no way to recover from a failed write.
244
+ // So don't await.
245
+ void recorder.write({
246
+ encumberedBalance,
247
+ shareWorth,
248
+ ...poolStats,
249
+ });
250
+ },
251
+ },
252
+
253
+ depositHandler: {
254
+ /** @param {ZCFSeat} lp */
255
+ async handle(lp) {
256
+ const { shareWorth, shareMint, poolSeat, encumberedBalance } =
257
+ this.state;
258
+ const { external } = this.facets;
259
+
260
+ /** @type {USDCProposalShapes['deposit']} */
261
+ // @ts-expect-error ensured by proposalShape
262
+ const proposal = lp.getProposal();
263
+ checkPoolBalance(poolSeat, shareWorth, USDC, encumberedBalance);
264
+ const post = depositCalc(shareWorth, proposal);
265
+
266
+ // COMMIT POINT
267
+
268
+ try {
269
+ const mint = shareMint.mintGains(post.payouts);
270
+ this.state.shareWorth = post.shareWorth;
271
+ zcf.atomicRearrange(
272
+ harden([
273
+ // zoe guarantees lp has proposal.give allocated
274
+ [lp, poolSeat, proposal.give],
275
+ // mintGains() above establishes that mint has post.payouts
276
+ [mint, lp, post.payouts],
277
+ ]),
278
+ );
279
+ lp.exit();
280
+ mint.exit();
281
+ } catch (cause) {
282
+ const reason = Error('🚨 cannot commit deposit', { cause });
283
+ console.error(reason.message, cause);
284
+ zcf.shutdownWithFailure(reason);
285
+ }
286
+ external.publishPoolMetrics();
287
+ },
288
+ },
289
+ withdrawHandler: {
290
+ /** @param {ZCFSeat} lp */
291
+ async handle(lp) {
292
+ const { shareWorth, shareMint, poolSeat, encumberedBalance } =
293
+ this.state;
294
+ const { external } = this.facets;
295
+
296
+ /** @type {USDCProposalShapes['withdraw']} */
297
+ // @ts-expect-error ensured by proposalShape
298
+ const proposal = lp.getProposal();
299
+ const { zcfSeat: burn } = zcf.makeEmptySeatKit();
300
+ checkPoolBalance(poolSeat, shareWorth, USDC, encumberedBalance);
301
+ const post = withdrawCalc(shareWorth, proposal);
302
+
303
+ // COMMIT POINT
304
+
305
+ try {
306
+ this.state.shareWorth = post.shareWorth;
307
+ zcf.atomicRearrange(
308
+ harden([
309
+ // zoe guarantees lp has proposal.give allocated
310
+ [lp, burn, proposal.give],
311
+ // checkPoolBalance() + withdrawCalc() guarantee poolSeat has enough
312
+ [poolSeat, lp, post.payouts],
313
+ ]),
314
+ );
315
+ shareMint.burnLosses(proposal.give, burn);
316
+ lp.exit();
317
+ burn.exit();
318
+ } catch (cause) {
319
+ const reason = Error('🚨 cannot commit withdraw', { cause });
320
+ console.error(reason.message, cause);
321
+ zcf.shutdownWithFailure(reason);
322
+ }
323
+ external.publishPoolMetrics();
324
+ },
325
+ },
326
+ public: {
327
+ makeDepositInvitation() {
328
+ return zcf.makeInvitation(
329
+ this.facets.depositHandler,
330
+ 'Deposit',
331
+ undefined,
332
+ this.state.proposalShapes.deposit,
333
+ );
334
+ },
335
+ makeWithdrawInvitation() {
336
+ return zcf.makeInvitation(
337
+ this.facets.withdrawHandler,
338
+ 'Withdraw',
339
+ undefined,
340
+ this.state.proposalShapes.withdraw,
341
+ );
342
+ },
343
+ getPublicTopics() {
344
+ const { poolMetricsRecorderKit } = this.state;
345
+ return {
346
+ poolMetrics: makeRecorderTopic(
347
+ 'poolMetrics',
348
+ poolMetricsRecorderKit,
349
+ ),
350
+ };
351
+ },
352
+ },
353
+ },
354
+ {
355
+ finish: ({ facets: { external } }) => {
356
+ void external.publishPoolMetrics();
357
+ },
358
+ },
359
+ );
360
+ };
361
+ harden(prepareLiquidityPoolKit);
362
+
363
+ /**
364
+ * @typedef {ReturnType<ReturnType<typeof prepareLiquidityPoolKit>>} LiquidityPoolKit
365
+ */
@@ -0,0 +1,120 @@
1
+ import { makeTracer } from '@agoric/internal';
2
+ import { Fail } from '@endo/errors';
3
+ import { M } from '@endo/patterns';
4
+ import { CctpTxEvidenceShape } from '../type-guards.js';
5
+
6
+ const trace = makeTracer('TxOperator');
7
+
8
+ /**
9
+ * @import {Zone} from '@agoric/zone';
10
+ * @import {CctpTxEvidence} from '../types.js';
11
+ */
12
+
13
+ /**
14
+ * @typedef {object} OperatorPowers
15
+ * @property {(evidence: CctpTxEvidence, operatorKit: OperatorKit) => void} submitEvidence
16
+ */
17
+
18
+ /**
19
+ * @typedef {object} OperatorStatus
20
+ * @property {boolean} [disabled]
21
+ * @property {string} operatorId
22
+ */
23
+
24
+ /**
25
+ * @typedef {Readonly<{ operatorId: string, powers: OperatorPowers }> & {disabled: boolean}} State
26
+ */
27
+
28
+ const OperatorKitI = {
29
+ admin: M.interface('Admin', {
30
+ disable: M.call().returns(),
31
+ }),
32
+
33
+ invitationMakers: M.interface('InvitationMakers', {
34
+ SubmitEvidence: M.call(CctpTxEvidenceShape).returns(M.promise()),
35
+ }),
36
+
37
+ operator: M.interface('Operator', {
38
+ submitEvidence: M.call(CctpTxEvidenceShape).returns(M.promise()),
39
+ getStatus: M.call().returns(M.record()),
40
+ }),
41
+ };
42
+
43
+ /**
44
+ * @param {Zone} zone
45
+ * @param {{ makeInertInvitation: Function }} staticPowers
46
+ */
47
+ export const prepareOperatorKit = (zone, staticPowers) =>
48
+ zone.exoClassKit(
49
+ 'Operator Kit',
50
+ OperatorKitI,
51
+ /**
52
+ * @param {string} operatorId
53
+ * @param {OperatorPowers} powers facet of the durable transaction feed
54
+ * @returns {State}
55
+ */
56
+ (operatorId, powers) => {
57
+ return {
58
+ operatorId,
59
+ powers,
60
+ disabled: false,
61
+ };
62
+ },
63
+ {
64
+ admin: {
65
+ disable() {
66
+ trace(`operator ${this.state.operatorId} disabled`);
67
+ this.state.disabled = true;
68
+ },
69
+ },
70
+ /**
71
+ * NB: when this kit is an offer result, the smart-wallet will detect the `invitationMakers`
72
+ * key and save it for future offers.
73
+ */
74
+ invitationMakers: {
75
+ /**
76
+ * Provide an API call in the form of an invitation maker, so that the
77
+ * capability is available in the smart-wallet bridge.
78
+ *
79
+ * NB: The `Invitation` object is evidence that the operation took
80
+ * place, rather than as a means of performing it as in the
81
+ * fluxAggregator contract used for price oracles.
82
+ *
83
+ * @param {CctpTxEvidence} evidence
84
+ * @returns {Promise<Invitation>}
85
+ */
86
+ async SubmitEvidence(evidence) {
87
+ const { operator } = this.facets;
88
+ // TODO(bootstrap integration): cause this call to throw and confirm that it
89
+ // shows up in the the smart-wallet UpdateRecord `error` property
90
+ await operator.submitEvidence(evidence);
91
+ return staticPowers.makeInertInvitation(
92
+ 'evidence was pushed in the invitation maker call',
93
+ );
94
+ },
95
+ },
96
+ operator: {
97
+ /**
98
+ * submit evidence from this operator
99
+ *
100
+ * @param {CctpTxEvidence} evidence
101
+ */
102
+ async submitEvidence(evidence) {
103
+ const { state } = this;
104
+ !state.disabled || Fail`submitEvidence for disabled operator`;
105
+ const result = state.powers.submitEvidence(evidence, this.facets);
106
+ return result;
107
+ },
108
+ /** @returns {OperatorStatus} */
109
+ getStatus() {
110
+ const { state } = this;
111
+ return {
112
+ operatorId: state.operatorId,
113
+ disabled: state.disabled,
114
+ };
115
+ },
116
+ },
117
+ },
118
+ );
119
+
120
+ /** @typedef {ReturnType<ReturnType<typeof prepareOperatorKit>>} OperatorKit */
@@ -0,0 +1,97 @@
1
+ import { assertAllDefined } from '@agoric/internal';
2
+ import { atob } from '@endo/base64';
3
+ import { makeError, q } from '@endo/errors';
4
+ import { M } from '@endo/patterns';
5
+
6
+ import { addressTools } from '../utils/address.js';
7
+
8
+ /**
9
+ * @import {FungibleTokenPacketData} from '@agoric/cosmic-proto/ibc/applications/transfer/v2/packet.js';
10
+ * @import {Denom} from '@agoric/orchestration';
11
+ * @import {IBCChannelID, VTransferIBCEvent} from '@agoric/vats';
12
+ * @import {Zone} from '@agoric/zone';
13
+ * @import {NobleAddress} from '../types.js';
14
+ * @import {StatusManager} from './status-manager.js';
15
+ */
16
+
17
+ /**
18
+ * @param {Zone} zone
19
+ * @param {object} caps
20
+ * @param {StatusManager} caps.statusManager
21
+ */
22
+ export const prepareSettler = (zone, { statusManager }) => {
23
+ assertAllDefined({ statusManager });
24
+ return zone.exoClass(
25
+ 'Fast USDC Settler',
26
+ M.interface('SettlerI', {
27
+ receiveUpcall: M.call(M.record()).returns(M.promise()),
28
+ }),
29
+ /**
30
+ *
31
+ * @param {{
32
+ * sourceChannel: IBCChannelID;
33
+ * remoteDenom: Denom
34
+ * }} config
35
+ */
36
+ config => harden(config),
37
+ {
38
+ /** @param {VTransferIBCEvent} event */
39
+ async receiveUpcall(event) {
40
+ if (event.packet.source_channel !== this.state.sourceChannel) {
41
+ // TODO #10390 log all early returns
42
+ // only interested in packets from the issuing chain
43
+ return;
44
+ }
45
+ const tx = /** @type {FungibleTokenPacketData} */ (
46
+ JSON.parse(atob(event.packet.data))
47
+ );
48
+ if (tx.denom !== this.state.remoteDenom) {
49
+ // only interested in uusdc
50
+ return;
51
+ }
52
+
53
+ if (!addressTools.hasQueryParams(tx.receiver)) {
54
+ // only interested in receivers with query params
55
+ return;
56
+ }
57
+
58
+ const { EUD } = addressTools.getQueryParams(tx.receiver);
59
+ if (!EUD) {
60
+ // only interested in receivers with EUD parameter
61
+ return;
62
+ }
63
+
64
+ // TODO discern between SETTLED and OBSERVED; each has different fees/destinations
65
+ const hasPendingSettlement = statusManager.hasPendingSettlement(
66
+ // given the sourceChannel check, we can be certain of this cast
67
+ /** @type {NobleAddress} */ (tx.sender),
68
+ BigInt(tx.amount),
69
+ );
70
+ if (!hasPendingSettlement) {
71
+ // TODO FAILURE PATH -> put money in recovery account or .transfer to receiver
72
+ // TODO should we have an ORPHANED TxStatus for this?
73
+ throw makeError(
74
+ `🚨 No pending settlement found for ${q(tx.sender)} ${q(tx.amount)}`,
75
+ );
76
+ }
77
+
78
+ // TODO disperse funds
79
+ // ~1. fee to contractFeeAccount
80
+ // ~2. remainder in poolAccount
81
+
82
+ // update status manager, marking tx `SETTLED`
83
+ statusManager.settle(
84
+ /** @type {NobleAddress} */ (tx.sender),
85
+ BigInt(tx.amount),
86
+ );
87
+ },
88
+ },
89
+ {
90
+ stateShape: harden({
91
+ sourceChannel: M.string(),
92
+ remoteDenom: M.string(),
93
+ }),
94
+ },
95
+ );
96
+ };
97
+ harden(prepareSettler);