Skip to content

Commit

Permalink
feat: fusdc status manager states (#10406)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
mergify[bot] authored Nov 12, 2024
2 parents 3b799b8 + f3d1e36 commit c236c55
Show file tree
Hide file tree
Showing 27 changed files with 1,355 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,7 @@ Generated by [AVA](https://avajs.dev).
},
},
},
denomHash: Function denomHash {},
prepareChainHubAdmin: Function prepareChainHubAdmin {},
prepareCosmosInterchainService: Function prepareCosmosInterchainService {},
withOrchestration: Function withOrchestration {},
Expand Down
Binary file not shown.
1 change: 1 addition & 0 deletions packages/fast-usdc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@agoric/orchestration": "^0.1.0",
"@agoric/store": "^0.9.2",
"@agoric/vow": "^0.1.0",
"@endo/base64": "^1.0.8",
"@endo/common": "^1.2.7",
"@endo/errors": "^1.2.7",
"@endo/eventual-send": "^1.2.7",
Expand Down
27 changes: 27 additions & 0 deletions packages/fast-usdc/src/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Status values for FastUSDC.
*
* @enum {(typeof TxStatus)[keyof typeof TxStatus]}
*/
export const TxStatus = /** @type {const} */ ({
/** tx was observed but not advanced */
Observed: 'OBSERVED',
/** IBC transfer is initiated */
Advanced: 'ADVANCED',
/** settlement for matching advance received and funds dispersed */
Settled: 'SETTLED',
});
harden(TxStatus);

/**
* Status values for the StatusManager.
*
* @enum {(typeof PendingTxStatus)[keyof typeof PendingTxStatus]}
*/
export const PendingTxStatus = /** @type {const} */ ({
/** tx was observed but not advanced */
Observed: 'OBSERVED',
/** IBC transfer is initiated */
Advanced: 'ADVANCED',
});
harden(PendingTxStatus);
26 changes: 26 additions & 0 deletions packages/fast-usdc/src/exos/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
## **StatusManager** state diagram, showing different transitions


### Contract state diagram

*Transactions are qualified by the OCW and EventFeed before arriving to the Advancer.*

```mermaid
stateDiagram-v2
[*] --> Advanced: Advancer .advance()
Advanced --> Settled: Settler .settle() after fees
[*] --> Observed: Advancer .observed()
Observed --> Settled: Settler .settle() sans fees
Settled --> [*]
```

### Complete state diagram (starting from OCW)

```mermaid
stateDiagram-v2
Observed --> Qualified
Observed --> Unqualified
Qualified --> Advanced
Advanced --> Settled
Qualified --> Settled
```
118 changes: 112 additions & 6 deletions packages/fast-usdc/src/exos/advancer.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,125 @@
import { assertAllDefined } from '@agoric/internal';
import { ChainAddressShape } from '@agoric/orchestration';
import { VowShape } from '@agoric/vow';
import { makeError, q } from '@endo/errors';
import { E } from '@endo/far';
import { M } from '@endo/patterns';
import { CctpTxEvidenceShape } from '../typeGuards.js';
import { addressTools } from '../utils/address.js';

/**
* @import {HostInterface} from '@agoric/async-flow';
* @import {ChainAddress, ChainHub, Denom, DenomAmount, OrchestrationAccount} from '@agoric/orchestration';
* @import {VowTools} from '@agoric/vow';
* @import {Zone} from '@agoric/zone';
* @import {TransactionFeed} from './transaction-feed.js';
* @import {CctpTxEvidence, LogFn} from '../types.js';
* @import {StatusManager} from './status-manager.js';
* @import {TransactionFeed} from './transaction-feed.js';
*/

import { assertAllDefined } from '@agoric/internal';

