Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: fusdc recovery paths #10659

Merged
merged 5 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 28 additions & 18 deletions packages/fast-usdc/src/exos/advancer.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { assertAllDefined, makeTracer } from '@agoric/internal';
import { AnyNatAmountShape, ChainAddressShape } from '@agoric/orchestration';
import { pickFacet } from '@agoric/vat-data';
import { VowShape } from '@agoric/vow';
import { q } from '@endo/errors';
import { E } from '@endo/far';
import { M } from '@endo/patterns';
import {
Expand Down Expand Up @@ -153,6 +152,7 @@ export const prepareAdvancerKit = (
recipientAddress,
EudParamShape,
);
log(`decoded EUD: ${EUD}`);
// throws if the bech32 prefix is not found
const destination = chainHub.makeChainAddress(EUD);

Expand All @@ -161,9 +161,8 @@ export const prepareAdvancerKit = (
const advanceAmount = feeTools.calculateAdvance(fullAmount);

const { zcfSeat: tmpSeat } = zcf.makeEmptySeatKit();
const amountKWR = harden({ USDC: advanceAmount });
// throws if the pool has insufficient funds
borrowerFacet.borrow(tmpSeat, amountKWR);
borrowerFacet.borrow(tmpSeat, advanceAmount);

// this cannot throw since `.isSeen()` is called in the same turn
statusManager.advance(evidence);
Expand All @@ -172,7 +171,7 @@ export const prepareAdvancerKit = (
tmpSeat,
// @ts-expect-error LocalAccountMethods vs OrchestrationAccount
poolAccount,
amountKWR,
harden({ USDC: advanceAmount }),
);
void watch(depositV, this.facets.depositHandler, {
fullAmount,
Expand All @@ -182,8 +181,8 @@ export const prepareAdvancerKit = (
tmpSeat,
txHash: evidence.txHash,
});
} catch (e) {
log('Advancer error:', q(e).toString());
} catch (error) {
log('Advancer error:', error);
statusManager.observe(evidence);
}
},
Expand Down Expand Up @@ -212,18 +211,28 @@ export const prepareAdvancerKit = (
});
},
/**
* We do not expect this to be a common failure. it should only occur
* if USDC is not registered in vbank or the tmpSeat has less than
* `advanceAmount`.
*
* If we do hit this path, we return funds to the Liquidity Pool and
* notify of Advancing failure.
*
* @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 }) {
log(
'TODO live payment on seat to return to LP',
q(tmpSeat).toString(),
'⚠️ deposit to localOrchAccount failed, attempting to return payment to LP',
error,
);
try {
const { borrowerFacet, notifyFacet } = this.state;
notifyFacet.notifyAdvancingResult(restCtx, false);
borrowerFacet.returnToPool(tmpSeat, advanceAmount);
} catch (e) {
log('🚨 deposit to localOrchAccount failure recovery failed', e);
}
},
},
transferHandler: {
Expand All @@ -234,10 +243,11 @@ export const prepareAdvancerKit = (
onFulfilled(result, ctx) {
const { notifyFacet } = this.state;
const { advanceAmount, destination, ...detail } = ctx;
log(
'Advance transfer fulfilled',
q({ advanceAmount, destination, result }).toString(),
);
log('Advance transfer fulfilled', {
advanceAmount,
destination,
result,
});
// During development, due to a bug, this call threw.
// The failure was silent (no diagnostics) due to:
// - #10576 Vows do not report unhandled rejections
Expand All @@ -252,7 +262,7 @@ export const prepareAdvancerKit = (
*/
onRejected(error, ctx) {
const { notifyFacet } = this.state;
log('Advance transfer rejected', q(error).toString());
log('Advance transfer rejected', error);
notifyFacet.notifyAdvancingResult(ctx, false);
},
},
Expand Down
95 changes: 51 additions & 44 deletions packages/fast-usdc/src/exos/liquidity-pool.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
* @import {PoolStats} from '../types.js';
*/

const { add, isEqual, makeEmpty } = AmountMath;
const { add, isEqual, isGTE, makeEmpty } = AmountMath;

