Skip to content

Commit 40ad937

Browse files
authored
feat: fusdc advancer (#10420)
refs: #10390 ## Description - [x] advancer performs balance check - [x] advancer requests payment from `LiquidityPool` - [x] advancer deposits payment in `PoolAccount` - [x] advancer submits IBC transfer ### Security Considerations Deals with live payments and ensures they do not get lost during failure paths ### Scaling Considerations No new ones introduced ### Documentation Considerations ### Testing Considerations Includes tests ### Upgrade Considerations NA, unreleased
2 parents b2a7540 + 99707ef commit 40ad937

14 files changed

+599
-240
lines changed

packages/fast-usdc/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"@agoric/notifier": "^0.6.2",
3838
"@agoric/orchestration": "^0.1.0",
3939
"@agoric/store": "^0.9.2",
40+
"@agoric/vat-data": "^0.5.2",
4041
"@agoric/vow": "^0.1.0",
4142
"@agoric/zoe": "^0.26.2",
4243
"@endo/base64": "^1.0.8",
+191-81
Original file line numberDiff line numberDiff line change
@@ -1,124 +1,234 @@
1+
import { AmountMath, AmountShape, PaymentShape } from '@agoric/ertp';
12
import { assertAllDefined } from '@agoric/internal';
23
import { ChainAddressShape } from '@agoric/orchestration';
4+
import { pickFacet } from '@agoric/vat-data';
35
import { VowShape } from '@agoric/vow';
4-
import { makeError, q } from '@endo/errors';
6+
import { q } from '@endo/errors';
57
import { E } from '@endo/far';
68
import { M } from '@endo/patterns';
7-
import { CctpTxEvidenceShape } from '../typeGuards.js';
9+
import { CctpTxEvidenceShape, EudParamShape } from '../typeGuards.js';
810
import { addressTools } from '../utils/address.js';
911

12+
const { isGTE } = AmountMath;
13+
1014
/**
1115
* @import {HostInterface} from '@agoric/async-flow';
16+
* @import {NatAmount} from '@agoric/ertp';
1217
* @import {ChainAddress, ChainHub, Denom, DenomAmount, OrchestrationAccount} from '@agoric/orchestration';
1318
* @import {VowTools} from '@agoric/vow';
1419
* @import {Zone} from '@agoric/zone';
1520
* @import {CctpTxEvidence, LogFn} from '../types.js';
1621
* @import {StatusManager} from './status-manager.js';
17-
* @import {TransactionFeedKit} from './transaction-feed.js';
1822
*/
1923

24+
/**
25+
* Expected interface from LiquidityPool
26+
*
27+
* @typedef {{
28+
* lookupBalance(): NatAmount;
29+
* borrow(amount: Amount<"nat">): Promise<Payment<"nat">>;
30+
* repay(payments: PaymentKeywordRecord): Promise<void>
31+
* }} AssetManagerFacet
32+
*/
33+
34+
/**
35+
* @typedef {{
36+
* chainHub: ChainHub;
37+
* log: LogFn;
38+
* statusManager: StatusManager;
39+
* usdc: { brand: Brand<'nat'>; denom: Denom; };
40+
* vowTools: VowTools;
41+
* }} AdvancerKitPowers
42+
*/
43+
44+
/** type guards internal to the AdvancerKit */
45+
const AdvancerKitI = harden({
46+
advancer: M.interface('AdvancerI', {
47+
handleTransactionEvent: M.callWhen(CctpTxEvidenceShape).returns(),
48+
}),
49+
depositHandler: M.interface('DepositHandlerI', {
50+
onFulfilled: M.call(AmountShape, {
51+
destination: ChainAddressShape,
52+
payment: PaymentShape,
53+
}).returns(VowShape),
54+
onRejected: M.call(M.error(), {
55+
destination: ChainAddressShape,
56+
payment: PaymentShape,
57+
}).returns(),
58+
}),
59+
transferHandler: M.interface('TransferHandlerI', {
60+
// TODO confirm undefined, and not bigint (sequence)
61+
onFulfilled: M.call(M.undefined(), {
62+
amount: AmountShape,
63+
destination: ChainAddressShape,
64+
}).returns(M.undefined()),
65+
onRejected: M.call(M.error(), {
66+
amount: AmountShape,
67+
destination: ChainAddressShape,
68+
}).returns(M.undefined()),
69+
}),
70+
});
71+
2072
/**
2173
* @param {Zone} zone
22-
* @param {object} caps
23-
* @param {ChainHub} caps.chainHub
24-
* @param {LogFn} caps.log
25-
* @param {StatusManager} caps.statusManager
26-
* @param {VowTools} caps.vowTools
74+
* @param {AdvancerKitPowers} caps
2775
*/
28-
export const prepareAdvancer = (
76+
export const prepareAdvancerKit = (
2977
zone,
30-
{ chainHub, log, statusManager, vowTools: { watch } },
78+
{ chainHub, log, statusManager, usdc, vowTools: { watch, when } },
3179
) => {
32-
assertAllDefined({ statusManager, watch });
80+
assertAllDefined({
81+
chainHub,
82+
statusManager,
83+
watch,
84+
when,
85+
});
3386

34-
const transferHandler = zone.exo(
35-
'Fast USDC Advance Transfer Handler',
36-
M.interface('TransferHandlerI', {
37-
// TODO confirm undefined, and not bigint (sequence)
38-
onFulfilled: M.call(M.undefined(), {
39-
amount: M.bigint(),
40-
destination: ChainAddressShape,
41-
}).returns(M.undefined()),
42-
onRejected: M.call(M.error(), {
43-
amount: M.bigint(),
44-
destination: ChainAddressShape,
45-
}).returns(M.undefined()),
46-
}),
47-
{
48-
/**
49-
* @param {undefined} result TODO confirm this is not a bigint (sequence)
50-
* @param {{ destination: ChainAddress; amount: bigint; }} ctx
51-
*/
52-
onFulfilled(result, { destination, amount }) {
53-
log(
54-
'Advance transfer fulfilled',
55-
q({ amount, destination, result }).toString(),
56-
);
57-
},
58-
onRejected(error) {
59-
// XXX retry logic?
60-
// What do we do if we fail, should we keep a Status?
61-
log('Advance transfer rejected', q(error).toString());
62-
},
63-
},
64-
);
87+
/** @param {bigint} value */
88+
const toAmount = value => AmountMath.make(usdc.brand, value);
6589

66-
return zone.exoClass(
90+
return zone.exoClassKit(
6791
'Fast USDC Advancer',
68-
M.interface('AdvancerI', {
69-
handleTransactionEvent: M.call(CctpTxEvidenceShape).returns(VowShape),
70-
}),
92+
AdvancerKitI,
7193
/**
7294
* @param {{
73-
* localDenom: Denom;
74-
* poolAccount: HostInterface<OrchestrationAccount<{ chainId: 'agoric' }>>;
95+
* assetManagerFacet: AssetManagerFacet;
96+
* poolAccount: ERef<HostInterface<OrchestrationAccount<{chainId: 'agoric'}>>>;
7597
* }} config
7698
*/
7799
config => harden(config),
78100
{
79-
/** @param {CctpTxEvidence} evidence */
80-
handleTransactionEvent(evidence) {
81-
// TODO EventFeed will perform input validation checks.
82-
const { recipientAddress } = evidence.aux;
83-
const { EUD } = addressTools.getQueryParams(recipientAddress).params;
84-
if (!EUD) {
85-
statusManager.observe(evidence);
86-
throw makeError(
87-
`recipientAddress does not contain EUD param: ${q(recipientAddress)}`,
88-
);
89-
}
90-
91-
// TODO #10391 this can throw, and should make a status update in the catch
92-
const destination = chainHub.makeChainAddress(EUD);
93-
94-
/** @type {DenomAmount} */
95-
const requestedAmount = harden({
96-
denom: this.state.localDenom,
97-
value: BigInt(evidence.tx.amount),
98-
});
101+
advancer: {
102+
/**
103+
* Must perform a status update for every observed transaction.
104+
*
105+
* We do not expect any callers to depend on the settlement of
106+
* `handleTransactionEvent` - errors caught are communicated to the
107+
* `StatusManager` - so we don't need to concern ourselves with
108+
* preserving the vow chain for callers.
109+
*
110+
* @param {CctpTxEvidence} evidence
111+
*/
112+
async handleTransactionEvent(evidence) {
113+
await null;
114+
try {
115+
// TODO poolAccount might be a vow we need to unwrap
116+
const { assetManagerFacet, poolAccount } = this.state;
117+
const { recipientAddress } = evidence.aux;
118+
const { EUD } = addressTools.getQueryParams(
119+
recipientAddress,
120+
EudParamShape,
121+
);
99122

100-
// TODO #10391 ensure there's enough funds in poolAccount
123+
// this will throw if the bech32 prefix is not found, but is handled by the catch
124+
const destination = chainHub.makeChainAddress(EUD);
125+
const requestedAmount = toAmount(evidence.tx.amount);
101126

102-
const transferV = E(this.state.poolAccount).transfer(
103-
destination,
104-
requestedAmount,
105-
);
127+
// TODO: consider skipping and using `borrow()`s internal balance check
128+
const poolBalance = assetManagerFacet.lookupBalance();
129+
if (!isGTE(poolBalance, requestedAmount)) {
130+
log(
131+
`Insufficient pool funds`,
132+
`Requested ${q(requestedAmount)} but only have ${q(poolBalance)}`,
133+
);
134+
statusManager.observe(evidence);
135+
return;
136+
}
106137

107-
// mark as Advanced since `transferV` initiates the advance
108-
statusManager.advance(evidence);
138+
try {
139+
// Mark as Advanced since `transferV` initiates the advance.
140+
// Will throw if we've already .skipped or .advanced this evidence.
141+
statusManager.advance(evidence);
142+
} catch (e) {
143+
// Only anticipated error is `assertNotSeen`, so intercept the
144+
// catch so we don't call .skip which also performs this check
145+
log('Advancer error:', q(e).toString());
146+
return;
147+
}
109148

110-
return watch(transferV, transferHandler, {
111-
destination,
112-
amount: requestedAmount.value,
113-
});
149+
try {
150+
const payment = await assetManagerFacet.borrow(requestedAmount);
151+
const depositV = E(poolAccount).deposit(payment);
152+
void watch(depositV, this.facets.depositHandler, {
153+
destination,
154+
payment,
155+
});
156+
} catch (e) {
157+
// `.borrow()` might fail if the balance changes since we
158+
// requested it. TODO - how to handle this? change ADVANCED -> OBSERVED?
159+
// Note: `depositHandler` handles the `.deposit()` failure
160+
log('🚨 advance borrow failed', q(e).toString());
161+
}
162+
} catch (e) {
163+
log('Advancer error:', q(e).toString());
164+
statusManager.observe(evidence);
165+
}
166+
},
167+
},
168+
depositHandler: {
169+
/**
170+
* @param {NatAmount} amount amount returned from deposit
171+
* @param {{ destination: ChainAddress; payment: Payment<'nat'> }} ctx
172+
*/
173+
onFulfilled(amount, { destination }) {
174+
const { poolAccount } = this.state;
175+
const transferV = E(poolAccount).transfer(
176+
destination,
177+
/** @type {DenomAmount} */ ({
178+
denom: usdc.denom,
179+
value: amount.value,
180+
}),
181+
);
182+
return watch(transferV, this.facets.transferHandler, {
183+
destination,
184+
amount,
185+
});
186+
},
187+
/**
188+
* @param {Error} error
189+
* @param {{ destination: ChainAddress; payment: Payment<'nat'> }} ctx
190+
*/
191+
onRejected(error, { payment }) {
192+
// TODO return live payment from ctx to LP
193+
log('🚨 advance deposit failed', q(error).toString());
194+
log('TODO live payment to return to LP', q(payment).toString());
195+
},
196+
},
197+
transferHandler: {
198+
/**
199+
* @param {undefined} result TODO confirm this is not a bigint (sequence)
200+
* @param {{ destination: ChainAddress; amount: NatAmount; }} ctx
201+
*/
202+
onFulfilled(result, { destination, amount }) {
203+
// TODO vstorage update?
204+
log(
205+
'Advance transfer fulfilled',
206+
q({ amount, destination, result }).toString(),
207+
);
208+
},
209+
onRejected(error) {
210+
// XXX retry logic?
211+
// What do we do if we fail, should we keep a Status?
212+
log('Advance transfer rejected', q(error).toString());
213+
},
114214
},
115215
},
116216
{
117217
stateShape: harden({
118-
localDenom: M.string(),
119-
poolAccount: M.remotable(),
218+
assetManagerFacet: M.remotable(),
219+
poolAccount: M.or(VowShape, M.remotable()),
120220
}),
121221
},
122222
);
123223
};
224+
harden(prepareAdvancerKit);
225+
226+
/**
227+
* @param {Zone} zone
228+
* @param {AdvancerKitPowers} caps
229+
*/
230+
export const prepareAdvancer = (zone, caps) => {
231+
const makeAdvancerKit = prepareAdvancerKit(zone, caps);
232+
return pickFacet(makeAdvancerKit, 'advancer');
233+
};
124234
harden(prepareAdvancer);

packages/fast-usdc/src/exos/settler.js

+2-3
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,8 @@ export const prepareSettler = (zone, { statusManager }) => {
5555
return;
5656
}
5757

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

packages/fast-usdc/src/fast-usdc.contract.js

+10-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { defineInertInvitation } from './utils/zoe.js';
1616
const trace = makeTracer('FastUsdc');
1717

1818
/**
19+
* @import {Denom} from '@agoric/orchestration';
1920
* @import {OrchestrationPowers, OrchestrationTools} from '@agoric/orchestration/src/utils/start-helper.js';
2021
* @import {Zone} from '@agoric/zone';
2122
* @import {OperatorKit} from './exos/operator-kit.js';
@@ -26,13 +27,15 @@ const trace = makeTracer('FastUsdc');
2627
* @typedef {{
2728
* poolFee: Amount<'nat'>;
2829
* contractFee: Amount<'nat'>;
30+
* usdcDenom: Denom;
2931
* }} FastUsdcTerms
3032
*/
3133
const NatAmountShape = { brand: BrandShape, value: M.nat() };
3234
export const meta = {
3335
customTermsShape: {
3436
contractFee: NatAmountShape,
3537
poolFee: NatAmountShape,
38+
usdcDenom: M.string(),
3639
},
3740
};
3841
harden(meta);
@@ -49,6 +52,7 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
4952
assert(tools, 'no tools');
5053
const terms = zcf.getTerms();
5154
assert('USDC' in terms.brands, 'no USDC brand');
55+
assert('usdcDenom' in terms, 'no usdcDenom');
5256

5357
const { makeRecorderKit } = prepareRecorderKitMakers(
5458
zone.mapStore('vstorage'),
@@ -61,6 +65,10 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
6165
const makeAdvancer = prepareAdvancer(zone, {
6266
chainHub,
6367
log: trace,
68+
usdc: harden({
69+
brand: terms.brands.USDC,
70+
denom: terms.usdcDenom,
71+
}),
6472
statusManager,
6573
vowTools,
6674
});
@@ -75,7 +83,7 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
7583
void observeIteration(subscribeEach(feedKit.public.getEvidenceSubscriber()), {
7684
updateState(evidence) {
7785
try {
78-
advancer.handleTransactionEvent(evidence);
86+
void advancer.handleTransactionEvent(evidence);
7987
} catch (err) {
8088
trace('🚨 Error handling transaction event', err);
8189
}
@@ -117,7 +125,7 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
117125
* @param {CctpTxEvidence} evidence
118126
*/
119127
makeTestPushInvitation(evidence) {
120-
advancer.handleTransactionEvent(evidence);
128+
void advancer.handleTransactionEvent(evidence);
121129
return makeTestInvitation();
122130
},
123131
makeDepositInvitation() {

0 commit comments

Comments
 (0)