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: adding CoW helper MVP #121

Merged
merged 45 commits into from
Jul 23, 2024
Merged

feat: adding CoW helper MVP #121

merged 45 commits into from
Jul 23, 2024

Conversation

wei3erHase
Copy link
Member

@wei3erHase wei3erHase commented Jun 27, 2024

BCoWHelper status:

The BCoWHelper contract is designed to be view-called by solvers in order to fetch the relevant information that is needed to swap in a Pool, given a pricing vectore. The contract implements a CoW library, that supports only 2-token unweighted pools.

  • Helper inherits ICOWAMMPoolHelper interface from cow-amm
  • Helper implements GetTradeableOrder from cow-amm
  • tokens method WILL revert with PoolDoesNotExist if:
    • Pool is not a children of the helper’s factory
    • Pool is not finalized (this one reverts with BPool_PoolNotFinalized)
    • Pool has more than 2 tokens
    • Pool tokens have different weights
  • order method internally calls tokens (same reverting cases)
  • order returns the commit pre-interaction and the order calculated in GetTradeableOrder library
  • factory aims to the BCoWFactory and reads the AppData from it

Considerations in limited compatibility:

  • The unweighted nature of GetTradeableOrder is definite, the calculation doesn’t fall true for pools with different token weights, a new calculation needs to be done in order to support them (perhaps GetTradeableWeightedOrder?).

  • The 2-token nature, on the other hand, is a workable limitation. The current design of the Helper just reverts if the Pool has more than 2 tokens. When we consider a 3-token pool (unweighted), we can imagine the interaction being: a 3-dimensional pricing vector is sent to the Helper, and the Helper will decide which of these 2 tokens should be traded (and build the GPv2Order + commit pre-interaction). This, at first sight is very difficult to implement, considering:

    • How do we create a 3-dimensional pricing vector?
    • How does the Helper know which tokens are best to trade?
    • How do we input a 3-dimensional array into the library?

    There is a workaround to this approach, and is making the helper accept 3-dimensional pricing vectors, but ONLY if 2 dimensions are non-zero. In that case, a X/Y/Z pool can be queried for [X,Y,0], [X,0,Z], and [0,Y,Z] separately, and reuse the 2-dimensional library that CoW provides.

  • Pool level, there is no 2-token restriction for BCoWPools, Pools with up to 8 tokens can be created, and the BCoW flow supports swaps from any token to any other token in the Pool. When swapping in the Pool, the only relevant tokens are the token IN and OUT, no other token bounded to the pool has any effect on the swap, or in other way: during swaps the Pool behaves always as 2-token. So surplus measurements shouldn’t be affected by supporting multiple-token pools.

Summary:

  • Helper is currently NOT supporting +2-token pools.
  • Helper is currently NOT supporting weighted pools.
  • Helper COULD support +2-token pools (iff the pricing vector has 2 non-zero values)
  • Helper COULD NOT support weighted pools (requires new calcs).
  • BCoWPool DOES support +2-token pools.
  • BCoWPool DOES support weighted pools.

Comments:

  • Adding a limitation to the BCoWFactory to allow only deployments of 2-token unweighted pools breaks the future compatibility of a newly developed Helper. In the event that the new calcs (to support weighted pools) are written, and some +2-token interaction is designed, the Factory would have to be redeployed in order to remove the check. And since supporting deployments of such Pools don’t cause any security issue, we recommend that the BCoWFactory is deployed as is (supporting +2-token weighted pools), that we keep the incompatibility checks at the Helper level, so that a new Helper can be added later without having to modify the Factory.

@wei3erHase wei3erHase marked this pull request as ready for review July 11, 2024 12:17
@wei3erHase wei3erHase changed the title [DRAFT] feat: helper PoC feat: adding CoW helper MVP Jul 11, 2024
Copy link

@fedgiac fedgiac left a comment

Choose a reason for hiding this comment

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

Everything in the PR description is reasonable and the code is a good implementation for the helper (with one caveat, more on this later). The considerations/limitations are as expected. More in detail:

we recommend that the BCoWFactory is deployed as is (supporting +2-token weighted pools), that we keep the incompatibility checks at the Helper level

