Skip to content

Commit

Permalink
feat(fast-usdc): support risk assessment arg
Browse files Browse the repository at this point in the history
  • Loading branch information
samsiegart committed Dec 20, 2024
1 parent 7b7ceb9 commit a265577
Show file tree
Hide file tree
Showing 17 changed files with 387 additions and 68 deletions.
8 changes: 5 additions & 3 deletions packages/fast-usdc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,14 @@ stateDiagram-v2

```mermaid
stateDiagram-v2
Observed --> Advancing
Observed --> Forwarding:Minted
Observed --> AdvanceSkipped : Risks identified
Observed --> Advancing : No risks, can advance
Observed --> Forwarding : No risks, Mint deposited before advance
Forwarding --> Forwarded
Advancing --> Advanced
Advanced --> Disbursed
AdvanceFailed --> Forwarding
AdvanceSkipped --> Forwarding : Mint deposited
AdvanceFailed --> Forwarding : Mint deposited
Advancing --> AdvanceFailed
Forwarding --> ForwardFailed
```
4 changes: 4 additions & 0 deletions packages/fast-usdc/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export const TxStatus = /** @type {const} */ ({
Advanced: 'ADVANCED',
/** IBC transfer failed (timed out) */
AdvanceFailed: 'ADVANCE_FAILED',
/** Advance skipped and waiting for forward */
AdvanceSkipped: 'ADVANCE_SKIPPED',
/** settlement for matching advance received and funds disbursed */
Disbursed: 'DISBURSED',
/** fallback: do not collect fees */
Expand Down Expand Up @@ -42,5 +44,7 @@ export const PendingTxStatus = /** @type {const} */ ({
AdvanceFailed: 'ADVANCE_FAILED',
/** IBC transfer is complete */
Advanced: 'ADVANCED',
/** Advance skipped and waiting for forward */
AdvanceSkipped: 'ADVANCE_SKIPPED',
});
harden(PendingTxStatus);
16 changes: 11 additions & 5 deletions packages/fast-usdc/src/exos/advancer.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import { E } from '@endo/far';
import { M, mustMatch } from '@endo/patterns';
import { Fail, q } from '@endo/errors';
import {
CctpTxEvidenceShape,
AddressHookShape,
EvmHashShape,
EvidenceWithRiskShape,
} from '../type-guards.js';
import { makeFeeTools } from '../utils/fees.js';

Expand All @@ -22,7 +22,7 @@ import { makeFeeTools } from '../utils/fees.js';
* @import {ZoeTools} from '@agoric/orchestration/src/utils/zoe-tools.js';
* @import {VowTools} from '@agoric/vow';
* @import {Zone} from '@agoric/zone';
* @import {CctpTxEvidence, AddressHook, EvmHash, FeeConfig, LogFn, NobleAddress} from '../types.js';
* @import {CctpTxEvidence, AddressHook, EvmHash, FeeConfig, LogFn, NobleAddress, EvidenceWithRisk} from '../types.js';
* @import {StatusManager} from './status-manager.js';
* @import {LiquidityPoolKit} from './liquidity-pool.js';
*/
Expand Down Expand Up @@ -55,7 +55,7 @@ const AdvancerVowCtxShape = M.splitRecord(
/** type guards internal to the AdvancerKit */
const AdvancerKitI = harden({
advancer: M.interface('AdvancerI', {
handleTransactionEvent: M.callWhen(CctpTxEvidenceShape).returns(),
handleTransactionEvent: M.callWhen(EvidenceWithRiskShape).returns(),
setIntermediateRecipient: M.call(ChainAddressShape).returns(),
}),
depositHandler: M.interface('DepositHandlerI', {
Expand Down Expand Up @@ -137,16 +137,22 @@ export const prepareAdvancerKit = (
* `StatusManager` - so we don't need to concern ourselves with
* preserving the vow chain for callers.
*
* @param {CctpTxEvidence} evidence
* @param {EvidenceWithRisk} evidenceWithRisk
*/
async handleTransactionEvent(evidence) {
async handleTransactionEvent({ evidence, risk }) {
await null;
try {
if (statusManager.hasBeenObserved(evidence)) {
log('txHash already seen:', evidence.txHash);
return;
}

if (risk.risksIdentified?.length) {
log('risks identified, skipping advance');
statusManager.skipAdvance(evidence, risk.risksIdentified);
return;
}

const { borrowerFacet, poolAccount, settlementAddress } =
this.state;
const { recipientAddress } = evidence.aux;
Expand Down
24 changes: 15 additions & 9 deletions packages/fast-usdc/src/exos/operator-kit.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { makeTracer } from '@agoric/internal';
import { Fail } from '@endo/errors';
import { M } from '@endo/patterns';
import { CctpTxEvidenceShape } from '../type-guards.js';
import { CctpTxEvidenceShape, RiskAssessmentShape } from '../type-guards.js';

const trace = makeTracer('TxOperator');

/**
* @import {Zone} from '@agoric/zone';
* @import {CctpTxEvidence} from '../types.js';
* @import {CctpTxEvidence, RiskAssessment} from '../types.js';
*/

/**
* @typedef {object} OperatorPowers
* @property {(evidence: CctpTxEvidence, operatorId: string) => void} attest
* @property {(evidence: CctpTxEvidence, riskAssessment: RiskAssessment, operatorId: string) => void} attest
*/

/**
Expand All @@ -31,11 +31,15 @@ const OperatorKitI = {
}),

invitationMakers: M.interface('InvitationMakers', {
SubmitEvidence: M.call(CctpTxEvidenceShape).returns(M.promise()),
SubmitEvidence: M.call(CctpTxEvidenceShape)
.optional(RiskAssessmentShape)
.returns(M.promise()),
}),

operator: M.interface('Operator', {
submitEvidence: M.call(CctpTxEvidenceShape).returns(),
submitEvidence: M.call(CctpTxEvidenceShape)
.optional(RiskAssessmentShape)
.returns(),
getStatus: M.call().returns(M.record()),
}),
};
Expand Down Expand Up @@ -81,13 +85,14 @@ export const prepareOperatorKit = (zone, staticPowers) =>
* fluxAggregator contract used for price oracles.
*
* @param {CctpTxEvidence} evidence
* @param {RiskAssessment} [riskAssessment]
* @returns {Promise<Invitation>}
*/
async SubmitEvidence(evidence) {
async SubmitEvidence(evidence, riskAssessment) {
const { operator } = this.facets;
// TODO(bootstrap integration): cause this call to throw and confirm that it
// shows up in the the smart-wallet UpdateRecord `error` property
operator.submitEvidence(evidence);
operator.submitEvidence(evidence, riskAssessment);
return staticPowers.makeInertInvitation(
'evidence was pushed in the invitation maker call',
);
Expand All @@ -98,12 +103,13 @@ export const prepareOperatorKit = (zone, staticPowers) =>
* submit evidence from this operator
*
* @param {CctpTxEvidence} evidence
* @param {RiskAssessment} [riskAssessment]
* @returns {void}
*/
submitEvidence(evidence) {
submitEvidence(evidence, riskAssessment = {}) {
const { state } = this;
!state.disabled || Fail`submitEvidence for disabled operator`;
state.powers.attest(evidence, state.operatorId);
state.powers.attest(evidence, riskAssessment, state.operatorId);
},
/** @returns {OperatorStatus} */
getStatus() {
Expand Down
1 change: 1 addition & 0 deletions packages/fast-usdc/src/exos/settler.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ export const prepareSettler = (
return;

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

Expand Down
33 changes: 29 additions & 4 deletions packages/fast-usdc/src/exos/status-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
/**
* @import {MapStore, SetStore} from '@agoric/store';
* @import {Zone} from '@agoric/zone';
* @import {CctpTxEvidence, NobleAddress, PendingTx, EvmHash, LogFn, TransactionRecord} from '../types.js';
* @import {CctpTxEvidence, NobleAddress, PendingTx, EvmHash, LogFn, TransactionRecord, EvidenceWithRisk, RiskAssessment} from '../types.js';
*/

/**
Expand Down Expand Up @@ -146,8 +146,9 @@ export const prepareStatusManager = (
*
* @param {CctpTxEvidence} evidence
* @param {PendingTxStatus} status
* @param {string[]} [risksIdentified]
*/
const initPendingTx = (evidence, status) => {
const initPendingTx = (evidence, status, risksIdentified) => {
const { txHash } = evidence;
if (seenTxs.has(txHash)) {
throw makeError(`Transaction already seen: ${q(txHash)}`);
Expand All @@ -160,7 +161,9 @@ export const prepareStatusManager = (
harden({ ...evidence, status }),
);
publishEvidence(txHash, evidence);
if (status !== PendingTxStatus.Observed) {
if (status === PendingTxStatus.AdvanceSkipped) {
void publishTxnRecord(txHash, harden({ status, risksIdentified }));
} else if (status !== PendingTxStatus.Observed) {
// publishEvidence publishes Observed
void publishTxnRecord(txHash, harden({ status }));
}
Expand Down Expand Up @@ -194,6 +197,9 @@ export const prepareStatusManager = (
// TODO: naming scheme for transition events
advance: M.call(CctpTxEvidenceShape).returns(M.undefined()),
advanceOutcome: M.call(M.string(), M.nat(), M.boolean()).returns(),
skipAdvance: M.call(CctpTxEvidenceShape, M.arrayOf(M.string())).returns(
M.undefined(),
),
observe: M.call(CctpTxEvidenceShape).returns(M.undefined()),
hasBeenObserved: M.call(CctpTxEvidenceShape).returns(M.boolean()),
deleteCompletedTxs: M.call().returns(M.undefined()),
Expand All @@ -203,6 +209,7 @@ export const prepareStatusManager = (
txHash: EvmHashShape,
status: M.or(
PendingTxStatus.Advanced,
PendingTxStatus.AdvanceSkipped,
PendingTxStatus.AdvanceFailed,
PendingTxStatus.Observed,
),
Expand All @@ -224,14 +231,32 @@ export const prepareStatusManager = (
/**
* Add a new transaction with ADVANCING status
*
* NB: this acts like observe() but skips recording the OBSERVED state
* NB: this acts like observe() but subsequently records an ADVANCING
* state
*
* @param {CctpTxEvidence} evidence
*/
advance(evidence) {
initPendingTx(evidence, PendingTxStatus.Advancing);
},

/**
* Add a new transaction with ADVANCE_SKIPPED status
*
* NB: this acts like observe() but subsequently records an
* ADVANCE_SKIPPED state along with risks identified
*
* @param {CctpTxEvidence} evidence
* @param {string[]} risksIdentified
*/
skipAdvance(evidence, risksIdentified) {
initPendingTx(
evidence,
PendingTxStatus.AdvanceSkipped,
risksIdentified,
);
},

/**
* Record result of ADVANCING
*
Expand Down
65 changes: 54 additions & 11 deletions packages/fast-usdc/src/exos/transaction-feed.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { makeTracer } from '@agoric/internal';
import { prepareDurablePublishKit } from '@agoric/notifier';
import { keyEQ, M } from '@endo/patterns';
import { Fail } from '@endo/errors';
import { CctpTxEvidenceShape } from '../type-guards.js';
import { CctpTxEvidenceShape, RiskAssessmentShape } from '../type-guards.js';
import { defineInertInvitation } from '../utils/zoe.js';
import { prepareOperatorKit } from './operator-kit.js';

/**
* @import {Zone} from '@agoric/zone';
* @import {OperatorKit} from './operator-kit.js';
* @import {CctpTxEvidence} from '../types.js';
* @import {CctpTxEvidence, EvidenceWithRisk, RiskAssessment} from '../types.js';
*/

const trace = makeTracer('TxFeed', true);
Expand All @@ -19,7 +19,11 @@ export const INVITATION_MAKERS_DESC = 'oracle operator invitation';

const TransactionFeedKitI = harden({
operatorPowers: M.interface('Transaction Feed Admin', {
attest: M.call(CctpTxEvidenceShape, M.string()).returns(),
attest: M.call(
CctpTxEvidenceShape,
RiskAssessmentShape,
M.string(),
).returns(),
}),
creator: M.interface('Transaction Feed Creator', {
// TODO narrow the return shape to OperatorKit
Expand All @@ -42,7 +46,7 @@ export const prepareTransactionFeedKit = (zone, zcf) => {
kinds,
'Transaction Feed',
);
/** @type {PublishKit<CctpTxEvidence>} */
/** @type {PublishKit<EvidenceWithRisk>} */
const { publisher, subscriber } = makeDurablePublishKit();

const makeInertInvitation = defineInertInvitation(zcf, 'submitting evidence');
Expand All @@ -63,7 +67,11 @@ export const prepareTransactionFeedKit = (zone, zcf) => {
const pending = zone.mapStore('pending', {
durable: true,
});
return { operators, pending };
/** @type {MapStore<string, MapStore<string, RiskAssessment>>} */
const risks = zone.mapStore('risks', {
durable: true,
});
return { operators, pending, risks };
},
{
creator: {
Expand All @@ -90,7 +98,7 @@ export const prepareTransactionFeedKit = (zone, zcf) => {
},
/** @param {string} operatorId */
initOperator(operatorId) {
const { operators, pending } = this.state;
const { operators, pending, risks } = this.state;
trace('initOperator', operatorId);

const operatorKit = makeOperatorKit(
Expand All @@ -102,6 +110,7 @@ export const prepareTransactionFeedKit = (zone, zcf) => {
operatorId,
zone.detached().mapStore('pending evidence'),
);
risks.init(operatorId, zone.detached().mapStore('risk assessments'));

return operatorKit;
},
Expand All @@ -122,10 +131,11 @@ export const prepareTransactionFeedKit = (zone, zcf) => {
* NB: the operatorKit is responsible for
*
* @param {CctpTxEvidence} evidence
* @param {RiskAssessment} riskAssessment
* @param {string} operatorId
*/
attest(evidence, operatorId) {
const { operators, pending } = this.state;
attest(evidence, riskAssessment, operatorId) {
const { operators, pending, risks } = this.state;
trace('submitEvidence', operatorId, evidence);

// TODO https://github.com/Agoric/agoric-sdk/pull/10720
Expand All @@ -144,6 +154,19 @@ export const prepareTransactionFeedKit = (zone, zcf) => {
}
}

// accept the risk assessment
{
const riskStore = risks.get(operatorId);
if (riskStore.has(txHash)) {
trace(
`operator ${operatorId} already reported risk for ${txHash}, updating...`,
);
riskStore.set(txHash, riskAssessment);
} else {
riskStore.init(txHash, riskAssessment);
}
}

// check agreement
const found = [...pending.values()].filter(store =>
store.has(txHash),
Expand Down Expand Up @@ -183,12 +206,32 @@ export const prepareTransactionFeedKit = (zone, zcf) => {
lastEvidence = next;
}

// sufficient agreement, so remove from pending and publish
// take the union of risks identified from all operators
/** @type {Set<string>} */
const setUnionRisks = new Set();
const riskStores = [...risks.values()].filter(store =>
store.has(txHash),
);
for (const store of riskStores) {
const next = store.get(txHash);
for (const risk of next.risksIdentified ?? []) {
setUnionRisks.add(risk);
}
}
const unionRisks = [...setUnionRisks.values()].sort();

// sufficient agreement, so remove from pending risks, then publish
for (const store of found) {
store.delete(txHash);
}
trace('publishing evidence', evidence);
publisher.publish(evidence);
for (const store of riskStores) {
store.delete(txHash);
}
trace('publishing evidence', evidence, unionRisks);
publisher.publish({
evidence,
risk: { risksIdentified: unionRisks },
});
},
},
public: {
Expand Down
Loading

0 comments on commit a265577

Please sign in to comment.