-
Notifications
You must be signed in to change notification settings - Fork 1
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
Conversation
ca413a4
to
b1ba43a
Compare
There was a problem hiding this 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.
There was a problem hiding this 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
There was a problem hiding this 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
src/contracts/BCoWHelper.sol
Outdated
using GPv2Order for GPv2Order.Data; | ||
|
||
/// @notice The app data used by this helper's factory. | ||
bytes32 public immutable APP_DATA; |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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
} | ||
|
||
// NOTE: reverting test, weighted pools are not supported | ||
function testWeightedOrder() public { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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/unit/BCoWHelper.tree
Outdated
│ ├── it should return a commit pre-interaction | ||
│ ├── it should return an empty post-interaction | ||
│ └── it should return a valid signature | ||
└── given a price vector |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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
_executeHelperOrder(pool); | ||
|
||
uint256 postSpotPrice = pool.getSpotPriceSansFee(address(WETH), address(DAI)); | ||
assertEq(postSpotPrice, 1_052_631_578_947_368); |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
assertEq(keccak256(validSig), keccak256(sig)); | ||
} | ||
|
||
function test_OrderGivenAPriceVector(uint256 priceSkewness, uint256 balanceToken0, uint256 balanceToken1) external { |
There was a problem hiding this comment.
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_000and
15_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?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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)
test/unit/BCoWHelper.t.sol
Outdated
// 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]); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this 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.t.sol
Outdated
|
||
// 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]); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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)
test/unit/BCoWHelper.t.sol
Outdated
// 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]); |
There was a problem hiding this comment.
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.
There was a problem hiding this 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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why this change?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
GetTradeableOrder
from cow-amm after this PR gets merged: chore: running forge update and fixing OZ related methods cowprotocol/cow-amm#98BCoWHelper 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.
ICOWAMMPoolHelper
interface from cow-ammGetTradeableOrder
from cow-ammtokens
method WILL revert withPoolDoesNotExist
if:BPool_PoolNotFinalized
)order
method internally callstokens
(same reverting cases)order
returns the commit pre-interaction and the order calculated inGetTradeableOrder
libraryfactory
aims to the BCoWFactory and reads the AppData from itConsiderations 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 (perhapsGetTradeableWeightedOrder
?).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: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:
Comments: