-
Notifications
You must be signed in to change notification settings - Fork 207
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
Showing
27 changed files
with
1,355 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Binary file modified
BIN
+33 Bytes
(100%)
packages/builders/test/snapshots/orchestration-imports.test.js.snap
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
Oops, something went wrong.