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
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
699d9d7
feat: non-weighted tradeable order test
wei3erHase Jun 27, 2024
271e00a
fix: adding GetTradeableOrder library
wei3erHase Jun 27, 2024
48888e3
feat: adding weights to the math
wei3erHase Jun 27, 2024
28565f7
feat: variable weights
wei3erHase Jun 27, 2024
1a44f5e
feat: adding test for helper
wei3erHase Jul 8, 2024
8bf22dd
feat: adding support for weights
wei3erHase Jul 8, 2024
3683943
fix: fmt
wei3erHase Jul 8, 2024
9422865
fix: sending correct price vector
wei3erHase Jul 8, 2024
be1701d
fix: making helper return valid signature
wei3erHase Jul 8, 2024
24553ab
chore: merge dev
wei3erHase Jul 9, 2024
441d117
feat: deprecated weights in helper
wei3erHase Jul 9, 2024
5630797
feat: updated buyAmount as per aaf44a3
wei3erHase Jul 9, 2024
627d878
refactor: mv GetTradeableOrder to libraries dir
wei3erHase Jul 9, 2024
cb2fb52
fix: update comment on BCoWHelper
wei3erHase Jul 9, 2024
5207c18
fix: rm unused variable warning
wei3erHase Jul 9, 2024
6f76f85
feat: adding BTT test for BCoWHelper
wei3erHase Jul 9, 2024
de99df4
fix: linter warnings
wei3erHase Jul 9, 2024
bd64dca
fix: rm remanent line change
wei3erHase Jul 9, 2024
ac17f69
fix: rm unused lines
wei3erHase Jul 9, 2024
6a14084
fix: missing natspec
wei3erHase Jul 9, 2024
96f0302
fix: uncommenting assertion in test
wei3erHase Jul 9, 2024
e8c4d33
fix: missing natspec
wei3erHase Jul 9, 2024
22480c5
fix: safety checks comments
wei3erHase Jul 9, 2024
3e5bfea
feat: vendoring interface from cow-amm
wei3erHase Jul 9, 2024
68e8368
fix: lint run
wei3erHase Jul 9, 2024
5afd282
chore: merge dev
wei3erHase Jul 9, 2024
9390e64
chore: pull dev no-rebase
wei3erHase Jul 9, 2024
b1ba43a
fix: gas snapshot
wei3erHase Jul 9, 2024
9ec1c34
refactor: reordered helper flow and cleanup
wei3erHase Jul 10, 2024
05d6271
feat: vendoring GetTradeableOrder from cow-amm
wei3erHase Jul 11, 2024
9923fc0
feat: improving commitment expectation
wei3erHase Jul 11, 2024
8d72ff8
Merge branch 'dev' of https://github.com/defi-wonderland/balancer-v1-…
wei3erHase Jul 11, 2024
699996e
fix: typos in comments
wei3erHase Jul 11, 2024
1bec1fd
fix: overwriting sellAmount in order to avoid rounding issues (#154)
wei3erHase Jul 15, 2024
1ffda8b
chore: addressing contract changes from PR comments
wei3erHase Jul 16, 2024
063d3f6
chore: addressing comments in tests
wei3erHase Jul 16, 2024
471b0e6
chore: merge dev
wei3erHase Jul 16, 2024
e0de518
fix: adding helper mock
wei3erHase Jul 16, 2024
d0bba5c
fix: addressing comments from PR
wei3erHase Jul 16, 2024
d8c49e6
refactor: deprecate fuzzed integration valid order test in favour of …
wei3erHase Jul 16, 2024
abe6f00
feat: simplifying helper integration test
wei3erHase Jul 17, 2024
b2056be
feat: improving unit test for valid order
wei3erHase Jul 17, 2024
ffb1bc0
feat: adding call tokens expectation
wei3erHase Jul 18, 2024
7719c39
fix: branching different behaviours given skewness
wei3erHase Jul 22, 2024
7db4e8e
fix: adding comments on the skewness sign
wei3erHase Jul 22, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .forge-snapshots/newBFactory.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
4130621
4130633
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"@cowprotocol/contracts": "github:cowprotocol/contracts.git#a10f40788a",
"@openzeppelin/contracts": "5.0.2",
"composable-cow": "github:cowprotocol/composable-cow.git#24d556b",
"cow-amm": "github:cowprotocol/cow-amm.git#6566128",
wei3erHase marked this conversation as resolved.
Show resolved Hide resolved
"solmate": "github:transmissions11/solmate#c892309"
},
"devDependencies": {
Expand Down
3 changes: 3 additions & 0 deletions remappings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ solmate/=node_modules/solmate/src
@cowprotocol/=node_modules/@cowprotocol/contracts/src/contracts
cowprotocol/=node_modules/@cowprotocol/contracts/src/
@composable-cow/=node_modules/composable-cow/
@cow-amm/=node_modules/cow-amm/src
lib/openzeppelin/=node_modules/@openzeppelin

contracts/=src/contracts
interfaces/=src/interfaces
libraries/=src/libraries
99 changes: 99 additions & 0 deletions src/contracts/BCoWHelper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity 0.8.25;

import {IBCoWFactory} from 'interfaces/IBCoWFactory.sol';
import {IBCoWPool} from 'interfaces/IBCoWPool.sol';

import {ICOWAMMPoolHelper} from '@cow-amm/interfaces/ICOWAMMPoolHelper.sol';
import {GetTradeableOrder} from '@cow-amm/libraries/GetTradeableOrder.sol';

import {IERC20} from '@cowprotocol/interfaces/IERC20.sol';
import {GPv2Interaction} from '@cowprotocol/libraries/GPv2Interaction.sol';
import {GPv2Order} from '@cowprotocol/libraries/GPv2Order.sol';

/**
* @title BCoWHelper
* @notice Helper contract that allows to trade on CoW Swap Protocol.
* @dev This contract supports only 2-token equal-weights pools.
*/
contract BCoWHelper is ICOWAMMPoolHelper {
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


/// @inheritdoc ICOWAMMPoolHelper
// solhint-disable-next-line style-guide-casing
address public immutable factory;

constructor(address factory_) {
factory = factory_;
APP_DATA = IBCoWFactory(factory_).APP_DATA();
}

/// @inheritdoc ICOWAMMPoolHelper
function order(
address pool,
uint256[] calldata prices
)
external
view
returns (
GPv2Order.Data memory order_,
GPv2Interaction.Data[] memory preInteractions,
GPv2Interaction.Data[] memory postInteractions,
bytes memory sig
)
{
address[] memory tokens_ = tokens(pool);

GetTradeableOrder.GetTradeableOrderParams memory params = GetTradeableOrder.GetTradeableOrderParams({
pool: pool,
token0: IERC20(tokens_[0]),
token1: IERC20(tokens_[1]),
// The price of this function is expressed as amount of
// token1 per amount of token0. The `prices` vector is
// expressed the other way around.
priceNumerator: prices[1],
priceDenominator: prices[0],
appData: APP_DATA
});

order_ = GetTradeableOrder.getTradeableOrder(params);

// A ERC-1271 signature on CoW Protocol is composed of two parts: the
// signer address and the valid ERC-1271 signature data for that signer.
bytes memory eip1271sig;
eip1271sig = abi.encode(order_);
sig = abi.encodePacked(pool, eip1271sig);

// Generate the order commitment pre-interaction
bytes32 domainSeparator = IBCoWPool(pool).SOLUTION_SETTLER_DOMAIN_SEPARATOR();
bytes32 orderCommitment = order_.hash(domainSeparator);

preInteractions = new GPv2Interaction.Data[](1);
preInteractions[0] = GPv2Interaction.Data({
target: pool,
value: 0,
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)

}

/// @inheritdoc ICOWAMMPoolHelper
function tokens(address pool) public view returns (address[] memory tokens_) {
// reverts in case pool is not deployed by the helper's factory
if (!IBCoWFactory(factory).isBPool(pool)) revert PoolDoesNotExist();
wei3erHase marked this conversation as resolved.
Show resolved Hide resolved
// call reverts with `BPool_PoolNotFinalized()` in case pool is not finalized
tokens_ = IBCoWPool(pool).getFinalTokens();
// reverts in case pool is not supported (non-2-token pool)
if (tokens_.length != 2) revert PoolDoesNotExist();
wei3erHase marked this conversation as resolved.
Show resolved Hide resolved
// reverts in case pool is not supported (non-equal weights)
if (IBCoWPool(pool).getNormalizedWeight(tokens_[0]) != IBCoWPool(pool).getNormalizedWeight(tokens_[1])) {
revert PoolDoesNotExist();
}

return tokens_;
wei3erHase marked this conversation as resolved.
Show resolved Hide resolved
}
}
196 changes: 196 additions & 0 deletions test/integration/BCoWHelper.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.24;

import {Test} from 'forge-std/Test.sol';

import {IERC20} from '@cowprotocol/interfaces/IERC20.sol';

import {IBCoWPool} from 'interfaces/IBCoWPool.sol';
import {IBPool} from 'interfaces/IBPool.sol';
import {ISettlement} from 'interfaces/ISettlement.sol';

import {ICOWAMMPoolHelper} from '@cow-amm/interfaces/ICOWAMMPoolHelper.sol';
import {GPv2Interaction} from '@cowprotocol/libraries/GPv2Interaction.sol';
import {GPv2Order} from '@cowprotocol/libraries/GPv2Order.sol';
import {GPv2Trade} from '@cowprotocol/libraries/GPv2Trade.sol';
import {GPv2Signing} from '@cowprotocol/mixins/GPv2Signing.sol';

import {GPv2TradeEncoder} from '@composable-cow/test/vendored/GPv2TradeEncoder.sol';

import {BCoWFactory} from 'contracts/BCoWFactory.sol';
import {BCoWHelper} from 'contracts/BCoWHelper.sol';

contract ConstantProductHelperForkedTest is Test {
using GPv2Order for GPv2Order.Data;

BCoWHelper private helper;

// All hardcoded addresses are mainnet addresses
address public lp = makeAddr('lp');

ISettlement private settlement = ISettlement(0x9008D19f58AAbD9eD0D60971565AA8510560ab41);
address private vaultRelayer;

address private solver = 0x423cEc87f19F0778f549846e0801ee267a917935;

BCoWFactory private ammFactory;
IBPool private weightedPool;
IBPool private basicPool;

IERC20 private constant DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F);
IERC20 private constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
wei3erHase marked this conversation as resolved.
Show resolved Hide resolved
IERC20 private constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);

uint256 constant VALID_AMOUNT = 1e6;
wei3erHase marked this conversation as resolved.
Show resolved Hide resolved
uint256 constant TEN_PERCENT = 0.1 ether;

function setUp() public {
vm.createSelectFork('mainnet', 20_012_063);

vaultRelayer = address(settlement.vaultRelayer());

ammFactory = new BCoWFactory(address(settlement), bytes32('appData'));
helper = new BCoWHelper(address(ammFactory));

deal(address(DAI), lp, 2 * VALID_AMOUNT);
deal(address(WETH), lp, 2 * VALID_AMOUNT);

vm.startPrank(lp);
weightedPool = ammFactory.newBPool();
basicPool = ammFactory.newBPool();

DAI.approve(address(weightedPool), type(uint256).max);
WETH.approve(address(weightedPool), type(uint256).max);
weightedPool.bind(address(DAI), VALID_AMOUNT, 8e18); // 80% weight
weightedPool.bind(address(WETH), VALID_AMOUNT, 2e18); // 20% weight

DAI.approve(address(basicPool), type(uint256).max);
WETH.approve(address(basicPool), type(uint256).max);
basicPool.bind(address(DAI), VALID_AMOUNT, 4.2e18); // no weight
basicPool.bind(address(WETH), VALID_AMOUNT, 4.2e18); // no weight

// finalize
weightedPool.finalize();
basicPool.finalize();

vm.stopPrank();
}

// NOTE: 1 ETH = 1000e6 DAI
wei3erHase marked this conversation as resolved.
Show resolved Hide resolved
uint256 constant INITIAL_SPOT_PRICE = 0.001e18;

Choose a reason for hiding this comment

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

feels weird to have a constant in between functions, are we sure it's best to disable solhint's ordering for tests?


function testBasicOrder() public {
IBCoWPool pool = IBCoWPool(address(basicPool));

uint256 ammWethInitialBalance = 1 ether;
uint256 ammDaiInitialBalance = 1000 ether;

deal(address(WETH), address(pool), ammWethInitialBalance);
deal(address(DAI), address(pool), ammDaiInitialBalance);

uint256 spotPrice = pool.getSpotPriceSansFee(address(WETH), address(DAI));
assertEq(spotPrice, INITIAL_SPOT_PRICE);

_executeHelperOrder(pool, ammWethInitialBalance, ammDaiInitialBalance);

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

}

// 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?

IBCoWPool pool = IBCoWPool(address(weightedPool));

uint256 ammWethInitialBalance = 1 ether;
uint256 ammDaiInitialBalance = 1000 ether;

deal(address(WETH), address(pool), ammWethInitialBalance);
// NOTE: pool is 80-20 DAI-WETH, has 4xDAI balance than basic, same spot price
deal(address(DAI), address(pool), 4 * ammDaiInitialBalance);

uint256 spotPrice = pool.getSpotPriceSansFee(address(WETH), address(DAI));
assertEq(spotPrice, INITIAL_SPOT_PRICE);

vm.expectRevert(ICOWAMMPoolHelper.PoolDoesNotExist.selector);
helper.order(address(pool), new uint256[](2));

// NOTE: not supported
// _executeHelperOrder(pool, ammWethInitialBalance, ammDaiInitialBalance);
// uint256 postSpotPrice = pool.getSpotPriceSansFee(address(WETH), address(DAI));
// assertEq(postSpotPrice, 1_052_631_578_947_368);
}

function addressVecToIerc20Vec(address[] memory addrVec) private pure returns (IERC20[] memory ierc20vec) {
assembly {
ierc20vec := addrVec
}
}

function _executeHelperOrder(IBPool pool, uint256 ammWethInitialBalance, uint256 ammDaiInitialBalance) internal {
IERC20[] memory tokens = addressVecToIerc20Vec(helper.tokens(address(pool)));
uint256 daiIndex = 0;
uint256 wethIndex = 1;
assertEq(tokens.length, 2);
assertEq(address(tokens[daiIndex]), address(DAI));
assertEq(address(tokens[wethIndex]), address(WETH));

// Prepare the price vector used in the execution of the settlement in
// CoW Protocol. We skew the price by ~5% towards a cheaper WETH, so
// that the AMM wants to buy WETH.
uint256[] memory prices = new uint256[](2);
// Note: oracle price are expressed in the same format as prices in
// a call to `settle`, where the price vector is expressed so that
// if the first token is DAI and the second WETH then a price of 3000
// DAI per WETH means a price vector of [1, 3000] (if the decimals are
// different, as in WETH/USDC, then the atom amount is what counts).
prices[daiIndex] = ammWethInitialBalance;
prices[wethIndex] = ammDaiInitialBalance * 95 / 100;

// The helper generates the AMM order
GPv2Order.Data memory ammOrder;
GPv2Interaction.Data[] memory preInteractions;
GPv2Interaction.Data[] memory postInteractions;
bytes memory sig;
(ammOrder, preInteractions, postInteractions, sig) = helper.order(address(pool), prices);

// We expect a commit interaction in pre interactions
assertEq(preInteractions.length, 1);
assertEq(postInteractions.length, 0);

// Because of how we changed the price, we expect to buy DAI
wei3erHase marked this conversation as resolved.
Show resolved Hide resolved
assertEq(address(ammOrder.sellToken), address(DAI));
assertEq(address(ammOrder.buyToken), address(WETH));

// Check that the amounts and price aren't unreasonable. We changed the
// price by about 5%, so the amounts aren't expected to change
// significantly more (say, about 2.5% of the original balance).
assertApproxEqRel(ammOrder.sellAmount, ammDaiInitialBalance * 25 / 1000, TEN_PERCENT);
assertApproxEqRel(ammOrder.buyAmount, ammWethInitialBalance * 25 / 1000, TEN_PERCENT);

GPv2Trade.Data[] memory trades = new GPv2Trade.Data[](1);

// pool's trade
trades[0] = GPv2Trade.Data({
sellTokenIndex: 0,
buyTokenIndex: 1,
receiver: ammOrder.receiver,
sellAmount: ammOrder.sellAmount,
buyAmount: ammOrder.buyAmount,
validTo: ammOrder.validTo,
appData: ammOrder.appData,
feeAmount: ammOrder.feeAmount,
flags: GPv2TradeEncoder.encodeFlags(ammOrder, GPv2Signing.Scheme.Eip1271),
executedAmount: ammOrder.sellAmount,
signature: sig
});

GPv2Interaction.Data[][3] memory interactions =
[new GPv2Interaction.Data[](1), new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0)];

