From c8ec060b2229f1d0949c0bd735827f3ffb5d4202 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Thu, 27 Feb 2025 20:49:24 -0500 Subject: [PATCH] fix: minted early tracking - ensure multiples of the same NFA+amount are tracked --- packages/fast-usdc/src/exos/settler.js | 38 ++++++- packages/fast-usdc/test/exos/settler.test.ts | 113 +++++++++++++++++++ 2 files changed, 145 insertions(+), 6 deletions(-) diff --git a/packages/fast-usdc/src/exos/settler.js b/packages/fast-usdc/src/exos/settler.js index e7d69b4c188..31cf2616abf 100644 --- a/packages/fast-usdc/src/exos/settler.js +++ b/packages/fast-usdc/src/exos/settler.js @@ -149,6 +149,7 @@ export const prepareSettler = ( ).returns(M.boolean()), }), self: M.interface('SettlerSelfI', { + queueMintedEarly: M.call(M.string(), M.nat()).returns(), disburse: M.call(EvmHashShape, M.nat()).returns(M.promise()), forward: M.call(EvmHashShape, M.nat(), M.string()).returns(), }), @@ -174,8 +175,8 @@ export const prepareSettler = ( intermediateRecipient: config.intermediateRecipient, /** @type {HostInterface|undefined} */ registration: undefined, - /** @type {SetStore>} */ - mintedEarly: zone.detached().setStore('mintedEarly'), + /** @type {MapStore, bigint>} */ + mintedEarly: zone.detached().mapStore('mintedEarly'), }; }, { @@ -221,7 +222,7 @@ export const prepareSettler = ( case PendingTxStatus.Advancing: log('⚠️ tap: minted while advancing', nfa, amount); - this.state.mintedEarly.add(makeMintedEarlyKey(nfa, amount)); + self.queueMintedEarly(nfa, amount); return; case PendingTxStatus.Observed: @@ -234,7 +235,7 @@ export const prepareSettler = ( log('⚠️ tap: minted before observed', nfa, amount); // XXX consider capturing in vstorage // we would need a new key, as this does not have a txHash - this.state.mintedEarly.add(makeMintedEarlyKey(nfa, amount)); + self.queueMintedEarly(nfa, amount); } }, }, @@ -256,7 +257,12 @@ export const prepareSettler = ( const { value: fullValue } = fullAmount; const key = makeMintedEarlyKey(forwardingAddress, fullValue); if (mintedEarly.has(key)) { - mintedEarly.delete(key); + const count = mintedEarly.get(key); + if (count === 1n) { + mintedEarly.delete(key); + } else { + mintedEarly.set(key, count - 1n); + } statusManager.advanceOutcomeForMintedEarly(txHash, success); if (success) { void this.facets.self.disburse(txHash, fullValue); @@ -290,7 +296,12 @@ export const prepareSettler = ( forwardingAddress, amount, ); - mintedEarly.delete(key); + const count = mintedEarly.get(key); + if (count === 1n) { + mintedEarly.delete(key); + } else { + mintedEarly.set(key, count - 1n); + } statusManager.advanceOutcomeForUnknownMint(evidence); void this.facets.self.forward(txHash, amount, destination.value); return true; @@ -299,6 +310,21 @@ export const prepareSettler = ( }, }, self: { + /** + * Helper function to track a minted-early transaction by incrementing or initializing its counter + * @param {NobleAddress} address + * @param {NatValue} amount + */ + queueMintedEarly(address, amount) { + const key = makeMintedEarlyKey(address, amount); + const { mintedEarly } = this.state; + if (mintedEarly.has(key)) { + const count = mintedEarly.get(key); + mintedEarly.set(key, count + 1n); + } else { + mintedEarly.init(key, 1n); + } + }, /** * @param {EvmHash} txHash * @param {NatValue} fullValue diff --git a/packages/fast-usdc/test/exos/settler.test.ts b/packages/fast-usdc/test/exos/settler.test.ts index 61ea505df15..089468100f9 100644 --- a/packages/fast-usdc/test/exos/settler.test.ts +++ b/packages/fast-usdc/test/exos/settler.test.ts @@ -546,6 +546,119 @@ test('Settlement for unknown transaction (minted early)', async t => { ]); }); +test('Multiple minted early transactions with same address and amount', async t => { + const { + common: { + brands: { usdc }, + }, + makeSettler, + defaultSettlerParams, + repayer, + accounts, + peekCalls, + inspectLogs, + makeSimulate, + storage, + } = t.context; + + const settler = makeSettler({ + repayer, + settlementAccount: accounts.settlement.account, + ...defaultSettlerParams, + }); + const simulate = makeSimulate(settler.notifier); + + t.log('Simulate first incoming IBC settlement'); + void settler.tap.receiveUpcall(MockVTransferEvents.AGORIC_PLUS_OSMO()); + await eventLoopIteration(); + + t.log('Simulate second incoming IBC settlement with same address and amount'); + void settler.tap.receiveUpcall(MockVTransferEvents.AGORIC_PLUS_OSMO()); + await eventLoopIteration(); + + t.log('Nothing transferred yet - both transactions are minted early'); + t.deepEqual(peekCalls(), []); + t.deepEqual(accounts.settlement.callLog, []); + const tapLogs = inspectLogs(); + + // Should show two instances of "minted before observed" + const mintedBeforeObservedLogs = tapLogs + .flat() + .filter( + log => typeof log === 'string' && log.includes('minted before observed'), + ); + t.is( + mintedBeforeObservedLogs.length, + 2, + 'Should have two "minted before observed" log entries', + ); + + t.log('Oracle operators report first transaction...'); + const evidence1 = simulate.observeLate( + MockCctpTxEvidences.AGORIC_PLUS_OSMO(), + ); + await eventLoopIteration(); + + t.log('First transfer should complete'); + t.is( + accounts.settlement.callLog.length, + 1, + 'First transfer should be initiated', + ); + accounts.settlement.transferVResolver.resolve(undefined); + await eventLoopIteration(); + t.deepEqual(storage.getDeserialized(`fun.txns.${evidence1.txHash}`), [ + { evidence: evidence1, status: 'OBSERVED' }, + { status: 'FORWARDED' }, + ]); + + t.log( + 'Oracle operators report second transaction with same address/amount...', + ); + const evidence2 = simulate.observeLate({ + ...MockCctpTxEvidences.AGORIC_PLUS_OSMO(), + txHash: + '0x0000000000000000000000000000000000000000000000000000000000000000', + }); + await eventLoopIteration(); + + t.log('Second transfer should also complete'); + t.is( + accounts.settlement.callLog.length, + 2, + 'Second transfer should be initiated', + ); + accounts.settlement.transferVResolver.resolve(undefined); + await eventLoopIteration(); + t.deepEqual(storage.getDeserialized(`fun.txns.${evidence2.txHash}`), [ + { evidence: evidence2, status: 'OBSERVED' }, + { status: 'FORWARDED' }, + ]); + + // Simulate a third transaction and verify no more are tracked as minted early + simulate.observe({ + ...MockCctpTxEvidences.AGORIC_PLUS_OSMO(), + txHash: + '0x0000000000000000000000000000000000000000000000000000000000000001', + }); + const foundMore = inspectLogs() + .flat() + .filter( + log => + typeof log === 'string' && log.includes('matched minted early key'), + ); + t.is( + foundMore.length, + 2, + 'Should not find any more minted early transactions', + ); + t.is( + accounts.settlement.callLog.length, + 2, + 'No additional transfers should be initiated', + ); +}); + test('Settlement for Advancing transaction (advance succeeds)', async t => { const { accounts,