From c5de989462e84a2ceb78142f0fc1e4cabb9ccebd Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Mon, 9 Dec 2024 13:50:44 -0500 Subject: [PATCH] chore: return advance payment to LP if `zoeTools.localTransfer` fails --- packages/fast-usdc/src/exos/advancer.js | 21 +++-- packages/fast-usdc/src/exos/liquidity-pool.js | 24 +++++- packages/fast-usdc/test/exos/advancer.test.ts | 79 +++++++++++++++++-- 3 files changed, 111 insertions(+), 13 deletions(-) diff --git a/packages/fast-usdc/src/exos/advancer.js b/packages/fast-usdc/src/exos/advancer.js index f244ced3884..5c029b6876d 100644 --- a/packages/fast-usdc/src/exos/advancer.js +++ b/packages/fast-usdc/src/exos/advancer.js @@ -212,15 +212,22 @@ export const prepareAdvancerKit = ( * @param {Error} error * @param {AdvancerVowCtx & { tmpSeat: ZCFSeat }} ctx */ - onRejected(error, { tmpSeat }) { - // TODO return seat allocation from ctx to LP? - log('🚨 advance deposit failed', q(error).toString()); - // TODO #10510 (comprehensive error testing) determine - // course of action here + onRejected(error, { tmpSeat, advanceAmount, ...restCtx }) { + // we don't expect this to be a common failure. if it happens, return funds to LP log( - 'TODO live payment on seat to return to LP', - q(tmpSeat).toString(), + '⚠️ deposit to localOrchAccount failed, attempting to return payment to LP', + q(error).toString(), ); + try { + const { borrowerFacet, notifyFacet } = this.state; + notifyFacet.notifyAdvancingResult(restCtx, false); + borrowerFacet.repay(tmpSeat, harden({ USDC: advanceAmount })); + } catch (e) { + log( + '🚨 deposit to localOrchAccount failure recovery failed', + q(error).toString(), + ); + } }, }, transferHandler: { diff --git a/packages/fast-usdc/src/exos/liquidity-pool.js b/packages/fast-usdc/src/exos/liquidity-pool.js index d8b2991a4df..fd03d4caf46 100644 --- a/packages/fast-usdc/src/exos/liquidity-pool.js +++ b/packages/fast-usdc/src/exos/liquidity-pool.js @@ -88,6 +88,10 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => { SeatShape, harden({ USDC: makeNatAmountShape(USDC, 1n) }), ).returns(), + repay: M.call( + SeatShape, + harden({ USDC: makeNatAmountShape(USDC, 1n) }), + ).returns(), }), repayer: M.interface('repayer', { repay: M.call( @@ -178,7 +182,25 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => { Object.assign(this.state, post); this.facets.external.publishPoolMetrics(); }, - // TODO method to repay failed `LOA.deposit()` + /** + * If something fails during advance, return funds to the pool. + * + * @param {ZCFSeat} borrowSeat + * @param {{ USDC: Amount<'nat'>}} amountKWR + */ + repay(borrowSeat, amountKWR) { + const { zcfSeat: repaySeat } = zcf.makeEmptySeatKit(); + const returnAmounts = harden({ + Principal: amountKWR.USDC, + PoolFee: makeEmpty(USDC), + ContractFee: makeEmpty(USDC), + }); + // arrange payments in a format repay is expecting + zcf.atomicRearrange( + harden([[borrowSeat, repaySeat, amountKWR, returnAmounts]]), + ); + return this.facets.repayer.repay(repaySeat, returnAmounts); + }, }, repayer: { /** diff --git a/packages/fast-usdc/test/exos/advancer.test.ts b/packages/fast-usdc/test/exos/advancer.test.ts index 818554dacca..4a5df9f788b 100644 --- a/packages/fast-usdc/test/exos/advancer.test.ts +++ b/packages/fast-usdc/test/exos/advancer.test.ts @@ -62,6 +62,11 @@ const createTestExtensions = (t, common: CommonSetup) => { // pretend funds move from tmpSeat to poolAccount localTransferVK.resolver.resolve(); }; + const rejectLocalTransfeferV = () => { + localTransferVK.resolver.reject( + new Error('One or more deposits failed: simulated error'), + ); + }; const mockZoeTools = Far('MockZoeTools', { localTransfer(...args: Parameters) { console.log('ZoeTools.localTransfer called with', args); @@ -94,19 +99,30 @@ const createTestExtensions = (t, common: CommonSetup) => { }, }); + const mockBorrowerFacetCalls: unknown[][] = []; + const mockBorrowerF = Far('LiquidityPool Borrow Facet', { borrow: (seat: ZCFSeat, amounts: { USDC: NatAmount }) => { - console.log('LP.borrow called with', amounts); + console.log('LpBorrowFacet.borrow called with', amounts); + mockBorrowerFacetCalls.push(['borrow', seat, amounts]); + }, + repay: (seat: ZCFSeat, amounts: { USDC: NatAmount }) => { + console.log('LpBorrowFacet.repay called with', amounts); + mockBorrowerFacetCalls.push(['repay', seat, amounts]); }, }); const mockBorrowerErrorF = Far('LiquidityPool Borrow Facet', { borrow: (seat: ZCFSeat, amounts: { USDC: NatAmount }) => { - console.log('LP.borrow called with', amounts); + console.log('LpBorrowFacet.borrow called with', amounts); throw new Error( `Cannot borrow. Requested ${q(amounts.USDC)} must be less than pool balance ${q(usdc.make(1n))}.`, ); }, + repay: (seat: ZCFSeat, amounts: { USDC: NatAmount }) => { + console.log('LpBorrowFacet.repay called with', amounts); + throw new Error('Mock Cannot repay. Contract will shut down.'); + }, }); const advancer = makeAdvancer({ @@ -124,12 +140,14 @@ const createTestExtensions = (t, common: CommonSetup) => { helpers: { inspectLogs, inspectNotifyCalls: () => harden(notifyAdvancingResultCalls), + inspectBorrowerFacetCalls: () => harden(mockBorrowerFacetCalls), }, mocks: { ...mockAccounts, mockBorrowerErrorF, mockNotifyF, resolveLocalTransferV, + rejectLocalTransfeferV, }, services: { advancer, @@ -391,6 +409,57 @@ test('will not advance same txHash:chainId evidence twice', async t => { ]); }); -test.todo( - '#10510 zoeTools.localTransfer fails to deposit borrowed USDC to LOA', -); +test('returns payment to LP if zoeTools.localTransfer fails', async t => { + const { + extensions: { + services: { advancer }, + helpers: { inspectLogs, inspectBorrowerFacetCalls, inspectNotifyCalls }, + mocks: { rejectLocalTransfeferV }, + }, + brands: { usdc }, + } = t.context; + const mockEvidence = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); + + void advancer.handleTransactionEvent(mockEvidence); + rejectLocalTransfeferV(); + + await eventLoopIteration(); + + t.deepEqual( + inspectLogs(0), + [ + '⚠️ deposit to localOrchAccount failed, attempting to return payment to LP', + '"[Error: One or more deposits failed: simulated error]"', + ], + 'contract logs report error', + ); + + const [borrowCall, repayCall] = inspectBorrowerFacetCalls(); + + t.is(borrowCall[0], 'borrow'); + t.like(borrowCall[2], { + USDC: { + brand: usdc.brand, + }, + }); + + t.is(repayCall[0], 'repay', 'repay is called when zt.localTransfer fails'); + t.is( + repayCall[1], + borrowCall[1], + 'same temp borrowSeat is supplied to LP during repay', + ); + t.deepEqual(repayCall[2], borrowCall[2], 'advance amount is returned to LP'); + + t.like( + inspectNotifyCalls()[0], + [ + { + txHash: mockEvidence.txHash, + forwardingAddress: mockEvidence.tx.forwardingAddress, + }, + false, // indicates advance failed + ], + 'Advancing tx is recorded as AdvanceFailed', + ); +});