The helper is indeed disposable and it's good to just deploy it as-is now, then replace it once we build up support for more tokens/weights (which is tricky for the reasons you mentioned).

About the caveat: the sell/buy price is computed with GetTradeableOrder.getTradeableOrder. This library works for a constant-product AMM like it's the case for the Balancer AMM as supported here. Still, the generated trade is (up to rounding) tight, in the sense that decreasing minimally the buy amount should cause the order to be invalid (manifested as an invalid signature, reverting with BPool_TokenAmountOutBelowMinOut). My concern is that, because of rounding, the order returned by GetTradeableOrder.getTradeableOrder might be slightly below the threshold for calcOutGivenIn and cause the helper to return a trade that isn't valid. The forked test right now works, but there could be variations of the input values (balances, weights (even if the same)) that may cause it to fail.
Concretely, this means that the CoW Protocol back-end would generate an invalid order and the AMM might be ignored for the batches where rounding causes the creation of a reverting trade.
A fix for this would be to replicate the GetTradeableOrder logic for the sell amount but use calcOutGivenIn for the sell amount. This should return the same result (up to rounding) but be unaffected by the issue above.

All other review comments are minor.

test/integration/BCoWHelper.t.sol Outdated Show resolved Hide resolved
test/integration/BCoWHelper.t.sol Outdated Show resolved Hide resolved
Copy link

@fedgiac fedgiac left a comment

Choose a reason for hiding this comment

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

Under the assumption that #154 is merged, the code looks good for being used by the CoW Protocol back end.

* fix: typos in comments

* fix: overwriting sellAmount in order to avoid rounding issues

* feat: improving the test case to cover both cases
Copy link

@0xteddybear 0xteddybear left a comment

Choose a reason for hiding this comment

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

looking good, I'd move more checks onto the unit tests and clean up the integrations so the valid scenario they are executing is easier to follow

package.json Show resolved Hide resolved
using GPv2Order for GPv2Order.Data;

/// @notice The app data used by this helper's factory.
bytes32 public immutable APP_DATA;

Choose a reason for hiding this comment

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

this is not in any interface, IIRC we intended to have every public method available in an interface as part of our internal style guide (although it's disabled in the natspec smells config, since we didn't want to deal with some methods on BMath and BNum)

Copy link
Member Author

Choose a reason for hiding this comment

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

hmmm we could make it internal tbh, this contract is supposed to be called only by the interface related methods, as it's al off-chain helper for solvers

src/contracts/BCoWHelper.sol Outdated Show resolved Hide resolved
src/contracts/BCoWHelper.sol Outdated Show resolved Hide resolved
src/contracts/BCoWHelper.sol Outdated Show resolved Hide resolved
}

// NOTE: reverting test, weighted pools are not supported
function testWeightedOrder() public {

Choose a reason for hiding this comment

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

I believe a unit test is enough for this

Copy link
Member Author

Choose a reason for hiding this comment

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

this one was chosen to maintain the shape in case the feature gets added later, the unit test of the helper reverting is there, and the integration perspective we keep it here, in the future, another helper will not revert on weighted pools, and this test can be reimplemented not to revert

Choose a reason for hiding this comment

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

... if this is meant to stay then wdyt about removing the commented code below?

test/integration/BCoWHelper.t.sol Outdated Show resolved Hide resolved
test/integration/BCoWHelper.t.sol Outdated Show resolved Hide resolved
test/integration/BCoWHelper.t.sol Outdated Show resolved Hide resolved
test/integration/BCoWHelper.t.sol Outdated Show resolved Hide resolved
│ ├── it should return a commit pre-interaction
│ ├── it should return an empty post-interaction
│ └── it should return a valid signature
└── given a price vector
Copy link
Member Author

Choose a reason for hiding this comment

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

cc-ing this to @drgorillamd, i'm breaking standard with given to not-branch but just make a test with a different focus, in this one in particular is fuzzing the balances and price vectors, it could have been done in the same scope as when the pool is supported, but think that separation of affairs is good there, since the code is looking at other things, wdyt?

Copy link
Member

Choose a reason for hiding this comment

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

given ftw :D cc @gas1cent ?

Copy link
Member

Choose a reason for hiding this comment

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

OK FINE

}

// NOTE: reverting test, weighted pools are not supported
function testWeightedOrder() public {

Choose a reason for hiding this comment

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

... if this is meant to stay then wdyt about removing the commented code below?

_executeHelperOrder(pool);

uint256 postSpotPrice = pool.getSpotPriceSansFee(address(WETH), address(DAI));
assertEq(postSpotPrice, 1_052_631_578_947_368);

Choose a reason for hiding this comment

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

I'd do a ranged assert with a computed amount, similar to how you did with order.{buy,sell}Amount below


contract MockBCoWHelper is BCoWHelper, Test {
// NOTE: manually added methods (internal immutable exposers not supported in smock)
function call__APP_DATA() external view returns (bytes32) {

Choose a reason for hiding this comment

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

why not make _APP_DATA public?

Copy link
Member Author

Choose a reason for hiding this comment

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

is not required in the interface that CoWSwap provides ICOWAMMPoolHelper, they only request to expose factory, tokens and order

test/unit/BCoWHelper.t.sol Outdated Show resolved Hide resolved
assertEq(keccak256(validSig), keccak256(sig));
}

function test_OrderGivenAPriceVector(uint256 priceSkewness, uint256 balanceToken0, uint256 balanceToken1) external {

Choose a reason for hiding this comment

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

I don't really follow what are the 5000, 10_000and15_000`, much less why we need them. Do you think it would be clearer if they had names? or some comments?

afaict we will skew the price by a particular factor from current spot price in order to allow for naturally ocurring slippage, is that correct?

Copy link
Member Author

Choose a reason for hiding this comment

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

making it uint256 base = 1e18 and the bounds between 0.5 and 1.5

) = helper.order(address(pool), priceVector);

// it should return a valid pool order
assertEq(order_.receiver, GPv2Order.RECEIVER_SAME_AS_OWNER);

Choose a reason for hiding this comment

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

I think it's worth it to precompute some values to make sure we're not passing parameters to the library in the inverse order (or other silly-but-consequential bugs like that). This won't block merging since we're already doing it for the integrations, but is still desireable. (related to https://github.com/defi-wonderland/balancer-v1-amm/pull/121/files#r1678466826)

// it should return a valid pool order
(GPv2Order.Data memory ammOrder,,,) = helper.order(address(pool), prices);

assertEq(address(ammOrder.buyToken), priceSkewness > 10_000 ? tokens[0] : tokens[1]);

Choose a reason for hiding this comment

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

are we sure these are not different branches?

Copy link
Member Author

Choose a reason for hiding this comment

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

wdym by "not different branches"?
i think is more of a different approach but same branch, test_OrderWhenThePoolIsSupported doesn't check the amounts and just use the default price vector (1:1.05), while test_OrderGivenAPriceVector fuzzes both balances and the skeweness but checks only that the pool can verify it, and that the token being sold is the correct one.

we could entirely fuzz test_OrderWhenThePoolIsSupported, but we'd be adding complexity to that test instead of maintaining the "valid scenario" approach, dunno wdyt of that

Choose a reason for hiding this comment

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

I believe that to fully benefit from btt tests we should make the .tree fully match the branches available in the (code|domain). I've checked and they're effectively different branches: node_modules/cow-amm/src/libraries/GetTradeableOrder.sol:56

It was not obvious for me reviewing it, and it could have been easier to see had the information of when token0 reserve * denominator > when token1 reserve * numerator -> buyToken is token1 been in the .tree file. I believe this is a case where you set some 'funny values' to get to that branch. I consider this PR should include:

  • fully fuzzed tests to report on rounding errors (this one is ok for these purposes, we could also be purists and have one fully-fuzzed for each branch. Definetly not required.) This doesnt fit neatly into the .tree but I can live with that.
  • 'valid scenario' 'funny value'd unit tests that are described in .tree and walk every available branch. In this case I'd consider the library an implementaiton detail and walk their branches as well, as that's critical to ensure this helper works as intended. This includes:
    • one testcase forcing token0 to be buyToken
    • one testcase forcing token1 to be buyToken
    • Some assertions on src/contracts/BCoWHelper.sol:74, since that defines two branches as well. Since they are coupled to the above, I don't know if they'll be their own separate two testcases or extra asserts inside them, do whatever feels right.

tl;dr: fuzzing is ok as is, we should have tests manually walking each branch.

Copy link

@0xteddybear 0xteddybear left a comment

Choose a reason for hiding this comment

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

good stuff!
having btt tests walk all branches is highly desireable, the rest are nitpicks.

test/unit/BCoWHelper.tree Show resolved Hide resolved

// it should return a valid pool order
(GPv2Order.Data memory ammOrder,,,) = helper.order(address(pool), prices);

assertEq(address(ammOrder.buyToken), priceSkewness > 10_000 ? tokens[0] : tokens[1]);
assertEq(address(ammOrder.buyToken), priceSkewness > 1e18 ? tokens[0] : tokens[1]);

Choose a reason for hiding this comment

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

use the base variable

callData: abi.encodeWithSelector(IBCoWPool.commit.selector, orderCommitment)
});

return (order_, preInteractions, postInteractions, sig);

Choose a reason for hiding this comment

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

from what you laid out here: #156 (comment) I suppose we should skip the explicit return

Choose a reason for hiding this comment

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

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 think is worth calling it "non-obvious return", specially since postInteractions will be empty and we cannot not declare it (compiler warnings)

// it should return a valid pool order
(GPv2Order.Data memory ammOrder,,,) = helper.order(address(pool), prices);

assertEq(address(ammOrder.buyToken), priceSkewness > 10_000 ? tokens[0] : tokens[1]);

Choose a reason for hiding this comment

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

I believe that to fully benefit from btt tests we should make the .tree fully match the branches available in the (code|domain). I've checked and they're effectively different branches: node_modules/cow-amm/src/libraries/GetTradeableOrder.sol:56

It was not obvious for me reviewing it, and it could have been easier to see had the information of when token0 reserve * denominator > when token1 reserve * numerator -> buyToken is token1 been in the .tree file. I believe this is a case where you set some 'funny values' to get to that branch. I consider this PR should include:

  • fully fuzzed tests to report on rounding errors (this one is ok for these purposes, we could also be purists and have one fully-fuzzed for each branch. Definetly not required.) This doesnt fit neatly into the .tree but I can live with that.
  • 'valid scenario' 'funny value'd unit tests that are described in .tree and walk every available branch. In this case I'd consider the library an implementaiton detail and walk their branches as well, as that's critical to ensure this helper works as intended. This includes:
    • one testcase forcing token0 to be buyToken
    • one testcase forcing token1 to be buyToken
    • Some assertions on src/contracts/BCoWHelper.sol:74, since that defines two branches as well. Since they are coupled to the above, I don't know if they'll be their own separate two testcases or extra asserts inside them, do whatever feels right.

tl;dr: fuzzing is ok as is, we should have tests manually walking each branch.

Copy link

@0xteddybear 0xteddybear left a comment

Choose a reason for hiding this comment

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

having fuzzing cover separate code branches in different tests is a good compromise, however as an improvement I would have preferred:

  • for the unit tests to include when- branches with 'funny values' that cause the different branches' execution with no fuzzing setup.
  • the above to include asserts on {buy,sell}Amount (not a hard requirement since they are covered in the integrations)
  • fuzzing separate from that, in given branches, possibly with less assertions


balanceToken0 = bound(balanceToken0, 1e18, 1e36);
balanceToken1 = bound(balanceToken1, 1e18, 1e36);
balanceToken0 = bound(balanceToken0, 1e18, 1e27);

Choose a reason for hiding this comment

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

why this change?

Copy link
Member Author

Choose a reason for hiding this comment

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

avoiding overflows

callData: abi.encodeWithSelector(IBCoWPool.commit.selector, orderCommitment)
});

return (order_, preInteractions, postInteractions, sig);

Choose a reason for hiding this comment

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

pool.set__totalWeight(2 * VALID_WEIGHT);
pool.set__finalized(true);

priceVector[0] = 1e18;

Choose a reason for hiding this comment

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

I meant something like 3000:1 so it's more evident, like you did with the integration tests

@wei3erHase wei3erHase merged commit c9a3849 into dev Jul 23, 2024
4 checks passed
@wei3erHase wei3erHase deleted the feat/helper-poc branch July 23, 2024 09:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants