Skip to content

Commit c236c55

Browse files
authored
feat: fusdc status manager states (#10406)
closes: #10389 ## Description - `StatusManager` with `OBSERVED`, `ADVANCED` states tracked in local `MapStore` - `StatusManager` state updates via `Settler` and `Advancer` - `CctpTxEvidence` type, typeGuard, and fixtures ### Security Considerations See FastUSDC thread model ### Scaling Considerations - includes a `seenTxs` SetStore that will keep track of every tx observed by fusdc contract to assert uniqueness ### Documentation Considerations Includes state diagram in exos/README.md and the usual jsdoc ### Testing Considerations Includes unit tests of exos with a mocked LCA. ### Upgrade Considerations None, unreleased
2 parents 3b799b8 + f3d1e36 commit c236c55

27 files changed

+1355
-27
lines changed

packages/builders/test/snapshots/orchestration-imports.test.js.md

+1
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,7 @@ Generated by [AVA](https://avajs.dev).
524524
},
525525
},
526526
},
527+
denomHash: Function denomHash {},
527528
prepareChainHubAdmin: Function prepareChainHubAdmin {},
528529
prepareCosmosInterchainService: Function prepareCosmosInterchainService {},
529530
withOrchestration: Function withOrchestration {},
Binary file not shown.

packages/fast-usdc/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"@agoric/orchestration": "^0.1.0",
3737
"@agoric/store": "^0.9.2",
3838
"@agoric/vow": "^0.1.0",
39+
"@endo/base64": "^1.0.8",
3940
"@endo/common": "^1.2.7",
4041
"@endo/errors": "^1.2.7",
4142
"@endo/eventual-send": "^1.2.7",

packages/fast-usdc/src/constants.js

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* Status values for FastUSDC.
3+
*
4+
* @enum {(typeof TxStatus)[keyof typeof TxStatus]}
5+
*/
6+
export const TxStatus = /** @type {const} */ ({
7+
/** tx was observed but not advanced */
8+
Observed: 'OBSERVED',
9+
/** IBC transfer is initiated */
10+
Advanced: 'ADVANCED',
11+
/** settlement for matching advance received and funds dispersed */
12+
Settled: 'SETTLED',
13+
});
14+
harden(TxStatus);
15+
16+
/**
17+
* Status values for the StatusManager.
18+
*
19+
* @enum {(typeof PendingTxStatus)[keyof typeof PendingTxStatus]}
20+
*/
21+
export const PendingTxStatus = /** @type {const} */ ({
22+
/** tx was observed but not advanced */
23+
Observed: 'OBSERVED',
24+
/** IBC transfer is initiated */
25+
Advanced: 'ADVANCED',
26+
});
27+
harden(PendingTxStatus);

packages/fast-usdc/src/exos/README.md

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
## **StatusManager** state diagram, showing different transitions
2+
3+
4+
### Contract state diagram
5+
6+
*Transactions are qualified by the OCW and EventFeed before arriving to the Advancer.*
7+
8+
```mermaid
9+
stateDiagram-v2
10+
[*] --> Advanced: Advancer .advance()
11+
Advanced --> Settled: Settler .settle() after fees
12+
[*] --> Observed: Advancer .observed()
13+
Observed --> Settled: Settler .settle() sans fees
14+
Settled --> [*]
15+
```
16+
17+
### Complete state diagram (starting from OCW)
18+
19+
```mermaid
20+
stateDiagram-v2
21+
Observed --> Qualified
22+
Observed --> Unqualified
23+
Qualified --> Advanced
24+
Advanced --> Settled
25+
Qualified --> Settled
26+
```
+112-6
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,125 @@
1+
import { assertAllDefined } from '@agoric/internal';
2+
import { ChainAddressShape } from '@agoric/orchestration';
3+
import { VowShape } from '@agoric/vow';
4+
import { makeError, q } from '@endo/errors';
5+
import { E } from '@endo/far';
6+
import { M } from '@endo/patterns';
7+
import { CctpTxEvidenceShape } from '../typeGuards.js';
8+
import { addressTools } from '../utils/address.js';
9+
110
/**
11+
* @import {HostInterface} from '@agoric/async-flow';
12+
* @import {ChainAddress, ChainHub, Denom, DenomAmount, OrchestrationAccount} from '@agoric/orchestration';
13+
* @import {VowTools} from '@agoric/vow';
214
* @import {Zone} from '@agoric/zone';
3-
* @import {TransactionFeed} from './transaction-feed.js';
15+
* @import {CctpTxEvidence, LogFn} from '../types.js';
416
* @import {StatusManager} from './status-manager.js';
17+
* @import {TransactionFeed} from './transaction-feed.js';
518
*/
619

