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(amm): support maxOut optional arg to swap #5125

Merged
merged 9 commits into from
Apr 20, 2022
Merged

Conversation

dtribble
Copy link
Member

@dtribble dtribble commented Apr 16, 2022

refs: #4856

Description

Implements the new API in terms of the existing underlying implementation.

  • Move swap implementation into shared swap.js
  • Implement maxOut using existing pricing operations
  • implement swapOut in terms of swapIn with maxOut
  • remove now-unneeded code in pool implementations
  • make allocateGainsAndLosses API the same for single and double pools

Security Considerations

N/A

Documentation Considerations

Need to deprecate the old API and add the new argument to Swap.

Testing Considerations

SwapOut is defined in terms of the new API, so it gets lot of testing now. Additional specific cases should be added for which the want is not the same as the maxOut.

Test cases from the liquidation branch

@dtribble dtribble requested a review from Chris-Hibbert April 16, 2022 03:52
@dtribble dtribble requested a review from turadg as a code owner April 16, 2022 03:52
Copy link
Member

@turadg turadg left a comment

Choose a reason for hiding this comment

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

I think the test failure is legit. Debt is empty in this block:

  // An emergency liquidation got less than full value
  const debtAfterLiquidation = await E(vault).getCurrentDebt();

);

// Three (!) awaits coming here. We can't use Promise.all because
// Multiple awaits coming here. We can't use Promise.all because
Copy link
Member

Choose a reason for hiding this comment

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

just one now, right?

Copy link
Member Author

Choose a reason for hiding this comment

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

fixed

packages/run-protocol/src/vpool-xyk-amm/swap.js Outdated Show resolved Hide resolved
@@ -193,6 +193,7 @@ const start = async (zcf, privateArgs) => {
}

const pool = isSecondary(brandOut) ? getPool(brandOut) : getPool(brandIn);
// @ts-expect-error cast
Copy link
Member

Choose a reason for hiding this comment

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

@Chris-Hibbert is it safe to cast to DoublePoolInternalFacet here?

Copy link
Contributor

Choose a reason for hiding this comment

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

Lines 184-193 are the doublePool case. This is handling a single pool, so the renaming of VPoolInternalFacet to DoublePoolInternalFacet looks like a mistake. This correctly returns VpoolWrapper.

Copy link
Member

Choose a reason for hiding this comment

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

renaming of VPoolInternalFacet to DoublePoolInternalFacet looks like a mistake

All properties of VPoolInternalFacet specified double,

/**
 * @typedef {Object} VPoolInternalFacet - virtual pool for price quotes and trading
 * @property {GetDoublePoolSwapQuote} getPriceForInput
 * @property {GetDoublePoolSwapQuote} getPriceForOutput
 * @property {(ZCFSeat, DoublePoolSwapResult) => string} allocateGainsAndLosses
 */

Is there a type for VPoolInternalFacet that doesn't presume pool type?

Copy link
Member Author

Choose a reason for hiding this comment

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

As far as I can tell, no. So the types are not generic here, even though the code kinda is.

Copy link
Member Author

Choose a reason for hiding this comment

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

It looks like someone made the type a union type, so it's now actually accurate.

@@ -193,6 +193,7 @@ const start = async (zcf, privateArgs) => {
}

const pool = isSecondary(brandOut) ? getPool(brandOut) : getPool(brandIn);
// @ts-expect-error cast
Copy link
Contributor

Choose a reason for hiding this comment

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

Lines 184-193 are the doublePool case. This is handling a single pool, so the renaming of VPoolInternalFacet to DoublePoolInternalFacet looks like a mistake. This correctly returns VpoolWrapper.

AmountMath.coerce(amountOut.brand, maxOut);
prices = pool.getPriceForOutput(amountIn, maxOut);
}
if (!prices || !AmountMath.isGTE(prices.swapperGets, maxOut)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think there are scenarios where current calls to swapOut() would fail to trade, and this code would fall through to getPriceForInput() and sell anyway. One I found is if there is insufficient liquidity to satisfy the swapOut. In that case, I think the failure to trade is the correct outcome, even though that's not what you want in the liquidation case.

I would also like to see reasonably thorough testing of the swapIn(amountIn, { maxOut }) cases.

Copy link
Member Author

@dtribble dtribble Apr 18, 2022

Choose a reason for hiding this comment

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

Since it also passes the want.Out as a want.Out for the swapIn, I believe in all those cases it should properly fail to reallocate because the want is not satisfied.

@dtribble dtribble force-pushed the 4856_simple_amm_api branch from cad92bb to e1688f5 Compare April 19, 2022 19:07
@dtribble dtribble requested a review from Chris-Hibbert April 19, 2022 19:18
@dtribble
Copy link
Member Author

@dtribble dtribble force-pushed the 4856_simple_amm_api branch from 2def151 to 1aa1371 Compare April 19, 2022 21:37
@Chris-Hibbert
Copy link
Contributor

switching between getPriceForInput and getPriceForOutput when the user calls either swapIn or swapOut breaks the assumptions laid out in the doc.

The first consequence I can see is that the way fees are calculated is supposed to depend on whether they called swapIn or swapOut ("The pool fee is charged against the side not specified by the user" assumes that swapIn specifies the input price as base, and swapOut specifies the output price as the base.) In some cases this means the pool fee will be charged in the opposite currency.

That's probably defensible, but we haven't done so yet.

I'm not sure those are the only consequences. I am convinced that offer safety constraints are not broken by this approach. When the user specifies amountOut, there won't be a transaction unless they get at least that much. If they specify stopAfter instead, they don't get offer safety.

You suggested that we could change to an approach where we charge a flat fee on top of the transaction and allocate it to pool and protocol fees separately from calculating prices. That would also be defensible, but that's not what this change does.

dtribble and others added 8 commits April 19, 2022 15:47
* Move swap implementation into shared `swap.js`
* Implement `maxOut` using existing pricing operations
* implement `swapOut` in terms of `swapIn` with `maxOut`
* remove now-unneeded code in pool implementations
* make `allocateGainsAndLosses` API the same for single and double pools
* remove fallback `swapOut` since it's not needed anymore
* suppress problematic typescript warnings
* to preclude any difference from documented behavior fro `swapOut`
  keep using the direct implementation for now
@dtribble dtribble force-pushed the 4856_simple_amm_api branch from 2b33b8b to 83a84f6 Compare April 19, 2022 22:47
Copy link
Contributor

@Chris-Hibbert Chris-Hibbert left a comment

Choose a reason for hiding this comment

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

Thanks for listening to my concerns.

This is good.


/**
* @param {ZCF} zcf
* @param {(brandIn: Brand, brandOut: Brand) => VPoolWrapper<unknown>} provideVPool
* @param {(brandIn: Brand, brandOut: Brand) => VPoolWrapper<DoublePoolInternalFacet>} provideVPool
Copy link
Contributor

Choose a reason for hiding this comment

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

provideVPool can provide either singlePool or doublePool, though no one should care. I'd make it VPoolWrapper<unknown>, which allows future extensions and declares that we don't care.

@dtribble dtribble added the automerge:squash Automatically squash merge label Apr 19, 2022
@mergify mergify bot merged commit 54151ac into master Apr 20, 2022
@mergify mergify bot deleted the 4856_simple_amm_api branch April 20, 2022 00:07
@dtribble dtribble mentioned this pull request Apr 22, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
automerge:squash Automatically squash merge
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants