Skip to content

Commit

Permalink
feat: fusdc advancer with fees (#10494)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
mergify[bot] authored Nov 18, 2024
2 parents 5a4fc01 + 3771e7f commit 7390224
Show file tree
Hide file tree
Showing 29 changed files with 461 additions and 104 deletions.
41 changes: 31 additions & 10 deletions packages/builders/scripts/fast-usdc/init-fast-usdc.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
getManifestForFastUSDC,
} from '@agoric/fast-usdc/src/fast-usdc.start.js';
import { toExternalConfig } from '@agoric/fast-usdc/src/utils/config-marshal.js';
import { objectMap } from '@agoric/internal';
import {
multiplyBy,
parseRatio,
Expand All @@ -22,16 +21,26 @@ import { parseArgs } from 'node:util';

/** @type {ParseArgsConfig['options']} */
const options = {
contractFee: { type: 'string', default: '0.01' },
poolFee: { type: 'string', default: '0.01' },
flatFee: { type: 'string', default: '0.01' },
variableRate: { type: 'string', default: '0.01' },
maxVariableFee: { type: 'string', default: '5' },
contractRate: { type: 'string', default: '0.2' },
oracle: { type: 'string', multiple: true },
usdcDenom: {
type: 'string',
default:
'ibc/FE98AAD68F02F03565E9FA39A5E627946699B2B07115889ED812D8BA639576A9',
},
};
const oraclesRequiredUsage = 'use --oracle name:address ...';
/**
* @typedef {{
* contractFee: string;
* poolFee: string;
* flatFee: string;
* variableRate: string;
* maxVariableFee: string;
* contractRate: string;
* oracle?: string[];
* usdcDenom: string;
* }} FastUSDCOpts
*/

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

const parseOracleArgs = () => {
Expand All @@ -88,15 +97,27 @@ export default async (homeP, endowments) => {
);
};

/** @param {string} numeral */
const toAmount = numeral => multiplyBy(unit, parseRatio(numeral, USDC));
/** @param {string} numeral */
const toRatio = numeral => parseRatio(numeral, USDC);
const parseFeeConfigArgs = () => {
const { flatFee, variableRate, maxVariableFee, contractRate } = fees;
return {
flat: toAmount(flatFee),
variableRate: toRatio(variableRate),
maxVariable: toAmount(maxVariableFee),
contractRate: toRatio(contractRate),
};
};

/** @type {FastUSDCConfig} */
const config = harden({
oracles: parseOracleArgs(),
terms: {
...objectMap(fees, numeral =>
multiplyBy(unit, parseRatio(numeral, USDC)),
),
usdcDenom: 'ibc/usdconagoric',
usdcDenom,
},
feeConfig: parseFeeConfigArgs(),
});

await writeCoreEval('start-fast-usdc', utils =>
Expand Down
17 changes: 17 additions & 0 deletions packages/builders/test/snapshots/orchestration-imports.test.js.md
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,23 @@ Generated by [AVA](https://avajs.dev).
},
],
},
OrchestrationPowersShape: {
agoricNames: Object @match:kind {
payload: 'remotable',
},
localchain: Object @match:kind {
payload: 'remotable',
},
orchestrationService: Object @match:kind {
payload: 'remotable',
},
storageNode: Object @match:kind {
payload: 'remotable',
},
timerService: Object @match:kind {
payload: 'remotable',
},
},
OutboundConnectionHandlerI: Object @guard:interfaceGuard {
payload: {
defaultGuards: undefined,
Expand Down
Binary file modified packages/builders/test/snapshots/orchestration-imports.test.js.snap
Binary file not shown.
18 changes: 12 additions & 6 deletions packages/fast-usdc/src/exos/advancer.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import { VowShape } from '@agoric/vow';
import { q } from '@endo/errors';
import { E } from '@endo/far';
import { M } from '@endo/patterns';
import { CctpTxEvidenceShape, EudParamShape } from '../typeGuards.js';
import { CctpTxEvidenceShape, EudParamShape } from '../type-guards.js';
import { addressTools } from '../utils/address.js';
import { makeFeeTools } from '../utils/fees.js';

const { isGTE } = AmountMath;

Expand All @@ -17,7 +18,7 @@ const { isGTE } = AmountMath;
* @import {ChainAddress, ChainHub, Denom, DenomAmount, OrchestrationAccount} from '@agoric/orchestration';
* @import {VowTools} from '@agoric/vow';
* @import {Zone} from '@agoric/zone';
* @import {CctpTxEvidence, LogFn} from '../types.js';
* @import {CctpTxEvidence, FeeConfig, LogFn} from '../types.js';
* @import {StatusManager} from './status-manager.js';
*/

Expand All @@ -34,6 +35,7 @@ const { isGTE } = AmountMath;
/**
* @typedef {{
* chainHub: ChainHub;
* feeConfig: FeeConfig;
* log: LogFn;
* statusManager: StatusManager;
* usdc: { brand: Brand<'nat'>; denom: Denom; };
Expand Down Expand Up @@ -75,15 +77,16 @@ const AdvancerKitI = harden({
*/
export const prepareAdvancerKit = (
zone,
{ chainHub, log, statusManager, usdc, vowTools: { watch, when } },
{ chainHub, feeConfig, log, statusManager, usdc, vowTools: { watch, when } },
) => {
assertAllDefined({
chainHub,
feeConfig,
statusManager,
watch,
when,
});

const feeTools = makeFeeTools(feeConfig);
/** @param {bigint} value */
const toAmount = value => AmountMath.make(usdc.brand, value);

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

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

try {
const payment = await assetManagerFacet.borrow(requestedAmount);
const payment = await assetManagerFacet.borrow(advanceAmount);
const depositV = E(poolAccount).deposit(payment);
void watch(depositV, this.facets.depositHandler, {
destination,
Expand Down
2 changes: 1 addition & 1 deletion packages/fast-usdc/src/exos/operator-kit.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { makeTracer } from '@agoric/internal';
import { Fail } from '@endo/errors';
import { M } from '@endo/patterns';
import { CctpTxEvidenceShape } from '../typeGuards.js';
import { CctpTxEvidenceShape } from '../type-guards.js';

const trace = makeTracer('TxOperator');

Expand Down
2 changes: 1 addition & 1 deletion packages/fast-usdc/src/exos/status-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { M } from '@endo/patterns';
import { makeError, q } from '@endo/errors';

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

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/fast-usdc/src/exos/transaction-feed.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { makeTracer } from '@agoric/internal';
import { prepareDurablePublishKit } from '@agoric/notifier';
import { M } from '@endo/patterns';
import { CctpTxEvidenceShape } from '../typeGuards.js';
import { CctpTxEvidenceShape } from '../type-guards.js';
import { defineInertInvitation } from '../utils/zoe.js';
import { prepareOperatorKit } from './operator-kit.js';

Expand Down
28 changes: 20 additions & 8 deletions packages/fast-usdc/src/fast-usdc.contract.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import { AssetKind } from '@agoric/ertp';
import { assertAllDefined, makeTracer } from '@agoric/internal';
import { observeIteration, subscribeEach } from '@agoric/notifier';
import { withOrchestration } from '@agoric/orchestration';
import {
OrchestrationPowersShape,
withOrchestration,
} from '@agoric/orchestration';
import { provideSingleton } from '@agoric/zoe/src/contractSupport/durability.js';
import { prepareRecorderKitMakers } from '@agoric/zoe/src/contractSupport/recorder.js';
import { M } from '@endo/patterns';
import { prepareAdvancer } from './exos/advancer.js';
import { prepareLiquidityPoolKit } from './exos/liquidity-pool.js';
import { prepareSettler } from './exos/settler.js';
import { prepareStatusManager } from './exos/status-manager.js';
import { prepareTransactionFeedKit } from './exos/transaction-feed.js';
import { defineInertInvitation } from './utils/zoe.js';
import { FastUSDCTermsShape } from './type-guards.js';
import { FastUSDCTermsShape, FeeConfigShape } from './type-guards.js';

const trace = makeTracer('FastUsdc');

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

/**
* @typedef {{
* poolFee: Amount<'nat'>;
* contractFee: Amount<'nat'>;
* usdcDenom: Denom;
* }} FastUsdcTerms
*/

/** @type {ContractMeta<typeof start>} */
export const meta = {
// @ts-expect-error TypedPattern not recognized as record
customTermsShape: FastUSDCTermsShape,
privateArgsShape: {
// @ts-expect-error TypedPattern not recognized as record
...OrchestrationPowersShape,
feeConfig: FeeConfigShape,
marshaller: M.remotable(),
},
};
harden(meta);

/**
* @param {ZCF<FastUsdcTerms>} zcf
* @param {OrchestrationPowers & {
* marshaller: Marshaller;
* feeConfig: FeeConfig;
* }} privateArgs
* @param {Zone} zone
* @param {OrchestrationTools} tools
Expand All @@ -47,17 +59,17 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
const terms = zcf.getTerms();
assert('USDC' in terms.brands, 'no USDC brand');
assert('usdcDenom' in terms, 'no usdcDenom');

const { feeConfig, marshaller } = privateArgs;
const { makeRecorderKit } = prepareRecorderKitMakers(
zone.mapStore('vstorage'),
privateArgs.marshaller,
marshaller,
);

const statusManager = prepareStatusManager(zone);
const makeSettler = prepareSettler(zone, { statusManager });
const { chainHub, vowTools } = tools;
const makeAdvancer = prepareAdvancer(zone, {
chainHub,
feeConfig,
log: trace,
usdc: harden({
brand: terms.brands.USDC,
Expand Down
9 changes: 7 additions & 2 deletions packages/fast-usdc/src/fast-usdc.start.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Fail } from '@endo/errors';
import { E } from '@endo/far';
import { makeMarshal } from '@endo/marshal';
import { M } from '@endo/patterns';
import { FastUSDCTermsShape } from './type-guards.js';
import { FastUSDCTermsShape, FeeConfigShape } from './type-guards.js';
import { fromExternalConfig } from './utils/config-marshal.js';

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

const trace = makeTracer('FUSD-Start', true);
Expand All @@ -25,12 +26,14 @@ const contractName = 'fastUsdc';
* @typedef {{
* terms: FastUsdcTerms;
* oracles: Record<string, string>;
* feeConfig: FeeConfig;
* }} FastUSDCConfig
*/
/** @type {TypedPattern<FastUSDCConfig>} */
export const FastUSDCConfigShape = M.splitRecord({
terms: FastUSDCTermsShape,
oracles: M.recordOf(M.string(), M.string()),
feeConfig: FeeConfigShape,
});

/**
Expand Down Expand Up @@ -128,12 +131,13 @@ export const startFastUSDC = async (
USDC: await E(USDCissuer).getBrand(),
});

const { terms, oracles } = fromExternalConfig(
const { terms, oracles, feeConfig } = fromExternalConfig(
config?.options, // just in case config is missing somehow
brands,
FastUSDCConfigShape,
);
trace('using terms', terms);
trace('using fee config', feeConfig);

trace('look up oracle deposit facets');
const oracleDepositFacets = await deeplyFulfilledObject(
Expand All @@ -159,6 +163,7 @@ export const startFastUSDC = async (
const privateArgs = await deeplyFulfilledObject(
harden({
agoricNames,
feeConfig,
localchain,
orchestrationService: cosmosInterchainService,
storageNode,
Expand Down
Loading

0 comments on commit 7390224

Please sign in to comment.