diff --git a/packages/boot/test/fast-usdc/fast-usdc.test.ts b/packages/boot/test/fast-usdc/fast-usdc.test.ts index 38b2268e2c4..60850171ada 100644 --- a/packages/boot/test/fast-usdc/fast-usdc.test.ts +++ b/packages/boot/test/fast-usdc/fast-usdc.test.ts @@ -311,6 +311,56 @@ test.serial('makes usdc advance', async t => { await documentStorageSchema(t, storage, doc); }); +test.serial('skips usdc advance when risks identified', async t => { + const { walletFactoryDriver: wd, storage } = t.context; + const oracles = await Promise.all([ + wd.provideSmartWallet('agoric19uscwxdac6cf6z7d5e26e0jm0lgwstc47cpll8'), + wd.provideSmartWallet('agoric1krunjcqfrf7la48zrvdfeeqtls5r00ep68mzkr'), + wd.provideSmartWallet('agoric1n4fcxsnkxe4gj6e24naec99hzmc4pjfdccy5nj'), + ]); + + const EUD = 'dydx1riskyeud'; + const lastNodeValue = storage.getValues('published.fastUsdc').at(-1); + const { settlementAccount } = JSON.parse(NonNullish(lastNodeValue)); + const evidence = MockCctpTxEvidences.AGORIC_PLUS_DYDX( + // mock with the read settlementAccount address + encodeAddressHook(settlementAccount, { EUD }), + ); + + await Promise.all( + oracles.map(wallet => + wallet.sendOffer({ + id: 'submit-mock-evidence-dydx-risky', + invitationSpec: { + source: 'continuing', + previousOffer: 'claim-oracle-invitation', + invitationMakerName: 'SubmitEvidence', + invitationArgs: [evidence, { risksIdentified: ['TOO_LARGE_AMOUNT'] }], + }, + proposal: {}, + }), + ), + ); + await eventLoopIteration(); + + t.deepEqual( + storage + .getValues(`published.fastUsdc.txns.${evidence.txHash}`) + .map(defaultSerializer.parse), + [ + { evidence, status: 'OBSERVED' }, // observation includes evidence observed + { status: 'ADVANCE_SKIPPED', risksIdentified: ['TOO_LARGE_AMOUNT'] }, + ], + ); + + const doc = { + node: `fastUsdc.txns`, + owner: `the Ethereum transactions upon which Fast USDC is acting`, + showValue: defaultSerializer.parse, + }; + await documentStorageSchema(t, storage, doc); +}); + test.serial('restart contract', async t => { const { EV } = t.context.runUtils; await null; diff --git a/packages/boot/test/fast-usdc/snapshots/fast-usdc.test.ts.md b/packages/boot/test/fast-usdc/snapshots/fast-usdc.test.ts.md index ace588bbdf2..726ac84a4ac 100644 --- a/packages/boot/test/fast-usdc/snapshots/fast-usdc.test.ts.md +++ b/packages/boot/test/fast-usdc/snapshots/fast-usdc.test.ts.md @@ -174,3 +174,28 @@ Generated by [AVA](https://avajs.dev). }, ], ] + +## skips usdc advance when risks identified + +> Under "published", the "fastUsdc.txns" node is delegated to the Ethereum transactions upon which Fast USDC is acting. +> The example below illustrates the schema of the data published there. +> +> See also board marshalling conventions (_to appear_). + + [ + [ + 'published.fastUsdc.txns.0xc81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761702', + { + status: 'ADVANCING', + }, + ], + [ + 'published.fastUsdc.txns.0xd81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761799', + { + risksIdentified: [ + 'TOO_LARGE_AMOUNT', + ], + status: 'ADVANCE_SKIPPED', + }, + ], + ] diff --git a/packages/boot/test/fast-usdc/snapshots/fast-usdc.test.ts.snap b/packages/boot/test/fast-usdc/snapshots/fast-usdc.test.ts.snap index 0098b4788bf..8528f3eda7c 100644 Binary files a/packages/boot/test/fast-usdc/snapshots/fast-usdc.test.ts.snap and b/packages/boot/test/fast-usdc/snapshots/fast-usdc.test.ts.snap differ diff --git a/packages/fast-usdc/src/exos/transaction-feed.js b/packages/fast-usdc/src/exos/transaction-feed.js index 5560ad01f63..55bcd6cb859 100644 --- a/packages/fast-usdc/src/exos/transaction-feed.js +++ b/packages/fast-usdc/src/exos/transaction-feed.js @@ -36,6 +36,22 @@ const TransactionFeedKitI = harden({ }), }); +/** + * @param {globalThis.MapStore[]} riskStores + * @param {string} txHash + */ +const allRisksIdentified = (riskStores, txHash) => { + /** @type {Set} */ + const setOfRisks = new Set(); + for (const store of riskStores) { + const next = store.get(txHash); + for (const risk of next.risksIdentified ?? []) { + setOfRisks.add(risk); + } + } + return [...setOfRisks.values()].sort(); +}; + /** * @param {Zone} zone * @param {ZCF} zcf @@ -60,17 +76,11 @@ export const prepareTransactionFeedKit = (zone, zcf) => { TransactionFeedKitI, () => { /** @type {MapStore} */ - const operators = zone.mapStore('operators', { - durable: true, - }); + const operators = zone.mapStore('operators'); /** @type {MapStore>} */ - const pending = zone.mapStore('pending', { - durable: true, - }); + const pending = zone.mapStore('pending'); /** @type {MapStore>} */ - const risks = zone.mapStore('risks', { - durable: true, - }); + const risks = zone.mapStore('risks'); return { operators, pending, risks }; }, { @@ -136,7 +146,7 @@ export const prepareTransactionFeedKit = (zone, zcf) => { */ attest(evidence, riskAssessment, operatorId) { const { operators, pending, risks } = this.state; - trace('submitEvidence', operatorId, evidence); + trace('attest', operatorId, evidence); // TODO https://github.com/Agoric/agoric-sdk/pull/10720 // TODO validate that it's a valid for Fast USDC before accepting @@ -151,18 +161,8 @@ export const prepareTransactionFeedKit = (zone, zcf) => { trace(`operator ${operatorId} already reported ${txHash}`); } else { pendingStore.init(txHash, evidence); - } - } - - // accept the risk assessment - { - const riskStore = risks.get(operatorId); - if (riskStore.has(txHash)) { - trace( - `operator ${operatorId} already reported risk for ${txHash}, updating...`, - ); - riskStore.set(txHash, riskAssessment); - } else { + // accept the risk assessment as well + const riskStore = risks.get(operatorId); riskStore.init(txHash, riskAssessment); } } @@ -206,19 +206,11 @@ export const prepareTransactionFeedKit = (zone, zcf) => { lastEvidence = next; } - // take the union of risks identified from all operators - /** @type {Set} */ - const setUnionRisks = new Set(); const riskStores = [...risks.values()].filter(store => store.has(txHash), ); - for (const store of riskStores) { - const next = store.get(txHash); - for (const risk of next.risksIdentified ?? []) { - setUnionRisks.add(risk); - } - } - const unionRisks = [...setUnionRisks.values()].sort(); + // take the union of risks identified from all operators + const risksIdentified = allRisksIdentified(riskStores, txHash); // sufficient agreement, so remove from pending risks, then publish for (const store of found) { @@ -227,10 +219,10 @@ export const prepareTransactionFeedKit = (zone, zcf) => { for (const store of riskStores) { store.delete(txHash); } - trace('publishing evidence', evidence, unionRisks); + trace('publishing evidence', evidence, risksIdentified); publisher.publish({ evidence, - risk: { risksIdentified: unionRisks }, + risk: { risksIdentified }, }); }, }, diff --git a/packages/fast-usdc/test/exos/settler.test.ts b/packages/fast-usdc/test/exos/settler.test.ts index 41453467923..5a8d9928385 100644 --- a/packages/fast-usdc/test/exos/settler.test.ts +++ b/packages/fast-usdc/test/exos/settler.test.ts @@ -399,7 +399,7 @@ test('skip advance: forward to EUD; remove pending tx', async t => { cctpTxEvidence.tx.amount, ), [], - 'SETTLED entry removed from StatusManger', + 'FORWARDED entry removed from StatusManger', ); const { storage } = t.context; t.deepEqual(storage.getDeserialized(`fun.txns.${cctpTxEvidence.txHash}`), [ diff --git a/packages/fast-usdc/test/exos/transaction-feed.test.ts b/packages/fast-usdc/test/exos/transaction-feed.test.ts index 7c7c6a04d5d..e3cb8df299b 100644 --- a/packages/fast-usdc/test/exos/transaction-feed.test.ts +++ b/packages/fast-usdc/test/exos/transaction-feed.test.ts @@ -93,6 +93,60 @@ test('takes union of risk assessments', async t => { }); }); +test('takes union of risk assessments pt. 2', async t => { + const feedKit = makeFeedKit(); + const evidenceSubscriber = feedKit.public.getEvidenceSubscriber(); + + const { op1, op2 } = await makeOperators(feedKit); + + const e1 = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); + op1.operator.submitEvidence(e1, { risksIdentified: ['RISK1'] }); + op2.operator.submitEvidence(e1); + + // Publishes with 2 of 3 + const accepted = await evidenceSubscriber.getUpdateSince(0); + t.deepEqual(accepted, { + value: { evidence: e1, risk: { risksIdentified: ['RISK1'] } }, + updateCount: 1n, + }); +}); + +test('takes union of risk assessments pt. 3', async t => { + const feedKit = makeFeedKit(); + const evidenceSubscriber = feedKit.public.getEvidenceSubscriber(); + + const { op1, op2 } = await makeOperators(feedKit); + + const e1 = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); + op1.operator.submitEvidence(e1, { risksIdentified: ['RISK1'] }); + op2.operator.submitEvidence(e1, { risksIdentified: ['RISK1'] }); + + // Publishes with 2 of 3 + const accepted = await evidenceSubscriber.getUpdateSince(0); + t.deepEqual(accepted, { + value: { evidence: e1, risk: { risksIdentified: ['RISK1'] } }, + updateCount: 1n, + }); +}); + +test('takes union of risk assessments pt. 4', async t => { + const feedKit = makeFeedKit(); + const evidenceSubscriber = feedKit.public.getEvidenceSubscriber(); + + const { op1, op2 } = await makeOperators(feedKit); + + const e1 = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); + op1.operator.submitEvidence(e1); + op2.operator.submitEvidence(e1); + + // Publishes with 2 of 3 + const accepted = await evidenceSubscriber.getUpdateSince(0); + t.deepEqual(accepted, { + value: { evidence: e1, risk: { risksIdentified: [] } }, + updateCount: 1n, + }); +}); + test('disagreement', async t => { const feedKit = makeFeedKit(); const { op1, op2 } = await makeOperators(feedKit);