Skip to content

Commit aa11d5c

Browse files
authoredNov 25, 2024··
feat(fast-usdc): settler disburses or forwards funds (#10530)
closes: #10391 ## Description - feat(fast-usdc): settler disburses or forwards funds Discussion of what to do if the minted USDC shows up at each state led to some refinement of states. So the scope of this PR expanded somewhat: - chore(fast-usdc): status manager: split out Advancing state - chore(fast-usdc): advancer: split ADVANCING state ### Security Considerations In addition to the normal case where funds are repaid to the pool and fees are distributed, the settler is responsible to forward funds in case they were not advanced. ### Scaling Considerations nothing novel ### Documentation Considerations Readers are assumed to be familiar with design docs. ### Testing Considerations Getting the tests to pass requires: - [ ] #10553 It's included in this PR for now but is expected to land separately DRAFT until - [x] test "Settlement for unknown transaction" case ### Upgrade Considerations This is a new component.
2 parents 8fd731c + d67cd22 commit aa11d5c

15 files changed

+1098
-380
lines changed
 

‎packages/fast-usdc/README.md

+41
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,44 @@ sequenceDiagram
5656
5757
A->>TF: notify(evidence)
5858
```
59+
60+
# Status Manager
61+
62+
### Pending Advance State Diagram
63+
64+
*Transactions are qualified by the OCW and EventFeed before arriving to the Advancer.*
65+
66+
```mermaid
67+
stateDiagram-v2
68+
[*] --> Observed: observe()
69+
[*] --> Advancing: advancing()
70+
71+
Advancing --> Advanced: advanceOutcome(...true)
72+
Advancing --> AdvanceFailed: advanceOutcome(...false)
73+
74+
Observed --> [*]: dequeueStatus()
75+
Advanced --> [*]: dequeueStatus()
76+
AdvanceFailed --> [*]: dequeueStatus()
77+
78+
note right of [*]
79+
After dequeueStatus():
80+
Transaction is removed
81+
from pendingTxs store.
82+
Settler will .disburse()
83+
or .forward()
84+
end note
85+
```
86+
87+
### Complete state diagram (starting from Transaction Feed into Advancer)
88+
89+
```mermaid
90+
stateDiagram-v2
91+
Observed --> Advancing
92+
Observed --> Forwarding:Minted
93+
Forwarding --> Forwarded
94+
Advancing --> Advanced
95+
Advanced --> Disbursed
96+
AdvanceFailed --> Forwarding
97+
Advancing --> AdvanceFailed
98+
Forwarding --> ForwardFailed
99+
```

‎packages/fast-usdc/src/constants.js

+16-2
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,22 @@ export const TxStatus = /** @type {const} */ ({
77
/** tx was observed but not advanced */
88
Observed: 'OBSERVED',
99
/** IBC transfer is initiated */
10+
Advancing: 'ADVANCING',
11+
/** IBC transfer is complete */
1012
Advanced: 'ADVANCED',
11-
/** settlement for matching advance received and funds dispersed */
12-
Settled: 'SETTLED',
13+
/** IBC transfer failed (timed out) */
14+
AdvanceFailed: 'ADVANCE_FAILED',
15+
/** settlement for matching advance received and funds disbursed */
16+
Disbursed: 'DISBURSED',
17+
/** fallback: do not collect fees */
18+
Forwarded: 'FORWARDED',
19+
/** failed to forward to EUD */
20+
ForwardFailed: 'FORWARD_FAILED',
1321
});
1422
harden(TxStatus);
1523

24+
// TODO: define valid state transitions
25+
1626
/**
1727
* Status values for the StatusManager.
1828
*
@@ -22,6 +32,10 @@ export const PendingTxStatus = /** @type {const} */ ({
2232
/** tx was observed but not advanced */
2333
Observed: 'OBSERVED',
2434
/** IBC transfer is initiated */
35+
Advancing: 'ADVANCING',
36+
/** IBC transfer failed (timed out) */
37+
AdvanceFailed: 'ADVANCE_FAILED',
38+
/** IBC transfer is complete */
2539
Advanced: 'ADVANCED',
2640
});
2741
harden(PendingTxStatus);

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

-26
This file was deleted.

‎packages/fast-usdc/src/exos/advancer.js

+72-47
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,22 @@ import { VowShape } from '@agoric/vow';
66
import { q } from '@endo/errors';
77
import { E } from '@endo/far';
88
import { M } from '@endo/patterns';
9-
import { CctpTxEvidenceShape, EudParamShape } from '../type-guards.js';
9+
import {
10+
CctpTxEvidenceShape,
11+
EudParamShape,
12+
EvmHashShape,
13+
} from '../type-guards.js';
1014
import { addressTools } from '../utils/address.js';
1115
import { makeFeeTools } from '../utils/fees.js';
1216

13-
const { isGTE } = AmountMath;
14-
1517
/**
1618
* @import {HostInterface} from '@agoric/async-flow';
1719
* @import {NatAmount} from '@agoric/ertp';
1820
* @import {ChainAddress, ChainHub, Denom, OrchestrationAccount} from '@agoric/orchestration';
1921
* @import {ZoeTools} from '@agoric/orchestration/src/utils/zoe-tools.js';
2022
* @import {VowTools} from '@agoric/vow';
2123
* @import {Zone} from '@agoric/zone';
22-
* @import {CctpTxEvidence, FeeConfig, LogFn} from '../types.js';
24+
* @import {CctpTxEvidence, EvmHash, FeeConfig, LogFn, NobleAddress} from '../types.js';
2325
* @import {StatusManager} from './status-manager.js';
2426
* @import {LiquidityPoolKit} from './liquidity-pool.js';
2527
*/
@@ -46,27 +48,44 @@ const AdvancerKitI = harden({
4648
onFulfilled: M.call(M.undefined(), {
4749
amount: AmountShape,
4850
destination: ChainAddressShape,
51+
forwardingAddress: M.string(),
4952
tmpSeat: M.remotable(),
53+
txHash: EvmHashShape,
5054
}).returns(VowShape),
5155
onRejected: M.call(M.error(), {
5256
amount: AmountShape,
5357
destination: ChainAddressShape,
58+
forwardingAddress: M.string(),
5459
tmpSeat: M.remotable(),
60+
txHash: EvmHashShape,
5561
}).returns(),
5662
}),
5763
transferHandler: M.interface('TransferHandlerI', {
5864
// TODO confirm undefined, and not bigint (sequence)
5965
onFulfilled: M.call(M.undefined(), {
6066
amount: AmountShape,
6167
destination: ChainAddressShape,
68+
forwardingAddress: M.string(),
69+
txHash: EvmHashShape,
6270
}).returns(M.undefined()),
6371
onRejected: M.call(M.error(), {
6472
amount: AmountShape,
6573
destination: ChainAddressShape,
74+
forwardingAddress: M.string(),
75+
txHash: EvmHashShape,
6676
}).returns(M.undefined()),
6777
}),
6878
});
6979

80+
/**
81+
* @typedef {{
82+
* amount: NatAmount;
83+
* destination: ChainAddress;
84+
* forwardingAddress: NobleAddress;
85+
* txHash: EvmHash;
86+
* }} AdvancerVowCtx
87+
*/
88+
7089
/**
7190
* @param {Zone} zone
7291
* @param {AdvancerKitPowers} caps
@@ -100,6 +119,7 @@ export const prepareAdvancerKit = (
100119
AdvancerKitI,
101120
/**
102121
* @param {{
122+
* notifyFacet: import('./settler.js').SettlerKit['notify'];
103123
* borrowerFacet: LiquidityPoolKit['borrower'];
104124
* poolAccount: HostInterface<OrchestrationAccount<{chainId: 'agoric'}>>;
105125
* }} config
@@ -120,51 +140,32 @@ export const prepareAdvancerKit = (
120140
async handleTransactionEvent(evidence) {
121141
await null;
122142
try {
143+
if (statusManager.hasBeenObserved(evidence)) {
144+
log('txHash already seen:', evidence.txHash);
145+
return;
146+
}
147+
123148
const { borrowerFacet, poolAccount } = this.state;
124149
const { recipientAddress } = evidence.aux;
150+
// throws if EUD is not found
125151
const { EUD } = addressTools.getQueryParams(
126152
recipientAddress,
127153
EudParamShape,
128154
);
129-
130-
// this will throw if the bech32 prefix is not found, but is handled by the catch
155+
// throws if the bech32 prefix is not found
131156
const destination = chainHub.makeChainAddress(EUD);
157+
132158
const requestedAmount = toAmount(evidence.tx.amount);
159+
// throws if requested does not exceed fees
133160
const advanceAmount = feeTools.calculateAdvance(requestedAmount);
134161

135-
// TODO: consider skipping and using `borrow()`s internal balance check
136-
const poolBalance = borrowerFacet.getBalance();
137-
if (!isGTE(poolBalance, requestedAmount)) {
138-
log(
139-
`Insufficient pool funds`,
140-
`Requested ${q(advanceAmount)} but only have ${q(poolBalance)}`,
141-
);
142-
statusManager.observe(evidence);
143-
return;
144-
}
145-
146-
try {
147-
// Mark as Advanced since `transferV` initiates the advance.
148-
// Will throw if we've already .skipped or .advanced this evidence.
149-
statusManager.advance(evidence);
150-
} catch (e) {
151-
// Only anticipated error is `assertNotSeen`, so intercept the
152-
// catch so we don't call .skip which also performs this check
153-
log('Advancer error:', q(e).toString());
154-
return;
155-
}
156-
157162
const { zcfSeat: tmpSeat } = zcf.makeEmptySeatKit();
158163
const amountKWR = harden({ USDC: advanceAmount });
159-
try {
160-
borrowerFacet.borrow(tmpSeat, amountKWR);
161-
} catch (e) {
162-
// We do not expect this to fail since there are no turn boundaries
163-
// between .getBalance() and .borrow().
164-
// We catch to report outside of the normal error flow since this is
165-
// not expected.
166-
log('🚨 advance borrow failed', q(e).toString());
167-
}
164+
// throws if the pool has insufficient funds
165+
borrowerFacet.borrow(tmpSeat, amountKWR);
166+
167+
// this cannot throw since `.isSeen()` is called in the same turn
168+
statusManager.advance(evidence);
168169

169170
const depositV = localTransfer(
170171
tmpSeat,
@@ -175,7 +176,9 @@ export const prepareAdvancerKit = (
175176
void watch(depositV, this.facets.depositHandler, {
176177
amount: advanceAmount,
177178
destination,
179+
forwardingAddress: evidence.tx.forwardingAddress,
178180
tmpSeat,
181+
txHash: evidence.txHash,
179182
});
180183
} catch (e) {
181184
log('Advancer error:', q(e).toString());
@@ -186,22 +189,25 @@ export const prepareAdvancerKit = (
186189
depositHandler: {
187190
/**
188191
* @param {undefined} result
189-
* @param {{ amount: Amount<'nat'>; destination: ChainAddress; tmpSeat: ZCFSeat }} ctx
192+
* @param {AdvancerVowCtx & { tmpSeat: ZCFSeat }} ctx
190193
*/
191-
onFulfilled(result, { amount, destination }) {
194+
onFulfilled(result, ctx) {
192195
const { poolAccount } = this.state;
196+
const { amount, destination, forwardingAddress, txHash } = ctx;
193197
const transferV = E(poolAccount).transfer(destination, {
194198
denom: usdc.denom,
195199
value: amount.value,
196200
});
197201
return watch(transferV, this.facets.transferHandler, {
198202
destination,
199203
amount,
204+
forwardingAddress,
205+
txHash,
200206
});
201207
},
202208
/**
203209
* @param {Error} error
204-
* @param {{ amount: Amount<'nat'>; destination: ChainAddress; tmpSeat: ZCFSeat }} ctx
210+
* @param {AdvancerVowCtx & { tmpSeat: ZCFSeat }} ctx
205211
*/
206212
onRejected(error, { tmpSeat }) {
207213
// TODO return seat allocation from ctx to LP?
@@ -217,25 +223,44 @@ export const prepareAdvancerKit = (
217223
transferHandler: {
218224
/**
219225
* @param {undefined} result TODO confirm this is not a bigint (sequence)
220-
* @param {{ destination: ChainAddress; amount: NatAmount; }} ctx
226+
* @param {AdvancerVowCtx} ctx
221227
*/
222-
onFulfilled(result, { destination, amount }) {
223-
// TODO vstorage update? We don't currently have a status for
224-
// Advanced + transferV settled
228+
onFulfilled(result, ctx) {
229+
const { notifyFacet } = this.state;
230+
const { amount, destination, forwardingAddress, txHash } = ctx;
225231
log(
226232
'Advance transfer fulfilled',
227233
q({ amount, destination, result }).toString(),
228234
);
235+
notifyFacet.notifyAdvancingResult(
236+
txHash,
237+
forwardingAddress,
238+
amount.value,
239+
destination.value,
240+
true,
241+
);
229242
},
230-
onRejected(error) {
231-
// TODO #10510 (comprehensive error testing) determine
232-
// course of action here. This might fail due to timeout.
243+
/**
244+
* @param {Error} error
245+
* @param {AdvancerVowCtx} ctx
246+
*/
247+
onRejected(error, ctx) {
248+
const { notifyFacet } = this.state;
249+
const { amount, destination, forwardingAddress, txHash } = ctx;
233250
log('Advance transfer rejected', q(error).toString());
251+
notifyFacet.notifyAdvancingResult(
252+
txHash,
253+
forwardingAddress,
254+
amount.value,
255+
destination.value,
256+
false,
257+
);
234258
},
235259
},
236260
},
237261
{
238262
stateShape: harden({
263+
notifyFacet: M.remotable(),
239264
borrowerFacet: M.remotable(),
240265
poolAccount: M.remotable(),
241266
}),

‎packages/fast-usdc/src/exos/liquidity-pool.js

+1-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { AmountMath, AmountShape } from '@agoric/ertp';
1+
import { AmountMath } from '@agoric/ertp';
22
import {
33
makeRecorderTopic,
44
TopicsRecordShape,
@@ -84,7 +84,6 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
8484
'Liquidity Pool',
8585
{
8686
borrower: M.interface('borrower', {
87-
getBalance: M.call().returns(AmountShape),
8887
borrow: M.call(
8988
SeatShape,
9089
harden({ USDC: makeNatAmountShape(USDC, 1n) }),
@@ -152,10 +151,6 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
152151
},
153152
{
154153
borrower: {
155-
getBalance() {
156-
const { poolSeat } = this.state;
157-
return poolSeat.getAmountAllocated('USDC', USDC);
158-
},
159154
/**
160155
* @param {ZCFSeat} toSeat
161156
* @param {{ USDC: Amount<'nat'>}} amountKWR

0 commit comments

Comments
 (0)
Please sign in to comment.