Skip to content

Commit 4c2be7c

Browse files
authored
chore(fusdc): settle ForwardFailed, minted while Advancing, and minted early txs (#10729)
closes: #10625 ## Description - fix: ensure "minted while `Advancing`" results in `Disburse` or `Forward` - feat: handle "unknown mint" by forwarding if and when Evidence is reported - via `Settler.forwardIfMinted()` called by `Advancer` - chore: capture `ForwardFailed` (terminal) state if `Forward` fails ### Security Considerations None new introduced, these behaviors are consistent with the product spec. ### Scaling Considerations The `mintedEarly` mapStore could grow large if an attacker spams the `settlementAccount` with uusdc ### Documentation Considerations None ### Testing Considerations Includes tests for all new behaviors. ### Upgrade Considerations Targeting FUSDC's first release
2 parents f4df38c + 8b18171 commit 4c2be7c

File tree

11 files changed

+709
-218
lines changed

11 files changed

+709
-218
lines changed

packages/fast-usdc/src/exos/advancer.js

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ const AdvancerKitI = harden({
6363
onRejected: M.call(M.error(), AdvancerVowCtxShape).returns(),
6464
}),
6565
transferHandler: M.interface('TransferHandlerI', {
66-
// TODO confirm undefined, and not bigint (sequence)
6766
onFulfilled: M.call(M.undefined(), AdvancerVowCtxShape).returns(
6867
M.undefined(),
6968
),
@@ -152,9 +151,7 @@ export const prepareAdvancerKit = (
152151
statusManager.skipAdvance(evidence, risk.risksIdentified);
153152
return;
154153
}
155-
156-
const { borrowerFacet, poolAccount, settlementAddress } =
157-
this.state;
154+
const { settlementAddress } = this.state;
158155
const { recipientAddress } = evidence.aux;
159156
const decoded = decodeAddressHook(recipientAddress);
160157
mustMatch(decoded, AddressHookShape);
@@ -167,6 +164,14 @@ export const prepareAdvancerKit = (
167164
const destination = chainHub.makeChainAddress(EUD);
168165

169166
const fullAmount = toAmount(evidence.tx.amount);
167+
const { borrowerFacet, notifyFacet, poolAccount } = this.state;
168+
// do not advance if we've already received a mint/settlement
169+
const mintedEarly = notifyFacet.checkMintedEarly(
170+
evidence,
171+
destination,
172+
);
173+
if (mintedEarly) return;
174+
170175
// throws if requested does not exceed fees
171176
const advanceAmount = feeTools.calculateAdvance(fullAmount);
172177

@@ -208,7 +213,7 @@ export const prepareAdvancerKit = (
208213
*/
209214
onFulfilled(result, ctx) {
210215
const { poolAccount, intermediateRecipient } = this.state;
211-
const { destination, advanceAmount, ...detail } = ctx;
216+
const { destination, advanceAmount, tmpSeat: _, ...detail } = ctx;
212217
const transferV = E(poolAccount).transfer(
213218
destination,
214219
{ denom: usdc.denom, value: advanceAmount.value },
@@ -273,7 +278,8 @@ export const prepareAdvancerKit = (
273278
onRejected(error, ctx) {
274279
const { notifyFacet } = this.state;
275280
log('Advance transfer rejected', error);
276-
notifyFacet.notifyAdvancingResult(ctx, false);
281+
const { advanceAmount: _, ...restCtx } = ctx;
282+
notifyFacet.notifyAdvancingResult(restCtx, false);
277283
},
278284
},
279285
},

packages/fast-usdc/src/exos/settler.js

Lines changed: 73 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ import { M } from '@endo/patterns';
88
import { decodeAddressHook } from '@agoric/cosmic-proto/address-hooks.js';
99
import { PendingTxStatus } from '../constants.js';
1010
import { makeFeeTools } from '../utils/fees.js';
11-
import { EvmHashShape } from '../type-guards.js';
11+
import {
12+
CctpTxEvidenceShape,
13+
EvmHashShape,
14+
makeNatAmountShape,
15+
} from '../type-guards.js';
1216

1317
/**
1418
* @import {FungibleTokenPacketData} from '@agoric/cosmic-proto/ibc/applications/transfer/v2/packet.js';
@@ -18,7 +22,7 @@ import { EvmHashShape } from '../type-guards.js';
1822
* @import {Zone} from '@agoric/zone';
1923
* @import {HostOf, HostInterface} from '@agoric/async-flow';
2024
* @import {TargetRegistration} from '@agoric/vats/src/bridge-target.js';
21-
* @import {NobleAddress, LiquidityPoolKit, FeeConfig, EvmHash, LogFn} from '../types.js';
25+
* @import {NobleAddress, LiquidityPoolKit, FeeConfig, EvmHash, LogFn, CctpTxEvidence} from '../types.js';
2226
* @import {StatusManager} from './status-manager.js';
2327
*/
2428

@@ -31,6 +35,15 @@ import { EvmHashShape } from '../type-guards.js';
3135
const makeMintedEarlyKey = (addr, amount) =>
3236
`pendingTx:${JSON.stringify([addr, String(amount)])}`;
3337

38+
/** @param {Brand<'nat'>} USDC */
39+
export const makeAdvanceDetailsShape = USDC =>
40+
harden({
41+
destination: ChainAddressShape,
42+
forwardingAddress: M.string(),
43+
fullAmount: makeNatAmountShape(USDC),
44+
txHash: EvmHashShape,
45+
});
46+
3447
/**
3548
* @param {Zone} zone
3649
* @param {object} caps
@@ -69,24 +82,21 @@ export const prepareSettler = (
6982
}),
7083
notify: M.interface('SettlerNotifyI', {
7184
notifyAdvancingResult: M.call(
72-
M.record(), // XXX fill in details TODO
85+
makeAdvanceDetailsShape(USDC),
7386
M.boolean(),
7487
).returns(),
88+
checkMintedEarly: M.call(
89+
CctpTxEvidenceShape,
90+
ChainAddressShape,
91+
).returns(M.boolean()),
7592
}),
7693
self: M.interface('SettlerSelfI', {
77-
disburse: M.call(EvmHashShape, M.string(), M.nat()).returns(
78-
M.promise(),
79-
),
80-
forward: M.call(
81-
M.opt(EvmHashShape),
82-
M.string(),
83-
M.nat(),
84-
M.string(),
85-
).returns(),
94+
disburse: M.call(EvmHashShape, M.nat()).returns(M.promise()),
95+
forward: M.call(EvmHashShape, M.nat(), M.string()).returns(),
8696
}),
8797
transferHandler: M.interface('SettlerTransferI', {
88-
onFulfilled: M.call(M.any(), M.record()).returns(),
89-
onRejected: M.call(M.any(), M.record()).returns(),
98+
onFulfilled: M.call(M.undefined(), M.string()).returns(),
99+
onRejected: M.call(M.error(), M.string()).returns(),
90100
}),
91101
},
92102
/**
@@ -174,20 +184,24 @@ export const prepareSettler = (
174184
log('dequeued', found, 'for', nfa, amount);
175185
switch (found?.status) {
176186
case PendingTxStatus.Advanced:
177-
return self.disburse(found.txHash, nfa, amount);
187+
return self.disburse(found.txHash, amount);
178188

179189
case PendingTxStatus.Advancing:
190+
log('⚠️ tap: minted while advancing', nfa, amount);
180191
this.state.mintedEarly.add(makeMintedEarlyKey(nfa, amount));
181192
return;
182193

183194
case PendingTxStatus.Observed:
184195
case PendingTxStatus.AdvanceSkipped:
185196
case PendingTxStatus.AdvanceFailed:
186-
return self.forward(found.txHash, nfa, amount, EUD);
197+
return self.forward(found.txHash, amount, EUD);
187198

188199
case undefined:
189200
default:
190-
log('⚠️ tap: no status for ', nfa, amount);
201+
log('⚠️ tap: minted before observed', nfa, amount);
202+
// XXX consider capturing in vstorage
203+
// we would need a new key, as this does not have a txHash
204+
this.state.mintedEarly.add(makeMintedEarlyKey(nfa, amount));
191205
}
192206
},
193207
},
@@ -210,16 +224,12 @@ export const prepareSettler = (
210224
const key = makeMintedEarlyKey(forwardingAddress, fullValue);
211225
if (mintedEarly.has(key)) {
212226
mintedEarly.delete(key);
227+
statusManager.advanceOutcomeForMintedEarly(txHash, success);
213228
if (success) {
214-
void this.facets.self.disburse(
215-
txHash,
216-
forwardingAddress,
217-
fullValue,
218-
);
229+
void this.facets.self.disburse(txHash, fullValue);
219230
} else {
220231
void this.facets.self.forward(
221232
txHash,
222-
forwardingAddress,
223233
fullValue,
224234
destination.value,
225235
);
@@ -228,14 +238,39 @@ export const prepareSettler = (
228238
statusManager.advanceOutcome(forwardingAddress, fullValue, success);
229239
}
230240
},
241+
/**
242+
* @param {CctpTxEvidence} evidence
243+
* @param {ChainAddress} destination
244+
* @returns {boolean}
245+
* @throws {Error} if minted early, so advancer doesn't advance
246+
*/
247+
checkMintedEarly(evidence, destination) {
248+
const {
249+
tx: { forwardingAddress, amount },
250+
txHash,
251+
} = evidence;
252+
const key = makeMintedEarlyKey(forwardingAddress, amount);
253+
const { mintedEarly } = this.state;
254+
if (mintedEarly.has(key)) {
255+
log(
256+
'matched minted early key, initiating forward',
257+
forwardingAddress,
258+
amount,
259+
);
260+
mintedEarly.delete(key);
261+
statusManager.advanceOutcomeForUnknownMint(evidence);
262+
void this.facets.self.forward(txHash, amount, destination.value);
263+
return true;
264+
}
265+
return false;
266+
},
231267
},
232268
self: {
233269
/**
234270
* @param {EvmHash} txHash
235-
* @param {NobleAddress} nfa
236271
* @param {NatValue} fullValue
237272
*/
238-
async disburse(txHash, nfa, fullValue) {
273+
async disburse(txHash, fullValue) {
239274
const { repayer, settlementAccount } = this.state;
240275
const received = AmountMath.make(USDC, fullValue);
241276
const { zcfSeat: settlingSeat } = zcf.makeEmptySeatKit();
@@ -260,56 +295,43 @@ export const prepareSettler = (
260295
);
261296
repayer.repay(settlingSeat, split);
262297

263-
// update status manager, marking tx `SETTLED`
298+
// update status manager, marking tx `DISBURSED`
264299
statusManager.disbursed(txHash, split);
265300
},
266301
/**
267302
* @param {EvmHash} txHash
268-
* @param {NobleAddress} nfa
269303
* @param {NatValue} fullValue
270304
* @param {string} EUD
271305
*/
272-
forward(txHash, nfa, fullValue, EUD) {
306+
forward(txHash, fullValue, EUD) {
273307
const { settlementAccount, intermediateRecipient } = this.state;
274-
275308
const dest = chainHub.makeChainAddress(EUD);
276-
277-
// TODO? statusManager.forwarding(txHash, sender, amount);
278309
const txfrV = E(settlementAccount).transfer(
279310
dest,
280311
AmountMath.make(USDC, fullValue),
281312
{ forwardOpts: { intermediateRecipient } },
282313
);
283-
void vowTools.watch(txfrV, this.facets.transferHandler, {
284-
txHash,
285-
nfa,
286-
fullValue,
287-
});
314+
void vowTools.watch(txfrV, this.facets.transferHandler, txHash);
288315
},
289316
},
290317
transferHandler: {
291318
/**
292319
* @param {unknown} _result
293-
* @param {SettlerTransferCtx} ctx
294-
*
295-
* @typedef {{
296-
* txHash: EvmHash;
297-
* nfa: NobleAddress;
298-
* fullValue: NatValue;
299-
* }} SettlerTransferCtx
320+
* @param {EvmHash} txHash
300321
*/
301-
onFulfilled(_result, ctx) {
302-
const { txHash, nfa, fullValue } = ctx;
303-
statusManager.forwarded(txHash, nfa, fullValue);
322+
onFulfilled(_result, txHash) {
323+
// update status manager, marking tx `FORWARDED` without fee split
324+
statusManager.forwarded(txHash, true);
304325
},
305326
/**
306327
* @param {unknown} reason
307-
* @param {SettlerTransferCtx} ctx
328+
* @param {EvmHash} txHash
308329
*/
309-
onRejected(reason, ctx) {
310-
log('⚠️ transfer rejected!', reason, ctx);
311-
// const { txHash, nfa, amount } = ctx;
312-
// TODO(#10510): statusManager.forwardFailed(txHash, nfa, amount);
330+
onRejected(reason, txHash) {
331+
log('⚠️ forward transfer rejected!', reason, txHash);
332+
// update status manager, flagging a terminal state that needs to be
333+
// manual intervention or a code update to remediate
334+
statusManager.forwarded(txHash, false);
313335
},
314336
},
315337
},

0 commit comments

Comments
 (0)