/**
* @param {Zone} zone
* @param {object} caps
* @param {ChainHub} caps.chainHub
* @param {TransactionFeed} caps.feed
* @param {LogFn} caps.log
* @param {StatusManager} caps.statusManager
* @param {VowTools} caps.vowTools
*/
export const prepareAdvancer = (zone, { feed, statusManager }) => {
assertAllDefined({ feed, statusManager });
return zone.exo('Fast USDC Advancer', undefined, {});
export const prepareAdvancer = (
zone,
{ chainHub, feed, log, statusManager, vowTools: { watch } },
) => {
assertAllDefined({ feed, statusManager, watch });

const transferHandler = zone.exo(
'Fast USDC Advance Transfer Handler',
M.interface('TransferHandlerI', {
// TODO confirm undefined, and not bigint (sequence)
onFulfilled: M.call(M.undefined(), {
amount: M.bigint(),
destination: ChainAddressShape,
}).returns(M.undefined()),
onRejected: M.call(M.error(), {
amount: M.bigint(),
destination: ChainAddressShape,
}).returns(M.undefined()),
}),
{
/**
* @param {undefined} result TODO confirm this is not a bigint (sequence)
* @param {{ destination: ChainAddress; amount: bigint; }} ctx
*/
onFulfilled(result, { destination, amount }) {
log(
'Advance transfer fulfilled',
q({ amount, destination, result }).toString(),
);
},
onRejected(error) {
// XXX retry logic?
// What do we do if we fail, should we keep a Status?
log('Advance transfer rejected', q(error).toString());
},
},
);

return zone.exoClass(
'Fast USDC Advancer',
M.interface('AdvancerI', {
handleTransactionEvent: M.call(CctpTxEvidenceShape).returns(VowShape),
}),
/**
* @param {{
* localDenom: Denom;
* poolAccount: HostInterface<OrchestrationAccount<{ chainId: 'agoric' }>>;
* }} config
*/
config => harden(config),
{
/** @param {CctpTxEvidence} evidence */
handleTransactionEvent(evidence) {
// TODO EventFeed will perform input validation checks.
const { recipientAddress } = evidence.aux;
const { EUD } = addressTools.getQueryParams(recipientAddress).params;
if (!EUD) {
statusManager.observe(evidence);
throw makeError(
`recipientAddress does not contain EUD param: ${q(recipientAddress)}`,
);
}

// TODO #10391 this can throw, and should make a status update in the catch
const destination = chainHub.makeChainAddress(EUD);

/** @type {DenomAmount} */
const requestedAmount = harden({
denom: this.state.localDenom,
value: BigInt(evidence.tx.amount),
});

// TODO #10391 ensure there's enough funds in poolAccount

const transferV = E(this.state.poolAccount).transfer(
destination,
requestedAmount,
);

// mark as Advanced since `transferV` initiates the advance
statusManager.advance(evidence);

return watch(transferV, transferHandler, {
destination,
amount: requestedAmount.value,
});
},
},
{
stateShape: harden({
localDenom: M.string(),
poolAccount: M.remotable(),
}),
},
);
};
harden(prepareAdvancer);
87 changes: 84 additions & 3 deletions packages/fast-usdc/src/exos/settler.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,98 @@
import { assertAllDefined } from '@agoric/internal';
import { atob } from '@endo/base64';
import { makeError, q } from '@endo/errors';
import { M } from '@endo/patterns';

import { addressTools } from '../utils/address.js';

/**
* @import {FungibleTokenPacketData} from '@agoric/cosmic-proto/ibc/applications/transfer/v2/packet.js';
* @import {Denom} from '@agoric/orchestration';
* @import {IBCChannelID, VTransferIBCEvent} from '@agoric/vats';
* @import {Zone} from '@agoric/zone';
* @import {NobleAddress} from '../types.js';
* @import {StatusManager} from './status-manager.js';
*/

import { assertAllDefined } from '@agoric/internal';

/**
* @param {Zone} zone
* @param {object} caps
* @param {StatusManager} caps.statusManager
*/
export const prepareSettler = (zone, { statusManager }) => {
assertAllDefined({ statusManager });
return zone.exo('Fast USDC Settler', undefined, {});
return zone.exoClass(
'Fast USDC Settler',
M.interface('SettlerI', {
receiveUpcall: M.call(M.record()).returns(M.promise()),
}),
/**
*
* @param {{
* sourceChannel: IBCChannelID;
* remoteDenom: Denom
* }} config
*/
config => harden(config),
{
/** @param {VTransferIBCEvent} event */
async receiveUpcall(event) {
if (event.packet.source_channel !== this.state.sourceChannel) {
// TODO #10390 log all early returns
// only interested in packets from the issuing chain
return;
}
const tx = /** @type {FungibleTokenPacketData} */ (
JSON.parse(atob(event.packet.data))
);
if (tx.denom !== this.state.remoteDenom) {
// only interested in uusdc
return;
}

if (!addressTools.hasQueryParams(tx.receiver)) {
// only interested in receivers with query params
return;
}

const { params } = addressTools.getQueryParams(tx.receiver);
// TODO - what's the schema address parameter schema for FUSDC?
if (!params?.EUD) {
// only interested in receivers with EUD parameter
return;
}

// TODO discern between SETTLED and OBSERVED; each has different fees/destinations
const hasPendingSettlement = statusManager.hasPendingSettlement(
// given the sourceChannel check, we can be certain of this cast
/** @type {NobleAddress} */ (tx.sender),
BigInt(tx.amount),
);
if (!hasPendingSettlement) {
// TODO FAILURE PATH -> put money in recovery account or .transfer to receiver
// TODO should we have an ORPHANED TxStatus for this?
throw makeError(
`🚨 No pending settlement found for ${q(tx.sender)} ${q(tx.amount)}`,
);
}

// TODO disperse funds
// ~1. fee to contractFeeAccount
// ~2. remainder in poolAccount

// update status manager, marking tx `SETTLED`
statusManager.settle(
/** @type {NobleAddress} */ (tx.sender),
BigInt(tx.amount),
);
},
},
{
stateShape: harden({
sourceChannel: M.string(),
remoteDenom: M.string(),
}),
},
);
};
harden(prepareSettler);
Loading

0 comments on commit c236c55

Please sign in to comment.