Skip to content

Commit 2d2dc7b

Browse files
committed
feat: evidence after mint results in forward
- Advancer calls `forwardIfMinted` to check for an early matching mint - if found, no Advance occurs and the minted funds are forwarded
1 parent 69740d2 commit 2d2dc7b

File tree

5 files changed

+282
-25
lines changed

5 files changed

+282
-25
lines changed

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

Lines changed: 35 additions & 7 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,26 @@ export const prepareAdvancerKit = (
167164
const destination = chainHub.makeChainAddress(EUD);
168165

169166
const fullAmount = toAmount(evidence.tx.amount);
167+
const {
168+
tx: { forwardingAddress },
169+
txHash,
170+
} = evidence;
171+
172+
const { borrowerFacet, notifyFacet, poolAccount } = this.state;
173+
if (
174+
notifyFacet.forwardIfMinted(
175+
destination,
176+
forwardingAddress,
177+
fullAmount,
178+
txHash,
179+
)
180+
) {
181+
// settlement already received; tx will Forward.
182+
// do not add to `pendingSettleTxs` by calling `.observe()`
183+
log('⚠️ minted before Observed');
184+
return;
185+
}
186+
170187
// throws if requested does not exceed fees
171188
const advanceAmount = feeTools.calculateAdvance(fullAmount);
172189

@@ -189,7 +206,7 @@ export const prepareAdvancerKit = (
189206
forwardingAddress: evidence.tx.forwardingAddress,
190207
fullAmount,
191208
tmpSeat,
192-
txHash: evidence.txHash,
209+
txHash,
193210
});
194211
} catch (error) {
195212
log('Advancer error:', error);
@@ -208,7 +225,13 @@ export const prepareAdvancerKit = (
208225
*/
209226
onFulfilled(result, ctx) {
210227
const { poolAccount, intermediateRecipient } = this.state;
211-
const { destination, advanceAmount, ...detail } = ctx;
228+
const {
229+
destination,
230+
advanceAmount,
231+
// eslint-disable-next-line no-unused-vars
232+
tmpSeat,
233+
...detail
234+
} = ctx;
212235
const transferV = E(poolAccount).transfer(
213236
destination,
214237
{ denom: usdc.denom, value: advanceAmount.value },
@@ -273,7 +296,12 @@ export const prepareAdvancerKit = (
273296
onRejected(error, ctx) {
274297
const { notifyFacet } = this.state;
275298
log('Advance transfer rejected', error);
276-
notifyFacet.notifyAdvancingResult(ctx, false);
299+
const {
300+
// eslint-disable-next-line no-unused-vars
301+
advanceAmount,
302+
...restCtx
303+
} = ctx;
304+
notifyFacet.notifyAdvancingResult(restCtx, false);
277305
},
278306
},
279307
},

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

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ export const prepareSettler = (
8181
makeAdvanceDetailsShape(USDC),
8282
M.boolean(),
8383
).returns(),
84+
forwardIfMinted: M.call(
85+
...Object.values(makeAdvanceDetailsShape(USDC)),
86+
).returns(M.boolean()),
8487
}),
8588
self: M.interface('SettlerSelfI', {
8689
disburse: M.call(EvmHashShape, M.nat()).returns(M.promise()),
@@ -190,7 +193,10 @@ export const prepareSettler = (
190193

191194
case undefined:
192195
default:
193-
log('⚠️ tap: no status for ', nfa, amount);
196+
log('⚠️ tap: minted before observed', nfa, amount);
197+
// XXX consider capturing in vstorage
198+
// we would need a new key, as this does not have a txHash
199+
this.state.mintedEarly.add(makeMintedEarlyKey(nfa, amount));
194200
}
195201
},
196202
},
@@ -231,6 +237,31 @@ export const prepareSettler = (
231237
statusManager.advanceOutcome(forwardingAddress, fullValue, success);
232238
}
233239
},
240+
/**
241+
* @param {ChainAddress} destination
242+
* @param {NobleAddress} forwardingAddress
243+
* @param {Amount<'nat'>} fullAmount
244+
* @param {EvmHash} txHash
245+
* @returns {boolean}
246+
* @throws {Error} if minted early, so advancer doesn't advance
247+
*/
248+
forwardIfMinted(destination, forwardingAddress, fullAmount, txHash) {
249+
const { value: fullValue } = fullAmount;
250+
const key = makeMintedEarlyKey(forwardingAddress, fullValue);
251+
const { mintedEarly } = this.state;
252+
if (mintedEarly.has(key)) {
253+
log(
254+
'matched minted early key, initiating forward',
255+
forwardingAddress,
256+
fullValue,
257+
);
258+
mintedEarly.delete(key);
259+
// TODO: does not write `OBSERVED` to vstorage
260+
void this.facets.self.forward(txHash, fullValue, destination.value);
261+
return true;
262+
}
263+
return false;
264+
},
234265
},
235266
self: {
236267
/**

packages/fast-usdc/test/exos/advancer.test.ts

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,17 @@ import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js';
99
import { denomHash } from '@agoric/orchestration';
1010
import fetchedChainInfo from '@agoric/orchestration/src/fetched-chain-info.js';
1111
import { type ZoeTools } from '@agoric/orchestration/src/utils/zoe-tools.js';
12-
import { q } from '@endo/errors';
12+
import { Fail, q } from '@endo/errors';
1313
import { Far } from '@endo/pass-style';
1414
import type { TestFn } from 'ava';
1515
import { makeTracer } from '@agoric/internal';
16+
import { M, mustMatch } from '@endo/patterns';
1617
import { PendingTxStatus } from '../../src/constants.js';
1718
import { prepareAdvancer } from '../../src/exos/advancer.js';
18-
import type { SettlerKit } from '../../src/exos/settler.js';
19+
import {
20+
makeAdvanceDetailsShape,
21+
type SettlerKit,
22+
} from '../../src/exos/settler.js';
1923
import { prepareStatusManager } from '../../src/exos/status-manager.js';
2024
import type { LiquidityPoolKit } from '../../src/types.js';
2125
import { makeFeeTools } from '../../src/utils/fees.js';
@@ -108,8 +112,19 @@ const createTestExtensions = (t, common: CommonSetup) => {
108112
const mockNotifyF = Far('Settler Notify Facet', {
109113
notifyAdvancingResult: (...args: NotifyArgs) => {
110114
trace('Settler.notifyAdvancingResult called with', args);
115+
const [advanceDetails, success] = args;
116+
mustMatch(harden(advanceDetails), makeAdvanceDetailsShape(usdc.brand));
117+
mustMatch(success, M.boolean());
111118
notifyAdvancingResultCalls.push(args);
112119
},
120+
// assume this never returns true for most tests
121+
forwardIfMinted: (...args) => {
122+
mustMatch(
123+
harden(args),
124+
harden([...Object.values(makeAdvanceDetailsShape(usdc.brand))]),
125+
);
126+
return false;
127+
},
113128
});
114129

115130
const mockBorrowerFacetCalls: {
@@ -361,7 +376,6 @@ test('calls notifyAdvancingResult (AdvancedFailed) on failed transfer', async t
361376
txHash: evidence.txHash,
362377
forwardingAddress: evidence.tx.forwardingAddress,
363378
fullAmount: usdc.make(evidence.tx.amount),
364-
advanceAmount: feeTools.calculateAdvance(usdc.make(evidence.tx.amount)),
365379
destination: {
366380
value: decodeAddressHook(evidence.aux.recipientAddress).query.EUD,
367381
},
@@ -659,3 +673,45 @@ test('rejects advances to unknown settlementAccount', async t => {
659673
],
660674
]);
661675
});
676+
677+
test('no status update if `forwardIfMinted` returns true', async t => {
678+
const {
679+
brands: { usdc },
680+
bootstrap: { storage },
681+
extensions: {
682+
services: { makeAdvancer },
683+
helpers: { inspectLogs },
684+
mocks: { mockPoolAccount, mockBorrowerF },
685+
},
686+
} = t.context;
687+
688+
const mockNotifyF = Far('Settler Notify Facet', {
689+
notifyAdvancingResult: () => {},
690+
forwardIfMinted: (destination, forwardingAddress, fullAmount, txHash) => {
691+
return true;
692+
},
693+
});
694+
695+
const advancer = makeAdvancer({
696+
borrowerFacet: mockBorrowerF,
697+
notifyFacet: mockNotifyF,
698+
poolAccount: mockPoolAccount.account,
699+
intermediateRecipient,
700+
settlementAddress,
701+
});
702+
703+
const evidence = MockCctpTxEvidences.AGORIC_PLUS_DYDX();
704+
void advancer.handleTransactionEvent({ evidence, risk: {} });
705+
await eventLoopIteration();
706+
707+
// advancer does not post a tx status; settler will Forward and
708+
// communicate Forwarded/ForwardFailed status'
709+
t.throws(() => storage.getDeserialized(`fun.txns.${evidence.txHash}`), {
710+
message: /no data at path fun.txns.0x/,
711+
});
712+
713+
t.deepEqual(inspectLogs(), [
714+
['decoded EUD: dydx183dejcnmkka5dzcu9xw6mywq0p2m5peks28men'],
715+
['⚠️ minted before Observed'],
716+
]);
717+
});

packages/fast-usdc/test/exos/settler.test.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,22 @@ const makeTestContext = async t => {
172172

173173
return cctpTxEvidence;
174174
},
175+
/**
176+
* mint early path. caller must simulate tap before calling
177+
* @param evidence
178+
*/
179+
observeLate: (evidence?: CctpTxEvidence) => {
180+
const cctpTxEvidence = makeEvidence(evidence);
181+
const { destination, forwardingAddress, fullAmount, txHash } =
182+
makeNotifyInfo(cctpTxEvidence);
183+
notifyFacet.forwardIfMinted(
184+
destination,
185+
forwardingAddress,
186+
fullAmount,
187+
txHash,
188+
);
189+
return cctpTxEvidence;
190+
},
175191
});
176192
return simulate;
177193
};
@@ -460,6 +476,77 @@ test('skip advance: forward to EUD; remove pending tx', async t => {
460476
t.is(storage.data.get(`fun.txns.${cctpTxEvidence.txHash}`), undefined);
461477
});
462478

