From 9ceffb89cd6782abb669fc52436fd3ee978a4eb2 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Tue, 17 Dec 2024 10:59:30 -0800 Subject: [PATCH 1/8] chore: constrain fast-usdc config for Mainnet --- packages/fast-usdc/src/utils/deploy-config.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/fast-usdc/src/utils/deploy-config.js b/packages/fast-usdc/src/utils/deploy-config.js index 7b160a7996c..22b2cd5a593 100644 --- a/packages/fast-usdc/src/utils/deploy-config.js +++ b/packages/fast-usdc/src/utils/deploy-config.js @@ -156,3 +156,11 @@ export const configurations = { }, }; harden(configurations); + +// Constraints on the configurations +const MAINNET_EXPECTED_ORACLES = 3; +assert( + new Set(Object.values(configurations.MAINNET.oracles)).size === + MAINNET_EXPECTED_ORACLES, + `Mainnet must have exactly ${MAINNET_EXPECTED_ORACLES} oracles`, +); From 01713ed8470225f4eee0763ddb665e897a493843 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Tue, 17 Dec 2024 11:10:42 -0800 Subject: [PATCH 2/8] test: disabled operators --- .../fast-usdc/test/exos/transaction-feed.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/fast-usdc/test/exos/transaction-feed.test.ts b/packages/fast-usdc/test/exos/transaction-feed.test.ts index 5f11c504dc2..ff3f2705aa6 100644 --- a/packages/fast-usdc/test/exos/transaction-feed.test.ts +++ b/packages/fast-usdc/test/exos/transaction-feed.test.ts @@ -79,6 +79,21 @@ test('happy aggregation', async t => { }); }); +test('disabled operator', async t => { + const feedKit = makeFeedKit(); + const { op1 } = await makeOperators(feedKit); + const evidence = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); + + // works before disabling + await op1.operator.submitEvidence(evidence); + + op1.admin.disable(); + + await t.throwsAsync(() => op1.operator.submitEvidence(evidence), { + message: 'submitEvidence for disabled operator', + }); +}); + // TODO: find a way to get this working test.skip('forged source', async t => { const feedKit = makeFeedKit(); From f7f6e109c82753ef2e3acfbefb6ba3d37e27e830 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Tue, 17 Dec 2024 11:18:27 -0800 Subject: [PATCH 3/8] refactor: TransactionFeed attest --- packages/fast-usdc/src/exos/operator-kit.js | 4 ++-- packages/fast-usdc/src/exos/transaction-feed.js | 6 ++++-- packages/fast-usdc/test/exos/transaction-feed.test.ts | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/fast-usdc/src/exos/operator-kit.js b/packages/fast-usdc/src/exos/operator-kit.js index 08b3ac9c368..b5e847f30d5 100644 --- a/packages/fast-usdc/src/exos/operator-kit.js +++ b/packages/fast-usdc/src/exos/operator-kit.js @@ -12,7 +12,7 @@ const trace = makeTracer('TxOperator'); /** * @typedef {object} OperatorPowers - * @property {(evidence: CctpTxEvidence, operatorKit: OperatorKit) => void} submitEvidence + * @property {(evidence: CctpTxEvidence, operatorKit: OperatorKit) => void} attest */ /** @@ -102,7 +102,7 @@ export const prepareOperatorKit = (zone, staticPowers) => async submitEvidence(evidence) { const { state } = this; !state.disabled || Fail`submitEvidence for disabled operator`; - const result = state.powers.submitEvidence(evidence, this.facets); + const result = state.powers.attest(evidence, this.facets); return result; }, /** @returns {OperatorStatus} */ diff --git a/packages/fast-usdc/src/exos/transaction-feed.js b/packages/fast-usdc/src/exos/transaction-feed.js index 782236d615f..40d456ef9ea 100644 --- a/packages/fast-usdc/src/exos/transaction-feed.js +++ b/packages/fast-usdc/src/exos/transaction-feed.js @@ -18,7 +18,7 @@ export const INVITATION_MAKERS_DESC = 'oracle operator invitation'; const TransactionFeedKitI = harden({ operatorPowers: M.interface('Transaction Feed Admin', { - submitEvidence: M.call(CctpTxEvidenceShape, M.any()).returns(), + attest: M.call(CctpTxEvidenceShape, M.any()).returns(), }), creator: M.interface('Transaction Feed Creator', { // TODO narrow the return shape to OperatorKit @@ -118,10 +118,12 @@ export const prepareTransactionFeedKit = (zone, zcf) => { /** * Add evidence from an operator. * + * NB: the operatorKit is responsible for + * * @param {CctpTxEvidence} evidence * @param {OperatorKit} operatorKit */ - submitEvidence(evidence, operatorKit) { + attest(evidence, operatorKit) { const { pending } = this.state; trace( 'submitEvidence', diff --git a/packages/fast-usdc/test/exos/transaction-feed.test.ts b/packages/fast-usdc/test/exos/transaction-feed.test.ts index ff3f2705aa6..64c6f8c34a1 100644 --- a/packages/fast-usdc/test/exos/transaction-feed.test.ts +++ b/packages/fast-usdc/test/exos/transaction-feed.test.ts @@ -102,7 +102,7 @@ test.skip('forged source', async t => { // op1 is different than the facets object the evidence must come from t.throws(() => - feedKit.operatorPowers.submitEvidence( + feedKit.operatorPowers.attest( evidence, // @ts-expect-error XXX Types of property '[GET_INTERFACE_GUARD]' are incompatible. op1, From 11e169126851f5bad2f68785cec9e071a2b15b6b Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Tue, 17 Dec 2024 11:41:05 -0800 Subject: [PATCH 4/8] refactor: sync submitEvidence --- packages/fast-usdc/src/exos/operator-kit.js | 10 +++---- .../test/exos/transaction-feed.test.ts | 28 ++++++++----------- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/packages/fast-usdc/src/exos/operator-kit.js b/packages/fast-usdc/src/exos/operator-kit.js index b5e847f30d5..310b10ad0a9 100644 --- a/packages/fast-usdc/src/exos/operator-kit.js +++ b/packages/fast-usdc/src/exos/operator-kit.js @@ -35,7 +35,7 @@ const OperatorKitI = { }), operator: M.interface('Operator', { - submitEvidence: M.call(CctpTxEvidenceShape).returns(M.promise()), + submitEvidence: M.call(CctpTxEvidenceShape).returns(), getStatus: M.call().returns(M.record()), }), }; @@ -87,7 +87,7 @@ export const prepareOperatorKit = (zone, staticPowers) => const { operator } = this.facets; // TODO(bootstrap integration): cause this call to throw and confirm that it // shows up in the the smart-wallet UpdateRecord `error` property - await operator.submitEvidence(evidence); + operator.submitEvidence(evidence); return staticPowers.makeInertInvitation( 'evidence was pushed in the invitation maker call', ); @@ -98,12 +98,12 @@ export const prepareOperatorKit = (zone, staticPowers) => * submit evidence from this operator * * @param {CctpTxEvidence} evidence + * @returns {void} */ - async submitEvidence(evidence) { + submitEvidence(evidence) { const { state } = this; !state.disabled || Fail`submitEvidence for disabled operator`; - const result = state.powers.attest(evidence, this.facets); - return result; + state.powers.attest(evidence, this.facets); }, /** @returns {OperatorStatus} */ getStatus() { diff --git a/packages/fast-usdc/test/exos/transaction-feed.test.ts b/packages/fast-usdc/test/exos/transaction-feed.test.ts index 64c6f8c34a1..9f1c902aaae 100644 --- a/packages/fast-usdc/test/exos/transaction-feed.test.ts +++ b/packages/fast-usdc/test/exos/transaction-feed.test.ts @@ -48,12 +48,9 @@ test('happy aggregation', async t => { const { op1, op2, op3 } = await makeOperators(feedKit); const evidence = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); - const results = await Promise.all([ - op1.operator.submitEvidence(evidence), - op2.operator.submitEvidence(evidence), - op3.operator.submitEvidence(evidence), - ]); - t.deepEqual(results, [undefined, undefined, undefined]); + op1.operator.submitEvidence(evidence); + op2.operator.submitEvidence(evidence); + op3.operator.submitEvidence(evidence); const accepted = await evidenceSubscriber.getUpdateSince(0); t.deepEqual(accepted, { @@ -62,18 +59,17 @@ test('happy aggregation', async t => { }); // verify that it doesn't publish until three match - await Promise.all([ - // once it publishes, it doesn't remember that it already saw these - op1.operator.submitEvidence(evidence), - op2.operator.submitEvidence(evidence), - // but this time the third is different - op3.operator.submitEvidence(MockCctpTxEvidences.AGORIC_PLUS_DYDX()), - ]); + // once it publishes, it doesn't remember that it already saw these + op1.operator.submitEvidence(evidence); + op2.operator.submitEvidence(evidence); + // but this time the third is different + op3.operator.submitEvidence(MockCctpTxEvidences.AGORIC_PLUS_DYDX()); + t.like(await evidenceSubscriber.getUpdateSince(0), { // Update count is still 1 updateCount: 1n, }); - await op3.operator.submitEvidence(evidence); + op3.operator.submitEvidence(evidence); t.like(await evidenceSubscriber.getUpdateSince(0), { updateCount: 2n, }); @@ -85,11 +81,11 @@ test('disabled operator', async t => { const evidence = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); // works before disabling - await op1.operator.submitEvidence(evidence); + op1.operator.submitEvidence(evidence); op1.admin.disable(); - await t.throwsAsync(() => op1.operator.submitEvidence(evidence), { + t.throws(() => op1.operator.submitEvidence(evidence), { message: 'submitEvidence for disabled operator', }); }); From 041cf831c8c3df06adc20b52ee92ac70896b7b7c Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Tue, 17 Dec 2024 11:51:16 -0800 Subject: [PATCH 5/8] refactor: attest by operatorId --- packages/fast-usdc/src/exos/operator-kit.js | 4 ++-- .../fast-usdc/src/exos/transaction-feed.js | 18 ++++-------------- .../test/exos/transaction-feed.test.ts | 16 ---------------- 3 files changed, 6 insertions(+), 32 deletions(-) diff --git a/packages/fast-usdc/src/exos/operator-kit.js b/packages/fast-usdc/src/exos/operator-kit.js index 310b10ad0a9..7a98af3da5a 100644 --- a/packages/fast-usdc/src/exos/operator-kit.js +++ b/packages/fast-usdc/src/exos/operator-kit.js @@ -12,7 +12,7 @@ const trace = makeTracer('TxOperator'); /** * @typedef {object} OperatorPowers - * @property {(evidence: CctpTxEvidence, operatorKit: OperatorKit) => void} attest + * @property {(evidence: CctpTxEvidence, operatorId: string) => void} attest */ /** @@ -103,7 +103,7 @@ export const prepareOperatorKit = (zone, staticPowers) => submitEvidence(evidence) { const { state } = this; !state.disabled || Fail`submitEvidence for disabled operator`; - state.powers.attest(evidence, this.facets); + state.powers.attest(evidence, state.operatorId); }, /** @returns {OperatorStatus} */ getStatus() { diff --git a/packages/fast-usdc/src/exos/transaction-feed.js b/packages/fast-usdc/src/exos/transaction-feed.js index 40d456ef9ea..b991c570022 100644 --- a/packages/fast-usdc/src/exos/transaction-feed.js +++ b/packages/fast-usdc/src/exos/transaction-feed.js @@ -18,7 +18,7 @@ export const INVITATION_MAKERS_DESC = 'oracle operator invitation'; const TransactionFeedKitI = harden({ operatorPowers: M.interface('Transaction Feed Admin', { - attest: M.call(CctpTxEvidenceShape, M.any()).returns(), + attest: M.call(CctpTxEvidenceShape, M.string()).returns(), }), creator: M.interface('Transaction Feed Creator', { // TODO narrow the return shape to OperatorKit @@ -121,21 +121,11 @@ export const prepareTransactionFeedKit = (zone, zcf) => { * NB: the operatorKit is responsible for * * @param {CctpTxEvidence} evidence - * @param {OperatorKit} operatorKit + * @param {string} operatorId */ - attest(evidence, operatorKit) { + attest(evidence, operatorId) { const { pending } = this.state; - trace( - 'submitEvidence', - operatorKit.operator.getStatus().operatorId, - evidence, - ); - const { operatorId } = operatorKit.operator.getStatus(); - - // TODO should this verify that the operator is one made by this exo? - // This doesn't work... - // operatorKit === operators.get(operatorId) || - // Fail`operatorKit mismatch`; + trace('submitEvidence', operatorId, evidence); // TODO validate that it's a valid for Fast USDC before accepting // E.g. that the `recipientAddress` is the FU settlement account and that diff --git a/packages/fast-usdc/test/exos/transaction-feed.test.ts b/packages/fast-usdc/test/exos/transaction-feed.test.ts index 9f1c902aaae..8e3b04f9271 100644 --- a/packages/fast-usdc/test/exos/transaction-feed.test.ts +++ b/packages/fast-usdc/test/exos/transaction-feed.test.ts @@ -89,19 +89,3 @@ test('disabled operator', async t => { message: 'submitEvidence for disabled operator', }); }); - -// TODO: find a way to get this working -test.skip('forged source', async t => { - const feedKit = makeFeedKit(); - const { op1 } = await makeOperators(feedKit); - const evidence = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); - - // op1 is different than the facets object the evidence must come from - t.throws(() => - feedKit.operatorPowers.attest( - evidence, - // @ts-expect-error XXX Types of property '[GET_INTERFACE_GUARD]' are incompatible. - op1, - ), - ); -}); From 0a696846640f3373f3aff2416bafb56c9c5f1562 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Tue, 17 Dec 2024 12:46:18 -0800 Subject: [PATCH 6/8] feat: operator majority logic --- .../fast-usdc/src/exos/transaction-feed.js | 28 +++++++++++++----- .../test/exos/transaction-feed.test.ts | 29 +++++++++---------- 2 files changed, 35 insertions(+), 22 deletions(-) diff --git a/packages/fast-usdc/src/exos/transaction-feed.js b/packages/fast-usdc/src/exos/transaction-feed.js index b991c570022..87eacf07b3f 100644 --- a/packages/fast-usdc/src/exos/transaction-feed.js +++ b/packages/fast-usdc/src/exos/transaction-feed.js @@ -124,7 +124,7 @@ export const prepareTransactionFeedKit = (zone, zcf) => { * @param {string} operatorId */ attest(evidence, operatorId) { - const { pending } = this.state; + const { operators, pending } = this.state; trace('submitEvidence', operatorId, evidence); // TODO validate that it's a valid for Fast USDC before accepting @@ -146,18 +146,32 @@ export const prepareTransactionFeedKit = (zone, zcf) => { const found = [...pending.values()].filter(store => store.has(txHash), ); - // TODO determine the real policy for checking agreement - if (found.length < pending.getSize()) { - // not all have seen it + trace('found these stores with the txHash', found.length); + const minAttestations = Math.ceil(operators.getSize() / 2); + trace( + 'transaction', + txHash, + 'has', + found.length, + 'of', + minAttestations, + 'necessary attestations', + ); + if (found.length < minAttestations) { return; } // TODO verify that all found deep equal - // all agree, so remove from pending and publish - for (const pendingStore of pending.values()) { - pendingStore.delete(txHash); + // sufficient agreement, so remove from pending and publish + for (const store of found) { + store.delete(txHash); } + trace( + 'stores with the txHash after delete()', + [...pending.values()].filter(store => store.has(txHash)).length, + ); + trace('publishing evidence', evidence); publisher.publish(evidence); }, }, diff --git a/packages/fast-usdc/test/exos/transaction-feed.test.ts b/packages/fast-usdc/test/exos/transaction-feed.test.ts index 8e3b04f9271..5c5b74e094b 100644 --- a/packages/fast-usdc/test/exos/transaction-feed.test.ts +++ b/packages/fast-usdc/test/exos/transaction-feed.test.ts @@ -47,31 +47,30 @@ test('happy aggregation', async t => { const evidenceSubscriber = feedKit.public.getEvidenceSubscriber(); const { op1, op2, op3 } = await makeOperators(feedKit); - const evidence = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); - op1.operator.submitEvidence(evidence); - op2.operator.submitEvidence(evidence); - op3.operator.submitEvidence(evidence); + 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, + value: e1, updateCount: 1n, }); - // verify that it doesn't publish until three match - // once it publishes, it doesn't remember that it already saw these - op1.operator.submitEvidence(evidence); - op2.operator.submitEvidence(evidence); - // but this time the third is different - op3.operator.submitEvidence(MockCctpTxEvidences.AGORIC_PLUS_DYDX()); - + // Now third operator catches up with same evidence already published + op3.operator.submitEvidence(e1); t.like(await evidenceSubscriber.getUpdateSince(0), { - // Update count is still 1 + // The confirming evidence doesn't change anything updateCount: 1n, }); - op3.operator.submitEvidence(evidence); + + const e2 = MockCctpTxEvidences.AGORIC_PLUS_DYDX(); + assert(e1.txHash !== e2.txHash); + op1.operator.submitEvidence(e2); t.like(await evidenceSubscriber.getUpdateSince(0), { - updateCount: 2n, + // op1 attestation insufficient + updateCount: 1n, }); }); From 0dc6bd30a69b43840e533ed540acb7cc62a3cae9 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Tue, 17 Dec 2024 13:07:19 -0800 Subject: [PATCH 7/8] feat: error on conflicting evidence --- .../fast-usdc/src/exos/transaction-feed.js | 24 ++++++++++- .../test/exos/transaction-feed.test.ts | 43 +++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/packages/fast-usdc/src/exos/transaction-feed.js b/packages/fast-usdc/src/exos/transaction-feed.js index 87eacf07b3f..5c7b7b5d22e 100644 --- a/packages/fast-usdc/src/exos/transaction-feed.js +++ b/packages/fast-usdc/src/exos/transaction-feed.js @@ -1,6 +1,7 @@ import { makeTracer } from '@agoric/internal'; import { prepareDurablePublishKit } from '@agoric/notifier'; -import { M } from '@endo/patterns'; +import { keyEQ, M } from '@endo/patterns'; +import { Fail } from '@endo/errors'; import { CctpTxEvidenceShape } from '../type-guards.js'; import { defineInertInvitation } from '../utils/zoe.js'; import { prepareOperatorKit } from './operator-kit.js'; @@ -161,7 +162,26 @@ export const prepareTransactionFeedKit = (zone, zcf) => { return; } - // TODO verify that all found deep equal + let lastEvidence; + for (const store of found) { + const next = store.get(txHash); + if (lastEvidence) { + if (keyEQ(lastEvidence, next)) { + lastEvidence = next; + } else { + trace( + '🚨 conflicting evidence for', + txHash, + ':', + lastEvidence, + '!=', + next, + ); + Fail`conflicting evidence for ${txHash}`; + } + } + lastEvidence = next; + } // sufficient agreement, so remove from pending and publish for (const store of found) { diff --git a/packages/fast-usdc/test/exos/transaction-feed.test.ts b/packages/fast-usdc/test/exos/transaction-feed.test.ts index 5c5b74e094b..8f5084af0ea 100644 --- a/packages/fast-usdc/test/exos/transaction-feed.test.ts +++ b/packages/fast-usdc/test/exos/transaction-feed.test.ts @@ -47,6 +47,7 @@ test('happy aggregation', async t => { const evidenceSubscriber = feedKit.public.getEvidenceSubscriber(); const { op1, op2, op3 } = await makeOperators(feedKit); + const e1 = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); op1.operator.submitEvidence(e1); op2.operator.submitEvidence(e1); @@ -74,6 +75,48 @@ test('happy aggregation', async t => { }); }); +test('disagreement', async t => { + const feedKit = makeFeedKit(); + const { op1, op2 } = await makeOperators(feedKit); + const e1 = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); + const e1bad = { ...e1, tx: { ...e1.tx, amount: 999_999_999n } }; + assert(e1.txHash === e1bad.txHash); + op1.operator.submitEvidence(e1); + + t.throws(() => op2.operator.submitEvidence(e1bad), { + message: + 'conflicting evidence for "0xc81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761702"', + }); +}); + +test('disagreement after publishing', async t => { + const feedKit = makeFeedKit(); + const evidenceSubscriber = feedKit.public.getEvidenceSubscriber(); + const { op1, op2, op3 } = await makeOperators(feedKit); + const e1 = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); + const e1bad = { ...e1, tx: { ...e1.tx, amount: 999_999_999n } }; + assert(e1.txHash === e1bad.txHash); + op1.operator.submitEvidence(e1); + op2.operator.submitEvidence(e1); + + t.like(await evidenceSubscriber.getUpdateSince(0), { + updateCount: 1n, + }); + + // it's simply ignored + t.notThrows(() => op3.operator.submitEvidence(e1bad)); + t.like(await evidenceSubscriber.getUpdateSince(0), { + updateCount: 1n, + }); + + // now another op repeats the bad evidence, so it's published to the stream. + // It's the responsibility of the Advancer to fail because it has already processed that tx hash. + op1.operator.submitEvidence(e1bad); + t.like(await evidenceSubscriber.getUpdateSince(0), { + updateCount: 2n, + }); +}); + test('disabled operator', async t => { const feedKit = makeFeedKit(); const { op1 } = await makeOperators(feedKit); From 110d9c958dcce4f8f280eba32cd96ae5f7e1339a Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Tue, 17 Dec 2024 13:42:21 -0800 Subject: [PATCH 8/8] feat: verify settlement account --- .../fast-usdc/src/exos/transaction-feed.js | 33 +++++++++++++++---- packages/fast-usdc/src/fast-usdc.contract.js | 6 ++-- .../test/exos/transaction-feed.test.ts | 13 ++++++-- .../fast-usdc/test/fast-usdc.contract.test.ts | 13 +++++--- packages/fast-usdc/test/fixtures.ts | 26 +++++++-------- packages/fast-usdc/test/supports.ts | 4 ++- 6 files changed, 63 insertions(+), 32 deletions(-) diff --git a/packages/fast-usdc/src/exos/transaction-feed.js b/packages/fast-usdc/src/exos/transaction-feed.js index 5c7b7b5d22e..21e8bd13a0c 100644 --- a/packages/fast-usdc/src/exos/transaction-feed.js +++ b/packages/fast-usdc/src/exos/transaction-feed.js @@ -2,6 +2,7 @@ import { makeTracer } from '@agoric/internal'; import { prepareDurablePublishKit } from '@agoric/notifier'; import { keyEQ, M } from '@endo/patterns'; import { Fail } from '@endo/errors'; +import { decodeAddressHook } from '@agoric/cosmic-proto/address-hooks.js'; import { CctpTxEvidenceShape } from '../type-guards.js'; import { defineInertInvitation } from '../utils/zoe.js'; import { prepareOperatorKit } from './operator-kit.js'; @@ -54,7 +55,11 @@ export const prepareTransactionFeedKit = (zone, zcf) => { return zone.exoClassKit( 'Fast USDC Feed', TransactionFeedKitI, - () => { + /** + * @param {import('@agoric/orchestration').ChainAddress} settlementAccountAddress + */ + settlementAccountAddress => { + assert(settlementAccountAddress, 'missing settlementAccountAddress'); /** @type {MapStore} */ const operators = zone.mapStore('operators', { durable: true, @@ -63,7 +68,7 @@ export const prepareTransactionFeedKit = (zone, zcf) => { const pending = zone.mapStore('pending', { durable: true, }); - return { operators, pending }; + return { operators, pending, settlementAccountAddress }; }, { creator: { @@ -125,13 +130,27 @@ export const prepareTransactionFeedKit = (zone, zcf) => { * @param {string} operatorId */ attest(evidence, operatorId) { - const { operators, pending } = this.state; + const { operators, pending, settlementAccountAddress } = this.state; trace('submitEvidence', operatorId, evidence); - // TODO validate that it's a valid for Fast USDC before accepting - // E.g. that the `recipientAddress` is the FU settlement account and that - // the EUD is a chain supported by FU. - const { txHash } = evidence; + const { + aux: { recipientAddress }, + tx: { forwardingAddress }, + txHash, + } = evidence; + assert( + forwardingAddress.startsWith('noble'), + 'only Noble forwarding supported', + ); + const hook = decodeAddressHook(recipientAddress); + assert.equal( + hook.baseAddress, + settlementAccountAddress.value, + 'only Fast USDC settlementAccount supported as recipient', + ); + // We could also verify that hook.query.EUD is chain officially + // supported by Fast USDC. We filter upstream to give the on-chain + // contract more flexibility. // accept the evidence { diff --git a/packages/fast-usdc/src/fast-usdc.contract.js b/packages/fast-usdc/src/fast-usdc.contract.js index 80f8c4a6df1..73cfe27c70b 100644 --- a/packages/fast-usdc/src/fast-usdc.contract.js +++ b/packages/fast-usdc/src/fast-usdc.contract.js @@ -270,8 +270,6 @@ export const contract = async (zcf, privateArgs, zone, tools) => { const nobleAccountV = zone.makeOnce('NobleAccount', () => makeNobleAccount()); - const feedKit = zone.makeOnce('Feed Kit', () => makeFeedKit()); - const poolAccountV = zone.makeOnce('PoolAccount', () => makeLocalAccount()); const settleAccountV = zone.makeOnce('SettleAccount', () => makeLocalAccount(), @@ -284,6 +282,10 @@ export const contract = async (zcf, privateArgs, zone, tools) => { trace('settlementAccount', settlementAccount); trace('poolAccount', poolAccount); + const feedKit = zone.makeOnce('Feed Kit', () => + makeFeedKit(settlementAccount.getAddress()), + ); + const [_agoric, _noble, agToNoble] = await vowTools.when( chainHub.getChainsAndConnection('agoric', 'noble'), ); diff --git a/packages/fast-usdc/test/exos/transaction-feed.test.ts b/packages/fast-usdc/test/exos/transaction-feed.test.ts index 8f5084af0ea..6f2937e6dc7 100644 --- a/packages/fast-usdc/test/exos/transaction-feed.test.ts +++ b/packages/fast-usdc/test/exos/transaction-feed.test.ts @@ -3,18 +3,27 @@ import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import { deeplyFulfilledObject } from '@agoric/internal'; import { makeHeapZone } from '@agoric/zone'; +import type { ChainAddress } from '@agoric/orchestration'; import { prepareTransactionFeedKit, type TransactionFeedKit, } from '../../src/exos/transaction-feed.js'; -import { MockCctpTxEvidences } from '../fixtures.js'; +import { + MockCctpTxEvidences, + mockSettlementAccountAddress, +} from '../fixtures.js'; const nullZcf = null as any; +const settlementAccountAddress: ChainAddress = { + chainId: 'agoric', + value: mockSettlementAccountAddress, + encoding: 'bech32', +}; const makeFeedKit = () => { const zone = makeHeapZone(); const makeKit = prepareTransactionFeedKit(zone, nullZcf); - return makeKit(); + return makeKit(settlementAccountAddress); }; const makeOperators = (feedKit: TransactionFeedKit) => { diff --git a/packages/fast-usdc/test/fast-usdc.contract.test.ts b/packages/fast-usdc/test/fast-usdc.contract.test.ts index f7869f3def1..a4b04044454 100644 --- a/packages/fast-usdc/test/fast-usdc.contract.test.ts +++ b/packages/fast-usdc/test/fast-usdc.contract.test.ts @@ -36,7 +36,10 @@ import type { FastUsdcSF } from '../src/fast-usdc.contract.js'; import { PoolMetricsShape } from '../src/type-guards.js'; import type { CctpTxEvidence, FeeConfig, PoolMetrics } from '../src/types.js'; import { makeFeeTools } from '../src/utils/fees.js'; -import { MockCctpTxEvidences } from './fixtures.js'; +import { + MockCctpTxEvidences, + mockSettlementAccountAddress, +} from './fixtures.js'; import { commonSetup, uusdcOnAgoric } from './supports.js'; const dirname = path.dirname(new URL(import.meta.url).pathname); @@ -135,7 +138,7 @@ const makeTestContext = async (t: ExecutionContext) => { }; const mint = async (e: CctpTxEvidence) => { - const settlerAddr = 'agoric1fakeLCAAddress1'; // TODO: get from contract + const settlerAddr = mockSettlementAccountAddress; // TODO: get from contract const rxd = await receiveUSDCAt(settlerAddr, e.tx.amount); await VE(transferBridge).fromBridge( buildVTransferEvent({ @@ -190,7 +193,7 @@ test('getStaticInfo', async t => { t.deepEqual(await E(publicFacet).getStaticInfo(), { addresses: { poolAccount: 'agoric1fakeLCAAddress', - settlementAccount: 'agoric1fakeLCAAddress1', + settlementAccount: mockSettlementAccountAddress, }, }); }); @@ -549,7 +552,7 @@ test.serial('STORY01: advancing happy path for 100 USDC', async t => { t.deepEqual(inspectBankBridge().at(-1), { amount: String(expectedAdvance.value), denom: uusdcOnAgoric, - recipient: 'agoric1fakeLCAAddress', + recipient: mockSettlementAccountAddress, type: 'VBANK_GIVE', }); @@ -779,7 +782,7 @@ test.serial('Settlement for unknown transaction (operator down)', async t => { }, { amount: '20000000', - recipient: 'agoric1fakeLCAAddress1', + recipient: mockSettlementAccountAddress, type: 'VBANK_GIVE', }, ], diff --git a/packages/fast-usdc/test/fixtures.ts b/packages/fast-usdc/test/fixtures.ts index 35ec0c231e2..d31cf312f37 100644 --- a/packages/fast-usdc/test/fixtures.ts +++ b/packages/fast-usdc/test/fixtures.ts @@ -14,6 +14,9 @@ const mockScenarios = [ type MockScenario = (typeof mockScenarios)[number]; +export const mockSettlementAccountAddress = + 'agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek'; + export const Senders = { default: '0xDefaultFakeEthereumAddress', } as unknown as Record; @@ -37,10 +40,9 @@ export const MockCctpTxEvidences: Record< forwardingChannel: 'channel-21', recipientAddress: receiverAddress || - encodeAddressHook( - 'agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek', - { EUD: 'osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men' }, - ), + encodeAddressHook(mockSettlementAccountAddress, { + EUD: 'osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men', + }), }, chainId: 1, }), @@ -59,10 +61,9 @@ export const MockCctpTxEvidences: Record< forwardingChannel: 'channel-21', recipientAddress: receiverAddress || - encodeAddressHook( - 'agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek', - { EUD: 'dydx183dejcnmkka5dzcu9xw6mywq0p2m5peks28men' }, - ), + encodeAddressHook(mockSettlementAccountAddress, { + EUD: 'dydx183dejcnmkka5dzcu9xw6mywq0p2m5peks28men', + }), }, chainId: 1, }), @@ -79,9 +80,7 @@ export const MockCctpTxEvidences: Record< }, aux: { forwardingChannel: 'channel-21', - recipientAddress: - receiverAddress || - 'agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek', + recipientAddress: receiverAddress || mockSettlementAccountAddress, }, chainId: 1, }), @@ -100,10 +99,7 @@ export const MockCctpTxEvidences: Record< forwardingChannel: 'channel-21', recipientAddress: receiverAddress || - encodeAddressHook( - 'agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek', - { EUD: 'random1addr' }, - ), + encodeAddressHook(mockSettlementAccountAddress, { EUD: 'random1addr' }), }, chainId: 1, }), diff --git a/packages/fast-usdc/test/supports.ts b/packages/fast-usdc/test/supports.ts index 8ecdc66bdb6..e06b26275e8 100644 --- a/packages/fast-usdc/test/supports.ts +++ b/packages/fast-usdc/test/supports.ts @@ -190,7 +190,9 @@ export const commonSetup = async (t: ExecutionContext) => { ibcSequenceNonce += 1n; // let the promise for the transfer start await eventLoopIteration(); - const lastMsgTransfer = localBridgeMessages.at(-1).messages[0]; + const lastLBM = localBridgeMessages.at(-1); + assert('messages' in lastLBM, 'expected a different localBridgeMessage'); + const lastMsgTransfer = lastLBM.messages[0]; await E(transferBridge).fromBridge( buildVTransferEvent({ receiver: lastMsgTransfer.receiver,