Skip to content

Commit

Permalink
chore: return to pool on advance failure
Browse files Browse the repository at this point in the history
  • Loading branch information
0xpatrickdev authored and samsiegart committed Feb 10, 2025
1 parent 82d456a commit 3c9d859
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 16 deletions.
63 changes: 59 additions & 4 deletions packages/fast-usdc/src/exos/advancer.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ import { makeFeeTools } from '../utils/fees.js';
* @typedef {{
* chainHub: ChainHub;
* feeConfig: FeeConfig;
* localTransfer: ZoeTools['localTransfer'];
* log?: LogFn;
* statusManager: StatusManager;
* usdc: { brand: Brand<'nat'>; denom: Denom; };
* vowTools: VowTools;
* zcf: ZCF;
* zoeTools: ZoeTools;
* }} AdvancerKitPowers
*/

Expand Down Expand Up @@ -69,6 +69,16 @@ const AdvancerKitI = harden({
),
onRejected: M.call(M.error(), AdvancerVowCtxShape).returns(M.undefined()),
}),
withdrawHandler: M.interface('WithdrawHandlerI', {
onFulfilled: M.call(M.undefined(), {
advanceAmount: AnyNatAmountShape,
tmpReturnSeat: M.remotable(),
}).returns(M.undefined()),
onRejected: M.call(M.error(), {
advanceAmount: AnyNatAmountShape,
tmpReturnSeat: M.remotable(),
}).returns(M.undefined()),
}),
});

/**
Expand Down Expand Up @@ -98,12 +108,12 @@ export const prepareAdvancerKit = (
{
chainHub,
feeConfig,
localTransfer,
log = makeTracer('Advancer', true),
statusManager,
usdc,
vowTools: { watch, when },
zcf,
zoeTools: { localTransfer, withdrawToSeat },
},
) => {
assertAllDefined({
Expand Down Expand Up @@ -287,10 +297,55 @@ export const prepareAdvancerKit = (
* @param {AdvancerVowCtx} ctx
*/
onRejected(error, ctx) {
const { notifier } = this.state;
const { notifier, poolAccount } = this.state;
log('Advance failed', error);
const { advanceAmount: _, ...restCtx } = ctx;
const { advanceAmount, ...restCtx } = ctx;
notifier.notifyAdvancingResult(restCtx, false);
const { zcfSeat: tmpReturnSeat } = zcf.makeEmptySeatKit();
const withdrawV = withdrawToSeat(
// @ts-expect-error LocalAccountMethods vs OrchestrationAccount
poolAccount,
tmpReturnSeat,
harden({ USDC: advanceAmount }),
);
void watch(withdrawV, this.facets.withdrawHandler, {
advanceAmount,
tmpReturnSeat,
});
},
},
withdrawHandler: {
/**
*
* @param {undefined} result
* @param {{ advanceAmount: Amount<'nat'>; tmpReturnSeat: ZCFSeat; }} ctx
*/
onFulfilled(result, { advanceAmount, tmpReturnSeat }) {
const { borrower } = this.state;
try {
borrower.returnToPool(tmpReturnSeat, advanceAmount);
} catch (e) {
// If we reach here, the unused advance funds will remain in `tmpReturnSeat`
// and must be retrieved from recovery sets.
log(
`🚨 return ${q(advanceAmount)} to pool failed. funds remain on "tmpReturnSeat"`,
e,
);
}
tmpReturnSeat.exit();
},
/**
* @param {Error} error
* @param {{ advanceAmount: Amount<'nat'>; tmpReturnSeat: ZCFSeat; }} ctx
*/
onRejected(error, { advanceAmount, tmpReturnSeat }) {
log(
`🚨 withdraw ${q(advanceAmount)} from "poolAccount" to return to pool failed`,
error,
);
// If we reach here, the unused advance funds will remain in the `poolAccount`.
// A contract update will be required to return them to the LiquidityPool.
tmpReturnSeat.exit();
},
},
},
Expand Down
4 changes: 2 additions & 2 deletions packages/fast-usdc/src/fast-usdc.contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,18 +125,18 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
chainHub,
});

const { localTransfer } = makeZoeTools(zcf, vowTools);
const zoeTools = makeZoeTools(zcf, vowTools);
const makeAdvancer = prepareAdvancer(zone, {
chainHub,
feeConfig,
localTransfer,
usdc: harden({
brand: terms.brands.USDC,
denom: terms.usdcDenom,
}),
statusManager,
vowTools,
zcf,
zoeTools,
});

const makeFeedKit = prepareTransactionFeedKit(zone, zcf);
Expand Down
54 changes: 44 additions & 10 deletions packages/fast-usdc/test/exos/advancer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,27 +81,33 @@ const createTestExtensions = (t, common: CommonSetup) => {
});

const localTransferVK = vowTools.makeVowKit<void>();
const resolveLocalTransferV = () => {
// pretend funds move from tmpSeat to poolAccount
localTransferVK.resolver.resolve();
};
const rejectLocalTransfeferV = () => {
// pretend funds move from tmpSeat to poolAccount
const resolveLocalTransferV = () => localTransferVK.resolver.resolve();
const rejectLocalTransfeferV = () =>
localTransferVK.resolver.reject(
new Error('One or more deposits failed: simulated error'),
);
};
const withdrawToSeatVK = vowTools.makeVowKit<void>();
const resolveWithdrawToSeatV = () => withdrawToSeatVK.resolver.resolve();
const rejectWithdrawToSeatV = () =>
withdrawToSeatVK.resolver.reject(
new Error('One or more deposits failed: simulated error'),
);
const mockZoeTools = Far('MockZoeTools', {
localTransfer(...args: Parameters<ZoeTools['localTransfer']>) {
trace('ZoeTools.localTransfer called with', args);
return localTransferVK.vow;
},
withdrawToSeat(...args: Parameters<ZoeTools['withdrawToSeat']>) {
trace('ZoeTools.withdrawToSeat called with', args);
return withdrawToSeatVK.vow;
},
});

const feeConfig = makeTestFeeConfig(usdc);
const makeAdvancer = prepareAdvancer(contractZone.subZone('advancer'), {
chainHub,
feeConfig,
localTransfer: mockZoeTools.localTransfer,
log,
statusManager,
usdc: harden({
Expand All @@ -111,6 +117,7 @@ const createTestExtensions = (t, common: CommonSetup) => {
vowTools,
// @ts-expect-error mocked zcf
zcf: mockZCF,
zoeTools: mockZoeTools,
});

type NotifyArgs = Parameters<SettlerKit['notifier']['notifyAdvancingResult']>;
Expand Down Expand Up @@ -169,6 +176,8 @@ const createTestExtensions = (t, common: CommonSetup) => {
mockNotifyF,
resolveLocalTransferV,
rejectLocalTransfeferV,
resolveWithdrawToSeatV,
rejectWithdrawToSeatV,
},
services: {
advancer,
Expand Down Expand Up @@ -340,13 +349,13 @@ test('updates status to OBSERVED if makeChainAddress fails', async t => {
]);
});

test('calls notifyAdvancingResult (AdvancedFailed) on failed transfer', async t => {
test('recovery behavior if Advance Fails (ADVANCE_FAILED)', async t => {
const {
bootstrap: { storage },
extensions: {
services: { advancer, feeTools },
helpers: { inspectLogs, inspectNotifyCalls },
mocks: { mockPoolAccount, resolveLocalTransferV },
helpers: { inspectBorrowerFacetCalls, inspectLogs, inspectNotifyCalls },
mocks: { mockPoolAccount, resolveLocalTransferV, resolveWithdrawToSeatV },
},
brands: { usdc },
} = t.context;
Expand Down Expand Up @@ -390,8 +399,31 @@ test('calls notifyAdvancingResult (AdvancedFailed) on failed transfer', async t
false, // this indicates transfer failed
],
]);

// simulate withdrawing `advanceAmount` from PoolAccount to tmpReturnSeat
resolveWithdrawToSeatV();
await eventLoopIteration();
const { returnToPool } = inspectBorrowerFacetCalls();
t.is(
returnToPool.length,
1,
'returnToPool is called after ibc transfer fails',
);
t.deepEqual(
returnToPool[0],
[
Far('MockZCFSeat', { exit: theExit }),
usdc.make(293999999n), // 300000000n net of fees
],
'same amount borrowed is returned to LP',
);
});

// unexpected, terminal state. test that log('🚨') is called
test.todo('witdrawToSeat fails during AdvanceFailed recovery');
// unexpected, terminal state. test that log('🚨') is called
test.todo('returnToPool fails during AdvanceFailed recovery');

test('updates status to OBSERVED if pre-condition checks fail', async t => {
const {
bootstrap: { storage },
Expand Down Expand Up @@ -816,4 +848,6 @@ test('notifies of advance failure if bank send fails', async t => {
false, // indicates send failed
],
]);

// TODO: returnToPool is called
});

0 comments on commit 3c9d859

Please sign in to comment.