Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: fusdc advancer with fees #10494

Merged
merged 4 commits into from
Nov 18, 2024
Merged

feat: fusdc advancer with fees #10494

merged 4 commits into from
Nov 18, 2024

Conversation

0xpatrickdev
Copy link
Member

@0xpatrickdev 0xpatrickdev commented Nov 15, 2024

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

Copy link

cloudflare-workers-and-pages bot commented Nov 15, 2024

Deploying agoric-sdk with  Cloudflare Pages  Cloudflare Pages

Latest commit: 3771e7f
Status: ✅  Deploy successful!
Preview URL: https://d3aaff76.agoric-sdk.pages.dev
Branch Preview URL: https://pc-fusdc-fees.agoric-sdk.pages.dev

View logs

@0xpatrickdev 0xpatrickdev marked this pull request as ready for review November 15, 2024 17:28
@0xpatrickdev 0xpatrickdev requested a review from a team as a code owner November 15, 2024 17:28
Base automatically changed from pc/fusdc-advancer to master November 15, 2024 18:07
Copy link

Base branch is changed to master. Please re-run the integration tests by adding 'force:integration' label.

export const meta = {
customTermsShape: {
contractFee: NatAmountShape,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah yes, they should never have been terms because they must be changeable

@@ -53,6 +50,7 @@ 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');
mustMatch(privateArgs.feeConfig, FeeConfigShape, 'must provide feeConfig');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please instead use meta.privateArgsShape

@@ -64,6 +62,7 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
const { chainHub, vowTools } = tools;
const makeAdvancer = prepareAdvancer(zone, {
chainHub,
feeConfig: privateArgs.feeConfig,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: cleaner to destructure in the fn params

* @param {Amount<'nat'>} requested must be greater than 0
*/
calculateAdvanceFee(requested) {
!isEqual(requested, emptyAmount) || Fail`Fee exceeds requested.`;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will error "Fee exceeds" if the brands don't match. The check should include brands. Consider instead a nonZeroAmount shape made using the limits param. Then this could handle both checks cleanly.

Suggested change
!isEqual(requested, emptyAmount) || Fail`Fee exceeds requested.`;
mustMatch(requested, nonZeroAmount);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good feedback. Just now I made NonZeroAmountShape = { brand: BrandShape, value: M.gte(1n) } and implemented mustMatch. I made it brand agnostic as the consuming services will have done this check already.

In light of Dan's feedback, I came to realize this check was vestigial and have removed it. Some iteration of this couldn't deal with zeros during math.

};
};

const feeToolsScenario = test.macro({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perfect use of macros

Copy link
Member

@dckc dckc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is how far I got before meetings...

Comment on lines 36 to 37
contractFee: NatAmountShape,
poolFee: NatAmountShape,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

taking these out of the terms is an unfortunate regression. I suppose I understand the motivation - they can change on restart / upgrade.

Plus, our clients can't do getTerms(). They can only read vstorage....

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes the ability to change fees is the main motivation here. We could consider putting initialFeeConfig in terms, but it seems superfluous to me.

@@ -53,6 +50,7 @@ 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');
mustMatch(privateArgs.feeConfig, FeeConfigShape, 'must provide feeConfig');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let Zoe do this: put FeeConfigShape and such in meta.privateArgsShape.

Comment on lines 50 to 53
flat: NatAmountShape,
variableRate: RatioShape,
maxVariable: NatAmountShape,
contractRate: RatioShape,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, we'd constrain the brand in all these. But we want this shape in meta.privateArgsShape, where we don't have the brand identity.

* @param {Amount<'nat'>} requested
*/
calculateAdvance(requested) {
const fee = this.calculateAdvanceFee(requested);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't use this except in exo's. (IOU pointer to docs)
Please define separate functions and then return a hardened record of them.

Or do const self = harden({ ... }); return self; and use self.calculateAdvanceFee. Or tools.

* @param {Amount<'nat'>} requested must be greater than 0
*/
calculateAdvanceFee(requested) {
!isEqual(requested, emptyAmount) || Fail`Fee exceeds requested.`;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
!isEqual(requested, emptyAmount) || Fail`Fee exceeds requested.`;
!isEmpty(requested) || Fail`Fee exceeds requested.`;

Why the special case, though?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL isEmpty, although it was right in front of my face, that's handy.

why the special case

You're right, no need for this. This is vestigial from when some iteration couldn't deal with zeros.

@@ -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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

likewise, add a @throws for calculateAdvance, please.

Comment on lines +138 to +139
// report `requestedAmount`, not `advancedAmount`... do we need to
// communicate net to `StatusManger` in case fees change in between?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest adding a test to answer this question -- a test to simulate a dashboard query on output of the StatusManager. It's possible that the dashboard requirements aren't sufficiently clear, but this would be a good form of review.

Copy link
Member Author

@0xpatrickdev 0xpatrickdev Nov 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

simulate a dashboard query

Agree, but we have a "Status Manager writes to vstorage" task scheduled for the future. We can comprehensively tackle this then.

P.S. - this particular question is more about settling with the same fee split we used during advance. I agree we should have a test for this, but the settler is not wired up to use the FeeConfig yet.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

scheduled for the future

yes; my review was rushed and I didn't manage to separate things in scope for this PR from other thinking-out-loud.

does a test.todo() seem cost-effective?

@@ -147,7 +153,7 @@ export const prepareAdvancerKit = (
}

try {
const payment = await assetManagerFacet.borrow(requestedAmount);
const payment = await assetManagerFacet.borrow(advanceAmount);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just below this, I see poolAccount. Its type is ERef<...>. Do we test the promise case? I don't think promises can be stored durably.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I expect poolAccount to be a vow or a remotable. The former can be stored durably.

Look for tests in #10497, or further in the future if scope is cut, as the contract doesn't currently create an LOA.

@@ -44,6 +40,7 @@ harden(meta);
* @param {ZCF<FastUsdcTerms>} zcf
* @param {OrchestrationPowers & {
* marshaller: Marshaller;
* feeConfig: FeeConfig;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add this to publicTopics to be sure vstorage clients can see them.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a future task "Write contract config to vstorage" where we can tackle this. I'd add a todo to the tasklist, but it's still a draft.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

Comment on lines 71 to 73
variableRate: makeRatio(2n, usdc.brand, 100n, usdc.brand),
maxVariable: usdc.units(100),
contractRate: makeRatio(2n, usdc.brand, 10n, usdc.brand),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The denominator brand defaults to the numerator brand.
And the denominator defaults to 100n.

Suggested change
variableRate: makeRatio(2n, usdc.brand, 100n, usdc.brand),
maxVariable: usdc.units(100),
contractRate: makeRatio(2n, usdc.brand, 10n, usdc.brand),
variableRate: makeRatio(2n, usdc.brand),
maxVariable: usdc.units(100),
contractRate: makeRatio(20n, usdc.brand),

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good tip, thanks

@0xpatrickdev 0xpatrickdev force-pushed the pc/fusdc-fees branch 4 times, most recently from 3ad5d52 to a1d931d Compare November 15, 2024 20:23
@0xpatrickdev 0xpatrickdev requested review from turadg and dckc November 15, 2024 22:20
Copy link
Member

@dckc dckc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup. let's do this.


/** @type {TypedPattern<string>} */
export const EvmHashShape = M.string({
stringLengthLimit: 66,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interesting... We don't have patterns for "starts with xyz" and the like, so I had only thought of string length limits as denial-of-service protection.

const NatAmountShape = { brand: BrandShape, value: M.nat() };
export const meta = {

export const meta = /** @type {ContractMeta<FastUsdcSF>} */ ({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how is this better than the existing convention?

Suggested change
export const meta = /** @type {ContractMeta<FastUsdcSF>} */ ({
/** @type {ContractMeta<typeof start>} */

calculateAdvanceFee(requested) {
const variable = min(multiplyBy(requested, variableRate), maxVariable);
const fee = add(variable, flat);
!isGTE(fee, requested) || Fail`Fee exceeds requested.`;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If fee == requested then isGTE will be true, so !isGTE will be false and the Fail will throw.

Suggested change
!isGTE(fee, requested) || Fail`Fee exceeds requested.`;
isGTE(requested, fee) || Fail`Fee exceeds requested.`;

Or,

Suggested change
!isGTE(fee, requested) || Fail`Fee exceeds requested.`;
!isGTE(fee, requested) || Fail`Request must exceed fees.`;

If the latter, updates the @throws too.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the error message and @throws

- useful for `privateArgsShape` in consuming contracts
- renames cosmos `orchestrationService`s powers from `OrchestrationPowers` -> `CosmosOrchestrationPowers`
@0xpatrickdev 0xpatrickdev added the automerge:rebase Automatically rebase updates, then merge label Nov 18, 2024
@mergify mergify bot merged commit 7390224 into master Nov 18, 2024
90 of 91 checks passed
@mergify mergify bot deleted the pc/fusdc-fees branch November 18, 2024 17:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
automerge:rebase Automatically rebase updates, then merge
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants