Skip to content

Commit 06ca4dd

Browse files
committed
feat: advancer with fees
1 parent 99707ef commit 06ca4dd

10 files changed

+299
-21
lines changed

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

+11-5
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { E } from '@endo/far';
88
import { M } from '@endo/patterns';
99
import { CctpTxEvidenceShape, EudParamShape } from '../typeGuards.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(usdc.brand, 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/fast-usdc.contract.js

+7-8
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
import { AssetKind } from '@agoric/ertp';
2-
import { BrandShape } from '@agoric/ertp/src/typeGuards.js';
32
import { assertAllDefined, makeTracer } from '@agoric/internal';
43
import { observeIteration, subscribeEach } from '@agoric/notifier';
54
import { withOrchestration } from '@agoric/orchestration';
65
import { provideSingleton } from '@agoric/zoe/src/contractSupport/durability.js';
76
import { prepareRecorderKitMakers } from '@agoric/zoe/src/contractSupport/recorder.js';
8-
import { M } from '@endo/patterns';
7+
import { M, mustMatch } from '@endo/patterns';
98
import { prepareAdvancer } from './exos/advancer.js';
109
import { prepareLiquidityPoolKit } from './exos/liquidity-pool.js';
1110
import { prepareSettler } from './exos/settler.js';
1211
import { prepareStatusManager } from './exos/status-manager.js';
1312
import { prepareTransactionFeedKit } from './exos/transaction-feed.js';
1413
import { defineInertInvitation } from './utils/zoe.js';
14+
import { FeeConfigShape } from './typeGuards.js';
1515

1616
const trace = makeTracer('FastUsdc');
1717

@@ -20,21 +20,17 @@ const trace = makeTracer('FastUsdc');
2020
* @import {OrchestrationPowers, OrchestrationTools} from '@agoric/orchestration/src/utils/start-helper.js';
2121
* @import {Zone} from '@agoric/zone';
2222
* @import {OperatorKit} from './exos/operator-kit.js';
23-
* @import {CctpTxEvidence} from './types.js';
23+
* @import {CctpTxEvidence, FeeConfig} from './types.js';
2424
*/
2525

2626
/**
2727
* @typedef {{
28-
* poolFee: Amount<'nat'>;
29-
* contractFee: Amount<'nat'>;
3028
* usdcDenom: Denom;
3129
* }} FastUsdcTerms
3230
*/
33-
const NatAmountShape = { brand: BrandShape, value: M.nat() };
31+
3432
export const meta = {
3533
customTermsShape: {
36-
contractFee: NatAmountShape,
37-
poolFee: NatAmountShape,
3834
usdcDenom: M.string(),
3935
},
4036
};
@@ -44,6 +40,7 @@ harden(meta);
4440
* @param {ZCF<FastUsdcTerms>} zcf
4541
* @param {OrchestrationPowers & {
4642
* marshaller: Marshaller;
43+
* feeConfig: FeeConfig;
4744
* }} privateArgs
4845
* @param {Zone} zone
4946
* @param {OrchestrationTools} tools
@@ -53,6 +50,7 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
5350
const terms = zcf.getTerms();
5451
assert('USDC' in terms.brands, 'no USDC brand');
5552
assert('usdcDenom' in terms, 'no usdcDenom');
53+
mustMatch(privateArgs.feeConfig, FeeConfigShape, 'must provide feeConfig');
5654

5755
const { makeRecorderKit } = prepareRecorderKitMakers(
5856
zone.mapStore('vstorage'),
@@ -64,6 +62,7 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
6462
const { chainHub, vowTools } = tools;
6563
const makeAdvancer = prepareAdvancer(zone, {
6664
chainHub,
65+
feeConfig: privateArgs.feeConfig,
6766
log: trace,
6867
usdc: harden({
6968
brand: terms.brands.USDC,

packages/fast-usdc/src/typeGuards.js

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { M } from '@endo/patterns';
2+
import { BrandShape, RatioShape } from '@agoric/ertp';
23
import { PendingTxStatus } from './constants.js';
34

45
/**
56
* @import {TypedPattern} from '@agoric/internal';
6-
* @import {CctpTxEvidence, PendingTx} from './types.js';
7+
* @import {CctpTxEvidence, FeeConfig, PendingTx} from './types.js';
78
*/
89

910
/** @type {TypedPattern<string>} */
@@ -42,3 +43,13 @@ export const EudParamShape = {
4243
EUD: M.string(),
4344
};
4445
harden(EudParamShape);
46+
47+
const NatAmountShape = { brand: BrandShape, value: M.nat() };
48+
/** @type {TypedPattern<FeeConfig>} */
49+
export const FeeConfigShape = {
50+
flat: NatAmountShape,
51+
variableRate: RatioShape,
52+
maxVariable: NatAmountShape,
53+
contractRate: RatioShape,
54+
};
55+
harden(FeeConfigShape);

packages/fast-usdc/src/types.ts

+7
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,11 @@ export type PendingTxKey = `pendingTx:${string}`;
3535
/** internal key for `StatusManager` exo */
3636
export type SeenTxKey = `seenTx:${string}`;
3737

38+
export type FeeConfig = {
39+
flat: Amount<'nat'>;
40+
variableRate: Ratio;
41+
maxVariable: Amount<'nat'>;
42+
contractRate: Ratio;
43+
};
44+
3845
export type * from './constants.js';

packages/fast-usdc/src/utils/fees.js

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { AmountMath } from '@agoric/ertp';
2+
import { multiplyBy } from '@agoric/zoe/src/contractSupport/ratio.js';
3+
import { Fail } from '@endo/errors';
4+
import { mustMatch } from '@endo/patterns';
5+
import { FeeConfigShape } from '../typeGuards.js';
6+
7+
const { add, subtract, isEqual, isGTE } = AmountMath;
8+
9+
/**
10+
* @import {Amount, Brand} from '@agoric/ertp';
11+
* @import {FeeConfig} from '../types.js';
12+
*/
13+
14+
/**
15+
* @param {Brand} brand
16+
* @param {FeeConfig} feeConfig
17+
*/
18+
export const makeFeeTools = (brand, feeConfig) => {
19+
mustMatch(feeConfig, FeeConfigShape, 'Must provide feeConfig');
20+
const { flat, variableRate, maxVariable } = feeConfig;
21+
const emptyAmount = AmountMath.makeEmpty(brand);
22+
return harden({
23+
/**
24+
* Calculate the net amount to advance after withholding fees.
25+
*
26+
* @param {Amount<'nat'>} requested
27+
*/
28+
calculateAdvance(requested) {
29+
const fee = this.calculateAdvanceFee(requested);
30+
return subtract(requested, fee);
31+
},
32+
/**
33+
* Calculate the total fee to charge for the advance.
34+
*
35+
* @param {Amount<'nat'>} requested must be greater than 0
36+
*/
37+
calculateAdvanceFee(requested) {
38+
!isEqual(requested, emptyAmount) || Fail`Fee exceeds requested.`;
39+
const potentialVariable = multiplyBy(requested, variableRate);
40+
const variable = isGTE(potentialVariable, maxVariable)
41+
? maxVariable
42+
: potentialVariable;
43+
const fee = add(variable, flat);
44+
!isGTE(fee, requested) || Fail`Fee exceeds requested.`;
45+
return fee;
46+
},
47+
/**
48+
* Calculate the split of fees between pool and contract.
49+
*
50+
* @param {Amount<'nat'>} requested
51+
* @returns {{ Principal: Amount<'nat'>, PoolFee: Amount<'nat'>, ContractFee: Amount<'nat'> }} AmountKeywordRecord
52+
*/
53+
calculateSplit(requested) {
54+
const fee = this.calculateAdvanceFee(requested);
55+
const Principal = subtract(requested, fee);
56+
const ContractFee = multiplyBy(fee, feeConfig.contractRate);
57+
const PoolFee = subtract(fee, ContractFee);
58+
return harden({ Principal, PoolFee, ContractFee });
59+
},
60+
});
61+
};

packages/fast-usdc/test/exos/advancer.test.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ import { prepareStatusManager } from '../../src/exos/status-manager.js';
1313

1414
import { commonSetup } from '../supports.js';
1515
import { MockCctpTxEvidences } from '../fixtures.js';
16-
import { makeTestLogger, prepareMockOrchAccounts } from '../mocks.js';
16+
import {
17+
makeTestFeeConfig,
18+
makeTestLogger,
19+
prepareMockOrchAccounts,
20+
} from '../mocks.js';
1721

1822
const LOCAL_DENOM = `ibc/${denomHash({
1923
denom: 'uusdc',
@@ -46,8 +50,10 @@ const createTestExtensions = (t, common: CommonSetup) => {
4650
usdc,
4751
});
4852

53+
const feeConfig = makeTestFeeConfig(usdc);
4954
const makeAdvancer = prepareAdvancer(rootZone.subZone('advancer'), {
5055
chainHub,
56+
feeConfig,
5157
log,
5258
statusManager,
5359
usdc: harden({
@@ -90,6 +96,7 @@ const createTestExtensions = (t, common: CommonSetup) => {
9096
return {
9197
constants: {
9298
localDenom: LOCAL_DENOM,
99+
feeConfig,
93100
},
94101
helpers: {
95102
inspectLogs,
@@ -187,7 +194,7 @@ test('updates status to OBSERVED on insufficient pool funds', async t => {
187194

188195
t.deepEqual(inspectLogs(0), [
189196
'Insufficient pool funds',
190-
'Requested {"brand":"[Alleged: USDC brand]","value":"[200000000n]"} but only have {"brand":"[Alleged: USDC brand]","value":"[1n]"}',
197+
'Requested {"brand":"[Alleged: USDC brand]","value":"[195000000n]"} but only have {"brand":"[Alleged: USDC brand]","value":"[1n]"}',
191198
]);
192199
});
193200

packages/fast-usdc/test/fast-usdc.contract.test.ts

-4
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,6 @@ const startContract = async (
5151
installation,
5252
{ USDC: usdc.issuer },
5353
{
54-
poolFee: usdc.make(1n),
55-
contractFee: usdc.make(1n),
5654
usdcDenom: 'ibc/usdconagoric',
5755
},
5856
commonPrivateArgs,
@@ -243,8 +241,6 @@ test('baggage', async t => {
243241
installation,
244242
{ USDC: usdc.issuer },
245243
{
246-
poolFee: usdc.make(1n),
247-
contractFee: usdc.make(1n),
248244
usdcDenom: 'ibc/usdconagoric',
249245
},
250246
commonPrivateArgs,

packages/fast-usdc/test/mocks.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ import type {
66
} from '@agoric/orchestration';
77
import type { Zone } from '@agoric/zone';
88
import type { VowTools } from '@agoric/vow';
9-
import type { LogFn } from '../src/types.js';
9+
import { makeRatio } from '@agoric/zoe/src/contractSupport/ratio.js';
10+
import type {
11+
AmountUtils,
12+
withAmountUtils,
13+
} from '@agoric/zoe/tools/test-utils.js';
14+
import type { FeeConfig, LogFn } from '../src/types.js';
1015

1116
export const prepareMockOrchAccounts = (
1217
zone: Zone,
@@ -59,3 +64,11 @@ export const makeTestLogger = (logger: LogFn) => {
5964
};
6065

6166
export type TestLogger = ReturnType<typeof makeTestLogger>;
67+
68+
export const makeTestFeeConfig = (usdc: Omit<AmountUtils, 'mint'>): FeeConfig =>
69+
harden({
70+
flat: usdc.units(1),
71+
variableRate: makeRatio(2n, usdc.brand, 100n, usdc.brand),
72+
maxVariable: usdc.units(100),
73+
contractRate: makeRatio(2n, usdc.brand, 10n, usdc.brand),
74+
});

packages/fast-usdc/test/supports.ts

+2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { makeHeapZone, type Zone } from '@agoric/zone';
2828
import { makeDurableZone } from '@agoric/zone/durable.js';
2929
import { E } from '@endo/far';
3030
import type { ExecutionContext } from 'ava';
31+
import { makeTestFeeConfig } from './mocks.js';
3132

3233
export {
3334
makeFakeLocalchainBridge,
@@ -184,6 +185,7 @@ export const commonSetup = async (t: ExecutionContext<any>) => {
184185
storageNode: storage.rootNode,
185186
marshaller,
186187
timerService: timer,
188+
feeConfig: makeTestFeeConfig(usdc),
187189
},
188190
facadeServices: {
189191
agoricNames,

0 commit comments

Comments
 (0)