/** @param {Brand} brand */
const makeDust = brand => AmountMath.make(brand, 1n);
Expand Down Expand Up @@ -84,10 +84,8 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
'Liquidity Pool',
{
borrower: M.interface('borrower', {
borrow: M.call(
SeatShape,
harden({ USDC: makeNatAmountShape(USDC, 1n) }),
).returns(),
borrow: M.call(SeatShape, makeNatAmountShape(USDC, 1n)).returns(),
returnToPool: M.call(SeatShape, makeNatAmountShape(USDC, 1n)).returns(),
}),
repayer: M.interface('repayer', {
repay: M.call(
Expand Down Expand Up @@ -153,32 +151,48 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
borrower: {
/**
* @param {ZCFSeat} toSeat
* @param {{ USDC: Amount<'nat'>}} amountKWR
* @param {Amount<'nat'>} amount
*/
borrow(toSeat, amountKWR) {
borrow(toSeat, amount) {
const { encumberedBalance, poolSeat, poolStats } = this.state;

// Validate amount is available in pool
const post = borrowCalc(
amountKWR.USDC,
amount,
poolSeat.getAmountAllocated('USDC', USDC),
encumberedBalance,
poolStats,
);

// COMMIT POINT
try {
zcf.atomicRearrange(harden([[poolSeat, toSeat, amountKWR]]));
} catch (cause) {
const reason = Error('🚨 cannot commit borrow', { cause });
console.error(reason.message, cause);
zcf.shutdownWithFailure(reason);
}
// UNTIL #10684: ability to terminate an incarnation w/o terminating the contract
zcf.atomicRearrange(harden([[poolSeat, toSeat, { USDC: amount }]]));

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 {Amount<'nat'>} amount
*/
returnToPool(borrowSeat, amount) {
const { zcfSeat: repaySeat } = zcf.makeEmptySeatKit();
const returnAmounts = harden({
Principal: amount,
PoolFee: makeEmpty(USDC),
ContractFee: makeEmpty(USDC),
});
const borrowSeatAllocation = borrowSeat.getCurrentAllocation();
isGTE(borrowSeatAllocation.USDC, amount) ||
Fail`⚠️ borrowSeatAllocation ${q(borrowSeatAllocation)} less than amountKWR ${q(amount)}`;
// arrange payments in a format repay is expecting
zcf.atomicRearrange(
harden([[borrowSeat, repaySeat, { USDC: amount }, returnAmounts]]),
);
return this.facets.repayer.repay(repaySeat, returnAmounts);
},
},
repayer: {
/**
Expand Down Expand Up @@ -208,23 +222,18 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
const { ContractFee, ...rest } = amounts;

// COMMIT POINT
try {
zcf.atomicRearrange(
harden([
[
fromSeat,
poolSeat,
rest,
{ USDC: add(amounts.PoolFee, amounts.Principal) },
],
[fromSeat, feeSeat, { ContractFee }, { USDC: ContractFee }],
]),
);
} catch (cause) {
const reason = Error('🚨 cannot commit repay', { cause });
console.error(reason.message, cause);
zcf.shutdownWithFailure(reason);
}
// UNTIL #10684: ability to terminate an incarnation w/o terminating the contract
zcf.atomicRearrange(
harden([
[
fromSeat,
poolSeat,
rest,
{ USDC: add(amounts.PoolFee, amounts.Principal) },
],
[fromSeat, feeSeat, { ContractFee }, { USDC: ContractFee }],
]),
);

Object.assign(this.state, post);
this.facets.external.publishPoolMetrics();
Expand Down Expand Up @@ -259,9 +268,8 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
const post = depositCalc(shareWorth, proposal);

// COMMIT POINT

const mint = shareMint.mintGains(post.payouts);
try {
const mint = shareMint.mintGains(post.payouts);
this.state.shareWorth = post.shareWorth;
zcf.atomicRearrange(
harden([
Expand All @@ -271,12 +279,12 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
[mint, lp, post.payouts],
]),
);
} catch (cause) {
// UNTIL #10684: ability to terminate an incarnation w/o terminating the contract
throw new Error('🚨 cannot commit deposit', { cause });
} finally {
lp.exit();
mint.exit();
} catch (cause) {
const reason = Error('🚨 cannot commit deposit', { cause });
console.error(reason.message, cause);
zcf.shutdownWithFailure(reason);
}
external.publishPoolMetrics();
},
Expand All @@ -296,7 +304,6 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
const post = withdrawCalc(shareWorth, proposal);

// COMMIT POINT

try {
this.state.shareWorth = post.shareWorth;
zcf.atomicRearrange(
Expand All @@ -308,12 +315,12 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
]),
);
shareMint.burnLosses(proposal.give, burn);
} catch (cause) {
// UNTIL #10684: ability to terminate an incarnation w/o terminating the contract
throw new Error('🚨 cannot commit withdraw', { cause });
} finally {
lp.exit();
burn.exit();
} catch (cause) {
const reason = Error('🚨 cannot commit withdraw', { cause });
console.error(reason.message, cause);
zcf.shutdownWithFailure(reason);
}
external.publishPoolMetrics();
},
Expand Down
10 changes: 5 additions & 5 deletions packages/fast-usdc/src/exos/settler.js
Original file line number Diff line number Diff line change
Expand Up @@ -237,13 +237,13 @@ export const prepareSettler = (
const split = calculateSplit(received);
log('disbursing', split);

// TODO: what if this throws?
// arguably, it cannot. Even if deposits
// and notifications get out of order,
// we don't ever withdraw more than has been deposited.
// If this throws, which arguably can't occur since we don't ever
// withdraw more than has been deposited (as denoted by
// `FungibleTokenPacketData`), funds will remain in the
// `settlementAccount`. A remediation can occur in a future upgrade.
await vowTools.when(
withdrawToSeat(
// @ts-expect-error Vow vs. Promise stuff. TODO: is this OK???
// @ts-expect-error LocalAccountMethods vs OrchestrationAccount
settlementAccount,
settlingSeat,
harden({ In: received }),
Expand Down
Loading
Loading