479+
test('Settlement for unknown transaction (minted early)', async t => {
480+
const {
481+
common: {
482+
brands: { usdc },
483+
},
484+
makeSettler,
485+
defaultSettlerParams,
486+
repayer,
487+
accounts,
488+
peekCalls,
489+
inspectLogs,
490+
makeSimulate,
491+
storage,
492+
} = t.context;
493+
494+
const settler = makeSettler({
495+
repayer,
496+
settlementAccount: accounts.settlement.account,
497+
...defaultSettlerParams,
498+
});
499+
const simulate = makeSimulate(settler.notify);
500+
501+
t.log('Simulate incoming IBC settlement');
502+
void settler.tap.receiveUpcall(MockVTransferEvents.AGORIC_PLUS_OSMO());
503+
await eventLoopIteration();
504+
505+
t.log('Nothing was transferred');
506+
t.deepEqual(peekCalls(), []);
507+
t.deepEqual(accounts.settlement.callLog, []);
508+
const tapLogs = inspectLogs();
509+
t.like(tapLogs, [
510+
['config', { sourceChannel: 'channel-21' }],
511+
['upcall event'],
512+
['dequeued', undefined],
513+
['⚠️ tap: minted before observed'],
514+
]);
515+
516+
t.log('Oracle operators eventually report...');
517+
const evidence = simulate.observeLate();
518+
t.deepEqual(inspectLogs().slice(tapLogs.length - 1), [
519+
[
520+
'matched minted early key, initiating forward',
521+
'noble1x0ydg69dh6fqvr27xjvp6maqmrldam6yfelqkd',
522+
150000000n,
523+
],
524+
]);
525+
await eventLoopIteration();
526+
t.like(accounts.settlement.callLog, [
527+
[
528+
'transfer',
529+
{
530+
value: 'osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men',
531+
},
532+
usdc.units(150),
533+
{
534+
forwardOpts: {
535+
intermediateRecipient: {
536+
value: 'noble1test',
537+
},
538+
},
539+
},
540+
],
541+
]);
542+
accounts.settlement.transferVResolver.resolve(undefined);
543+
await eventLoopIteration();
544+
t.deepEqual(storage.getDeserialized(`fun.txns.${evidence.txHash}`), [
545+
/// TODO with no observed / evidence, does this break reporting reqs?
546+
{ status: 'FORWARDED' },
547+
]);
548+
});
549+
463550
test('Settlement for Advancing transaction (advance succeeds)', async t => {
464551
const {
465552
accounts,

0 commit comments

Comments
 (0)