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

feat(fast-usdc): support risk assessment arg #10753

Merged
merged 1 commit into from
Dec 20, 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
50 changes: 50 additions & 0 deletions packages/boot/test/fast-usdc/fast-usdc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,56 @@ test.serial('makes usdc advance', async t => {
await documentStorageSchema(t, storage, doc);
});

test.serial('skips usdc advance when risks identified', async t => {
const { walletFactoryDriver: wd, storage } = t.context;
const oracles = await Promise.all([
wd.provideSmartWallet('agoric19uscwxdac6cf6z7d5e26e0jm0lgwstc47cpll8'),
wd.provideSmartWallet('agoric1krunjcqfrf7la48zrvdfeeqtls5r00ep68mzkr'),
wd.provideSmartWallet('agoric1n4fcxsnkxe4gj6e24naec99hzmc4pjfdccy5nj'),
]);

const EUD = 'dydx1riskyeud';
const lastNodeValue = storage.getValues('published.fastUsdc').at(-1);
const { settlementAccount } = JSON.parse(NonNullish(lastNodeValue));
const evidence = MockCctpTxEvidences.AGORIC_PLUS_DYDX(
// mock with the read settlementAccount address
encodeAddressHook(settlementAccount, { EUD }),
);

await Promise.all(
oracles.map(wallet =>
wallet.sendOffer({
id: 'submit-mock-evidence-dydx-risky',
invitationSpec: {
source: 'continuing',
previousOffer: 'claim-oracle-invitation',
invitationMakerName: 'SubmitEvidence',
invitationArgs: [evidence, { risksIdentified: ['TOO_LARGE_AMOUNT'] }],
},
proposal: {},
}),
),
);
await eventLoopIteration();

t.deepEqual(
storage
.getValues(`published.fastUsdc.txns.${evidence.txHash}`)
.map(defaultSerializer.parse),
[
{ evidence, status: 'OBSERVED' }, // observation includes evidence observed
{ status: 'ADVANCE_SKIPPED', risksIdentified: ['TOO_LARGE_AMOUNT'] },
],
);

const doc = {
node: `fastUsdc.txns`,
owner: `the Ethereum transactions upon which Fast USDC is acting`,
showValue: defaultSerializer.parse,
};
await documentStorageSchema(t, storage, doc);
});

test.serial('restart contract', async t => {
const { EV } = t.context.runUtils;
await null;
Expand Down
25 changes: 25 additions & 0 deletions packages/boot/test/fast-usdc/snapshots/fast-usdc.test.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,28 @@ Generated by [AVA](https://avajs.dev).
},
],
]

## skips usdc advance when risks identified

> Under "published", the "fastUsdc.txns" node is delegated to the Ethereum transactions upon which Fast USDC is acting.
> The example below illustrates the schema of the data published there.
>
> See also board marshalling conventions (_to appear_).

[
[
'published.fastUsdc.txns.0xc81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761702',
{
status: 'ADVANCING',
},
],
[
'published.fastUsdc.txns.0xd81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761799',
{
risksIdentified: [
'TOO_LARGE_AMOUNT',
],
status: 'ADVANCE_SKIPPED',
},
],
]
Binary file modified packages/boot/test/fast-usdc/snapshots/fast-usdc.test.ts.snap
Binary file not shown.
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
Loading
Loading