7-
import { assertAllDefined } from '@agoric/internal';
8-
920
/**
1021
* @param {Zone} zone
1122
* @param {object} caps
23+
* @param {ChainHub} caps.chainHub
1224
* @param {TransactionFeed} caps.feed
25+
* @param {LogFn} caps.log
1326
* @param {StatusManager} caps.statusManager
27+
* @param {VowTools} caps.vowTools
1428
*/
15-
export const prepareAdvancer = (zone, { feed, statusManager }) => {
16-
assertAllDefined({ feed, statusManager });
17-
return zone.exo('Fast USDC Advancer', undefined, {});
29+
export const prepareAdvancer = (
30+
zone,
31+
{ chainHub, feed, log, statusManager, vowTools: { watch } },
32+
) => {
33+
assertAllDefined({ feed, statusManager, watch });
34+
35+
const transferHandler = zone.exo(
36+
'Fast USDC Advance Transfer Handler',
37+
M.interface('TransferHandlerI', {
38+
// TODO confirm undefined, and not bigint (sequence)
39+
onFulfilled: M.call(M.undefined(), {
40+
amount: M.bigint(),
41+
destination: ChainAddressShape,
42+
}).returns(M.undefined()),
43+
onRejected: M.call(M.error(), {
44+
amount: M.bigint(),
45+
destination: ChainAddressShape,
46+
}).returns(M.undefined()),
47+
}),
48+
{
49+
/**
50+
* @param {undefined} result TODO confirm this is not a bigint (sequence)
51+
* @param {{ destination: ChainAddress; amount: bigint; }} ctx
52+
*/
53+
onFulfilled(result, { destination, amount }) {
54+
log(
55+
'Advance transfer fulfilled',
56+
q({ amount, destination, result }).toString(),
57+
);
58+
},
59+
onRejected(error) {
60+
// XXX retry logic?
61+
// What do we do if we fail, should we keep a Status?
62+
log('Advance transfer rejected', q(error).toString());
63+
},
64+
},
65+
);
66+
67+
return zone.exoClass(
68+
'Fast USDC Advancer',
69+
M.interface('AdvancerI', {
70+
handleTransactionEvent: M.call(CctpTxEvidenceShape).returns(VowShape),
71+
}),
72+
/**
73+
* @param {{
74+
* localDenom: Denom;
75+
* poolAccount: HostInterface<OrchestrationAccount<{ chainId: 'agoric' }>>;
76+
* }} config
77+
*/
78+
config => harden(config),
79+
{
80+
/** @param {CctpTxEvidence} evidence */
81+
handleTransactionEvent(evidence) {
82+
// TODO EventFeed will perform input validation checks.
83+
const { recipientAddress } = evidence.aux;
84+
const { EUD } = addressTools.getQueryParams(recipientAddress).params;
85+
if (!EUD) {
86+
statusManager.observe(evidence);
87+
throw makeError(
88+
`recipientAddress does not contain EUD param: ${q(recipientAddress)}`,
89+
);
90+
}
91+
92+
// TODO #10391 this can throw, and should make a status update in the catch
93+
const destination = chainHub.makeChainAddress(EUD);
94+
95+
/** @type {DenomAmount} */
96+
const requestedAmount = harden({
97+
denom: this.state.localDenom,
98+
value: BigInt(evidence.tx.amount),
99+
});
100+
101+
// TODO #10391 ensure there's enough funds in poolAccount
102+
103+
const transferV = E(this.state.poolAccount).transfer(
104+
destination,
105+
requestedAmount,
106+
);
107+
108+
// mark as Advanced since `transferV` initiates the advance
109+
statusManager.advance(evidence);
110+
111+
return watch(transferV, transferHandler, {
112+
destination,
113+
amount: requestedAmount.value,
114+
});
115+
},
116+
},
117+
{
118+
stateShape: harden({
119+
localDenom: M.string(),
120+
poolAccount: M.remotable(),
121+
}),
122+
},
123+
);
18124
};
19125
harden(prepareAdvancer);
+84-3
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,98 @@
1+
import { assertAllDefined } from '@agoric/internal';
2+
import { atob } from '@endo/base64';
3+
import { makeError, q } from '@endo/errors';
4+
import { M } from '@endo/patterns';
5+
6+
import { addressTools } from '../utils/address.js';
7+
18
/**
9+
* @import {FungibleTokenPacketData} from '@agoric/cosmic-proto/ibc/applications/transfer/v2/packet.js';
10+
* @import {Denom} from '@agoric/orchestration';
11+
* @import {IBCChannelID, VTransferIBCEvent} from '@agoric/vats';
212
* @import {Zone} from '@agoric/zone';
13+
* @import {NobleAddress} from '../types.js';
314
* @import {StatusManager} from './status-manager.js';
415
*/
516

6-
import { assertAllDefined } from '@agoric/internal';
7-
817
/**
918
* @param {Zone} zone
1019
* @param {object} caps
1120
* @param {StatusManager} caps.statusManager
1221
*/
1322
export const prepareSettler = (zone, { statusManager }) => {
1423
assertAllDefined({ statusManager });
15-
return zone.exo('Fast USDC Settler', undefined, {});
24+
return zone.exoClass(
25+
'Fast USDC Settler',
26+
M.interface('SettlerI', {
27+
receiveUpcall: M.call(M.record()).returns(M.promise()),
28+
}),
29+
/**
30+
*
31+
* @param {{
32+
* sourceChannel: IBCChannelID;
33+
* remoteDenom: Denom
34+
* }} config
35+
*/
36+
config => harden(config),
37+
{
38+
/** @param {VTransferIBCEvent} event */
39+
async receiveUpcall(event) {
40+
if (event.packet.source_channel !== this.state.sourceChannel) {
41+
// TODO #10390 log all early returns
42+
// only interested in packets from the issuing chain
43+
return;
44+
}
45+
const tx = /** @type {FungibleTokenPacketData} */ (
46+
JSON.parse(atob(event.packet.data))
47+
);
48+
if (tx.denom !== this.state.remoteDenom) {
49+
// only interested in uusdc
50+
return;
51+
}
52+
53+
if (!addressTools.hasQueryParams(tx.receiver)) {
54+
// only interested in receivers with query params
55+
return;
56+
}
57+
58+
const { params } = addressTools.getQueryParams(tx.receiver);
59+
// TODO - what's the schema address parameter schema for FUSDC?
60+
if (!params?.EUD) {
61+
// only interested in receivers with EUD parameter
62+
return;
63+
}
64+
65+
// TODO discern between SETTLED and OBSERVED; each has different fees/destinations
66+
const hasPendingSettlement = statusManager.hasPendingSettlement(
67+
// given the sourceChannel check, we can be certain of this cast
68+
/** @type {NobleAddress} */ (tx.sender),
69+
BigInt(tx.amount),
70+
);
71+
if (!hasPendingSettlement) {
72+
// TODO FAILURE PATH -> put money in recovery account or .transfer to receiver
73+
// TODO should we have an ORPHANED TxStatus for this?
74+
throw makeError(
75+
`🚨 No pending settlement found for ${q(tx.sender)} ${q(tx.amount)}`,
76+
);
77+
}
78+
79+
// TODO disperse funds
80+
// ~1. fee to contractFeeAccount
81+
// ~2. remainder in poolAccount
82+
83+
// update status manager, marking tx `SETTLED`
84+
statusManager.settle(
85+
/** @type {NobleAddress} */ (tx.sender),
86+
BigInt(tx.amount),
87+
);
88+
},
89+
},
90+
{
91+
stateShape: harden({
92+
sourceChannel: M.string(),
93+
remoteDenom: M.string(),
94+
}),
95+
},
96+
);
1697
};
1798
harden(prepareSettler);

0 commit comments

Comments
 (0)