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): settle ForwardFailed, minted while Advancing, and minted early txs #10729

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
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
18 changes: 12 additions & 6 deletions packages/fast-usdc/src/exos/advancer.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ const AdvancerKitI = harden({
onRejected: M.call(M.error(), AdvancerVowCtxShape).returns(),
}),
transferHandler: M.interface('TransferHandlerI', {
// TODO confirm undefined, and not bigint (sequence)
onFulfilled: M.call(M.undefined(), AdvancerVowCtxShape).returns(
M.undefined(),
),
Expand Down Expand Up @@ -152,9 +151,7 @@ export const prepareAdvancerKit = (
statusManager.skipAdvance(evidence, risk.risksIdentified);
return;
}

const { borrowerFacet, poolAccount, settlementAddress } =
this.state;
const { settlementAddress } = this.state;
const { recipientAddress } = evidence.aux;
const decoded = decodeAddressHook(recipientAddress);
mustMatch(decoded, AddressHookShape);
Expand All @@ -167,6 +164,14 @@ export const prepareAdvancerKit = (
const destination = chainHub.makeChainAddress(EUD);

const fullAmount = toAmount(evidence.tx.amount);
const { borrowerFacet, notifyFacet, poolAccount } = this.state;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sometime before this PR, but I'm surprised to see Facet in the name of the object. the consumer needn't be concerned with the structure in which the object resides.

I've added this to revisit in #10432

// do not advance if we've already received a mint/settlement
const mintedEarly = notifyFacet.checkMintedEarly(
evidence,
destination,
);
if (mintedEarly) return;

// throws if requested does not exceed fees
const advanceAmount = feeTools.calculateAdvance(fullAmount);

Expand Down Expand Up @@ -208,7 +213,7 @@ export const prepareAdvancerKit = (
*/
onFulfilled(result, ctx) {
const { poolAccount, intermediateRecipient } = this.state;
const { destination, advanceAmount, ...detail } = ctx;
const { destination, advanceAmount, tmpSeat: _, ...detail } = ctx;
const transferV = E(poolAccount).transfer(
destination,
{ denom: usdc.denom, value: advanceAmount.value },
Expand Down Expand Up @@ -273,7 +278,8 @@ export const prepareAdvancerKit = (
onRejected(error, ctx) {
const { notifyFacet } = this.state;
log('Advance transfer rejected', error);
notifyFacet.notifyAdvancingResult(ctx, false);
const { advanceAmount: _, ...restCtx } = ctx;
notifyFacet.notifyAdvancingResult(restCtx, false);
},
},
},
Expand Down
124 changes: 73 additions & 51 deletions packages/fast-usdc/src/exos/settler.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import { M } from '@endo/patterns';
import { decodeAddressHook } from '@agoric/cosmic-proto/address-hooks.js';
import { PendingTxStatus } from '../constants.js';
import { makeFeeTools } from '../utils/fees.js';
import { EvmHashShape } from '../type-guards.js';
import {
CctpTxEvidenceShape,
EvmHashShape,
makeNatAmountShape,
} from '../type-guards.js';

/**
* @import {FungibleTokenPacketData} from '@agoric/cosmic-proto/ibc/applications/transfer/v2/packet.js';
Expand All @@ -18,7 +22,7 @@ import { EvmHashShape } from '../type-guards.js';
* @import {Zone} from '@agoric/zone';
* @import {HostOf, HostInterface} from '@agoric/async-flow';
* @import {TargetRegistration} from '@agoric/vats/src/bridge-target.js';
* @import {NobleAddress, LiquidityPoolKit, FeeConfig, EvmHash, LogFn} from '../types.js';
* @import {NobleAddress, LiquidityPoolKit, FeeConfig, EvmHash, LogFn, CctpTxEvidence} from '../types.js';
* @import {StatusManager} from './status-manager.js';
*/

Expand All @@ -31,6 +35,15 @@ import { EvmHashShape } from '../type-guards.js';
const makeMintedEarlyKey = (addr, amount) =>
`pendingTx:${JSON.stringify([addr, String(amount)])}`;

/** @param {Brand<'nat'>} USDC */
export const makeAdvanceDetailsShape = USDC =>
harden({
destination: ChainAddressShape,
forwardingAddress: M.string(),
fullAmount: makeNatAmountShape(USDC),
txHash: EvmHashShape,
});

/**
* @param {Zone} zone
* @param {object} caps
Expand Down Expand Up @@ -69,24 +82,21 @@ export const prepareSettler = (
}),
notify: M.interface('SettlerNotifyI', {
notifyAdvancingResult: M.call(
M.record(), // XXX fill in details TODO
makeAdvanceDetailsShape(USDC),
turadg marked this conversation as resolved.
Show resolved Hide resolved
M.boolean(),
).returns(),
checkMintedEarly: M.call(
CctpTxEvidenceShape,
ChainAddressShape,
).returns(M.boolean()),
}),
self: M.interface('SettlerSelfI', {
disburse: M.call(EvmHashShape, M.string(), M.nat()).returns(
M.promise(),
),
forward: M.call(
M.opt(EvmHashShape),
M.string(),
M.nat(),
M.string(),
).returns(),
disburse: M.call(EvmHashShape, M.nat()).returns(M.promise()),
forward: M.call(EvmHashShape, M.nat(), M.string()).returns(),
}),
transferHandler: M.interface('SettlerTransferI', {
onFulfilled: M.call(M.any(), M.record()).returns(),
onRejected: M.call(M.any(), M.record()).returns(),
onFulfilled: M.call(M.undefined(), M.string()).returns(),
onRejected: M.call(M.error(), M.string()).returns(),
}),
},
/**
Expand Down Expand Up @@ -174,20 +184,24 @@ export const prepareSettler = (
log('dequeued', found, 'for', nfa, amount);
switch (found?.status) {
case PendingTxStatus.Advanced:
return self.disburse(found.txHash, nfa, amount);
return self.disburse(found.txHash, amount);

case PendingTxStatus.Advancing:
log('⚠️ tap: minted while advancing', nfa, amount);
this.state.mintedEarly.add(makeMintedEarlyKey(nfa, amount));
return;

case PendingTxStatus.Observed:
case PendingTxStatus.AdvanceSkipped:
case PendingTxStatus.AdvanceFailed:
return self.forward(found.txHash, nfa, amount, EUD);
return self.forward(found.txHash, amount, EUD);

case undefined:
default:
log('⚠️ tap: no status for ', nfa, amount);
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));
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realized while writing this PR desc:

The mintedEarly mapStore could grow large if an attacker spams the settlementAccount with uusdc

Worth adding a check here to see if the advance exceeds min fees? That should like help with 1uusdc spam.

}
},
},
Expand All @@ -210,16 +224,12 @@ export const prepareSettler = (
const key = makeMintedEarlyKey(forwardingAddress, fullValue);
if (mintedEarly.has(key)) {
mintedEarly.delete(key);
statusManager.advanceOutcomeForMintedEarly(txHash, success);
if (success) {
void this.facets.self.disburse(
txHash,
forwardingAddress,
fullValue,
);
void this.facets.self.disburse(txHash, fullValue);
} else {
void this.facets.self.forward(
txHash,
forwardingAddress,
fullValue,
destination.value,
);
Expand All @@ -228,14 +238,39 @@ export const prepareSettler = (
statusManager.advanceOutcome(forwardingAddress, fullValue, success);
}
},
/**
* @param {CctpTxEvidence} evidence
* @param {ChainAddress} destination
* @returns {boolean}
* @throws {Error} if minted early, so advancer doesn't advance
*/
checkMintedEarly(evidence, destination) {
const {
tx: { forwardingAddress, amount },
txHash,
} = evidence;
const key = makeMintedEarlyKey(forwardingAddress, amount);
const { mintedEarly } = this.state;
if (mintedEarly.has(key)) {
log(
'matched minted early key, initiating forward',
forwardingAddress,
amount,
);
mintedEarly.delete(key);
statusManager.advanceOutcomeForUnknownMint(evidence);
void this.facets.self.forward(txHash, amount, destination.value);
return true;
}
return false;
},
},
self: {
/**
* @param {EvmHash} txHash
* @param {NobleAddress} nfa
* @param {NatValue} fullValue
*/
async disburse(txHash, nfa, fullValue) {
async disburse(txHash, fullValue) {
const { repayer, settlementAccount } = this.state;
const received = AmountMath.make(USDC, fullValue);
const { zcfSeat: settlingSeat } = zcf.makeEmptySeatKit();
Expand All @@ -260,56 +295,43 @@ export const prepareSettler = (
);
repayer.repay(settlingSeat, split);

// update status manager, marking tx `SETTLED`
// update status manager, marking tx `DISBURSED`
statusManager.disbursed(txHash, split);
},
/**
* @param {EvmHash} txHash
* @param {NobleAddress} nfa
* @param {NatValue} fullValue
* @param {string} EUD
*/
forward(txHash, nfa, fullValue, EUD) {
forward(txHash, fullValue, EUD) {
const { settlementAccount, intermediateRecipient } = this.state;

const dest = chainHub.makeChainAddress(EUD);

// TODO? statusManager.forwarding(txHash, sender, amount);
const txfrV = E(settlementAccount).transfer(
dest,
AmountMath.make(USDC, fullValue),
{ forwardOpts: { intermediateRecipient } },
);
void vowTools.watch(txfrV, this.facets.transferHandler, {
txHash,
nfa,
fullValue,
});
void vowTools.watch(txfrV, this.facets.transferHandler, txHash);
},
},
transferHandler: {
/**
* @param {unknown} _result
* @param {SettlerTransferCtx} ctx
*
* @typedef {{
* txHash: EvmHash;
* nfa: NobleAddress;
* fullValue: NatValue;
* }} SettlerTransferCtx
* @param {EvmHash} txHash
*/
onFulfilled(_result, ctx) {
const { txHash, nfa, fullValue } = ctx;
statusManager.forwarded(txHash, nfa, fullValue);
onFulfilled(_result, txHash) {
// update status manager, marking tx `FORWARDED` without fee split
statusManager.forwarded(txHash, true);
},
/**
* @param {unknown} reason
* @param {SettlerTransferCtx} ctx
* @param {EvmHash} txHash
*/
onRejected(reason, ctx) {
log('⚠️ transfer rejected!', reason, ctx);
// const { txHash, nfa, amount } = ctx;
// TODO(#10510): statusManager.forwardFailed(txHash, nfa, amount);
onRejected(reason, txHash) {
log('⚠️ forward transfer rejected!', reason, txHash);
// update status manager, flagging a terminal state that needs to be
// manual intervention or a code update to remediate
statusManager.forwarded(txHash, false);
},
},
},
Expand Down
Loading
Loading