Skip to content

Commit 7390224

Browse files
authored
feat: fusdc advancer with fees (#10494)
refs: #10390 ## Description - Add `FeeConfig` to `privateArgs` so deployers can adjust fees over time (terms are immutable) - Adds `FeeTools` for calculating net advance and fee splits - Integrates `FeeTools` with `Advancer` ### Security Considerations - None really for these specific changes. We might need to think about timing from when the `Advancer` calls `calculateAdvance` and the `Settler` calls `calculateSplit`, as the `FeeConfig` values might change. - From a product POV, users might pay more in fees than the net advance they receive. So we might consider a config parameter for a "minimum request amount". ### Scaling Considerations None really, mainly contains AmountMath computation. ### Documentation Considerations Includes jsdoc and code comments ### Testing Considerations Includes unit tests for FeeTools, attempting to cover all foreseeable scenarios. A bit light on testing integrations with the advancer and settler. ### Upgrade Considerations N/A, unreleased
2 parents 5a4fc01 + 3771e7f commit 7390224

29 files changed

+461
-104
lines changed

packages/builders/scripts/fast-usdc/init-fast-usdc.js

+31-10
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {
66
getManifestForFastUSDC,
77
} from '@agoric/fast-usdc/src/fast-usdc.start.js';
88
import { toExternalConfig } from '@agoric/fast-usdc/src/utils/config-marshal.js';
9-
import { objectMap } from '@agoric/internal';
109
import {
1110
multiplyBy,
1211
parseRatio,
@@ -22,16 +21,26 @@ import { parseArgs } from 'node:util';
2221

2322
/** @type {ParseArgsConfig['options']} */
2423
const options = {
25-
contractFee: { type: 'string', default: '0.01' },
26-
poolFee: { type: 'string', default: '0.01' },
24+
flatFee: { type: 'string', default: '0.01' },
25+
variableRate: { type: 'string', default: '0.01' },
26+
maxVariableFee: { type: 'string', default: '5' },
27+
contractRate: { type: 'string', default: '0.2' },
2728
oracle: { type: 'string', multiple: true },
29+
usdcDenom: {
30+
type: 'string',
31+
default:
32+
'ibc/FE98AAD68F02F03565E9FA39A5E627946699B2B07115889ED812D8BA639576A9',
33+
},
2834
};
2935
const oraclesRequiredUsage = 'use --oracle name:address ...';
3036
/**
3137
* @typedef {{
32-
* contractFee: string;
33-
* poolFee: string;
38+
* flatFee: string;
39+
* variableRate: string;
40+
* maxVariableFee: string;
41+
* contractRate: string;
3442
* oracle?: string[];
43+
* usdcDenom: string;
3544
* }} FastUSDCOpts
3645
*/
3746

@@ -73,7 +82,7 @@ export default async (homeP, endowments) => {
7382
/** @type {{ values: FastUSDCOpts }} */
7483
// @ts-expect-error ensured by options
7584
const {
76-
values: { oracle: oracleArgs, ...fees },
85+
values: { oracle: oracleArgs, usdcDenom, ...fees },
7786
} = parseArgs({ args: scriptArgs, options });
7887

7988
const parseOracleArgs = () => {
@@ -88,15 +97,27 @@ export default async (homeP, endowments) => {
8897
);
8998
};
9099

100+
/** @param {string} numeral */
101+
const toAmount = numeral => multiplyBy(unit, parseRatio(numeral, USDC));
102+
/** @param {string} numeral */
103+
const toRatio = numeral => parseRatio(numeral, USDC);
104+
const parseFeeConfigArgs = () => {
105+
const { flatFee, variableRate, maxVariableFee, contractRate } = fees;
106+
return {
107+
flat: toAmount(flatFee),
108+
variableRate: toRatio(variableRate),
109+
maxVariable: toAmount(maxVariableFee),
110+
contractRate: toRatio(contractRate),
111+
};
112+
};
113+
91114
/** @type {FastUSDCConfig} */
92115
const config = harden({
93116
oracles: parseOracleArgs(),
94117
terms: {
95-
...objectMap(fees, numeral =>
96-
multiplyBy(unit, parseRatio(numeral, USDC)),
97-
),
98-
usdcDenom: 'ibc/usdconagoric',
118+
usdcDenom,
99119
},
120+
feeConfig: parseFeeConfigArgs(),
100121
});
101122

102123
await writeCoreEval('start-fast-usdc', utils =>

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

+17
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,23 @@ Generated by [AVA](https://avajs.dev).
369369
},
370370
],
371371
},
372+
OrchestrationPowersShape: {
373+
agoricNames: Object @match:kind {
374+
payload: 'remotable',
375+
},
376+
localchain: Object @match:kind {
377+
payload: 'remotable',
378+
},
379+
orchestrationService: Object @match:kind {
380+
payload: 'remotable',
381+
},
382+
storageNode: Object @match:kind {
383+
payload: 'remotable',
384+
},
385+
timerService: Object @match:kind {
386+
payload: 'remotable',
387+
},
388+
},
372389
OutboundConnectionHandlerI: Object @guard:interfaceGuard {
373390
payload: {
374391
defaultGuards: undefined,
Binary file not shown.

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

+12-6
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ 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 '../typeGuards.js';
9+
import { CctpTxEvidenceShape, EudParamShape } from '../type-guards.js';
1010
import { addressTools } from '../utils/address.js';
11+
import { makeFeeTools } from '../utils/fees.js';
1112

1213
const { isGTE } = AmountMath;
1314

@@ -17,7 +18,7 @@ const { isGTE } = AmountMath;
1718
* @import {ChainAddress, ChainHub, Denom, DenomAmount, OrchestrationAccount} from '@agoric/orchestration';
1819
* @import {VowTools} from '@agoric/vow';
1920
* @import {Zone} from '@agoric/zone';
20-
* @import {CctpTxEvidence, LogFn} from '../types.js';
21+
* @import {CctpTxEvidence, FeeConfig, LogFn} from '../types.js';
2122
* @import {StatusManager} from './status-manager.js';
2223
*/
2324

@@ -34,6 +35,7 @@ const { isGTE } = AmountMath;
3435
/**
3536
* @typedef {{
3637
* chainHub: ChainHub;
38+
* feeConfig: FeeConfig;
3739
* log: LogFn;
3840
* statusManager: StatusManager;
3941
* usdc: { brand: Brand<'nat'>; denom: Denom; };
@@ -75,15 +77,16 @@ const AdvancerKitI = harden({
7577
*/
7678
export const prepareAdvancerKit = (
7779
zone,
78-
{ chainHub, log, statusManager, usdc, vowTools: { watch, when } },
80+
{ chainHub, feeConfig, log, statusManager, usdc, vowTools: { watch, when } },
7981
) => {
8082
assertAllDefined({
8183
chainHub,
84+
feeConfig,
8285
statusManager,
8386
watch,
8487
when,
8588
});
86-
89+
const feeTools = makeFeeTools(feeConfig);
8790
/** @param {bigint} value */
8891
const toAmount = value => AmountMath.make(usdc.brand, value);
8992

@@ -123,14 +126,17 @@ export const prepareAdvancerKit = (
123126
// this will throw if the bech32 prefix is not found, but is handled by the catch
124127
const destination = chainHub.makeChainAddress(EUD);
125128
const requestedAmount = toAmount(evidence.tx.amount);
129+
const advanceAmount = feeTools.calculateAdvance(requestedAmount);
126130

127131
// TODO: consider skipping and using `borrow()`s internal balance check
128132
const poolBalance = assetManagerFacet.lookupBalance();
129133
if (!isGTE(poolBalance, requestedAmount)) {
130134
log(
131135
`Insufficient pool funds`,
132-
`Requested ${q(requestedAmount)} but only have ${q(poolBalance)}`,
136+
`Requested ${q(advanceAmount)} but only have ${q(poolBalance)}`,
133137
);
138+
// report `requestedAmount`, not `advancedAmount`... do we need to
139+
// communicate net to `StatusManger` in case fees change in between?
134140
statusManager.observe(evidence);
135141
return;
136142
}
@@ -147,7 +153,7 @@ export const prepareAdvancerKit = (
147153
}
148154

149155
try {
150-
const payment = await assetManagerFacet.borrow(requestedAmount);
156+
const payment = await assetManagerFacet.borrow(advanceAmount);
151157
const depositV = E(poolAccount).deposit(payment);
152158
void watch(depositV, this.facets.depositHandler, {
153159
destination,

packages/fast-usdc/src/exos/operator-kit.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { makeTracer } from '@agoric/internal';
22
import { Fail } from '@endo/errors';
33
import { M } from '@endo/patterns';
4-
import { CctpTxEvidenceShape } from '../typeGuards.js';
4+
import { CctpTxEvidenceShape } from '../type-guards.js';
55

66
const trace = makeTracer('TxOperator');
77

packages/fast-usdc/src/exos/status-manager.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { M } from '@endo/patterns';
22
import { makeError, q } from '@endo/errors';
33

44
import { appendToStoredArray } from '@agoric/store/src/stores/store-utils.js';
5-
import { CctpTxEvidenceShape, PendingTxShape } from '../typeGuards.js';
5+
import { CctpTxEvidenceShape, PendingTxShape } from '../type-guards.js';
66
import { PendingTxStatus } from '../constants.js';
77

88
/**

packages/fast-usdc/src/exos/transaction-feed.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { makeTracer } from '@agoric/internal';
22
import { prepareDurablePublishKit } from '@agoric/notifier';
33
import { M } from '@endo/patterns';
4-
import { CctpTxEvidenceShape } from '../typeGuards.js';
4+
import { CctpTxEvidenceShape } from '../type-guards.js';
55
import { defineInertInvitation } from '../utils/zoe.js';
66
import { prepareOperatorKit } from './operator-kit.js';
77

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

+20-8
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
import { AssetKind } from '@agoric/ertp';
22
import { assertAllDefined, makeTracer } from '@agoric/internal';
33
import { observeIteration, subscribeEach } from '@agoric/notifier';
4-
import { withOrchestration } from '@agoric/orchestration';
4+
import {
5+
OrchestrationPowersShape,
6+
withOrchestration,
7+
} from '@agoric/orchestration';
58
import { provideSingleton } from '@agoric/zoe/src/contractSupport/durability.js';
69
import { prepareRecorderKitMakers } from '@agoric/zoe/src/contractSupport/recorder.js';
10+
import { M } from '@endo/patterns';
711
import { prepareAdvancer } from './exos/advancer.js';
812
import { prepareLiquidityPoolKit } from './exos/liquidity-pool.js';
913
import { prepareSettler } from './exos/settler.js';
1014
import { prepareStatusManager } from './exos/status-manager.js';
1115
import { prepareTransactionFeedKit } from './exos/transaction-feed.js';
1216
import { defineInertInvitation } from './utils/zoe.js';
13-
import { FastUSDCTermsShape } from './type-guards.js';
17+
import { FastUSDCTermsShape, FeeConfigShape } from './type-guards.js';
1418

1519
const trace = makeTracer('FastUsdc');
1620

@@ -19,25 +23,33 @@ const trace = makeTracer('FastUsdc');
1923
* @import {OrchestrationPowers, OrchestrationTools} from '@agoric/orchestration/src/utils/start-helper.js';
2024
* @import {Zone} from '@agoric/zone';
2125
* @import {OperatorKit} from './exos/operator-kit.js';
22-
* @import {CctpTxEvidence} from './types.js';
26+
* @import {CctpTxEvidence, FeeConfig} from './types.js';
2327
*/
2428

2529
/**
2630
* @typedef {{
27-
* poolFee: Amount<'nat'>;
28-
* contractFee: Amount<'nat'>;
2931
* usdcDenom: Denom;
3032
* }} FastUsdcTerms
3133
*/
34+
35+
/** @type {ContractMeta<typeof start>} */
3236
export const meta = {
37+
// @ts-expect-error TypedPattern not recognized as record
3338
customTermsShape: FastUSDCTermsShape,
39+
privateArgsShape: {
40+
// @ts-expect-error TypedPattern not recognized as record
41+
...OrchestrationPowersShape,
42+
feeConfig: FeeConfigShape,
43+
marshaller: M.remotable(),
44+
},
3445
};
3546
harden(meta);
3647

3748
/**
3849
* @param {ZCF<FastUsdcTerms>} zcf
3950
* @param {OrchestrationPowers & {
4051
* marshaller: Marshaller;
52+
* feeConfig: FeeConfig;
4153
* }} privateArgs
4254
* @param {Zone} zone
4355
* @param {OrchestrationTools} tools
@@ -47,17 +59,17 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
4759
const terms = zcf.getTerms();
4860
assert('USDC' in terms.brands, 'no USDC brand');
4961
assert('usdcDenom' in terms, 'no usdcDenom');
50-
62+
const { feeConfig, marshaller } = privateArgs;
5163
const { makeRecorderKit } = prepareRecorderKitMakers(
5264
zone.mapStore('vstorage'),
53-
privateArgs.marshaller,
65+
marshaller,
5466
);
55-
5667
const statusManager = prepareStatusManager(zone);
5768
const makeSettler = prepareSettler(zone, { statusManager });
5869
const { chainHub, vowTools } = tools;
5970
const makeAdvancer = prepareAdvancer(zone, {
6071
chainHub,
72+
feeConfig,
6173
log: trace,
6274
usdc: harden({
6375
brand: terms.brands.USDC,

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

+7-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Fail } from '@endo/errors';
33
import { E } from '@endo/far';
44
import { makeMarshal } from '@endo/marshal';
55
import { M } from '@endo/patterns';
6-
import { FastUSDCTermsShape } from './type-guards.js';
6+
import { FastUSDCTermsShape, FeeConfigShape } from './type-guards.js';
77
import { fromExternalConfig } from './utils/config-marshal.js';
88

99
/**
@@ -15,6 +15,7 @@ import { fromExternalConfig } from './utils/config-marshal.js';
1515
* @import {BootstrapManifest} from '@agoric/vats/src/core/lib-boot.js'
1616
* @import {LegibleCapData} from './utils/config-marshal.js'
1717
* @import {FastUsdcSF, FastUsdcTerms} from './fast-usdc.contract.js'
18+
* @import {FeeConfig} from './types.js'
1819
*/
1920

2021
const trace = makeTracer('FUSD-Start', true);
@@ -25,12 +26,14 @@ const contractName = 'fastUsdc';
2526
* @typedef {{
2627
* terms: FastUsdcTerms;
2728
* oracles: Record<string, string>;
29+
* feeConfig: FeeConfig;
2830
* }} FastUSDCConfig
2931
*/
3032
/** @type {TypedPattern<FastUSDCConfig>} */
3133
export const FastUSDCConfigShape = M.splitRecord({
3234
terms: FastUSDCTermsShape,
3335
oracles: M.recordOf(M.string(), M.string()),
36+
feeConfig: FeeConfigShape,
3437
});
3538

3639
/**
@@ -128,12 +131,13 @@ export const startFastUSDC = async (
128131
USDC: await E(USDCissuer).getBrand(),
129132
});
130133

131-
const { terms, oracles } = fromExternalConfig(
134+
const { terms, oracles, feeConfig } = fromExternalConfig(
132135
config?.options, // just in case config is missing somehow
133136
brands,
134137
FastUSDCConfigShape,
135138
);
136139
trace('using terms', terms);
140+
trace('using fee config', feeConfig);
137141

138142
trace('look up oracle deposit facets');
139143
const oracleDepositFacets = await deeplyFulfilledObject(
@@ -159,6 +163,7 @@ export const startFastUSDC = async (
159163
const privateArgs = await deeplyFulfilledObject(
160164
harden({
161165
agoricNames,
166+
feeConfig,
162167
localchain,
163168
orchestrationService: cosmosInterchainService,
164169
storageNode,

0 commit comments

Comments
 (0)