diff --git a/packages/fast-usdc/README.md b/packages/fast-usdc/README.md index f242d8cc12a..d9be1d93a7f 100644 --- a/packages/fast-usdc/README.md +++ b/packages/fast-usdc/README.md @@ -59,43 +59,39 @@ sequenceDiagram # Status Manager -### Pending Advance State Diagram +### State Diagram -*Transactions are qualified by the OCW and EventFeed before arriving to the Advancer.* +*Transactions are qualified by the OCW and TransactionFeed before being +delivered to the Advancer.* -```mermaid -stateDiagram-v2 - [*] --> Observed: observe() - [*] --> Advancing: advancing() - - Advancing --> Advanced: advanceOutcome(...true) - Advancing --> AdvanceFailed: advanceOutcome(...false) - - Observed --> [*]: dequeueStatus() - Advanced --> [*]: dequeueStatus() - AdvanceFailed --> [*]: dequeueStatus() - - note right of [*] - After dequeueStatus(): - Transaction is removed - from pendingTxs store. - Settler will .disburse() - or .forward() - end note -``` +The transactionFeed receives attestations from Oracles, records their +evidence, and when enough oracles agree, (if no risks are identified) +it publishes the results for the advancer to act on. + +The Advancer subscribes (via `handleTransactionEvent`) to events published by +the transactionFeed. When notified of an appropriate opportunity, it is +responsible for advancing funds to fastUSDC payees. -### Complete state diagram (starting from Transaction Feed into Advancer) +The Settler is responsible for monitoring (via `receiveUpcall`) deposits to the +settlementAccount. It either `disburse`s funds to the Pool (if funds were +`advance`d to the payee), or `forwards` funds to the payee (if pool funds +were not `advance`d). ```mermaid stateDiagram-v2 + state Forwarding <> + state AdvancingChoice <> + Observed --> AdvanceSkipped : Risks identified Observed --> Advancing : No risks, can advance Observed --> Forwarding : No risks, Mint deposited before advance - Forwarding --> Forwarded - Advancing --> Advanced - Advanced --> Disbursed + + Forwarding --> Forwarded : settler.forward() succeeds + Advancing --> AdvancingChoice + AdvancingChoice --> Advanced : advancer's transferHandler detects success + Advanced --> Disbursed : settler.disburse() AdvanceSkipped --> Forwarding : Mint deposited AdvanceFailed --> Forwarding : Mint deposited - Advancing --> AdvanceFailed - Forwarding --> ForwardFailed -``` + AdvancingChoice --> AdvanceFailed : advancer's transferHandler detects failure + Forwarding --> ForwardFailed : settler.forward() fails + ``` diff --git a/packages/fast-usdc/src/constants.js b/packages/fast-usdc/src/constants.js index baeb178df71..a03a8cf8827 100644 --- a/packages/fast-usdc/src/constants.js +++ b/packages/fast-usdc/src/constants.js @@ -1,5 +1,5 @@ /** - * Status values for FastUSDC. + * Status values for FastUSDC. Includes states for advancing and settling. * * @enum {(typeof TxStatus)[keyof typeof TxStatus]} */ @@ -31,7 +31,7 @@ export const TerminalTxStatus = { }; /** - * Status values for the StatusManager. + * Status values for the StatusManager while an advance is being processed. * * @enum {(typeof PendingTxStatus)[keyof typeof PendingTxStatus]} */ diff --git a/packages/fast-usdc/src/exos/advancer.js b/packages/fast-usdc/src/exos/advancer.js index 9ba161b0d62..c40e82a44f9 100644 --- a/packages/fast-usdc/src/exos/advancer.js +++ b/packages/fast-usdc/src/exos/advancer.js @@ -1,3 +1,5 @@ +/** @file main export: @see {prepareAdvancerKit} */ + import { decodeAddressHook } from '@agoric/cosmic-proto/address-hooks.js'; import { AmountMath } from '@agoric/ertp'; import { assertAllDefined, makeTracer } from '@agoric/internal'; @@ -29,6 +31,7 @@ import { makeFeeTools } from '../utils/fees.js'; * @import {AddressHook, EvmHash, FeeConfig, LogFn, NobleAddress, EvidenceWithRisk} from '../types.js'; * @import {StatusManager} from './status-manager.js'; * @import {LiquidityPoolKit} from './liquidity-pool.js'; + * @import { TransactionFeedKit } from './transaction-feed.js'; */ /** @@ -103,6 +106,10 @@ export const stateShape = harden({ }); /** + * Advancer subscribes (using handleTransactionEvent) to events published by the + * {@link TransactionFeedKit}. When notified of an appropriate opportunity, it + * is responsible for advancing funds to EUD. + * * @param {Zone} zone * @param {AdvancerKitPowers} caps */ diff --git a/packages/fast-usdc/src/exos/liquidity-pool.js b/packages/fast-usdc/src/exos/liquidity-pool.js index 09903c5c89d..ab59b588977 100644 --- a/packages/fast-usdc/src/exos/liquidity-pool.js +++ b/packages/fast-usdc/src/exos/liquidity-pool.js @@ -269,15 +269,15 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => { const post = depositCalc(shareWorth, proposal); // COMMIT POINT - const mint = shareMint.mintGains(post.payouts); + const sharePayout = shareMint.mintGains(post.payouts); try { this.state.shareWorth = post.shareWorth; zcf.atomicRearrange( harden([ // zoe guarantees lp has proposal.give allocated [lp, poolSeat, proposal.give], - // mintGains() above establishes that mint has post.payouts - [mint, lp, post.payouts], + // mintGains() above establishes that sharePayout has post.payouts + [sharePayout, lp, post.payouts], ]), ); } catch (cause) { @@ -285,7 +285,7 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => { throw new Error('🚨 cannot commit deposit', { cause }); } finally { lp.exit(); - mint.exit(); + sharePayout.exit(); } external.publishPoolMetrics(); }, diff --git a/packages/fast-usdc/src/exos/settler.js b/packages/fast-usdc/src/exos/settler.js index 41ef127821e..663d4cdc5a9 100644 --- a/packages/fast-usdc/src/exos/settler.js +++ b/packages/fast-usdc/src/exos/settler.js @@ -1,3 +1,5 @@ +/** @file main export: @see {prepareSettler} */ + import { AmountMath } from '@agoric/ertp'; import { assertAllDefined, makeTracer } from '@agoric/internal'; import { CosmosChainAddressShape } from '@agoric/orchestration'; @@ -105,6 +107,17 @@ export const stateShape = harden({ }); /** + * The Settler is responsible for monitoring (using receiveUpcall) deposits to + * the settlementAccount. It either "disburses" funds to the Pool (if funds were + * "advance"d to the payee), or "forwards" funds to the payee (if pool funds + * were not advanced). + * + * Funds are forwarded + * + * `receivUpcall` is configured to receive notifications in + * `monitorMintingDeposits()`, with a call to + * `settlementAccount.monitorTransfers()`. + * * @param {Zone} zone * @param {object} caps * @param {StatusManager} caps.statusManager @@ -227,7 +240,6 @@ export const prepareSettler = ( self.addMintedEarly(nfa, amount); return; - case PendingTxStatus.Observed: case PendingTxStatus.AdvanceSkipped: case PendingTxStatus.AdvanceFailed: return self.forward(found.txHash, amount, EUD); @@ -275,10 +287,12 @@ export const prepareSettler = ( } }, /** + * If the EUD received minted funds without an advance, forward the + * funds to the pool. + * * @param {CctpTxEvidence} evidence * @param {CosmosChainAddress} destination - * @returns {boolean} - * @throws {Error} if minted early, so advancer doesn't advance + * @returns {boolean} whether the EUD received funds without an advance */ checkMintedEarly(evidence, destination) { const { @@ -313,6 +327,9 @@ export const prepareSettler = ( asMultiset(mintedEarly).add(key); }, /** + * The intended payee received an advance from the pool. When the funds + * are minted, disburse them to the pool and fee seats. + * * @param {EvmHash} txHash * @param {NatValue} fullValue */ @@ -344,6 +361,8 @@ export const prepareSettler = ( statusManager.disbursed(txHash, split); }, /** + * Funds were not advanced. Forward proceeds to the payee directly. + * * @param {EvmHash} txHash * @param {NatValue} fullValue * @param {string} EUD diff --git a/packages/fast-usdc/src/exos/status-manager.js b/packages/fast-usdc/src/exos/status-manager.js index 2403f045c59..fa077772eeb 100644 --- a/packages/fast-usdc/src/exos/status-manager.js +++ b/packages/fast-usdc/src/exos/status-manager.js @@ -209,7 +209,6 @@ export const prepareStatusManager = ( skipAdvance: M.call(CctpTxEvidenceShape, M.arrayOf(M.string())).returns(), advanceOutcomeForMintedEarly: M.call(EvmHashShape, M.boolean()).returns(), advanceOutcomeForUnknownMint: M.call(CctpTxEvidenceShape).returns(), - observe: M.call(CctpTxEvidenceShape).returns(), hasBeenObserved: M.call(CctpTxEvidenceShape).returns(M.boolean()), deleteCompletedTxs: M.call().returns(M.undefined()), dequeueStatus: M.call(M.string(), M.bigint()).returns( @@ -311,14 +310,6 @@ export const prepareStatusManager = ( publishEvidence(txHash, evidence); }, - /** - * Add a new transaction with OBSERVED status - * @param {CctpTxEvidence} evidence - */ - observe(evidence) { - initPendingTx(evidence, PendingTxStatus.Observed); - }, - /** * Note: ADVANCING state implies tx has been OBSERVED * diff --git a/packages/fast-usdc/src/exos/transaction-feed.js b/packages/fast-usdc/src/exos/transaction-feed.js index 2c9f9643cfb..f2eb4d4869b 100644 --- a/packages/fast-usdc/src/exos/transaction-feed.js +++ b/packages/fast-usdc/src/exos/transaction-feed.js @@ -1,4 +1,5 @@ /** @file Exo for @see {prepareTransactionFeedKit} */ + import { makeTracer } from '@agoric/internal'; import { prepareDurablePublishKit } from '@agoric/notifier'; import { Fail, quote } from '@endo/errors'; diff --git a/packages/fast-usdc/test/exos/settler.test.ts b/packages/fast-usdc/test/exos/settler.test.ts index bd14fa738e8..2521e924f73 100644 --- a/packages/fast-usdc/test/exos/settler.test.ts +++ b/packages/fast-usdc/test/exos/settler.test.ts @@ -170,18 +170,6 @@ const makeTestContext = async t => { return cctpTxEvidence; }, - /** - * slow path - e.g. insufficient pool funds - * @param evidence - */ - observe: (evidence?: CctpTxEvidence) => { - const cctpTxEvidence = makeEvidence(evidence); - t.log('Mock CCTP Evidence:', cctpTxEvidence); - t.log('Pretend we `OBSERVED` (did not advance)'); - statusManager.observe(cctpTxEvidence); - - return cctpTxEvidence; - }, /** * mint early path. caller must simulate tap before calling * @param evidence @@ -338,15 +326,7 @@ test('slow path: forward to EUD; remove pending tx', async t => { ...defaultSettlerParams, }); const simulate = makeSimulate(settler.notifier); - const cctpTxEvidence = simulate.observe(); - t.deepEqual( - statusManager.lookupPending( - cctpTxEvidence.tx.forwardingAddress, - cctpTxEvidence.tx.amount, - ), - [{ ...cctpTxEvidence, status: PendingTxStatus.Observed }], - 'statusManager shows this tx is only observed', - ); + const cctpTxEvidence = simulate.skipAdvance(['TOO_LARGE_AMOUNT']); t.log('Simulate incoming IBC settlement'); void settler.tap.receiveUpcall(MockVTransferEvents.AGORIC_PLUS_OSMO()); @@ -384,6 +364,7 @@ test('slow path: forward to EUD; remove pending tx', async t => { await eventLoopIteration(); t.deepEqual(storage.getDeserialized(`fun.txns.${cctpTxEvidence.txHash}`), [ { evidence: cctpTxEvidence, status: 'OBSERVED' }, + { risksIdentified: ['TOO_LARGE_AMOUNT'], status: 'ADVANCE_SKIPPED' }, { status: 'FORWARDED' }, ]); @@ -628,7 +609,7 @@ test('Multiple minted early transactions with same address and amount', async t ]); // Simulate a third transaction and verify no more are tracked as minted early - simulate.observe({ + simulate.observeLate({ ...MockCctpTxEvidences.AGORIC_PLUS_OSMO(), txHash: '0x0000000000000000000000000000000000000000000000000000000000000001', @@ -775,12 +756,11 @@ test('slow path, and forward fails (terminal state)', async t => { const { common, makeSettler, - statusManager, defaultSettlerParams, repayer, makeSimulate, + inspectLogs, accounts, - peekCalls, storage, } = t.context; const { usdc } = common.brands; @@ -790,43 +770,32 @@ test('slow path, and forward fails (terminal state)', async t => { settlementAccount: accounts.settlement.account, ...defaultSettlerParams, }); - const simulate = makeSimulate(settler.notifier); - const cctpTxEvidence = simulate.observe(); - t.deepEqual( - statusManager.lookupPending( - cctpTxEvidence.tx.forwardingAddress, - cctpTxEvidence.tx.amount, - ), - [{ ...cctpTxEvidence, status: PendingTxStatus.Observed }], - 'statusManager shows this tx is only observed', - ); + + t.log('simulating forward failure (e.g. unknown route)'); + const mockE = Error('no connection info found'); + accounts.settlement.transferVResolver.reject(mockE); + await eventLoopIteration(); t.log('Simulate incoming IBC settlement'); void settler.tap.receiveUpcall(MockVTransferEvents.AGORIC_PLUS_OSMO()); await eventLoopIteration(); + const simulate = makeSimulate(settler.notifier); - t.log('funds are forwarded; no interaction with LP'); - t.like(accounts.settlement.callLog, [ + const evidence = simulate.observeLate(); + + const rawLogs = inspectLogs(); + const penultimateLog = rawLogs.slice(rawLogs.length - 2, rawLogs.length - 1); + await eventLoopIteration(); + t.deepEqual(penultimateLog, [ [ - 'transfer', - 'cosmos:osmosis-1:osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men', - usdc.units(150), - { - forwardOpts: { - intermediateRecipient: { - value: 'noble1test', - }, - }, - }, + '⚠️ tap: minted before observed', + 'noble1x0ydg69dh6fqvr27xjvp6maqmrldam6yfelqkd', + 150000000n, ], ]); - t.log('simulating forward failure (e.g. unknown route)'); - const mockE = Error('no connection info found'); - accounts.settlement.transferVResolver.reject(mockE); - await eventLoopIteration(); - t.deepEqual(storage.getDeserialized(`fun.txns.${cctpTxEvidence.txHash}`), [ - { evidence: cctpTxEvidence, status: 'OBSERVED' }, + t.deepEqual(storage.getDeserialized(`fun.txns.${evidence.txHash}`), [ + { evidence, status: 'OBSERVED' }, { status: 'FORWARD_FAILED' }, ]); }); diff --git a/packages/fast-usdc/test/exos/status-manager.test.ts b/packages/fast-usdc/test/exos/status-manager.test.ts index 2e2e49a447e..0c7f7828a63 100644 --- a/packages/fast-usdc/test/exos/status-manager.test.ts +++ b/packages/fast-usdc/test/exos/status-manager.test.ts @@ -97,32 +97,6 @@ test('ADVANCE_SKIPPED transactions are published to vstorage', async t => { ]); }); -test('observe creates new entry with OBSERVED status', t => { - const { statusManager } = t.context; - const evidence = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); - statusManager.observe(evidence); - - const entries = statusManager.lookupPending( - evidence.tx.forwardingAddress, - evidence.tx.amount, - ); - - t.is(entries[0]?.status, PendingTxStatus.Observed); -}); - -test('OBSERVED transactions are published to vstorage', async t => { - const { statusManager } = t.context; - - const evidence = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); - statusManager.observe(evidence); - await eventLoopIteration(); - - const { storage } = t.context; - t.deepEqual(storage.getDeserialized(`fun.txns.${evidence.txHash}`), [ - { evidence, status: 'OBSERVED' }, - ]); -}); - test('cannot process same tx twice', t => { const { statusManager } = t.context; @@ -134,11 +108,6 @@ test('cannot process same tx twice', t => { 'Transaction already seen: "0xc81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761702"', }); - t.throws(() => statusManager.observe(evidence), { - message: - 'Transaction already seen: "0xc81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761702"', - }); - // new txHash should not throw t.notThrows(() => statusManager.advance({ ...evidence, txHash: '0xtest2' })); }); @@ -153,7 +122,7 @@ test('isSeen checks if a tx has been processed', t => { const e2 = MockCctpTxEvidences.AGORIC_PLUS_DYDX(); t.false(statusManager.hasBeenObserved(e2)); - statusManager.observe(e2); + statusManager.skipAdvance(e2, []); t.true(statusManager.hasBeenObserved(e2)); }); @@ -166,7 +135,7 @@ test('dequeueStatus removes entries from PendingTxs', t => { statusManager.advanceOutcome(e1.tx.forwardingAddress, e1.tx.amount, true); statusManager.advance(e2); statusManager.advanceOutcome(e2.tx.forwardingAddress, e2.tx.amount, false); - statusManager.observe({ ...e1, txHash: '0xtest1' }); + statusManager.skipAdvance({ ...e1, txHash: '0xtest1' }, []); t.deepEqual( statusManager.dequeueStatus(e1.tx.forwardingAddress, e1.tx.amount), @@ -188,7 +157,7 @@ test('dequeueStatus removes entries from PendingTxs', t => { statusManager.dequeueStatus(e1.tx.forwardingAddress, e1.tx.amount), { txHash: '0xtest1', - status: PendingTxStatus.Observed, + status: PendingTxStatus.AdvanceSkipped, }, ); @@ -217,7 +186,7 @@ test('cannot advanceOutcome without ADVANCING entry', t => { message: expectedErrMsg, }); - statusManager.observe(e1); + statusManager.skipAdvance(e1, []); t.throws(advanceOutcomeFn, { message: expectedErrMsg, }); @@ -299,9 +268,6 @@ test('dequeueStatus returns first (earliest) matched entry', async t => { true, ); - // can dequeue OBSERVED statuses - statusManager.observe({ ...evidence, txHash: '0xtest3' }); - // dequeue will return the first match t.like( statusManager.dequeueStatus( @@ -316,23 +282,8 @@ test('dequeueStatus returns first (earliest) matched entry', async t => { evidence.tx.forwardingAddress, evidence.tx.amount, ); - t.is(entries0.length, 1); - t.deepEqual( - entries0?.[0].status, - PendingTxStatus.Observed, - 'order of remaining entries preserved', - ); + t.is(entries0.length, 0); - // dequeue again wih same ags to settle remaining observe - t.like( - statusManager.dequeueStatus( - evidence.tx.forwardingAddress, - evidence.tx.amount, - ), - { - status: 'OBSERVED', - }, - ); const entries1 = statusManager.lookupPending( evidence.tx.forwardingAddress, evidence.tx.amount,