interactions[0][0] = preInteractions[0];

// finally, settle
vm.prank(solver);
settlement.settle(tokens, prices, trades, interactions);
}
}
1 change: 1 addition & 0 deletions test/integration/BCowPool.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {GPv2Interaction} from '@cowprotocol/libraries/GPv2Interaction.sol';
import {GPv2Order} from '@cowprotocol/libraries/GPv2Order.sol';
import {GPv2Trade} from '@cowprotocol/libraries/GPv2Trade.sol';
import {GPv2Signing} from '@cowprotocol/mixins/GPv2Signing.sol';

import {BCoWConst} from 'contracts/BCoWConst.sol';
import {BCoWFactory} from 'contracts/BCoWFactory.sol';

Expand Down
43 changes: 42 additions & 1 deletion test/manual-smock/MockBCoWFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ import {BCoWFactory, BCoWPool, BFactory, IBCoWFactory, IBPool} from '../../src/c
import {Test} from 'forge-std/Test.sol';

contract MockBCoWFactory is BCoWFactory, Test {
// NOTE: manually added methods (immutable overrides not supported in smock)
function mock_call_APP_DATA(bytes32 _appData) public {
vm.mockCall(address(this), abi.encodeWithSignature('APP_DATA()'), abi.encode(_appData));
}

function expectCall_APP_DATA() public {
vm.expectCall(address(this), abi.encodeWithSignature('APP_DATA()'));
}

// BCoWFactory methods
constructor(address solutionSettler, bytes32 appData) BCoWFactory(solutionSettler, appData) {}

function mock_call_logBCoWPool() public {
Expand All @@ -31,8 +41,39 @@ contract MockBCoWFactory is BCoWFactory, Test {
}

// MockBFactory methods

function set__isBPool(address _key0, bool _value) public {
_isBPool[_key0] = _value;
}

function call__isBPool(address _key0) public view returns (bool) {
return _isBPool[_key0];
}

function set__bDao(address __bDao) public {
_bDao = __bDao;
}

function call__bDao() public view returns (address) {
return _bDao;
}

function mock_call_newBPool(IBPool bPool) public {
vm.mockCall(address(this), abi.encodeWithSignature('newBPool()'), abi.encode(bPool));
}

function mock_call_setBDao(address bDao) public {
vm.mockCall(address(this), abi.encodeWithSignature('setBDao(address)', bDao), abi.encode());
}

function mock_call_collect(IBPool bPool) public {
vm.mockCall(address(this), abi.encodeWithSignature('collect(IBPool)', bPool), abi.encode());
}

function mock_call_isBPool(address bPool, bool _returnParam0) public {
vm.mockCall(address(this), abi.encodeWithSignature('isBPool(address)', bPool), abi.encode(_returnParam0));
}

function mock_call_getBDao(address _returnParam0) public {
vm.mockCall(address(this), abi.encodeWithSignature('getBDao()'), abi.encode(_returnParam0));
}
}
Loading
Loading