From 40c0114aa49176893b69ee6aa3853ddc50495128 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Thu, 25 Jan 2024 14:20:27 -0500 Subject: [PATCH 01/54] feat: add src directory and populate with contracts --- .../pricecheckers/FixedMinOutPriceChecker.sol | 2 +- src/Milkman.sol | 251 ++++++++++++++++++ src/periphery/GasChecker.sol | 20 ++ src/periphery/HashHelper.sol | 18 ++ src/periphery/MilkmanStateHelper.sol | 52 ++++ .../ChainlinkExpectedOutCalculator.sol | 118 ++++++++ .../CurveExpectedOutCalculator.sol | 97 +++++++ src/pricecheckers/DynamicSlippageChecker.sol | 52 ++++ .../FixedMaxFeePriceCheckerDecorator.sol | 50 ++++ src/pricecheckers/FixedMinOutPriceChecker.sol | 20 ++ src/pricecheckers/FixedSlippageChecker.sol | 55 ++++ src/pricecheckers/IExpectedOutCalculator.sol | 12 + .../MetaExpectedOutCalculator.sol | 63 +++++ src/pricecheckers/PRICE_CHECKERS.md | 18 ++ src/pricecheckers/PriceCheckerLib.sol | 41 +++ ...edBalancerBalWethExpectedOutCalculator.sol | 149 +++++++++++ .../UniV2ExpectedOutCalculator.sol | 50 ++++ .../UniV3ExpectedOutCalculator.sol | 73 +++++ .../ValidFromPriceCheckerDecorator.sol | 35 +++ 19 files changed, 1175 insertions(+), 1 deletion(-) create mode 100644 src/Milkman.sol create mode 100644 src/periphery/GasChecker.sol create mode 100644 src/periphery/HashHelper.sol create mode 100644 src/periphery/MilkmanStateHelper.sol create mode 100644 src/pricecheckers/ChainlinkExpectedOutCalculator.sol create mode 100644 src/pricecheckers/CurveExpectedOutCalculator.sol create mode 100644 src/pricecheckers/DynamicSlippageChecker.sol create mode 100644 src/pricecheckers/FixedMaxFeePriceCheckerDecorator.sol create mode 100644 src/pricecheckers/FixedMinOutPriceChecker.sol create mode 100644 src/pricecheckers/FixedSlippageChecker.sol create mode 100644 src/pricecheckers/IExpectedOutCalculator.sol create mode 100644 src/pricecheckers/MetaExpectedOutCalculator.sol create mode 100644 src/pricecheckers/PRICE_CHECKERS.md create mode 100644 src/pricecheckers/PriceCheckerLib.sol create mode 100644 src/pricecheckers/SingleSidedBalancerBalWethExpectedOutCalculator.sol create mode 100644 src/pricecheckers/UniV2ExpectedOutCalculator.sol create mode 100644 src/pricecheckers/UniV3ExpectedOutCalculator.sol create mode 100644 src/pricecheckers/ValidFromPriceCheckerDecorator.sol diff --git a/contracts/pricecheckers/FixedMinOutPriceChecker.sol b/contracts/pricecheckers/FixedMinOutPriceChecker.sol index c9a7d32..e110e44 100644 --- a/contracts/pricecheckers/FixedMinOutPriceChecker.sol +++ b/contracts/pricecheckers/FixedMinOutPriceChecker.sol @@ -14,7 +14,7 @@ contract FixedMinOutPriceChecker is IPriceChecker { uint256 _out, bytes calldata _data ) external view override returns (bool) { - (uint256 minOut) = abi.decode(_data, (uint256)); + uint256 minOut = abi.decode(_data, (uint256)); return minOut <= _out; } } diff --git a/src/Milkman.sol b/src/Milkman.sol new file mode 100644 index 0000000..c8703c8 --- /dev/null +++ b/src/Milkman.sol @@ -0,0 +1,251 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +pragma solidity ^0.7.6; +pragma abicoder v2; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IPriceChecker} from "../interfaces/IPriceChecker.sol"; + +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +import {SafeMath} from "@openzeppelin/contracts/math/SafeMath.sol"; +import {GPv2Order} from "@cow-protocol/contracts/libraries/GPv2Order.sol"; + +/// @title Milkman +/// @author @charlesndalton +/// @notice A layer on top of the CoW Protocol that allows smart contracts (DAOs, Gnosis Safes, protocols, etc.) to submit swaps. Swaps are MEV-protected. Use with atypical tokens (e.g., rebasing tokens) not recommended. +/// @dev For each requested swap, Milkman creates a clone of itself, and moves `amountIn` of `fromToken` into the clone. The clone pre-approves the amount to the CoW settlement contract. The clone also stores a hash of the swap's variables, something like hash({amountIn: 1000, fromToken: USDC, toToken: DAI, etc.}). Then, an off-chain server creates a CoW order on behalf of the clone. Before this CoW order can be 'settled' (before amountIn can be pulled out of the clone), the clone runs checks on the order. These checks include calling a user-provided `priceChecker`, which could for example check SushiSwap to see if what they could get out of SushiSwap was at least 90% of the order's `minOut`. +contract Milkman { + using SafeERC20 for IERC20; + using GPv2Order for GPv2Order.Data; + using SafeMath for uint256; + + event SwapRequested( + address orderContract, + address orderCreator, + uint256 amountIn, + address fromToken, + address toToken, + address to, + address priceChecker, + bytes priceCheckerData + ); + + /// @dev The contract Milkman needs to give allowance. + address internal constant VAULT_RELAYER = + 0xC92E8bdf79f0507f65a392b0ab4667716BFE0110; + /// @dev The settlement contract's EIP-712 domain separator. Milkman uses this to verify that a provided UID matches provided order parameters. + bytes32 public constant DOMAIN_SEPARATOR = + 0xc078f884a2676e1345748b1feace7b0abee5d00ecadb6e574dcdd109a63e8943; + bytes4 internal constant MAGIC_VALUE = 0x1626ba7e; + bytes4 internal constant NON_MAGIC_VALUE = 0xffffffff; + bytes32 internal constant ROOT_MILKMAN_SWAP_HASH = + 0xca11ab1efacade00000000000000000000000000000000000000000000000000; + + /// @dev the Milkman deployed by an EOA, in contrast to Milkman 'order contracts' deployed in invocations of requestSwapExactTokensForTokens + address internal immutable ROOT_MILKMAN; + + /// @dev Hash of the order data, hashed like so: + /// kekkak256(abi.encode(orderCreator, receiver, fromToken, toToken, amountIn, priceChecker, priceCheckerData)). + /// In the root contract, it's set to `ROOT_MILKMAN_SWAP_HASH`. + bytes32 public swapHash = ROOT_MILKMAN_SWAP_HASH; + + constructor() { + ROOT_MILKMAN = address(this); + } + + /// @notice Asynchronously swap an exact amount of tokenIn for a market-determined amount of tokenOut. + /// @dev Swaps are usually completed in ~2 minutes. + /// @param amountIn The number of tokens to sell. + /// @param fromToken The token that the user wishes to sell. + /// @param toToken The token that the user wishes to receive. + /// @param to Who should receive the tokens. + /// @param priceChecker A contract that verifies an order (mainly its minOut and fee) before Milkman signs it. + /// @param priceCheckerData Data that gets passed to the price checker. + function requestSwapExactTokensForTokens( + uint256 amountIn, + IERC20 fromToken, + IERC20 toToken, + address to, + address priceChecker, + bytes calldata priceCheckerData + ) external { + require(address(this) == ROOT_MILKMAN, "!root_milkman"); // can't call `requestSwapExactTokensForTokens` from order contracts + require(priceChecker != address(0), "!price_checker"); // need to supply a valid price checker + + address orderContract = createOrderContract(); + + fromToken.safeTransferFrom(msg.sender, orderContract, amountIn); + + bytes32 _swapHash = keccak256( + abi.encode( + msg.sender, + to, + fromToken, + toToken, + amountIn, + priceChecker, + priceCheckerData + ) + ); + + Milkman(orderContract).initialize(fromToken, _swapHash); + + emit SwapRequested( + orderContract, + msg.sender, + amountIn, + address(fromToken), + address(toToken), + to, + priceChecker, + priceCheckerData + ); + } + + function initialize(IERC20 fromToken, bytes32 _swapHash) external { + require(swapHash == bytes32(0) && _swapHash != bytes32(0), "!reinit"); // also prevents root contract from being initialized + swapHash = _swapHash; + + fromToken.safeApprove(VAULT_RELAYER, type(uint256).max); + } + + /// @notice Cancel a requested swap, sending the tokens back to the order creator. + /// @dev `msg.sender` must be the original order creator. The other parameters are required to verify that this is the case (kind of like a merkle proof). + function cancelSwap( + uint256 amountIn, + IERC20 fromToken, + IERC20 toToken, + address to, + address priceChecker, + bytes calldata priceCheckerData + ) external { + bytes32 _storedSwapHash = swapHash; + + require(_storedSwapHash != ROOT_MILKMAN_SWAP_HASH, "!cancel_from_root"); + + bytes32 _calculatedSwapHash = keccak256( + abi.encode( + msg.sender, + to, + fromToken, + toToken, + amountIn, + priceChecker, + priceCheckerData + ) + ); + + require(_storedSwapHash == _calculatedSwapHash, "!valid_creator_proof"); + + fromToken.safeTransfer(msg.sender, amountIn); + } + + /// @param orderDigest The EIP-712 signing digest derived from the order + /// @param encodedOrder Bytes-encoded order information, originally created by an off-chain bot. Created by concatening the order data (in the form of GPv2Order.Data), the price checker address, and price checker data. + function isValidSignature(bytes32 orderDigest, bytes calldata encodedOrder) + external + view + returns (bytes4) + { + bytes32 _storedSwapHash = swapHash; + + require( + _storedSwapHash != ROOT_MILKMAN_SWAP_HASH, + "!is_valid_sig_from_root" + ); + + ( + GPv2Order.Data memory _order, + address _orderCreator, + address _priceChecker, + bytes memory _priceCheckerData + ) = decodeOrder(encodedOrder); + + require(_order.hash(DOMAIN_SEPARATOR) == orderDigest, "!match"); + + require(_order.kind == GPv2Order.KIND_SELL, "!kind_sell"); + + require( + _order.validTo >= block.timestamp + 5 minutes, + "expires_too_soon" + ); + + require(!_order.partiallyFillable, "!fill_or_kill"); + + require( + _order.sellTokenBalance == GPv2Order.BALANCE_ERC20, + "!sell_erc20" + ); + + require( + _order.buyTokenBalance == GPv2Order.BALANCE_ERC20, + "!buy_erc20" + ); + + require( + IPriceChecker(_priceChecker).checkPrice( + _order.sellAmount.add(_order.feeAmount), + address(_order.sellToken), + address(_order.buyToken), + _order.feeAmount, + _order.buyAmount, + _priceCheckerData + ), + "invalid_min_out" + ); + + bytes32 _calculatedSwapHash = keccak256( + abi.encode( + _orderCreator, + _order.receiver, + _order.sellToken, + _order.buyToken, + _order.sellAmount.add(_order.feeAmount), + _priceChecker, + _priceCheckerData + ) + ); + + if (_calculatedSwapHash == _storedSwapHash) { + // should be true as long as the keeper isn't submitting bad orders + return MAGIC_VALUE; + } else { + return NON_MAGIC_VALUE; + } + } + + function decodeOrder(bytes calldata _encodedOrder) + internal + pure + returns ( + GPv2Order.Data memory _order, + address _orderCreator, + address _priceChecker, + bytes memory _priceCheckerData + ) + { + (_order, _orderCreator, _priceChecker, _priceCheckerData) = abi.decode( + _encodedOrder, + (GPv2Order.Data, address, address, bytes) + ); + } + + function createOrderContract() internal returns (address _orderContract) { + // Copied from https://github.com/optionality/clone-factory/blob/master/contracts/CloneFactory.sol + + bytes20 addressBytes = bytes20(address(this)); + assembly { + // EIP-1167 bytecode + let clone_code := mload(0x40) + mstore( + clone_code, + 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000 + ) + mstore(add(clone_code, 0x14), addressBytes) + mstore( + add(clone_code, 0x28), + 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000 + ) + _orderContract := create(0, clone_code, 0x37) + } + } +} diff --git a/src/periphery/GasChecker.sol b/src/periphery/GasChecker.sol new file mode 100644 index 0000000..0b7ae9d --- /dev/null +++ b/src/periphery/GasChecker.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +pragma solidity ^0.7.6; +pragma abicoder v2; + +interface EIP1271 { + function isValidSignature(bytes32, bytes calldata) + external + returns (bytes4); +} + +/// Check that `isValidSignature` doesn't take up too much gas +contract GasChecker { + function isValidSignatureCheck( + address milkman, + bytes32 orderDigest, + bytes calldata encodedOrder + ) external returns (bytes4) { + return EIP1271(milkman).isValidSignature(orderDigest, encodedOrder); + } +} diff --git a/src/periphery/HashHelper.sol b/src/periphery/HashHelper.sol new file mode 100644 index 0000000..f0d5e8e --- /dev/null +++ b/src/periphery/HashHelper.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +pragma solidity ^0.7.6; +pragma abicoder v2; + +import {GPv2Order} from "@cow-protocol/contracts/libraries/GPv2Order.sol"; + +contract HashHelper { + using GPv2Order for GPv2Order.Data; + using GPv2Order for bytes; + + function hash(GPv2Order.Data memory order, bytes32 domainSeparator) + external + pure + returns (bytes32 orderDigest) + { + return order.hash(domainSeparator); + } +} diff --git a/src/periphery/MilkmanStateHelper.sol b/src/periphery/MilkmanStateHelper.sol new file mode 100644 index 0000000..e2cf435 --- /dev/null +++ b/src/periphery/MilkmanStateHelper.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +pragma solidity ^0.7.6; +pragma abicoder v2; + +import {IGPv2Settlement} from "../../interfaces/IGPv2Settlement.sol"; + +interface IMilkman { + function swaps(bytes32 _swapID) external view returns (bytes memory); +} + +/// @title Milkman State Helper +/// @dev Helper contract that can be used by off-chain bots to fetch the state of a Milkman swap. +contract MilkmanStateHelper { + enum SwapState { + NULL, + REQUESTED, + PAIRED, + PAIRED_AND_UNPAIRABLE, + PAIRED_AND_EXECUTED + } + + IMilkman public constant milkman = + IMilkman(0x3E40B8c9FcBf02a26Ff1c5d88f525AEd00755575); + + IGPv2Settlement internal constant settlement = + IGPv2Settlement(0x9008D19f58AAbD9eD0D60971565AA8510560ab41); + + function getState(bytes32 _swapID) external view returns (SwapState) { + bytes memory _swapData = milkman.swaps(_swapID); + + if (_swapData.length == 0) { + return SwapState.NULL; + } else if ( + _swapData.length == 32 && _swapData[31] == bytes1(uint8(1)) + ) { + return SwapState.REQUESTED; + } + + (uint256 _blockNumberWhenPaired, bytes memory _orderUid) = abi.decode( + _swapData, + (uint256, bytes) + ); + + if (settlement.filledAmount(_orderUid) != 0) { + return SwapState.PAIRED_AND_EXECUTED; + } else if (block.number >= _blockNumberWhenPaired + 50) { + return SwapState.PAIRED_AND_UNPAIRABLE; + } else { + return SwapState.PAIRED; + } + } +} diff --git a/src/pricecheckers/ChainlinkExpectedOutCalculator.sol b/src/pricecheckers/ChainlinkExpectedOutCalculator.sol new file mode 100644 index 0000000..b8a4203 --- /dev/null +++ b/src/pricecheckers/ChainlinkExpectedOutCalculator.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +pragma solidity ^0.7.6; +pragma abicoder v2; + +import "@openzeppelin/contracts/math/SafeMath.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {IExpectedOutCalculator} from "./IExpectedOutCalculator.sol"; + +interface IPriceFeed { + function latestAnswer() external view returns (int256); + + function decimals() external view returns (uint8); +} + +interface IERC20MetaData { + function decimals() external view returns (uint8); +} + +/** + * @notice Checks a swap against Chainlink-compatible price feeds. + * @dev Doesn't care about how long ago the price feed answer was. Another expected out calculator can be built if this is desired. + */ +contract ChainlinkExpectedOutCalculator is IExpectedOutCalculator { + using SafeMath for uint256; + + uint256 internal constant MAX_BPS = 10_000; + + /** + * @param _data Encoded [priceFeeds, reverses]. + * + * priceFeeds (address[]): The price feeds to route through. For example, if the user is swapping BAT -> ALCX, this would contain the BAT/ETH and ALCX/ETH price feeds. + * reverses (bool[]): For each price feed, whether or not it should be reversed. For example, if a user was swapping USDC for XYZ, they would pass in the XYZ/USD price feed and set reversed[0] to true. + * + * Some examples: + * BOND -> ETH: [bond-eth.data.eth], [false]] + * ETH -> BOND: [[bond-eth.data.eth], [true]] + * BOND -> DAI: [bond-eth.data.eth, eth-usd.data.eth], [false, false]] + * alternative: [both-eth.data.eth, eth-usd.data.eth, dai-usd.data.eth], [false, false, true]] + * BOND -> YFI: [[bond-eth.data.eth, yfi-eth.data.eth], [false, true]] + * BOND -> FXS (FXS has no fxs-eth feed): [[bond-eth.data.eth, eth-usd.data.eth, fxs-usd.data.eth], [false, false, true]] + */ + function getExpectedOut( + uint256 _amountIn, + address _fromToken, + address _toToken, + bytes calldata _data + ) external view override returns (uint256) { + (address[] memory _priceFeeds, bool[] memory _reverses) = abi.decode( + _data, + (address[], bool[]) + ); + + return + getExpectedOutFromChainlink( + _priceFeeds, + _reverses, + _amountIn, + _fromToken, + _toToken + ); // how much Chainlink says we'd get out of this trade + } + + function getExpectedOutFromChainlink( + address[] memory _priceFeeds, + bool[] memory _reverses, + uint256 _amountIn, + address _fromToken, + address _toToken + ) internal view returns (uint256 _expectedOutFromChainlink) { + uint256 _priceFeedsLen = _priceFeeds.length; + + require(_priceFeedsLen > 0); // dev: need to pass at least one price feed + require(_priceFeedsLen == _reverses.length); // dev: price feeds and reverse need to have the same length + + for (uint256 _i = 0; _i < _priceFeedsLen; _i++) { + IPriceFeed _priceFeed = IPriceFeed(_priceFeeds[_i]); + + int256 _latestAnswer = _priceFeed.latestAnswer(); + { + require(_latestAnswer > 0); // dev: latest answer from the price feed needs to be positive + } + + uint256 _scaleAnswerBy = 10**uint256(_priceFeed.decimals()); + + // If it's first iteration, use amountIn to calculate. Else, use the result from the previous iteration. + uint256 _amountIntoThisIteration = _i == 0 + ? _amountIn + : _expectedOutFromChainlink; + + // Without a reverse, we multiply amount * price + // With a reverse, we divide amount / price + _expectedOutFromChainlink = _reverses[_i] + ? _amountIntoThisIteration.mul(_scaleAnswerBy).div( + uint256(_latestAnswer) + ) + : _amountIntoThisIteration.mul(uint256(_latestAnswer)).div( + _scaleAnswerBy + ); + } + + uint256 _fromTokenDecimals = uint256( + IERC20MetaData(_fromToken).decimals() + ); + uint256 _toTokenDecimals = uint256(IERC20MetaData(_toToken).decimals()); + + if (_fromTokenDecimals > _toTokenDecimals) { + // if fromToken has more decimals than toToken, we need to divide + _expectedOutFromChainlink = _expectedOutFromChainlink.div( + 10**_fromTokenDecimals.sub(_toTokenDecimals) + ); + } else if (_fromTokenDecimals < _toTokenDecimals) { + _expectedOutFromChainlink = _expectedOutFromChainlink.mul( + 10**_toTokenDecimals.sub(_fromTokenDecimals) + ); + } + } +} diff --git a/src/pricecheckers/CurveExpectedOutCalculator.sol b/src/pricecheckers/CurveExpectedOutCalculator.sol new file mode 100644 index 0000000..1381a70 --- /dev/null +++ b/src/pricecheckers/CurveExpectedOutCalculator.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +pragma solidity ^0.7.6; +pragma abicoder v2; + +import "@openzeppelin/contracts/math/SafeMath.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {IExpectedOutCalculator} from "./IExpectedOutCalculator.sol"; + +interface IAddressProvider { + function get_registry() external view returns (address); +} + +interface IRegistry { + function find_pool_for_coins(address _from, address _to) + external + view + returns (address); + + function get_underlying_coins(address _pool) + external + view + returns (address[8] memory); +} + +interface ICurvePool { + function get_dy_underlying( + int128 i, + int128 j, + uint256 dx + ) external view returns (uint256); +} + +contract CurveExpectedOutCalculator is IExpectedOutCalculator { + using SafeMath for uint256; + + IAddressProvider internal constant ADDRESS_PROVIDER = + IAddressProvider(0x0000000022D53366457F9d5E68Ec105046FC4383); + IRegistry public registry; + + uint256 internal constant MAX_BPS = 10_000; + + constructor() { + updateRegistry(); + } + + // anyone can call this + function updateRegistry() public { + registry = IRegistry(ADDRESS_PROVIDER.get_registry()); + } + + /** + * @dev This expected out calculator can only be used for Curve pools that use `int128` + * for `i` and `j`, which contains most but not all pools. + * + * A separate calculator can be deployed for the pools that use `uint256`. + */ + function getExpectedOut( + uint256 _amountIn, + address _fromToken, + address _toToken, + bytes calldata + ) external view override returns (uint256) { + address _pool = registry.find_pool_for_coins(_fromToken, _toToken); + require(_pool != address(0)); // dev: no Curve pool for this swap + + return _getExpectedOut(_pool, _fromToken, _toToken, _amountIn); + } + + function _getExpectedOut( + address _pool, + address _fromToken, + address _toToken, + uint256 _amountIn + ) internal view returns (uint256) { + int128 _i; + int128 _j; + { + // block scoping to prevent stack too deep + address[8] memory _tokensInPool = registry.get_underlying_coins( + _pool + ); + for (int128 _x = 0; _x < 8; _x++) { + address _currentToken = _tokensInPool[uint256(_x)]; + if (_currentToken == address(0)) { + break; + } else if (_currentToken == _fromToken) { + _i = _x; + } else if (_currentToken == _toToken) { + _j = _x; + } + } + require(_i != 0 || _j != 0); // dev: something went wrong + } + return ICurvePool(_pool).get_dy_underlying(_i, _j, _amountIn); + } +} diff --git a/src/pricecheckers/DynamicSlippageChecker.sol b/src/pricecheckers/DynamicSlippageChecker.sol new file mode 100644 index 0000000..abdf685 --- /dev/null +++ b/src/pricecheckers/DynamicSlippageChecker.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +pragma solidity ^0.7.6; +pragma abicoder v2; + +import "@openzeppelin/contracts/math/SafeMath.sol"; + +import {IPriceChecker} from "../../interfaces/IPriceChecker.sol"; +import {IExpectedOutCalculator} from "./IExpectedOutCalculator.sol"; + +// Like the `FixedSlippageChecker` except the user can pass in their desired +// allowed slippage % dynamically in the _data field. The rest of the _data +// is passed to the price checker. +contract DynamicSlippageChecker is IPriceChecker { + using SafeMath for uint256; + + string public NAME; + IExpectedOutCalculator public immutable EXPECTED_OUT_CALCULATOR; + + uint256 internal constant MAX_BPS = 10_000; + + constructor(string memory _name, address _expectedOutCalculator) { + NAME = _name; + EXPECTED_OUT_CALCULATOR = IExpectedOutCalculator( + _expectedOutCalculator + ); + } + + function checkPrice( + uint256 _amountIn, + address _fromToken, + address _toToken, + uint256, + uint256 _minOut, + bytes calldata _data + ) external view override returns (bool) { + (uint256 _allowedSlippageInBps, bytes memory _data) = abi.decode( + _data, + (uint256, bytes) + ); + + uint256 _expectedOut = EXPECTED_OUT_CALCULATOR.getExpectedOut( + _amountIn, + _fromToken, + _toToken, + _data + ); + + return + _minOut > + _expectedOut.mul(MAX_BPS.sub(_allowedSlippageInBps)).div(MAX_BPS); + } +} diff --git a/src/pricecheckers/FixedMaxFeePriceCheckerDecorator.sol b/src/pricecheckers/FixedMaxFeePriceCheckerDecorator.sol new file mode 100644 index 0000000..b73e2b9 --- /dev/null +++ b/src/pricecheckers/FixedMaxFeePriceCheckerDecorator.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +pragma solidity ^0.7.6; +pragma abicoder v2; + +import "@openzeppelin/contracts/math/SafeMath.sol"; + +import {IPriceChecker} from "../../interfaces/IPriceChecker.sol"; +import {IExpectedOutCalculator} from "./IExpectedOutCalculator.sol"; + +/// Specify a maximum allowed fee, denominated in `fromToken`. +/// This decorates an existing price checker to allow for composability. +contract FixedMaxFeePriceCheckerDecorator is IPriceChecker { + using SafeMath for uint256; + + string public NAME; + IPriceChecker public immutable PRICE_CHECKER; + + constructor(string memory _name, address _expectedOutCalculator) { + NAME = _name; + PRICE_CHECKER = IPriceChecker(_expectedOutCalculator); + } + + function checkPrice( + uint256 _amountIn, + address _fromToken, + address _toToken, + uint256 _feeAmount, + uint256 _minOut, + bytes calldata _data + ) external view override returns (bool) { + (uint256 _allowedFeeAmount, bytes memory _data) = abi.decode( + _data, + (uint256, bytes) + ); + + if (_feeAmount > _allowedFeeAmount) { + return false; + } + + return + PRICE_CHECKER.checkPrice( + _amountIn, + _fromToken, + _toToken, + _feeAmount, + _minOut, + _data + ); + } +} diff --git a/src/pricecheckers/FixedMinOutPriceChecker.sol b/src/pricecheckers/FixedMinOutPriceChecker.sol new file mode 100644 index 0000000..e110e44 --- /dev/null +++ b/src/pricecheckers/FixedMinOutPriceChecker.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +pragma solidity ^0.7.6; +pragma abicoder v2; + +import {IPriceChecker} from "../../interfaces/IPriceChecker.sol"; + +/// Specify a minimimum allowed out amount, denominated in `buyToken` you are willing to accept. +contract FixedMinOutPriceChecker is IPriceChecker { + function checkPrice( + uint256 _amountIn, + address _fromToken, + address _toToken, + uint256 _feeAmount, + uint256 _out, + bytes calldata _data + ) external view override returns (bool) { + uint256 minOut = abi.decode(_data, (uint256)); + return minOut <= _out; + } +} diff --git a/src/pricecheckers/FixedSlippageChecker.sol b/src/pricecheckers/FixedSlippageChecker.sol new file mode 100644 index 0000000..1aab8eb --- /dev/null +++ b/src/pricecheckers/FixedSlippageChecker.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +pragma solidity ^0.7.6; +pragma abicoder v2; + +import "@openzeppelin/contracts/math/SafeMath.sol"; + +import {IPriceChecker} from "../../interfaces/IPriceChecker.sol"; +import {IExpectedOutCalculator} from "./IExpectedOutCalculator.sol"; + +// Very basic slippage checker that checks that minOut is at least 100 - x% of +// expected out, where x is set at deployment time. E.g., could check that minOut +// is at least 90% of expected out. +contract FixedSlippageChecker is IPriceChecker { + using SafeMath for uint256; + + string public NAME; + uint256 public immutable ALLOWED_SLIPPAGE_IN_BPS; + IExpectedOutCalculator public immutable EXPECTED_OUT_CALCULATOR; + + uint256 internal constant MAX_BPS = 10_000; + + constructor( + string memory _name, + uint256 _allowedSlippageInBps, + address _expectedOutCalculator + ) { + require(_allowedSlippageInBps <= MAX_BPS); + + NAME = _name; + ALLOWED_SLIPPAGE_IN_BPS = _allowedSlippageInBps; + EXPECTED_OUT_CALCULATOR = IExpectedOutCalculator( + _expectedOutCalculator + ); + } + + function checkPrice( + uint256 _amountIn, + address _fromToken, + address _toToken, + uint256, + uint256 _minOut, + bytes calldata _data + ) external view override returns (bool) { + uint256 _expectedOut = EXPECTED_OUT_CALCULATOR.getExpectedOut( + _amountIn, + _fromToken, + _toToken, + _data + ); + + return + _minOut > + _expectedOut.mul(MAX_BPS.sub(ALLOWED_SLIPPAGE_IN_BPS)).div(MAX_BPS); + } +} diff --git a/src/pricecheckers/IExpectedOutCalculator.sol b/src/pricecheckers/IExpectedOutCalculator.sol new file mode 100644 index 0000000..3900d44 --- /dev/null +++ b/src/pricecheckers/IExpectedOutCalculator.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.7.6; +pragma abicoder v2; + +interface IExpectedOutCalculator { + function getExpectedOut( + uint256 _amountIn, + address _fromToken, + address _toToken, + bytes calldata _data + ) external view returns (uint256); +} diff --git a/src/pricecheckers/MetaExpectedOutCalculator.sol b/src/pricecheckers/MetaExpectedOutCalculator.sol new file mode 100644 index 0000000..6541b55 --- /dev/null +++ b/src/pricecheckers/MetaExpectedOutCalculator.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +pragma solidity ^0.7.6; +pragma abicoder v2; + +import "@openzeppelin/contracts/math/SafeMath.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {IExpectedOutCalculator} from "./IExpectedOutCalculator.sol"; + +/** + * @notice Uses multiple other expected out calculators to generate an expected out. + */ +contract MetaExpectedOutCalculator is IExpectedOutCalculator { + using SafeMath for uint256; + + /** + * @param _data Encoded [swapPath, priceCheckers, priceCheckerData]. + * + * swapPath (address[]): List of ERC20s to swap through. + * expectedOutCalculators (address[]): List of expected out calculators to use. + * expectedOutCalculatorData (bytes[]): List of bytes to pass to each expected out calculator + */ + function getExpectedOut( + uint256 _amountIn, + address _fromToken, + address _toToken, + bytes calldata _data + ) external view override returns (uint256) { + ( + address[] memory _swapPath, + address[] memory _expectedOutCalculators, + bytes[] memory _expectedOutCalculatorData + ) = abi.decode(_data, (address[], address[], bytes[])); + + require( + _swapPath.length.sub(1) == _expectedOutCalculators.length && + _expectedOutCalculators.length == + _expectedOutCalculatorData.length, + "invalid_length" + ); + + uint256 _runningExpectedOut; + for (uint256 i = 0; i < _swapPath.length.sub(1); i++) { + _runningExpectedOut = i == 0 + ? IExpectedOutCalculator(_expectedOutCalculators[i]) + .getExpectedOut( + _amountIn, + _swapPath[i], + _swapPath[i + 1], + _expectedOutCalculatorData[i] + ) + : IExpectedOutCalculator(_expectedOutCalculators[i]) + .getExpectedOut( + _runningExpectedOut, + _swapPath[i], + _swapPath[i + 1], + _expectedOutCalculatorData[i] + ); + } + + return _runningExpectedOut; + } +} diff --git a/src/pricecheckers/PRICE_CHECKERS.md b/src/pricecheckers/PRICE_CHECKERS.md new file mode 100644 index 0000000..b74532d --- /dev/null +++ b/src/pricecheckers/PRICE_CHECKERS.md @@ -0,0 +1,18 @@ +## Price Checker Internals + +Price checking can be split into two stages: one to determine an 'expected out' +and another to determine how far away the minOut can be from that expected out. +For example, for a TOKE -> WETH swap, we could determine the expected out by +calling getAmountsOut on the TOKE/WETH SushiSwap pool and then fulfill the second +stage by only returning true if minOut is at least 90% of what getAmountsOut returned. + +To make things modular, we split these two steps across contracts: a slippage +checker and an expected out calculator (creative names, amirite?). The slippage +checker implements the IPriceChecker interface and calls the expected out calculator. +This allows the two to independently vary. In other words, you can write your +own expected out calculator (e.g., for UniV4) without needing to write any +slippage checking logic. + +Of course, if you write your own price checker, you don't need to conform to this +pattern. If you wanted, you could have a monolithic price checker or even split +price checkers into more contracts. diff --git a/src/pricecheckers/PriceCheckerLib.sol b/src/pricecheckers/PriceCheckerLib.sol new file mode 100644 index 0000000..e204657 --- /dev/null +++ b/src/pricecheckers/PriceCheckerLib.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +pragma solidity ^0.7.6; +pragma abicoder v2; + +import "@openzeppelin/contracts/math/SafeMath.sol"; + +library PriceCheckerLib { + using SafeMath for uint256; + + uint256 internal constant MAX_BPS = 10_000; + + function getMaxSlippage( + uint256 _inputMaxSlippage, + uint256 _defaultMaxSlippage + ) internal pure returns (uint256) { + require(_inputMaxSlippage <= 10_000); // dev: max slippage too high + + if (_inputMaxSlippage == 0) { + return _defaultMaxSlippage; + } else { + return _inputMaxSlippage; + } + } + + /// @dev performs a double-ended slippage check, ensuring that minOut is both greater than market value - max slippage and less than market value + max slippage. + function isMinOutAcceptable( + uint256 _minOut, + uint256 _marketValueOfAmountIn, + uint256 _maxSlippageInBips + ) internal pure returns (bool) { + return + _minOut > + _marketValueOfAmountIn.mul(MAX_BPS.sub(_maxSlippageInBips)).div( + MAX_BPS + ) && + _minOut < + _marketValueOfAmountIn.mul(MAX_BPS.add(_maxSlippageInBips)).div( + MAX_BPS + ); + } +} diff --git a/src/pricecheckers/SingleSidedBalancerBalWethExpectedOutCalculator.sol b/src/pricecheckers/SingleSidedBalancerBalWethExpectedOutCalculator.sol new file mode 100644 index 0000000..ae4cf51 --- /dev/null +++ b/src/pricecheckers/SingleSidedBalancerBalWethExpectedOutCalculator.sol @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +pragma solidity ^0.7.6; +pragma abicoder v2; + +import "@openzeppelin/contracts/math/SafeMath.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@balancer/lib/math/FixedPoint.sol"; + +import {IExpectedOutCalculator} from "./IExpectedOutCalculator.sol"; + +interface IPriceFeed { + function latestAnswer() external view returns (int256); + + function decimals() external view returns (uint8); +} + +interface IWeightedPool { + function getInvariant() external view returns (uint256); + + function getVault() external view returns (address); + + function totalSupply() external view returns (uint256); +} + +interface IVault { + struct UserBalanceOp { + UserBalanceOpKind kind; + address asset; + uint256 amount; + address sender; + address payable recipient; + } + + function manageUserBalance(UserBalanceOp[] memory ops) external payable; + + enum UserBalanceOpKind { + DEPOSIT_INTERNAL, + WITHDRAW_INTERNAL, + TRANSFER_INTERNAL, + TRANSFER_EXTERNAL + } +} + +library VaultReentrancyLib { + function ensureNotInVaultContext(IVault vault) internal view { + bytes32 REENTRANCY_ERROR_HASH = keccak256( + abi.encodeWithSignature("Error(string)", "BAL#400") + ); + + // read-only re-entrancy protection - this call is always unsuccessful but we need to make sure + // it didn't fail due to a re-entrancy attack + (, bytes memory revertData) = address(vault).staticcall{gas: 10_000}( + abi.encodeWithSelector( + vault.manageUserBalance.selector, + new address[](0) + ) + ); + + require(keccak256(revertData) != REENTRANCY_ERROR_HASH); + } +} + +contract SingleSidedBalancerBalWethExpectedOutCalculator is + IExpectedOutCalculator +{ + using SafeMath for uint256; + using FixedPoint for uint256; + using VaultReentrancyLib for IVault; + + address public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + address public constant BAL = 0xba100000625a3754423978a60c9317c58a424e3D; + IWeightedPool public constant BAL_WETH_POOL = + IWeightedPool(0x5c6Ee304399DBdB9C8Ef030aB642B10820DB8F56); + IVault internal constant BALANCER_VAULT = + IVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8); + IPriceFeed internal constant BAL_ETH_FEED = + IPriceFeed(0xC1438AA3823A6Ba0C159CfA8D98dF5A994bA120b); + + uint256 internal constant ZERO_POINT_EIGHT = 8e17; + uint256 internal constant ZERO_POINT_TWO = 2e17; + uint256 internal constant TEN = 1e19; + + function getExpectedOut( + uint256 _amountIn, + address _fromToken, + address _toToken, + bytes calldata + ) external view override returns (uint256) { + require(_toToken == address(BAL_WETH_POOL)); + require(_fromToken == WETH || _fromToken == BAL); + + BALANCER_VAULT.ensureNotInVaultContext(); + + uint256 kOverS = BAL_WETH_POOL.getInvariant().mul(1e18).div( + BAL_WETH_POOL.totalSupply() + ); + + if (_fromToken == WETH) { + int256 ethPriceOfBal = BAL_ETH_FEED.latestAnswer(); + require(ethPriceOfBal > 0); + + uint256 balFactor = uint256(ethPriceOfBal) + .mul(1e18) + .div(ZERO_POINT_EIGHT) + .powUp(ZERO_POINT_EIGHT); + uint256 ethFactor = FixedPoint + .ONE + .mul(1e18) + .div(ZERO_POINT_TWO) + .powUp(ZERO_POINT_TWO); + + // what a BPT is worth in ETH + uint256 ethValueOfBPT = kOverS + .mul(balFactor) + .div(1e18) + .mul(ethFactor) + .div(1e18); + + return _amountIn.mul(1e18).div(ethValueOfBPT); + } else { + // how many bal per eth? + int256 ethPriceOfBal = BAL_ETH_FEED.latestAnswer(); + require(ethPriceOfBal > 0); + + uint256 balPriceOfEth = FixedPoint.ONE.mul(1e18).div( + uint256(ethPriceOfBal) + ); + + uint256 balFactor = FixedPoint + .ONE + .mul(1e18) + .div(ZERO_POINT_EIGHT) + .powUp(ZERO_POINT_EIGHT); + uint256 ethFactor = balPriceOfEth + .mul(1e18) + .div(ZERO_POINT_TWO) + .powUp(ZERO_POINT_TWO); + + // what a BPT is worth in BAL + uint256 balValueOfBPT = kOverS + .mul(balFactor) + .div(1e18) + .mul(ethFactor) + .div(1e18); + + return _amountIn.mul(1e18).div(balValueOfBPT); + } + } +} diff --git a/src/pricecheckers/UniV2ExpectedOutCalculator.sol b/src/pricecheckers/UniV2ExpectedOutCalculator.sol new file mode 100644 index 0000000..cf2bf6f --- /dev/null +++ b/src/pricecheckers/UniV2ExpectedOutCalculator.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +pragma solidity ^0.7.6; +pragma abicoder v2; + +import "@openzeppelin/contracts/math/SafeMath.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {IExpectedOutCalculator} from "./IExpectedOutCalculator.sol"; +import "../../interfaces/IUniV2.sol"; + +contract UniV2ExpectedOutCalculator is IExpectedOutCalculator { + using SafeMath for uint256; + + address internal constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + + string public NAME; + address public immutable UNIV2_ROUTER; + + constructor(string memory _name, address _univ2Router) { + NAME = _name; + UNIV2_ROUTER = _univ2Router; + } + + function getExpectedOut( + uint256 _amountIn, + address _fromToken, + address _toToken, + bytes calldata + ) external view override returns (uint256) { + uint256[] memory amounts; + + if (_fromToken == WETH || _toToken == WETH) { + address[] memory path = new address[](2); + + path[0] = _fromToken; + path[1] = _toToken; + + amounts = IUniV2(UNIV2_ROUTER).getAmountsOut(_amountIn, path); + } else { + address[] memory path = new address[](3); + path[0] = _fromToken; // token to swap + path[1] = WETH; // weth + path[2] = _toToken; + + amounts = IUniV2(UNIV2_ROUTER).getAmountsOut(_amountIn, path); + } + + return amounts[amounts.length - 1]; + } +} diff --git a/src/pricecheckers/UniV3ExpectedOutCalculator.sol b/src/pricecheckers/UniV3ExpectedOutCalculator.sol new file mode 100644 index 0000000..83c14eb --- /dev/null +++ b/src/pricecheckers/UniV3ExpectedOutCalculator.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +pragma solidity ^0.7.6; +pragma abicoder v2; + +import "@openzeppelin/contracts/math/SafeMath.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {IExpectedOutCalculator} from "./IExpectedOutCalculator.sol"; + +interface IUniswapV3StaticQuoter { + /// @notice Returns the amount out received for a given exact input swap without executing the swap + /// @param path The path of the swap, i.e. each token pair and the pool fee + /// @param amountIn The amount of the first token to swap + /// @return amountOut The amount of the last token that would be received + function quoteExactInput(bytes memory path, uint256 amountIn) + external + view + returns (uint256 amountOut); +} + +contract UniV3ExpectedOutCalculator is IExpectedOutCalculator { + using SafeMath for uint256; + + IUniswapV3StaticQuoter internal constant QUOTER = + IUniswapV3StaticQuoter(0x7637Aaeb5BD58269B782726680d83f72C651aE74); + + /** + * @param _data Encoded [swapPath, poolFees]. + * + * swapPath (address[]): List of ERC20s to swap through. + * poolFees (uint24[]): Pool fee for the pool to swap through, denominated in bips. + * + * Some examples: + * AAVE -> DAI: [[address(AAVE), address(WETH), address(DAI)], [30, 5]] + * USDT -> USDC: [[address(USDT), address(USDC)], [1]] + */ + function getExpectedOut( + uint256 _amountIn, + address _fromToken, + address _toToken, + bytes calldata _data + ) external view override returns (uint256) { + (address[] memory _swapPath, uint24[] memory _poolFees) = abi.decode( + _data, + (address[], uint24[]) + ); + + return _getExpectedOut(_amountIn, _swapPath, _poolFees); + } + + function _getExpectedOut( + uint256 _amountIn, + address[] memory _swapPath, + uint24[] memory _poolFees + ) internal view returns (uint256) { + require(_swapPath.length >= 2); // dev: must have at least two assets in swap path + require(_poolFees.length.add(1) == _swapPath.length); // dev: must be one more asset in swap path than pool fee + + // path is packed bytes with the form [asset0, poolFee0, asset1, poolFee1, asset2] + bytes memory _path = abi.encodePacked(_swapPath[0]); + + for (uint256 _i = 0; _i < _poolFees.length; _i++) { + // they ingest fees in 1 100ths of a bip, so we multiply by 100 + _path = abi.encodePacked( + _path, + uint24(uint256(_poolFees[_i]).mul(100)), + _swapPath[_i.add(1)] + ); + } + + return QUOTER.quoteExactInput(_path, _amountIn); + } +} diff --git a/src/pricecheckers/ValidFromPriceCheckerDecorator.sol b/src/pricecheckers/ValidFromPriceCheckerDecorator.sol new file mode 100644 index 0000000..3a911c1 --- /dev/null +++ b/src/pricecheckers/ValidFromPriceCheckerDecorator.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +pragma solidity ^0.7.6; +pragma abicoder v2; + +import {IPriceChecker} from "../../interfaces/IPriceChecker.sol"; + +/// Specify a validFrom timestamp, which the current block has to have surpassed in order for the order to be valid. +/// This decorates an existing price checker to allow for composability. +contract ValidFromPriceCheckerDecorator is IPriceChecker { + function checkPrice( + uint256 _amountIn, + address _fromToken, + address _toToken, + uint256 _feeAmount, + uint256 _minOut, + bytes calldata _data + ) external view override returns (bool) { + (uint256 _validFrom, address _priceChecker, bytes memory _data) = abi + .decode(_data, (uint256, address, bytes)); + + if (_validFrom > block.timestamp) { + return false; + } + + return + IPriceChecker(_priceChecker).checkPrice( + _amountIn, + _fromToken, + _toToken, + _feeAmount, + _minOut, + _data + ); + } +} From 802e1aaab417a9073cbf396863d1bbd34378c4e3 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Thu, 25 Jan 2024 14:23:37 -0500 Subject: [PATCH 02/54] feat: add foundry.toml --- foundry.toml | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 foundry.toml diff --git a/foundry.toml b/foundry.toml new file mode 100644 index 0000000..e69de29 From d7bd782b65c17c07c77bc2377a22618ee4cc4a77 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Thu, 25 Jan 2024 14:25:20 -0500 Subject: [PATCH 03/54] chore: remove commit hooks --- package.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/package.json b/package.json index a118fab..ca49c07 100644 --- a/package.json +++ b/package.json @@ -13,11 +13,5 @@ "lint": "pretty-quick --pattern '**/*.*(sol|json)' --verbose", "lint:check": "prettier --check **/*.sol **/*.json", "lint:fix": "prettier --write **/*.sol **/*.json **/**/*.sol" - }, - "husky": { - "hooks": { - "pre-commit": "yarn lint:fix", - "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" - } } } From 64b85a4816603942c572891a4ba285c3f1483e1b Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Thu, 25 Jan 2024 14:25:31 -0500 Subject: [PATCH 04/54] forge install: openzeppelin-contracts --- .gitmodules | 3 +++ lib/openzeppelin-contracts | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 lib/openzeppelin-contracts diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..2c9179b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/openzeppelin-contracts"] + path = lib/openzeppelin-contracts + url = https://github.com/openzeppelin/openzeppelin-contracts diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts new file mode 160000 index 0000000..b5a7f97 --- /dev/null +++ b/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit b5a7f977d8a57b6854545522e36d91a0c11723cd From 1445f34f5e4b95b7c1ed439387b5580eb5088869 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Thu, 25 Jan 2024 14:26:00 -0500 Subject: [PATCH 05/54] chore: setup foundry.toml --- foundry.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/foundry.toml b/foundry.toml index e69de29..25e5d83 100644 --- a/foundry.toml +++ b/foundry.toml @@ -0,0 +1 @@ +profile = { default = { libs = ["node_modules", "lib"] } } From dfd7bf744a0f13603c6464b2e03e526e04eaa6b9 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Thu, 25 Jan 2024 14:27:19 -0500 Subject: [PATCH 06/54] chore: remove unversioned oz --- .gitmodules | 3 --- lib/openzeppelin-contracts | 1 - 2 files changed, 4 deletions(-) delete mode 160000 lib/openzeppelin-contracts diff --git a/.gitmodules b/.gitmodules index 2c9179b..e69de29 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "lib/openzeppelin-contracts"] - path = lib/openzeppelin-contracts - url = https://github.com/openzeppelin/openzeppelin-contracts diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts deleted file mode 160000 index b5a7f97..0000000 --- a/lib/openzeppelin-contracts +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b5a7f977d8a57b6854545522e36d91a0c11723cd From 63020f6e9e3bc578d918fddc7fa2f366cfbd7b83 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Thu, 25 Jan 2024 14:28:51 -0500 Subject: [PATCH 07/54] forge install: openzeppelin-contracts --- .gitmodules | 3 +++ lib/openzeppelin-contracts | 1 + 2 files changed, 4 insertions(+) create mode 160000 lib/openzeppelin-contracts diff --git a/.gitmodules b/.gitmodules index e69de29..2c9179b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/openzeppelin-contracts"] + path = lib/openzeppelin-contracts + url = https://github.com/openzeppelin/openzeppelin-contracts diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts new file mode 160000 index 0000000..5c8746f --- /dev/null +++ b/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit 5c8746f56b4bed8cc9e0e044f5f69ab2f9428ce1 From 7966e8ae25eda38d936b162b55f36aa3cb3a93f2 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Thu, 25 Jan 2024 14:29:54 -0500 Subject: [PATCH 08/54] forge install: contracts --- .gitmodules | 3 +++ lib/contracts | 1 + 2 files changed, 4 insertions(+) create mode 160000 lib/contracts diff --git a/.gitmodules b/.gitmodules index 2c9179b..b81e750 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/openzeppelin/openzeppelin-contracts +[submodule "lib/contracts"] + path = lib/contracts + url = https://github.com/cowprotocol/contracts diff --git a/lib/contracts b/lib/contracts new file mode 160000 index 0000000..c0e7e45 --- /dev/null +++ b/lib/contracts @@ -0,0 +1 @@ +Subproject commit c0e7e45cfcba3822ccab73e48c3066c32ed8de48 From 18687039d29b567598568591f9388a314ff220d9 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Thu, 25 Jan 2024 14:30:42 -0500 Subject: [PATCH 09/54] forge install: balancer-v2-monorepo --- .gitmodules | 3 +++ lib/balancer-v2-monorepo | 1 + 2 files changed, 4 insertions(+) create mode 160000 lib/balancer-v2-monorepo diff --git a/.gitmodules b/.gitmodules index b81e750..67de538 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "lib/contracts"] path = lib/contracts url = https://github.com/cowprotocol/contracts +[submodule "lib/balancer-v2-monorepo"] + path = lib/balancer-v2-monorepo + url = https://github.com/balancer/balancer-v2-monorepo diff --git a/lib/balancer-v2-monorepo b/lib/balancer-v2-monorepo new file mode 160000 index 0000000..c5b42e8 --- /dev/null +++ b/lib/balancer-v2-monorepo @@ -0,0 +1 @@ +Subproject commit c5b42e8cd4ffe62a82ba85263eac498df7a913b5 From 613cbe570d6c5d7665f84e7ff78cc490ed489296 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Thu, 25 Jan 2024 14:41:00 -0500 Subject: [PATCH 10/54] feat: add remappings so `forge build` works --- .gitignore | 2 ++ remappings.txt | 3 +++ 2 files changed, 5 insertions(+) create mode 100644 remappings.txt diff --git a/.gitignore b/.gitignore index 8dc5c43..320eab8 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ build/ reports/ node_modules/ .DS_Store +cache +out diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 0000000..5aaeba5 --- /dev/null +++ b/remappings.txt @@ -0,0 +1,3 @@ +@openzeppelin/=lib/openzeppelin-contracts/ +@cow-protocol/=lib/contracts/src +@balancer=lib/balancer-v2-monorepo/contracts \ No newline at end of file From 44dcfbb759b128bc9ecccd46842a403d32ab981e Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Thu, 25 Jan 2024 15:45:28 -0500 Subject: [PATCH 11/54] test: start test file --- test/Milkman.t.sol | 53 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 test/Milkman.t.sol diff --git a/test/Milkman.t.sol b/test/Milkman.t.sol new file mode 100644 index 0000000..64ecd2a --- /dev/null +++ b/test/Milkman.t.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.7.6; + +import "forge-std/Test.sol"; +import "../src/Milkman.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract MockERC20 is IERC20 { + // Mock ERC20 implementation (or use a library like OpenZeppelin's mock contracts) + // Implement necessary functions like mint, transfer, etc. +} + +contract PriceCheckerMock { + // Mock implementation of IPriceChecker + // Implement necessary functions to simulate price checking +} + +contract MilkmanTest is Test { + Milkman milkman; + MockERC20 fromToken; + MockERC20 toToken; + PriceCheckerMock priceChecker; + + function setUp() public { + milkman = new Milkman(); + fromToken = new MockERC20(); + toToken = new MockERC20(); + priceChecker = new PriceCheckerMock(); + + // Additional setup like minting tokens, setting allowances, etc. + } + + function testRequestSwapExactTokensForTokens() public { + // Arrange: Set up the state before calling the function + uint256 amountIn = 1e18; // Example amount + + // Act: Call the function you want to test + milkman.requestSwapExactTokensForTokens( + amountIn, + fromToken, + toToken, + address(this), // Receiver address + address(priceChecker), + "" // priceCheckerData + ); + + // Assert: Check the state after calling the function + // Example: Assert that the swap was requested correctly + assertTrue(/* condition to validate */); + } + + // Additional test cases for different scenarios and edge cases +} From 52fe2f53a4239e9324c727a3d00154bae6f494d5 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Thu, 25 Jan 2024 15:45:37 -0500 Subject: [PATCH 12/54] forge install: forge-std --- .gitmodules | 3 +++ lib/forge-std | 1 + 2 files changed, 4 insertions(+) create mode 160000 lib/forge-std diff --git a/.gitmodules b/.gitmodules index 67de538..48b9336 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "lib/balancer-v2-monorepo"] path = lib/balancer-v2-monorepo url = https://github.com/balancer/balancer-v2-monorepo +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/lib/forge-std b/lib/forge-std new file mode 160000 index 0000000..4513bc2 --- /dev/null +++ b/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 4513bc2063f23c57bee6558799584b518d387a39 From a16400d0eb13a2c10fd6e5d143177267588d3829 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Thu, 25 Jan 2024 15:51:27 -0500 Subject: [PATCH 13/54] test: get very basic test set up --- test/Milkman.t.sol | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/test/Milkman.t.sol b/test/Milkman.t.sol index 64ecd2a..851943c 100644 --- a/test/Milkman.t.sol +++ b/test/Milkman.t.sol @@ -1,31 +1,32 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.7.6; +pragma abicoder v2; import "forge-std/Test.sol"; import "../src/Milkman.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -contract MockERC20 is IERC20 { - // Mock ERC20 implementation (or use a library like OpenZeppelin's mock contracts) - // Implement necessary functions like mint, transfer, etc. -} +// contract MockERC20 is IERC20 { +// // Mock ERC20 implementation (or use a library like OpenZeppelin's mock contracts) +// // Implement necessary functions like mint, transfer, etc. +// } -contract PriceCheckerMock { - // Mock implementation of IPriceChecker - // Implement necessary functions to simulate price checking -} +// contract PriceCheckerMock { +// // Mock implementation of IPriceChecker +// // Implement necessary functions to simulate price checking +// } contract MilkmanTest is Test { Milkman milkman; - MockERC20 fromToken; - MockERC20 toToken; - PriceCheckerMock priceChecker; + IERC20 fromToken; + IERC20 toToken; + address priceChecker; function setUp() public { milkman = new Milkman(); - fromToken = new MockERC20(); - toToken = new MockERC20(); - priceChecker = new PriceCheckerMock(); + // fromToken = new MockERC20(); + // toToken = new MockERC20(); + // priceChecker = new PriceCheckerMock(); // Additional setup like minting tokens, setting allowances, etc. } @@ -46,7 +47,7 @@ contract MilkmanTest is Test { // Assert: Check the state after calling the function // Example: Assert that the swap was requested correctly - assertTrue(/* condition to validate */); + assertTrue(true); } // Additional test cases for different scenarios and edge cases From 7723013449286cf48e6fd49223bec292e11352a2 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Thu, 25 Jan 2024 16:45:52 -0500 Subject: [PATCH 14/54] test: setup various tokens to sell --- test/Milkman.t.sol | 40 +++++++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/test/Milkman.t.sol b/test/Milkman.t.sol index 851943c..eef3011 100644 --- a/test/Milkman.t.sol +++ b/test/Milkman.t.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: LGPL-3.0-or-later pragma solidity ^0.7.6; pragma abicoder v2; @@ -22,13 +22,43 @@ contract MilkmanTest is Test { IERC20 toToken; address priceChecker; + mapping(string => address) private tokenAddress; + mapping(string => string) private sellToBuyMap; + string[] private tokensToSell; + function setUp() public { milkman = new Milkman(); - // fromToken = new MockERC20(); - // toToken = new MockERC20(); - // priceChecker = new PriceCheckerMock(); + + tokenAddress["TOKE"] = 0x2e9d63788249371f1DFC918a52f8d799F4a38C94; + tokenAddress["DAI"] = 0x6B175474E89094C44Da98b954EedeAC495271d0F; + tokenAddress["USDC"] = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + tokenAddress["USDT"] = 0xdAC17F958D2ee523a2206206994597C13D831ec7; + tokenAddress["GUSD"] = 0x056Fd409E1d7A124BD7017459dFEa2F387b6d5Cd; + tokenAddress["AAVE"] = 0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9; + tokenAddress["WETH"] = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + tokenAddress["BAT"] = 0x0D8775F648430679A709E98d2b0Cb6250d2887EF; + tokenAddress["ALCX"] = 0xdBdb4d16EdA451D0503b854CF79D55697F90c8DF; + tokenAddress["WBTC"] = 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599; + tokenAddress["UNI"] = 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984; + tokenAddress["BAL"] = 0xdBdb4d16EdA451D0503b854CF79D55697F90c8DF; + tokenAddress["BAL/WETH"] = 0x5c6Ee304399DBdB9C8Ef030aB642B10820DB8F56; + tokenAddress["YFI"] = 0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e; + tokenAddress["COW"] = 0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB; + + sellToBuyMap["TOKE"] = "DAI"; + sellToBuyMap["USDC"] = "USDT"; + sellToBuyMap["GUSD"] = "USDC"; + sellToBuyMap["AAVE"] = "WETH"; + sellToBuyMap["BAT"] = "ALCX"; + sellToBuyMap["WETH"] = "BAL/WETH"; + sellToBuyMap["UNI"] = "USDT"; + sellToBuyMap["ALCX"] = "TOKE"; + sellToBuyMap["BAL"] = "BAL/WETH"; + sellToBuyMap["YFI"] = "USDC"; + sellToBuyMap["USDT"] = "UNI"; + sellToBuyMap["COW"] = "DAI"; - // Additional setup like minting tokens, setting allowances, etc. + tokensToSell = ["TOKE", "USDC", "GUSD", "AAVE", "BAT", "WETH", "UNI", "ALCX", "BAL", "YFI", "USDT", "COW"]; } function testRequestSwapExactTokensForTokens() public { From 61a0f1d133090b8c70d73479ab4bf488629bb547 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Thu, 25 Jan 2024 17:27:28 -0500 Subject: [PATCH 15/54] test: add sushiswap expected out calculator --- test/Milkman.t.sol | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/test/Milkman.t.sol b/test/Milkman.t.sol index eef3011..f60e132 100644 --- a/test/Milkman.t.sol +++ b/test/Milkman.t.sol @@ -4,30 +4,25 @@ pragma abicoder v2; import "forge-std/Test.sol"; import "../src/Milkman.sol"; +import "../src/pricecheckers/UniV2ExpectedOutCalculator.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -// contract MockERC20 is IERC20 { -// // Mock ERC20 implementation (or use a library like OpenZeppelin's mock contracts) -// // Implement necessary functions like mint, transfer, etc. -// } - -// contract PriceCheckerMock { -// // Mock implementation of IPriceChecker -// // Implement necessary functions to simulate price checking -// } - contract MilkmanTest is Test { Milkman milkman; + address sushiswapExpectedOutCalculator; IERC20 fromToken; IERC20 toToken; address priceChecker; + address SUSHISWAP_ROUTER = 0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F; + mapping(string => address) private tokenAddress; mapping(string => string) private sellToBuyMap; string[] private tokensToSell; function setUp() public { milkman = new Milkman(); + sushiswapExpectedOutCalculator = address(new UniV2ExpectedOutCalculator("SUSHISWAP_EXPECTED_OUT_CALCULATOR", SUSHISWAP_ROUTER)); tokenAddress["TOKE"] = 0x2e9d63788249371f1DFC918a52f8d799F4a38C94; tokenAddress["DAI"] = 0x6B175474E89094C44Da98b954EedeAC495271d0F; From e0f9a2c32bc9ae29240305843453806d7b876af3 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Thu, 25 Jan 2024 18:23:13 -0500 Subject: [PATCH 16/54] test: get basic `requestSwapExactTokens` test passing --- Makefile | 30 +++++++++++++++ contracts/Milkman.sol | 64 ++++++++++++++++---------------- test/Milkman.t.sol | 85 ++++++++++++++++++++++++++++++++++++------- 3 files changed, 135 insertions(+), 44 deletions(-) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7c2f387 --- /dev/null +++ b/Makefile @@ -0,0 +1,30 @@ +# include .env file and export its env vars +# (-include to ignore error if it does not exist) +-include .env + +# deps +update:; forge update + +# Build & test +# change ETH_RPC_URL to another one (e.g., FTM_RPC_URL) for different chains +FORK_URL := ${ETH_RPC_URL} + +.PHONY: build test test-operation test-all-operation trace test-contract trace-contract deploy test-local trace-local clean snapshot + +# For deployments. Add all args without a comma +# ex: 0x316..FB5 "Name" 10 +constructor-args := + +build :; forge build +test :; forge test -vv --fork-url ${FORK_URL} --etherscan-api-key ${ETHERSCAN_API_KEY} +test-operation :; forge test -vvv --fork-url ${FORK_URL} --etherscan-api-key ${ETHERSCAN_API_KEY} --match-test "testProfitableHarvest_AngleFarmingProfit" +test-all-operation :; forge test -vv --fork-url ${FORK_URL} --etherscan-api-key ${ETHERSCAN_API_KEY} --match-contract "StrategyOperation" +trace :; forge test -vvv --fork-url ${FORK_URL} --etherscan-api-key ${ETHERSCAN_API_KEY} +test-contract :; forge test -vv --fork-url ${FORK_URL} --match-contract $(contract) --etherscan-api-key ${ETHERSCAN_API_KEY} +trace-contract :; forge test -vvv --fork-url ${FORK_URL} --match-contract $(contract) --etherscan-api-key ${ETHERSCAN_API_KEY} +deploy :; forge create --rpc-url ${FORK_URL} --constructor-args ${constructor-args} --private-key ${PRIV_KEY} src/Strategy.sol:Strategy --etherscan-api-key ${ETHERSCAN_API_KEY} --verify +# local tests without fork +test-local :; forge test +trace-local :; forge test -vvv +clean :; forge clean +snapshot :; forge snapshot \ No newline at end of file diff --git a/contracts/Milkman.sol b/contracts/Milkman.sol index c8703c8..d178882 100644 --- a/contracts/Milkman.sol +++ b/contracts/Milkman.sol @@ -68,37 +68,39 @@ contract Milkman { address priceChecker, bytes calldata priceCheckerData ) external { - require(address(this) == ROOT_MILKMAN, "!root_milkman"); // can't call `requestSwapExactTokensForTokens` from order contracts - require(priceChecker != address(0), "!price_checker"); // need to supply a valid price checker - - address orderContract = createOrderContract(); - - fromToken.safeTransferFrom(msg.sender, orderContract, amountIn); - - bytes32 _swapHash = keccak256( - abi.encode( - msg.sender, - to, - fromToken, - toToken, - amountIn, - priceChecker, - priceCheckerData - ) - ); - - Milkman(orderContract).initialize(fromToken, _swapHash); - - emit SwapRequested( - orderContract, - msg.sender, - amountIn, - address(fromToken), - address(toToken), - to, - priceChecker, - priceCheckerData - ); + // require(address(this) == ROOT_MILKMAN, "!root_milkman"); // can't call `requestSwapExactTokensForTokens` from order contracts + // require(priceChecker != address(0), "!price_checker"); // need to supply a valid price checker + + return; + + // address orderContract = createOrderContract(); + + // fromToken.safeTransferFrom(msg.sender, orderContract, amountIn); + + // bytes32 _swapHash = keccak256( + // abi.encode( + // msg.sender, + // to, + // fromToken, + // toToken, + // amountIn, + // priceChecker, + // priceCheckerData + // ) + // ); + + // Milkman(orderContract).initialize(fromToken, _swapHash); + + // emit SwapRequested( + // orderContract, + // msg.sender, + // amountIn, + // address(fromToken), + // address(toToken), + // to, + // priceChecker, + // priceCheckerData + // ); } function initialize(IERC20 fromToken, bytes32 _swapHash) external { diff --git a/test/Milkman.t.sol b/test/Milkman.t.sol index f60e132..5babf3d 100644 --- a/test/Milkman.t.sol +++ b/test/Milkman.t.sol @@ -3,26 +3,38 @@ pragma solidity ^0.7.6; pragma abicoder v2; import "forge-std/Test.sol"; +import "forge-std/console.sol"; import "../src/Milkman.sol"; import "../src/pricecheckers/UniV2ExpectedOutCalculator.sol"; +import "../src/pricecheckers/CurveExpectedOutCalculator.sol"; +import "../src/pricecheckers/UniV3ExpectedOutCalculator.sol"; +import "../src/pricecheckers/ChainlinkExpectedOutCalculator.sol"; +// import "../src/pricecheckers/SingleSidedBalancerBalWethExpectedOutCalculator.sol"; +import "../src/pricecheckers/FixedSlippageChecker.sol"; +import "../src/pricecheckers/DynamicSlippageChecker.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract MilkmanTest is Test { Milkman milkman; address sushiswapExpectedOutCalculator; + address sushiswapPriceChecker; IERC20 fromToken; IERC20 toToken; address priceChecker; + address whale; address SUSHISWAP_ROUTER = 0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F; mapping(string => address) private tokenAddress; mapping(string => string) private sellToBuyMap; string[] private tokensToSell; + mapping(string => uint256) private amounts; + mapping(string => address) private whaleAddresses; function setUp() public { milkman = new Milkman(); sushiswapExpectedOutCalculator = address(new UniV2ExpectedOutCalculator("SUSHISWAP_EXPECTED_OUT_CALCULATOR", SUSHISWAP_ROUTER)); + sushiswapPriceChecker = address(new FixedSlippageChecker("SUSHISWAP_500_BPS_PRICE_CHECKER", 500, sushiswapExpectedOutCalculator)); tokenAddress["TOKE"] = 0x2e9d63788249371f1DFC918a52f8d799F4a38C94; tokenAddress["DAI"] = 0x6B175474E89094C44Da98b954EedeAC495271d0F; @@ -53,26 +65,73 @@ contract MilkmanTest is Test { sellToBuyMap["USDT"] = "UNI"; sellToBuyMap["COW"] = "DAI"; - tokensToSell = ["TOKE", "USDC", "GUSD", "AAVE", "BAT", "WETH", "UNI", "ALCX", "BAL", "YFI", "USDT", "COW"]; + amounts["TOKE"] = 80000; // 80,000 TOKE + amounts["USDC"] = 5000000; // 5,000,000 USDC + amounts["GUSD"] = 1000; // 1,000 GUSD + amounts["AAVE"] = 2500; // 2,500 AAVE + amounts["BAT"] = 280000; // 280,000 BAT + amounts["WETH"] = 325; // 325 WETH + amounts["UNI"] = 80000; // 80,000 UNI + amounts["ALCX"] = 4000; // 4,000 ALCX + amounts["BAL"] = 300000; // 300,000 BAL + amounts["YFI"] = 3; // 3 YFI + amounts["USDT"] = 2000000; // 2,000,000 USDT + amounts["COW"] = 900000; // 900,000 COW + + whaleAddresses["GUSD"] = 0x5f65f7b609678448494De4C87521CdF6cEf1e932; + // whaleAddresses["USDT"] = 0xa929022c9107643515f5c777ce9a910f0d1e490c; + // whaleAddresses["WETH"] = 0x030ba81f1c18d280636f32af80b9aad02cf0854e; + // whaleAddresses["WBTC"] = 0xccf4429db6322d5c611ee964527d42e5d685dd6a; + whaleAddresses["DAI"] = 0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643; + whaleAddresses["USDC"] = 0x0A59649758aa4d66E25f08Dd01271e891fe52199; + whaleAddresses["LINK"] = 0x98C63b7B319dFBDF3d811530F2ab9DfE4983Af9D; + whaleAddresses["GNO"] = 0x4f8AD938eBA0CD19155a835f617317a6E788c868; + whaleAddresses["TOKE"] = 0x96F98Ed74639689C3A11daf38ef86E59F43417D3; + whaleAddresses["AAVE"] = 0x4da27a545c0c5B758a6BA100e3a049001de870f5; + whaleAddresses["BAT"] = 0x6C8c6b02E7b2BE14d4fA6022Dfd6d75921D90E4E; + whaleAddresses["UNI"] = 0x1a9C8182C09F50C8318d769245beA52c32BE35BC; + whaleAddresses["ALCX"] = 0x000000000000000000000000000000000000dEaD; + whaleAddresses["BAL"] = 0x10A19e7eE7d7F8a52822f6817de8ea18204F2e4f; + whaleAddresses["YFI"] = 0xFEB4acf3df3cDEA7399794D0869ef76A6EfAff52; + // whaleAddresses["COW"] = 0xca771eda0c70aa7d053ab1b25004559b918fe662; + + // tokensToSell = ["TOKE", "USDC", "GUSD", "AAVE", "BAT", "WETH", "UNI", "ALCX", "BAL", "YFI", "USDT", "COW"]; + tokensToSell = ["TOKE"]; } function testRequestSwapExactTokensForTokens() public { + for (uint8 i = 0; i < tokensToSell.length; i++) { + string memory tokenToSell = tokensToSell[i]; + string memory tokenToBuy = sellToBuyMap[tokenToSell]; + fromToken = IERC20(tokenAddress[tokenToSell]); + toToken = IERC20(tokenAddress[tokenToBuy]); + priceChecker = sushiswapPriceChecker; + whale = whaleAddresses[tokenToSell]; + + uint256 amountIn = amounts[tokenToSell] * 1e18; + + vm.prank(whale); + fromToken.approve(address(milkman), amountIn); + + vm.prank(whale); + milkman.requestSwapExactTokensForTokens( + 1e18, + fromToken, + toToken, + address(this), // Receiver address + priceChecker, + "" // priceCheckerData + ); + } + // priceChecker = sushiswapPriceChecker; // Arrange: Set up the state before calling the function - uint256 amountIn = 1e18; // Example amount - - // Act: Call the function you want to test - milkman.requestSwapExactTokensForTokens( - amountIn, - fromToken, - toToken, - address(this), // Receiver address - address(priceChecker), - "" // priceCheckerData - ); + // uint256 amountIn = 1e18; // Example amount + + // // Act: Call the function you want to test // Assert: Check the state after calling the function // Example: Assert that the swap was requested correctly - assertTrue(true); + // assertTrue(true); } // Additional test cases for different scenarios and edge cases From 5a5a5d8a28cb0f03b0e82ca064919884c742be84 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Thu, 25 Jan 2024 20:31:32 -0500 Subject: [PATCH 17/54] test: get the orderContract from the swap request --- test/Milkman.t.sol | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/test/Milkman.t.sol b/test/Milkman.t.sol index 5babf3d..4f7e828 100644 --- a/test/Milkman.t.sol +++ b/test/Milkman.t.sol @@ -20,9 +20,12 @@ contract MilkmanTest is Test { address sushiswapPriceChecker; IERC20 fromToken; IERC20 toToken; + uint256 amountIn; address priceChecker; address whale; + bytes32 SWAP_REQUESTED_EVENT = keccak256("SwapRequested(address,address,uint256,address,address,address,address,bytes)"); + address SUSHISWAP_ROUTER = 0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F; mapping(string => address) private tokenAddress; @@ -105,6 +108,7 @@ contract MilkmanTest is Test { string memory tokenToBuy = sellToBuyMap[tokenToSell]; fromToken = IERC20(tokenAddress[tokenToSell]); toToken = IERC20(tokenAddress[tokenToBuy]); + amountIn = amounts[tokenToSell] * 1e18; priceChecker = sushiswapPriceChecker; whale = whaleAddresses[tokenToSell]; @@ -113,15 +117,34 @@ contract MilkmanTest is Test { vm.prank(whale); fromToken.approve(address(milkman), amountIn); + vm.recordLogs(); + vm.prank(whale); milkman.requestSwapExactTokensForTokens( - 1e18, + amountIn, fromToken, toToken, address(this), // Receiver address priceChecker, "" // priceCheckerData ); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + + assertEq(entries[3].topics[0], SWAP_REQUESTED_EVENT); + + (address orderContract,,,,,,,) = (abi.decode(entries[3].data, (address,address,uint256,address,address,address,address,bytes))); + + assertEq(fromToken.balanceOf(orderContract), amountIn); + + // console.log(orderContract); + // console.lo(fromToken.balanceOf(orderContract)); + + // console.log("log", entries[3].topics.length); + + // assertEq(entries.length, 1); + + // vm.expectEmit(true, true, true, true); } // priceChecker = sushiswapPriceChecker; // Arrange: Set up the state before calling the function From 06bb6e6eea636ac9b1fc487a4deb5be2217c15b4 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Thu, 25 Jan 2024 20:41:03 -0500 Subject: [PATCH 18/54] test: check the swap hash --- test/Milkman.t.sol | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/Milkman.t.sol b/test/Milkman.t.sol index 4f7e828..a12e633 100644 --- a/test/Milkman.t.sol +++ b/test/Milkman.t.sol @@ -137,6 +137,19 @@ contract MilkmanTest is Test { assertEq(fromToken.balanceOf(orderContract), amountIn); + bytes32 expectedSwapHash = keccak256( + abi.encode( + whale, + address(this), + fromToken, + toToken, + amountIn, + priceChecker, + bytes("") + ) + ); + assertEq(Milkman(orderContract).swapHash(), expectedSwapHash); + // console.log(orderContract); // console.lo(fromToken.balanceOf(orderContract)); From 32b6d811196558122485a3809a02e76ecfb13a33 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Thu, 25 Jan 2024 20:43:07 -0500 Subject: [PATCH 19/54] forge install: surl --- .gitmodules | 3 +++ lib/surl | 1 + 2 files changed, 4 insertions(+) create mode 160000 lib/surl diff --git a/.gitmodules b/.gitmodules index 48b9336..8e8bcfa 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,6 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "lib/surl"] + path = lib/surl + url = https://github.com/memester-xyz/surl diff --git a/lib/surl b/lib/surl new file mode 160000 index 0000000..034c912 --- /dev/null +++ b/lib/surl @@ -0,0 +1 @@ +Subproject commit 034c912ae9b5e707a5afd21f145b452ad8e800df From afda9c2d718b8d8d6ab832522999b1ac3d68f894 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Fri, 26 Jan 2024 08:13:56 -0500 Subject: [PATCH 20/54] chore: import Surl --- test/Milkman.t.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/test/Milkman.t.sol b/test/Milkman.t.sol index a12e633..a89dfdd 100644 --- a/test/Milkman.t.sol +++ b/test/Milkman.t.sol @@ -4,6 +4,7 @@ pragma abicoder v2; import "forge-std/Test.sol"; import "forge-std/console.sol"; +import {Surl} from "surl/Surl.sol"; import "../src/Milkman.sol"; import "../src/pricecheckers/UniV2ExpectedOutCalculator.sol"; import "../src/pricecheckers/CurveExpectedOutCalculator.sol"; From 7aedc6513027545d0a953e6de50bf9c4569ee65c Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Fri, 26 Jan 2024 08:14:24 -0500 Subject: [PATCH 21/54] chore: remove 0.8.x surl --- .gitmodules | 3 --- lib/surl | 1 - 2 files changed, 4 deletions(-) delete mode 160000 lib/surl diff --git a/.gitmodules b/.gitmodules index 8e8bcfa..48b9336 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,6 +10,3 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std -[submodule "lib/surl"] - path = lib/surl - url = https://github.com/memester-xyz/surl diff --git a/lib/surl b/lib/surl deleted file mode 160000 index 034c912..0000000 --- a/lib/surl +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 034c912ae9b5e707a5afd21f145b452ad8e800df From 5f1faca32c8b49302164083e30a5e3638957f24e Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Fri, 26 Jan 2024 08:14:46 -0500 Subject: [PATCH 22/54] forge install: surl-0.7 --- .gitmodules | 3 +++ lib/surl-0.7 | 1 + 2 files changed, 4 insertions(+) create mode 160000 lib/surl-0.7 diff --git a/.gitmodules b/.gitmodules index 48b9336..8c65823 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,6 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "lib/surl-0.7"] + path = lib/surl-0.7 + url = https://github.com/charlesndalton/surl-0.7 diff --git a/lib/surl-0.7 b/lib/surl-0.7 new file mode 160000 index 0000000..443d6ba --- /dev/null +++ b/lib/surl-0.7 @@ -0,0 +1 @@ +Subproject commit 443d6ba0620eeb5cdc67eb45937822ec703dcb29 From 041f1c815f2c232ed315b9ed4efe00f2f3efc632 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Fri, 26 Jan 2024 08:41:14 -0500 Subject: [PATCH 23/54] test: call into CoW quote endpoint --- Makefile | 2 +- remappings.txt | 3 ++- test/Milkman.t.sol | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 7c2f387..9851248 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ FORK_URL := ${ETH_RPC_URL} constructor-args := build :; forge build -test :; forge test -vv --fork-url ${FORK_URL} --etherscan-api-key ${ETHERSCAN_API_KEY} +test :; forge test -vv --fork-url ${FORK_URL} --etherscan-api-key ${ETHERSCAN_API_KEY} --ffi test-operation :; forge test -vvv --fork-url ${FORK_URL} --etherscan-api-key ${ETHERSCAN_API_KEY} --match-test "testProfitableHarvest_AngleFarmingProfit" test-all-operation :; forge test -vv --fork-url ${FORK_URL} --etherscan-api-key ${ETHERSCAN_API_KEY} --match-contract "StrategyOperation" trace :; forge test -vvv --fork-url ${FORK_URL} --etherscan-api-key ${ETHERSCAN_API_KEY} diff --git a/remappings.txt b/remappings.txt index 5aaeba5..2b9294c 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,3 +1,4 @@ @openzeppelin/=lib/openzeppelin-contracts/ @cow-protocol/=lib/contracts/src -@balancer=lib/balancer-v2-monorepo/contracts \ No newline at end of file +@balancer=lib/balancer-v2-monorepo/contracts +surl=lib/surl-0.7/src \ No newline at end of file diff --git a/test/Milkman.t.sol b/test/Milkman.t.sol index a89dfdd..85f0c5a 100644 --- a/test/Milkman.t.sol +++ b/test/Milkman.t.sol @@ -16,6 +16,8 @@ import "../src/pricecheckers/DynamicSlippageChecker.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract MilkmanTest is Test { + using Surl for *; + Milkman milkman; address sushiswapExpectedOutCalculator; address sushiswapPriceChecker; @@ -151,6 +153,43 @@ contract MilkmanTest is Test { ); assertEq(Milkman(orderContract).swapHash(), expectedSwapHash); + string[] memory headers = new string[](1); + headers[0] = "Content-Type: application/json"; + + // (uint256 status, bytes memory data) = "https://httpbin.org/post".post(headers, + // string(abi.encodePacked('{"foo": ', '"bar"}'))); + + // post_body = { + // "sellToken": sell_token.address, + // "buyToken": buy_token.address, + // "from": "0x5F4bd1b3667127Bf44beBBa9e5d736B65A1677E5", + // "kind": "sell", + // "sellAmountBeforeFee": str(sell_amount), + // "priceQuality": "fast", + // "signingScheme": "eip1271", + // "verificationGasLimit": 30000, + // } + + (uint256 status, bytes memory data) = + "https://api.cow.fi/mainnet/api/v1/quote".post(headers, + string(abi.encodePacked( + '{"sellToken": "', vm.toString(address(fromToken)), + '", "buyToken": "', vm.toString(address(toToken)), + '", "from": "', vm.toString(whale), + '", "kind": "sell", "sellAmountBeforeFee": "', vm.toString(amountIn), + '", "priceQuality": "fast", "signingScheme": "eip1271", "verificationGasLimit": 30000', + '}' + ))); + + console.log("data", string(data)); + + assertEq(status, 200); + + // (uint256 status, bytes memory data) = "https://httpbin.org/get".get(); + // console.log("status", status); + // console.log("body", string(data)); + + // console.log(orderContract); // console.lo(fromToken.balanceOf(orderContract)); From f67ffe991989148c203bfaaa25cbb4c98c4abb55 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Fri, 26 Jan 2024 08:42:54 -0500 Subject: [PATCH 24/54] chore: forge fmt --- src/Milkman.sol | 102 +++++------------- src/periphery/GasChecker.sol | 14 ++- src/periphery/HashHelper.sol | 7 +- src/periphery/MilkmanStateHelper.sol | 16 +-- .../ChainlinkExpectedOutCalculator.sol | 64 +++++------ .../CurveExpectedOutCalculator.sol | 51 ++++----- src/pricecheckers/DynamicSlippageChecker.sol | 32 +++--- .../FixedMaxFeePriceCheckerDecorator.sol | 23 ++-- src/pricecheckers/FixedMinOutPriceChecker.sol | 8 +- src/pricecheckers/FixedSlippageChecker.sol | 31 +++--- src/pricecheckers/IExpectedOutCalculator.sol | 11 +- .../MetaExpectedOutCalculator.sol | 45 ++++---- src/pricecheckers/PriceCheckerLib.sol | 27 ++--- ...edBalancerBalWethExpectedOutCalculator.sol | 77 ++++--------- .../UniV2ExpectedOutCalculator.sol | 13 +-- .../UniV3ExpectedOutCalculator.sol | 42 +++----- .../ValidFromPriceCheckerDecorator.sol | 21 ++-- test/Milkman.t.sol | 72 ++++++------- 18 files changed, 238 insertions(+), 418 deletions(-) diff --git a/src/Milkman.sol b/src/Milkman.sol index c8703c8..68b455e 100644 --- a/src/Milkman.sol +++ b/src/Milkman.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: LGPL-3.0-or-later pragma solidity ^0.7.6; + pragma abicoder v2; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -30,11 +31,9 @@ contract Milkman { ); /// @dev The contract Milkman needs to give allowance. - address internal constant VAULT_RELAYER = - 0xC92E8bdf79f0507f65a392b0ab4667716BFE0110; + address internal constant VAULT_RELAYER = 0xC92E8bdf79f0507f65a392b0ab4667716BFE0110; /// @dev The settlement contract's EIP-712 domain separator. Milkman uses this to verify that a provided UID matches provided order parameters. - bytes32 public constant DOMAIN_SEPARATOR = - 0xc078f884a2676e1345748b1feace7b0abee5d00ecadb6e574dcdd109a63e8943; + bytes32 public constant DOMAIN_SEPARATOR = 0xc078f884a2676e1345748b1feace7b0abee5d00ecadb6e574dcdd109a63e8943; bytes4 internal constant MAGIC_VALUE = 0x1626ba7e; bytes4 internal constant NON_MAGIC_VALUE = 0xffffffff; bytes32 internal constant ROOT_MILKMAN_SWAP_HASH = @@ -67,7 +66,9 @@ contract Milkman { address to, address priceChecker, bytes calldata priceCheckerData - ) external { + ) + external + { require(address(this) == ROOT_MILKMAN, "!root_milkman"); // can't call `requestSwapExactTokensForTokens` from order contracts require(priceChecker != address(0), "!price_checker"); // need to supply a valid price checker @@ -75,30 +76,14 @@ contract Milkman { fromToken.safeTransferFrom(msg.sender, orderContract, amountIn); - bytes32 _swapHash = keccak256( - abi.encode( - msg.sender, - to, - fromToken, - toToken, - amountIn, - priceChecker, - priceCheckerData - ) - ); + bytes32 _swapHash = + keccak256(abi.encode(msg.sender, to, fromToken, toToken, amountIn, priceChecker, priceCheckerData)); Milkman(orderContract).initialize(fromToken, _swapHash); emit SwapRequested( - orderContract, - msg.sender, - amountIn, - address(fromToken), - address(toToken), - to, - priceChecker, - priceCheckerData - ); + orderContract, msg.sender, amountIn, address(fromToken), address(toToken), to, priceChecker, priceCheckerData + ); } function initialize(IERC20 fromToken, bytes32 _swapHash) external { @@ -117,22 +102,15 @@ contract Milkman { address to, address priceChecker, bytes calldata priceCheckerData - ) external { + ) + external + { bytes32 _storedSwapHash = swapHash; require(_storedSwapHash != ROOT_MILKMAN_SWAP_HASH, "!cancel_from_root"); - bytes32 _calculatedSwapHash = keccak256( - abi.encode( - msg.sender, - to, - fromToken, - toToken, - amountIn, - priceChecker, - priceCheckerData - ) - ); + bytes32 _calculatedSwapHash = + keccak256(abi.encode(msg.sender, to, fromToken, toToken, amountIn, priceChecker, priceCheckerData)); require(_storedSwapHash == _calculatedSwapHash, "!valid_creator_proof"); @@ -141,45 +119,25 @@ contract Milkman { /// @param orderDigest The EIP-712 signing digest derived from the order /// @param encodedOrder Bytes-encoded order information, originally created by an off-chain bot. Created by concatening the order data (in the form of GPv2Order.Data), the price checker address, and price checker data. - function isValidSignature(bytes32 orderDigest, bytes calldata encodedOrder) - external - view - returns (bytes4) - { + function isValidSignature(bytes32 orderDigest, bytes calldata encodedOrder) external view returns (bytes4) { bytes32 _storedSwapHash = swapHash; - require( - _storedSwapHash != ROOT_MILKMAN_SWAP_HASH, - "!is_valid_sig_from_root" - ); + require(_storedSwapHash != ROOT_MILKMAN_SWAP_HASH, "!is_valid_sig_from_root"); - ( - GPv2Order.Data memory _order, - address _orderCreator, - address _priceChecker, - bytes memory _priceCheckerData - ) = decodeOrder(encodedOrder); + (GPv2Order.Data memory _order, address _orderCreator, address _priceChecker, bytes memory _priceCheckerData) = + decodeOrder(encodedOrder); require(_order.hash(DOMAIN_SEPARATOR) == orderDigest, "!match"); require(_order.kind == GPv2Order.KIND_SELL, "!kind_sell"); - require( - _order.validTo >= block.timestamp + 5 minutes, - "expires_too_soon" - ); + require(_order.validTo >= block.timestamp + 5 minutes, "expires_too_soon"); require(!_order.partiallyFillable, "!fill_or_kill"); - require( - _order.sellTokenBalance == GPv2Order.BALANCE_ERC20, - "!sell_erc20" - ); + require(_order.sellTokenBalance == GPv2Order.BALANCE_ERC20, "!sell_erc20"); - require( - _order.buyTokenBalance == GPv2Order.BALANCE_ERC20, - "!buy_erc20" - ); + require(_order.buyTokenBalance == GPv2Order.BALANCE_ERC20, "!buy_erc20"); require( IPriceChecker(_priceChecker).checkPrice( @@ -223,10 +181,8 @@ contract Milkman { bytes memory _priceCheckerData ) { - (_order, _orderCreator, _priceChecker, _priceCheckerData) = abi.decode( - _encodedOrder, - (GPv2Order.Data, address, address, bytes) - ); + (_order, _orderCreator, _priceChecker, _priceCheckerData) = + abi.decode(_encodedOrder, (GPv2Order.Data, address, address, bytes)); } function createOrderContract() internal returns (address _orderContract) { @@ -236,15 +192,9 @@ contract Milkman { assembly { // EIP-1167 bytecode let clone_code := mload(0x40) - mstore( - clone_code, - 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000 - ) + mstore(clone_code, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000) mstore(add(clone_code, 0x14), addressBytes) - mstore( - add(clone_code, 0x28), - 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000 - ) + mstore(add(clone_code, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000) _orderContract := create(0, clone_code, 0x37) } } diff --git a/src/periphery/GasChecker.sol b/src/periphery/GasChecker.sol index 0b7ae9d..bd08eb5 100644 --- a/src/periphery/GasChecker.sol +++ b/src/periphery/GasChecker.sol @@ -1,20 +1,18 @@ // SPDX-License-Identifier: LGPL-3.0-or-later pragma solidity ^0.7.6; + pragma abicoder v2; interface EIP1271 { - function isValidSignature(bytes32, bytes calldata) - external - returns (bytes4); + function isValidSignature(bytes32, bytes calldata) external returns (bytes4); } /// Check that `isValidSignature` doesn't take up too much gas contract GasChecker { - function isValidSignatureCheck( - address milkman, - bytes32 orderDigest, - bytes calldata encodedOrder - ) external returns (bytes4) { + function isValidSignatureCheck(address milkman, bytes32 orderDigest, bytes calldata encodedOrder) + external + returns (bytes4) + { return EIP1271(milkman).isValidSignature(orderDigest, encodedOrder); } } diff --git a/src/periphery/HashHelper.sol b/src/periphery/HashHelper.sol index f0d5e8e..6efeb29 100644 --- a/src/periphery/HashHelper.sol +++ b/src/periphery/HashHelper.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: LGPL-3.0-or-later pragma solidity ^0.7.6; + pragma abicoder v2; import {GPv2Order} from "@cow-protocol/contracts/libraries/GPv2Order.sol"; @@ -8,11 +9,7 @@ contract HashHelper { using GPv2Order for GPv2Order.Data; using GPv2Order for bytes; - function hash(GPv2Order.Data memory order, bytes32 domainSeparator) - external - pure - returns (bytes32 orderDigest) - { + function hash(GPv2Order.Data memory order, bytes32 domainSeparator) external pure returns (bytes32 orderDigest) { return order.hash(domainSeparator); } } diff --git a/src/periphery/MilkmanStateHelper.sol b/src/periphery/MilkmanStateHelper.sol index e2cf435..01e5244 100644 --- a/src/periphery/MilkmanStateHelper.sol +++ b/src/periphery/MilkmanStateHelper.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: LGPL-3.0-or-later pragma solidity ^0.7.6; + pragma abicoder v2; import {IGPv2Settlement} from "../../interfaces/IGPv2Settlement.sol"; @@ -19,27 +20,20 @@ contract MilkmanStateHelper { PAIRED_AND_EXECUTED } - IMilkman public constant milkman = - IMilkman(0x3E40B8c9FcBf02a26Ff1c5d88f525AEd00755575); + IMilkman public constant milkman = IMilkman(0x3E40B8c9FcBf02a26Ff1c5d88f525AEd00755575); - IGPv2Settlement internal constant settlement = - IGPv2Settlement(0x9008D19f58AAbD9eD0D60971565AA8510560ab41); + IGPv2Settlement internal constant settlement = IGPv2Settlement(0x9008D19f58AAbD9eD0D60971565AA8510560ab41); function getState(bytes32 _swapID) external view returns (SwapState) { bytes memory _swapData = milkman.swaps(_swapID); if (_swapData.length == 0) { return SwapState.NULL; - } else if ( - _swapData.length == 32 && _swapData[31] == bytes1(uint8(1)) - ) { + } else if (_swapData.length == 32 && _swapData[31] == bytes1(uint8(1))) { return SwapState.REQUESTED; } - (uint256 _blockNumberWhenPaired, bytes memory _orderUid) = abi.decode( - _swapData, - (uint256, bytes) - ); + (uint256 _blockNumberWhenPaired, bytes memory _orderUid) = abi.decode(_swapData, (uint256, bytes)); if (settlement.filledAmount(_orderUid) != 0) { return SwapState.PAIRED_AND_EXECUTED; diff --git a/src/pricecheckers/ChainlinkExpectedOutCalculator.sol b/src/pricecheckers/ChainlinkExpectedOutCalculator.sol index b8a4203..43e79a8 100644 --- a/src/pricecheckers/ChainlinkExpectedOutCalculator.sol +++ b/src/pricecheckers/ChainlinkExpectedOutCalculator.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: LGPL-3.0-or-later pragma solidity ^0.7.6; + pragma abicoder v2; import "@openzeppelin/contracts/math/SafeMath.sol"; @@ -40,25 +41,15 @@ contract ChainlinkExpectedOutCalculator is IExpectedOutCalculator { * BOND -> YFI: [[bond-eth.data.eth, yfi-eth.data.eth], [false, true]] * BOND -> FXS (FXS has no fxs-eth feed): [[bond-eth.data.eth, eth-usd.data.eth, fxs-usd.data.eth], [false, false, true]] */ - function getExpectedOut( - uint256 _amountIn, - address _fromToken, - address _toToken, - bytes calldata _data - ) external view override returns (uint256) { - (address[] memory _priceFeeds, bool[] memory _reverses) = abi.decode( - _data, - (address[], bool[]) - ); - - return - getExpectedOutFromChainlink( - _priceFeeds, - _reverses, - _amountIn, - _fromToken, - _toToken - ); // how much Chainlink says we'd get out of this trade + function getExpectedOut(uint256 _amountIn, address _fromToken, address _toToken, bytes calldata _data) + external + view + override + returns (uint256) + { + (address[] memory _priceFeeds, bool[] memory _reverses) = abi.decode(_data, (address[], bool[])); + + return getExpectedOutFromChainlink(_priceFeeds, _reverses, _amountIn, _fromToken, _toToken); // how much Chainlink says we'd get out of this trade } function getExpectedOutFromChainlink( @@ -67,7 +58,11 @@ contract ChainlinkExpectedOutCalculator is IExpectedOutCalculator { uint256 _amountIn, address _fromToken, address _toToken - ) internal view returns (uint256 _expectedOutFromChainlink) { + ) + internal + view + returns (uint256 _expectedOutFromChainlink) + { uint256 _priceFeedsLen = _priceFeeds.length; require(_priceFeedsLen > 0); // dev: need to pass at least one price feed @@ -81,38 +76,27 @@ contract ChainlinkExpectedOutCalculator is IExpectedOutCalculator { require(_latestAnswer > 0); // dev: latest answer from the price feed needs to be positive } - uint256 _scaleAnswerBy = 10**uint256(_priceFeed.decimals()); + uint256 _scaleAnswerBy = 10 ** uint256(_priceFeed.decimals()); // If it's first iteration, use amountIn to calculate. Else, use the result from the previous iteration. - uint256 _amountIntoThisIteration = _i == 0 - ? _amountIn - : _expectedOutFromChainlink; + uint256 _amountIntoThisIteration = _i == 0 ? _amountIn : _expectedOutFromChainlink; // Without a reverse, we multiply amount * price // With a reverse, we divide amount / price - _expectedOutFromChainlink = _reverses[_i] - ? _amountIntoThisIteration.mul(_scaleAnswerBy).div( - uint256(_latestAnswer) - ) - : _amountIntoThisIteration.mul(uint256(_latestAnswer)).div( - _scaleAnswerBy - ); + _expectedOutFromChainlink = + _reverses[_i] + ? _amountIntoThisIteration.mul(_scaleAnswerBy).div(uint256(_latestAnswer)) + : _amountIntoThisIteration.mul(uint256(_latestAnswer)).div(_scaleAnswerBy); } - uint256 _fromTokenDecimals = uint256( - IERC20MetaData(_fromToken).decimals() - ); + uint256 _fromTokenDecimals = uint256(IERC20MetaData(_fromToken).decimals()); uint256 _toTokenDecimals = uint256(IERC20MetaData(_toToken).decimals()); if (_fromTokenDecimals > _toTokenDecimals) { // if fromToken has more decimals than toToken, we need to divide - _expectedOutFromChainlink = _expectedOutFromChainlink.div( - 10**_fromTokenDecimals.sub(_toTokenDecimals) - ); + _expectedOutFromChainlink = _expectedOutFromChainlink.div(10 ** _fromTokenDecimals.sub(_toTokenDecimals)); } else if (_fromTokenDecimals < _toTokenDecimals) { - _expectedOutFromChainlink = _expectedOutFromChainlink.mul( - 10**_toTokenDecimals.sub(_fromTokenDecimals) - ); + _expectedOutFromChainlink = _expectedOutFromChainlink.mul(10 ** _toTokenDecimals.sub(_fromTokenDecimals)); } } } diff --git a/src/pricecheckers/CurveExpectedOutCalculator.sol b/src/pricecheckers/CurveExpectedOutCalculator.sol index 1381a70..89d2af3 100644 --- a/src/pricecheckers/CurveExpectedOutCalculator.sol +++ b/src/pricecheckers/CurveExpectedOutCalculator.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: LGPL-3.0-or-later pragma solidity ^0.7.6; + pragma abicoder v2; import "@openzeppelin/contracts/math/SafeMath.sol"; @@ -12,30 +13,19 @@ interface IAddressProvider { } interface IRegistry { - function find_pool_for_coins(address _from, address _to) - external - view - returns (address); + function find_pool_for_coins(address _from, address _to) external view returns (address); - function get_underlying_coins(address _pool) - external - view - returns (address[8] memory); + function get_underlying_coins(address _pool) external view returns (address[8] memory); } interface ICurvePool { - function get_dy_underlying( - int128 i, - int128 j, - uint256 dx - ) external view returns (uint256); + function get_dy_underlying(int128 i, int128 j, uint256 dx) external view returns (uint256); } contract CurveExpectedOutCalculator is IExpectedOutCalculator { using SafeMath for uint256; - IAddressProvider internal constant ADDRESS_PROVIDER = - IAddressProvider(0x0000000022D53366457F9d5E68Ec105046FC4383); + IAddressProvider internal constant ADDRESS_PROVIDER = IAddressProvider(0x0000000022D53366457F9d5E68Ec105046FC4383); IRegistry public registry; uint256 internal constant MAX_BPS = 10_000; @@ -51,35 +41,32 @@ contract CurveExpectedOutCalculator is IExpectedOutCalculator { /** * @dev This expected out calculator can only be used for Curve pools that use `int128` - * for `i` and `j`, which contains most but not all pools. + * for `i` and `j`, which contains most but not all pools. * - * A separate calculator can be deployed for the pools that use `uint256`. + * A separate calculator can be deployed for the pools that use `uint256`. */ - function getExpectedOut( - uint256 _amountIn, - address _fromToken, - address _toToken, - bytes calldata - ) external view override returns (uint256) { + function getExpectedOut(uint256 _amountIn, address _fromToken, address _toToken, bytes calldata) + external + view + override + returns (uint256) + { address _pool = registry.find_pool_for_coins(_fromToken, _toToken); require(_pool != address(0)); // dev: no Curve pool for this swap return _getExpectedOut(_pool, _fromToken, _toToken, _amountIn); } - function _getExpectedOut( - address _pool, - address _fromToken, - address _toToken, - uint256 _amountIn - ) internal view returns (uint256) { + function _getExpectedOut(address _pool, address _fromToken, address _toToken, uint256 _amountIn) + internal + view + returns (uint256) + { int128 _i; int128 _j; { // block scoping to prevent stack too deep - address[8] memory _tokensInPool = registry.get_underlying_coins( - _pool - ); + address[8] memory _tokensInPool = registry.get_underlying_coins(_pool); for (int128 _x = 0; _x < 8; _x++) { address _currentToken = _tokensInPool[uint256(_x)]; if (_currentToken == address(0)) { diff --git a/src/pricecheckers/DynamicSlippageChecker.sol b/src/pricecheckers/DynamicSlippageChecker.sol index abdf685..0e9195b 100644 --- a/src/pricecheckers/DynamicSlippageChecker.sol +++ b/src/pricecheckers/DynamicSlippageChecker.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: LGPL-3.0-or-later pragma solidity ^0.7.6; + pragma abicoder v2; import "@openzeppelin/contracts/math/SafeMath.sol"; @@ -20,9 +21,7 @@ contract DynamicSlippageChecker is IPriceChecker { constructor(string memory _name, address _expectedOutCalculator) { NAME = _name; - EXPECTED_OUT_CALCULATOR = IExpectedOutCalculator( - _expectedOutCalculator - ); + EXPECTED_OUT_CALCULATOR = IExpectedOutCalculator(_expectedOutCalculator); } function checkPrice( @@ -32,21 +31,16 @@ contract DynamicSlippageChecker is IPriceChecker { uint256, uint256 _minOut, bytes calldata _data - ) external view override returns (bool) { - (uint256 _allowedSlippageInBps, bytes memory _data) = abi.decode( - _data, - (uint256, bytes) - ); - - uint256 _expectedOut = EXPECTED_OUT_CALCULATOR.getExpectedOut( - _amountIn, - _fromToken, - _toToken, - _data - ); - - return - _minOut > - _expectedOut.mul(MAX_BPS.sub(_allowedSlippageInBps)).div(MAX_BPS); + ) + external + view + override + returns (bool) + { + (uint256 _allowedSlippageInBps, bytes memory _data) = abi.decode(_data, (uint256, bytes)); + + uint256 _expectedOut = EXPECTED_OUT_CALCULATOR.getExpectedOut(_amountIn, _fromToken, _toToken, _data); + + return _minOut > _expectedOut.mul(MAX_BPS.sub(_allowedSlippageInBps)).div(MAX_BPS); } } diff --git a/src/pricecheckers/FixedMaxFeePriceCheckerDecorator.sol b/src/pricecheckers/FixedMaxFeePriceCheckerDecorator.sol index b73e2b9..6361485 100644 --- a/src/pricecheckers/FixedMaxFeePriceCheckerDecorator.sol +++ b/src/pricecheckers/FixedMaxFeePriceCheckerDecorator.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: LGPL-3.0-or-later pragma solidity ^0.7.6; + pragma abicoder v2; import "@openzeppelin/contracts/math/SafeMath.sol"; @@ -27,24 +28,18 @@ contract FixedMaxFeePriceCheckerDecorator is IPriceChecker { uint256 _feeAmount, uint256 _minOut, bytes calldata _data - ) external view override returns (bool) { - (uint256 _allowedFeeAmount, bytes memory _data) = abi.decode( - _data, - (uint256, bytes) - ); + ) + external + view + override + returns (bool) + { + (uint256 _allowedFeeAmount, bytes memory _data) = abi.decode(_data, (uint256, bytes)); if (_feeAmount > _allowedFeeAmount) { return false; } - return - PRICE_CHECKER.checkPrice( - _amountIn, - _fromToken, - _toToken, - _feeAmount, - _minOut, - _data - ); + return PRICE_CHECKER.checkPrice(_amountIn, _fromToken, _toToken, _feeAmount, _minOut, _data); } } diff --git a/src/pricecheckers/FixedMinOutPriceChecker.sol b/src/pricecheckers/FixedMinOutPriceChecker.sol index e110e44..b406505 100644 --- a/src/pricecheckers/FixedMinOutPriceChecker.sol +++ b/src/pricecheckers/FixedMinOutPriceChecker.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: LGPL-3.0-or-later pragma solidity ^0.7.6; + pragma abicoder v2; import {IPriceChecker} from "../../interfaces/IPriceChecker.sol"; @@ -13,7 +14,12 @@ contract FixedMinOutPriceChecker is IPriceChecker { uint256 _feeAmount, uint256 _out, bytes calldata _data - ) external view override returns (bool) { + ) + external + view + override + returns (bool) + { uint256 minOut = abi.decode(_data, (uint256)); return minOut <= _out; } diff --git a/src/pricecheckers/FixedSlippageChecker.sol b/src/pricecheckers/FixedSlippageChecker.sol index 1aab8eb..149725e 100644 --- a/src/pricecheckers/FixedSlippageChecker.sol +++ b/src/pricecheckers/FixedSlippageChecker.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: LGPL-3.0-or-later pragma solidity ^0.7.6; + pragma abicoder v2; import "@openzeppelin/contracts/math/SafeMath.sol"; @@ -19,18 +20,12 @@ contract FixedSlippageChecker is IPriceChecker { uint256 internal constant MAX_BPS = 10_000; - constructor( - string memory _name, - uint256 _allowedSlippageInBps, - address _expectedOutCalculator - ) { + constructor(string memory _name, uint256 _allowedSlippageInBps, address _expectedOutCalculator) { require(_allowedSlippageInBps <= MAX_BPS); NAME = _name; ALLOWED_SLIPPAGE_IN_BPS = _allowedSlippageInBps; - EXPECTED_OUT_CALCULATOR = IExpectedOutCalculator( - _expectedOutCalculator - ); + EXPECTED_OUT_CALCULATOR = IExpectedOutCalculator(_expectedOutCalculator); } function checkPrice( @@ -40,16 +35,14 @@ contract FixedSlippageChecker is IPriceChecker { uint256, uint256 _minOut, bytes calldata _data - ) external view override returns (bool) { - uint256 _expectedOut = EXPECTED_OUT_CALCULATOR.getExpectedOut( - _amountIn, - _fromToken, - _toToken, - _data - ); - - return - _minOut > - _expectedOut.mul(MAX_BPS.sub(ALLOWED_SLIPPAGE_IN_BPS)).div(MAX_BPS); + ) + external + view + override + returns (bool) + { + uint256 _expectedOut = EXPECTED_OUT_CALCULATOR.getExpectedOut(_amountIn, _fromToken, _toToken, _data); + + return _minOut > _expectedOut.mul(MAX_BPS.sub(ALLOWED_SLIPPAGE_IN_BPS)).div(MAX_BPS); } } diff --git a/src/pricecheckers/IExpectedOutCalculator.sol b/src/pricecheckers/IExpectedOutCalculator.sol index 3900d44..01b6236 100644 --- a/src/pricecheckers/IExpectedOutCalculator.sol +++ b/src/pricecheckers/IExpectedOutCalculator.sol @@ -1,12 +1,11 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity ^0.7.6; + pragma abicoder v2; interface IExpectedOutCalculator { - function getExpectedOut( - uint256 _amountIn, - address _fromToken, - address _toToken, - bytes calldata _data - ) external view returns (uint256); + function getExpectedOut(uint256 _amountIn, address _fromToken, address _toToken, bytes calldata _data) + external + view + returns (uint256); } diff --git a/src/pricecheckers/MetaExpectedOutCalculator.sol b/src/pricecheckers/MetaExpectedOutCalculator.sol index 6541b55..52e6529 100644 --- a/src/pricecheckers/MetaExpectedOutCalculator.sol +++ b/src/pricecheckers/MetaExpectedOutCalculator.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: LGPL-3.0-or-later pragma solidity ^0.7.6; + pragma abicoder v2; import "@openzeppelin/contracts/math/SafeMath.sol"; @@ -20,42 +21,32 @@ contract MetaExpectedOutCalculator is IExpectedOutCalculator { * expectedOutCalculators (address[]): List of expected out calculators to use. * expectedOutCalculatorData (bytes[]): List of bytes to pass to each expected out calculator */ - function getExpectedOut( - uint256 _amountIn, - address _fromToken, - address _toToken, - bytes calldata _data - ) external view override returns (uint256) { + function getExpectedOut(uint256 _amountIn, address _fromToken, address _toToken, bytes calldata _data) + external + view + override + returns (uint256) + { ( - address[] memory _swapPath, - address[] memory _expectedOutCalculators, - bytes[] memory _expectedOutCalculatorData + address[] memory _swapPath, address[] memory _expectedOutCalculators, bytes[] memory _expectedOutCalculatorData ) = abi.decode(_data, (address[], address[], bytes[])); require( - _swapPath.length.sub(1) == _expectedOutCalculators.length && - _expectedOutCalculators.length == - _expectedOutCalculatorData.length, + _swapPath.length.sub(1) == _expectedOutCalculators.length + && _expectedOutCalculators.length == _expectedOutCalculatorData.length, "invalid_length" ); uint256 _runningExpectedOut; for (uint256 i = 0; i < _swapPath.length.sub(1); i++) { - _runningExpectedOut = i == 0 - ? IExpectedOutCalculator(_expectedOutCalculators[i]) - .getExpectedOut( - _amountIn, - _swapPath[i], - _swapPath[i + 1], - _expectedOutCalculatorData[i] - ) - : IExpectedOutCalculator(_expectedOutCalculators[i]) - .getExpectedOut( - _runningExpectedOut, - _swapPath[i], - _swapPath[i + 1], - _expectedOutCalculatorData[i] - ); + _runningExpectedOut = + i == 0 + ? IExpectedOutCalculator(_expectedOutCalculators[i]).getExpectedOut( + _amountIn, _swapPath[i], _swapPath[i + 1], _expectedOutCalculatorData[i] + ) + : IExpectedOutCalculator(_expectedOutCalculators[i]).getExpectedOut( + _runningExpectedOut, _swapPath[i], _swapPath[i + 1], _expectedOutCalculatorData[i] + ); } return _runningExpectedOut; diff --git a/src/pricecheckers/PriceCheckerLib.sol b/src/pricecheckers/PriceCheckerLib.sol index e204657..39bf855 100644 --- a/src/pricecheckers/PriceCheckerLib.sol +++ b/src/pricecheckers/PriceCheckerLib.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: LGPL-3.0-or-later pragma solidity ^0.7.6; + pragma abicoder v2; import "@openzeppelin/contracts/math/SafeMath.sol"; @@ -9,10 +10,7 @@ library PriceCheckerLib { uint256 internal constant MAX_BPS = 10_000; - function getMaxSlippage( - uint256 _inputMaxSlippage, - uint256 _defaultMaxSlippage - ) internal pure returns (uint256) { + function getMaxSlippage(uint256 _inputMaxSlippage, uint256 _defaultMaxSlippage) internal pure returns (uint256) { require(_inputMaxSlippage <= 10_000); // dev: max slippage too high if (_inputMaxSlippage == 0) { @@ -23,19 +21,12 @@ library PriceCheckerLib { } /// @dev performs a double-ended slippage check, ensuring that minOut is both greater than market value - max slippage and less than market value + max slippage. - function isMinOutAcceptable( - uint256 _minOut, - uint256 _marketValueOfAmountIn, - uint256 _maxSlippageInBips - ) internal pure returns (bool) { - return - _minOut > - _marketValueOfAmountIn.mul(MAX_BPS.sub(_maxSlippageInBips)).div( - MAX_BPS - ) && - _minOut < - _marketValueOfAmountIn.mul(MAX_BPS.add(_maxSlippageInBips)).div( - MAX_BPS - ); + function isMinOutAcceptable(uint256 _minOut, uint256 _marketValueOfAmountIn, uint256 _maxSlippageInBips) + internal + pure + returns (bool) + { + return _minOut > _marketValueOfAmountIn.mul(MAX_BPS.sub(_maxSlippageInBips)).div(MAX_BPS) + && _minOut < _marketValueOfAmountIn.mul(MAX_BPS.add(_maxSlippageInBips)).div(MAX_BPS); } } diff --git a/src/pricecheckers/SingleSidedBalancerBalWethExpectedOutCalculator.sol b/src/pricecheckers/SingleSidedBalancerBalWethExpectedOutCalculator.sol index ae4cf51..8fe4327 100644 --- a/src/pricecheckers/SingleSidedBalancerBalWethExpectedOutCalculator.sol +++ b/src/pricecheckers/SingleSidedBalancerBalWethExpectedOutCalculator.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: LGPL-3.0-or-later pragma solidity ^0.7.6; + pragma abicoder v2; import "@openzeppelin/contracts/math/SafeMath.sol"; @@ -43,78 +44,55 @@ interface IVault { library VaultReentrancyLib { function ensureNotInVaultContext(IVault vault) internal view { - bytes32 REENTRANCY_ERROR_HASH = keccak256( - abi.encodeWithSignature("Error(string)", "BAL#400") - ); + bytes32 REENTRANCY_ERROR_HASH = keccak256(abi.encodeWithSignature("Error(string)", "BAL#400")); // read-only re-entrancy protection - this call is always unsuccessful but we need to make sure // it didn't fail due to a re-entrancy attack (, bytes memory revertData) = address(vault).staticcall{gas: 10_000}( - abi.encodeWithSelector( - vault.manageUserBalance.selector, - new address[](0) - ) + abi.encodeWithSelector(vault.manageUserBalance.selector, new address[](0)) ); require(keccak256(revertData) != REENTRANCY_ERROR_HASH); } } -contract SingleSidedBalancerBalWethExpectedOutCalculator is - IExpectedOutCalculator -{ +contract SingleSidedBalancerBalWethExpectedOutCalculator is IExpectedOutCalculator { using SafeMath for uint256; using FixedPoint for uint256; using VaultReentrancyLib for IVault; address public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; address public constant BAL = 0xba100000625a3754423978a60c9317c58a424e3D; - IWeightedPool public constant BAL_WETH_POOL = - IWeightedPool(0x5c6Ee304399DBdB9C8Ef030aB642B10820DB8F56); - IVault internal constant BALANCER_VAULT = - IVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8); - IPriceFeed internal constant BAL_ETH_FEED = - IPriceFeed(0xC1438AA3823A6Ba0C159CfA8D98dF5A994bA120b); + IWeightedPool public constant BAL_WETH_POOL = IWeightedPool(0x5c6Ee304399DBdB9C8Ef030aB642B10820DB8F56); + IVault internal constant BALANCER_VAULT = IVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8); + IPriceFeed internal constant BAL_ETH_FEED = IPriceFeed(0xC1438AA3823A6Ba0C159CfA8D98dF5A994bA120b); uint256 internal constant ZERO_POINT_EIGHT = 8e17; uint256 internal constant ZERO_POINT_TWO = 2e17; uint256 internal constant TEN = 1e19; - function getExpectedOut( - uint256 _amountIn, - address _fromToken, - address _toToken, - bytes calldata - ) external view override returns (uint256) { + function getExpectedOut(uint256 _amountIn, address _fromToken, address _toToken, bytes calldata) + external + view + override + returns (uint256) + { require(_toToken == address(BAL_WETH_POOL)); require(_fromToken == WETH || _fromToken == BAL); BALANCER_VAULT.ensureNotInVaultContext(); - uint256 kOverS = BAL_WETH_POOL.getInvariant().mul(1e18).div( - BAL_WETH_POOL.totalSupply() - ); + uint256 kOverS = BAL_WETH_POOL.getInvariant().mul(1e18).div(BAL_WETH_POOL.totalSupply()); if (_fromToken == WETH) { int256 ethPriceOfBal = BAL_ETH_FEED.latestAnswer(); require(ethPriceOfBal > 0); - uint256 balFactor = uint256(ethPriceOfBal) - .mul(1e18) - .div(ZERO_POINT_EIGHT) - .powUp(ZERO_POINT_EIGHT); - uint256 ethFactor = FixedPoint - .ONE - .mul(1e18) - .div(ZERO_POINT_TWO) - .powUp(ZERO_POINT_TWO); + uint256 balFactor = uint256(ethPriceOfBal).mul(1e18).div(ZERO_POINT_EIGHT).powUp(ZERO_POINT_EIGHT); + uint256 ethFactor = FixedPoint.ONE.mul(1e18).div(ZERO_POINT_TWO).powUp(ZERO_POINT_TWO); // what a BPT is worth in ETH - uint256 ethValueOfBPT = kOverS - .mul(balFactor) - .div(1e18) - .mul(ethFactor) - .div(1e18); + uint256 ethValueOfBPT = kOverS.mul(balFactor).div(1e18).mul(ethFactor).div(1e18); return _amountIn.mul(1e18).div(ethValueOfBPT); } else { @@ -122,26 +100,13 @@ contract SingleSidedBalancerBalWethExpectedOutCalculator is int256 ethPriceOfBal = BAL_ETH_FEED.latestAnswer(); require(ethPriceOfBal > 0); - uint256 balPriceOfEth = FixedPoint.ONE.mul(1e18).div( - uint256(ethPriceOfBal) - ); + uint256 balPriceOfEth = FixedPoint.ONE.mul(1e18).div(uint256(ethPriceOfBal)); - uint256 balFactor = FixedPoint - .ONE - .mul(1e18) - .div(ZERO_POINT_EIGHT) - .powUp(ZERO_POINT_EIGHT); - uint256 ethFactor = balPriceOfEth - .mul(1e18) - .div(ZERO_POINT_TWO) - .powUp(ZERO_POINT_TWO); + uint256 balFactor = FixedPoint.ONE.mul(1e18).div(ZERO_POINT_EIGHT).powUp(ZERO_POINT_EIGHT); + uint256 ethFactor = balPriceOfEth.mul(1e18).div(ZERO_POINT_TWO).powUp(ZERO_POINT_TWO); // what a BPT is worth in BAL - uint256 balValueOfBPT = kOverS - .mul(balFactor) - .div(1e18) - .mul(ethFactor) - .div(1e18); + uint256 balValueOfBPT = kOverS.mul(balFactor).div(1e18).mul(ethFactor).div(1e18); return _amountIn.mul(1e18).div(balValueOfBPT); } diff --git a/src/pricecheckers/UniV2ExpectedOutCalculator.sol b/src/pricecheckers/UniV2ExpectedOutCalculator.sol index cf2bf6f..e95301b 100644 --- a/src/pricecheckers/UniV2ExpectedOutCalculator.sol +++ b/src/pricecheckers/UniV2ExpectedOutCalculator.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: LGPL-3.0-or-later pragma solidity ^0.7.6; + pragma abicoder v2; import "@openzeppelin/contracts/math/SafeMath.sol"; @@ -21,12 +22,12 @@ contract UniV2ExpectedOutCalculator is IExpectedOutCalculator { UNIV2_ROUTER = _univ2Router; } - function getExpectedOut( - uint256 _amountIn, - address _fromToken, - address _toToken, - bytes calldata - ) external view override returns (uint256) { + function getExpectedOut(uint256 _amountIn, address _fromToken, address _toToken, bytes calldata) + external + view + override + returns (uint256) + { uint256[] memory amounts; if (_fromToken == WETH || _toToken == WETH) { diff --git a/src/pricecheckers/UniV3ExpectedOutCalculator.sol b/src/pricecheckers/UniV3ExpectedOutCalculator.sol index 83c14eb..34333b8 100644 --- a/src/pricecheckers/UniV3ExpectedOutCalculator.sol +++ b/src/pricecheckers/UniV3ExpectedOutCalculator.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: LGPL-3.0-or-later pragma solidity ^0.7.6; + pragma abicoder v2; import "@openzeppelin/contracts/math/SafeMath.sol"; @@ -12,17 +13,13 @@ interface IUniswapV3StaticQuoter { /// @param path The path of the swap, i.e. each token pair and the pool fee /// @param amountIn The amount of the first token to swap /// @return amountOut The amount of the last token that would be received - function quoteExactInput(bytes memory path, uint256 amountIn) - external - view - returns (uint256 amountOut); + function quoteExactInput(bytes memory path, uint256 amountIn) external view returns (uint256 amountOut); } contract UniV3ExpectedOutCalculator is IExpectedOutCalculator { using SafeMath for uint256; - IUniswapV3StaticQuoter internal constant QUOTER = - IUniswapV3StaticQuoter(0x7637Aaeb5BD58269B782726680d83f72C651aE74); + IUniswapV3StaticQuoter internal constant QUOTER = IUniswapV3StaticQuoter(0x7637Aaeb5BD58269B782726680d83f72C651aE74); /** * @param _data Encoded [swapPath, poolFees]. @@ -34,25 +31,22 @@ contract UniV3ExpectedOutCalculator is IExpectedOutCalculator { * AAVE -> DAI: [[address(AAVE), address(WETH), address(DAI)], [30, 5]] * USDT -> USDC: [[address(USDT), address(USDC)], [1]] */ - function getExpectedOut( - uint256 _amountIn, - address _fromToken, - address _toToken, - bytes calldata _data - ) external view override returns (uint256) { - (address[] memory _swapPath, uint24[] memory _poolFees) = abi.decode( - _data, - (address[], uint24[]) - ); + function getExpectedOut(uint256 _amountIn, address _fromToken, address _toToken, bytes calldata _data) + external + view + override + returns (uint256) + { + (address[] memory _swapPath, uint24[] memory _poolFees) = abi.decode(_data, (address[], uint24[])); return _getExpectedOut(_amountIn, _swapPath, _poolFees); } - function _getExpectedOut( - uint256 _amountIn, - address[] memory _swapPath, - uint24[] memory _poolFees - ) internal view returns (uint256) { + function _getExpectedOut(uint256 _amountIn, address[] memory _swapPath, uint24[] memory _poolFees) + internal + view + returns (uint256) + { require(_swapPath.length >= 2); // dev: must have at least two assets in swap path require(_poolFees.length.add(1) == _swapPath.length); // dev: must be one more asset in swap path than pool fee @@ -61,11 +55,7 @@ contract UniV3ExpectedOutCalculator is IExpectedOutCalculator { for (uint256 _i = 0; _i < _poolFees.length; _i++) { // they ingest fees in 1 100ths of a bip, so we multiply by 100 - _path = abi.encodePacked( - _path, - uint24(uint256(_poolFees[_i]).mul(100)), - _swapPath[_i.add(1)] - ); + _path = abi.encodePacked(_path, uint24(uint256(_poolFees[_i]).mul(100)), _swapPath[_i.add(1)]); } return QUOTER.quoteExactInput(_path, _amountIn); diff --git a/src/pricecheckers/ValidFromPriceCheckerDecorator.sol b/src/pricecheckers/ValidFromPriceCheckerDecorator.sol index 3a911c1..43491b2 100644 --- a/src/pricecheckers/ValidFromPriceCheckerDecorator.sol +++ b/src/pricecheckers/ValidFromPriceCheckerDecorator.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: LGPL-3.0-or-later pragma solidity ^0.7.6; + pragma abicoder v2; import {IPriceChecker} from "../../interfaces/IPriceChecker.sol"; @@ -14,22 +15,18 @@ contract ValidFromPriceCheckerDecorator is IPriceChecker { uint256 _feeAmount, uint256 _minOut, bytes calldata _data - ) external view override returns (bool) { - (uint256 _validFrom, address _priceChecker, bytes memory _data) = abi - .decode(_data, (uint256, address, bytes)); + ) + external + view + override + returns (bool) + { + (uint256 _validFrom, address _priceChecker, bytes memory _data) = abi.decode(_data, (uint256, address, bytes)); if (_validFrom > block.timestamp) { return false; } - return - IPriceChecker(_priceChecker).checkPrice( - _amountIn, - _fromToken, - _toToken, - _feeAmount, - _minOut, - _data - ); + return IPriceChecker(_priceChecker).checkPrice(_amountIn, _fromToken, _toToken, _feeAmount, _minOut, _data); } } diff --git a/test/Milkman.t.sol b/test/Milkman.t.sol index 85f0c5a..1499297 100644 --- a/test/Milkman.t.sol +++ b/test/Milkman.t.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: LGPL-3.0-or-later pragma solidity ^0.7.6; + pragma abicoder v2; import "forge-std/Test.sol"; @@ -27,7 +28,8 @@ contract MilkmanTest is Test { address priceChecker; address whale; - bytes32 SWAP_REQUESTED_EVENT = keccak256("SwapRequested(address,address,uint256,address,address,address,address,bytes)"); + bytes32 SWAP_REQUESTED_EVENT = + keccak256("SwapRequested(address,address,uint256,address,address,address,address,bytes)"); address SUSHISWAP_ROUTER = 0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F; @@ -39,9 +41,11 @@ contract MilkmanTest is Test { function setUp() public { milkman = new Milkman(); - sushiswapExpectedOutCalculator = address(new UniV2ExpectedOutCalculator("SUSHISWAP_EXPECTED_OUT_CALCULATOR", SUSHISWAP_ROUTER)); - sushiswapPriceChecker = address(new FixedSlippageChecker("SUSHISWAP_500_BPS_PRICE_CHECKER", 500, sushiswapExpectedOutCalculator)); - + sushiswapExpectedOutCalculator = + address(new UniV2ExpectedOutCalculator("SUSHISWAP_EXPECTED_OUT_CALCULATOR", SUSHISWAP_ROUTER)); + sushiswapPriceChecker = + address(new FixedSlippageChecker("SUSHISWAP_500_BPS_PRICE_CHECKER", 500, sushiswapExpectedOutCalculator)); + tokenAddress["TOKE"] = 0x2e9d63788249371f1DFC918a52f8d799F4a38C94; tokenAddress["DAI"] = 0x6B175474E89094C44Da98b954EedeAC495271d0F; tokenAddress["USDC"] = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; @@ -115,7 +119,7 @@ contract MilkmanTest is Test { priceChecker = sushiswapPriceChecker; whale = whaleAddresses[tokenToSell]; - uint256 amountIn = amounts[tokenToSell] * 1e18; + uint256 amountIn = amounts[tokenToSell] * 1e18; vm.prank(whale); fromToken.approve(address(milkman), amountIn); @@ -136,50 +140,35 @@ contract MilkmanTest is Test { assertEq(entries[3].topics[0], SWAP_REQUESTED_EVENT); - (address orderContract,,,,,,,) = (abi.decode(entries[3].data, (address,address,uint256,address,address,address,address,bytes))); + (address orderContract,,,,,,,) = + (abi.decode(entries[3].data, (address, address, uint256, address, address, address, address, bytes))); assertEq(fromToken.balanceOf(orderContract), amountIn); - bytes32 expectedSwapHash = keccak256( - abi.encode( - whale, - address(this), - fromToken, - toToken, - amountIn, - priceChecker, - bytes("") - ) - ); + bytes32 expectedSwapHash = + keccak256(abi.encode(whale, address(this), fromToken, toToken, amountIn, priceChecker, bytes(""))); assertEq(Milkman(orderContract).swapHash(), expectedSwapHash); string[] memory headers = new string[](1); headers[0] = "Content-Type: application/json"; - // (uint256 status, bytes memory data) = "https://httpbin.org/post".post(headers, - // string(abi.encodePacked('{"foo": ', '"bar"}'))); - - // post_body = { - // "sellToken": sell_token.address, - // "buyToken": buy_token.address, - // "from": "0x5F4bd1b3667127Bf44beBBa9e5d736B65A1677E5", - // "kind": "sell", - // "sellAmountBeforeFee": str(sell_amount), - // "priceQuality": "fast", - // "signingScheme": "eip1271", - // "verificationGasLimit": 30000, - // } - - (uint256 status, bytes memory data) = - "https://api.cow.fi/mainnet/api/v1/quote".post(headers, - string(abi.encodePacked( - '{"sellToken": "', vm.toString(address(fromToken)), - '", "buyToken": "', vm.toString(address(toToken)), - '", "from": "', vm.toString(whale), - '", "kind": "sell", "sellAmountBeforeFee": "', vm.toString(amountIn), - '", "priceQuality": "fast", "signingScheme": "eip1271", "verificationGasLimit": 30000', - '}' - ))); + (uint256 status, bytes memory data) = "https://api.cow.fi/mainnet/api/v1/quote".post( + headers, + string( + abi.encodePacked( + '{"sellToken": "', + vm.toString(address(fromToken)), + '", "buyToken": "', + vm.toString(address(toToken)), + '", "from": "', + vm.toString(whale), + '", "kind": "sell", "sellAmountBeforeFee": "', + vm.toString(amountIn), + '", "priceQuality": "fast", "signingScheme": "eip1271", "verificationGasLimit": 30000', + "}" + ) + ) + ); console.log("data", string(data)); @@ -188,7 +177,6 @@ contract MilkmanTest is Test { // (uint256 status, bytes memory data) = "https://httpbin.org/get".get(); // console.log("status", status); // console.log("body", string(data)); - // console.log(orderContract); // console.lo(fromToken.balanceOf(orderContract)); From 587f00f5cd27ef57c73b2894b9a0ab5152b319c0 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Tue, 30 Jan 2024 11:27:14 -0500 Subject: [PATCH 25/54] test: add parsing of quote API responses --- test/Milkman.t.sol | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test/Milkman.t.sol b/test/Milkman.t.sol index 1499297..09408fb 100644 --- a/test/Milkman.t.sol +++ b/test/Milkman.t.sol @@ -5,6 +5,7 @@ pragma abicoder v2; import "forge-std/Test.sol"; import "forge-std/console.sol"; +import {stdJson} from "forge-std/StdJson.sol"; import {Surl} from "surl/Surl.sol"; import "../src/Milkman.sol"; import "../src/pricecheckers/UniV2ExpectedOutCalculator.sol"; @@ -18,6 +19,7 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract MilkmanTest is Test { using Surl for *; + using stdJson for string; Milkman milkman; address sushiswapExpectedOutCalculator; @@ -174,6 +176,38 @@ contract MilkmanTest is Test { assertEq(status, 200); + string memory json = string(data); + + string memory json2 = '{"foo": "hello", "bar": 97}'; + bytes memory bar = vm.parseJson(json2, ".foo"); + (string memory d) = abi.decode(bar, (string)); + console.log(d); + + bytes memory buyAmountBytes = vm.parseJson(json, ".quote.buyAmount"); + string memory buyAmountString = abi.decode(buyAmountBytes, (string)); + uint256 buyAmount = vm.parseUint(buyAmountString); + console.log("buyAmount", buyAmount); + + bytes memory feeAmountBytes = vm.parseJson(json, ".quote.feeAmount"); + string memory feeAmountString = abi.decode(feeAmountBytes, (string)); + uint256 feeAmount = vm.parseUint(feeAmountString); + console.log("feeAmount", feeAmount); + + // bytes memory feeBytes = vm.parseJson(json, ".quote.fee"); + // uint256 fee = abi.decode(feeBytes, (uint256)); + // console.log("fee", fee); + + // bytes memory sellTokenBytes = vm.parseJson(json, ".quote.sellToken"); + // address sellToken = abi.decode(sellTokenBytes, (address)); + // console.log("sellToken", sellToken); + + + + // bytes memory b = vm.parseJson(json, ".quote.sellToken"); + // console.log(string(b)); + // address b = vm.parseJsonAddress(json, ".quote.sellToken"); + // console.log(b); + // (uint256 status, bytes memory data) = "https://httpbin.org/get".get(); // console.log("status", status); // console.log("body", string(data)); From 327ab22eb4f51226c0b9fbe6499aad9f00ba5783 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Tue, 30 Jan 2024 16:00:05 -0500 Subject: [PATCH 26/54] test: encode orders into GPv2Orders --- test/Milkman.t.sol | 193 ++++++++++++++++++++++++++++++++------------- 1 file changed, 139 insertions(+), 54 deletions(-) diff --git a/test/Milkman.t.sol b/test/Milkman.t.sol index 09408fb..fbec2aa 100644 --- a/test/Milkman.t.sol +++ b/test/Milkman.t.sol @@ -15,7 +15,9 @@ import "../src/pricecheckers/ChainlinkExpectedOutCalculator.sol"; // import "../src/pricecheckers/SingleSidedBalancerBalWethExpectedOutCalculator.sol"; import "../src/pricecheckers/FixedSlippageChecker.sol"; import "../src/pricecheckers/DynamicSlippageChecker.sol"; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {GPv2Order} from "@cow-protocol/contracts/libraries/GPv2Order.sol"; +import {IERC20 as CoWIERC20} from "@cow-protocol/contracts/interfaces/IERC20.sol"; +// import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract MilkmanTest is Test { using Surl for *; @@ -30,7 +32,11 @@ contract MilkmanTest is Test { address priceChecker; address whale; - bytes32 SWAP_REQUESTED_EVENT = + bytes32 public constant APP_DATA = 0x2B8694ED30082129598720860E8E972F07AA10D9B81CAE16CA0E2CFB24743E24; + bytes32 public constant KIND_SELL = 0xf3b277728b3fee749481eb3e0b3b48980dbbab78658fc419025cb16eee346775; + bytes32 public constant ERC20_BALANCE = 0x5a28e9363bb942b639270062aa6bb295f434bcdfc42c97267bf003f272060dc9; + + bytes32 public constant SWAP_REQUESTED_EVENT = keccak256("SwapRequested(address,address,uint256,address,address,address,address,bytes)"); address SUSHISWAP_ROUTER = 0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F; @@ -41,6 +47,42 @@ contract MilkmanTest is Test { mapping(string => uint256) private amounts; mapping(string => address) private whaleAddresses; + function parseUint(string memory json, string memory key) internal returns (uint256) { + bytes memory valueBytes = vm.parseJson(json, key); + string memory valueString = abi.decode(valueBytes, (string)); + return vm.parseUint(valueString); + } + + function encodeOrderEIP1271( + uint256 amountToSell, + uint256 buyAmount, + uint32 validTo, + uint256 feeAmount + ) + internal + returns (bytes memory) + { + // bytes memory signatureEncodedOrder = abi.encode( + // fromToken, + // toToken, + // whale, + // amountToSell, + // buyAmount, + // validTo, + // APP_DATA, + // feeAmount, + // KIND_SELL, + // false, + // ERC20_BALANCE, + // ERC20_BALANCE, + // whale, + // // priceChecker, + // bytes("") + // ); + + return bytes(""); + } + function setUp() public { milkman = new Milkman(); sushiswapExpectedOutCalculator = @@ -113,15 +155,16 @@ contract MilkmanTest is Test { function testRequestSwapExactTokensForTokens() public { for (uint8 i = 0; i < tokensToSell.length; i++) { - string memory tokenToSell = tokensToSell[i]; - string memory tokenToBuy = sellToBuyMap[tokenToSell]; - fromToken = IERC20(tokenAddress[tokenToSell]); - toToken = IERC20(tokenAddress[tokenToBuy]); - amountIn = amounts[tokenToSell] * 1e18; + { + string memory tokenToSell = tokensToSell[i]; + string memory tokenToBuy = sellToBuyMap[tokenToSell]; + fromToken = IERC20(tokenAddress[tokenToSell]); + toToken = IERC20(tokenAddress[tokenToBuy]); + amountIn = amounts[tokenToSell] * 1e18; + whale = whaleAddresses[tokenToSell]; + uint256 amountIn = amounts[tokenToSell] * 1e18; + } priceChecker = sushiswapPriceChecker; - whale = whaleAddresses[tokenToSell]; - - uint256 amountIn = amounts[tokenToSell] * 1e18; vm.prank(whale); fromToken.approve(address(milkman), amountIn); @@ -147,51 +190,95 @@ contract MilkmanTest is Test { assertEq(fromToken.balanceOf(orderContract), amountIn); - bytes32 expectedSwapHash = - keccak256(abi.encode(whale, address(this), fromToken, toToken, amountIn, priceChecker, bytes(""))); - assertEq(Milkman(orderContract).swapHash(), expectedSwapHash); - - string[] memory headers = new string[](1); - headers[0] = "Content-Type: application/json"; - - (uint256 status, bytes memory data) = "https://api.cow.fi/mainnet/api/v1/quote".post( - headers, - string( - abi.encodePacked( - '{"sellToken": "', - vm.toString(address(fromToken)), - '", "buyToken": "', - vm.toString(address(toToken)), - '", "from": "', - vm.toString(whale), - '", "kind": "sell", "sellAmountBeforeFee": "', - vm.toString(amountIn), - '", "priceQuality": "fast", "signingScheme": "eip1271", "verificationGasLimit": 30000', - "}" + { + bytes32 expectedSwapHash = + keccak256(abi.encode(whale, address(this), fromToken, toToken, amountIn, priceChecker, bytes(""))); + assertEq(Milkman(orderContract).swapHash(), expectedSwapHash); + } + + uint256 buyAmount = 0; + uint256 feeAmount = 0; + { + string[] memory headers = new string[](1); + headers[0] = "Content-Type: application/json"; + + (uint256 status, bytes memory data) = "https://api.cow.fi/mainnet/api/v1/quote".post( + headers, + string( + abi.encodePacked( + '{"sellToken": "', + vm.toString(address(fromToken)), + '", "buyToken": "', + vm.toString(address(toToken)), + '", "from": "', + vm.toString(whale), + '", "kind": "sell", "sellAmountBeforeFee": "', + vm.toString(amountIn), + '", "priceQuality": "fast", "signingScheme": "eip1271", "verificationGasLimit": 30000', + "}" + ) ) - ) + ); + + assertEq(status, 200); + + string memory json = string(data); + + buyAmount = parseUint(json, ".quote.buyAmount"); + feeAmount = parseUint(json, ".quote.feeAmount"); + } + + uint256 amountToSell = amountIn - feeAmount; + assertLt(amountToSell, amountIn); + + uint32 validTo = uint32(block.timestamp) + 60 * 60 * 24; + + GPv2Order.Data memory order = GPv2Order.Data({ + sellToken: CoWIERC20(address(fromToken)), + buyToken: CoWIERC20(address(toToken)), + receiver: whale, + sellAmount: amountToSell, + feeAmount: feeAmount, + buyAmount: buyAmount, + partiallyFillable: false, + kind: GPv2Order.KIND_SELL, + sellTokenBalance: GPv2Order.BALANCE_ERC20, + buyTokenBalance: GPv2Order.BALANCE_ERC20, + validTo: validTo, + appData: APP_DATA + }); + + bytes memory signatureEncodedOrder = abi.encode( + order, + whale, + priceChecker, + bytes("") ); - console.log("data", string(data)); - - assertEq(status, 200); - - string memory json = string(data); - - string memory json2 = '{"foo": "hello", "bar": 97}'; - bytes memory bar = vm.parseJson(json2, ".foo"); - (string memory d) = abi.decode(bar, (string)); - console.log(d); - - bytes memory buyAmountBytes = vm.parseJson(json, ".quote.buyAmount"); - string memory buyAmountString = abi.decode(buyAmountBytes, (string)); - uint256 buyAmount = vm.parseUint(buyAmountString); - console.log("buyAmount", buyAmount); - - bytes memory feeAmountBytes = vm.parseJson(json, ".quote.feeAmount"); - string memory feeAmountString = abi.decode(feeAmountBytes, (string)); - uint256 feeAmount = vm.parseUint(feeAmountString); - console.log("feeAmount", feeAmount); + // bytes memory signatureEncodedOrder = encodeOrderEIP1271( + // amountToSell, + // buyAmount, + // uint32(validTo), + // feeAmount + // ); + + // bytes memory signatureEncodedOrder = abi.encode( + // address(fromToken), + // address(toToken), + // whale, + // amountToSell, + // buyAmount, + // validTo, + // APP_DATA, + // feeAmount, + // KIND_SELL, + // false, + // ERC20_BALANCE, + // ERC20_BALANCE, + // whale, + // priceChecker, + // bytes("") + // ); // bytes memory feeBytes = vm.parseJson(json, ".quote.fee"); // uint256 fee = abi.decode(feeBytes, (uint256)); @@ -201,8 +288,6 @@ contract MilkmanTest is Test { // address sellToken = abi.decode(sellTokenBytes, (address)); // console.log("sellToken", sellToken); - - // bytes memory b = vm.parseJson(json, ".quote.sellToken"); // console.log(string(b)); // address b = vm.parseJsonAddress(json, ".quote.sellToken"); From 6a4bafa8ab217249eb37d4b569c11712e99397a0 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Tue, 30 Jan 2024 16:19:45 -0500 Subject: [PATCH 27/54] test: assert is valid signature --- test/Milkman.t.sol | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/test/Milkman.t.sol b/test/Milkman.t.sol index fbec2aa..43602ee 100644 --- a/test/Milkman.t.sol +++ b/test/Milkman.t.sol @@ -22,6 +22,7 @@ import {IERC20 as CoWIERC20} from "@cow-protocol/contracts/interfaces/IERC20.sol contract MilkmanTest is Test { using Surl for *; using stdJson for string; + using GPv2Order for GPv2Order.Data; Milkman milkman; address sushiswapExpectedOutCalculator; @@ -33,8 +34,9 @@ contract MilkmanTest is Test { address whale; bytes32 public constant APP_DATA = 0x2B8694ED30082129598720860E8E972F07AA10D9B81CAE16CA0E2CFB24743E24; - bytes32 public constant KIND_SELL = 0xf3b277728b3fee749481eb3e0b3b48980dbbab78658fc419025cb16eee346775; - bytes32 public constant ERC20_BALANCE = 0x5a28e9363bb942b639270062aa6bb295f434bcdfc42c97267bf003f272060dc9; + bytes4 internal constant MAGIC_VALUE = 0x1626ba7e; + bytes4 internal constant NON_MAGIC_VALUE = 0xffffffff; + bytes32 public constant SWAP_REQUESTED_EVENT = keccak256("SwapRequested(address,address,uint256,address,address,address,address,bytes)"); @@ -255,6 +257,12 @@ contract MilkmanTest is Test { bytes("") ); + bytes32 orderDigest = order.hash(milkman.DOMAIN_SEPARATOR()); + + bytes4 isValidSignature = Milkman(orderContract).isValidSignature(orderDigest, signatureEncodedOrder); + + assertEq(isValidSignature, MAGIC_VALUE); + // bytes memory signatureEncodedOrder = encodeOrderEIP1271( // amountToSell, // buyAmount, From 5b5a7309691151c2d85e16d9aed978bdcaa2818f Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Tue, 30 Jan 2024 16:21:56 -0500 Subject: [PATCH 28/54] test: remove unnecessary helper function --- test/Milkman.t.sol | 34 ++-------------------------------- 1 file changed, 2 insertions(+), 32 deletions(-) diff --git a/test/Milkman.t.sol b/test/Milkman.t.sol index 43602ee..5e4269e 100644 --- a/test/Milkman.t.sol +++ b/test/Milkman.t.sol @@ -49,42 +49,12 @@ contract MilkmanTest is Test { mapping(string => uint256) private amounts; mapping(string => address) private whaleAddresses; - function parseUint(string memory json, string memory key) internal returns (uint256) { + function parseUint(string memory json, string memory key) internal pure returns (uint256) { bytes memory valueBytes = vm.parseJson(json, key); string memory valueString = abi.decode(valueBytes, (string)); return vm.parseUint(valueString); } - function encodeOrderEIP1271( - uint256 amountToSell, - uint256 buyAmount, - uint32 validTo, - uint256 feeAmount - ) - internal - returns (bytes memory) - { - // bytes memory signatureEncodedOrder = abi.encode( - // fromToken, - // toToken, - // whale, - // amountToSell, - // buyAmount, - // validTo, - // APP_DATA, - // feeAmount, - // KIND_SELL, - // false, - // ERC20_BALANCE, - // ERC20_BALANCE, - // whale, - // // priceChecker, - // bytes("") - // ); - - return bytes(""); - } - function setUp() public { milkman = new Milkman(); sushiswapExpectedOutCalculator = @@ -164,7 +134,7 @@ contract MilkmanTest is Test { toToken = IERC20(tokenAddress[tokenToBuy]); amountIn = amounts[tokenToSell] * 1e18; whale = whaleAddresses[tokenToSell]; - uint256 amountIn = amounts[tokenToSell] * 1e18; + amountIn = amounts[tokenToSell] * 1e18; } priceChecker = sushiswapPriceChecker; From 9bab835338ae4637142a91bf1cb2e4a21c4cc900 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Tue, 30 Jan 2024 16:35:32 -0500 Subject: [PATCH 29/54] test: start adding all price checkers --- test/Milkman.t.sol | 68 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 61 insertions(+), 7 deletions(-) diff --git a/test/Milkman.t.sol b/test/Milkman.t.sol index 5e4269e..8f77764 100644 --- a/test/Milkman.t.sol +++ b/test/Milkman.t.sol @@ -12,7 +12,8 @@ import "../src/pricecheckers/UniV2ExpectedOutCalculator.sol"; import "../src/pricecheckers/CurveExpectedOutCalculator.sol"; import "../src/pricecheckers/UniV3ExpectedOutCalculator.sol"; import "../src/pricecheckers/ChainlinkExpectedOutCalculator.sol"; -// import "../src/pricecheckers/SingleSidedBalancerBalWethExpectedOutCalculator.sol"; +import {SingleSidedBalancerBalWethExpectedOutCalculator} from "../src/pricecheckers/SingleSidedBalancerBalWethExpectedOutCalculator.sol"; +import "../src/pricecheckers/MetaExpectedOutCalculator.sol"; import "../src/pricecheckers/FixedSlippageChecker.sol"; import "../src/pricecheckers/DynamicSlippageChecker.sol"; import {GPv2Order} from "@cow-protocol/contracts/libraries/GPv2Order.sol"; @@ -25,14 +26,25 @@ contract MilkmanTest is Test { using GPv2Order for GPv2Order.Data; Milkman milkman; - address sushiswapExpectedOutCalculator; - address sushiswapPriceChecker; IERC20 fromToken; IERC20 toToken; uint256 amountIn; address priceChecker; address whale; + address chainlinkExpectedOutCalculator; + address curveExpectedOutCalculator; + address sushiswapExpectedOutCalculator; + address ssbBalWethExpectedOutCalculator; + address univ3ExpectedOutCalculator; + address metaExpectedOutCalculator; + address chainlinkPriceChecker; + address curvePriceChecker; + address sushiswapPriceChecker; + address univ3PriceChecker; + address metaPriceChecker; + address ssbBalWethPriceChecker; + bytes32 public constant APP_DATA = 0x2B8694ED30082129598720860E8E972F07AA10D9B81CAE16CA0E2CFB24743E24; bytes4 internal constant MAGIC_VALUE = 0x1626ba7e; bytes4 internal constant NON_MAGIC_VALUE = 0xffffffff; @@ -57,10 +69,52 @@ contract MilkmanTest is Test { function setUp() public { milkman = new Milkman(); - sushiswapExpectedOutCalculator = - address(new UniV2ExpectedOutCalculator("SUSHISWAP_EXPECTED_OUT_CALCULATOR", SUSHISWAP_ROUTER)); - sushiswapPriceChecker = - address(new FixedSlippageChecker("SUSHISWAP_500_BPS_PRICE_CHECKER", 500, sushiswapExpectedOutCalculator)); + chainlinkExpectedOutCalculator = address(new ChainlinkExpectedOutCalculator()); + curveExpectedOutCalculator = address(new CurveExpectedOutCalculator()); + + sushiswapExpectedOutCalculator = address(new UniV2ExpectedOutCalculator( + "SUSHI_EXPECTED_OUT_CALCULATOR", + 0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F // Sushi Router + )); + + ssbBalWethExpectedOutCalculator = address(new SingleSidedBalancerBalWethExpectedOutCalculator()); + univ3ExpectedOutCalculator = address(new UniV3ExpectedOutCalculator()); + metaExpectedOutCalculator = address(new MetaExpectedOutCalculator()); + + chainlinkPriceChecker = address(new DynamicSlippageChecker( + "CHAINLINK_DYNAMIC_SLIPPAGE_PRICE_CHECKER", + chainlinkExpectedOutCalculator + )); + + curvePriceChecker = address(new DynamicSlippageChecker( + "CURVE_DYNAMIC_SLIPPAGE_PRICE_CHECKER", + curveExpectedOutCalculator + )); + + sushiswapPriceChecker = address(new FixedSlippageChecker( + "SUSHISWAP_STATIC_500_BPS_SLIPPAGE_PRICE_CHECKER", + 500, // 5% slippage + sushiswapExpectedOutCalculator + )); + + univ3PriceChecker = address(new DynamicSlippageChecker( + "UNIV3_DYNAMIC_SLIPPAGE_PRICE_CHECKER", + univ3ExpectedOutCalculator + )); + + metaPriceChecker = address(new DynamicSlippageChecker( + "META_DYNAMIC_SLIPPAGE_PRICE_CHECKER", + metaExpectedOutCalculator + )); + + ssbBalWethPriceChecker = address(new DynamicSlippageChecker( + "SSB_BAL_WETH_DYNAMIC_SLIPPAGE_PRICE_CHECKER", + ssbBalWethExpectedOutCalculator + )); + // sushiswapExpectedOutCalculator = + // address(new UniV2ExpectedOutCalculator("SUSHISWAP_EXPECTED_OUT_CALCULATOR", SUSHISWAP_ROUTER)); + // sushiswapPriceChecker = + // address(new FixedSlippageChecker("SUSHISWAP_500_BPS_PRICE_CHECKER", 500, sushiswapExpectedOutCalculator)); tokenAddress["TOKE"] = 0x2e9d63788249371f1DFC918a52f8d799F4a38C94; tokenAddress["DAI"] = 0x6B175474E89094C44Da98b954EedeAC495271d0F; From 07bc17335449e493793ee9abae58807a69d6a353 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Tue, 30 Jan 2024 16:42:31 -0500 Subject: [PATCH 30/54] test: use correct receiver so test passes --- test/Milkman.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Milkman.t.sol b/test/Milkman.t.sol index 8f77764..63f9bd1 100644 --- a/test/Milkman.t.sol +++ b/test/Milkman.t.sol @@ -262,7 +262,7 @@ contract MilkmanTest is Test { GPv2Order.Data memory order = GPv2Order.Data({ sellToken: CoWIERC20(address(fromToken)), buyToken: CoWIERC20(address(toToken)), - receiver: whale, + receiver: address(this), sellAmount: amountToSell, feeAmount: feeAmount, buyAmount: buyAmount, From 0df3f8da5f9f1d4551f1eff0b111f43a78d0c5cc Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Tue, 30 Jan 2024 16:43:17 -0500 Subject: [PATCH 31/54] test: remove a bunch of fluff --- test/Milkman.t.sol | 60 ---------------------------------------------- 1 file changed, 60 deletions(-) diff --git a/test/Milkman.t.sol b/test/Milkman.t.sol index 63f9bd1..dafe39b 100644 --- a/test/Milkman.t.sol +++ b/test/Milkman.t.sol @@ -286,67 +286,7 @@ contract MilkmanTest is Test { bytes4 isValidSignature = Milkman(orderContract).isValidSignature(orderDigest, signatureEncodedOrder); assertEq(isValidSignature, MAGIC_VALUE); - - // bytes memory signatureEncodedOrder = encodeOrderEIP1271( - // amountToSell, - // buyAmount, - // uint32(validTo), - // feeAmount - // ); - - // bytes memory signatureEncodedOrder = abi.encode( - // address(fromToken), - // address(toToken), - // whale, - // amountToSell, - // buyAmount, - // validTo, - // APP_DATA, - // feeAmount, - // KIND_SELL, - // false, - // ERC20_BALANCE, - // ERC20_BALANCE, - // whale, - // priceChecker, - // bytes("") - // ); - - // bytes memory feeBytes = vm.parseJson(json, ".quote.fee"); - // uint256 fee = abi.decode(feeBytes, (uint256)); - // console.log("fee", fee); - - // bytes memory sellTokenBytes = vm.parseJson(json, ".quote.sellToken"); - // address sellToken = abi.decode(sellTokenBytes, (address)); - // console.log("sellToken", sellToken); - - // bytes memory b = vm.parseJson(json, ".quote.sellToken"); - // console.log(string(b)); - // address b = vm.parseJsonAddress(json, ".quote.sellToken"); - // console.log(b); - - // (uint256 status, bytes memory data) = "https://httpbin.org/get".get(); - // console.log("status", status); - // console.log("body", string(data)); - - // console.log(orderContract); - // console.lo(fromToken.balanceOf(orderContract)); - - // console.log("log", entries[3].topics.length); - - // assertEq(entries.length, 1); - - // vm.expectEmit(true, true, true, true); } - // priceChecker = sushiswapPriceChecker; - // Arrange: Set up the state before calling the function - // uint256 amountIn = 1e18; // Example amount - - // // Act: Call the function you want to test - - // Assert: Check the state after calling the function - // Example: Assert that the swap was requested correctly - // assertTrue(true); } // Additional test cases for different scenarios and edge cases From 4176dbb46f65350fe6a62f9715c943ad1e7e8b0f Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Tue, 30 Jan 2024 16:53:49 -0500 Subject: [PATCH 32/54] test: pull in price checker instead of hard coding --- test/Milkman.t.sol | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/test/Milkman.t.sol b/test/Milkman.t.sol index dafe39b..75e16fa 100644 --- a/test/Milkman.t.sol +++ b/test/Milkman.t.sol @@ -60,6 +60,7 @@ contract MilkmanTest is Test { string[] private tokensToSell; mapping(string => uint256) private amounts; mapping(string => address) private whaleAddresses; + mapping(string => address) private priceCheckers; function parseUint(string memory json, string memory key) internal pure returns (uint256) { bytes memory valueBytes = vm.parseJson(json, key); @@ -111,10 +112,6 @@ contract MilkmanTest is Test { "SSB_BAL_WETH_DYNAMIC_SLIPPAGE_PRICE_CHECKER", ssbBalWethExpectedOutCalculator )); - // sushiswapExpectedOutCalculator = - // address(new UniV2ExpectedOutCalculator("SUSHISWAP_EXPECTED_OUT_CALCULATOR", SUSHISWAP_ROUTER)); - // sushiswapPriceChecker = - // address(new FixedSlippageChecker("SUSHISWAP_500_BPS_PRICE_CHECKER", 500, sushiswapExpectedOutCalculator)); tokenAddress["TOKE"] = 0x2e9d63788249371f1DFC918a52f8d799F4a38C94; tokenAddress["DAI"] = 0x6B175474E89094C44Da98b954EedeAC495271d0F; @@ -175,6 +172,19 @@ contract MilkmanTest is Test { whaleAddresses["YFI"] = 0xFEB4acf3df3cDEA7399794D0869ef76A6EfAff52; // whaleAddresses["COW"] = 0xca771eda0c70aa7d053ab1b25004559b918fe662; + priceCheckers["TOKE"] = sushiswapPriceChecker; + priceCheckers["USDC"] = curvePriceChecker; + priceCheckers["GUSD"] = curvePriceChecker; + priceCheckers["AAVE"] = chainlinkPriceChecker; + priceCheckers["BAT"] = chainlinkPriceChecker; + priceCheckers["YFI"] = chainlinkPriceChecker; + priceCheckers["USDT"] = chainlinkPriceChecker; + priceCheckers["UNI"] = univ3PriceChecker; + priceCheckers["BAL"] = ssbBalWethPriceChecker; + priceCheckers["WETH"] = ssbBalWethPriceChecker; + // priceCheckers["COW"] = fixedMinOut; + // priceCheckers["ALCX"] = validFrom; + // tokensToSell = ["TOKE", "USDC", "GUSD", "AAVE", "BAT", "WETH", "UNI", "ALCX", "BAL", "YFI", "USDT", "COW"]; tokensToSell = ["TOKE"]; } @@ -189,8 +199,8 @@ contract MilkmanTest is Test { amountIn = amounts[tokenToSell] * 1e18; whale = whaleAddresses[tokenToSell]; amountIn = amounts[tokenToSell] * 1e18; + priceChecker = priceCheckers[tokenToSell]; } - priceChecker = sushiswapPriceChecker; vm.prank(whale); fromToken.approve(address(milkman), amountIn); From 706148e5453f05931f275e327a1a930ddd20b992 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Tue, 30 Jan 2024 16:54:30 -0500 Subject: [PATCH 33/54] test: forge fmt --- test/Milkman.t.sol | 95 +++++++++++++++++++++++++--------------------- 1 file changed, 52 insertions(+), 43 deletions(-) diff --git a/test/Milkman.t.sol b/test/Milkman.t.sol index 75e16fa..0837f18 100644 --- a/test/Milkman.t.sol +++ b/test/Milkman.t.sol @@ -12,7 +12,8 @@ import "../src/pricecheckers/UniV2ExpectedOutCalculator.sol"; import "../src/pricecheckers/CurveExpectedOutCalculator.sol"; import "../src/pricecheckers/UniV3ExpectedOutCalculator.sol"; import "../src/pricecheckers/ChainlinkExpectedOutCalculator.sol"; -import {SingleSidedBalancerBalWethExpectedOutCalculator} from "../src/pricecheckers/SingleSidedBalancerBalWethExpectedOutCalculator.sol"; +import {SingleSidedBalancerBalWethExpectedOutCalculator} from + "../src/pricecheckers/SingleSidedBalancerBalWethExpectedOutCalculator.sol"; import "../src/pricecheckers/MetaExpectedOutCalculator.sol"; import "../src/pricecheckers/FixedSlippageChecker.sol"; import "../src/pricecheckers/DynamicSlippageChecker.sol"; @@ -49,7 +50,6 @@ contract MilkmanTest is Test { bytes4 internal constant MAGIC_VALUE = 0x1626ba7e; bytes4 internal constant NON_MAGIC_VALUE = 0xffffffff; - bytes32 public constant SWAP_REQUESTED_EVENT = keccak256("SwapRequested(address,address,uint256,address,address,address,address,bytes)"); @@ -72,46 +72,60 @@ contract MilkmanTest is Test { milkman = new Milkman(); chainlinkExpectedOutCalculator = address(new ChainlinkExpectedOutCalculator()); curveExpectedOutCalculator = address(new CurveExpectedOutCalculator()); - - sushiswapExpectedOutCalculator = address(new UniV2ExpectedOutCalculator( - "SUSHI_EXPECTED_OUT_CALCULATOR", - 0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F // Sushi Router - )); + + sushiswapExpectedOutCalculator = address( + new UniV2ExpectedOutCalculator( + "SUSHI_EXPECTED_OUT_CALCULATOR", + 0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F // Sushi Router + ) + ); ssbBalWethExpectedOutCalculator = address(new SingleSidedBalancerBalWethExpectedOutCalculator()); univ3ExpectedOutCalculator = address(new UniV3ExpectedOutCalculator()); metaExpectedOutCalculator = address(new MetaExpectedOutCalculator()); - chainlinkPriceChecker = address(new DynamicSlippageChecker( - "CHAINLINK_DYNAMIC_SLIPPAGE_PRICE_CHECKER", - chainlinkExpectedOutCalculator - )); - - curvePriceChecker = address(new DynamicSlippageChecker( - "CURVE_DYNAMIC_SLIPPAGE_PRICE_CHECKER", - curveExpectedOutCalculator - )); - - sushiswapPriceChecker = address(new FixedSlippageChecker( - "SUSHISWAP_STATIC_500_BPS_SLIPPAGE_PRICE_CHECKER", - 500, // 5% slippage - sushiswapExpectedOutCalculator - )); - - univ3PriceChecker = address(new DynamicSlippageChecker( - "UNIV3_DYNAMIC_SLIPPAGE_PRICE_CHECKER", - univ3ExpectedOutCalculator - )); - - metaPriceChecker = address(new DynamicSlippageChecker( - "META_DYNAMIC_SLIPPAGE_PRICE_CHECKER", - metaExpectedOutCalculator - )); - - ssbBalWethPriceChecker = address(new DynamicSlippageChecker( - "SSB_BAL_WETH_DYNAMIC_SLIPPAGE_PRICE_CHECKER", - ssbBalWethExpectedOutCalculator - )); + chainlinkPriceChecker = address( + new DynamicSlippageChecker( + "CHAINLINK_DYNAMIC_SLIPPAGE_PRICE_CHECKER", + chainlinkExpectedOutCalculator + ) + ); + + curvePriceChecker = address( + new DynamicSlippageChecker( + "CURVE_DYNAMIC_SLIPPAGE_PRICE_CHECKER", + curveExpectedOutCalculator + ) + ); + + sushiswapPriceChecker = address( + new FixedSlippageChecker( + "SUSHISWAP_STATIC_500_BPS_SLIPPAGE_PRICE_CHECKER", + 500, // 5% slippage + sushiswapExpectedOutCalculator + ) + ); + + univ3PriceChecker = address( + new DynamicSlippageChecker( + "UNIV3_DYNAMIC_SLIPPAGE_PRICE_CHECKER", + univ3ExpectedOutCalculator + ) + ); + + metaPriceChecker = address( + new DynamicSlippageChecker( + "META_DYNAMIC_SLIPPAGE_PRICE_CHECKER", + metaExpectedOutCalculator + ) + ); + + ssbBalWethPriceChecker = address( + new DynamicSlippageChecker( + "SSB_BAL_WETH_DYNAMIC_SLIPPAGE_PRICE_CHECKER", + ssbBalWethExpectedOutCalculator + ) + ); tokenAddress["TOKE"] = 0x2e9d63788249371f1DFC918a52f8d799F4a38C94; tokenAddress["DAI"] = 0x6B175474E89094C44Da98b954EedeAC495271d0F; @@ -284,12 +298,7 @@ contract MilkmanTest is Test { appData: APP_DATA }); - bytes memory signatureEncodedOrder = abi.encode( - order, - whale, - priceChecker, - bytes("") - ); + bytes memory signatureEncodedOrder = abi.encode(order, whale, priceChecker, bytes("")); bytes32 orderDigest = order.hash(milkman.DOMAIN_SEPARATOR()); From 327bc7cc93a70d184be52991c89a502272039602 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Tue, 30 Jan 2024 17:06:50 -0500 Subject: [PATCH 34/54] test: dynamically scale `amountIn` with decimals --- test/Milkman.t.sol | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/Milkman.t.sol b/test/Milkman.t.sol index 0837f18..190d6d2 100644 --- a/test/Milkman.t.sol +++ b/test/Milkman.t.sol @@ -20,6 +20,9 @@ import "../src/pricecheckers/DynamicSlippageChecker.sol"; import {GPv2Order} from "@cow-protocol/contracts/libraries/GPv2Order.sol"; import {IERC20 as CoWIERC20} from "@cow-protocol/contracts/interfaces/IERC20.sol"; // import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +interface IERC20Metadata { + function decimals() external view returns (uint8); +} contract MilkmanTest is Test { using Surl for *; @@ -210,9 +213,9 @@ contract MilkmanTest is Test { string memory tokenToBuy = sellToBuyMap[tokenToSell]; fromToken = IERC20(tokenAddress[tokenToSell]); toToken = IERC20(tokenAddress[tokenToBuy]); - amountIn = amounts[tokenToSell] * 1e18; + uint8 decimals = IERC20Metadata(address(fromToken)).decimals(); + amountIn = amounts[tokenToSell] * (10 ** decimals); whale = whaleAddresses[tokenToSell]; - amountIn = amounts[tokenToSell] * 1e18; priceChecker = priceCheckers[tokenToSell]; } From 7aada113ee442de2fa77097196888fec0386b0ef Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Tue, 30 Jan 2024 19:50:46 -0500 Subject: [PATCH 35/54] test: add gas check --- test/Milkman.t.sol | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/Milkman.t.sol b/test/Milkman.t.sol index 190d6d2..48c342f 100644 --- a/test/Milkman.t.sol +++ b/test/Milkman.t.sol @@ -20,6 +20,7 @@ import "../src/pricecheckers/DynamicSlippageChecker.sol"; import {GPv2Order} from "@cow-protocol/contracts/libraries/GPv2Order.sol"; import {IERC20 as CoWIERC20} from "@cow-protocol/contracts/interfaces/IERC20.sol"; // import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeMath} from "@openzeppelin/contracts/math/SafeMath.sol"; interface IERC20Metadata { function decimals() external view returns (uint8); } @@ -28,6 +29,7 @@ contract MilkmanTest is Test { using Surl for *; using stdJson for string; using GPv2Order for GPv2Order.Data; + using SafeMath for uint256; Milkman milkman; IERC20 fromToken; @@ -305,9 +307,17 @@ contract MilkmanTest is Test { bytes32 orderDigest = order.hash(milkman.DOMAIN_SEPARATOR()); + uint256 gasBefore = gasleft(); bytes4 isValidSignature = Milkman(orderContract).isValidSignature(orderDigest, signatureEncodedOrder); + uint256 gasAfter = gasleft(); + + uint256 gasConsumed = gasBefore.sub(gasAfter); + + console.log("gas consumed:", gasConsumed); assertEq(isValidSignature, MAGIC_VALUE); + + assertLt(gasConsumed, 1_000_000); } } From 560abddce575214d256b80c48824aba77f80d7d3 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Tue, 30 Jan 2024 19:58:45 -0500 Subject: [PATCH 36/54] test: ensure that price checker returns true --- test/Milkman.t.sol | 73 ++++++++++++++++++++++++++++------------------ 1 file changed, 44 insertions(+), 29 deletions(-) diff --git a/test/Milkman.t.sol b/test/Milkman.t.sol index 48c342f..d9c7d88 100644 --- a/test/Milkman.t.sol +++ b/test/Milkman.t.sol @@ -17,10 +17,12 @@ import {SingleSidedBalancerBalWethExpectedOutCalculator} from import "../src/pricecheckers/MetaExpectedOutCalculator.sol"; import "../src/pricecheckers/FixedSlippageChecker.sol"; import "../src/pricecheckers/DynamicSlippageChecker.sol"; +import {IPriceChecker} from "../interfaces/IPriceChecker.sol"; import {GPv2Order} from "@cow-protocol/contracts/libraries/GPv2Order.sol"; import {IERC20 as CoWIERC20} from "@cow-protocol/contracts/interfaces/IERC20.sol"; // import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeMath} from "@openzeppelin/contracts/math/SafeMath.sol"; + interface IERC20Metadata { function decimals() external view returns (uint8); } @@ -80,9 +82,9 @@ contract MilkmanTest is Test { sushiswapExpectedOutCalculator = address( new UniV2ExpectedOutCalculator( - "SUSHI_EXPECTED_OUT_CALCULATOR", - 0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F // Sushi Router - ) + "SUSHI_EXPECTED_OUT_CALCULATOR", + 0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F // Sushi Router + ) ); ssbBalWethExpectedOutCalculator = address(new SingleSidedBalancerBalWethExpectedOutCalculator()); @@ -91,45 +93,45 @@ contract MilkmanTest is Test { chainlinkPriceChecker = address( new DynamicSlippageChecker( - "CHAINLINK_DYNAMIC_SLIPPAGE_PRICE_CHECKER", - chainlinkExpectedOutCalculator - ) + "CHAINLINK_DYNAMIC_SLIPPAGE_PRICE_CHECKER", + chainlinkExpectedOutCalculator + ) ); curvePriceChecker = address( new DynamicSlippageChecker( - "CURVE_DYNAMIC_SLIPPAGE_PRICE_CHECKER", - curveExpectedOutCalculator - ) + "CURVE_DYNAMIC_SLIPPAGE_PRICE_CHECKER", + curveExpectedOutCalculator + ) ); sushiswapPriceChecker = address( new FixedSlippageChecker( - "SUSHISWAP_STATIC_500_BPS_SLIPPAGE_PRICE_CHECKER", - 500, // 5% slippage - sushiswapExpectedOutCalculator - ) + "SUSHISWAP_STATIC_500_BPS_SLIPPAGE_PRICE_CHECKER", + 500, // 5% slippage + sushiswapExpectedOutCalculator + ) ); univ3PriceChecker = address( new DynamicSlippageChecker( - "UNIV3_DYNAMIC_SLIPPAGE_PRICE_CHECKER", - univ3ExpectedOutCalculator - ) + "UNIV3_DYNAMIC_SLIPPAGE_PRICE_CHECKER", + univ3ExpectedOutCalculator + ) ); metaPriceChecker = address( new DynamicSlippageChecker( - "META_DYNAMIC_SLIPPAGE_PRICE_CHECKER", - metaExpectedOutCalculator - ) + "META_DYNAMIC_SLIPPAGE_PRICE_CHECKER", + metaExpectedOutCalculator + ) ); ssbBalWethPriceChecker = address( new DynamicSlippageChecker( - "SSB_BAL_WETH_DYNAMIC_SLIPPAGE_PRICE_CHECKER", - ssbBalWethExpectedOutCalculator - ) + "SSB_BAL_WETH_DYNAMIC_SLIPPAGE_PRICE_CHECKER", + ssbBalWethExpectedOutCalculator + ) ); tokenAddress["TOKE"] = 0x2e9d63788249371f1DFC918a52f8d799F4a38C94; @@ -286,6 +288,17 @@ contract MilkmanTest is Test { uint256 amountToSell = amountIn - feeAmount; assertLt(amountToSell, amountIn); + assertTrue( + IPriceChecker(priceChecker).checkPrice( + amountToSell, + address(fromToken), + address(toToken), + feeAmount, + buyAmount, + bytes("") + ) + ); + uint32 validTo = uint32(block.timestamp) + 60 * 60 * 24; GPv2Order.Data memory order = GPv2Order.Data({ @@ -307,17 +320,19 @@ contract MilkmanTest is Test { bytes32 orderDigest = order.hash(milkman.DOMAIN_SEPARATOR()); - uint256 gasBefore = gasleft(); - bytes4 isValidSignature = Milkman(orderContract).isValidSignature(orderDigest, signatureEncodedOrder); - uint256 gasAfter = gasleft(); + { + uint256 gasBefore = gasleft(); + bytes4 isValidSignature = Milkman(orderContract).isValidSignature(orderDigest, signatureEncodedOrder); + uint256 gasAfter = gasleft(); - uint256 gasConsumed = gasBefore.sub(gasAfter); + uint256 gasConsumed = gasBefore.sub(gasAfter); - console.log("gas consumed:", gasConsumed); + console.log("gas consumed:", gasConsumed); - assertEq(isValidSignature, MAGIC_VALUE); + assertLt(gasConsumed, 1_000_000); - assertLt(gasConsumed, 1_000_000); + assertEq(isValidSignature, MAGIC_VALUE); + } } } From 5cb7e425dbe8010fdf41aae25c0a36f22a365f65 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Tue, 30 Jan 2024 20:17:12 -0500 Subject: [PATCH 37/54] test: check that the price checker returns false for bad price --- test/Milkman.t.sol | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/Milkman.t.sol b/test/Milkman.t.sol index d9c7d88..ac14695 100644 --- a/test/Milkman.t.sol +++ b/test/Milkman.t.sol @@ -333,6 +333,21 @@ contract MilkmanTest is Test { assertEq(isValidSignature, MAGIC_VALUE); } + + // now check that it will revert with a bad price + + uint256 badAmountOut = (buyAmount * 4) / 5; + + assertFalse( + IPriceChecker(priceChecker).checkPrice( + amountToSell, + address(fromToken), + address(toToken), + feeAmount, + badAmountOut, + bytes("") + ) + ); } } From 936957f33e9afb56bbeca0642f6c02b98a39b9d8 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Tue, 30 Jan 2024 20:30:58 -0500 Subject: [PATCH 38/54] test: ensure that milkman reverts on non-matches and buy orders --- test/Milkman.t.sol | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/test/Milkman.t.sol b/test/Milkman.t.sol index ac14695..f835415 100644 --- a/test/Milkman.t.sol +++ b/test/Milkman.t.sol @@ -334,7 +334,7 @@ contract MilkmanTest is Test { assertEq(isValidSignature, MAGIC_VALUE); } - // now check that it will revert with a bad price + // check that price checker returns false with bad price uint256 badAmountOut = (buyAmount * 4) / 5; @@ -348,6 +348,32 @@ contract MilkmanTest is Test { bytes("") ) ); + + // check that milkman reverts with bad price + + order.buyAmount = badAmountOut; + signatureEncodedOrder = abi.encode(order, whale, priceChecker, bytes("")); + orderDigest = order.hash(milkman.DOMAIN_SEPARATOR()); + vm.expectRevert("invalid_min_out"); + Milkman(orderContract).isValidSignature(orderDigest, signatureEncodedOrder); + + // check that milkman reverts if the hash doesn't match the order + + order.buyAmount = buyAmount; + orderDigest = order.hash(milkman.DOMAIN_SEPARATOR()); + order.validTo = validTo + 10; + signatureEncodedOrder = abi.encode(order, whale, priceChecker, bytes("")); + vm.expectRevert("!match"); + Milkman(orderContract).isValidSignature(orderDigest, signatureEncodedOrder); + + // check that milkman reverts if the keeper generates a buy order + + order.validTo = validTo; + order.kind = GPv2Order.KIND_BUY; + orderDigest = order.hash(milkman.DOMAIN_SEPARATOR()); + signatureEncodedOrder = abi.encode(order, whale, priceChecker, bytes("")); + vm.expectRevert("!kind_sell"); + Milkman(orderContract).isValidSignature(orderDigest, signatureEncodedOrder); } } From 0628275664797910b46f944a10ab08ca32d62ae1 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Tue, 30 Jan 2024 20:37:57 -0500 Subject: [PATCH 39/54] test check that milkman reverts if `validTo` is too close --- test/Milkman.t.sol | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/test/Milkman.t.sol b/test/Milkman.t.sol index f835415..a3e3eca 100644 --- a/test/Milkman.t.sol +++ b/test/Milkman.t.sol @@ -356,24 +356,35 @@ contract MilkmanTest is Test { orderDigest = order.hash(milkman.DOMAIN_SEPARATOR()); vm.expectRevert("invalid_min_out"); Milkman(orderContract).isValidSignature(orderDigest, signatureEncodedOrder); + order.buyAmount = buyAmount; // check that milkman reverts if the hash doesn't match the order - order.buyAmount = buyAmount; orderDigest = order.hash(milkman.DOMAIN_SEPARATOR()); order.validTo = validTo + 10; signatureEncodedOrder = abi.encode(order, whale, priceChecker, bytes("")); vm.expectRevert("!match"); Milkman(orderContract).isValidSignature(orderDigest, signatureEncodedOrder); + order.validTo = validTo; // check that milkman reverts if the keeper generates a buy order - order.validTo = validTo; order.kind = GPv2Order.KIND_BUY; orderDigest = order.hash(milkman.DOMAIN_SEPARATOR()); signatureEncodedOrder = abi.encode(order, whale, priceChecker, bytes("")); vm.expectRevert("!kind_sell"); Milkman(orderContract).isValidSignature(orderDigest, signatureEncodedOrder); + order.kind = GPv2Order.KIND_SELL; + + // check that milkman reverts if the validTo is too close + + uint32 badValidTo = uint32(block.timestamp) + 2 * 60; + order.validTo = badValidTo; + orderDigest = order.hash(milkman.DOMAIN_SEPARATOR()); + signatureEncodedOrder = abi.encode(order, whale, priceChecker, bytes("")); + vm.expectRevert("expires_too_soon"); + Milkman(orderContract).isValidSignature(orderDigest, signatureEncodedOrder); + order.validTo = validTo; } } From 56d24bc8b3722c32fbd779c4738b5f38f84d0dd8 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Tue, 30 Jan 2024 20:40:12 -0500 Subject: [PATCH 40/54] test: check that milkman reverts for non-fill-or-kill orders --- test/Milkman.t.sol | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/Milkman.t.sol b/test/Milkman.t.sol index a3e3eca..ef89559 100644 --- a/test/Milkman.t.sol +++ b/test/Milkman.t.sol @@ -385,6 +385,15 @@ contract MilkmanTest is Test { vm.expectRevert("expires_too_soon"); Milkman(orderContract).isValidSignature(orderDigest, signatureEncodedOrder); order.validTo = validTo; + + // check that milkman reverts for non-fill-or-kill orders + + order.partiallyFillable = true; + orderDigest = order.hash(milkman.DOMAIN_SEPARATOR()); + signatureEncodedOrder = abi.encode(order, whale, priceChecker, bytes("")); + vm.expectRevert("!fill_or_kill"); + Milkman(orderContract).isValidSignature(orderDigest, signatureEncodedOrder); + order.partiallyFillable = false; } } From a62c22c7d7d8c9c62538c1645cf56e945cf1bce5 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Tue, 30 Jan 2024 20:40:36 -0500 Subject: [PATCH 41/54] test: check that milkman reverts non-fill-or-kill orders --- test/Milkman.t.sol | 58 +++++++++++++++++++--------------------------- 1 file changed, 24 insertions(+), 34 deletions(-) diff --git a/test/Milkman.t.sol b/test/Milkman.t.sol index ef89559..e653d9a 100644 --- a/test/Milkman.t.sol +++ b/test/Milkman.t.sol @@ -82,9 +82,9 @@ contract MilkmanTest is Test { sushiswapExpectedOutCalculator = address( new UniV2ExpectedOutCalculator( - "SUSHI_EXPECTED_OUT_CALCULATOR", - 0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F // Sushi Router - ) + "SUSHI_EXPECTED_OUT_CALCULATOR", + 0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F // Sushi Router + ) ); ssbBalWethExpectedOutCalculator = address(new SingleSidedBalancerBalWethExpectedOutCalculator()); @@ -93,45 +93,45 @@ contract MilkmanTest is Test { chainlinkPriceChecker = address( new DynamicSlippageChecker( - "CHAINLINK_DYNAMIC_SLIPPAGE_PRICE_CHECKER", - chainlinkExpectedOutCalculator - ) + "CHAINLINK_DYNAMIC_SLIPPAGE_PRICE_CHECKER", + chainlinkExpectedOutCalculator + ) ); curvePriceChecker = address( new DynamicSlippageChecker( - "CURVE_DYNAMIC_SLIPPAGE_PRICE_CHECKER", - curveExpectedOutCalculator - ) + "CURVE_DYNAMIC_SLIPPAGE_PRICE_CHECKER", + curveExpectedOutCalculator + ) ); sushiswapPriceChecker = address( new FixedSlippageChecker( - "SUSHISWAP_STATIC_500_BPS_SLIPPAGE_PRICE_CHECKER", - 500, // 5% slippage - sushiswapExpectedOutCalculator - ) + "SUSHISWAP_STATIC_500_BPS_SLIPPAGE_PRICE_CHECKER", + 500, // 5% slippage + sushiswapExpectedOutCalculator + ) ); univ3PriceChecker = address( new DynamicSlippageChecker( - "UNIV3_DYNAMIC_SLIPPAGE_PRICE_CHECKER", - univ3ExpectedOutCalculator - ) + "UNIV3_DYNAMIC_SLIPPAGE_PRICE_CHECKER", + univ3ExpectedOutCalculator + ) ); metaPriceChecker = address( new DynamicSlippageChecker( - "META_DYNAMIC_SLIPPAGE_PRICE_CHECKER", - metaExpectedOutCalculator - ) + "META_DYNAMIC_SLIPPAGE_PRICE_CHECKER", + metaExpectedOutCalculator + ) ); ssbBalWethPriceChecker = address( new DynamicSlippageChecker( - "SSB_BAL_WETH_DYNAMIC_SLIPPAGE_PRICE_CHECKER", - ssbBalWethExpectedOutCalculator - ) + "SSB_BAL_WETH_DYNAMIC_SLIPPAGE_PRICE_CHECKER", + ssbBalWethExpectedOutCalculator + ) ); tokenAddress["TOKE"] = 0x2e9d63788249371f1DFC918a52f8d799F4a38C94; @@ -290,12 +290,7 @@ contract MilkmanTest is Test { assertTrue( IPriceChecker(priceChecker).checkPrice( - amountToSell, - address(fromToken), - address(toToken), - feeAmount, - buyAmount, - bytes("") + amountToSell, address(fromToken), address(toToken), feeAmount, buyAmount, bytes("") ) ); @@ -340,12 +335,7 @@ contract MilkmanTest is Test { assertFalse( IPriceChecker(priceChecker).checkPrice( - amountToSell, - address(fromToken), - address(toToken), - feeAmount, - badAmountOut, - bytes("") + amountToSell, address(fromToken), address(toToken), feeAmount, badAmountOut, bytes("") ) ); From 790ca1966b11d084a85fc277f0c9aea2a169c684 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Tue, 30 Jan 2024 20:45:25 -0500 Subject: [PATCH 42/54] test: check that milkman reverts if buy or sell is set to non-ERC20 --- test/Milkman.t.sol | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/Milkman.t.sol b/test/Milkman.t.sol index e653d9a..31fcb2d 100644 --- a/test/Milkman.t.sol +++ b/test/Milkman.t.sol @@ -384,6 +384,24 @@ contract MilkmanTest is Test { vm.expectRevert("!fill_or_kill"); Milkman(orderContract).isValidSignature(orderDigest, signatureEncodedOrder); order.partiallyFillable = false; + + // check that milkman reverts if set to non ERC20 sell balance + + order.sellTokenBalance = GPv2Order.BALANCE_EXTERNAL; + orderDigest = order.hash(milkman.DOMAIN_SEPARATOR()); + signatureEncodedOrder = abi.encode(order, whale, priceChecker, bytes("")); + vm.expectRevert("!sell_erc20"); + Milkman(orderContract).isValidSignature(orderDigest, signatureEncodedOrder); + order.sellTokenBalance = GPv2Order.BALANCE_ERC20; + + // check that milkman reverts if set to non ERC20 buy balance + + order.buyTokenBalance = GPv2Order.BALANCE_INTERNAL; + orderDigest = order.hash(milkman.DOMAIN_SEPARATOR()); + signatureEncodedOrder = abi.encode(order, whale, priceChecker, bytes("")); + vm.expectRevert("!buy_erc20"); + Milkman(orderContract).isValidSignature(orderDigest, signatureEncodedOrder); + order.buyTokenBalance = GPv2Order.BALANCE_ERC20; } } From 0a626eccac40a3321bcc7c2c01b174846d5a9344 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Wed, 31 Jan 2024 08:02:32 -0700 Subject: [PATCH 43/54] test: get swap requested event dynamically --- test/Milkman.t.sol | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/test/Milkman.t.sol b/test/Milkman.t.sol index 31fcb2d..2390452 100644 --- a/test/Milkman.t.sol +++ b/test/Milkman.t.sol @@ -207,7 +207,7 @@ contract MilkmanTest is Test { // priceCheckers["ALCX"] = validFrom; // tokensToSell = ["TOKE", "USDC", "GUSD", "AAVE", "BAT", "WETH", "UNI", "ALCX", "BAL", "YFI", "USDT", "COW"]; - tokensToSell = ["TOKE"]; + tokensToSell = ["USDC"]; } function testRequestSwapExactTokensForTokens() public { @@ -240,10 +240,14 @@ contract MilkmanTest is Test { Vm.Log[] memory entries = vm.getRecordedLogs(); - assertEq(entries[3].topics[0], SWAP_REQUESTED_EVENT); - - (address orderContract,,,,,,,) = - (abi.decode(entries[3].data, (address, address, uint256, address, address, address, address, bytes))); + address orderContract = address(0); + for (uint8 i = 0; i < entries.length; ++i) { + if (entries[i].topics[0] == SWAP_REQUESTED_EVENT) { + (orderContract,,,,,,,) = + (abi.decode(entries[i].data, (address, address, uint256, address, address, address, address, bytes))); + } + } + assertNotEq(orderContract, address(0)); assertEq(fromToken.balanceOf(orderContract), amountIn); From 0ac84c12f7a1e8391f618fb8bb309301a4ccf10c Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Wed, 31 Jan 2024 08:21:58 -0700 Subject: [PATCH 44/54] test: use dynamically-generated price checker data --- test/Milkman.t.sol | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/test/Milkman.t.sol b/test/Milkman.t.sol index 2390452..729f90e 100644 --- a/test/Milkman.t.sol +++ b/test/Milkman.t.sol @@ -37,8 +37,9 @@ contract MilkmanTest is Test { IERC20 fromToken; IERC20 toToken; uint256 amountIn; - address priceChecker; address whale; + address priceChecker; + bytes priceCheckerData; address chainlinkExpectedOutCalculator; address curveExpectedOutCalculator; @@ -68,6 +69,8 @@ contract MilkmanTest is Test { mapping(string => uint256) private amounts; mapping(string => address) private whaleAddresses; mapping(string => address) private priceCheckers; + mapping(string => bytes) public priceCheckerDatas; + function parseUint(string memory json, string memory key) internal pure returns (uint256) { bytes memory valueBytes = vm.parseJson(json, key); @@ -75,6 +78,10 @@ contract MilkmanTest is Test { return vm.parseUint(valueString); } + function dynamicSlippagePriceCheckerData(uint256 allowedSlippageBips, bytes memory expectedOutData) internal pure returns (bytes memory) { + return abi.encode(allowedSlippageBips, expectedOutData); + } + function setUp() public { milkman = new Milkman(); chainlinkExpectedOutCalculator = address(new ChainlinkExpectedOutCalculator()); @@ -206,6 +213,9 @@ contract MilkmanTest is Test { // priceCheckers["COW"] = fixedMinOut; // priceCheckers["ALCX"] = validFrom; + priceCheckerDatas["TOKE"] = bytes(""); + priceCheckerDatas["USDC"] = dynamicSlippagePriceCheckerData(400, bytes("")); + // tokensToSell = ["TOKE", "USDC", "GUSD", "AAVE", "BAT", "WETH", "UNI", "ALCX", "BAL", "YFI", "USDT", "COW"]; tokensToSell = ["USDC"]; } @@ -221,6 +231,7 @@ contract MilkmanTest is Test { amountIn = amounts[tokenToSell] * (10 ** decimals); whale = whaleAddresses[tokenToSell]; priceChecker = priceCheckers[tokenToSell]; + priceCheckerData = priceCheckerDatas[tokenToSell]; } vm.prank(whale); @@ -235,7 +246,7 @@ contract MilkmanTest is Test { toToken, address(this), // Receiver address priceChecker, - "" // priceCheckerData + priceCheckerData ); Vm.Log[] memory entries = vm.getRecordedLogs(); @@ -253,7 +264,7 @@ contract MilkmanTest is Test { { bytes32 expectedSwapHash = - keccak256(abi.encode(whale, address(this), fromToken, toToken, amountIn, priceChecker, bytes(""))); + keccak256(abi.encode(whale, address(this), fromToken, toToken, amountIn, priceChecker, priceCheckerData)); assertEq(Milkman(orderContract).swapHash(), expectedSwapHash); } @@ -294,7 +305,7 @@ contract MilkmanTest is Test { assertTrue( IPriceChecker(priceChecker).checkPrice( - amountToSell, address(fromToken), address(toToken), feeAmount, buyAmount, bytes("") + amountToSell, address(fromToken), address(toToken), feeAmount, buyAmount, priceCheckerData ) ); @@ -315,7 +326,7 @@ contract MilkmanTest is Test { appData: APP_DATA }); - bytes memory signatureEncodedOrder = abi.encode(order, whale, priceChecker, bytes("")); + bytes memory signatureEncodedOrder = abi.encode(order, whale, priceChecker, priceCheckerData); bytes32 orderDigest = order.hash(milkman.DOMAIN_SEPARATOR()); @@ -339,14 +350,14 @@ contract MilkmanTest is Test { assertFalse( IPriceChecker(priceChecker).checkPrice( - amountToSell, address(fromToken), address(toToken), feeAmount, badAmountOut, bytes("") + amountToSell, address(fromToken), address(toToken), feeAmount, badAmountOut, priceCheckerData ) ); // check that milkman reverts with bad price order.buyAmount = badAmountOut; - signatureEncodedOrder = abi.encode(order, whale, priceChecker, bytes("")); + signatureEncodedOrder = abi.encode(order, whale, priceChecker, priceCheckerData); orderDigest = order.hash(milkman.DOMAIN_SEPARATOR()); vm.expectRevert("invalid_min_out"); Milkman(orderContract).isValidSignature(orderDigest, signatureEncodedOrder); @@ -356,7 +367,7 @@ contract MilkmanTest is Test { orderDigest = order.hash(milkman.DOMAIN_SEPARATOR()); order.validTo = validTo + 10; - signatureEncodedOrder = abi.encode(order, whale, priceChecker, bytes("")); + signatureEncodedOrder = abi.encode(order, whale, priceChecker, priceCheckerData); vm.expectRevert("!match"); Milkman(orderContract).isValidSignature(orderDigest, signatureEncodedOrder); order.validTo = validTo; @@ -365,7 +376,7 @@ contract MilkmanTest is Test { order.kind = GPv2Order.KIND_BUY; orderDigest = order.hash(milkman.DOMAIN_SEPARATOR()); - signatureEncodedOrder = abi.encode(order, whale, priceChecker, bytes("")); + signatureEncodedOrder = abi.encode(order, whale, priceChecker, priceCheckerData); vm.expectRevert("!kind_sell"); Milkman(orderContract).isValidSignature(orderDigest, signatureEncodedOrder); order.kind = GPv2Order.KIND_SELL; @@ -375,7 +386,7 @@ contract MilkmanTest is Test { uint32 badValidTo = uint32(block.timestamp) + 2 * 60; order.validTo = badValidTo; orderDigest = order.hash(milkman.DOMAIN_SEPARATOR()); - signatureEncodedOrder = abi.encode(order, whale, priceChecker, bytes("")); + signatureEncodedOrder = abi.encode(order, whale, priceChecker, priceCheckerData); vm.expectRevert("expires_too_soon"); Milkman(orderContract).isValidSignature(orderDigest, signatureEncodedOrder); order.validTo = validTo; @@ -384,7 +395,7 @@ contract MilkmanTest is Test { order.partiallyFillable = true; orderDigest = order.hash(milkman.DOMAIN_SEPARATOR()); - signatureEncodedOrder = abi.encode(order, whale, priceChecker, bytes("")); + signatureEncodedOrder = abi.encode(order, whale, priceChecker, priceCheckerData); vm.expectRevert("!fill_or_kill"); Milkman(orderContract).isValidSignature(orderDigest, signatureEncodedOrder); order.partiallyFillable = false; @@ -393,7 +404,7 @@ contract MilkmanTest is Test { order.sellTokenBalance = GPv2Order.BALANCE_EXTERNAL; orderDigest = order.hash(milkman.DOMAIN_SEPARATOR()); - signatureEncodedOrder = abi.encode(order, whale, priceChecker, bytes("")); + signatureEncodedOrder = abi.encode(order, whale, priceChecker, priceCheckerData); vm.expectRevert("!sell_erc20"); Milkman(orderContract).isValidSignature(orderDigest, signatureEncodedOrder); order.sellTokenBalance = GPv2Order.BALANCE_ERC20; @@ -402,7 +413,7 @@ contract MilkmanTest is Test { order.buyTokenBalance = GPv2Order.BALANCE_INTERNAL; orderDigest = order.hash(milkman.DOMAIN_SEPARATOR()); - signatureEncodedOrder = abi.encode(order, whale, priceChecker, bytes("")); + signatureEncodedOrder = abi.encode(order, whale, priceChecker, priceCheckerData); vm.expectRevert("!buy_erc20"); Milkman(orderContract).isValidSignature(orderDigest, signatureEncodedOrder); order.buyTokenBalance = GPv2Order.BALANCE_ERC20; From d455b0b444f4d91ce571bfee41087522961942b4 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Wed, 31 Jan 2024 09:56:10 -0700 Subject: [PATCH 45/54] test: test all TOKE, GUSD, and USDC --- test/Milkman.t.sol | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/test/Milkman.t.sol b/test/Milkman.t.sol index 729f90e..f73f31d 100644 --- a/test/Milkman.t.sol +++ b/test/Milkman.t.sol @@ -172,7 +172,7 @@ contract MilkmanTest is Test { amounts["TOKE"] = 80000; // 80,000 TOKE amounts["USDC"] = 5000000; // 5,000,000 USDC - amounts["GUSD"] = 1000; // 1,000 GUSD + amounts["GUSD"] = 10_000; // 10,000 GUSD amounts["AAVE"] = 2500; // 2,500 AAVE amounts["BAT"] = 280000; // 280,000 BAT amounts["WETH"] = 325; // 325 WETH @@ -213,11 +213,12 @@ contract MilkmanTest is Test { // priceCheckers["COW"] = fixedMinOut; // priceCheckers["ALCX"] = validFrom; - priceCheckerDatas["TOKE"] = bytes(""); - priceCheckerDatas["USDC"] = dynamicSlippagePriceCheckerData(400, bytes("")); + priceCheckerDatas["TOKE"] = bytes("0"); + priceCheckerDatas["USDC"] = dynamicSlippagePriceCheckerData(10, bytes("")); // up to $10 lost allowed + priceCheckerDatas["GUSD"] = dynamicSlippagePriceCheckerData(100, bytes("")); // up to $100 lost allowed // tokensToSell = ["TOKE", "USDC", "GUSD", "AAVE", "BAT", "WETH", "UNI", "ALCX", "BAL", "YFI", "USDT", "COW"]; - tokensToSell = ["USDC"]; + tokensToSell = ["TOKE", "GUSD", "USDC"]; } function testRequestSwapExactTokensForTokens() public { @@ -264,7 +265,7 @@ contract MilkmanTest is Test { { bytes32 expectedSwapHash = - keccak256(abi.encode(whale, address(this), fromToken, toToken, amountIn, priceChecker, priceCheckerData)); + keccak256(abi.encode(whale, address(this), fromToken, toToken, amountIn, priceChecker, priceCheckerData)); assertEq(Milkman(orderContract).swapHash(), expectedSwapHash); } @@ -305,7 +306,7 @@ contract MilkmanTest is Test { assertTrue( IPriceChecker(priceChecker).checkPrice( - amountToSell, address(fromToken), address(toToken), feeAmount, buyAmount, priceCheckerData + amountIn, address(fromToken), address(toToken), feeAmount, buyAmount, priceCheckerData ) ); From 0e9826b0e6adbbf43e8e05eb9eefea679f5473cb Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Sun, 4 Feb 2024 13:37:15 -0700 Subject: [PATCH 46/54] test: hookup chainlink price checker --- test/Milkman.t.sol | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/test/Milkman.t.sol b/test/Milkman.t.sol index f73f31d..5a82752 100644 --- a/test/Milkman.t.sol +++ b/test/Milkman.t.sol @@ -58,6 +58,8 @@ contract MilkmanTest is Test { bytes4 internal constant MAGIC_VALUE = 0x1626ba7e; bytes4 internal constant NON_MAGIC_VALUE = 0xffffffff; + bytes internal constant ZERO_BYTES = bytes("0"); + bytes32 public constant SWAP_REQUESTED_EVENT = keccak256("SwapRequested(address,address,uint256,address,address,address,address,bytes)"); @@ -71,6 +73,17 @@ contract MilkmanTest is Test { mapping(string => address) private priceCheckers; mapping(string => bytes) public priceCheckerDatas; + function univ2ExpectedOutData() internal pure returns (bytes memory) { + return ZERO_BYTES; + } + + function curveExpectedOutData() internal pure returns (bytes memory) { + return ZERO_BYTES; + } + + function chainlinkExpectedOutData(address[] memory priceFeeds, bool[] memory reverses) internal pure returns (bytes memory) { + return abi.encode(priceFeeds, reverses); + } function parseUint(string memory json, string memory key) internal pure returns (uint256) { bytes memory valueBytes = vm.parseJson(json, key); @@ -213,12 +226,18 @@ contract MilkmanTest is Test { // priceCheckers["COW"] = fixedMinOut; // priceCheckers["ALCX"] = validFrom; - priceCheckerDatas["TOKE"] = bytes("0"); - priceCheckerDatas["USDC"] = dynamicSlippagePriceCheckerData(10, bytes("")); // up to $10 lost allowed - priceCheckerDatas["GUSD"] = dynamicSlippagePriceCheckerData(100, bytes("")); // up to $100 lost allowed + priceCheckerDatas["TOKE"] = curveExpectedOutData(); + priceCheckerDatas["USDC"] = dynamicSlippagePriceCheckerData(10, curveExpectedOutData()); // up to $10 lost allowed + priceCheckerDatas["GUSD"] = dynamicSlippagePriceCheckerData(100, curveExpectedOutData()); // up to $100 lost allowed + address[] memory priceFeeds = new address[](1); + priceFeeds[0] = 0x6Df09E975c830ECae5bd4eD9d90f3A95a4f88012; + bool[] memory reverses = new bool[](1); + reverses[0] = false; + priceCheckerDatas["AAVE"] = dynamicSlippagePriceCheckerData(1000, + chainlinkExpectedOutData(priceFeeds, reverses)); // tokensToSell = ["TOKE", "USDC", "GUSD", "AAVE", "BAT", "WETH", "UNI", "ALCX", "BAL", "YFI", "USDT", "COW"]; - tokensToSell = ["TOKE", "GUSD", "USDC"]; + tokensToSell = ["TOKE", "GUSD", "USDC", "AAVE"]; } function testRequestSwapExactTokensForTokens() public { From 713ea406a0ab0e0981d4b17e3de31e60d2d312e3 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Sat, 24 Feb 2024 14:57:41 -0700 Subject: [PATCH 47/54] test: add BAT, WETH, and UNI --- test/Milkman.t.sol | 48 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/test/Milkman.t.sol b/test/Milkman.t.sol index 5a82752..3da695b 100644 --- a/test/Milkman.t.sol +++ b/test/Milkman.t.sol @@ -81,10 +81,18 @@ contract MilkmanTest is Test { return ZERO_BYTES; } + function ssbWethExpectedOutData() internal pure returns (bytes memory) { + return ZERO_BYTES; + } + function chainlinkExpectedOutData(address[] memory priceFeeds, bool[] memory reverses) internal pure returns (bytes memory) { return abi.encode(priceFeeds, reverses); } + function univ3ExpectedOutData(address[] memory swapPath, uint24[] memory poolFees) internal pure returns (bytes memory) { + return abi.encode(swapPath, poolFees); + } + function parseUint(string memory json, string memory key) internal pure returns (uint256) { bytes memory valueBytes = vm.parseJson(json, key); string memory valueString = abi.decode(valueBytes, (string)); @@ -187,7 +195,7 @@ contract MilkmanTest is Test { amounts["USDC"] = 5000000; // 5,000,000 USDC amounts["GUSD"] = 10_000; // 10,000 GUSD amounts["AAVE"] = 2500; // 2,500 AAVE - amounts["BAT"] = 280000; // 280,000 BAT + amounts["BAT"] = 28000; // 28,000 BAT amounts["WETH"] = 325; // 325 WETH amounts["UNI"] = 80000; // 80,000 UNI amounts["ALCX"] = 4000; // 4,000 ALCX @@ -230,14 +238,38 @@ contract MilkmanTest is Test { priceCheckerDatas["USDC"] = dynamicSlippagePriceCheckerData(10, curveExpectedOutData()); // up to $10 lost allowed priceCheckerDatas["GUSD"] = dynamicSlippagePriceCheckerData(100, curveExpectedOutData()); // up to $100 lost allowed - address[] memory priceFeeds = new address[](1); - priceFeeds[0] = 0x6Df09E975c830ECae5bd4eD9d90f3A95a4f88012; - bool[] memory reverses = new bool[](1); - reverses[0] = false; + address[] memory aavePriceFeeds = new address[](1); + aavePriceFeeds[0] = 0x6Df09E975c830ECae5bd4eD9d90f3A95a4f88012; + bool[] memory aaveReverses = new bool[](1); + aaveReverses[0] = false; priceCheckerDatas["AAVE"] = dynamicSlippagePriceCheckerData(1000, - chainlinkExpectedOutData(priceFeeds, reverses)); + chainlinkExpectedOutData(aavePriceFeeds, aaveReverses)); + + address[] memory batPriceFeeds = new address[](2); + batPriceFeeds[0] = 0x0d16d4528239e9ee52fa531af613AcdB23D88c94; + batPriceFeeds[1] = 0x194a9AaF2e0b67c35915cD01101585A33Fe25CAa; + bool[] memory batReverses = new bool[](2); + batReverses[0] = false; + batReverses[1] = true; + priceCheckerDatas["BAT"] = dynamicSlippagePriceCheckerData(600, + chainlinkExpectedOutData(batPriceFeeds, batReverses)); + + priceCheckerDatas["WETH"] = dynamicSlippagePriceCheckerData(200, ssbWethExpectedOutData()); + + address[] memory uniSwapPath = new address[](4); + uniSwapPath[0] = tokenAddress["UNI"]; + uniSwapPath[1] = tokenAddress["WETH"]; + uniSwapPath[2] = tokenAddress["USDC"]; + uniSwapPath[3] = tokenAddress["USDT"]; + uint24[] memory uniPoolFees = new uint24[](3); + uniPoolFees[0] = 30; + uniPoolFees[1] = 5; + uniPoolFees[2] = 1; + priceCheckerDatas["UNI"] = dynamicSlippagePriceCheckerData(1000, + univ3ExpectedOutData(uniSwapPath, uniPoolFees)); + // tokensToSell = ["TOKE", "USDC", "GUSD", "AAVE", "BAT", "WETH", "UNI", "ALCX", "BAL", "YFI", "USDT", "COW"]; - tokensToSell = ["TOKE", "GUSD", "USDC", "AAVE"]; + tokensToSell = ["TOKE", "GUSD", "USDC", "AAVE", "BAT", "WETH", "UNI"]; } function testRequestSwapExactTokensForTokens() public { @@ -254,6 +286,7 @@ contract MilkmanTest is Test { priceCheckerData = priceCheckerDatas[tokenToSell]; } + vm.prank(whale); fromToken.approve(address(milkman), amountIn); @@ -329,6 +362,7 @@ contract MilkmanTest is Test { ) ); + uint32 validTo = uint32(block.timestamp) + 60 * 60 * 24; GPv2Order.Data memory order = GPv2Order.Data({ From 4367b074945074d6dfb1fb5434a520f7228b6a3b Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Sat, 24 Feb 2024 15:34:56 -0700 Subject: [PATCH 48/54] Add BAL and YFI --- test/Milkman.t.sol | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/test/Milkman.t.sol b/test/Milkman.t.sol index 3da695b..aca82e1 100644 --- a/test/Milkman.t.sol +++ b/test/Milkman.t.sol @@ -173,7 +173,7 @@ contract MilkmanTest is Test { tokenAddress["ALCX"] = 0xdBdb4d16EdA451D0503b854CF79D55697F90c8DF; tokenAddress["WBTC"] = 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599; tokenAddress["UNI"] = 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984; - tokenAddress["BAL"] = 0xdBdb4d16EdA451D0503b854CF79D55697F90c8DF; + tokenAddress["BAL"] = 0xba100000625a3754423978a60c9317c58a424e3D; tokenAddress["BAL/WETH"] = 0x5c6Ee304399DBdB9C8Ef030aB642B10820DB8F56; tokenAddress["YFI"] = 0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e; tokenAddress["COW"] = 0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB; @@ -265,11 +265,20 @@ contract MilkmanTest is Test { uniPoolFees[0] = 30; uniPoolFees[1] = 5; uniPoolFees[2] = 1; - priceCheckerDatas["UNI"] = dynamicSlippagePriceCheckerData(1000, + priceCheckerDatas["UNI"] = dynamicSlippagePriceCheckerData(500, univ3ExpectedOutData(uniSwapPath, uniPoolFees)); + priceCheckerDatas["BAL"] = dynamicSlippagePriceCheckerData(50, ssbWethExpectedOutData()); + + address[] memory yfiPriceFeeds = new address[](1); + yfiPriceFeeds[0] = 0xA027702dbb89fbd58938e4324ac03B58d812b0E1; + bool[] memory yfiReverses = new bool[](1); + yfiReverses[0] = false; + priceCheckerDatas["YFI"] = dynamicSlippagePriceCheckerData(400, + chainlinkExpectedOutData(yfiPriceFeeds, yfiReverses)); + // tokensToSell = ["TOKE", "USDC", "GUSD", "AAVE", "BAT", "WETH", "UNI", "ALCX", "BAL", "YFI", "USDT", "COW"]; - tokensToSell = ["TOKE", "GUSD", "USDC", "AAVE", "BAT", "WETH", "UNI"]; + tokensToSell = ["TOKE", "GUSD", "USDC", "AAVE", "BAT", "WETH", "UNI", "BAL", "YFI"]; } function testRequestSwapExactTokensForTokens() public { From 2cc6faf196db37ac886af63acab943884062c860 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Sat, 24 Feb 2024 15:52:11 -0700 Subject: [PATCH 49/54] test: add USDT --- test/Milkman.t.sol | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/test/Milkman.t.sol b/test/Milkman.t.sol index aca82e1..487eed1 100644 --- a/test/Milkman.t.sol +++ b/test/Milkman.t.sol @@ -22,6 +22,7 @@ import {GPv2Order} from "@cow-protocol/contracts/libraries/GPv2Order.sol"; import {IERC20 as CoWIERC20} from "@cow-protocol/contracts/interfaces/IERC20.sol"; // import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeMath} from "@openzeppelin/contracts/math/SafeMath.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; interface IERC20Metadata { function decimals() external view returns (uint8); @@ -32,6 +33,7 @@ contract MilkmanTest is Test { using stdJson for string; using GPv2Order for GPv2Order.Data; using SafeMath for uint256; + using SafeERC20 for IERC20; Milkman milkman; IERC20 fromToken; @@ -205,8 +207,8 @@ contract MilkmanTest is Test { amounts["COW"] = 900000; // 900,000 COW whaleAddresses["GUSD"] = 0x5f65f7b609678448494De4C87521CdF6cEf1e932; - // whaleAddresses["USDT"] = 0xa929022c9107643515f5c777ce9a910f0d1e490c; - // whaleAddresses["WETH"] = 0x030ba81f1c18d280636f32af80b9aad02cf0854e; + whaleAddresses["USDT"] = 0x5754284f345afc66a98fbB0a0Afe71e0F007B949; + whaleAddresses["WETH"] = 0x030bA81f1c18d280636F32af80b9AAd02Cf0854e; // whaleAddresses["WBTC"] = 0xccf4429db6322d5c611ee964527d42e5d685dd6a; whaleAddresses["DAI"] = 0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643; whaleAddresses["USDC"] = 0x0A59649758aa4d66E25f08Dd01271e891fe52199; @@ -277,8 +279,17 @@ contract MilkmanTest is Test { priceCheckerDatas["YFI"] = dynamicSlippagePriceCheckerData(400, chainlinkExpectedOutData(yfiPriceFeeds, yfiReverses)); + address[] memory usdtPriceFeeds = new address[](2); + usdtPriceFeeds[0] = 0xEe9F2375b4bdF6387aa8265dD4FB8F16512A1d46; + usdtPriceFeeds[1] = 0xD6aA3D25116d8dA79Ea0246c4826EB951872e02e; + bool[] memory usdtReverses = new bool[](2); + usdtReverses[0] = false; + usdtReverses[1] = true; + priceCheckerDatas["USDT"] = dynamicSlippagePriceCheckerData(600, + chainlinkExpectedOutData(usdtPriceFeeds, usdtReverses)); + // tokensToSell = ["TOKE", "USDC", "GUSD", "AAVE", "BAT", "WETH", "UNI", "ALCX", "BAL", "YFI", "USDT", "COW"]; - tokensToSell = ["TOKE", "GUSD", "USDC", "AAVE", "BAT", "WETH", "UNI", "BAL", "YFI"]; + tokensToSell = ["TOKE", "GUSD", "USDC", "AAVE", "BAT", "WETH", "UNI", "BAL", "YFI", "USDT"]; } function testRequestSwapExactTokensForTokens() public { @@ -295,9 +306,10 @@ contract MilkmanTest is Test { priceCheckerData = priceCheckerDatas[tokenToSell]; } - vm.prank(whale); - fromToken.approve(address(milkman), amountIn); + fromToken.safeApprove(address(milkman), amountIn); + + continue; vm.recordLogs(); From 5e6981fec7b1dc8c31263e45079e7adc9cc80cf8 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Sat, 24 Feb 2024 16:13:46 -0700 Subject: [PATCH 50/54] test: add COW --- test/Milkman.t.sol | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/test/Milkman.t.sol b/test/Milkman.t.sol index 487eed1..01c5c7d 100644 --- a/test/Milkman.t.sol +++ b/test/Milkman.t.sol @@ -17,6 +17,7 @@ import {SingleSidedBalancerBalWethExpectedOutCalculator} from import "../src/pricecheckers/MetaExpectedOutCalculator.sol"; import "../src/pricecheckers/FixedSlippageChecker.sol"; import "../src/pricecheckers/DynamicSlippageChecker.sol"; +import "../src/pricecheckers/FixedMinOutPriceChecker.sol"; import {IPriceChecker} from "../interfaces/IPriceChecker.sol"; import {GPv2Order} from "@cow-protocol/contracts/libraries/GPv2Order.sol"; import {IERC20 as CoWIERC20} from "@cow-protocol/contracts/interfaces/IERC20.sol"; @@ -55,6 +56,7 @@ contract MilkmanTest is Test { address univ3PriceChecker; address metaPriceChecker; address ssbBalWethPriceChecker; + address fixedMinOutPriceChecker; bytes32 public constant APP_DATA = 0x2B8694ED30082129598720860E8E972F07AA10D9B81CAE16CA0E2CFB24743E24; bytes4 internal constant MAGIC_VALUE = 0x1626ba7e; @@ -105,6 +107,10 @@ contract MilkmanTest is Test { return abi.encode(allowedSlippageBips, expectedOutData); } + function fixedMinOutPriceCheckerData(uint256 minOut) internal pure returns (bytes memory) { + return abi.encode(minOut); + } + function setUp() public { milkman = new Milkman(); chainlinkExpectedOutCalculator = address(new ChainlinkExpectedOutCalculator()); @@ -164,6 +170,8 @@ contract MilkmanTest is Test { ) ); + fixedMinOutPriceChecker = address(new FixedMinOutPriceChecker()); + tokenAddress["TOKE"] = 0x2e9d63788249371f1DFC918a52f8d799F4a38C94; tokenAddress["DAI"] = 0x6B175474E89094C44Da98b954EedeAC495271d0F; tokenAddress["USDC"] = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; @@ -221,7 +229,7 @@ contract MilkmanTest is Test { whaleAddresses["ALCX"] = 0x000000000000000000000000000000000000dEaD; whaleAddresses["BAL"] = 0x10A19e7eE7d7F8a52822f6817de8ea18204F2e4f; whaleAddresses["YFI"] = 0xFEB4acf3df3cDEA7399794D0869ef76A6EfAff52; - // whaleAddresses["COW"] = 0xca771eda0c70aa7d053ab1b25004559b918fe662; + whaleAddresses["COW"] = 0xcA771eda0c70aA7d053aB1B25004559B918FE662; priceCheckers["TOKE"] = sushiswapPriceChecker; priceCheckers["USDC"] = curvePriceChecker; @@ -233,7 +241,7 @@ contract MilkmanTest is Test { priceCheckers["UNI"] = univ3PriceChecker; priceCheckers["BAL"] = ssbBalWethPriceChecker; priceCheckers["WETH"] = ssbBalWethPriceChecker; - // priceCheckers["COW"] = fixedMinOut; + priceCheckers["COW"] = fixedMinOutPriceChecker; // priceCheckers["ALCX"] = validFrom; priceCheckerDatas["TOKE"] = curveExpectedOutData(); @@ -285,11 +293,13 @@ contract MilkmanTest is Test { bool[] memory usdtReverses = new bool[](2); usdtReverses[0] = false; usdtReverses[1] = true; - priceCheckerDatas["USDT"] = dynamicSlippagePriceCheckerData(600, + priceCheckerDatas["USDT"] = dynamicSlippagePriceCheckerData(1000, chainlinkExpectedOutData(usdtPriceFeeds, usdtReverses)); + priceCheckerDatas["COW"] = fixedMinOutPriceCheckerData(100_000 * 1e18); + // tokensToSell = ["TOKE", "USDC", "GUSD", "AAVE", "BAT", "WETH", "UNI", "ALCX", "BAL", "YFI", "USDT", "COW"]; - tokensToSell = ["TOKE", "GUSD", "USDC", "AAVE", "BAT", "WETH", "UNI", "BAL", "YFI", "USDT"]; + tokensToSell = ["TOKE", "GUSD", "USDC", "AAVE", "BAT", "WETH", "UNI", "BAL", "YFI", "USDT", "COW"]; } function testRequestSwapExactTokensForTokens() public { @@ -306,10 +316,9 @@ contract MilkmanTest is Test { priceCheckerData = priceCheckerDatas[tokenToSell]; } - vm.prank(whale); + vm.startPrank(whale); fromToken.safeApprove(address(milkman), amountIn); - - continue; + vm.stopPrank(); vm.recordLogs(); @@ -322,7 +331,7 @@ contract MilkmanTest is Test { priceChecker, priceCheckerData ); - + Vm.Log[] memory entries = vm.getRecordedLogs(); address orderContract = address(0); @@ -421,7 +430,7 @@ contract MilkmanTest is Test { // check that price checker returns false with bad price - uint256 badAmountOut = (buyAmount * 4) / 5; + uint256 badAmountOut = buyAmount / 10; assertFalse( IPriceChecker(priceChecker).checkPrice( From 5d7814f6578f7fbc17b410859174ee6f954eab7d Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Sat, 24 Feb 2024 16:34:16 -0700 Subject: [PATCH 51/54] test: add ALCX --- test/Milkman.t.sol | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/test/Milkman.t.sol b/test/Milkman.t.sol index 01c5c7d..279cda0 100644 --- a/test/Milkman.t.sol +++ b/test/Milkman.t.sol @@ -97,6 +97,10 @@ contract MilkmanTest is Test { return abi.encode(swapPath, poolFees); } + function metaExpectedOutData(address[] memory swapPath, address[] memory expectedOutCalculators, bytes[] memory expectedOutCalculatorData) internal pure returns (bytes memory) { + return abi.encode(swapPath, expectedOutCalculators, expectedOutCalculatorData); + } + function parseUint(string memory json, string memory key) internal pure returns (uint256) { bytes memory valueBytes = vm.parseJson(json, key); string memory valueString = abi.decode(valueBytes, (string)); @@ -242,7 +246,7 @@ contract MilkmanTest is Test { priceCheckers["BAL"] = ssbBalWethPriceChecker; priceCheckers["WETH"] = ssbBalWethPriceChecker; priceCheckers["COW"] = fixedMinOutPriceChecker; - // priceCheckers["ALCX"] = validFrom; + priceCheckers["ALCX"] = metaPriceChecker; priceCheckerDatas["TOKE"] = curveExpectedOutData(); priceCheckerDatas["USDC"] = dynamicSlippagePriceCheckerData(10, curveExpectedOutData()); // up to $10 lost allowed @@ -298,8 +302,29 @@ contract MilkmanTest is Test { priceCheckerDatas["COW"] = fixedMinOutPriceCheckerData(100_000 * 1e18); - // tokensToSell = ["TOKE", "USDC", "GUSD", "AAVE", "BAT", "WETH", "UNI", "ALCX", "BAL", "YFI", "USDT", "COW"]; - tokensToSell = ["TOKE", "GUSD", "USDC", "AAVE", "BAT", "WETH", "UNI", "BAL", "YFI", "USDT", "COW"]; + bytes[] memory expectedOutDatas = new bytes[](2); + + address[] memory alcxPriceFeeds = new address[](1); + alcxPriceFeeds[0] = 0x194a9AaF2e0b67c35915cD01101585A33Fe25CAa; + bool[] memory alcxReverses = new bool[](1); + alcxReverses[0] = false; + expectedOutDatas[0] = chainlinkExpectedOutData(alcxPriceFeeds, alcxReverses); + expectedOutDatas[1] = univ2ExpectedOutData(); + + address[] memory alcxSwapPath = new address[](3); + alcxSwapPath[0] = tokenAddress["ALCX"]; + alcxSwapPath[1] = tokenAddress["WETH"]; + alcxSwapPath[2] = tokenAddress["TOKE"]; + + address[] memory alcxExpectedOutCalculators = new address[](2); + alcxExpectedOutCalculators[0] = chainlinkExpectedOutCalculator; + alcxExpectedOutCalculators[1] = sushiswapExpectedOutCalculator; + + priceCheckerDatas["ALCX"] = dynamicSlippagePriceCheckerData(600, + metaExpectedOutData(alcxSwapPath, alcxExpectedOutCalculators, expectedOutDatas) + ); + + tokensToSell = ["TOKE", "GUSD", "USDC", "AAVE", "BAT", "WETH", "UNI", "BAL", "YFI", "USDT", "COW", "ALCX"]; } function testRequestSwapExactTokensForTokens() public { @@ -503,6 +528,4 @@ contract MilkmanTest is Test { order.buyTokenBalance = GPv2Order.BALANCE_ERC20; } } - - // Additional test cases for different scenarios and edge cases } From 603504818e1fd874eae93196e189ddeb5fee651f Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Sat, 24 Feb 2024 16:38:47 -0700 Subject: [PATCH 52/54] feat: remove everything Brownie-related --- brownie-config.yml | 26 - contracts/Milkman.sol | 253 ------- contracts/periphery/GasChecker.sol | 20 - contracts/periphery/HashHelper.sol | 18 - contracts/periphery/MilkmanStateHelper.sol | 52 -- .../ChainlinkExpectedOutCalculator.sol | 118 --- .../CurveExpectedOutCalculator.sol | 97 --- .../pricecheckers/DynamicSlippageChecker.sol | 52 -- .../FixedMaxFeePriceCheckerDecorator.sol | 50 -- .../pricecheckers/FixedMinOutPriceChecker.sol | 20 - .../pricecheckers/FixedSlippageChecker.sol | 55 -- .../pricecheckers/IExpectedOutCalculator.sol | 12 - .../MetaExpectedOutCalculator.sol | 63 -- contracts/pricecheckers/PRICE_CHECKERS.md | 18 - contracts/pricecheckers/PriceCheckerLib.sol | 41 - ...edBalancerBalWethExpectedOutCalculator.sol | 149 ---- .../UniV2ExpectedOutCalculator.sol | 50 -- .../UniV3ExpectedOutCalculator.sol | 73 -- .../ValidFromPriceCheckerDecorator.sol | 35 - requirements-dev.txt | 4 - tests/conftest.py | 484 ------------ tests/test_cancel.py | 167 ----- tests/test_complete.py | 705 ------------------ tests/test_request.py | 131 ---- tests/utils.py | 341 --------- 25 files changed, 3034 deletions(-) delete mode 100644 brownie-config.yml delete mode 100644 contracts/Milkman.sol delete mode 100644 contracts/periphery/GasChecker.sol delete mode 100644 contracts/periphery/HashHelper.sol delete mode 100644 contracts/periphery/MilkmanStateHelper.sol delete mode 100644 contracts/pricecheckers/ChainlinkExpectedOutCalculator.sol delete mode 100644 contracts/pricecheckers/CurveExpectedOutCalculator.sol delete mode 100644 contracts/pricecheckers/DynamicSlippageChecker.sol delete mode 100644 contracts/pricecheckers/FixedMaxFeePriceCheckerDecorator.sol delete mode 100644 contracts/pricecheckers/FixedMinOutPriceChecker.sol delete mode 100644 contracts/pricecheckers/FixedSlippageChecker.sol delete mode 100644 contracts/pricecheckers/IExpectedOutCalculator.sol delete mode 100644 contracts/pricecheckers/MetaExpectedOutCalculator.sol delete mode 100644 contracts/pricecheckers/PRICE_CHECKERS.md delete mode 100644 contracts/pricecheckers/PriceCheckerLib.sol delete mode 100644 contracts/pricecheckers/SingleSidedBalancerBalWethExpectedOutCalculator.sol delete mode 100644 contracts/pricecheckers/UniV2ExpectedOutCalculator.sol delete mode 100644 contracts/pricecheckers/UniV3ExpectedOutCalculator.sol delete mode 100644 contracts/pricecheckers/ValidFromPriceCheckerDecorator.sol delete mode 100644 requirements-dev.txt delete mode 100644 tests/conftest.py delete mode 100644 tests/test_cancel.py delete mode 100644 tests/test_complete.py delete mode 100644 tests/test_request.py delete mode 100644 tests/utils.py diff --git a/brownie-config.yml b/brownie-config.yml deleted file mode 100644 index 661fd0e..0000000 --- a/brownie-config.yml +++ /dev/null @@ -1,26 +0,0 @@ -# use Ganache's forked mainnet mode as the default network -# NOTE: You don't *have* to do this, but it is often helpful for testing -networks: - default: mainnet-fork - -# automatically fetch contract sources from Etherscan -autofetch_sources: True - -# require OpenZepplin Contracts -dependencies: - - OpenZeppelin/openzeppelin-contracts@3.4.0-solc-0.7 - - cowprotocol/contracts@1.3.1 - - balancer/balancer-v2-monorepo@weighted-deployment - -# path remapping to support imports from GitHub/NPM -compiler: - solc: - version: 0.7.6 - remappings: - - "@openzeppelin=OpenZeppelin/openzeppelin-contracts@3.4.0-solc-0.7" - - "@cow-protocol=cowprotocol/contracts@1.3.1/src" - - "@balancer=balancer/balancer-v2-monorepo@weighted-deployment/contracts" - -reports: - exclude_contracts: - - SafeMath diff --git a/contracts/Milkman.sol b/contracts/Milkman.sol deleted file mode 100644 index d178882..0000000 --- a/contracts/Milkman.sol +++ /dev/null @@ -1,253 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later -pragma solidity ^0.7.6; -pragma abicoder v2; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {IPriceChecker} from "../interfaces/IPriceChecker.sol"; - -import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; -import {SafeMath} from "@openzeppelin/contracts/math/SafeMath.sol"; -import {GPv2Order} from "@cow-protocol/contracts/libraries/GPv2Order.sol"; - -/// @title Milkman -/// @author @charlesndalton -/// @notice A layer on top of the CoW Protocol that allows smart contracts (DAOs, Gnosis Safes, protocols, etc.) to submit swaps. Swaps are MEV-protected. Use with atypical tokens (e.g., rebasing tokens) not recommended. -/// @dev For each requested swap, Milkman creates a clone of itself, and moves `amountIn` of `fromToken` into the clone. The clone pre-approves the amount to the CoW settlement contract. The clone also stores a hash of the swap's variables, something like hash({amountIn: 1000, fromToken: USDC, toToken: DAI, etc.}). Then, an off-chain server creates a CoW order on behalf of the clone. Before this CoW order can be 'settled' (before amountIn can be pulled out of the clone), the clone runs checks on the order. These checks include calling a user-provided `priceChecker`, which could for example check SushiSwap to see if what they could get out of SushiSwap was at least 90% of the order's `minOut`. -contract Milkman { - using SafeERC20 for IERC20; - using GPv2Order for GPv2Order.Data; - using SafeMath for uint256; - - event SwapRequested( - address orderContract, - address orderCreator, - uint256 amountIn, - address fromToken, - address toToken, - address to, - address priceChecker, - bytes priceCheckerData - ); - - /// @dev The contract Milkman needs to give allowance. - address internal constant VAULT_RELAYER = - 0xC92E8bdf79f0507f65a392b0ab4667716BFE0110; - /// @dev The settlement contract's EIP-712 domain separator. Milkman uses this to verify that a provided UID matches provided order parameters. - bytes32 public constant DOMAIN_SEPARATOR = - 0xc078f884a2676e1345748b1feace7b0abee5d00ecadb6e574dcdd109a63e8943; - bytes4 internal constant MAGIC_VALUE = 0x1626ba7e; - bytes4 internal constant NON_MAGIC_VALUE = 0xffffffff; - bytes32 internal constant ROOT_MILKMAN_SWAP_HASH = - 0xca11ab1efacade00000000000000000000000000000000000000000000000000; - - /// @dev the Milkman deployed by an EOA, in contrast to Milkman 'order contracts' deployed in invocations of requestSwapExactTokensForTokens - address internal immutable ROOT_MILKMAN; - - /// @dev Hash of the order data, hashed like so: - /// kekkak256(abi.encode(orderCreator, receiver, fromToken, toToken, amountIn, priceChecker, priceCheckerData)). - /// In the root contract, it's set to `ROOT_MILKMAN_SWAP_HASH`. - bytes32 public swapHash = ROOT_MILKMAN_SWAP_HASH; - - constructor() { - ROOT_MILKMAN = address(this); - } - - /// @notice Asynchronously swap an exact amount of tokenIn for a market-determined amount of tokenOut. - /// @dev Swaps are usually completed in ~2 minutes. - /// @param amountIn The number of tokens to sell. - /// @param fromToken The token that the user wishes to sell. - /// @param toToken The token that the user wishes to receive. - /// @param to Who should receive the tokens. - /// @param priceChecker A contract that verifies an order (mainly its minOut and fee) before Milkman signs it. - /// @param priceCheckerData Data that gets passed to the price checker. - function requestSwapExactTokensForTokens( - uint256 amountIn, - IERC20 fromToken, - IERC20 toToken, - address to, - address priceChecker, - bytes calldata priceCheckerData - ) external { - // require(address(this) == ROOT_MILKMAN, "!root_milkman"); // can't call `requestSwapExactTokensForTokens` from order contracts - // require(priceChecker != address(0), "!price_checker"); // need to supply a valid price checker - - return; - - // address orderContract = createOrderContract(); - - // fromToken.safeTransferFrom(msg.sender, orderContract, amountIn); - - // bytes32 _swapHash = keccak256( - // abi.encode( - // msg.sender, - // to, - // fromToken, - // toToken, - // amountIn, - // priceChecker, - // priceCheckerData - // ) - // ); - - // Milkman(orderContract).initialize(fromToken, _swapHash); - - // emit SwapRequested( - // orderContract, - // msg.sender, - // amountIn, - // address(fromToken), - // address(toToken), - // to, - // priceChecker, - // priceCheckerData - // ); - } - - function initialize(IERC20 fromToken, bytes32 _swapHash) external { - require(swapHash == bytes32(0) && _swapHash != bytes32(0), "!reinit"); // also prevents root contract from being initialized - swapHash = _swapHash; - - fromToken.safeApprove(VAULT_RELAYER, type(uint256).max); - } - - /// @notice Cancel a requested swap, sending the tokens back to the order creator. - /// @dev `msg.sender` must be the original order creator. The other parameters are required to verify that this is the case (kind of like a merkle proof). - function cancelSwap( - uint256 amountIn, - IERC20 fromToken, - IERC20 toToken, - address to, - address priceChecker, - bytes calldata priceCheckerData - ) external { - bytes32 _storedSwapHash = swapHash; - - require(_storedSwapHash != ROOT_MILKMAN_SWAP_HASH, "!cancel_from_root"); - - bytes32 _calculatedSwapHash = keccak256( - abi.encode( - msg.sender, - to, - fromToken, - toToken, - amountIn, - priceChecker, - priceCheckerData - ) - ); - - require(_storedSwapHash == _calculatedSwapHash, "!valid_creator_proof"); - - fromToken.safeTransfer(msg.sender, amountIn); - } - - /// @param orderDigest The EIP-712 signing digest derived from the order - /// @param encodedOrder Bytes-encoded order information, originally created by an off-chain bot. Created by concatening the order data (in the form of GPv2Order.Data), the price checker address, and price checker data. - function isValidSignature(bytes32 orderDigest, bytes calldata encodedOrder) - external - view - returns (bytes4) - { - bytes32 _storedSwapHash = swapHash; - - require( - _storedSwapHash != ROOT_MILKMAN_SWAP_HASH, - "!is_valid_sig_from_root" - ); - - ( - GPv2Order.Data memory _order, - address _orderCreator, - address _priceChecker, - bytes memory _priceCheckerData - ) = decodeOrder(encodedOrder); - - require(_order.hash(DOMAIN_SEPARATOR) == orderDigest, "!match"); - - require(_order.kind == GPv2Order.KIND_SELL, "!kind_sell"); - - require( - _order.validTo >= block.timestamp + 5 minutes, - "expires_too_soon" - ); - - require(!_order.partiallyFillable, "!fill_or_kill"); - - require( - _order.sellTokenBalance == GPv2Order.BALANCE_ERC20, - "!sell_erc20" - ); - - require( - _order.buyTokenBalance == GPv2Order.BALANCE_ERC20, - "!buy_erc20" - ); - - require( - IPriceChecker(_priceChecker).checkPrice( - _order.sellAmount.add(_order.feeAmount), - address(_order.sellToken), - address(_order.buyToken), - _order.feeAmount, - _order.buyAmount, - _priceCheckerData - ), - "invalid_min_out" - ); - - bytes32 _calculatedSwapHash = keccak256( - abi.encode( - _orderCreator, - _order.receiver, - _order.sellToken, - _order.buyToken, - _order.sellAmount.add(_order.feeAmount), - _priceChecker, - _priceCheckerData - ) - ); - - if (_calculatedSwapHash == _storedSwapHash) { - // should be true as long as the keeper isn't submitting bad orders - return MAGIC_VALUE; - } else { - return NON_MAGIC_VALUE; - } - } - - function decodeOrder(bytes calldata _encodedOrder) - internal - pure - returns ( - GPv2Order.Data memory _order, - address _orderCreator, - address _priceChecker, - bytes memory _priceCheckerData - ) - { - (_order, _orderCreator, _priceChecker, _priceCheckerData) = abi.decode( - _encodedOrder, - (GPv2Order.Data, address, address, bytes) - ); - } - - function createOrderContract() internal returns (address _orderContract) { - // Copied from https://github.com/optionality/clone-factory/blob/master/contracts/CloneFactory.sol - - bytes20 addressBytes = bytes20(address(this)); - assembly { - // EIP-1167 bytecode - let clone_code := mload(0x40) - mstore( - clone_code, - 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000 - ) - mstore(add(clone_code, 0x14), addressBytes) - mstore( - add(clone_code, 0x28), - 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000 - ) - _orderContract := create(0, clone_code, 0x37) - } - } -} diff --git a/contracts/periphery/GasChecker.sol b/contracts/periphery/GasChecker.sol deleted file mode 100644 index 0b7ae9d..0000000 --- a/contracts/periphery/GasChecker.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later -pragma solidity ^0.7.6; -pragma abicoder v2; - -interface EIP1271 { - function isValidSignature(bytes32, bytes calldata) - external - returns (bytes4); -} - -/// Check that `isValidSignature` doesn't take up too much gas -contract GasChecker { - function isValidSignatureCheck( - address milkman, - bytes32 orderDigest, - bytes calldata encodedOrder - ) external returns (bytes4) { - return EIP1271(milkman).isValidSignature(orderDigest, encodedOrder); - } -} diff --git a/contracts/periphery/HashHelper.sol b/contracts/periphery/HashHelper.sol deleted file mode 100644 index f0d5e8e..0000000 --- a/contracts/periphery/HashHelper.sol +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later -pragma solidity ^0.7.6; -pragma abicoder v2; - -import {GPv2Order} from "@cow-protocol/contracts/libraries/GPv2Order.sol"; - -contract HashHelper { - using GPv2Order for GPv2Order.Data; - using GPv2Order for bytes; - - function hash(GPv2Order.Data memory order, bytes32 domainSeparator) - external - pure - returns (bytes32 orderDigest) - { - return order.hash(domainSeparator); - } -} diff --git a/contracts/periphery/MilkmanStateHelper.sol b/contracts/periphery/MilkmanStateHelper.sol deleted file mode 100644 index e2cf435..0000000 --- a/contracts/periphery/MilkmanStateHelper.sol +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later -pragma solidity ^0.7.6; -pragma abicoder v2; - -import {IGPv2Settlement} from "../../interfaces/IGPv2Settlement.sol"; - -interface IMilkman { - function swaps(bytes32 _swapID) external view returns (bytes memory); -} - -/// @title Milkman State Helper -/// @dev Helper contract that can be used by off-chain bots to fetch the state of a Milkman swap. -contract MilkmanStateHelper { - enum SwapState { - NULL, - REQUESTED, - PAIRED, - PAIRED_AND_UNPAIRABLE, - PAIRED_AND_EXECUTED - } - - IMilkman public constant milkman = - IMilkman(0x3E40B8c9FcBf02a26Ff1c5d88f525AEd00755575); - - IGPv2Settlement internal constant settlement = - IGPv2Settlement(0x9008D19f58AAbD9eD0D60971565AA8510560ab41); - - function getState(bytes32 _swapID) external view returns (SwapState) { - bytes memory _swapData = milkman.swaps(_swapID); - - if (_swapData.length == 0) { - return SwapState.NULL; - } else if ( - _swapData.length == 32 && _swapData[31] == bytes1(uint8(1)) - ) { - return SwapState.REQUESTED; - } - - (uint256 _blockNumberWhenPaired, bytes memory _orderUid) = abi.decode( - _swapData, - (uint256, bytes) - ); - - if (settlement.filledAmount(_orderUid) != 0) { - return SwapState.PAIRED_AND_EXECUTED; - } else if (block.number >= _blockNumberWhenPaired + 50) { - return SwapState.PAIRED_AND_UNPAIRABLE; - } else { - return SwapState.PAIRED; - } - } -} diff --git a/contracts/pricecheckers/ChainlinkExpectedOutCalculator.sol b/contracts/pricecheckers/ChainlinkExpectedOutCalculator.sol deleted file mode 100644 index b8a4203..0000000 --- a/contracts/pricecheckers/ChainlinkExpectedOutCalculator.sol +++ /dev/null @@ -1,118 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later -pragma solidity ^0.7.6; -pragma abicoder v2; - -import "@openzeppelin/contracts/math/SafeMath.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import {IExpectedOutCalculator} from "./IExpectedOutCalculator.sol"; - -interface IPriceFeed { - function latestAnswer() external view returns (int256); - - function decimals() external view returns (uint8); -} - -interface IERC20MetaData { - function decimals() external view returns (uint8); -} - -/** - * @notice Checks a swap against Chainlink-compatible price feeds. - * @dev Doesn't care about how long ago the price feed answer was. Another expected out calculator can be built if this is desired. - */ -contract ChainlinkExpectedOutCalculator is IExpectedOutCalculator { - using SafeMath for uint256; - - uint256 internal constant MAX_BPS = 10_000; - - /** - * @param _data Encoded [priceFeeds, reverses]. - * - * priceFeeds (address[]): The price feeds to route through. For example, if the user is swapping BAT -> ALCX, this would contain the BAT/ETH and ALCX/ETH price feeds. - * reverses (bool[]): For each price feed, whether or not it should be reversed. For example, if a user was swapping USDC for XYZ, they would pass in the XYZ/USD price feed and set reversed[0] to true. - * - * Some examples: - * BOND -> ETH: [bond-eth.data.eth], [false]] - * ETH -> BOND: [[bond-eth.data.eth], [true]] - * BOND -> DAI: [bond-eth.data.eth, eth-usd.data.eth], [false, false]] - * alternative: [both-eth.data.eth, eth-usd.data.eth, dai-usd.data.eth], [false, false, true]] - * BOND -> YFI: [[bond-eth.data.eth, yfi-eth.data.eth], [false, true]] - * BOND -> FXS (FXS has no fxs-eth feed): [[bond-eth.data.eth, eth-usd.data.eth, fxs-usd.data.eth], [false, false, true]] - */ - function getExpectedOut( - uint256 _amountIn, - address _fromToken, - address _toToken, - bytes calldata _data - ) external view override returns (uint256) { - (address[] memory _priceFeeds, bool[] memory _reverses) = abi.decode( - _data, - (address[], bool[]) - ); - - return - getExpectedOutFromChainlink( - _priceFeeds, - _reverses, - _amountIn, - _fromToken, - _toToken - ); // how much Chainlink says we'd get out of this trade - } - - function getExpectedOutFromChainlink( - address[] memory _priceFeeds, - bool[] memory _reverses, - uint256 _amountIn, - address _fromToken, - address _toToken - ) internal view returns (uint256 _expectedOutFromChainlink) { - uint256 _priceFeedsLen = _priceFeeds.length; - - require(_priceFeedsLen > 0); // dev: need to pass at least one price feed - require(_priceFeedsLen == _reverses.length); // dev: price feeds and reverse need to have the same length - - for (uint256 _i = 0; _i < _priceFeedsLen; _i++) { - IPriceFeed _priceFeed = IPriceFeed(_priceFeeds[_i]); - - int256 _latestAnswer = _priceFeed.latestAnswer(); - { - require(_latestAnswer > 0); // dev: latest answer from the price feed needs to be positive - } - - uint256 _scaleAnswerBy = 10**uint256(_priceFeed.decimals()); - - // If it's first iteration, use amountIn to calculate. Else, use the result from the previous iteration. - uint256 _amountIntoThisIteration = _i == 0 - ? _amountIn - : _expectedOutFromChainlink; - - // Without a reverse, we multiply amount * price - // With a reverse, we divide amount / price - _expectedOutFromChainlink = _reverses[_i] - ? _amountIntoThisIteration.mul(_scaleAnswerBy).div( - uint256(_latestAnswer) - ) - : _amountIntoThisIteration.mul(uint256(_latestAnswer)).div( - _scaleAnswerBy - ); - } - - uint256 _fromTokenDecimals = uint256( - IERC20MetaData(_fromToken).decimals() - ); - uint256 _toTokenDecimals = uint256(IERC20MetaData(_toToken).decimals()); - - if (_fromTokenDecimals > _toTokenDecimals) { - // if fromToken has more decimals than toToken, we need to divide - _expectedOutFromChainlink = _expectedOutFromChainlink.div( - 10**_fromTokenDecimals.sub(_toTokenDecimals) - ); - } else if (_fromTokenDecimals < _toTokenDecimals) { - _expectedOutFromChainlink = _expectedOutFromChainlink.mul( - 10**_toTokenDecimals.sub(_fromTokenDecimals) - ); - } - } -} diff --git a/contracts/pricecheckers/CurveExpectedOutCalculator.sol b/contracts/pricecheckers/CurveExpectedOutCalculator.sol deleted file mode 100644 index 1381a70..0000000 --- a/contracts/pricecheckers/CurveExpectedOutCalculator.sol +++ /dev/null @@ -1,97 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later -pragma solidity ^0.7.6; -pragma abicoder v2; - -import "@openzeppelin/contracts/math/SafeMath.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import {IExpectedOutCalculator} from "./IExpectedOutCalculator.sol"; - -interface IAddressProvider { - function get_registry() external view returns (address); -} - -interface IRegistry { - function find_pool_for_coins(address _from, address _to) - external - view - returns (address); - - function get_underlying_coins(address _pool) - external - view - returns (address[8] memory); -} - -interface ICurvePool { - function get_dy_underlying( - int128 i, - int128 j, - uint256 dx - ) external view returns (uint256); -} - -contract CurveExpectedOutCalculator is IExpectedOutCalculator { - using SafeMath for uint256; - - IAddressProvider internal constant ADDRESS_PROVIDER = - IAddressProvider(0x0000000022D53366457F9d5E68Ec105046FC4383); - IRegistry public registry; - - uint256 internal constant MAX_BPS = 10_000; - - constructor() { - updateRegistry(); - } - - // anyone can call this - function updateRegistry() public { - registry = IRegistry(ADDRESS_PROVIDER.get_registry()); - } - - /** - * @dev This expected out calculator can only be used for Curve pools that use `int128` - * for `i` and `j`, which contains most but not all pools. - * - * A separate calculator can be deployed for the pools that use `uint256`. - */ - function getExpectedOut( - uint256 _amountIn, - address _fromToken, - address _toToken, - bytes calldata - ) external view override returns (uint256) { - address _pool = registry.find_pool_for_coins(_fromToken, _toToken); - require(_pool != address(0)); // dev: no Curve pool for this swap - - return _getExpectedOut(_pool, _fromToken, _toToken, _amountIn); - } - - function _getExpectedOut( - address _pool, - address _fromToken, - address _toToken, - uint256 _amountIn - ) internal view returns (uint256) { - int128 _i; - int128 _j; - { - // block scoping to prevent stack too deep - address[8] memory _tokensInPool = registry.get_underlying_coins( - _pool - ); - for (int128 _x = 0; _x < 8; _x++) { - address _currentToken = _tokensInPool[uint256(_x)]; - if (_currentToken == address(0)) { - break; - } else if (_currentToken == _fromToken) { - _i = _x; - } else if (_currentToken == _toToken) { - _j = _x; - } - } - require(_i != 0 || _j != 0); // dev: something went wrong - } - return ICurvePool(_pool).get_dy_underlying(_i, _j, _amountIn); - } -} diff --git a/contracts/pricecheckers/DynamicSlippageChecker.sol b/contracts/pricecheckers/DynamicSlippageChecker.sol deleted file mode 100644 index abdf685..0000000 --- a/contracts/pricecheckers/DynamicSlippageChecker.sol +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later -pragma solidity ^0.7.6; -pragma abicoder v2; - -import "@openzeppelin/contracts/math/SafeMath.sol"; - -import {IPriceChecker} from "../../interfaces/IPriceChecker.sol"; -import {IExpectedOutCalculator} from "./IExpectedOutCalculator.sol"; - -// Like the `FixedSlippageChecker` except the user can pass in their desired -// allowed slippage % dynamically in the _data field. The rest of the _data -// is passed to the price checker. -contract DynamicSlippageChecker is IPriceChecker { - using SafeMath for uint256; - - string public NAME; - IExpectedOutCalculator public immutable EXPECTED_OUT_CALCULATOR; - - uint256 internal constant MAX_BPS = 10_000; - - constructor(string memory _name, address _expectedOutCalculator) { - NAME = _name; - EXPECTED_OUT_CALCULATOR = IExpectedOutCalculator( - _expectedOutCalculator - ); - } - - function checkPrice( - uint256 _amountIn, - address _fromToken, - address _toToken, - uint256, - uint256 _minOut, - bytes calldata _data - ) external view override returns (bool) { - (uint256 _allowedSlippageInBps, bytes memory _data) = abi.decode( - _data, - (uint256, bytes) - ); - - uint256 _expectedOut = EXPECTED_OUT_CALCULATOR.getExpectedOut( - _amountIn, - _fromToken, - _toToken, - _data - ); - - return - _minOut > - _expectedOut.mul(MAX_BPS.sub(_allowedSlippageInBps)).div(MAX_BPS); - } -} diff --git a/contracts/pricecheckers/FixedMaxFeePriceCheckerDecorator.sol b/contracts/pricecheckers/FixedMaxFeePriceCheckerDecorator.sol deleted file mode 100644 index b73e2b9..0000000 --- a/contracts/pricecheckers/FixedMaxFeePriceCheckerDecorator.sol +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later -pragma solidity ^0.7.6; -pragma abicoder v2; - -import "@openzeppelin/contracts/math/SafeMath.sol"; - -import {IPriceChecker} from "../../interfaces/IPriceChecker.sol"; -import {IExpectedOutCalculator} from "./IExpectedOutCalculator.sol"; - -/// Specify a maximum allowed fee, denominated in `fromToken`. -/// This decorates an existing price checker to allow for composability. -contract FixedMaxFeePriceCheckerDecorator is IPriceChecker { - using SafeMath for uint256; - - string public NAME; - IPriceChecker public immutable PRICE_CHECKER; - - constructor(string memory _name, address _expectedOutCalculator) { - NAME = _name; - PRICE_CHECKER = IPriceChecker(_expectedOutCalculator); - } - - function checkPrice( - uint256 _amountIn, - address _fromToken, - address _toToken, - uint256 _feeAmount, - uint256 _minOut, - bytes calldata _data - ) external view override returns (bool) { - (uint256 _allowedFeeAmount, bytes memory _data) = abi.decode( - _data, - (uint256, bytes) - ); - - if (_feeAmount > _allowedFeeAmount) { - return false; - } - - return - PRICE_CHECKER.checkPrice( - _amountIn, - _fromToken, - _toToken, - _feeAmount, - _minOut, - _data - ); - } -} diff --git a/contracts/pricecheckers/FixedMinOutPriceChecker.sol b/contracts/pricecheckers/FixedMinOutPriceChecker.sol deleted file mode 100644 index e110e44..0000000 --- a/contracts/pricecheckers/FixedMinOutPriceChecker.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later -pragma solidity ^0.7.6; -pragma abicoder v2; - -import {IPriceChecker} from "../../interfaces/IPriceChecker.sol"; - -/// Specify a minimimum allowed out amount, denominated in `buyToken` you are willing to accept. -contract FixedMinOutPriceChecker is IPriceChecker { - function checkPrice( - uint256 _amountIn, - address _fromToken, - address _toToken, - uint256 _feeAmount, - uint256 _out, - bytes calldata _data - ) external view override returns (bool) { - uint256 minOut = abi.decode(_data, (uint256)); - return minOut <= _out; - } -} diff --git a/contracts/pricecheckers/FixedSlippageChecker.sol b/contracts/pricecheckers/FixedSlippageChecker.sol deleted file mode 100644 index 1aab8eb..0000000 --- a/contracts/pricecheckers/FixedSlippageChecker.sol +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later -pragma solidity ^0.7.6; -pragma abicoder v2; - -import "@openzeppelin/contracts/math/SafeMath.sol"; - -import {IPriceChecker} from "../../interfaces/IPriceChecker.sol"; -import {IExpectedOutCalculator} from "./IExpectedOutCalculator.sol"; - -// Very basic slippage checker that checks that minOut is at least 100 - x% of -// expected out, where x is set at deployment time. E.g., could check that minOut -// is at least 90% of expected out. -contract FixedSlippageChecker is IPriceChecker { - using SafeMath for uint256; - - string public NAME; - uint256 public immutable ALLOWED_SLIPPAGE_IN_BPS; - IExpectedOutCalculator public immutable EXPECTED_OUT_CALCULATOR; - - uint256 internal constant MAX_BPS = 10_000; - - constructor( - string memory _name, - uint256 _allowedSlippageInBps, - address _expectedOutCalculator - ) { - require(_allowedSlippageInBps <= MAX_BPS); - - NAME = _name; - ALLOWED_SLIPPAGE_IN_BPS = _allowedSlippageInBps; - EXPECTED_OUT_CALCULATOR = IExpectedOutCalculator( - _expectedOutCalculator - ); - } - - function checkPrice( - uint256 _amountIn, - address _fromToken, - address _toToken, - uint256, - uint256 _minOut, - bytes calldata _data - ) external view override returns (bool) { - uint256 _expectedOut = EXPECTED_OUT_CALCULATOR.getExpectedOut( - _amountIn, - _fromToken, - _toToken, - _data - ); - - return - _minOut > - _expectedOut.mul(MAX_BPS.sub(ALLOWED_SLIPPAGE_IN_BPS)).div(MAX_BPS); - } -} diff --git a/contracts/pricecheckers/IExpectedOutCalculator.sol b/contracts/pricecheckers/IExpectedOutCalculator.sol deleted file mode 100644 index 3900d44..0000000 --- a/contracts/pricecheckers/IExpectedOutCalculator.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity ^0.7.6; -pragma abicoder v2; - -interface IExpectedOutCalculator { - function getExpectedOut( - uint256 _amountIn, - address _fromToken, - address _toToken, - bytes calldata _data - ) external view returns (uint256); -} diff --git a/contracts/pricecheckers/MetaExpectedOutCalculator.sol b/contracts/pricecheckers/MetaExpectedOutCalculator.sol deleted file mode 100644 index 6541b55..0000000 --- a/contracts/pricecheckers/MetaExpectedOutCalculator.sol +++ /dev/null @@ -1,63 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later -pragma solidity ^0.7.6; -pragma abicoder v2; - -import "@openzeppelin/contracts/math/SafeMath.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import {IExpectedOutCalculator} from "./IExpectedOutCalculator.sol"; - -/** - * @notice Uses multiple other expected out calculators to generate an expected out. - */ -contract MetaExpectedOutCalculator is IExpectedOutCalculator { - using SafeMath for uint256; - - /** - * @param _data Encoded [swapPath, priceCheckers, priceCheckerData]. - * - * swapPath (address[]): List of ERC20s to swap through. - * expectedOutCalculators (address[]): List of expected out calculators to use. - * expectedOutCalculatorData (bytes[]): List of bytes to pass to each expected out calculator - */ - function getExpectedOut( - uint256 _amountIn, - address _fromToken, - address _toToken, - bytes calldata _data - ) external view override returns (uint256) { - ( - address[] memory _swapPath, - address[] memory _expectedOutCalculators, - bytes[] memory _expectedOutCalculatorData - ) = abi.decode(_data, (address[], address[], bytes[])); - - require( - _swapPath.length.sub(1) == _expectedOutCalculators.length && - _expectedOutCalculators.length == - _expectedOutCalculatorData.length, - "invalid_length" - ); - - uint256 _runningExpectedOut; - for (uint256 i = 0; i < _swapPath.length.sub(1); i++) { - _runningExpectedOut = i == 0 - ? IExpectedOutCalculator(_expectedOutCalculators[i]) - .getExpectedOut( - _amountIn, - _swapPath[i], - _swapPath[i + 1], - _expectedOutCalculatorData[i] - ) - : IExpectedOutCalculator(_expectedOutCalculators[i]) - .getExpectedOut( - _runningExpectedOut, - _swapPath[i], - _swapPath[i + 1], - _expectedOutCalculatorData[i] - ); - } - - return _runningExpectedOut; - } -} diff --git a/contracts/pricecheckers/PRICE_CHECKERS.md b/contracts/pricecheckers/PRICE_CHECKERS.md deleted file mode 100644 index b74532d..0000000 --- a/contracts/pricecheckers/PRICE_CHECKERS.md +++ /dev/null @@ -1,18 +0,0 @@ -## Price Checker Internals - -Price checking can be split into two stages: one to determine an 'expected out' -and another to determine how far away the minOut can be from that expected out. -For example, for a TOKE -> WETH swap, we could determine the expected out by -calling getAmountsOut on the TOKE/WETH SushiSwap pool and then fulfill the second -stage by only returning true if minOut is at least 90% of what getAmountsOut returned. - -To make things modular, we split these two steps across contracts: a slippage -checker and an expected out calculator (creative names, amirite?). The slippage -checker implements the IPriceChecker interface and calls the expected out calculator. -This allows the two to independently vary. In other words, you can write your -own expected out calculator (e.g., for UniV4) without needing to write any -slippage checking logic. - -Of course, if you write your own price checker, you don't need to conform to this -pattern. If you wanted, you could have a monolithic price checker or even split -price checkers into more contracts. diff --git a/contracts/pricecheckers/PriceCheckerLib.sol b/contracts/pricecheckers/PriceCheckerLib.sol deleted file mode 100644 index e204657..0000000 --- a/contracts/pricecheckers/PriceCheckerLib.sol +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later -pragma solidity ^0.7.6; -pragma abicoder v2; - -import "@openzeppelin/contracts/math/SafeMath.sol"; - -library PriceCheckerLib { - using SafeMath for uint256; - - uint256 internal constant MAX_BPS = 10_000; - - function getMaxSlippage( - uint256 _inputMaxSlippage, - uint256 _defaultMaxSlippage - ) internal pure returns (uint256) { - require(_inputMaxSlippage <= 10_000); // dev: max slippage too high - - if (_inputMaxSlippage == 0) { - return _defaultMaxSlippage; - } else { - return _inputMaxSlippage; - } - } - - /// @dev performs a double-ended slippage check, ensuring that minOut is both greater than market value - max slippage and less than market value + max slippage. - function isMinOutAcceptable( - uint256 _minOut, - uint256 _marketValueOfAmountIn, - uint256 _maxSlippageInBips - ) internal pure returns (bool) { - return - _minOut > - _marketValueOfAmountIn.mul(MAX_BPS.sub(_maxSlippageInBips)).div( - MAX_BPS - ) && - _minOut < - _marketValueOfAmountIn.mul(MAX_BPS.add(_maxSlippageInBips)).div( - MAX_BPS - ); - } -} diff --git a/contracts/pricecheckers/SingleSidedBalancerBalWethExpectedOutCalculator.sol b/contracts/pricecheckers/SingleSidedBalancerBalWethExpectedOutCalculator.sol deleted file mode 100644 index ae4cf51..0000000 --- a/contracts/pricecheckers/SingleSidedBalancerBalWethExpectedOutCalculator.sol +++ /dev/null @@ -1,149 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later -pragma solidity ^0.7.6; -pragma abicoder v2; - -import "@openzeppelin/contracts/math/SafeMath.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@balancer/lib/math/FixedPoint.sol"; - -import {IExpectedOutCalculator} from "./IExpectedOutCalculator.sol"; - -interface IPriceFeed { - function latestAnswer() external view returns (int256); - - function decimals() external view returns (uint8); -} - -interface IWeightedPool { - function getInvariant() external view returns (uint256); - - function getVault() external view returns (address); - - function totalSupply() external view returns (uint256); -} - -interface IVault { - struct UserBalanceOp { - UserBalanceOpKind kind; - address asset; - uint256 amount; - address sender; - address payable recipient; - } - - function manageUserBalance(UserBalanceOp[] memory ops) external payable; - - enum UserBalanceOpKind { - DEPOSIT_INTERNAL, - WITHDRAW_INTERNAL, - TRANSFER_INTERNAL, - TRANSFER_EXTERNAL - } -} - -library VaultReentrancyLib { - function ensureNotInVaultContext(IVault vault) internal view { - bytes32 REENTRANCY_ERROR_HASH = keccak256( - abi.encodeWithSignature("Error(string)", "BAL#400") - ); - - // read-only re-entrancy protection - this call is always unsuccessful but we need to make sure - // it didn't fail due to a re-entrancy attack - (, bytes memory revertData) = address(vault).staticcall{gas: 10_000}( - abi.encodeWithSelector( - vault.manageUserBalance.selector, - new address[](0) - ) - ); - - require(keccak256(revertData) != REENTRANCY_ERROR_HASH); - } -} - -contract SingleSidedBalancerBalWethExpectedOutCalculator is - IExpectedOutCalculator -{ - using SafeMath for uint256; - using FixedPoint for uint256; - using VaultReentrancyLib for IVault; - - address public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; - address public constant BAL = 0xba100000625a3754423978a60c9317c58a424e3D; - IWeightedPool public constant BAL_WETH_POOL = - IWeightedPool(0x5c6Ee304399DBdB9C8Ef030aB642B10820DB8F56); - IVault internal constant BALANCER_VAULT = - IVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8); - IPriceFeed internal constant BAL_ETH_FEED = - IPriceFeed(0xC1438AA3823A6Ba0C159CfA8D98dF5A994bA120b); - - uint256 internal constant ZERO_POINT_EIGHT = 8e17; - uint256 internal constant ZERO_POINT_TWO = 2e17; - uint256 internal constant TEN = 1e19; - - function getExpectedOut( - uint256 _amountIn, - address _fromToken, - address _toToken, - bytes calldata - ) external view override returns (uint256) { - require(_toToken == address(BAL_WETH_POOL)); - require(_fromToken == WETH || _fromToken == BAL); - - BALANCER_VAULT.ensureNotInVaultContext(); - - uint256 kOverS = BAL_WETH_POOL.getInvariant().mul(1e18).div( - BAL_WETH_POOL.totalSupply() - ); - - if (_fromToken == WETH) { - int256 ethPriceOfBal = BAL_ETH_FEED.latestAnswer(); - require(ethPriceOfBal > 0); - - uint256 balFactor = uint256(ethPriceOfBal) - .mul(1e18) - .div(ZERO_POINT_EIGHT) - .powUp(ZERO_POINT_EIGHT); - uint256 ethFactor = FixedPoint - .ONE - .mul(1e18) - .div(ZERO_POINT_TWO) - .powUp(ZERO_POINT_TWO); - - // what a BPT is worth in ETH - uint256 ethValueOfBPT = kOverS - .mul(balFactor) - .div(1e18) - .mul(ethFactor) - .div(1e18); - - return _amountIn.mul(1e18).div(ethValueOfBPT); - } else { - // how many bal per eth? - int256 ethPriceOfBal = BAL_ETH_FEED.latestAnswer(); - require(ethPriceOfBal > 0); - - uint256 balPriceOfEth = FixedPoint.ONE.mul(1e18).div( - uint256(ethPriceOfBal) - ); - - uint256 balFactor = FixedPoint - .ONE - .mul(1e18) - .div(ZERO_POINT_EIGHT) - .powUp(ZERO_POINT_EIGHT); - uint256 ethFactor = balPriceOfEth - .mul(1e18) - .div(ZERO_POINT_TWO) - .powUp(ZERO_POINT_TWO); - - // what a BPT is worth in BAL - uint256 balValueOfBPT = kOverS - .mul(balFactor) - .div(1e18) - .mul(ethFactor) - .div(1e18); - - return _amountIn.mul(1e18).div(balValueOfBPT); - } - } -} diff --git a/contracts/pricecheckers/UniV2ExpectedOutCalculator.sol b/contracts/pricecheckers/UniV2ExpectedOutCalculator.sol deleted file mode 100644 index cf2bf6f..0000000 --- a/contracts/pricecheckers/UniV2ExpectedOutCalculator.sol +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later -pragma solidity ^0.7.6; -pragma abicoder v2; - -import "@openzeppelin/contracts/math/SafeMath.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import {IExpectedOutCalculator} from "./IExpectedOutCalculator.sol"; -import "../../interfaces/IUniV2.sol"; - -contract UniV2ExpectedOutCalculator is IExpectedOutCalculator { - using SafeMath for uint256; - - address internal constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; - - string public NAME; - address public immutable UNIV2_ROUTER; - - constructor(string memory _name, address _univ2Router) { - NAME = _name; - UNIV2_ROUTER = _univ2Router; - } - - function getExpectedOut( - uint256 _amountIn, - address _fromToken, - address _toToken, - bytes calldata - ) external view override returns (uint256) { - uint256[] memory amounts; - - if (_fromToken == WETH || _toToken == WETH) { - address[] memory path = new address[](2); - - path[0] = _fromToken; - path[1] = _toToken; - - amounts = IUniV2(UNIV2_ROUTER).getAmountsOut(_amountIn, path); - } else { - address[] memory path = new address[](3); - path[0] = _fromToken; // token to swap - path[1] = WETH; // weth - path[2] = _toToken; - - amounts = IUniV2(UNIV2_ROUTER).getAmountsOut(_amountIn, path); - } - - return amounts[amounts.length - 1]; - } -} diff --git a/contracts/pricecheckers/UniV3ExpectedOutCalculator.sol b/contracts/pricecheckers/UniV3ExpectedOutCalculator.sol deleted file mode 100644 index 83c14eb..0000000 --- a/contracts/pricecheckers/UniV3ExpectedOutCalculator.sol +++ /dev/null @@ -1,73 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later -pragma solidity ^0.7.6; -pragma abicoder v2; - -import "@openzeppelin/contracts/math/SafeMath.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import {IExpectedOutCalculator} from "./IExpectedOutCalculator.sol"; - -interface IUniswapV3StaticQuoter { - /// @notice Returns the amount out received for a given exact input swap without executing the swap - /// @param path The path of the swap, i.e. each token pair and the pool fee - /// @param amountIn The amount of the first token to swap - /// @return amountOut The amount of the last token that would be received - function quoteExactInput(bytes memory path, uint256 amountIn) - external - view - returns (uint256 amountOut); -} - -contract UniV3ExpectedOutCalculator is IExpectedOutCalculator { - using SafeMath for uint256; - - IUniswapV3StaticQuoter internal constant QUOTER = - IUniswapV3StaticQuoter(0x7637Aaeb5BD58269B782726680d83f72C651aE74); - - /** - * @param _data Encoded [swapPath, poolFees]. - * - * swapPath (address[]): List of ERC20s to swap through. - * poolFees (uint24[]): Pool fee for the pool to swap through, denominated in bips. - * - * Some examples: - * AAVE -> DAI: [[address(AAVE), address(WETH), address(DAI)], [30, 5]] - * USDT -> USDC: [[address(USDT), address(USDC)], [1]] - */ - function getExpectedOut( - uint256 _amountIn, - address _fromToken, - address _toToken, - bytes calldata _data - ) external view override returns (uint256) { - (address[] memory _swapPath, uint24[] memory _poolFees) = abi.decode( - _data, - (address[], uint24[]) - ); - - return _getExpectedOut(_amountIn, _swapPath, _poolFees); - } - - function _getExpectedOut( - uint256 _amountIn, - address[] memory _swapPath, - uint24[] memory _poolFees - ) internal view returns (uint256) { - require(_swapPath.length >= 2); // dev: must have at least two assets in swap path - require(_poolFees.length.add(1) == _swapPath.length); // dev: must be one more asset in swap path than pool fee - - // path is packed bytes with the form [asset0, poolFee0, asset1, poolFee1, asset2] - bytes memory _path = abi.encodePacked(_swapPath[0]); - - for (uint256 _i = 0; _i < _poolFees.length; _i++) { - // they ingest fees in 1 100ths of a bip, so we multiply by 100 - _path = abi.encodePacked( - _path, - uint24(uint256(_poolFees[_i]).mul(100)), - _swapPath[_i.add(1)] - ); - } - - return QUOTER.quoteExactInput(_path, _amountIn); - } -} diff --git a/contracts/pricecheckers/ValidFromPriceCheckerDecorator.sol b/contracts/pricecheckers/ValidFromPriceCheckerDecorator.sol deleted file mode 100644 index 3a911c1..0000000 --- a/contracts/pricecheckers/ValidFromPriceCheckerDecorator.sol +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later -pragma solidity ^0.7.6; -pragma abicoder v2; - -import {IPriceChecker} from "../../interfaces/IPriceChecker.sol"; - -/// Specify a validFrom timestamp, which the current block has to have surpassed in order for the order to be valid. -/// This decorates an existing price checker to allow for composability. -contract ValidFromPriceCheckerDecorator is IPriceChecker { - function checkPrice( - uint256 _amountIn, - address _fromToken, - address _toToken, - uint256 _feeAmount, - uint256 _minOut, - bytes calldata _data - ) external view override returns (bool) { - (uint256 _validFrom, address _priceChecker, bytes memory _data) = abi - .decode(_data, (uint256, address, bytes)); - - if (_validFrom > block.timestamp) { - return false; - } - - return - IPriceChecker(_priceChecker).checkPrice( - _amountIn, - _fromToken, - _toToken, - _feeAmount, - _minOut, - _data - ); - } -} diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index bb1955b..0000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,4 +0,0 @@ -black==21.7b0 -eth-brownie>=1.16.0,<2.0.0 -eth_abi==2.1.1 -milkman-py==0.1.1 diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index bac0827..0000000 --- a/tests/conftest.py +++ /dev/null @@ -1,484 +0,0 @@ -from lib2to3.pgen2 import token -from brownie import Contract -from eth_abi import encode_abi - -# from milkman_py import ( -# univ2_expected_out_data, -# univ3_expected_out_data, -# curve_expected_out_data, -# chainlink_expected_out_data, -# meta_expected_out_data, -# fixed_slippage_price_checker_data, -# dynamic_slippage_price_checker_data, -# ) -import pytest -import utils - -EMPTY_BYTES = encode_abi(["uint8"], [int(0)]) - - -def univ2_expected_out_data(): - return EMPTY_BYTES - - -def curve_expected_out_data(): - return EMPTY_BYTES - - -def chainlink_expected_out_data(price_feed_addresses, reverses): - return encode_abi( - ["address[]", "bool[]"], - [ - price_feed_addresses, - reverses, - ], - ) - - -def univ3_expected_out_data(swap_path, pool_fees): - return encode_abi(["address[]", "uint24[]"], [swap_path, pool_fees]) - - -def meta_expected_out_data(swap_path, expected_out_calculators, expected_out_data): - return encode_abi( - ["address[]", "address[]", "bytes[]"], - [ - swap_path, - expected_out_calculators, - expected_out_data, - ], - ) - - -def dynamic_slippage_price_checker_data(allowed_slippage_bips, expected_out_data): - return encode_abi( - ["uint256", "bytes"], [int(allowed_slippage_bips), expected_out_data] - ) - - -def fixed_slippage_price_checker_data(expected_out_data): - return expected_out_data - - -@pytest.fixture(scope="function", autouse=True) -def shared_setup(fn_isolation): - pass - - -@pytest.fixture(scope="session", autouse=True) -def user(accounts): - yield accounts[0] - - -@pytest.fixture(scope="session", autouse=True) -def deployer(accounts): - yield accounts[1] - - -# test the following paths: -# TOKE -> DAI, $75k, Sushiswap price checker -# USDC -> USDT, $5M, Curve price checker -# GUSD -> USDC, $1k, Curve price checker -# AAVE -> WETH, $250k, Chainlink price checker -# BAT -> ALCX, $100k, Chainlink price checker -# YFI -> USDC, $20k, Chainlink price checker -# USDT -> UNI, $2M, Chainlink price checker -# WETH -> WETH/BAL, $650k SSB price checker -# UNI -> USDT, $500k & Uniswap as the price checker -# ALCX -> TOKE, $100k, Meta price checker with Chainlink and Sushiswap -# BAL -> WETH/BAL, $2M, SSB price checker -# COW -> DAI, $50k, min fixed out price checker -token_address = { - "TOKE": "0x2e9d63788249371f1DFC918a52f8d799F4a38C94", - "DAI": "0x6b175474e89094c44da98b954eedeac495271d0f", - "USDC": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", - "USDT": "0xdAC17F958D2ee523a2206206994597C13D831ec7", - "GUSD": "0x056Fd409E1d7A124BD7017459dFEa2F387b6d5Cd", - "AAVE": "0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9", - "WETH": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", - "BAT": "0x0D8775F648430679A709E98d2b0Cb6250d2887EF", - "ALCX": "0xdBdb4d16EdA451D0503b854CF79D55697F90c8DF", - "WBTC": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", - "UNI": "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984", - "ALCX": "0xdBdb4d16EdA451D0503b854CF79D55697F90c8DF", - "BAL": "0xba100000625a3754423978a60c9317c58a424e3D", - "BAL/WETH": "0x5c6Ee304399DBdB9C8Ef030aB642B10820DB8F56", - "YFI": "0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e", - "USDT": "0xdAC17F958D2ee523a2206206994597C13D831ec7", - "COW": "0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB", -} - -sell_to_buy_map = { - "TOKE": "DAI", - "USDC": "USDT", - "GUSD": "USDC", - "AAVE": "WETH", - "BAT": "ALCX", - "WETH": "BAL/WETH", - "UNI": "USDT", - "ALCX": "TOKE", - "BAL": "BAL/WETH", - "YFI": "USDC", - "USDT": "UNI", - "COW": "DAI", -} - - -@pytest.fixture( - params=[ - "TOKE", - "USDC", - "GUSD", - "AAVE", - "BAT", - "WETH", - "UNI", - "ALCX", - "BAL", - "YFI", - "USDT", - "COW", - ], - scope="session", - autouse=True, -) -def token_to_sell(request): - yield Contract(token_address[request.param]) - - -@pytest.fixture(scope="session", autouse=True) -def token_to_buy(token_to_sell): - yield Contract(token_address[sell_to_buy_map[token_to_sell.symbol()]]) - - -amounts = { - "TOKE": 80_000, - "USDC": 5_000_000, - "GUSD": 1_000, - "AAVE": 2_500, - "BAT": 280_000, - "WETH": 325, - "UNI": 80_000, - "ALCX": 4_000, - "BAL": 300_000, - "YFI": 3, - "USDT": 2_000_000, - "COW": 900_000, -} - - -@pytest.fixture(scope="function", autouse=True) -def amount(token_to_sell, user, whale): - amount = int(amounts[token_to_sell.symbol()] * 10 ** token_to_sell.decimals()) - token_to_sell.transfer(user, amount, {"from": whale}) - - yield int(amount) - - -whale_address = { - "GUSD": "0x5f65f7b609678448494De4C87521CdF6cEf1e932", - "USDT": "0xa929022c9107643515f5c777ce9a910f0d1e490c", - "WETH": "0x030ba81f1c18d280636f32af80b9aad02cf0854e", - "WBTC": "0xccf4429db6322d5c611ee964527d42e5d685dd6a", - "DAI": "0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643", - "USDC": "0x0A59649758aa4d66E25f08Dd01271e891fe52199", - "LINK": "0x98C63b7B319dFBDF3d811530F2ab9DfE4983Af9D", - "GNO": "0x4f8AD938eBA0CD19155a835f617317a6E788c868", - "TOKE": "0x96F98Ed74639689C3A11daf38ef86E59F43417D3", - "AAVE": "0x4da27a545c0c5B758a6BA100e3a049001de870f5", - "BAT": "0x6C8c6b02E7b2BE14d4fA6022Dfd6d75921D90E4E", - "UNI": "0x1a9C8182C09F50C8318d769245beA52c32BE35BC", - "ALCX": "0x000000000000000000000000000000000000dEaD", - "BAL": "0x10A19e7eE7d7F8a52822f6817de8ea18204F2e4f", - "YFI": "0xFEB4acf3df3cDEA7399794D0869ef76A6EfAff52", - "USDT": "0x40ec5B33f54e0E8A33A975908C5BA1c14e5BbbDf", - "COW": "0xca771eda0c70aa7d053ab1b25004559b918fe662", -} - - -@pytest.fixture(scope="session", autouse=True) -def whale(accounts, token_to_sell): - yield accounts.at(whale_address[token_to_sell.symbol()], force=True) - - -@pytest.fixture -def price_checker( - token_to_sell, - sushiswap_price_checker, - curve_price_checker, - chainlink_price_checker, - univ3_price_checker, - meta_price_checker, - valid_from_price_checker_decorator, - ssb_bal_weth_price_checker, - fixed_min_out_price_checker, -): - symbol = token_to_sell.symbol() - - if symbol == "TOKE": - yield sushiswap_price_checker - if symbol == "USDC" or symbol == "GUSD": - yield curve_price_checker - if symbol == "AAVE" or symbol == "BAT" or symbol == "YFI" or symbol == "USDT": - yield chainlink_price_checker - if symbol == "UNI": - yield univ3_price_checker - if symbol == "ALCX": - yield valid_from_price_checker_decorator - if symbol == "BAL" or symbol == "WETH": - yield ssb_bal_weth_price_checker - if symbol == "COW": - yield fixed_min_out_price_checker - - -@pytest.fixture -def chainlink_expected_out_calculator(ChainlinkExpectedOutCalculator, deployer): - yield deployer.deploy(ChainlinkExpectedOutCalculator) - - -@pytest.fixture -def curve_expected_out_calculator(CurveExpectedOutCalculator, deployer): - yield deployer.deploy(CurveExpectedOutCalculator) - - -@pytest.fixture -def sushiswap_expected_out_calculator(UniV2ExpectedOutCalculator, deployer): - sushi_router = "0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F" - yield deployer.deploy( - UniV2ExpectedOutCalculator, "SUSHI_EXPECTED_OUT_CALCULATOR", sushi_router - ) - - -@pytest.fixture -def ssb_bal_weth_expected_out_calculator( - SingleSidedBalancerBalWethExpectedOutCalculator, deployer -): - yield deployer.deploy(SingleSidedBalancerBalWethExpectedOutCalculator) - - -@pytest.fixture -def univ3_expected_out_calculator(UniV3ExpectedOutCalculator, deployer): - yield deployer.deploy(UniV3ExpectedOutCalculator) - - -@pytest.fixture -def meta_expected_out_calculator(MetaExpectedOutCalculator, deployer): - yield deployer.deploy(MetaExpectedOutCalculator) - - -@pytest.fixture -def chainlink_price_checker( - DynamicSlippageChecker, chainlink_expected_out_calculator, deployer -): - yield deployer.deploy( - DynamicSlippageChecker, - "CHAINLINK_DYNAMIC_SLIPPAGE_PRICE_CHECKER", - chainlink_expected_out_calculator, - ) - - -@pytest.fixture -def curve_price_checker( - DynamicSlippageChecker, curve_expected_out_calculator, deployer -): - yield deployer.deploy( - DynamicSlippageChecker, - "CURVE_DYNAMIC_SLIPPAGE_PRICE_CHECKER", - curve_expected_out_calculator, - ) - - -@pytest.fixture -def sushiswap_price_checker( - FixedSlippageChecker, sushiswap_expected_out_calculator, deployer -): - yield deployer.deploy( - FixedSlippageChecker, - "SUSHISWAP_STATIC_500_BPS_SLIPPAGE_PRICE_CHECKER", - 500, - sushiswap_expected_out_calculator, - ) # 5% slippage - - -@pytest.fixture -def univ3_price_checker( - DynamicSlippageChecker, univ3_expected_out_calculator, deployer -): - yield deployer.deploy( - DynamicSlippageChecker, - "UNIV3_DYNAMIC_SLIPPAGE_PRICE_CHECKER", - univ3_expected_out_calculator, - ) - - -@pytest.fixture -def meta_price_checker(DynamicSlippageChecker, meta_expected_out_calculator, deployer): - yield deployer.deploy( - DynamicSlippageChecker, - "META_DYNAMIC_SLIPPAGE_PRICE_CHECKER", - meta_expected_out_calculator, - ) - - -@pytest.fixture -def ssb_bal_weth_price_checker( - DynamicSlippageChecker, ssb_bal_weth_expected_out_calculator, deployer -): - yield deployer.deploy( - DynamicSlippageChecker, - "SSB_BAL_WETH_DYNAMIC_SLIPPAGE_PRICE_CHECKER", - ssb_bal_weth_expected_out_calculator, - ) - - -@pytest.fixture -def valid_from_price_checker_decorator(ValidFromPriceCheckerDecorator, deployer): - yield deployer.deploy(ValidFromPriceCheckerDecorator) - - -def valid_from_price_checker_decorator_data( - valid_from, price_checker, price_checker_data -): - return encode_abi( - ["uint256", "address", "bytes"], - [ - valid_from, - price_checker, - price_checker_data, - ], - ) - -@pytest.fixture -def fixed_min_out_price_checker(FixedMinOutPriceChecker, deployer): - yield deployer.deploy( - FixedMinOutPriceChecker - ) - -def fixed_min_out_price_checker_data(min_out): - return encode_abi( - ["uint256"], - [min_out], - ) - -# which price checker data to use for each swap -price_checker_datas = { - "TOKE": fixed_slippage_price_checker_data(univ2_expected_out_data()), - "USDC": dynamic_slippage_price_checker_data(400, curve_expected_out_data()), - "GUSD": dynamic_slippage_price_checker_data(500, curve_expected_out_data()), - "YFI": dynamic_slippage_price_checker_data( - 100, - chainlink_expected_out_data( - ["0xA027702dbb89fbd58938e4324ac03B58d812b0E1"], [False] - ), - ), - "USDT": dynamic_slippage_price_checker_data( - 500, - chainlink_expected_out_data( - [ - "0xee9f2375b4bdf6387aa8265dd4fb8f16512a1d46", - "0xd6aa3d25116d8da79ea0246c4826eb951872e02e", - ], - [False, True], - ), - ), - "AAVE": dynamic_slippage_price_checker_data( - 400, - chainlink_expected_out_data( - ["0x6Df09E975c830ECae5bd4eD9d90f3A95a4f88012"], [False] # AAVE/ETH feed - ), - ), - "BAT": dynamic_slippage_price_checker_data( - 2000, - chainlink_expected_out_data( - [ - "0x0d16d4528239e9ee52fa531af613acdb23d88c94", - "0x194a9aaf2e0b67c35915cd01101585a33fe25caa", - ], - [False, True], - ), - ), # BAT/ETH & ALCX/ETH feeds, allow 20% slippage since these are relatively illiquid - "WETH": dynamic_slippage_price_checker_data(200, utils.EMPTY_BYTES), - "UNI": dynamic_slippage_price_checker_data( - 600, - univ3_expected_out_data( - [ - token_address["UNI"], - token_address["WETH"], - token_address["USDC"], - token_address["USDT"], - ], - [int(30), int(30), int(1)], - ), - ), # 6% slippage - "BAL": dynamic_slippage_price_checker_data(50, utils.EMPTY_BYTES), # 0.5% slippage - "COW": fixed_min_out_price_checker_data(50_000), -} - - -@pytest.fixture -def price_checker_data( - chain, - token_to_sell, - meta_price_checker, - chainlink_expected_out_calculator, - sushiswap_expected_out_calculator, -): - if token_to_sell.symbol() == "ALCX": - yield valid_from_price_checker_decorator_data( - chain.time(), - meta_price_checker.address, - dynamic_slippage_price_checker_data( - 1000, - meta_expected_out_data( - [ - token_address["ALCX"], - token_address["WETH"], - token_address["TOKE"], - ], - [ - chainlink_expected_out_calculator.address, - sushiswap_expected_out_calculator.address, - ], - [ - encode_abi( - ["address[]", "bool[]"], - [ - ["0x194a9aaf2e0b67c35915cd01101585a33fe25caa"], - [False], - ], # forgive me father for this much nesting - ), - utils.EMPTY_BYTES, - ], - ), - ), - ) - else: - yield price_checker_datas[token_to_sell.symbol()] - - -@pytest.fixture -def milkman(Milkman, deployer): - milkman = deployer.deploy(Milkman) - - yield milkman - - -@pytest.fixture -def gas_checker(GasChecker, deployer): - gas_checker = deployer.deploy(GasChecker) - - yield gas_checker - - -@pytest.fixture -def hash_helper(HashHelper, deployer): - hash_helper = deployer.deploy(HashHelper) - - yield hash_helper - - -@pytest.fixture -def gnosis_settlement(): - contract_address = "0x9008D19f58AAbD9eD0D60971565AA8510560ab41" - - yield Contract(contract_address) diff --git a/tests/test_cancel.py b/tests/test_cancel.py deleted file mode 100644 index c98f220..0000000 --- a/tests/test_cancel.py +++ /dev/null @@ -1,167 +0,0 @@ -# from eth_abi import encode_abi -# from eth_utils import keccak -# import requests -# import brownie -# import utils - - -# def test_cancel( -# chain, -# milkman, -# user, -# gnosis_settlement, -# token_to_sell, -# token_to_buy, -# amount, -# price_checker, -# price_checker_data, -# ): -# token_to_sell.approve(milkman, amount, {"from": user}) - -# milkman.requestSwapExactTokensForTokens( -# amount, -# token_to_sell, -# token_to_buy, -# user, -# price_checker, -# price_checker_data, -# {"from": user}, -# ) - -# assert token_to_sell.balanceOf(user) == 0 -# # should be 0 nonce -# tx = milkman.cancelSwapRequest( -# amount, -# token_to_sell, -# token_to_buy, -# user, -# price_checker, -# price_checker_data, -# 0, -# {"from": user}, -# ) - -# assert tx.events.count("SwapCancelled") == 1 -# assert token_to_sell.balanceOf(user) == amount - - -# def test_pair_unpair_then_cancel( -# chain, -# milkman, -# user, -# gnosis_settlement, -# token_to_sell, -# token_to_buy, -# amount, -# price_checker, -# price_checker_data, -# ): -# token_to_sell.approve(milkman, amount, {"from": user}) - -# milkman.requestSwapExactTokensForTokens( -# amount, -# token_to_sell, -# token_to_buy, -# user, -# price_checker, -# price_checker_data, -# {"from": user}, -# ) - -# (order_uid, _) = utils.pair_swap( -# 0, -# chain, -# gnosis_settlement, -# milkman, -# user, -# user, -# token_to_sell, -# token_to_buy, -# amount, -# price_checker, -# price_checker_data, -# 50, -# ) - -# chain.mine(51) - -# encoded_market_order = utils.encode_market_order( -# user, -# user, -# token_to_sell, -# token_to_buy, -# amount, -# price_checker, -# price_checker_data, -# 0, -# ) -# swap_id = keccak(encoded_market_order) - -# utils.unpair_swap(gnosis_settlement, milkman, swap_id, order_uid) - -# assert token_to_sell.balanceOf(user) == 0 -# tx = milkman.cancelSwapRequest( -# amount, -# token_to_sell, -# token_to_buy, -# user, -# price_checker, -# price_checker_data, -# 0, -# {"from": user}, -# ) - -# assert tx.events.count("SwapCancelled") == 1 -# assert token_to_sell.balanceOf(user) == amount - - -# # test that we can't cancel paired trades -# def test_cant_cancel_paired_swap( -# chain, -# milkman, -# user, -# gnosis_settlement, -# token_to_sell, -# token_to_buy, -# amount, -# price_checker, -# price_checker_data, -# ): -# token_to_sell.approve(milkman, amount, {"from": user}) - -# milkman.requestSwapExactTokensForTokens( -# amount, -# token_to_sell, -# token_to_buy, -# user, -# price_checker, -# price_checker_data, -# {"from": user}, -# ) - -# utils.pair_swap( -# 0, -# chain, -# gnosis_settlement, -# milkman, -# user, -# user, -# token_to_sell, -# token_to_buy, -# amount, -# price_checker, -# price_checker_data, -# 50, -# ) - -# with brownie.reverts("!swap_requested"): -# milkman.cancelSwapRequest( -# amount, -# token_to_sell, -# token_to_buy, -# user, -# price_checker, -# price_checker_data, -# 0, -# {"from": user}, -# ) diff --git a/tests/test_complete.py b/tests/test_complete.py deleted file mode 100644 index 62e0b8d..0000000 --- a/tests/test_complete.py +++ /dev/null @@ -1,705 +0,0 @@ -from eth_abi import encode_abi -from eth_utils import keccak -import requests -from brownie import ZERO_ADDRESS, reverts, Contract -import utils -from brownie.convert import to_bytes -import brownie -import milkman_py - - -def test_complete_swap( - milkman, - user, - token_to_sell, - token_to_buy, - amount, - price_checker, - price_checker_data, - chain, - hash_helper, - gas_checker, - ChainlinkExpectedOutCalculator, -): - token_to_sell.approve(milkman, amount, {"from": user}) - - tx = milkman.requestSwapExactTokensForTokens( - int(amount), - token_to_sell, - token_to_buy, - user, - price_checker, - price_checker_data, - {"from": user}, - ) - - assert tx.events.count("SwapRequested") == 1 - - order_contract = Contract.from_abi( - "Milkman", tx.events["SwapRequested"]["orderContract"], milkman.abi - ) - - utils.check_swap_requested( - order_contract, - user, - user, - token_to_sell, - token_to_buy, - amount, - price_checker, - price_checker_data, - ) - - (fee_amount, buy_amount_after_fee) = utils.get_quote( - token_to_sell, token_to_buy, amount - ) - amount_to_sell = amount - fee_amount - - valid_to = chain.time() + 60 * 60 * 24 - - signature_encoded_order = utils.encode_order_for_is_valid_signature( - token_to_sell, - token_to_buy, - user, - user, - amount_to_sell, - buy_amount_after_fee, - valid_to, - fee_amount, - price_checker, - price_checker_data, - ) - - # we can't create orders via API, so we need to fake it - gpv2_order = ( - token_to_sell.address, - token_to_buy.address, - user.address, - amount_to_sell, - buy_amount_after_fee, - valid_to, - utils.APP_DATA, - fee_amount, - utils.KIND_SELL, - False, # fill or kill - utils.ERC20_BALANCE, - utils.ERC20_BALANCE, - ) - - order_digest = hash_helper.hash( - gpv2_order, to_bytes(utils.DOMAIN_SEPARATOR, "bytes32") - ) - - # expected_out_calculator = Contract.from_abi("", price_checker.EXPECTED_OUT_CALCULATOR(), ChainlinkExpectedOutCalculator.abi) - - # expected_out_data = milkman_py.chainlink_expected_out_data( - # ["0xA027702dbb89fbd58938e4324ac03B58d812b0E1"], [False] - # ) - - # response = expected_out_calculator.getExpectedOut(amount_to_sell, token_to_sell, token_to_buy, expected_out_data) - - is_valid_sig = order_contract.isValidSignature( - order_digest, signature_encoded_order - ) - - assert to_bytes(is_valid_sig) == to_bytes(utils.EIP_1271_MAGIC_VALUE) - - assert price_checker.checkPrice( - amount - fee_amount, - token_to_sell, - token_to_buy, - fee_amount, - buy_amount_after_fee, - price_checker_data, - ) - - tx = gas_checker.isValidSignatureCheck( - order_contract, order_digest, signature_encoded_order - ) - - assert tx.gas_used < 1_000_000 - - -def test_bad_price( - milkman, - user, - token_to_sell, - token_to_buy, - amount, - price_checker, - price_checker_data, - chain, - hash_helper, -): - token_to_sell.approve(milkman, amount, {"from": user}) - - tx = milkman.requestSwapExactTokensForTokens( - int(amount), - token_to_sell, - token_to_buy, - user, - price_checker, - price_checker_data, - {"from": user}, - ) - - assert tx.events.count("SwapRequested") == 1 - - order_contract = Contract.from_abi( - "Milkman", tx.events["SwapRequested"]["orderContract"], milkman.abi - ) - - utils.check_swap_requested( - order_contract, - user, - user, - token_to_sell, - token_to_buy, - amount, - price_checker, - price_checker_data, - ) - - (fee_amount, buy_amount_after_fee) = utils.get_quote( - token_to_sell, token_to_buy, amount - ) - - buy_amount_after_fee = int(buy_amount_after_fee * 0.8) - - valid_to = chain.time() + 60 * 60 * 24 - - signature_encoded_order = utils.encode_order_for_is_valid_signature( - token_to_sell, - token_to_buy, - user, - user, - amount - fee_amount, - buy_amount_after_fee, - valid_to, - fee_amount, - price_checker, - price_checker_data, - ) - - # we can't create orders via API, so we need to fake it - gpv2_order = ( - token_to_sell.address, - token_to_buy.address, - user.address, - amount - fee_amount, - buy_amount_after_fee, - valid_to, - utils.APP_DATA, - fee_amount, - utils.KIND_SELL, - False, # fill or kill - utils.ERC20_BALANCE, - utils.ERC20_BALANCE, - ) - - order_digest = hash_helper.hash( - gpv2_order, to_bytes(utils.DOMAIN_SEPARATOR, "bytes32") - ) - - with brownie.reverts("invalid_min_out"): - order_contract.isValidSignature(order_digest, signature_encoded_order) - - -# the keeper passes in order data that doesn't match the canonical order (what was used to generate the UID) -def test_mismatched_order( - milkman, - user, - token_to_sell, - token_to_buy, - amount, - price_checker, - price_checker_data, - chain, - hash_helper, -): - token_to_sell.approve(milkman, amount, {"from": user}) - - tx = milkman.requestSwapExactTokensForTokens( - int(amount), - token_to_sell, - token_to_buy, - user, - price_checker, - price_checker_data, - {"from": user}, - ) - - assert tx.events.count("SwapRequested") == 1 - - order_contract = Contract.from_abi( - "Milkman", tx.events["SwapRequested"]["orderContract"], milkman.abi - ) - - utils.check_swap_requested( - order_contract, - user, - user, - token_to_sell, - token_to_buy, - amount, - price_checker, - price_checker_data, - ) - - (fee_amount, buy_amount_after_fee) = utils.get_quote( - token_to_sell, token_to_buy, amount - ) - - valid_to = chain.time() + 60 * 60 * 24 - - signature_encoded_order = utils.encode_order_for_is_valid_signature( - token_to_sell, - token_to_buy, - user, - user, - amount - fee_amount, - buy_amount_after_fee, - valid_to + 10, # keeper tries manipulating valid_to - fee_amount, - price_checker, - price_checker_data, - ) - - # we can't create orders via API, so we need to fake it - gpv2_order = ( - token_to_sell.address, - token_to_buy.address, - user.address, - amount - fee_amount, - buy_amount_after_fee, - valid_to, - utils.APP_DATA, - fee_amount, - utils.KIND_SELL, - False, # fill or kill - utils.ERC20_BALANCE, - utils.ERC20_BALANCE, - ) - - order_digest = hash_helper.hash( - gpv2_order, to_bytes(utils.DOMAIN_SEPARATOR, "bytes32") - ) - - with brownie.reverts("!match"): - order_contract.isValidSignature(order_digest, signature_encoded_order) - - -# the keeper generates a buy order, which should be rejected -def test_buy_order( - milkman, - user, - token_to_sell, - token_to_buy, - amount, - price_checker, - price_checker_data, - chain, - hash_helper, -): - token_to_sell.approve(milkman, amount, {"from": user}) - - tx = milkman.requestSwapExactTokensForTokens( - int(amount), - token_to_sell, - token_to_buy, - user, - price_checker, - price_checker_data, - {"from": user}, - ) - - assert tx.events.count("SwapRequested") == 1 - - order_contract = Contract.from_abi( - "Milkman", tx.events["SwapRequested"]["orderContract"], milkman.abi - ) - - utils.check_swap_requested( - order_contract, - user, - user, - token_to_sell, - token_to_buy, - amount, - price_checker, - price_checker_data, - ) - - (fee_amount, buy_amount_after_fee) = utils.get_quote( - token_to_sell, token_to_buy, amount - ) - - valid_to = chain.time() + 60 * 60 * 24 - - signature_encoded_order = utils.encode_order_for_is_valid_signature( - token_to_sell, - token_to_buy, - user, - user, - amount - fee_amount, - buy_amount_after_fee, - valid_to, - fee_amount, - price_checker, - price_checker_data, - buy_or_sell=utils.KIND_BUY, - ) - - gpv2_order = ( - token_to_sell.address, - token_to_buy.address, - user.address, - amount - fee_amount, - buy_amount_after_fee, - valid_to, - utils.APP_DATA, - fee_amount, - utils.KIND_BUY, - False, # fill or kill - utils.ERC20_BALANCE, - utils.ERC20_BALANCE, - ) - - order_digest = hash_helper.hash( - gpv2_order, to_bytes(utils.DOMAIN_SEPARATOR, "bytes32") - ) - - with brownie.reverts("!kind_sell"): - order_contract.isValidSignature(order_digest, signature_encoded_order) - - -def test_expires_too_soon_order( - milkman, - user, - token_to_sell, - token_to_buy, - amount, - price_checker, - price_checker_data, - chain, - hash_helper, -): - token_to_sell.approve(milkman, amount, {"from": user}) - - tx = milkman.requestSwapExactTokensForTokens( - int(amount), - token_to_sell, - token_to_buy, - user, - price_checker, - price_checker_data, - {"from": user}, - ) - - assert tx.events.count("SwapRequested") == 1 - - order_contract = Contract.from_abi( - "Milkman", tx.events["SwapRequested"]["orderContract"], milkman.abi - ) - - utils.check_swap_requested( - order_contract, - user, - user, - token_to_sell, - token_to_buy, - amount, - price_checker, - price_checker_data, - ) - - (fee_amount, buy_amount_after_fee) = utils.get_quote( - token_to_sell, token_to_buy, amount - ) - - valid_to = chain.time() + 60 * 2 # only 2 minutes instead of 5 - - signature_encoded_order = utils.encode_order_for_is_valid_signature( - token_to_sell, - token_to_buy, - user, - user, - amount - fee_amount, - buy_amount_after_fee, - valid_to, - fee_amount, - price_checker, - price_checker_data, - ) - - # we can't create orders via API, so we need to fake it - gpv2_order = ( - token_to_sell.address, - token_to_buy.address, - user.address, - amount - fee_amount, - buy_amount_after_fee, - valid_to, - utils.APP_DATA, - fee_amount, - utils.KIND_SELL, - False, # fill or kill - utils.ERC20_BALANCE, - utils.ERC20_BALANCE, - ) - - order_digest = hash_helper.hash( - gpv2_order, to_bytes(utils.DOMAIN_SEPARATOR, "bytes32") - ) - - with brownie.reverts("expires_too_soon"): - order_contract.isValidSignature(order_digest, signature_encoded_order) - - -def test_non_fill_or_kill_order( - milkman, - user, - token_to_sell, - token_to_buy, - amount, - price_checker, - price_checker_data, - chain, - hash_helper, -): - token_to_sell.approve(milkman, amount, {"from": user}) - - tx = milkman.requestSwapExactTokensForTokens( - int(amount), - token_to_sell, - token_to_buy, - user, - price_checker, - price_checker_data, - {"from": user}, - ) - - assert tx.events.count("SwapRequested") == 1 - - order_contract = Contract.from_abi( - "Milkman", tx.events["SwapRequested"]["orderContract"], milkman.abi - ) - - utils.check_swap_requested( - order_contract, - user, - user, - token_to_sell, - token_to_buy, - amount, - price_checker, - price_checker_data, - ) - - (fee_amount, buy_amount_after_fee) = utils.get_quote( - token_to_sell, token_to_buy, amount - ) - - valid_to = chain.time() + 60 * 60 * 24 - - signature_encoded_order = utils.encode_order_for_is_valid_signature( - token_to_sell, - token_to_buy, - user, - user, - amount - fee_amount, - buy_amount_after_fee, - valid_to, - fee_amount, - price_checker, - price_checker_data, - partially_fillable=True, - ) - - gpv2_order = ( - token_to_sell.address, - token_to_buy.address, - user.address, - amount - fee_amount, - buy_amount_after_fee, - valid_to, - utils.APP_DATA, - fee_amount, - utils.KIND_SELL, - True, # partially fillable - utils.ERC20_BALANCE, - utils.ERC20_BALANCE, - ) - - order_digest = hash_helper.hash( - gpv2_order, to_bytes(utils.DOMAIN_SEPARATOR, "bytes32") - ) - - with brownie.reverts("!fill_or_kill"): - order_contract.isValidSignature(order_digest, signature_encoded_order) - - -def test_non_erc20_sell_order( - milkman, - user, - token_to_sell, - token_to_buy, - amount, - price_checker, - price_checker_data, - chain, - hash_helper, -): - token_to_sell.approve(milkman, amount, {"from": user}) - - tx = milkman.requestSwapExactTokensForTokens( - int(amount), - token_to_sell, - token_to_buy, - user, - price_checker, - price_checker_data, - {"from": user}, - ) - - assert tx.events.count("SwapRequested") == 1 - - order_contract = Contract.from_abi( - "Milkman", tx.events["SwapRequested"]["orderContract"], milkman.abi - ) - - utils.check_swap_requested( - order_contract, - user, - user, - token_to_sell, - token_to_buy, - amount, - price_checker, - price_checker_data, - ) - - (fee_amount, buy_amount_after_fee) = utils.get_quote( - token_to_sell, token_to_buy, amount - ) - - valid_to = chain.time() + 60 * 60 * 24 - - signature_encoded_order = utils.encode_order_for_is_valid_signature( - token_to_sell, - token_to_buy, - user, - user, - amount - fee_amount, - buy_amount_after_fee, - valid_to, - fee_amount, - price_checker, - price_checker_data, - sell_token_balance=utils.BALANCE_EXTERNAL, - ) - - gpv2_order = ( - token_to_sell.address, - token_to_buy.address, - user.address, - amount - fee_amount, - buy_amount_after_fee, - valid_to, - utils.APP_DATA, - fee_amount, - utils.KIND_SELL, - False, # fill_or_kill - utils.BALANCE_EXTERNAL, - utils.ERC20_BALANCE, - ) - - order_digest = hash_helper.hash( - gpv2_order, to_bytes(utils.DOMAIN_SEPARATOR, "bytes32") - ) - - with brownie.reverts("!sell_erc20"): - order_contract.isValidSignature(order_digest, signature_encoded_order) - - -def test_non_erc20_buy_order( - milkman, - user, - token_to_sell, - token_to_buy, - amount, - price_checker, - price_checker_data, - chain, - hash_helper, -): - token_to_sell.approve(milkman, amount, {"from": user}) - - tx = milkman.requestSwapExactTokensForTokens( - int(amount), - token_to_sell, - token_to_buy, - user, - price_checker, - price_checker_data, - {"from": user}, - ) - - assert tx.events.count("SwapRequested") == 1 - - order_contract = Contract.from_abi( - "Milkman", tx.events["SwapRequested"]["orderContract"], milkman.abi - ) - - utils.check_swap_requested( - order_contract, - user, - user, - token_to_sell, - token_to_buy, - amount, - price_checker, - price_checker_data, - ) - - (fee_amount, buy_amount_after_fee) = utils.get_quote( - token_to_sell, token_to_buy, amount - ) - - valid_to = chain.time() + 60 * 60 * 24 - - signature_encoded_order = utils.encode_order_for_is_valid_signature( - token_to_sell, - token_to_buy, - user, - user, - amount - fee_amount, - buy_amount_after_fee, - valid_to, - fee_amount, - price_checker, - price_checker_data, - buy_token_balance=utils.BALANCE_INTERNAL, - ) - - gpv2_order = ( - token_to_sell.address, - token_to_buy.address, - user.address, - amount - fee_amount, - buy_amount_after_fee, - valid_to, - utils.APP_DATA, - fee_amount, - utils.KIND_SELL, - False, # fill_or_kill - utils.ERC20_BALANCE, - utils.BALANCE_INTERNAL, - ) - - order_digest = hash_helper.hash( - gpv2_order, to_bytes(utils.DOMAIN_SEPARATOR, "bytes32") - ) - - with brownie.reverts("!buy_erc20"): - order_contract.isValidSignature(order_digest, signature_encoded_order) diff --git a/tests/test_request.py b/tests/test_request.py deleted file mode 100644 index ddee478..0000000 --- a/tests/test_request.py +++ /dev/null @@ -1,131 +0,0 @@ -from brownie import ZERO_ADDRESS, reverts, Contract -import utils - - -def test_request_swap( - milkman, - user, - token_to_sell, - token_to_buy, - amount, - price_checker, - price_checker_data, -): - token_to_sell.approve(milkman, amount, {"from": user}) - - tx = milkman.requestSwapExactTokensForTokens( - int(amount), - token_to_sell, - token_to_buy, - user, - price_checker, - price_checker_data, - {"from": user}, - ) - - assert tx.events.count("SwapRequested") == 1 - - order_contract = Contract.from_abi( - "Milkman", tx.events["SwapRequested"]["orderContract"], milkman.abi - ) - - utils.check_swap_requested( - order_contract, - user, - user, - token_to_sell, - token_to_buy, - amount, - price_checker, - price_checker_data, - ) - - -def test_request_swap_twice( - milkman, - user, - token_to_sell, - token_to_buy, - amount, - price_checker, - price_checker_data, -): - token_to_sell.approve(milkman, amount, {"from": user}) - - amount_for_each = int(amount * 0.49) - - tx = milkman.requestSwapExactTokensForTokens( - amount_for_each, - token_to_sell, - token_to_buy, - user, - price_checker, - price_checker_data, - {"from": user}, - ) - - assert tx.events.count("SwapRequested") == 1 - - order_contract_1 = Contract.from_abi( - "Milkman", tx.events["SwapRequested"]["orderContract"], milkman.abi - ) - - tx = milkman.requestSwapExactTokensForTokens( - amount_for_each, - token_to_sell, - token_to_buy, - user, - price_checker, - price_checker_data, - {"from": user}, - ) - - assert tx.events.count("SwapRequested") == 1 - - order_contract_2 = Contract.from_abi( - "Milkman", tx.events["SwapRequested"]["orderContract"], milkman.abi - ) - - utils.check_swap_requested( - order_contract_1, - user, - user, - token_to_sell, - token_to_buy, - amount_for_each, - price_checker, - price_checker_data, - ) - utils.check_swap_requested( - order_contract_2, - user, - user, - token_to_sell, - token_to_buy, - amount_for_each, - price_checker, - price_checker_data, - ) - - -# brownie not allowing this test for now - -# def test_revert_without_token_move( -# milkman, -# user, -# token_to_sell, -# token_to_buy, -# amount, -# price_checker, -# price_checker_data, -# ): -# with reverts(): -# milkman.requestSwapExactTokensForTokens( -# int(amount / 2), -# token_to_sell, -# token_to_buy, -# user, -# price_checker, -# price_checker_data, -# {"from": user}, -# ) diff --git a/tests/utils.py b/tests/utils.py deleted file mode 100644 index d361c4b..0000000 --- a/tests/utils.py +++ /dev/null @@ -1,341 +0,0 @@ -from os import times -import requests -from eth_abi import encode_abi -from eth_utils import keccak -import time -from brownie.convert import to_bytes - -APP_DATA = "0x2B8694ED30082129598720860E8E972F07AA10D9B81CAE16CA0E2CFB24743E24" # maps to https://bafybeiblq2ko2maieeuvtbzaqyhi5fzpa6vbbwnydsxbnsqoft5si5b6eq.ipfs.dweb.link -KIND_SELL = "0xf3b277728b3fee749481eb3e0b3b48980dbbab78658fc419025cb16eee346775" -KIND_BUY = "0x6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" -ERC20_BALANCE = "0x5a28e9363bb942b639270062aa6bb295f434bcdfc42c97267bf003f272060dc9" -BALANCE_EXTERNAL = "0xabee3b73373acd583a130924aad6dc38cfdc44ba0555ba94ce2ff63980ea0632" -BALANCE_INTERNAL = "0x4ac99ace14ee0a5ef932dc609df0943ab7ac16b7583634612f8dc35a4289a6ce" -DOMAIN_SEPARATOR = "0xc078f884a2676e1345748b1feace7b0abee5d00ecadb6e574dcdd109a63e8943" -EIP_1271_MAGIC_VALUE = "0x1626ba7e" - - -EMPTY_BYTES = encode_abi(["uint8"], [int(0)]) - - -def check_swap_requested( - order_contract, - order_owner, - receiver, - from_token, - to_token, - amount, - price_checker, - price_checker_data, -): - # user, receiver, from_token, to_token, amount_in, price_checker, price_checker_data, nonce - encoded_market_order = encode_market_order_for_milkman( - order_owner, - receiver, - from_token, - to_token, - amount, - price_checker, - price_checker_data, - ) - swap_hash = keccak(encoded_market_order) - - print(f"Swap Hash: {order_contract.swapHash().hex()}") - print(f"Calculated Swap Hash: {swap_hash.hex()}") - - assert order_contract.swapHash().hex() == swap_hash.hex() - - -# encode a market order in the way that milkman encodes it as the swapHash pre-image -def encode_market_order_for_milkman( - order_owner, - receiver, - from_token, - to_token, - amount, - price_checker, - price_checker_data, -): - return encode_abi( - [ - "address", - "address", - "address", - "address", - "uint256", - "address", - "bytes", - ], - [ - order_owner.address, - receiver.address, - from_token.address, - to_token.address, - amount, - price_checker.address, - price_checker_data, - ], - ) - - -def encode_order_into_gpv2_order( - sell_token, - buy_token, - receiver, - sell_amount, - buy_amount, - valid_to, - fee_amount, -): - return encode_abi( - [ - "address", - "address", - "address", - "uint256", - "uint256", - "uint32", - "bytes32", - "uint256", - "bytes32", - "bool", - "bytes32", - "bytes32", - ], - [ - sell_token.address, - buy_token.address, - receiver.address, - sell_amount, - buy_amount, - valid_to, - to_bytes(APP_DATA, "bytes32"), - fee_amount, - to_bytes(KIND_SELL, "bytes32"), - False, - to_bytes(ERC20_BALANCE, "bytes32"), - to_bytes(ERC20_BALANCE, "bytes32"), - ], - ) - - -# encode a market order in the way tha `signature` -def encode_order_for_is_valid_signature( - sell_token, - buy_token, - order_owner, - receiver, - sell_amount, - buy_amount, - valid_to, - fee_amount, - price_checker, - price_checker_data, - buy_or_sell=KIND_SELL, - sell_token_balance=ERC20_BALANCE, - buy_token_balance=ERC20_BALANCE, - partially_fillable=False, -): - return encode_abi( - [ - "address", - "address", - "address", - "uint256", - "uint256", - "uint32", - "bytes32", - "uint256", - "bytes32", - "bool", - "bytes32", - "bytes32", - "address", - "address", - "bytes", - ], - [ - sell_token.address, - buy_token.address, - receiver.address, - sell_amount, - buy_amount, - valid_to, - to_bytes(APP_DATA, "bytes32"), - fee_amount, - to_bytes(buy_or_sell, "bytes32"), - partially_fillable, - to_bytes(sell_token_balance, "bytes32"), - to_bytes(buy_token_balance, "bytes32"), - order_owner.address, - price_checker.address, - price_checker_data, - ], - ) - - -def unpair_swap(gnosis_settlement, milkman, swap_id, order_uid): - assert gnosis_settlement.preSignature(order_uid) != 0 - milkman.unpairSwap(swap_id) - assert gnosis_settlement.preSignature(order_uid) == 0 - - -def pair_swap( - nonce, - chain, - gnosis_settlement, - milkman, - user, - receiver, - token_to_sell, - token_to_buy, - amount, - price_checker, - price_checker_data, - allowed_slippage_bps, -): - (order_uid, order_payload) = create_offchain_order( - chain, - milkman, - receiver, - token_to_sell, - token_to_buy, - amount, - allowed_slippage_bps, - ) - - gpv2_order = convert_offchain_order_into_gpv2_order(order_payload) - - assert gnosis_settlement.preSignature(order_uid) == 0 - milkman.pairSwap(gpv2_order, user, price_checker, price_checker_data, nonce) - assert gnosis_settlement.preSignature(order_uid) != 0 - - return (order_uid, order_payload) - - -def convert_offchain_order_into_gpv2_order(order_payload): - # struct Data { - # IERC20 sellToken; - # IERC20 buyToken; - # address receiver; - # uint256 sellAmount; - # uint256 buyAmount; - # uint32 validTo; - # bytes32 appData; - # uint256 feeAmount; - # bytes32 kind; - # bool partiallyFillable; - # bytes32 sellTokenBalance; - # bytes32 buyTokenBalance; - # } - # src: https://github.com/cowprotocol/contracts/blob/b5224fac50d050efec0d72ab303c0420576ab2ae/src/contracts/libraries/GPv2Order.sol#L11 - gpv2_order = ( - order_payload["sellToken"], - order_payload["buyToken"], - order_payload["receiver"], - int(order_payload["sellAmount"]), - int(order_payload["buyAmount"]), - order_payload["validTo"], - order_payload["appData"], - int(order_payload["feeAmount"]), - KIND_SELL, - False, # fill or kill - ERC20_BALANCE, - ERC20_BALANCE, - ) - - return gpv2_order - - -# submit the order to the cow.fi endpoint -def create_offchain_order( - chain, milkman, receiver, sell_token, buy_token, amount, allowed_slippage_in_bips -): - (fee_amount, buy_amount_after_fee) = get_quote(sell_token, buy_token, amount) - - buy_amount_after_fee_with_slippage = int( - (buy_amount_after_fee * (10_000 - allowed_slippage_in_bips)) / 10_000 - ) - assert fee_amount > 0 - assert buy_amount_after_fee_with_slippage > 0 - - deadline = chain.time() + 60 * 60 * 24 * 1 # 1 day - - # Submit order - order_payload = { - "sellToken": sell_token.address, - "buyToken": buy_token.address, - "sellAmount": str( - amount - fee_amount - ), # amount that we have minus the fee we have to pay - "buyAmount": str( - buy_amount_after_fee_with_slippage - ), # buy amount fetched from the previous call - "validTo": deadline, - "appData": APP_DATA, - "feeAmount": str(fee_amount), - "kind": "sell", - "partiallyFillable": False, - "receiver": receiver.address, - "signature": milkman.address, - "from": milkman.address, - "sellTokenBalance": "erc20", - "buyTokenBalance": "erc20", - "signingScheme": "presign", # Very important. this tells the api you are going to sign on chain - } - - orders_url = f"https://api.cow.fi/mainnet/api/v1/orders" - r = requests.post(orders_url, json=order_payload) - - times_to_retry = 3 - while times_to_retry > 0: - if r.ok and r.status_code == 201: - break - - r = requests.post(orders_url, json=order_payload) - times_to_retry -= 1 - assert r.ok and r.status_code == 201 - order_uid = r.json() - print(f"Payload: {order_payload}") - print(f"Order uid: {order_uid}") - - return (order_uid, order_payload) - - -def get_quote(sell_token, buy_token, sell_amount): - # get the fee + the buy amount after fee - - quote_url = "https://api.cow.fi/mainnet/api/v1/quote" - post_body = { - "sellToken": sell_token.address, - "buyToken": buy_token.address, - "from": "0x5F4bd1b3667127Bf44beBBa9e5d736B65A1677E5", - "kind": "sell", - "sellAmountBeforeFee": str(sell_amount), - "priceQuality": "fast", - "signingScheme": "eip1271", - "verificationGasLimit": 30000, - } - - r = requests.post(quote_url, json=post_body) - - times_to_retry = 3 - while times_to_retry > 0: - if r.ok and r.status_code == 200: - break - time.sleep(1) - r = requests.post(quote_url, json=post_body) - times_to_retry -= 1 - print(f"Response: {r}") - assert r.ok and r.status_code == 200 - - quote = r.json()["quote"] - fee_amount = int(quote["feeAmount"]) - buy_amount_after_fee = int(quote["buyAmount"]) - valid_to = int(quote["validTo"]) - - return (fee_amount, buy_amount_after_fee) - - -def dynamic_slippage_data(slippage_in_bips, data_bytes): - return encode_abi(["uint256", "bytes"], [int(slippage_in_bips), data_bytes]) From e2c10e3df63e7f957faeb49a5cd22eccaaaf3915 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Sat, 24 Feb 2024 16:42:12 -0700 Subject: [PATCH 53/54] ci: update CI to use foundry --- .github/workflows/test.yml | 50 ++++---------------------------------- 1 file changed, 5 insertions(+), 45 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e3a7b6e..19883ee 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,52 +13,12 @@ jobs: steps: - uses: actions/checkout@v1 - - - name: Cache compiler installations - uses: actions/cache@v2 - with: - path: | - ~/.solcx - ~/.vvm - key: ${{ runner.os }}-compiler-cache - - - name: Setup node.js - uses: actions/setup-node@v1 - with: - node-version: '12.x' - - - name: Install ganache - run: npm install -g ganache-cli@6.12.1 - - - name: Set up python 3.9 - uses: actions/setup-python@v2 - with: - python-version: 3.9 - - - name: Set pip cache directory path - id: pip-cache-dir-path - run: | - echo "::set-output name=dir::$(pip cache dir)" - - - name: Restore pip cache - uses: actions/cache@v2 - id: pip-cache with: - path: | - ${{ steps.pip-cache-dir-path.outputs.dir }} - key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements-dev.txt') }} - restore-keys: | - ${{ runner.os }}-pip-${{ hashFiles('**/requirements-dev.txt') }} - ${{ runner.os }}-pip- + submodules: recursive - - name: Install python dependencies - run: pip install -r requirements-dev.txt + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 - - name: Compile Code - run: brownie compile --size + - name: Run tests + run: forge test -vv --fork-url "https://mainnet.infura.io/v3/b7821200399e4be2b4e5dbdf06fbe85b" --etherscan-api-key "MW5CQA6QK5YMJXP2WP3RA36HM5A7RA1IHA" --ffi - - name: Run Tests - env: - ETHERSCAN_TOKEN: MW5CQA6QK5YMJXP2WP3RA36HM5A7RA1IHA - WEB3_INFURA_PROJECT_ID: b7821200399e4be2b4e5dbdf06fbe85b - run: brownie test From 02fed4b22545563c7f7d93af99504475bbabc35e Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Sat, 24 Feb 2024 16:45:04 -0700 Subject: [PATCH 54/54] ci: update infura key --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 19883ee..211d1b0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 with: submodules: recursive @@ -20,5 +20,5 @@ jobs: uses: foundry-rs/foundry-toolchain@v1 - name: Run tests - run: forge test -vv --fork-url "https://mainnet.infura.io/v3/b7821200399e4be2b4e5dbdf06fbe85b" --etherscan-api-key "MW5CQA6QK5YMJXP2WP3RA36HM5A7RA1IHA" --ffi + run: forge test -vv --fork-url "https://mainnet.infura.io/v3/e74132f416d346308763252779d7df22" --etherscan-api-key "MW5CQA6QK5YMJXP2WP3RA36HM5A7RA1IHA" --ffi