From 316d1ffdb14e2e05749f7a8864c19bc1cc0a703c Mon Sep 17 00:00:00 2001 From: pistomat Date: Wed, 29 Nov 2023 12:55:56 +0100 Subject: [PATCH 01/16] Reformat and remove redundant @dev notices --- evm/foundry.toml | 1 + evm/interfaces/ISwapAdapter.sol | 105 ------------------ evm/remappings.txt | 2 +- evm/src/interfaces/ISwapAdapter.sol | 104 +++++++++++++++++ .../interfaces/ISwapAdapterTypes.sol | 31 +++--- evm/src/uniswap-v2/UniswapV2SwapAdapter.sol | 6 +- evm/test/UniswapV2SwapAdapter.t.sol | 19 ++-- 7 files changed, 135 insertions(+), 133 deletions(-) delete mode 100644 evm/interfaces/ISwapAdapter.sol create mode 100644 evm/src/interfaces/ISwapAdapter.sol rename evm/{ => src}/interfaces/ISwapAdapterTypes.sol (73%) diff --git a/evm/foundry.toml b/evm/foundry.toml index 7010ce4..992083f 100644 --- a/evm/foundry.toml +++ b/evm/foundry.toml @@ -9,6 +9,7 @@ mainnet = "${ETH_RPC_URL}" [fmt] line_length = 80 +wrap_comments = true [etherscan] mainnet = { key = "${ETHERSCAN_MAINNET_KEY}" } diff --git a/evm/interfaces/ISwapAdapter.sol b/evm/interfaces/ISwapAdapter.sol deleted file mode 100644 index 9995783..0000000 --- a/evm/interfaces/ISwapAdapter.sol +++ /dev/null @@ -1,105 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity ^0.8.13; - -import "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; -import "interfaces/ISwapAdapterTypes.sol"; - -/// @title ISwapAdapterTypes -/// @dev Implement this interface to support propeller routing through your pairs. -/// @dev Before implementing the interface we need to introduce three function for a -/// @dev given pair: The swap(x), gas(x) and price(x) functions: -/// @dev The swap function accepts some specified token amount: x and returns the -/// @dev amount y a user can get by swapping x through the venue. -/// @dev The gas function simply returns the estimated gas cost given a specified -/// @dev amount x. -/// @dev Last but not least, the price function is the derivative of the swap -/// @dev function. It represents the best possible price a user can get from a -/// @dev pair after swapping x of the specified token. -/// @dev During calls to swap and getLimits, the caller can be assumed to -/// @dev have the required sell or buy token balance as well as unlimited approvals -/// @dev to this contract. -interface ISwapAdapter is ISwapAdapterTypes { - /// @notice Calculates pair prices for specified amounts (optional). - /// @dev The returned prices should include all dex fees, in case the fee - /// @dev is dynamic, the returned price is expected to include the minimum fee. - /// @dev Ideally this method should be implemented, although it is optional as - /// @dev the price function can be numerically estimated from the swap function. - /// @dev In case it is not available it should be flagged via capabilities and - /// @dev calling it should revert using the `NotImplemented` error. - /// @dev The method needs to be implemented as view as this is usually more efficient - /// @dev and can be run in parallel. - /// @dev all. - /// @param pairId The ID of the trading pair. - /// @param sellToken The token being sold. - /// @param buyToken The token being bought. - /// @param sellAmounts The specified amounts used for price calculation. - /// @return prices array of prices as fractions corresponding to the provided amounts. - function price( - bytes32 pairId, - IERC20 sellToken, - IERC20 buyToken, - uint256[] memory sellAmounts - ) external view returns (Fraction[] memory prices); - - /// @notice Simulates swapping tokens on a given pair. - /// @dev This function should be state modifying meaning it should actually execute - /// @dev the swap and change the state of the evm accordingly. - /// @dev Please include a gas usage estimate for each amount. This can be achieved - /// @dev e.g. by using the `gasleft()` function. - /// @dev The return type trade, has a price attribute which should contain the - /// value of `price(specifiedAmount)`. As this is optional, defined via - /// `Capability.PriceFunction`, it is valid to return a zero value for this - /// price in that case it will be estimated numerically. To return zero use - /// Fraction(0, 1). - /// @param pairId The ID of the trading pair. - /// @param sellToken The token being sold. - /// @param buyToken The token being bought. - /// @param side The side of the trade (Sell or Buy). - /// @param specifiedAmount The amount to be traded. - /// @return trade Trade struct representing the executed trade. - function swap( - bytes32 pairId, - IERC20 sellToken, - IERC20 buyToken, - SwapSide side, - uint256 specifiedAmount - ) external returns (Trade memory trade); - - /// @notice Retrieves the limits for each token. - /// @dev Retrieve the maximum limits of a token that can be traded. The limit is reached - /// @dev when the change in the received amounts is zero or close to zero. If in doubt - /// @dev over estimate. The swap function should not error with `LimitExceeded` if - /// @dev called with amounts below the limit. - /// @param pairId The ID of the trading pair. - /// @return An array of limits. - function getLimits(bytes32 pairId, SwapSide side) - external - returns (uint256[] memory); - - /// @notice Retrieves the capabilities of the selected pair. - /// @param pairId The ID of the trading pair. - /// @return An array of Capabilities. - function getCapabilities(bytes32 pairId, IERC20 sellToken, IERC20 buyToken) - external - returns (Capabilities[] memory); - - /// @notice Retrieves the tokens in the selected pair. - /// @dev Mainly used for testing as this is redundant with the required substreams - /// @dev implementation. - /// @param pairId The ID of the trading pair. - /// @return tokens array of IERC20 contracts. - function getTokens(bytes32 pairId) - external - returns (IERC20[] memory tokens); - - /// @notice Retrieves a range of pool IDs. - /// @dev Mainly used for testing it is alright to not return all available pools here. - /// @dev Nevertheless this is useful to test against the substreams implementation. If - /// @dev implemented it safes time writing custom tests. - /// @param offset The starting index from which to retrieve pool IDs. - /// @param limit The maximum number of pool IDs to retrieve. - /// @return ids array of pool IDs. - function getPoolIds(uint256 offset, uint256 limit) - external - returns (bytes32[] memory ids); -} diff --git a/evm/remappings.txt b/evm/remappings.txt index d5cf6c1..2c8eb8b 100644 --- a/evm/remappings.txt +++ b/evm/remappings.txt @@ -1,4 +1,4 @@ -interfaces/=interfaces/ +interfaces/=src/interfaces/ forge-std/=lib/forge-std/src/ openzeppelin-contracts/=lib/openzeppelin-contracts/ src/=src/ \ No newline at end of file diff --git a/evm/src/interfaces/ISwapAdapter.sol b/evm/src/interfaces/ISwapAdapter.sol new file mode 100644 index 0000000..c138586 --- /dev/null +++ b/evm/src/interfaces/ISwapAdapter.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.13; + +import "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; +import "interfaces/ISwapAdapterTypes.sol"; + +/// @title ISwapAdapterTypes +/// @dev Implement this interface to support propeller routing through your +/// pairs. Before implementing the interface we need to introduce three function +/// for a given pair: The swap(x), gas(x) and price(x) functions: The swap +/// function accepts some specified token amount: x and returns the amount y a +/// user can get by swapping x through the venue. The gas function simply +/// returns the estimated gas cost given a specified amount x. Last but not +/// least, the price function is the derivative of the swap function. It +/// represents the best possible price a user can get from a pair after swapping +/// x of the specified token. During calls to swap and getLimits, the caller can +/// be assumed to have the required sell or buy token balance as well as +/// unlimited approvals to this contract. +interface ISwapAdapter is ISwapAdapterTypes { + /// @notice Calculates pair prices for specified amounts (optional). + /// @dev The returned prices should include all dex fees, in case the fee is + /// dynamic, the returned price is expected to include the minimum fee. + /// Ideally this method should be implemented, although it is optional as + /// the price function can be numerically estimated from the swap function. + /// In case it is not available it should be flagged via capabilities and + /// calling it should revert using the `NotImplemented` error. The method + /// needs to be implemented as view as this is usually more efficient and + /// can be run in parallel. all. + /// @param pairId The ID of the trading pair. + /// @param sellToken The token being sold. + /// @param buyToken The token being bought. + /// @param sellAmounts The specified amounts used for price calculation. + /// @return prices array of prices as fractions corresponding to the + /// provided amounts. + function price( + bytes32 pairId, + IERC20 sellToken, + IERC20 buyToken, + uint256[] memory sellAmounts + ) external view returns (Fraction[] memory prices); + + /** + * @notice Simulates swapping tokens on a given pair. + * @dev This function should be state modifying meaning it should actually + * execute the swap and change the state of the evm accordingly. Please + * include a gas usage estimate for each amount. This can be achieved e.g. + * by using the `gasleft()` function. The return type trade, has a price + * attribute which should contain the value of `price(specifiedAmount)`. As + * this is optional, defined via `Capability.PriceFunction`, it is valid to + * return a zero value for this price in that case it will be estimated + * numerically. To return zero use Fraction(0, 1). + * @param pairId The ID of the trading pair. + * @param sellToken The token being sold. + * @param buyToken The token being bought. + * @param side The side of the trade (Sell or Buy). + * @param specifiedAmount The amount to be traded. + * @return trade Trade struct representing the executed trade. + */ + function swap( + bytes32 pairId, + IERC20 sellToken, + IERC20 buyToken, + SwapSide side, + uint256 specifiedAmount + ) external returns (Trade memory trade); + + /// @notice Retrieves the limits for each token. + /// @dev Retrieve the maximum limits of a token that can be traded. The + /// limit is reached when the change in the received amounts is zero or + /// close to zero. If in doubt over estimate. The swap function should not + /// error with `LimitExceeded` if called with amounts below the limit. + /// @param pairId The ID of the trading pair. + /// @return An array of limits. + function getLimits(bytes32 pairId, SwapSide side) + external + returns (uint256[] memory); + + /// @notice Retrieves the capabilities of the selected pair. + /// @param pairId The ID of the trading pair. + /// @return An array of Capabilities. + function getCapabilities(bytes32 pairId, IERC20 sellToken, IERC20 buyToken) + external + returns (Capabilities[] memory); + + /// @notice Retrieves the tokens in the selected pair. + /// @dev Mainly used for testing as this is redundant with the required + /// substreams implementation. + /// @param pairId The ID of the trading pair. + /// @return tokens array of IERC20 contracts. + function getTokens(bytes32 pairId) + external + returns (IERC20[] memory tokens); + + /// @notice Retrieves a range of pool IDs. + /// @dev Mainly used for testing it is alright to not return all available + /// pools here. Nevertheless this is useful to test against the substreams + /// implementation. If implemented it safes time writing custom tests. + /// @param offset The starting index from which to retrieve pool IDs. + /// @param limit The maximum number of pool IDs to retrieve. + /// @return ids array of pool IDs. + function getPoolIds(uint256 offset, uint256 limit) + external + returns (bytes32[] memory ids); +} diff --git a/evm/interfaces/ISwapAdapterTypes.sol b/evm/src/interfaces/ISwapAdapterTypes.sol similarity index 73% rename from evm/interfaces/ISwapAdapterTypes.sol rename to evm/src/interfaces/ISwapAdapterTypes.sol index abe5392..3825593 100644 --- a/evm/interfaces/ISwapAdapterTypes.sol +++ b/evm/src/interfaces/ISwapAdapterTypes.sol @@ -4,14 +4,16 @@ pragma solidity ^0.8.13; import "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; interface ISwapAdapterTypes { - /// @dev The SwapSide enum represents possible sides of a trade: Sell or Buy. - /// @dev E.g. if SwapSide is Sell, the sell amount is interpreted to be fixed. + /// @dev The SwapSide enum represents possible sides of a trade: Sell or + /// Buy. E.g. if SwapSide is Sell, the sell amount is interpreted to be + /// fixed. enum SwapSide { Sell, Buy } - /// @dev The Capabilities enum represents possible features of a trading pair. + /// @dev The Capabilities enum represents possible features of a trading + /// pair. enum Capabilities { Unset, // Support SwapSide.Sell values (required) @@ -22,22 +24,20 @@ interface ISwapAdapterTypes { PriceFunction, // Support tokens that charge a fee on transfer (optional) FeeOnTransfer, - // The pair does not suffer from price impact and mantains - // a constant price for increasingly larger speficied amounts. - // (optional) + // The pair does not suffer from price impact and mantains a constant + // price for increasingly larger speficied amounts. (optional) ConstantPrice, - // Indicates that the pair does not read it's own token balances - // while swapping. (optional) + // Indicates that the pair does not read it's own token balances while + // swapping. (optional) TokenBalanceIndependent, - // Indicates that prices are returned scaled, else it is assumed - // prices still require scaling by token decimals. + // Indicates that prices are returned scaled, else it is assumed prices + // still require scaling by token decimals. ScaledPrices } /// @dev Representation used for rational numbers such as prices. struct Fraction { - // TODO: rename numerator - uint256 nominator; + uint256 numerator; uint256 denominator; } @@ -49,11 +49,10 @@ interface ISwapAdapterTypes { } /// @dev The Unavailable error is thrown when a pool or swap is not - /// @dev available for unexpected reason, e.g. because it was paused - /// @dev due to a bug. + /// available for unexpected reason. E.g. it was paused due to a bug. error Unavailable(string reason); - /// @dev The LimitExceeded error is thrown when a limit has been - /// @dev exceeded. E.g. the specified amount can't be traded safely. + /// @dev The LimitExceeded error is thrown when a limit has been exceeded. + /// E.g. the specified amount can't be traded safely. error LimitExceeded(uint256 limit); } diff --git a/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol b/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol index d86c505..aac2bef 100644 --- a/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol +++ b/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol @@ -101,7 +101,8 @@ contract UniswapV2SwapAdapter is ISwapAdapter { return amountOut; } - // given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset + // given an input amount of an asset and pair reserves, returns the maximum + // output amount of the other asset function getAmountOut( uint256 amountIn, uint256 reserveIn, @@ -142,7 +143,8 @@ contract UniswapV2SwapAdapter is ISwapAdapter { return amount; } - // given an output amount of an asset and pair reserves, returns a required input amount of the other asset + // given an output amount of an asset and pair reserves, returns a required + // input amount of the other asset function getAmountIn( uint256 amountOut, uint256 reserveIn, diff --git a/evm/test/UniswapV2SwapAdapter.t.sol b/evm/test/UniswapV2SwapAdapter.t.sol index 78ebe7f..8a82725 100644 --- a/evm/test/UniswapV2SwapAdapter.t.sol +++ b/evm/test/UniswapV2SwapAdapter.t.sol @@ -33,7 +33,7 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { pairFunctions.price(pair, WETH, USDC, amounts); for (uint256 i = 0; i < prices.length; i++) { - assertGt(prices[i].nominator, 0); + assertGt(prices[i].numerator, 0); assertGt(prices[i].denominator, 0); } } @@ -61,14 +61,15 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { pure returns (int8) { - uint256 crossProduct1 = frac1.nominator * frac2.denominator; - uint256 crossProduct2 = frac2.nominator * frac1.denominator; + uint256 crossProduct1 = frac1.numerator * frac2.denominator; + uint256 crossProduct2 = frac2.numerator * frac1.denominator; - if (crossProduct1 == crossProduct2) return 0; // fractions are equal - - else if (crossProduct1 > crossProduct2) return 1; // frac1 is greater than frac2 - - else return -1; // frac1 is less than frac2 + // fractions are equal + if (crossProduct1 == crossProduct2) return 0; + // frac1 is greater than frac2 + else if (crossProduct1 > crossProduct2) return 1; + // frac1 is less than frac2 + else return -1; } function testSwapFuzz(uint256 amount, bool isBuy) public { @@ -127,6 +128,6 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { function testGetLimits() public { bytes32 pair = bytes32(bytes20(USDC_WETH_PAIR)); - pairFunctions.getLimits(pair, SwapSide.Sell); + uint256[] memory limits = pairFunctions.getLimits(pair, SwapSide.Sell); } } From 092fce79f683159ca072f8558ddc2ca5467f335b Mon Sep 17 00:00:00 2001 From: pistomat Date: Fri, 1 Dec 2023 13:09:02 +0100 Subject: [PATCH 02/16] Refactor and add Balancer V2 stub --- .gitignore | 1 + .vscode/settings.json | 2 +- docs/logic/vm-integration/README.md | 19 +++++ .../logic/vm-integration/ethereum-solidity.md | 4 +- evm/src/balancer-v2/BalancerV2SwapAdapter.sol | 84 +++++++++++++++++++ evm/src/balancer-v2/manifest.yaml | 36 ++++++++ evm/src/interfaces/ISwapAdapter.sol | 30 +++---- evm/src/interfaces/ISwapAdapterTypes.sol | 26 ++++-- evm/src/uniswap-v2/UniswapV2SwapAdapter.sol | 19 ++--- evm/src/uniswap-v2/manifest.yaml | 2 +- evm/test/UniswapV2SwapAdapter.t.sol | 2 +- 11 files changed, 187 insertions(+), 38 deletions(-) create mode 100644 .gitignore create mode 100644 evm/src/balancer-v2/BalancerV2SwapAdapter.sol create mode 100644 evm/src/balancer-v2/manifest.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..485dee6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea diff --git a/.vscode/settings.json b/.vscode/settings.json index 0335d4f..b00c8e4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,6 @@ { "solidity.formatter": "forge", - "solidity.compileUsingRemoteVersion": "v0.8.19", + "solidity.compileUsingRemoteVersion": "v0.8.20", "solidity.packageDefaultDependenciesContractsDirectory": "evm/src", "solidity.packageDefaultDependenciesDirectory": "evm/lib", } \ No newline at end of file diff --git a/docs/logic/vm-integration/README.md b/docs/logic/vm-integration/README.md index 33f67af..cb62c79 100644 --- a/docs/logic/vm-integration/README.md +++ b/docs/logic/vm-integration/README.md @@ -4,3 +4,22 @@ This page describes the interface required to implement protocol logic component To create a VM implementation, it is required two provide a manifest file as well as a implementation of the corresponding adapter interface. +## Step by step + +### Prerequisites + +1. Start by making a local copy of the Propeller Protocol Lib repository: +```bash +git clone https://github.com/propeller-heads/propeller-protocol-lib +``` + +2. Install `Foundry`, the smart contract development toolchain we use. We recommend installation using [foundryup](https://book.getfoundry.sh/getting-started/installation#using-foundryup) + +3. Install forge dependencies: +```bash +cd evm +forge install +``` + +4. Your integration should be in a separate directory in the `evm/src` folder. You can clone one of the example directories `evm/src/uniswap-v2` or `evm/src/balancer` and rename it to your integration name. +``` \ No newline at end of file diff --git a/docs/logic/vm-integration/ethereum-solidity.md b/docs/logic/vm-integration/ethereum-solidity.md index 81ff61a..0946a56 100644 --- a/docs/logic/vm-integration/ethereum-solidity.md +++ b/docs/logic/vm-integration/ethereum-solidity.md @@ -46,7 +46,7 @@ instances: arguments: - "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f" -# Specify some automatic test cases in case getPools and +# Specify some automatic test cases in case getPoolIds and # getTokens are not implemented. tests: instances: @@ -117,7 +117,7 @@ Retrieves the capabilities of the selected pair. ```solidity function getCapabilities(bytes32 pairId, IERC20 sellToken, IERC20 buyToken) external - returns (Capabilities[] memory); + returns (Capability[] memory); ``` #### getTokens (optional) diff --git a/evm/src/balancer-v2/BalancerV2SwapAdapter.sol b/evm/src/balancer-v2/BalancerV2SwapAdapter.sol new file mode 100644 index 0000000..3175f32 --- /dev/null +++ b/evm/src/balancer-v2/BalancerV2SwapAdapter.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.13; + +import {IERC20, ISwapAdapter} from "interfaces/ISwapAdapter.sol"; + +contract BalancerV2SwapAdapter is ISwapAdapter { + + constructor() { + } + + function getPairReserves( + bytes32 pairId, + IERC20 sellToken, + IERC20 buyToken + ) internal view returns (uint112 r0, uint112 r1) { + revert NotImplemented("BalancerV2SwapAdapter.getPairReserves"); + } + + function price( + bytes32 pairId, + IERC20 sellToken, + IERC20 buyToken, + uint256[] memory sellAmounts + ) external view override returns (Fraction[] memory prices) { + revert NotImplemented("BalancerV2SwapAdapter.price"); + } + + function getPriceAt(uint256 amountIn, uint256 reserveIn, uint256 reserveOut) + internal + pure + returns (Fraction memory) + { + revert NotImplemented("BalancerV2SwapAdapter.getPriceAt"); + } + + function swap( + bytes32 pairId, + IERC20 sellToken, + IERC20 buyToken, + SwapSide side, + uint256 specifiedAmount + ) external override returns (Trade memory trade) { + revert NotImplemented("BalancerV2SwapAdapter.swap"); + } + + function getLimits(bytes32 pairId, SwapSide side) + external + view + override + returns (uint256[] memory limits) + { + revert NotImplemented("BalancerV2SwapAdapter.getLimits"); + } + + function getCapabilities(bytes32, IERC20, IERC20) + external + pure + override + returns (Capability[] memory capabilities) + { + capabilities = new Capability[](3); + capabilities[0] = Capability.SellSide; + capabilities[1] = Capability.BuySide; + capabilities[2] = Capability.PriceFunction; + } + + function getTokens(bytes32 pairId) + external + view + override + returns (IERC20[] memory tokens) + { + revert NotImplemented("BalancerV2SwapAdapter.getTokens"); + } + + function getPoolIds(uint256 offset, uint256 limit) + external + view + override + returns (bytes32[] memory ids) + { + revert NotImplemented("BalancerV2SwapAdapter.getPoolIds"); + } +} diff --git a/evm/src/balancer-v2/manifest.yaml b/evm/src/balancer-v2/manifest.yaml new file mode 100644 index 0000000..822388d --- /dev/null +++ b/evm/src/balancer-v2/manifest.yaml @@ -0,0 +1,36 @@ +# information about the author helps us reach out in case of issues. +author: + name: Propellerheads.xyz + email: pistomat@propellerheads.xyz + +# Protocol Constants +constants: + protocol_gas: 30000 + # minimum capabilities we can expect, individual pools may extend these + capabilities: + - SellSide + - BuySide + - PriceFunction + +# The file containing the adapter contract +contract: BalancerV2SwapAdapter.sol + +# Deployment instances used to generate chain specific bytecode. +instances: + - chain: + name: mainnet + id: 0 + arguments: + - "0xBA12222222228d8Ba445958a75a0704d566BF2C8" + +# Specify some automatic test cases in case getPoolIds and +# getTokens are not implemented. +tests: + instances: + - pair_id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc" + sell_token: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + buy_token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + block: 17000000 + chain: + id: 0 + name: mainnet diff --git a/evm/src/interfaces/ISwapAdapter.sol b/evm/src/interfaces/ISwapAdapter.sol index c138586..1714bc7 100644 --- a/evm/src/interfaces/ISwapAdapter.sol +++ b/evm/src/interfaces/ISwapAdapter.sol @@ -1,14 +1,14 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity ^0.8.13; -import "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; -import "interfaces/ISwapAdapterTypes.sol"; +import {IERC20} from "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; +import {ISwapAdapterTypes} from "interfaces/ISwapAdapterTypes.sol"; -/// @title ISwapAdapterTypes +/// @title ISwapAdapter /// @dev Implement this interface to support propeller routing through your /// pairs. Before implementing the interface we need to introduce three function /// for a given pair: The swap(x), gas(x) and price(x) functions: The swap -/// function accepts some specified token amount: x and returns the amount y a +/// function accepts some specified token amount x and returns the amount y a /// user can get by swapping x through the venue. The gas function simply /// returns the estimated gas cost given a specified amount x. Last but not /// least, the price function is the derivative of the swap function. It @@ -44,7 +44,7 @@ interface ISwapAdapter is ISwapAdapterTypes { * @dev This function should be state modifying meaning it should actually * execute the swap and change the state of the evm accordingly. Please * include a gas usage estimate for each amount. This can be achieved e.g. - * by using the `gasleft()` function. The return type trade, has a price + * by using the `gasleft()` function. The return type `Trade` has a price * attribute which should contain the value of `price(specifiedAmount)`. As * this is optional, defined via `Capability.PriceFunction`, it is valid to * return a zero value for this price in that case it will be estimated @@ -67,26 +67,28 @@ interface ISwapAdapter is ISwapAdapterTypes { /// @notice Retrieves the limits for each token. /// @dev Retrieve the maximum limits of a token that can be traded. The /// limit is reached when the change in the received amounts is zero or - /// close to zero. If in doubt over estimate. The swap function should not - /// error with `LimitExceeded` if called with amounts below the limit. + /// close to zero. Overestimate if in doubt rather than underestimate. The + /// swap function should not error with `LimitExceeded` if called with + /// amounts below the limit. /// @param pairId The ID of the trading pair. - /// @return An array of limits. + /// @param side The side of the trade (Sell or Buy). + /// @return limits An array of limits. function getLimits(bytes32 pairId, SwapSide side) external - returns (uint256[] memory); + returns (uint256[] memory limits); /// @notice Retrieves the capabilities of the selected pair. /// @param pairId The ID of the trading pair. - /// @return An array of Capabilities. + /// @return capabilities An array of Capability. function getCapabilities(bytes32 pairId, IERC20 sellToken, IERC20 buyToken) external - returns (Capabilities[] memory); + returns (Capability[] memory capabilities); /// @notice Retrieves the tokens in the selected pair. /// @dev Mainly used for testing as this is redundant with the required /// substreams implementation. /// @param pairId The ID of the trading pair. - /// @return tokens array of IERC20 contracts. + /// @return tokens An array of IERC20 contracts. function getTokens(bytes32 pairId) external returns (IERC20[] memory tokens); @@ -94,10 +96,10 @@ interface ISwapAdapter is ISwapAdapterTypes { /// @notice Retrieves a range of pool IDs. /// @dev Mainly used for testing it is alright to not return all available /// pools here. Nevertheless this is useful to test against the substreams - /// implementation. If implemented it safes time writing custom tests. + /// implementation. If implemented it saves time writing custom tests. /// @param offset The starting index from which to retrieve pool IDs. /// @param limit The maximum number of pool IDs to retrieve. - /// @return ids array of pool IDs. + /// @return ids An array of pool IDs. function getPoolIds(uint256 offset, uint256 limit) external returns (bytes32[] memory ids); diff --git a/evm/src/interfaces/ISwapAdapterTypes.sol b/evm/src/interfaces/ISwapAdapterTypes.sol index 3825593..d6a5f73 100644 --- a/evm/src/interfaces/ISwapAdapterTypes.sol +++ b/evm/src/interfaces/ISwapAdapterTypes.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity ^0.8.13; -import "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; +import {IERC20} from "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; interface ISwapAdapterTypes { /// @dev The SwapSide enum represents possible sides of a trade: Sell or @@ -12,9 +12,9 @@ interface ISwapAdapterTypes { Buy } - /// @dev The Capabilities enum represents possible features of a trading + /// @dev The Capability enum represents possible features of a trading /// pair. - enum Capabilities { + enum Capability { Unset, // Support SwapSide.Sell values (required) SellSide, @@ -24,18 +24,19 @@ interface ISwapAdapterTypes { PriceFunction, // Support tokens that charge a fee on transfer (optional) FeeOnTransfer, - // The pair does not suffer from price impact and mantains a constant - // price for increasingly larger speficied amounts. (optional) + // The pair does not suffer from price impact and maintains a constant + // price for increasingly larger specified amounts. (optional) ConstantPrice, // Indicates that the pair does not read it's own token balances while // swapping. (optional) TokenBalanceIndependent, // Indicates that prices are returned scaled, else it is assumed prices - // still require scaling by token decimals. + // still require scaling by token decimals. (required) ScaledPrices } /// @dev Representation used for rational numbers such as prices. + // TODO: Use only uint128 for numerator and denominator. struct Fraction { uint256 numerator; uint256 denominator; @@ -43,9 +44,12 @@ interface ISwapAdapterTypes { /// @dev The Trade struct holds data about an executed trade. struct Trade { - uint256 receivedAmount; // The amount received from the trade. - uint256 gasUsed; // The amount of gas used in the trade. - Fraction price; // The price of the pair after the trade. + // The amount received from the trade. + uint256 receivedAmount; + // The amount of gas used in the trade. + uint256 gasUsed; + // The price of the pair after the trade. For zero use Fraction(0, 1). + Fraction price; } /// @dev The Unavailable error is thrown when a pool or swap is not @@ -55,4 +59,8 @@ interface ISwapAdapterTypes { /// @dev The LimitExceeded error is thrown when a limit has been exceeded. /// E.g. the specified amount can't be traded safely. error LimitExceeded(uint256 limit); + + /// @dev The NotImplemented error is thrown when a function is not + /// implemented. + error NotImplemented(string reason); } diff --git a/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol b/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol index aac2bef..3345bfc 100644 --- a/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol +++ b/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity ^0.8.13; -import "interfaces/ISwapAdapter.sol"; +import {IERC20, ISwapAdapter} from "interfaces/ISwapAdapter.sol"; contract UniswapV2SwapAdapter is ISwapAdapter { IUniswapV2Factory immutable factory; @@ -56,11 +56,10 @@ contract UniswapV2SwapAdapter is ISwapAdapter { uint256 specifiedAmount ) external override returns (Trade memory trade) { if (specifiedAmount == 0) { - return trade; + return trade; // TODO: This returns Fraction(0, 0) instead of the expected zero Fraction(0, 1) } IUniswapV2Pair pair = IUniswapV2Pair(address(bytes20(pairId))); - uint256 gasBefore = 0; uint112 r0; uint112 r1; bool zero2one = sellToken < buyToken; @@ -69,7 +68,7 @@ contract UniswapV2SwapAdapter is ISwapAdapter { } else { (r1, r0,) = pair.getReserves(); } - gasBefore = gasleft(); + uint256 gasBefore = gasleft(); if (side == SwapSide.Sell) { trade.receivedAmount = sell(pair, sellToken, zero2one, r0, r1, specifiedAmount); @@ -101,7 +100,7 @@ contract UniswapV2SwapAdapter is ISwapAdapter { return amountOut; } - // given an input amount of an asset and pair reserves, returns the maximum + // Given an input amount of an asset and pair reserves, returns the maximum // output amount of the other asset function getAmountOut( uint256 amountIn, @@ -183,12 +182,12 @@ contract UniswapV2SwapAdapter is ISwapAdapter { external pure override - returns (Capabilities[] memory capabilities) + returns (Capability[] memory capabilities) { - capabilities = new Capabilities[](3); - capabilities[0] = Capabilities.SellSide; - capabilities[1] = Capabilities.BuySide; - capabilities[2] = Capabilities.PriceFunction; + capabilities = new Capability[](3); + capabilities[0] = Capability.SellSide; + capabilities[1] = Capability.BuySide; + capabilities[2] = Capability.PriceFunction; } function getTokens(bytes32 pairId) diff --git a/evm/src/uniswap-v2/manifest.yaml b/evm/src/uniswap-v2/manifest.yaml index 63119f3..5864481 100644 --- a/evm/src/uniswap-v2/manifest.yaml +++ b/evm/src/uniswap-v2/manifest.yaml @@ -23,7 +23,7 @@ instances: arguments: - "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f" -# Specify some automatic test cases in case getPools and +# Specify some automatic test cases in case getPoolIds and # getTokens are not implemented. tests: instances: diff --git a/evm/test/UniswapV2SwapAdapter.t.sol b/evm/test/UniswapV2SwapAdapter.t.sol index 8a82725..d6951c9 100644 --- a/evm/test/UniswapV2SwapAdapter.t.sol +++ b/evm/test/UniswapV2SwapAdapter.t.sol @@ -120,7 +120,7 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { } function testGetCapabilities(bytes32 pair, address t0, address t1) public { - Capabilities[] memory res = + Capability[] memory res = pairFunctions.getCapabilities(pair, IERC20(t0), IERC20(t1)); assertEq(res.length, 3); From 7ddfa351c97cdcc79d6a4b022aa5237ee797705b Mon Sep 17 00:00:00 2001 From: pistomat Date: Fri, 1 Dec 2023 13:13:02 +0100 Subject: [PATCH 03/16] forge install: balancer-v2-monorepo --- .gitmodules | 3 +++ evm/lib/balancer-v2-monorepo | 1 + 2 files changed, 4 insertions(+) create mode 160000 evm/lib/balancer-v2-monorepo diff --git a/.gitmodules b/.gitmodules index 6c9874d..cc50581 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "lib/openzeppelin-contracts"] path = evm/lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "evm/lib/balancer-v2-monorepo"] + path = evm/lib/balancer-v2-monorepo + url = https://github.com/balancer/balancer-v2-monorepo diff --git a/evm/lib/balancer-v2-monorepo b/evm/lib/balancer-v2-monorepo new file mode 160000 index 0000000..c7d4abb --- /dev/null +++ b/evm/lib/balancer-v2-monorepo @@ -0,0 +1 @@ +Subproject commit c7d4abbea39834e7778f9ff7999aaceb4e8aa048 From 0fa898c69d4a8ad95fa83a7149eaf9790a520731 Mon Sep 17 00:00:00 2001 From: pistomat Date: Fri, 1 Dec 2023 14:00:15 +0100 Subject: [PATCH 04/16] WIP BalancerV2SwapAdapter getLimits --- .gitmodules | 3 - evm/lib/balancer-v2-monorepo | 1 - evm/remappings.txt | 4 +- evm/src/balancer-v2/BalancerV2SwapAdapter.sol | 69 ++++++++++++++++--- evm/src/interfaces/ISwapAdapter.sol | 2 +- evm/src/uniswap-v2/UniswapV2SwapAdapter.sol | 12 ++-- evm/test/BalancerV2SwapAdapter.t.sol | 1 + evm/test/UniswapV2SwapAdapter.t.sol | 4 +- 8 files changed, 72 insertions(+), 24 deletions(-) delete mode 160000 evm/lib/balancer-v2-monorepo create mode 100644 evm/test/BalancerV2SwapAdapter.t.sol diff --git a/.gitmodules b/.gitmodules index cc50581..6c9874d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,6 +4,3 @@ [submodule "lib/openzeppelin-contracts"] path = evm/lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts -[submodule "evm/lib/balancer-v2-monorepo"] - path = evm/lib/balancer-v2-monorepo - url = https://github.com/balancer/balancer-v2-monorepo diff --git a/evm/lib/balancer-v2-monorepo b/evm/lib/balancer-v2-monorepo deleted file mode 160000 index c7d4abb..0000000 --- a/evm/lib/balancer-v2-monorepo +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c7d4abbea39834e7778f9ff7999aaceb4e8aa048 diff --git a/evm/remappings.txt b/evm/remappings.txt index 2c8eb8b..5df14d4 100644 --- a/evm/remappings.txt +++ b/evm/remappings.txt @@ -1,4 +1,4 @@ -interfaces/=src/interfaces/ forge-std/=lib/forge-std/src/ openzeppelin-contracts/=lib/openzeppelin-contracts/ -src/=src/ \ No newline at end of file +src/=src/ +balancer-v2/interfaces/=lib/balancer-v2-monorepo/pkg/interfaces/contracts diff --git a/evm/src/balancer-v2/BalancerV2SwapAdapter.sol b/evm/src/balancer-v2/BalancerV2SwapAdapter.sol index 3175f32..da182f6 100644 --- a/evm/src/balancer-v2/BalancerV2SwapAdapter.sol +++ b/evm/src/balancer-v2/BalancerV2SwapAdapter.sol @@ -1,19 +1,67 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity ^0.8.13; -import {IERC20, ISwapAdapter} from "interfaces/ISwapAdapter.sol"; +import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; -contract BalancerV2SwapAdapter is ISwapAdapter { +interface IVault { + function getPoolTokens(bytes32 poolId) + external + view + returns ( + IERC20[] memory tokens, + uint256[] memory balances, + uint256 lastChangeBlock + ); - constructor() { + function swap( + SingleSwap memory singleSwap, + FundManagement memory funds, + uint256 limit, + uint256 deadline + ) external payable returns (uint256); + + function queryBatchSwap( + SwapKind kind, + BatchSwapStep[] memory swaps, + IAsset[] memory assets, + FundManagement memory funds + ) external returns (int256[] memory assetDeltas); + + struct SingleSwap { + bytes32 poolId; + SwapKind kind; + IAsset assetIn; + IAsset assetOut; + uint256 amount; + bytes userData; } - function getPairReserves( - bytes32 pairId, - IERC20 sellToken, - IERC20 buyToken - ) internal view returns (uint112 r0, uint112 r1) { - revert NotImplemented("BalancerV2SwapAdapter.getPairReserves"); + struct BatchSwapStep { + bytes32 poolId; + uint256 assetInIndex; + uint256 assetOutIndex; + uint256 amount; + bytes userData; + } + + struct FundManagement { + address sender; + bool fromInternalBalance; + address payable recipient; + bool toInternalBalance; + } + + enum SwapKind { GIVEN_IN, GIVEN_OUT } +} + +interface IAsset is IERC20 { +} + +contract BalancerV2SwapAdapter is ISwapAdapter { + IVault immutable vault; + + constructor(address vault_) { + vault = IVault(vault_); } function price( @@ -49,7 +97,7 @@ contract BalancerV2SwapAdapter is ISwapAdapter { override returns (uint256[] memory limits) { - revert NotImplemented("BalancerV2SwapAdapter.getLimits"); + (, limits, ) = vault.getPoolTokens(pairId); } function getCapabilities(bytes32, IERC20, IERC20) @@ -73,6 +121,7 @@ contract BalancerV2SwapAdapter is ISwapAdapter { revert NotImplemented("BalancerV2SwapAdapter.getTokens"); } + /// @dev Balancer V2 does not support listing pools. function getPoolIds(uint256 offset, uint256 limit) external view diff --git a/evm/src/interfaces/ISwapAdapter.sol b/evm/src/interfaces/ISwapAdapter.sol index 1714bc7..bc7ebe4 100644 --- a/evm/src/interfaces/ISwapAdapter.sol +++ b/evm/src/interfaces/ISwapAdapter.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.13; import {IERC20} from "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; -import {ISwapAdapterTypes} from "interfaces/ISwapAdapterTypes.sol"; +import {ISwapAdapterTypes} from "src/interfaces/ISwapAdapterTypes.sol"; /// @title ISwapAdapter /// @dev Implement this interface to support propeller routing through your diff --git a/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol b/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol index 3345bfc..9d4ef9e 100644 --- a/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol +++ b/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol @@ -1,7 +1,9 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity ^0.8.13; -import {IERC20, ISwapAdapter} from "interfaces/ISwapAdapter.sol"; +import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; + +uint256 constant RESERVE_LIMIT_FACTOR = 10; contract UniswapV2SwapAdapter is ISwapAdapter { IUniswapV2Factory immutable factory; @@ -170,11 +172,11 @@ contract UniswapV2SwapAdapter is ISwapAdapter { limits = new uint256[](2); (uint256 r0, uint256 r1,) = pair.getReserves(); if (side == SwapSide.Sell) { - limits[0] = r0 * 10; - limits[1] = r1 * 10; + limits[0] = r0 * RESERVE_LIMIT_FACTOR; + limits[1] = r1 * RESERVE_LIMIT_FACTOR; } else { - limits[0] = r1 * 10; - limits[1] = r0 * 10; + limits[0] = r1 * RESERVE_LIMIT_FACTOR; + limits[1] = r0 * RESERVE_LIMIT_FACTOR; } } diff --git a/evm/test/BalancerV2SwapAdapter.t.sol b/evm/test/BalancerV2SwapAdapter.t.sol new file mode 100644 index 0000000..64b955a --- /dev/null +++ b/evm/test/BalancerV2SwapAdapter.t.sol @@ -0,0 +1 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/evm/test/UniswapV2SwapAdapter.t.sol b/evm/test/UniswapV2SwapAdapter.t.sol index d6951c9..f662972 100644 --- a/evm/test/UniswapV2SwapAdapter.t.sol +++ b/evm/test/UniswapV2SwapAdapter.t.sol @@ -1,10 +1,10 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity ^0.8.13; import "forge-std/Test.sol"; import "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; import "src/uniswap-v2/UniswapV2SwapAdapter.sol"; -import "interfaces/ISwapAdapterTypes.sol"; +import "src/interfaces/ISwapAdapterTypes.sol"; contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { UniswapV2SwapAdapter pairFunctions; From 7ffee9ac2e12d8e123b77be7c537a0e0e4b87af1 Mon Sep 17 00:00:00 2001 From: pistomat Date: Mon, 4 Dec 2023 13:50:02 +0100 Subject: [PATCH 05/16] Fix UniswapV2 tests --- .../logic/vm-integration/ethereum-solidity.md | 4 +- evm/src/balancer-v2/BalancerV2SwapAdapter.sol | 378 ++++++++++++++---- evm/src/interfaces/ISwapAdapter.sol | 7 +- evm/src/interfaces/ISwapAdapterTypes.sol | 17 +- evm/src/uniswap-v2/UniswapV2SwapAdapter.sol | 31 +- evm/test/UniswapV2SwapAdapter.t.sol | 19 +- 6 files changed, 347 insertions(+), 109 deletions(-) diff --git a/docs/logic/vm-integration/ethereum-solidity.md b/docs/logic/vm-integration/ethereum-solidity.md index 0946a56..9e8926d 100644 --- a/docs/logic/vm-integration/ethereum-solidity.md +++ b/docs/logic/vm-integration/ethereum-solidity.md @@ -93,7 +93,7 @@ function swap( bytes32 pairId, IERC20 sellToken, IERC20 buyToken, - SwapSide side, + OrderSide side, uint256 specifiedAmount ) external returns (Trade memory trade); ``` @@ -105,7 +105,7 @@ Retrieves the limits for each token. This method returns the maximum limits of a token that can be traded. The limit is reached when the change in the received amounts is zero or close to zero. If in doubt over estimate the limit. The swap function should not error with LimitExceeded if called with any amounts below the limit. ```solidity -function getLimits(bytes32 pairId, SwapSide side) +function getLimits(bytes32 pairId, OrderSide side) external returns (uint256[] memory); ``` diff --git a/evm/src/balancer-v2/BalancerV2SwapAdapter.sol b/evm/src/balancer-v2/BalancerV2SwapAdapter.sol index da182f6..1250cd9 100644 --- a/evm/src/balancer-v2/BalancerV2SwapAdapter.sol +++ b/evm/src/balancer-v2/BalancerV2SwapAdapter.sol @@ -1,66 +1,13 @@ // SPDX-License-Identifier: AGPL-3.0-or-later +pragma experimental ABIEncoderV2; pragma solidity ^0.8.13; import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; -interface IVault { - function getPoolTokens(bytes32 poolId) - external - view - returns ( - IERC20[] memory tokens, - uint256[] memory balances, - uint256 lastChangeBlock - ); - - function swap( - SingleSwap memory singleSwap, - FundManagement memory funds, - uint256 limit, - uint256 deadline - ) external payable returns (uint256); - - function queryBatchSwap( - SwapKind kind, - BatchSwapStep[] memory swaps, - IAsset[] memory assets, - FundManagement memory funds - ) external returns (int256[] memory assetDeltas); - - struct SingleSwap { - bytes32 poolId; - SwapKind kind; - IAsset assetIn; - IAsset assetOut; - uint256 amount; - bytes userData; - } - - struct BatchSwapStep { - bytes32 poolId; - uint256 assetInIndex; - uint256 assetOutIndex; - uint256 amount; - bytes userData; - } - - struct FundManagement { - address sender; - bool fromInternalBalance; - address payable recipient; - bool toInternalBalance; - } - - enum SwapKind { GIVEN_IN, GIVEN_OUT } -} - -interface IAsset is IERC20 { -} - contract BalancerV2SwapAdapter is ISwapAdapter { IVault immutable vault; - constructor(address vault_) { + constructor(address payable vault_) { vault = IVault(vault_); } @@ -73,31 +20,59 @@ contract BalancerV2SwapAdapter is ISwapAdapter { revert NotImplemented("BalancerV2SwapAdapter.price"); } - function getPriceAt(uint256 amountIn, uint256 reserveIn, uint256 reserveOut) - internal - pure - returns (Fraction memory) - { - revert NotImplemented("BalancerV2SwapAdapter.getPriceAt"); - } - function swap( bytes32 pairId, IERC20 sellToken, IERC20 buyToken, - SwapSide side, + OrderSide side, uint256 specifiedAmount ) external override returns (Trade memory trade) { - revert NotImplemented("BalancerV2SwapAdapter.swap"); + if (side == OrderSide.Sell) { + sellToken.approve(address(vault), specifiedAmount); + } else { + buyToken.approve(address(vault), type(uint256).max); + } + uint256 gasBefore = gasleft(); + trade.receivedAmount = vault.swap( + IVault.SingleSwap({ + poolId: pairId, + kind: side == OrderSide.Sell + ? IVault.SwapKind.GIVEN_IN + : IVault.SwapKind.GIVEN_OUT, + assetIn: address(sellToken), + assetOut: address(buyToken), + amount: specifiedAmount, + userData: "" + }), + IVault.FundManagement({ + sender: msg.sender, + fromInternalBalance: false, + recipient: payable(msg.sender), + toInternalBalance: false + }), + 0, + block.number + ); + trade.gasUsed = gasBefore - gasleft(); + trade.price = Fraction(0, 1); // Without the price function return 0. } - function getLimits(bytes32 pairId, SwapSide side) + function getLimits(bytes32 pairId, IERC20 sellToken, IERC20 buyToken) external view override returns (uint256[] memory limits) { - (, limits, ) = vault.getPoolTokens(pairId); + (IERC20[] memory tokens, uint256[] memory balances,) = + vault.getPoolTokens(pairId); + for (uint256 i = 0; i < tokens.length; i++) { + if (tokens[i] == sellToken) { + limits[0] = balances[i]; + } + if (tokens[i] == buyToken) { + limits[1] = balances[i]; + } + } } function getCapabilities(bytes32, IERC20, IERC20) @@ -106,10 +81,9 @@ contract BalancerV2SwapAdapter is ISwapAdapter { override returns (Capability[] memory capabilities) { - capabilities = new Capability[](3); - capabilities[0] = Capability.SellSide; - capabilities[1] = Capability.BuySide; - capabilities[2] = Capability.PriceFunction; + capabilities = new Capability[](2); + capabilities[0] = Capability.SellOrder; + capabilities[1] = Capability.BuyOrder; } function getTokens(bytes32 pairId) @@ -118,10 +92,10 @@ contract BalancerV2SwapAdapter is ISwapAdapter { override returns (IERC20[] memory tokens) { - revert NotImplemented("BalancerV2SwapAdapter.getTokens"); + (tokens,,) = vault.getPoolTokens(pairId); } - /// @dev Balancer V2 does not support listing pools. + /// @dev Balancer V2 does not support enumerating pools, they have to be indexed off-chain. function getPoolIds(uint256 offset, uint256 limit) external view @@ -131,3 +105,261 @@ contract BalancerV2SwapAdapter is ISwapAdapter { revert NotImplemented("BalancerV2SwapAdapter.getPoolIds"); } } + +interface IVault { + struct BatchSwapStep { + bytes32 poolId; + uint256 assetInIndex; + uint256 assetOutIndex; + uint256 amount; + bytes userData; + } + + struct FundManagement { + address sender; + bool fromInternalBalance; + address recipient; + bool toInternalBalance; + } + + struct ExitPoolRequest { + address[] assets; + uint256[] minAmountsOut; + bytes userData; + bool toInternalBalance; + } + + struct JoinPoolRequest { + address[] assets; + uint256[] maxAmountsIn; + bytes userData; + bool fromInternalBalance; + } + + struct PoolBalanceOp { + SwapKind kind; + bytes32 poolId; + address token; + uint256 amount; + } + + struct UserBalanceOp { + SwapKind kind; + address asset; + uint256 amount; + address sender; + address recipient; + } + + struct SingleSwap { + bytes32 poolId; + SwapKind kind; + address assetIn; + address assetOut; + uint256 amount; + bytes userData; + } + + event AuthorizerChanged(address indexed newAuthorizer); + event ExternalBalanceTransfer( + address indexed token, + address indexed sender, + address recipient, + uint256 amount + ); + event FlashLoan( + address indexed recipient, + address indexed token, + uint256 amount, + uint256 feeAmount + ); + event InternalBalanceChanged( + address indexed user, + address indexed token, + int256 delta + ); + event PausedStateChanged(bool paused); + event PoolBalanceChanged( + bytes32 indexed poolId, + address indexed liquidityProvider, + address[] tokens, + int256[] deltas, + uint256[] protocolFeeAmounts + ); + event PoolBalanceManaged( + bytes32 indexed poolId, + address indexed assetManager, + address indexed token, + int256 cashDelta, + int256 managedDelta + ); + event PoolRegistered( + bytes32 indexed poolId, + address indexed poolAddress, + uint8 specialization + ); + event RelayerApprovalChanged( + address indexed relayer, + address indexed sender, + bool approved + ); + event Swap( + bytes32 indexed poolId, + address indexed tokenIn, + address indexed tokenOut, + uint256 amountIn, + uint256 amountOut + ); + event TokensDeregistered(bytes32 indexed poolId, address[] tokens); + event TokensRegistered( + bytes32 indexed poolId, + address[] tokens, + address[] assetManagers + ); + + function WETH() external view returns (address); + + function batchSwap( + SwapKind kind, + BatchSwapStep[] memory swaps, + address[] memory assets, + FundManagement memory funds, + int256[] memory limits, + uint256 deadline + ) external payable returns (int256[] memory assetDeltas); + + function deregisterTokens(bytes32 poolId, address[] memory tokens) external; + + function exitPool( + bytes32 poolId, + address sender, + address recipient, + ExitPoolRequest memory request + ) external; + + function flashLoan( + address recipient, + address[] memory tokens, + uint256[] memory amounts, + bytes memory userData + ) external; + + function getActionId(bytes4 selector) external view returns (bytes32); + + function getAuthorizer() external view returns (address); + + function getDomainSeparator() external view returns (bytes32); + + function getInternalBalance(address user, address[] memory tokens) + external + view + returns (uint256[] memory balances); + + function getNextNonce(address user) external view returns (uint256); + + function getPausedState() + external + view + returns ( + bool paused, + uint256 pauseWindowEndTime, + uint256 bufferPeriodEndTime + ); + + function getPool(bytes32 poolId) external view returns (address, uint8); + + function getPoolTokenInfo(bytes32 poolId, address token) + external + view + returns ( + uint256 cash, + uint256 managed, + uint256 lastChangeBlock, + address assetManager + ); + + function getProtocolFeesCollector() external view returns (address); + + function hasApprovedRelayer(address user, address relayer) + external + view + returns (bool); + + function joinPool( + bytes32 poolId, + address sender, + address recipient, + JoinPoolRequest memory request + ) external payable; + + function managePoolBalance(PoolBalanceOp[] memory ops) external; + + function manageUserBalance(UserBalanceOp[] memory ops) external payable; + + function queryBatchSwap( + SwapKind kind, + BatchSwapStep[] memory swaps, + address[] memory assets, + FundManagement memory funds + ) external returns (int256[] memory); + + function registerPool(uint8 specialization) external returns (bytes32); + + function registerTokens( + bytes32 poolId, + address[] memory tokens, + address[] memory assetManagers + ) external; + + function setAuthorizer(address newAuthorizer) external; + + function setPaused(bool paused) external; + + function setRelayerApproval( + address sender, + address relayer, + bool approved + ) external; + + /** + * @dev Performs a swap with a single Pool. + * + * If the swap is 'given in' (the number of tokens to send to the Pool is + * known), it returns the amount of tokens + * taken from the Pool, which must be greater than or equal to `limit`. + * + * If the swap is 'given out' (the number of tokens to take from the Pool is + * known), it returns the amount of tokens + * sent to the Pool, which must be less than or equal to `limit`. + * + * Internal Balance usage and the recipient are determined by the `funds` + * struct. + * + * Emits a `Swap` event. + */ + function swap( + SingleSwap memory singleSwap, + FundManagement memory funds, + uint256 limit, + uint256 deadline + ) external payable returns (uint256); + + receive() external payable; + + function getPoolTokens(bytes32 poolId) + external + view + returns ( + IERC20[] memory tokens, + uint256[] memory balances, + uint256 lastChangeBlock + ); + + enum SwapKind + { + /// The number of tokens to send to the Pool is known + GIVEN_IN, + /// The number of tokens to take from the Pool is known + GIVEN_OUT + } +} diff --git a/evm/src/interfaces/ISwapAdapter.sol b/evm/src/interfaces/ISwapAdapter.sol index bc7ebe4..f93bed6 100644 --- a/evm/src/interfaces/ISwapAdapter.sol +++ b/evm/src/interfaces/ISwapAdapter.sol @@ -60,7 +60,7 @@ interface ISwapAdapter is ISwapAdapterTypes { bytes32 pairId, IERC20 sellToken, IERC20 buyToken, - SwapSide side, + OrderSide side, uint256 specifiedAmount ) external returns (Trade memory trade); @@ -71,9 +71,10 @@ interface ISwapAdapter is ISwapAdapterTypes { /// swap function should not error with `LimitExceeded` if called with /// amounts below the limit. /// @param pairId The ID of the trading pair. - /// @param side The side of the trade (Sell or Buy). + /// @param sellToken The token being sold. + /// @param buyToken The token being bought. /// @return limits An array of limits. - function getLimits(bytes32 pairId, SwapSide side) + function getLimits(bytes32 pairId, IERC20 sellToken, IERC20 buyToken) external returns (uint256[] memory limits); diff --git a/evm/src/interfaces/ISwapAdapterTypes.sol b/evm/src/interfaces/ISwapAdapterTypes.sol index d6a5f73..0fa0f84 100644 --- a/evm/src/interfaces/ISwapAdapterTypes.sol +++ b/evm/src/interfaces/ISwapAdapterTypes.sol @@ -4,10 +4,10 @@ pragma solidity ^0.8.13; import {IERC20} from "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; interface ISwapAdapterTypes { - /// @dev The SwapSide enum represents possible sides of a trade: Sell or - /// Buy. E.g. if SwapSide is Sell, the sell amount is interpreted to be + /// @dev The OrderSide enum represents possible sides of a trade: Sell or + /// Buy. E.g. if OrderSide is Sell, the sell amount is interpreted to be /// fixed. - enum SwapSide { + enum OrderSide { Sell, Buy } @@ -16,10 +16,10 @@ interface ISwapAdapterTypes { /// pair. enum Capability { Unset, - // Support SwapSide.Sell values (required) - SellSide, - // Support SwapSide.Buy values (optional) - BuySide, + // Support OrderSide.Sell values (required) + SellOrder, + // Support OrderSide.Buy values (optional) + BuyOrder, // Support evaluating the price function (optional) PriceFunction, // Support tokens that charge a fee on transfer (optional) @@ -44,7 +44,8 @@ interface ISwapAdapterTypes { /// @dev The Trade struct holds data about an executed trade. struct Trade { - // The amount received from the trade. + // If the side is sell, it is the amount of tokens sold. If the side is + // buy, it is the amount of tokens bought. uint256 receivedAmount; // The amount of gas used in the trade. uint256 gasUsed; diff --git a/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol b/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol index 9d4ef9e..dab63ac 100644 --- a/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol +++ b/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.13; import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; -uint256 constant RESERVE_LIMIT_FACTOR = 10; +uint256 constant RESERVE_LIMIT_FACTOR = 10; // TODO why is the factor so high? contract UniswapV2SwapAdapter is ISwapAdapter { IUniswapV2Factory immutable factory; @@ -54,11 +54,12 @@ contract UniswapV2SwapAdapter is ISwapAdapter { bytes32 pairId, IERC20 sellToken, IERC20 buyToken, - SwapSide side, + OrderSide side, uint256 specifiedAmount ) external override returns (Trade memory trade) { if (specifiedAmount == 0) { - return trade; // TODO: This returns Fraction(0, 0) instead of the expected zero Fraction(0, 1) + return trade; // TODO: This returns Fraction(0, 0) instead of the + // expected zero Fraction(0, 1) } IUniswapV2Pair pair = IUniswapV2Pair(address(bytes20(pairId))); @@ -71,7 +72,7 @@ contract UniswapV2SwapAdapter is ISwapAdapter { (r1, r0,) = pair.getReserves(); } uint256 gasBefore = gasleft(); - if (side == SwapSide.Sell) { + if (side == OrderSide.Sell) { trade.receivedAmount = sell(pair, sellToken, zero2one, r0, r1, specifiedAmount); } else { @@ -154,15 +155,18 @@ contract UniswapV2SwapAdapter is ISwapAdapter { if (amountIn == 0) { return 0; } - if (reserveIn == 0 || reserveOut == 0) { - revert Unavailable("At least one reserve is zero!"); + if (reserveIn == 0) { + revert Unavailable("reserveIn is zero"); + } + if (reserveOut == 0) { + revert Unavailable("reserveOut is zero"); } uint256 numerator = reserveIn * amountOut * 1000; uint256 denominator = (reserveOut - amountOut) * 997; amountIn = (numerator / denominator) + 1; } - function getLimits(bytes32 pairId, SwapSide side) + function getLimits(bytes32 pairId, IERC20 sellToken, IERC20 buyToken) external view override @@ -170,13 +174,10 @@ contract UniswapV2SwapAdapter is ISwapAdapter { { IUniswapV2Pair pair = IUniswapV2Pair(address(bytes20(pairId))); limits = new uint256[](2); - (uint256 r0, uint256 r1,) = pair.getReserves(); - if (side == SwapSide.Sell) { - limits[0] = r0 * RESERVE_LIMIT_FACTOR; - limits[1] = r1 * RESERVE_LIMIT_FACTOR; + if (sellToken < buyToken) { + (limits[0], limits[1],) = pair.getReserves(); } else { - limits[0] = r1 * RESERVE_LIMIT_FACTOR; - limits[1] = r0 * RESERVE_LIMIT_FACTOR; + (limits[1], limits[0],) = pair.getReserves(); } } @@ -187,8 +188,8 @@ contract UniswapV2SwapAdapter is ISwapAdapter { returns (Capability[] memory capabilities) { capabilities = new Capability[](3); - capabilities[0] = Capability.SellSide; - capabilities[1] = Capability.BuySide; + capabilities[0] = Capability.SellOrder; + capabilities[1] = Capability.BuyOrder; capabilities[2] = Capability.PriceFunction; } diff --git a/evm/test/UniswapV2SwapAdapter.t.sol b/evm/test/UniswapV2SwapAdapter.t.sol index f662972..116b920 100644 --- a/evm/test/UniswapV2SwapAdapter.t.sol +++ b/evm/test/UniswapV2SwapAdapter.t.sol @@ -21,7 +21,7 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { function testPriceFuzz(uint256 amount0, uint256 amount1) public { bytes32 pair = bytes32(bytes20(USDC_WETH_PAIR)); - uint256[] memory limits = pairFunctions.getLimits(pair, SwapSide.Sell); + uint256[] memory limits = pairFunctions.getLimits(pair, USDC, WETH); vm.assume(amount0 < limits[0]); vm.assume(amount1 < limits[0]); @@ -74,11 +74,14 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { function testSwapFuzz(uint256 amount, bool isBuy) public { bytes32 pair = bytes32(bytes20(USDC_WETH_PAIR)); - SwapSide side = SwapSide.Sell; + OrderSide side = OrderSide.Sell; + uint256[] memory limits; if (isBuy) { - side = SwapSide.Buy; + side = OrderSide.Buy; + limits = pairFunctions.getLimits(pair, WETH, USDC); + } else { + limits = pairFunctions.getLimits(pair, USDC, WETH); } - uint256[] memory limits = pairFunctions.getLimits(pair, side); vm.assume(amount < limits[0]); deal(address(USDC), address(this), amount); USDC.approve(address(pairFunctions), amount); @@ -87,10 +90,10 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { } function testSwapSellIncreasing() public { - executeIncreasingSwaps(SwapSide.Sell); + executeIncreasingSwaps(OrderSide.Sell); } - function executeIncreasingSwaps(SwapSide side) internal { + function executeIncreasingSwaps(OrderSide side) internal { bytes32 pair = bytes32(bytes20(USDC_WETH_PAIR)); uint256[] memory amounts = new uint256[](100); @@ -116,7 +119,7 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { } function testSwapBuyIncreasing() public { - executeIncreasingSwaps(SwapSide.Buy); + executeIncreasingSwaps(OrderSide.Buy); } function testGetCapabilities(bytes32 pair, address t0, address t1) public { @@ -128,6 +131,6 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { function testGetLimits() public { bytes32 pair = bytes32(bytes20(USDC_WETH_PAIR)); - uint256[] memory limits = pairFunctions.getLimits(pair, SwapSide.Sell); + uint256[] memory limits = pairFunctions.getLimits(pair, USDC, WETH); } } From 6c6525be2be34b32903eddd2a741ac27459ffbb8 Mon Sep 17 00:00:00 2001 From: pistomat Date: Mon, 4 Dec 2023 16:30:57 +0100 Subject: [PATCH 06/16] Balancer swap test --- evm/src/balancer-v2/BalancerV2SwapAdapter.sol | 124 ++++++++++++++---- evm/src/interfaces/ISwapAdapter.sol | 5 +- evm/src/uniswap-v2/UniswapV2SwapAdapter.sol | 8 +- evm/test/BalancerV2SwapAdapter.t.sol | 112 ++++++++++++++++ evm/test/UniswapV2SwapAdapter.t.sol | 14 +- 5 files changed, 220 insertions(+), 43 deletions(-) diff --git a/evm/src/balancer-v2/BalancerV2SwapAdapter.sol b/evm/src/balancer-v2/BalancerV2SwapAdapter.sol index 1250cd9..f545269 100644 --- a/evm/src/balancer-v2/BalancerV2SwapAdapter.sol +++ b/evm/src/balancer-v2/BalancerV2SwapAdapter.sol @@ -4,6 +4,9 @@ pragma solidity ^0.8.13; import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; +uint256 constant RESERVE_LIMIT_FACTOR = 10; // TODO why is the factor so high? +uint256 constant SWAP_DEADLINE_SEC = 1000; + contract BalancerV2SwapAdapter is ISwapAdapter { IVault immutable vault; @@ -11,12 +14,57 @@ contract BalancerV2SwapAdapter is ISwapAdapter { vault = IVault(vault_); } - function price( + function priceSingle( bytes32 pairId, IERC20 sellToken, IERC20 buyToken, - uint256[] memory sellAmounts - ) external view override returns (Fraction[] memory prices) { + uint256 sellAmount + ) public returns (Fraction memory calculatedPrice) { + IVault.BatchSwapStep[] memory swapSteps = new IVault.BatchSwapStep[](1); + swapSteps[0] = IVault.BatchSwapStep({ + poolId: pairId, + assetInIndex: 0, + assetOutIndex: 1, + amount: sellAmount, + userData: "" + }); + address[] memory assets = new address[](2); + assets[0] = address(sellToken); + assets[1] = address(buyToken); + IVault.FundManagement memory funds = IVault.FundManagement({ + sender: msg.sender, + fromInternalBalance: false, + recipient: payable(msg.sender), + toInternalBalance: false + }); + + // assetDeltas correspond to the assets array + int256[] memory assetDeltas = new int256[](2); + assetDeltas = vault.queryBatchSwap( + IVault.SwapKind.GIVEN_IN, swapSteps, assets, funds + ); + + calculatedPrice = Fraction(uint256(assetDeltas[1]), sellAmount); + } + + function priceBatch( + bytes32 pairId, + IERC20 sellToken, + IERC20 buyToken, + uint256[] memory specifiedAmounts + ) external returns (Fraction[] memory calculatedPrices) { + for (uint256 i = 0; i < specifiedAmounts.length; i++) { + calculatedPrices[i] = + priceSingle(pairId, sellToken, buyToken, specifiedAmounts[i]); + } + } + + function price(bytes32, IERC20, IERC20, uint256[] memory) + external + pure + override + returns (Fraction[] memory) + { revert NotImplemented("BalancerV2SwapAdapter.price"); } @@ -44,14 +92,15 @@ contract BalancerV2SwapAdapter is ISwapAdapter { amount: specifiedAmount, userData: "" }), + // This contract is not an approved relayer (yet), so the sender and recipient cannot be msg.sender IVault.FundManagement({ - sender: msg.sender, + sender: address(this), fromInternalBalance: false, - recipient: payable(msg.sender), + recipient: payable(address(this)), toInternalBalance: false }), 0, - block.number + block.timestamp + SWAP_DEADLINE_SEC ); trade.gasUsed = gasBefore - gasleft(); trade.price = Fraction(0, 1); // Without the price function return 0. @@ -63,14 +112,16 @@ contract BalancerV2SwapAdapter is ISwapAdapter { override returns (uint256[] memory limits) { + limits = new uint256[](2); (IERC20[] memory tokens, uint256[] memory balances,) = vault.getPoolTokens(pairId); + for (uint256 i = 0; i < tokens.length; i++) { if (tokens[i] == sellToken) { - limits[0] = balances[i]; + limits[0] = balances[i] * RESERVE_LIMIT_FACTOR; } if (tokens[i] == buyToken) { - limits[1] = balances[i]; + limits[1] = balances[i] * RESERVE_LIMIT_FACTOR; } } } @@ -95,12 +146,13 @@ contract BalancerV2SwapAdapter is ISwapAdapter { (tokens,,) = vault.getPoolTokens(pairId); } - /// @dev Balancer V2 does not support enumerating pools, they have to be indexed off-chain. - function getPoolIds(uint256 offset, uint256 limit) + /// @dev Balancer V2 does not support enumerating pools, they have to be + /// indexed off-chain. + function getPoolIds(uint256, uint256) external - view + pure override - returns (bytes32[] memory ids) + returns (bytes32[] memory) { revert NotImplemented("BalancerV2SwapAdapter.getPoolIds"); } @@ -174,9 +226,7 @@ interface IVault { uint256 feeAmount ); event InternalBalanceChanged( - address indexed user, - address indexed token, - int256 delta + address indexed user, address indexed token, int256 delta ); event PausedStateChanged(bool paused); event PoolBalanceChanged( @@ -199,9 +249,7 @@ interface IVault { uint8 specialization ); event RelayerApprovalChanged( - address indexed relayer, - address indexed sender, - bool approved + address indexed relayer, address indexed sender, bool approved ); event Swap( bytes32 indexed poolId, @@ -212,9 +260,7 @@ interface IVault { ); event TokensDeregistered(bytes32 indexed poolId, address[] tokens); event TokensRegistered( - bytes32 indexed poolId, - address[] tokens, - address[] assetManagers + bytes32 indexed poolId, address[] tokens, address[] assetManagers ); function WETH() external view returns (address); @@ -228,7 +274,8 @@ interface IVault { uint256 deadline ) external payable returns (int256[] memory assetDeltas); - function deregisterTokens(bytes32 poolId, address[] memory tokens) external; + function deregisterTokens(bytes32 poolId, address[] memory tokens) + external; function exitPool( bytes32 poolId, @@ -296,6 +343,28 @@ interface IVault { function manageUserBalance(UserBalanceOp[] memory ops) external payable; + /** + * @dev Simulates a call to `batchSwap`, returning an array of Vault asset + * deltas. Calls to `swap` cannot be + * simulated directly, but an equivalent `batchSwap` call can and will yield + * the exact same result. + * + * Each element in the array corresponds to the asset at the same index, and + * indicates the number of tokens (or ETH) + * the Vault would take from the sender (if positive) or send to the + * recipient (if negative). The arguments it + * receives are the same that an equivalent `batchSwap` call would receive. + * + * Unlike `batchSwap`, this function performs no checks on the sender or + * recipient field in the `funds` struct. + * This makes it suitable to be called by off-chain applications via + * eth_call without needing to hold tokens, + * approve them for the Vault, or even know a user's address. + * + * Note that this function is not 'view' (due to implementation details): + * the client code must explicitly execute + * eth_call instead of eth_sendTransaction. + */ function queryBatchSwap( SwapKind kind, BatchSwapStep[] memory swaps, @@ -315,11 +384,8 @@ interface IVault { function setPaused(bool paused) external; - function setRelayerApproval( - address sender, - address relayer, - bool approved - ) external; + function setRelayerApproval(address sender, address relayer, bool approved) + external; /** * @dev Performs a swap with a single Pool. @@ -345,7 +411,7 @@ interface IVault { ) external payable returns (uint256); receive() external payable; - + function getPoolTokens(bytes32 poolId) external view @@ -356,8 +422,8 @@ interface IVault { ); enum SwapKind + /// The number of tokens to send to the Pool is known { - /// The number of tokens to send to the Pool is known GIVEN_IN, /// The number of tokens to take from the Pool is known GIVEN_OUT diff --git a/evm/src/interfaces/ISwapAdapter.sol b/evm/src/interfaces/ISwapAdapter.sol index f93bed6..ed8b017 100644 --- a/evm/src/interfaces/ISwapAdapter.sol +++ b/evm/src/interfaces/ISwapAdapter.sol @@ -29,14 +29,15 @@ interface ISwapAdapter is ISwapAdapterTypes { /// @param pairId The ID of the trading pair. /// @param sellToken The token being sold. /// @param buyToken The token being bought. - /// @param sellAmounts The specified amounts used for price calculation. + /// @param specifiedAmounts The specified amounts used for price + /// calculation. /// @return prices array of prices as fractions corresponding to the /// provided amounts. function price( bytes32 pairId, IERC20 sellToken, IERC20 buyToken, - uint256[] memory sellAmounts + uint256[] memory specifiedAmounts ) external view returns (Fraction[] memory prices); /** diff --git a/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol b/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol index dab63ac..fad2924 100644 --- a/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol +++ b/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol @@ -16,9 +16,9 @@ contract UniswapV2SwapAdapter is ISwapAdapter { bytes32 pairId, IERC20 sellToken, IERC20 buyToken, - uint256[] memory sellAmounts + uint256[] memory specifiedAmounts ) external view override returns (Fraction[] memory prices) { - prices = new Fraction[](sellAmounts.length); + prices = new Fraction[](specifiedAmounts.length); IUniswapV2Pair pair = IUniswapV2Pair(address(bytes20(pairId))); uint112 r0; uint112 r1; @@ -28,8 +28,8 @@ contract UniswapV2SwapAdapter is ISwapAdapter { (r1, r0,) = pair.getReserves(); } - for (uint256 i = 0; i < sellAmounts.length; i++) { - prices[i] = getPriceAt(sellAmounts[i], r0, r1); + for (uint256 i = 0; i < specifiedAmounts.length; i++) { + prices[i] = getPriceAt(specifiedAmounts[i], r0, r1); } } diff --git a/evm/test/BalancerV2SwapAdapter.t.sol b/evm/test/BalancerV2SwapAdapter.t.sol index 64b955a..491bde5 100644 --- a/evm/test/BalancerV2SwapAdapter.t.sol +++ b/evm/test/BalancerV2SwapAdapter.t.sol @@ -1 +1,113 @@ // SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import { + BalancerV2SwapAdapter, + IERC20, + IVault +} from "src/balancer-v2/BalancerV2SwapAdapter.sol"; +import {ISwapAdapterTypes} from "src/interfaces/ISwapAdapterTypes.sol"; + +contract BalancerV2SwapAdapterTest is Test, ISwapAdapterTypes { + IVault constant balancerV2Vault = + IVault(payable(0xBA12222222228d8Ba445958a75a0704d566BF2C8)); + BalancerV2SwapAdapter adapter; + + IERC20 constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + IERC20 constant BAL = IERC20(0xba100000625a3754423978a60c9317c58a424e3D); + address constant B_80BAL_20WETH = 0x5c6Ee304399DBdB9C8Ef030aB642B10820DB8F56; + bytes32 constant B_80BAL_20WETH_POOL_ID = + 0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014; + + function setUp() public { + uint256 forkBlock = 17000000; + vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); + + adapter = new BalancerV2SwapAdapter(payable(address(balancerV2Vault))); + + vm.label(address(balancerV2Vault), "IVault"); + vm.label(address(adapter), "BalancerV2SwapAdapter"); + vm.label(address(WETH), "WETH"); + vm.label(address(BAL), "BAL"); + vm.label(address(B_80BAL_20WETH), "B_80BAL_20WETH"); + } + + function testPrice() public { + uint256[] memory amounts = new uint256[](2); + amounts[0] = 100; + amounts[1] = 200; + vm.expectRevert( + abi.encodeWithSelector( + NotImplemented.selector, "BalancerV2SwapAdapter.price" + ) + ); + adapter.price(B_80BAL_20WETH_POOL_ID, BAL, WETH, amounts); + } + + // function testPriceSingleFuzz(uint256 amount) public { + // uint256[] memory limits = adapter.getLimits(B_80BAL_20WETH_POOL_ID, BAL, WETH); + // vm.assume(amount < limits[0]); + // vm.assume(amount > 100); + + // uint256[] memory amounts = new uint256[](1); + // amounts[0] = amount; + + // Fraction memory price = + // adapter.priceSingle(B_80BAL_20WETH_POOL_ID, BAL, WETH, amount); + + // console.log("price.numerator: ", price.numerator); + // console.log("price.denominator: ", price.denominator); + + // assertGt(price.numerator, 0); + // } + + function testSwapFuzz() public { + // uint256[] memory limits = adapter.getLimits(B_80BAL_20WETH_POOL_ID, BAL, WETH); + // vm.assume(amount < limits[0]); + // vm.assume(amount > 1000000); // TODO getting reverts for amounts near zero + uint256 amount = 100000; + + OrderSide side = OrderSide.Sell; + + deal(address(BAL), address(adapter), amount); + // BAL.approve(address(adapter), amount); + // BAL.approve(address(balancerV2Vault), amount); + + adapter.swap(B_80BAL_20WETH_POOL_ID, BAL, WETH, side, amount); + } + + function testGetLimits() public view { + uint256[] memory limits = + adapter.getLimits(B_80BAL_20WETH_POOL_ID, BAL, WETH); + + assert(limits.length == 2); + assert(limits[0] > 0); + assert(limits[1] > 0); + } + + function testGetCapabilitiesFuzz(bytes32 pair, address t0, address t1) public { + Capability[] memory res = + adapter.getCapabilities(pair, IERC20(t0), IERC20(t1)); + + assertEq(res.length, 2); + assertEq(uint256(res[0]), uint256(Capability.SellOrder)); + assertEq(uint256(res[1]), uint256(Capability.BuyOrder)); + } + + function testGetTokens() public { + IERC20[] memory tokens = adapter.getTokens(B_80BAL_20WETH_POOL_ID); + + assertEq(address(tokens[0]), address(BAL)); + assertEq(address(tokens[1]), address(WETH)); + } + + function testGetPoolIds() public { + vm.expectRevert( + abi.encodeWithSelector( + NotImplemented.selector, "BalancerV2SwapAdapter.getPoolIds" + ) + ); + adapter.getPoolIds(100, 200); + } +} diff --git a/evm/test/UniswapV2SwapAdapter.t.sol b/evm/test/UniswapV2SwapAdapter.t.sol index 116b920..9a89e4f 100644 --- a/evm/test/UniswapV2SwapAdapter.t.sol +++ b/evm/test/UniswapV2SwapAdapter.t.sol @@ -74,15 +74,11 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { function testSwapFuzz(uint256 amount, bool isBuy) public { bytes32 pair = bytes32(bytes20(USDC_WETH_PAIR)); - OrderSide side = OrderSide.Sell; - uint256[] memory limits; - if (isBuy) { - side = OrderSide.Buy; - limits = pairFunctions.getLimits(pair, WETH, USDC); - } else { - limits = pairFunctions.getLimits(pair, USDC, WETH); - } + OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell; + + uint256[] memory limits = pairFunctions.getLimits(pair, USDC, WETH); vm.assume(amount < limits[0]); + deal(address(USDC), address(this), amount); USDC.approve(address(pairFunctions), amount); @@ -132,5 +128,7 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { function testGetLimits() public { bytes32 pair = bytes32(bytes20(USDC_WETH_PAIR)); uint256[] memory limits = pairFunctions.getLimits(pair, USDC, WETH); + + assertEq(limits.length, 2); } } From f581b0d958e301083038c819f6158018ac638211 Mon Sep 17 00:00:00 2001 From: pistomat Date: Tue, 5 Dec 2023 14:32:35 +0100 Subject: [PATCH 07/16] Fix getAmountIn bug always returning 0 --- evm/src/uniswap-v2/UniswapV2SwapAdapter.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol b/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol index fad2924..9329bc8 100644 --- a/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol +++ b/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol @@ -152,7 +152,7 @@ contract UniswapV2SwapAdapter is ISwapAdapter { uint256 reserveIn, uint256 reserveOut ) internal pure returns (uint256 amountIn) { - if (amountIn == 0) { + if (amountOut == 0) { return 0; } if (reserveIn == 0) { From f3c0835552863b568829ffb0dbe59705b97262db Mon Sep 17 00:00:00 2001 From: pistomat Date: Tue, 5 Dec 2023 17:06:29 +0100 Subject: [PATCH 08/16] Fix Uniswap tests and work on balancer test --- evm/src/balancer-v2/BalancerV2SwapAdapter.sol | 60 ++++++++++++++++--- evm/src/interfaces/ISwapAdapterTypes.sol | 2 +- evm/src/uniswap-v2/UniswapV2SwapAdapter.sol | 21 ++++--- evm/test/BalancerV2SwapAdapter.t.sol | 43 +++++++++---- evm/test/UniswapV2SwapAdapter.t.sol | 55 +++++++++++++---- 5 files changed, 140 insertions(+), 41 deletions(-) diff --git a/evm/src/balancer-v2/BalancerV2SwapAdapter.sol b/evm/src/balancer-v2/BalancerV2SwapAdapter.sol index f545269..9cbf25c 100644 --- a/evm/src/balancer-v2/BalancerV2SwapAdapter.sol +++ b/evm/src/balancer-v2/BalancerV2SwapAdapter.sol @@ -44,9 +44,43 @@ contract BalancerV2SwapAdapter is ISwapAdapter { IVault.SwapKind.GIVEN_IN, swapSteps, assets, funds ); + // TODO: the delta of buyToken is negative, so we need to flip the sign calculatedPrice = Fraction(uint256(assetDeltas[1]), sellAmount); } + function getSellAmount( + bytes32 pairId, + IERC20 sellToken, + IERC20 buyToken, + uint256 buyAmount + ) public returns (uint256 sellAmount) { + IVault.BatchSwapStep[] memory swapSteps = new IVault.BatchSwapStep[](1); + swapSteps[0] = IVault.BatchSwapStep({ + poolId: pairId, + assetInIndex: 0, + assetOutIndex: 1, + amount: buyAmount, + userData: "" + }); + address[] memory assets = new address[](2); + assets[0] = address(sellToken); + assets[1] = address(buyToken); + IVault.FundManagement memory funds = IVault.FundManagement({ + sender: msg.sender, + fromInternalBalance: false, + recipient: payable(msg.sender), + toInternalBalance: false + }); + + // assetDeltas correspond to the assets array + int256[] memory assetDeltas = new int256[](2); + assetDeltas = vault.queryBatchSwap( + IVault.SwapKind.GIVEN_OUT, swapSteps, assets, funds + ); + + sellAmount = uint256(assetDeltas[0]); + } + function priceBatch( bytes32 pairId, IERC20 sellToken, @@ -75,31 +109,39 @@ contract BalancerV2SwapAdapter is ISwapAdapter { OrderSide side, uint256 specifiedAmount ) external override returns (Trade memory trade) { + uint256 sellAmount; + IVault.SwapKind kind; + uint256 limit; // TODO set this slippage limit properly if (side == OrderSide.Sell) { - sellToken.approve(address(vault), specifiedAmount); + kind = IVault.SwapKind.GIVEN_IN; + sellAmount = specifiedAmount; + limit = 0; } else { - buyToken.approve(address(vault), type(uint256).max); + kind = IVault.SwapKind.GIVEN_OUT; + sellAmount = getSellAmount(pairId, sellToken, buyToken, specifiedAmount); + limit = type(uint256).max; } + + sellToken.transferFrom(msg.sender, address(this), sellAmount); + sellToken.approve(address(vault), sellAmount); + uint256 gasBefore = gasleft(); - trade.receivedAmount = vault.swap( + trade.calculatedAmount = vault.swap( IVault.SingleSwap({ poolId: pairId, - kind: side == OrderSide.Sell - ? IVault.SwapKind.GIVEN_IN - : IVault.SwapKind.GIVEN_OUT, + kind: kind, assetIn: address(sellToken), assetOut: address(buyToken), amount: specifiedAmount, userData: "" }), - // This contract is not an approved relayer (yet), so the sender and recipient cannot be msg.sender IVault.FundManagement({ sender: address(this), fromInternalBalance: false, - recipient: payable(address(this)), + recipient: msg.sender, toInternalBalance: false }), - 0, + limit, block.timestamp + SWAP_DEADLINE_SEC ); trade.gasUsed = gasBefore - gasleft(); diff --git a/evm/src/interfaces/ISwapAdapterTypes.sol b/evm/src/interfaces/ISwapAdapterTypes.sol index 0fa0f84..81767e0 100644 --- a/evm/src/interfaces/ISwapAdapterTypes.sol +++ b/evm/src/interfaces/ISwapAdapterTypes.sol @@ -46,7 +46,7 @@ interface ISwapAdapterTypes { struct Trade { // If the side is sell, it is the amount of tokens sold. If the side is // buy, it is the amount of tokens bought. - uint256 receivedAmount; + uint256 calculatedAmount; // The amount of gas used in the trade. uint256 gasUsed; // The price of the pair after the trade. For zero use Fraction(0, 1). diff --git a/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol b/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol index 9329bc8..a413947 100644 --- a/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol +++ b/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.13; import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; -uint256 constant RESERVE_LIMIT_FACTOR = 10; // TODO why is the factor so high? +uint256 constant RESERVE_LIMIT_FACTOR = 2; // TODO why is the factor so high? contract UniswapV2SwapAdapter is ISwapAdapter { IUniswapV2Factory immutable factory; @@ -73,10 +73,10 @@ contract UniswapV2SwapAdapter is ISwapAdapter { } uint256 gasBefore = gasleft(); if (side == OrderSide.Sell) { - trade.receivedAmount = + trade.calculatedAmount = sell(pair, sellToken, zero2one, r0, r1, specifiedAmount); } else { - trade.receivedAmount = + trade.calculatedAmount = buy(pair, sellToken, zero2one, r0, r1, specifiedAmount); } trade.gasUsed = gasBefore - gasleft(); @@ -90,11 +90,12 @@ contract UniswapV2SwapAdapter is ISwapAdapter { uint112 reserveIn, uint112 reserveOut, uint256 amount - ) internal returns (uint256 receivedAmount) { + ) internal returns (uint256 calculatedAmount) { address swapper = msg.sender; + uint256 amountOut = getAmountOut(amount, reserveIn, reserveOut); + // TODO: use safeTransferFrom sellToken.transferFrom(swapper, address(pair), amount); - uint256 amountOut = getAmountOut(amount, reserveIn, reserveOut); if (zero2one) { pair.swap(0, amountOut, swapper, ""); } else { @@ -129,9 +130,10 @@ contract UniswapV2SwapAdapter is ISwapAdapter { uint112 reserveIn, uint112 reserveOut, uint256 amountOut - ) internal returns (uint256 receivedAmount) { + ) internal returns (uint256 calculatedAmount) { address swapper = msg.sender; uint256 amount = getAmountIn(amountOut, reserveIn, reserveOut); + if (amount == 0) { return 0; } @@ -174,10 +176,13 @@ contract UniswapV2SwapAdapter is ISwapAdapter { { IUniswapV2Pair pair = IUniswapV2Pair(address(bytes20(pairId))); limits = new uint256[](2); + (uint256 r0, uint256 r1,) = pair.getReserves(); if (sellToken < buyToken) { - (limits[0], limits[1],) = pair.getReserves(); + limits[0] = r0 / RESERVE_LIMIT_FACTOR; + limits[1] = r1 / RESERVE_LIMIT_FACTOR; } else { - (limits[1], limits[0],) = pair.getReserves(); + limits[0] = r1 / RESERVE_LIMIT_FACTOR; + limits[1] = r0 / RESERVE_LIMIT_FACTOR; } } diff --git a/evm/test/BalancerV2SwapAdapter.t.sol b/evm/test/BalancerV2SwapAdapter.t.sol index 491bde5..4aa1e08 100644 --- a/evm/test/BalancerV2SwapAdapter.t.sol +++ b/evm/test/BalancerV2SwapAdapter.t.sol @@ -21,7 +21,7 @@ contract BalancerV2SwapAdapterTest is Test, ISwapAdapterTypes { 0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014; function setUp() public { - uint256 forkBlock = 17000000; + uint256 forkBlock = 18710000; vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); adapter = new BalancerV2SwapAdapter(payable(address(balancerV2Vault))); @@ -62,19 +62,40 @@ contract BalancerV2SwapAdapterTest is Test, ISwapAdapterTypes { // assertGt(price.numerator, 0); // } - function testSwapFuzz() public { - // uint256[] memory limits = adapter.getLimits(B_80BAL_20WETH_POOL_ID, BAL, WETH); - // vm.assume(amount < limits[0]); - // vm.assume(amount > 1000000); // TODO getting reverts for amounts near zero - uint256 amount = 100000; + function testSwapFuzz(uint256 specifiedAmount, bool isBuy) public { + OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell; + vm.assume(specifiedAmount > 0); - OrderSide side = OrderSide.Sell; + uint256[] memory limits = adapter.getLimits(B_80BAL_20WETH_POOL_ID, BAL, WETH); - deal(address(BAL), address(adapter), amount); - // BAL.approve(address(adapter), amount); - // BAL.approve(address(balancerV2Vault), amount); + if (side == OrderSide.Buy) { + vm.assume(specifiedAmount < limits[1]); - adapter.swap(B_80BAL_20WETH_POOL_ID, BAL, WETH, side, amount); + // sellAmount is not specified for buy orders + deal(address(BAL), address(this), type(uint256).max); + BAL.approve(address(adapter), type(uint256).max); + } + else { + vm.assume(specifiedAmount < limits[0]); + + deal(address(BAL), address(this), specifiedAmount); + BAL.approve(address(adapter), specifiedAmount); + } + + uint256 bal_balance = BAL.balanceOf(address(this)); + uint256 weth_balance = WETH.balanceOf(address(this)); + + Trade memory trade = adapter.swap(B_80BAL_20WETH_POOL_ID, BAL, WETH, side, specifiedAmount); + + if (trade.calculatedAmount > 0) { + if (side == OrderSide.Buy) { + assertEq(specifiedAmount, WETH.balanceOf(address(this)) - weth_balance); + assertEq(trade.calculatedAmount, bal_balance - BAL.balanceOf(address(this))); + } else { + assertEq(specifiedAmount, bal_balance - BAL.balanceOf(address(this))); + assertEq(trade.calculatedAmount, WETH.balanceOf(address(this)) - weth_balance); + } + } } function testGetLimits() public view { diff --git a/evm/test/UniswapV2SwapAdapter.t.sol b/evm/test/UniswapV2SwapAdapter.t.sol index 9a89e4f..c160dc7 100644 --- a/evm/test/UniswapV2SwapAdapter.t.sol +++ b/evm/test/UniswapV2SwapAdapter.t.sol @@ -17,6 +17,11 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); pairFunctions = new UniswapV2SwapAdapter(0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f); + + vm.label(address(pairFunctions), "UniswapV2SwapAdapter"); + vm.label(address(WETH), "WETH"); + vm.label(address(USDC), "USDC"); + vm.label(address(USDC_WETH_PAIR), "USDC_WETH_PAIR"); } function testPriceFuzz(uint256 amount0, uint256 amount1) public { @@ -72,17 +77,40 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { else return -1; } - function testSwapFuzz(uint256 amount, bool isBuy) public { - bytes32 pair = bytes32(bytes20(USDC_WETH_PAIR)); + function testSwapFuzz(uint256 specifiedAmount, bool isBuy) public { OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell; + bytes32 pair = bytes32(bytes20(USDC_WETH_PAIR)); uint256[] memory limits = pairFunctions.getLimits(pair, USDC, WETH); - vm.assume(amount < limits[0]); - - deal(address(USDC), address(this), amount); - USDC.approve(address(pairFunctions), amount); - pairFunctions.swap(pair, USDC, WETH, side, amount); + if (side == OrderSide.Buy) { + vm.assume(specifiedAmount < limits[1]); + + // sellAmount is not specified for buy orders + deal(address(USDC), address(this), type(uint256).max); + USDC.approve(address(pairFunctions), type(uint256).max); + } + else { + vm.assume(specifiedAmount < limits[0]); + + deal(address(USDC), address(this), specifiedAmount); + USDC.approve(address(pairFunctions), specifiedAmount); + } + + uint256 usdc_balance = USDC.balanceOf(address(this)); + uint256 weth_balance = WETH.balanceOf(address(this)); + + Trade memory trade = pairFunctions.swap(pair, USDC, WETH, side, specifiedAmount); + + if (trade.calculatedAmount > 0) { + if (side == OrderSide.Buy) { + assertEq(specifiedAmount, WETH.balanceOf(address(this)) - weth_balance); + assertEq(trade.calculatedAmount, usdc_balance - USDC.balanceOf(address(this))); + } else { + assertEq(specifiedAmount, usdc_balance - USDC.balanceOf(address(this))); + assertEq(trade.calculatedAmount, WETH.balanceOf(address(this)) - weth_balance); + } + } } function testSwapSellIncreasing() public { @@ -91,24 +119,27 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { function executeIncreasingSwaps(OrderSide side) internal { bytes32 pair = bytes32(bytes20(USDC_WETH_PAIR)); + uint256 iterations = 100; - uint256[] memory amounts = new uint256[](100); + uint256[] memory amounts = new uint256[](iterations); for (uint256 i = 0; i < 100; i++) { amounts[i] = 1000 * i * 10 ** 6; } - Trade[] memory trades = new Trade [](100); + Trade[] memory trades = new Trade[](iterations); uint256 beforeSwap; - for (uint256 i = 0; i < 100; i++) { + for (uint256 i = 0; i < iterations; i++) { beforeSwap = vm.snapshot(); + deal(address(USDC), address(this), amounts[i]); USDC.approve(address(pairFunctions), amounts[i]); + trades[i] = pairFunctions.swap(pair, USDC, WETH, side, amounts[i]); vm.revertTo(beforeSwap); } - for (uint256 i = 1; i < 99; i++) { - assertLe(trades[i].receivedAmount, trades[i + 1].receivedAmount); + for (uint256 i = 1; i < iterations - 1; i++) { + assertLe(trades[i].calculatedAmount, trades[i + 1].calculatedAmount); assertLe(trades[i].gasUsed, trades[i + 1].gasUsed); assertEq(compareFractions(trades[i].price, trades[i + 1].price), 1); } From 6097a32ab3d84801868a986c7e014e4125f16b15 Mon Sep 17 00:00:00 2001 From: pistomat Date: Wed, 6 Dec 2023 09:37:20 +0100 Subject: [PATCH 09/16] Fix BAL#304 MAX_IN_RATIO --- evm/src/balancer-v2/BalancerV2SwapAdapter.sol | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/evm/src/balancer-v2/BalancerV2SwapAdapter.sol b/evm/src/balancer-v2/BalancerV2SwapAdapter.sol index 9cbf25c..77b75c0 100644 --- a/evm/src/balancer-v2/BalancerV2SwapAdapter.sol +++ b/evm/src/balancer-v2/BalancerV2SwapAdapter.sol @@ -4,7 +4,8 @@ pragma solidity ^0.8.13; import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; -uint256 constant RESERVE_LIMIT_FACTOR = 10; // TODO why is the factor so high? +// Maximum Swap In/Out Ratio - 0.3 https://balancer.gitbook.io/balancer/core-concepts/protocol/limitations#v2-limits +uint256 constant RESERVE_LIMIT_FACTOR = 4; uint256 constant SWAP_DEADLINE_SEC = 1000; contract BalancerV2SwapAdapter is ISwapAdapter { @@ -160,10 +161,10 @@ contract BalancerV2SwapAdapter is ISwapAdapter { for (uint256 i = 0; i < tokens.length; i++) { if (tokens[i] == sellToken) { - limits[0] = balances[i] * RESERVE_LIMIT_FACTOR; + limits[0] = balances[i] / RESERVE_LIMIT_FACTOR; } if (tokens[i] == buyToken) { - limits[1] = balances[i] * RESERVE_LIMIT_FACTOR; + limits[1] = balances[i] / RESERVE_LIMIT_FACTOR; } } } From 223df970d36ca3228aaf46e9afeee5ba347ebbcb Mon Sep 17 00:00:00 2001 From: pistomat Date: Wed, 6 Dec 2023 15:06:55 +0100 Subject: [PATCH 10/16] Finish tests --- evm/src/balancer-v2/BalancerV2SwapAdapter.sol | 32 +++- evm/src/libraries/FractionMath.sol | 26 ++++ evm/test/BalancerV2SwapAdapter.t.sol | 141 +++++++++++++++--- evm/test/UniswapV2SwapAdapter.t.sol | 68 ++++----- 4 files changed, 203 insertions(+), 64 deletions(-) create mode 100644 evm/src/libraries/FractionMath.sol diff --git a/evm/src/balancer-v2/BalancerV2SwapAdapter.sol b/evm/src/balancer-v2/BalancerV2SwapAdapter.sol index 77b75c0..d7b5c2f 100644 --- a/evm/src/balancer-v2/BalancerV2SwapAdapter.sol +++ b/evm/src/balancer-v2/BalancerV2SwapAdapter.sol @@ -3,18 +3,31 @@ pragma experimental ABIEncoderV2; pragma solidity ^0.8.13; import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; +import "forge-std/Test.sol"; -// Maximum Swap In/Out Ratio - 0.3 https://balancer.gitbook.io/balancer/core-concepts/protocol/limitations#v2-limits -uint256 constant RESERVE_LIMIT_FACTOR = 4; +// Maximum Swap In/Out Ratio - 0.3 +// https://balancer.gitbook.io/balancer/core-concepts/protocol/limitations#v2-limits +uint256 constant RESERVE_LIMIT_FACTOR = 4; uint256 constant SWAP_DEADLINE_SEC = 1000; -contract BalancerV2SwapAdapter is ISwapAdapter { +contract BalancerV2SwapAdapter is ISwapAdapter, Test { IVault immutable vault; constructor(address payable vault_) { vault = IVault(vault_); } + /// @notice Calculate the price of the buy token in terms of the sell token. + /// @dev The resulting price is not scaled by the token decimals. + /// Also this function is not 'view' because Balancer V2 simulates the swap + /// and + /// then returns the amount diff in revert data. + /// @param pairId The ID of the trading pool. + /// @param sellToken The token being sold. + /// @param buyToken The token being bought. + /// @param sellAmount The amount of tokens being sold. + /// @return calculatedPrice The price of the buy token in terms of the sell + /// as a Fraction struct. function priceSingle( bytes32 pairId, IERC20 sellToken, @@ -44,9 +57,11 @@ contract BalancerV2SwapAdapter is ISwapAdapter { assetDeltas = vault.queryBatchSwap( IVault.SwapKind.GIVEN_IN, swapSteps, assets, funds ); - - // TODO: the delta of buyToken is negative, so we need to flip the sign - calculatedPrice = Fraction(uint256(assetDeltas[1]), sellAmount); + // assetDeltas[1] is the amount of tokens sent from the vault (i.e. + // bought), so the sign is negative, which means the sign should be + // flipped to get the price. + calculatedPrice = + Fraction(uint256(-assetDeltas[1]), uint256(assetDeltas[0])); } function getSellAmount( @@ -119,7 +134,8 @@ contract BalancerV2SwapAdapter is ISwapAdapter { limit = 0; } else { kind = IVault.SwapKind.GIVEN_OUT; - sellAmount = getSellAmount(pairId, sellToken, buyToken, specifiedAmount); + sellAmount = + getSellAmount(pairId, sellToken, buyToken, specifiedAmount); limit = type(uint256).max; } @@ -146,7 +162,7 @@ contract BalancerV2SwapAdapter is ISwapAdapter { block.timestamp + SWAP_DEADLINE_SEC ); trade.gasUsed = gasBefore - gasleft(); - trade.price = Fraction(0, 1); // Without the price function return 0. + trade.price = priceSingle(pairId, sellToken, buyToken, specifiedAmount); } function getLimits(bytes32 pairId, IERC20 sellToken, IERC20 buyToken) diff --git a/evm/src/libraries/FractionMath.sol b/evm/src/libraries/FractionMath.sol new file mode 100644 index 0000000..43f2ef7 --- /dev/null +++ b/evm/src/libraries/FractionMath.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.13; + +import "src/interfaces/ISwapAdapterTypes.sol"; + +library FractionMath { + /// @dev Compares two Fraction instances from ISwapAdapterTypes. + /// @param frac1 The first Fraction instance. + /// @param frac2 The second Fraction instance. + /// @return int8 Returns 0 if fractions are equal, 1 if frac1 is greater, -1 + /// if frac1 is lesser. + function compareFractions( + ISwapAdapterTypes.Fraction memory frac1, + ISwapAdapterTypes.Fraction memory frac2 + ) internal pure returns (int8) { + uint256 crossProduct1 = frac1.numerator * frac2.denominator; + uint256 crossProduct2 = frac2.numerator * frac1.denominator; + + // fractions are equal + if (crossProduct1 == crossProduct2) return 0; + // frac1 is greater than frac2 + else if (crossProduct1 > crossProduct2) return 1; + // frac1 is less than frac2 + else return -1; + } +} diff --git a/evm/test/BalancerV2SwapAdapter.t.sol b/evm/test/BalancerV2SwapAdapter.t.sol index 4aa1e08..ccf3447 100644 --- a/evm/test/BalancerV2SwapAdapter.t.sol +++ b/evm/test/BalancerV2SwapAdapter.t.sol @@ -8,8 +8,11 @@ import { IVault } from "src/balancer-v2/BalancerV2SwapAdapter.sol"; import {ISwapAdapterTypes} from "src/interfaces/ISwapAdapterTypes.sol"; +import {FractionMath} from "src/libraries/FractionMath.sol"; contract BalancerV2SwapAdapterTest is Test, ISwapAdapterTypes { + using FractionMath for Fraction; + IVault constant balancerV2Vault = IVault(payable(0xBA12222222228d8Ba445958a75a0704d566BF2C8)); BalancerV2SwapAdapter adapter; @@ -20,6 +23,8 @@ contract BalancerV2SwapAdapterTest is Test, ISwapAdapterTypes { bytes32 constant B_80BAL_20WETH_POOL_ID = 0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014; + uint256 constant TEST_ITERATIONS = 100; + function setUp() public { uint256 forkBlock = 18710000; vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); @@ -45,28 +50,50 @@ contract BalancerV2SwapAdapterTest is Test, ISwapAdapterTypes { adapter.price(B_80BAL_20WETH_POOL_ID, BAL, WETH, amounts); } - // function testPriceSingleFuzz(uint256 amount) public { - // uint256[] memory limits = adapter.getLimits(B_80BAL_20WETH_POOL_ID, BAL, WETH); - // vm.assume(amount < limits[0]); - // vm.assume(amount > 100); + function testPriceSingleFuzz() public { + uint256 specifiedAmount = 100 * 10 ** 18; + // Assume OrderSide.Sell + uint256[] memory limits = + adapter.getLimits(B_80BAL_20WETH_POOL_ID, BAL, WETH); - // uint256[] memory amounts = new uint256[](1); - // amounts[0] = amount; + vm.assume(specifiedAmount > 0); + vm.assume(specifiedAmount < limits[0]); - // Fraction memory price = - // adapter.priceSingle(B_80BAL_20WETH_POOL_ID, BAL, WETH, amount); + Fraction memory price = adapter.priceSingle( + B_80BAL_20WETH_POOL_ID, BAL, WETH, specifiedAmount + ); - // console.log("price.numerator: ", price.numerator); - // console.log("price.denominator: ", price.denominator); + assertGt(price.numerator, 0); + assertGt(price.denominator, 0); + } - // assertGt(price.numerator, 0); - // } + function testPriceDecreasing() public { + uint256[] memory amounts = new uint256[](TEST_ITERATIONS); + Fraction[] memory prices = new Fraction[](TEST_ITERATIONS); + + for (uint256 i = 0; i < TEST_ITERATIONS; i++) { + amounts[i] = 1000 * (i + 1) * 10 ** 18; + console.log("i = ", i); + console.log("amounts[i] = ", amounts[i]); + prices[i] = adapter.priceSingle( + B_80BAL_20WETH_POOL_ID, BAL, WETH, amounts[i] + ); + console.log("prices = ", prices[i].numerator, prices[i].denominator); + } + + for (uint256 i = 0; i < TEST_ITERATIONS - 1; i++) { + assertEq(prices[i].compareFractions(prices[i + 1]), 1); + assertGt(prices[i].denominator, 0); + assertGt(prices[i + 1].denominator, 0); + } + } function testSwapFuzz(uint256 specifiedAmount, bool isBuy) public { OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell; vm.assume(specifiedAmount > 0); - uint256[] memory limits = adapter.getLimits(B_80BAL_20WETH_POOL_ID, BAL, WETH); + uint256[] memory limits = + adapter.getLimits(B_80BAL_20WETH_POOL_ID, BAL, WETH); if (side == OrderSide.Buy) { vm.assume(specifiedAmount < limits[1]); @@ -74,30 +101,98 @@ contract BalancerV2SwapAdapterTest is Test, ISwapAdapterTypes { // sellAmount is not specified for buy orders deal(address(BAL), address(this), type(uint256).max); BAL.approve(address(adapter), type(uint256).max); - } - else { + } else { vm.assume(specifiedAmount < limits[0]); deal(address(BAL), address(this), specifiedAmount); BAL.approve(address(adapter), specifiedAmount); } - + uint256 bal_balance = BAL.balanceOf(address(this)); uint256 weth_balance = WETH.balanceOf(address(this)); - Trade memory trade = adapter.swap(B_80BAL_20WETH_POOL_ID, BAL, WETH, side, specifiedAmount); + Trade memory trade = adapter.swap( + B_80BAL_20WETH_POOL_ID, BAL, WETH, side, specifiedAmount + ); if (trade.calculatedAmount > 0) { if (side == OrderSide.Buy) { - assertEq(specifiedAmount, WETH.balanceOf(address(this)) - weth_balance); - assertEq(trade.calculatedAmount, bal_balance - BAL.balanceOf(address(this))); + assertEq( + specifiedAmount, + WETH.balanceOf(address(this)) - weth_balance + ); + assertEq( + trade.calculatedAmount, + bal_balance - BAL.balanceOf(address(this)) + ); } else { - assertEq(specifiedAmount, bal_balance - BAL.balanceOf(address(this))); - assertEq(trade.calculatedAmount, WETH.balanceOf(address(this)) - weth_balance); + assertEq( + specifiedAmount, bal_balance - BAL.balanceOf(address(this)) + ); + assertEq( + trade.calculatedAmount, + WETH.balanceOf(address(this)) - weth_balance + ); } } } + function testSwapSellIncreasing() public { + uint256[] memory amounts = new uint256[](TEST_ITERATIONS); + Trade[] memory trades = new Trade[](TEST_ITERATIONS); + + for (uint256 i = 0; i < TEST_ITERATIONS; i++) { + amounts[i] = 1000 * (i + 1) * 10 ** 18; + + uint256 beforeSwap = vm.snapshot(); + + deal(address(BAL), address(this), amounts[i]); + BAL.approve(address(adapter), amounts[i]); + trades[i] = adapter.swap( + B_80BAL_20WETH_POOL_ID, BAL, WETH, OrderSide.Sell, amounts[i] + ); + + vm.revertTo(beforeSwap); + } + + for (uint256 i = 0; i < TEST_ITERATIONS - 1; i++) { + assertLe(trades[i].calculatedAmount, trades[i + 1].calculatedAmount); + assertLe(trades[i].gasUsed, trades[i + 1].gasUsed); + assertEq(trades[i].price.compareFractions(trades[i + 1].price), 1); + } + } + + function testSwapBuyIncreasing() public { + uint256[] memory amounts = new uint256[](TEST_ITERATIONS); + Trade[] memory trades = new Trade[](TEST_ITERATIONS); + + for (uint256 i = 0; i < TEST_ITERATIONS; i++) { + amounts[i] = 10 * (i + 1) * 10 ** 18; + + uint256 beforeSwap = vm.snapshot(); + + Fraction memory price = adapter.priceSingle( + B_80BAL_20WETH_POOL_ID, BAL, WETH, amounts[i] + ); + uint256 amountIn = + (amounts[i] * price.denominator / price.numerator) * 2; + + deal(address(BAL), address(this), amountIn); + BAL.approve(address(adapter), amountIn); + trades[i] = adapter.swap( + B_80BAL_20WETH_POOL_ID, BAL, WETH, OrderSide.Buy, amounts[i] + ); + + vm.revertTo(beforeSwap); + } + + for (uint256 i = 0; i < TEST_ITERATIONS - 1; i++) { + assertLe(trades[i].calculatedAmount, trades[i + 1].calculatedAmount); + assertLe(trades[i].gasUsed, trades[i + 1].gasUsed); + assertEq(trades[i].price.compareFractions(trades[i + 1].price), 1); + } + } + function testGetLimits() public view { uint256[] memory limits = adapter.getLimits(B_80BAL_20WETH_POOL_ID, BAL, WETH); @@ -107,7 +202,9 @@ contract BalancerV2SwapAdapterTest is Test, ISwapAdapterTypes { assert(limits[1] > 0); } - function testGetCapabilitiesFuzz(bytes32 pair, address t0, address t1) public { + function testGetCapabilitiesFuzz(bytes32 pair, address t0, address t1) + public + { Capability[] memory res = adapter.getCapabilities(pair, IERC20(t0), IERC20(t1)); diff --git a/evm/test/UniswapV2SwapAdapter.t.sol b/evm/test/UniswapV2SwapAdapter.t.sol index c160dc7..fa4970a 100644 --- a/evm/test/UniswapV2SwapAdapter.t.sol +++ b/evm/test/UniswapV2SwapAdapter.t.sol @@ -5,13 +5,18 @@ import "forge-std/Test.sol"; import "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; import "src/uniswap-v2/UniswapV2SwapAdapter.sol"; import "src/interfaces/ISwapAdapterTypes.sol"; +import "src/libraries/FractionMath.sol"; contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { + using FractionMath for Fraction; + UniswapV2SwapAdapter pairFunctions; IERC20 constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); IERC20 constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); address constant USDC_WETH_PAIR = 0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc; + uint256 constant TEST_ITERATIONS = 100; + function setUp() public { uint256 forkBlock = 17000000; vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); @@ -45,38 +50,22 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { function testPriceDecreasing() public { bytes32 pair = bytes32(bytes20(USDC_WETH_PAIR)); - uint256[] memory amounts = new uint256[](100); + uint256[] memory amounts = new uint256[](TEST_ITERATIONS); - for (uint256 i = 0; i < 100; i++) { + for (uint256 i = 0; i < TEST_ITERATIONS; i++) { amounts[i] = 1000 * i * 10 ** 6; } Fraction[] memory prices = pairFunctions.price(pair, WETH, USDC, amounts); - for (uint256 i = 0; i < 99; i++) { - assertEq(compareFractions(prices[i], prices[i + 1]), 1); + for (uint256 i = 0; i < TEST_ITERATIONS - 1; i++) { + assertEq(prices[i].compareFractions(prices[i + 1]), 1); assertGt(prices[i].denominator, 0); assertGt(prices[i + 1].denominator, 0); } } - function compareFractions(Fraction memory frac1, Fraction memory frac2) - internal - pure - returns (int8) - { - uint256 crossProduct1 = frac1.numerator * frac2.denominator; - uint256 crossProduct2 = frac2.numerator * frac1.denominator; - - // fractions are equal - if (crossProduct1 == crossProduct2) return 0; - // frac1 is greater than frac2 - else if (crossProduct1 > crossProduct2) return 1; - // frac1 is less than frac2 - else return -1; - } - function testSwapFuzz(uint256 specifiedAmount, bool isBuy) public { OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell; @@ -89,8 +78,7 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { // sellAmount is not specified for buy orders deal(address(USDC), address(this), type(uint256).max); USDC.approve(address(pairFunctions), type(uint256).max); - } - else { + } else { vm.assume(specifiedAmount < limits[0]); deal(address(USDC), address(this), specifiedAmount); @@ -100,15 +88,28 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { uint256 usdc_balance = USDC.balanceOf(address(this)); uint256 weth_balance = WETH.balanceOf(address(this)); - Trade memory trade = pairFunctions.swap(pair, USDC, WETH, side, specifiedAmount); + Trade memory trade = + pairFunctions.swap(pair, USDC, WETH, side, specifiedAmount); if (trade.calculatedAmount > 0) { if (side == OrderSide.Buy) { - assertEq(specifiedAmount, WETH.balanceOf(address(this)) - weth_balance); - assertEq(trade.calculatedAmount, usdc_balance - USDC.balanceOf(address(this))); + assertEq( + specifiedAmount, + WETH.balanceOf(address(this)) - weth_balance + ); + assertEq( + trade.calculatedAmount, + usdc_balance - USDC.balanceOf(address(this)) + ); } else { - assertEq(specifiedAmount, usdc_balance - USDC.balanceOf(address(this))); - assertEq(trade.calculatedAmount, WETH.balanceOf(address(this)) - weth_balance); + assertEq( + specifiedAmount, + usdc_balance - USDC.balanceOf(address(this)) + ); + assertEq( + trade.calculatedAmount, + WETH.balanceOf(address(this)) - weth_balance + ); } } } @@ -119,16 +120,15 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { function executeIncreasingSwaps(OrderSide side) internal { bytes32 pair = bytes32(bytes20(USDC_WETH_PAIR)); - uint256 iterations = 100; - uint256[] memory amounts = new uint256[](iterations); - for (uint256 i = 0; i < 100; i++) { + uint256[] memory amounts = new uint256[](TEST_ITERATIONS); + for (uint256 i = 0; i < TEST_ITERATIONS; i++) { amounts[i] = 1000 * i * 10 ** 6; } - Trade[] memory trades = new Trade[](iterations); + Trade[] memory trades = new Trade[](TEST_ITERATIONS); uint256 beforeSwap; - for (uint256 i = 0; i < iterations; i++) { + for (uint256 i = 0; i < TEST_ITERATIONS; i++) { beforeSwap = vm.snapshot(); deal(address(USDC), address(this), amounts[i]); @@ -138,10 +138,10 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { vm.revertTo(beforeSwap); } - for (uint256 i = 1; i < iterations - 1; i++) { + for (uint256 i = 1; i < TEST_ITERATIONS - 1; i++) { assertLe(trades[i].calculatedAmount, trades[i + 1].calculatedAmount); assertLe(trades[i].gasUsed, trades[i + 1].gasUsed); - assertEq(compareFractions(trades[i].price, trades[i + 1].price), 1); + assertEq(trades[i].price.compareFractions(trades[i + 1].price), 1); } } From 9105b898d6562008c4de8155e63cceb5f2d8183c Mon Sep 17 00:00:00 2001 From: pistomat Date: Wed, 6 Dec 2023 15:12:43 +0100 Subject: [PATCH 11/16] rename pairId to more general poolId --- .../logic/vm-integration/ethereum-solidity.md | 20 +++++------ evm/src/balancer-v2/BalancerV2SwapAdapter.sol | 30 ++++++++-------- evm/src/balancer-v2/manifest.yaml | 2 +- evm/src/interfaces/ISwapAdapter.sol | 34 +++++++++---------- evm/src/interfaces/ISwapAdapterTypes.sol | 8 ++--- evm/src/uniswap-v2/UniswapV2SwapAdapter.sol | 16 ++++----- evm/src/uniswap-v2/manifest.yaml | 2 +- evm/test/BalancerV2SwapAdapter.t.sol | 4 +-- 8 files changed, 58 insertions(+), 58 deletions(-) diff --git a/docs/logic/vm-integration/ethereum-solidity.md b/docs/logic/vm-integration/ethereum-solidity.md index 9e8926d..6f894e1 100644 --- a/docs/logic/vm-integration/ethereum-solidity.md +++ b/docs/logic/vm-integration/ethereum-solidity.md @@ -50,7 +50,7 @@ instances: # getTokens are not implemented. tests: instances: - - pair_id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc" + - pool_id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc" sell_token: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" buy_token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" block: 17000000 @@ -61,7 +61,7 @@ tests: #### Price (optional) -Calculates pair prices for specified amounts (optional). +Calculates pool prices for specified amounts (optional). The returned prices should include all protocol fees, in case the fee is dynamic, the returned price is expected to include the minimum fee. @@ -71,7 +71,7 @@ The method needs to be implemented as view as this is usually more efficient and ```solidity function price( - bytes32 pairId, + bytes32 poolId, IERC20 sellToken, IERC20 buyToken, uint256[] memory sellAmounts @@ -80,7 +80,7 @@ function price( #### Swap -Simulates swapping tokens on a given pair. +Simulates swapping tokens on a given pool. This function should be state modifying meaning it should actually execute the swap and change the state of the vm accordingly. @@ -90,7 +90,7 @@ The return type Trade, has a price attribute which should contain the value of p ```solidity function swap( - bytes32 pairId, + bytes32 poolId, IERC20 sellToken, IERC20 buyToken, OrderSide side, @@ -105,29 +105,29 @@ Retrieves the limits for each token. This method returns the maximum limits of a token that can be traded. The limit is reached when the change in the received amounts is zero or close to zero. If in doubt over estimate the limit. The swap function should not error with LimitExceeded if called with any amounts below the limit. ```solidity -function getLimits(bytes32 pairId, OrderSide side) +function getLimits(bytes32 poolId, OrderSide side) external returns (uint256[] memory); ``` #### getCapabilities -Retrieves the capabilities of the selected pair. +Retrieves the capabilities of the selected pool. ```solidity -function getCapabilities(bytes32 pairId, IERC20 sellToken, IERC20 buyToken) +function getCapabilities(bytes32 poolId, IERC20 sellToken, IERC20 buyToken) external returns (Capability[] memory); ``` #### getTokens (optional) -Retrieves the tokens for the given pair. +Retrieves the tokens for the given pool. _Mainly used for testing as this is redundant with the required substreams implementation._ ```solidity -function getTokens(bytes32 pairId) +function getTokens(bytes32 poolId) external returns (IERC20[] memory tokens); ``` diff --git a/evm/src/balancer-v2/BalancerV2SwapAdapter.sol b/evm/src/balancer-v2/BalancerV2SwapAdapter.sol index d7b5c2f..f048771 100644 --- a/evm/src/balancer-v2/BalancerV2SwapAdapter.sol +++ b/evm/src/balancer-v2/BalancerV2SwapAdapter.sol @@ -22,21 +22,21 @@ contract BalancerV2SwapAdapter is ISwapAdapter, Test { /// Also this function is not 'view' because Balancer V2 simulates the swap /// and /// then returns the amount diff in revert data. - /// @param pairId The ID of the trading pool. + /// @param poolId The ID of the trading pool. /// @param sellToken The token being sold. /// @param buyToken The token being bought. /// @param sellAmount The amount of tokens being sold. /// @return calculatedPrice The price of the buy token in terms of the sell /// as a Fraction struct. function priceSingle( - bytes32 pairId, + bytes32 poolId, IERC20 sellToken, IERC20 buyToken, uint256 sellAmount ) public returns (Fraction memory calculatedPrice) { IVault.BatchSwapStep[] memory swapSteps = new IVault.BatchSwapStep[](1); swapSteps[0] = IVault.BatchSwapStep({ - poolId: pairId, + poolId: poolId, assetInIndex: 0, assetOutIndex: 1, amount: sellAmount, @@ -65,14 +65,14 @@ contract BalancerV2SwapAdapter is ISwapAdapter, Test { } function getSellAmount( - bytes32 pairId, + bytes32 poolId, IERC20 sellToken, IERC20 buyToken, uint256 buyAmount ) public returns (uint256 sellAmount) { IVault.BatchSwapStep[] memory swapSteps = new IVault.BatchSwapStep[](1); swapSteps[0] = IVault.BatchSwapStep({ - poolId: pairId, + poolId: poolId, assetInIndex: 0, assetOutIndex: 1, amount: buyAmount, @@ -98,14 +98,14 @@ contract BalancerV2SwapAdapter is ISwapAdapter, Test { } function priceBatch( - bytes32 pairId, + bytes32 poolId, IERC20 sellToken, IERC20 buyToken, uint256[] memory specifiedAmounts ) external returns (Fraction[] memory calculatedPrices) { for (uint256 i = 0; i < specifiedAmounts.length; i++) { calculatedPrices[i] = - priceSingle(pairId, sellToken, buyToken, specifiedAmounts[i]); + priceSingle(poolId, sellToken, buyToken, specifiedAmounts[i]); } } @@ -119,7 +119,7 @@ contract BalancerV2SwapAdapter is ISwapAdapter, Test { } function swap( - bytes32 pairId, + bytes32 poolId, IERC20 sellToken, IERC20 buyToken, OrderSide side, @@ -135,7 +135,7 @@ contract BalancerV2SwapAdapter is ISwapAdapter, Test { } else { kind = IVault.SwapKind.GIVEN_OUT; sellAmount = - getSellAmount(pairId, sellToken, buyToken, specifiedAmount); + getSellAmount(poolId, sellToken, buyToken, specifiedAmount); limit = type(uint256).max; } @@ -145,7 +145,7 @@ contract BalancerV2SwapAdapter is ISwapAdapter, Test { uint256 gasBefore = gasleft(); trade.calculatedAmount = vault.swap( IVault.SingleSwap({ - poolId: pairId, + poolId: poolId, kind: kind, assetIn: address(sellToken), assetOut: address(buyToken), @@ -162,10 +162,10 @@ contract BalancerV2SwapAdapter is ISwapAdapter, Test { block.timestamp + SWAP_DEADLINE_SEC ); trade.gasUsed = gasBefore - gasleft(); - trade.price = priceSingle(pairId, sellToken, buyToken, specifiedAmount); + trade.price = priceSingle(poolId, sellToken, buyToken, specifiedAmount); } - function getLimits(bytes32 pairId, IERC20 sellToken, IERC20 buyToken) + function getLimits(bytes32 poolId, IERC20 sellToken, IERC20 buyToken) external view override @@ -173,7 +173,7 @@ contract BalancerV2SwapAdapter is ISwapAdapter, Test { { limits = new uint256[](2); (IERC20[] memory tokens, uint256[] memory balances,) = - vault.getPoolTokens(pairId); + vault.getPoolTokens(poolId); for (uint256 i = 0; i < tokens.length; i++) { if (tokens[i] == sellToken) { @@ -196,13 +196,13 @@ contract BalancerV2SwapAdapter is ISwapAdapter, Test { capabilities[1] = Capability.BuyOrder; } - function getTokens(bytes32 pairId) + function getTokens(bytes32 poolId) external view override returns (IERC20[] memory tokens) { - (tokens,,) = vault.getPoolTokens(pairId); + (tokens,,) = vault.getPoolTokens(poolId); } /// @dev Balancer V2 does not support enumerating pools, they have to be diff --git a/evm/src/balancer-v2/manifest.yaml b/evm/src/balancer-v2/manifest.yaml index 822388d..0f985e9 100644 --- a/evm/src/balancer-v2/manifest.yaml +++ b/evm/src/balancer-v2/manifest.yaml @@ -27,7 +27,7 @@ instances: # getTokens are not implemented. tests: instances: - - pair_id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc" + - pool_id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc" sell_token: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" buy_token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" block: 17000000 diff --git a/evm/src/interfaces/ISwapAdapter.sol b/evm/src/interfaces/ISwapAdapter.sol index ed8b017..df1e131 100644 --- a/evm/src/interfaces/ISwapAdapter.sol +++ b/evm/src/interfaces/ISwapAdapter.sol @@ -6,18 +6,18 @@ import {ISwapAdapterTypes} from "src/interfaces/ISwapAdapterTypes.sol"; /// @title ISwapAdapter /// @dev Implement this interface to support propeller routing through your -/// pairs. Before implementing the interface we need to introduce three function -/// for a given pair: The swap(x), gas(x) and price(x) functions: The swap +/// pools. Before implementing the interface we need to introduce three function +/// for a given pool: The swap(x), gas(x) and price(x) functions: The swap /// function accepts some specified token amount x and returns the amount y a /// user can get by swapping x through the venue. The gas function simply /// returns the estimated gas cost given a specified amount x. Last but not /// least, the price function is the derivative of the swap function. It -/// represents the best possible price a user can get from a pair after swapping +/// represents the best possible price a user can get from a pool after swapping /// x of the specified token. During calls to swap and getLimits, the caller can /// be assumed to have the required sell or buy token balance as well as /// unlimited approvals to this contract. interface ISwapAdapter is ISwapAdapterTypes { - /// @notice Calculates pair prices for specified amounts (optional). + /// @notice Calculates pool prices for specified amounts (optional). /// @dev The returned prices should include all dex fees, in case the fee is /// dynamic, the returned price is expected to include the minimum fee. /// Ideally this method should be implemented, although it is optional as @@ -26,7 +26,7 @@ interface ISwapAdapter is ISwapAdapterTypes { /// calling it should revert using the `NotImplemented` error. The method /// needs to be implemented as view as this is usually more efficient and /// can be run in parallel. all. - /// @param pairId The ID of the trading pair. + /// @param poolId The ID of the trading pool. /// @param sellToken The token being sold. /// @param buyToken The token being bought. /// @param specifiedAmounts The specified amounts used for price @@ -34,14 +34,14 @@ interface ISwapAdapter is ISwapAdapterTypes { /// @return prices array of prices as fractions corresponding to the /// provided amounts. function price( - bytes32 pairId, + bytes32 poolId, IERC20 sellToken, IERC20 buyToken, uint256[] memory specifiedAmounts ) external view returns (Fraction[] memory prices); /** - * @notice Simulates swapping tokens on a given pair. + * @notice Simulates swapping tokens on a given pool. * @dev This function should be state modifying meaning it should actually * execute the swap and change the state of the evm accordingly. Please * include a gas usage estimate for each amount. This can be achieved e.g. @@ -50,7 +50,7 @@ interface ISwapAdapter is ISwapAdapterTypes { * this is optional, defined via `Capability.PriceFunction`, it is valid to * return a zero value for this price in that case it will be estimated * numerically. To return zero use Fraction(0, 1). - * @param pairId The ID of the trading pair. + * @param poolId The ID of the trading pool. * @param sellToken The token being sold. * @param buyToken The token being bought. * @param side The side of the trade (Sell or Buy). @@ -58,7 +58,7 @@ interface ISwapAdapter is ISwapAdapterTypes { * @return trade Trade struct representing the executed trade. */ function swap( - bytes32 pairId, + bytes32 poolId, IERC20 sellToken, IERC20 buyToken, OrderSide side, @@ -71,27 +71,27 @@ interface ISwapAdapter is ISwapAdapterTypes { /// close to zero. Overestimate if in doubt rather than underestimate. The /// swap function should not error with `LimitExceeded` if called with /// amounts below the limit. - /// @param pairId The ID of the trading pair. + /// @param poolId The ID of the trading pool. /// @param sellToken The token being sold. /// @param buyToken The token being bought. /// @return limits An array of limits. - function getLimits(bytes32 pairId, IERC20 sellToken, IERC20 buyToken) + function getLimits(bytes32 poolId, IERC20 sellToken, IERC20 buyToken) external returns (uint256[] memory limits); - /// @notice Retrieves the capabilities of the selected pair. - /// @param pairId The ID of the trading pair. + /// @notice Retrieves the capabilities of the selected pool. + /// @param poolId The ID of the trading pool. /// @return capabilities An array of Capability. - function getCapabilities(bytes32 pairId, IERC20 sellToken, IERC20 buyToken) + function getCapabilities(bytes32 poolId, IERC20 sellToken, IERC20 buyToken) external returns (Capability[] memory capabilities); - /// @notice Retrieves the tokens in the selected pair. + /// @notice Retrieves the tokens in the selected pool. /// @dev Mainly used for testing as this is redundant with the required /// substreams implementation. - /// @param pairId The ID of the trading pair. + /// @param poolId The ID of the trading pool. /// @return tokens An array of IERC20 contracts. - function getTokens(bytes32 pairId) + function getTokens(bytes32 poolId) external returns (IERC20[] memory tokens); diff --git a/evm/src/interfaces/ISwapAdapterTypes.sol b/evm/src/interfaces/ISwapAdapterTypes.sol index 81767e0..e3d7462 100644 --- a/evm/src/interfaces/ISwapAdapterTypes.sol +++ b/evm/src/interfaces/ISwapAdapterTypes.sol @@ -13,7 +13,7 @@ interface ISwapAdapterTypes { } /// @dev The Capability enum represents possible features of a trading - /// pair. + /// pool. enum Capability { Unset, // Support OrderSide.Sell values (required) @@ -24,10 +24,10 @@ interface ISwapAdapterTypes { PriceFunction, // Support tokens that charge a fee on transfer (optional) FeeOnTransfer, - // The pair does not suffer from price impact and maintains a constant + // The pool does not suffer from price impact and maintains a constant // price for increasingly larger specified amounts. (optional) ConstantPrice, - // Indicates that the pair does not read it's own token balances while + // Indicates that the pool does not read it's own token balances while // swapping. (optional) TokenBalanceIndependent, // Indicates that prices are returned scaled, else it is assumed prices @@ -49,7 +49,7 @@ interface ISwapAdapterTypes { uint256 calculatedAmount; // The amount of gas used in the trade. uint256 gasUsed; - // The price of the pair after the trade. For zero use Fraction(0, 1). + // The price of the pool after the trade. For zero use Fraction(0, 1). Fraction price; } diff --git a/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol b/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol index a413947..52d4771 100644 --- a/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol +++ b/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol @@ -13,13 +13,13 @@ contract UniswapV2SwapAdapter is ISwapAdapter { } function price( - bytes32 pairId, + bytes32 poolId, IERC20 sellToken, IERC20 buyToken, uint256[] memory specifiedAmounts ) external view override returns (Fraction[] memory prices) { prices = new Fraction[](specifiedAmounts.length); - IUniswapV2Pair pair = IUniswapV2Pair(address(bytes20(pairId))); + IUniswapV2Pair pair = IUniswapV2Pair(address(bytes20(poolId))); uint112 r0; uint112 r1; if (sellToken < buyToken) { @@ -51,7 +51,7 @@ contract UniswapV2SwapAdapter is ISwapAdapter { } function swap( - bytes32 pairId, + bytes32 poolId, IERC20 sellToken, IERC20 buyToken, OrderSide side, @@ -62,7 +62,7 @@ contract UniswapV2SwapAdapter is ISwapAdapter { // expected zero Fraction(0, 1) } - IUniswapV2Pair pair = IUniswapV2Pair(address(bytes20(pairId))); + IUniswapV2Pair pair = IUniswapV2Pair(address(bytes20(poolId))); uint112 r0; uint112 r1; bool zero2one = sellToken < buyToken; @@ -168,13 +168,13 @@ contract UniswapV2SwapAdapter is ISwapAdapter { amountIn = (numerator / denominator) + 1; } - function getLimits(bytes32 pairId, IERC20 sellToken, IERC20 buyToken) + function getLimits(bytes32 poolId, IERC20 sellToken, IERC20 buyToken) external view override returns (uint256[] memory limits) { - IUniswapV2Pair pair = IUniswapV2Pair(address(bytes20(pairId))); + IUniswapV2Pair pair = IUniswapV2Pair(address(bytes20(poolId))); limits = new uint256[](2); (uint256 r0, uint256 r1,) = pair.getReserves(); if (sellToken < buyToken) { @@ -198,14 +198,14 @@ contract UniswapV2SwapAdapter is ISwapAdapter { capabilities[2] = Capability.PriceFunction; } - function getTokens(bytes32 pairId) + function getTokens(bytes32 poolId) external view override returns (IERC20[] memory tokens) { tokens = new IERC20[](2); - IUniswapV2Pair pair = IUniswapV2Pair(address(bytes20(pairId))); + IUniswapV2Pair pair = IUniswapV2Pair(address(bytes20(poolId))); tokens[0] = IERC20(pair.token0()); tokens[1] = IERC20(pair.token1()); } diff --git a/evm/src/uniswap-v2/manifest.yaml b/evm/src/uniswap-v2/manifest.yaml index 5864481..a6f469c 100644 --- a/evm/src/uniswap-v2/manifest.yaml +++ b/evm/src/uniswap-v2/manifest.yaml @@ -27,7 +27,7 @@ instances: # getTokens are not implemented. tests: instances: - - pair_id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc" + - pool_id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc" sell_token: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" buy_token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" block: 17000000 diff --git a/evm/test/BalancerV2SwapAdapter.t.sol b/evm/test/BalancerV2SwapAdapter.t.sol index ccf3447..7fb4564 100644 --- a/evm/test/BalancerV2SwapAdapter.t.sol +++ b/evm/test/BalancerV2SwapAdapter.t.sol @@ -202,11 +202,11 @@ contract BalancerV2SwapAdapterTest is Test, ISwapAdapterTypes { assert(limits[1] > 0); } - function testGetCapabilitiesFuzz(bytes32 pair, address t0, address t1) + function testGetCapabilitiesFuzz(bytes32 pool, address t0, address t1) public { Capability[] memory res = - adapter.getCapabilities(pair, IERC20(t0), IERC20(t1)); + adapter.getCapabilities(pool, IERC20(t0), IERC20(t1)); assertEq(res.length, 2); assertEq(uint256(res[0]), uint256(Capability.SellOrder)); From b7b750de38d462a3c4caea55df908739d7f423a3 Mon Sep 17 00:00:00 2001 From: pistomat Date: Wed, 6 Dec 2023 15:13:16 +0100 Subject: [PATCH 12/16] Rename pairFunctions to adapter --- evm/test/UniswapV2SwapAdapter.t.sol | 30 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/evm/test/UniswapV2SwapAdapter.t.sol b/evm/test/UniswapV2SwapAdapter.t.sol index fa4970a..78d007c 100644 --- a/evm/test/UniswapV2SwapAdapter.t.sol +++ b/evm/test/UniswapV2SwapAdapter.t.sol @@ -10,7 +10,7 @@ import "src/libraries/FractionMath.sol"; contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { using FractionMath for Fraction; - UniswapV2SwapAdapter pairFunctions; + UniswapV2SwapAdapter adapter; IERC20 constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); IERC20 constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); address constant USDC_WETH_PAIR = 0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc; @@ -20,10 +20,10 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { function setUp() public { uint256 forkBlock = 17000000; vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); - pairFunctions = new + adapter = new UniswapV2SwapAdapter(0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f); - vm.label(address(pairFunctions), "UniswapV2SwapAdapter"); + vm.label(address(adapter), "UniswapV2SwapAdapter"); vm.label(address(WETH), "WETH"); vm.label(address(USDC), "USDC"); vm.label(address(USDC_WETH_PAIR), "USDC_WETH_PAIR"); @@ -31,7 +31,7 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { function testPriceFuzz(uint256 amount0, uint256 amount1) public { bytes32 pair = bytes32(bytes20(USDC_WETH_PAIR)); - uint256[] memory limits = pairFunctions.getLimits(pair, USDC, WETH); + uint256[] memory limits = adapter.getLimits(pair, USDC, WETH); vm.assume(amount0 < limits[0]); vm.assume(amount1 < limits[0]); @@ -39,8 +39,7 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { amounts[0] = amount0; amounts[1] = amount1; - Fraction[] memory prices = - pairFunctions.price(pair, WETH, USDC, amounts); + Fraction[] memory prices = adapter.price(pair, WETH, USDC, amounts); for (uint256 i = 0; i < prices.length; i++) { assertGt(prices[i].numerator, 0); @@ -56,8 +55,7 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { amounts[i] = 1000 * i * 10 ** 6; } - Fraction[] memory prices = - pairFunctions.price(pair, WETH, USDC, amounts); + Fraction[] memory prices = adapter.price(pair, WETH, USDC, amounts); for (uint256 i = 0; i < TEST_ITERATIONS - 1; i++) { assertEq(prices[i].compareFractions(prices[i + 1]), 1); @@ -70,26 +68,26 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell; bytes32 pair = bytes32(bytes20(USDC_WETH_PAIR)); - uint256[] memory limits = pairFunctions.getLimits(pair, USDC, WETH); + uint256[] memory limits = adapter.getLimits(pair, USDC, WETH); if (side == OrderSide.Buy) { vm.assume(specifiedAmount < limits[1]); // sellAmount is not specified for buy orders deal(address(USDC), address(this), type(uint256).max); - USDC.approve(address(pairFunctions), type(uint256).max); + USDC.approve(address(adapter), type(uint256).max); } else { vm.assume(specifiedAmount < limits[0]); deal(address(USDC), address(this), specifiedAmount); - USDC.approve(address(pairFunctions), specifiedAmount); + USDC.approve(address(adapter), specifiedAmount); } uint256 usdc_balance = USDC.balanceOf(address(this)); uint256 weth_balance = WETH.balanceOf(address(this)); Trade memory trade = - pairFunctions.swap(pair, USDC, WETH, side, specifiedAmount); + adapter.swap(pair, USDC, WETH, side, specifiedAmount); if (trade.calculatedAmount > 0) { if (side == OrderSide.Buy) { @@ -132,9 +130,9 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { beforeSwap = vm.snapshot(); deal(address(USDC), address(this), amounts[i]); - USDC.approve(address(pairFunctions), amounts[i]); + USDC.approve(address(adapter), amounts[i]); - trades[i] = pairFunctions.swap(pair, USDC, WETH, side, amounts[i]); + trades[i] = adapter.swap(pair, USDC, WETH, side, amounts[i]); vm.revertTo(beforeSwap); } @@ -151,14 +149,14 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { function testGetCapabilities(bytes32 pair, address t0, address t1) public { Capability[] memory res = - pairFunctions.getCapabilities(pair, IERC20(t0), IERC20(t1)); + adapter.getCapabilities(pair, IERC20(t0), IERC20(t1)); assertEq(res.length, 3); } function testGetLimits() public { bytes32 pair = bytes32(bytes20(USDC_WETH_PAIR)); - uint256[] memory limits = pairFunctions.getLimits(pair, USDC, WETH); + uint256[] memory limits = adapter.getLimits(pair, USDC, WETH); assertEq(limits.length, 2); } From a7798374e1cc3cf949242581703d86986c98ba16 Mon Sep 17 00:00:00 2001 From: pistomat Date: Thu, 7 Dec 2023 17:44:47 +0100 Subject: [PATCH 13/16] Add more documentation and begin with templates --- docs/logic/vm-integration/README.md | 29 +++++++++--- evm/src/balancer-v2/BalancerV2SwapAdapter.sol | 3 +- evm/src/interfaces/ISwapAdapter.sol | 25 +++++----- evm/src/template/TemplateSwapAdapter.sol | 0 evm/src/template/manifest.yaml | 36 +++++++++++++++ evm/src/uniswap-v2/UniswapV2SwapAdapter.sol | 46 ++++++++++++++++--- evm/test/BalancerV2SwapAdapter.t.sol | 5 +- evm/test/UniswapV2SwapAdapter.t.sol | 2 +- 8 files changed, 114 insertions(+), 32 deletions(-) create mode 100644 evm/src/template/TemplateSwapAdapter.sol create mode 100644 evm/src/template/manifest.yaml diff --git a/docs/logic/vm-integration/README.md b/docs/logic/vm-integration/README.md index cb62c79..3571c5a 100644 --- a/docs/logic/vm-integration/README.md +++ b/docs/logic/vm-integration/README.md @@ -8,18 +8,35 @@ To create a VM implementation, it is required two provide a manifest file as wel ### Prerequisites -1. Start by making a local copy of the Propeller Protocol Lib repository: +1. Install [Foundry](https://book.getfoundry.sh/getting-started/installation#using-foundryup). +```bash +curl -L https://foundry.paradigm.xyz | bash +``` + +2. Start by making a local copy of the Propeller Protocol Lib repository: ```bash git clone https://github.com/propeller-heads/propeller-protocol-lib ``` -2. Install `Foundry`, the smart contract development toolchain we use. We recommend installation using [foundryup](https://book.getfoundry.sh/getting-started/installation#using-foundryup) - 3. Install forge dependencies: ```bash -cd evm +cd ./propeller-protocol-lib/evm/ forge install ``` -4. Your integration should be in a separate directory in the `evm/src` folder. You can clone one of the example directories `evm/src/uniswap-v2` or `evm/src/balancer` and rename it to your integration name. -``` \ No newline at end of file +### Understanding the ISwapAdapter + +1. Read the the documentation of the [Ethereum Solidity interface](ethereum-solidity.md). It describes the functions that need to be implemented as well as the manifest file. +2. Additionally read through the docstring of the [ISwapAdapter.sol](../../../evm/src/interfaces/ISwapAdapter.sol) interface and the [ISwapAdapterTypes.sol](../../../evm/src/interfaces/ISwapAdapterTypes.sol) interface which defines the data types and errors used by the adapter interface. +3. You can also generate the documentation locally and the look at the generated documentation in the `./docs` folder: +```bash +forge doc +``` +### Implementing the ISwapAdapter interface +1. Your integration should be in a separate directory in the `evm/src` folder. Start by cloning the template directory: +```bash +cp -r ./evm/src/template ./evm/src/ +``` +2. Implement the `ISwapAdapter` interface in the `./evm/src/.sol` file. +3. Create tests for your implementation in the `./evm/test/.t.sol` file, again based on the template `./evm/test/TemplateSwapAdapter.t.sol`. + diff --git a/evm/src/balancer-v2/BalancerV2SwapAdapter.sol b/evm/src/balancer-v2/BalancerV2SwapAdapter.sol index f048771..1b37799 100644 --- a/evm/src/balancer-v2/BalancerV2SwapAdapter.sol +++ b/evm/src/balancer-v2/BalancerV2SwapAdapter.sol @@ -20,8 +20,7 @@ contract BalancerV2SwapAdapter is ISwapAdapter, Test { /// @notice Calculate the price of the buy token in terms of the sell token. /// @dev The resulting price is not scaled by the token decimals. /// Also this function is not 'view' because Balancer V2 simulates the swap - /// and - /// then returns the amount diff in revert data. + /// and then returns the amount diff in revert data. /// @param poolId The ID of the trading pool. /// @param sellToken The token being sold. /// @param buyToken The token being bought. diff --git a/evm/src/interfaces/ISwapAdapter.sol b/evm/src/interfaces/ISwapAdapter.sol index df1e131..5482fb8 100644 --- a/evm/src/interfaces/ISwapAdapter.sol +++ b/evm/src/interfaces/ISwapAdapter.sol @@ -5,11 +5,11 @@ import {IERC20} from "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; import {ISwapAdapterTypes} from "src/interfaces/ISwapAdapterTypes.sol"; /// @title ISwapAdapter -/// @dev Implement this interface to support propeller routing through your -/// pools. Before implementing the interface we need to introduce three function -/// for a given pool: The swap(x), gas(x) and price(x) functions: The swap -/// function accepts some specified token amount x and returns the amount y a -/// user can get by swapping x through the venue. The gas function simply +/// @dev Implement this interface to support Propeller routing through your +/// pools. Before implementing the interface we need to introduce some function +/// for a given pool. The main one, the swap(x) function, implements a sell +/// order of a specified . +/// The gas function simply /// returns the estimated gas cost given a specified amount x. Last but not /// least, the price function is the derivative of the swap function. It /// represents the best possible price a user can get from a pool after swapping @@ -44,12 +44,12 @@ interface ISwapAdapter is ISwapAdapterTypes { * @notice Simulates swapping tokens on a given pool. * @dev This function should be state modifying meaning it should actually * execute the swap and change the state of the evm accordingly. Please - * include a gas usage estimate for each amount. This can be achieved e.g. - * by using the `gasleft()` function. The return type `Trade` has a price - * attribute which should contain the value of `price(specifiedAmount)`. As - * this is optional, defined via `Capability.PriceFunction`, it is valid to - * return a zero value for this price in that case it will be estimated - * numerically. To return zero use Fraction(0, 1). + * include a gas usage estimate for each amount. This can be achieved e.g. by + * using the `gasleft()` function. The return type `Trade` has an attribute + * called price which should contain the value of `price(specifiedAmount)`. + * As this is optional, defined via `Capability.PriceFunction`, it is valid + * to return a Fraction(0, 0) value for this price. In that case the price + * will be estimated numerically. * @param poolId The ID of the trading pool. * @param sellToken The token being sold. * @param buyToken The token being bought. @@ -68,7 +68,8 @@ interface ISwapAdapter is ISwapAdapterTypes { /// @notice Retrieves the limits for each token. /// @dev Retrieve the maximum limits of a token that can be traded. The /// limit is reached when the change in the received amounts is zero or - /// close to zero. Overestimate if in doubt rather than underestimate. The + /// close to zero or when the swap fails because of the pools restrictions. + /// Overestimate if in doubt rather than underestimate. The /// swap function should not error with `LimitExceeded` if called with /// amounts below the limit. /// @param poolId The ID of the trading pool. diff --git a/evm/src/template/TemplateSwapAdapter.sol b/evm/src/template/TemplateSwapAdapter.sol new file mode 100644 index 0000000..e69de29 diff --git a/evm/src/template/manifest.yaml b/evm/src/template/manifest.yaml new file mode 100644 index 0000000..7343007 --- /dev/null +++ b/evm/src/template/manifest.yaml @@ -0,0 +1,36 @@ +# information about the author helps us reach out in case of issues. +author: + name: YourCompany + email: developer@yourcompany.xyz + +# Protocol Constants +constants: + protocol_gas: 30000 + # minimum capabilities we can expect, individual pools may extend these + capabilities: + - SellSide + - BuySide + - PriceFunction + +# The file containing the adapter contract +contract: TemplateSwapAdapter.sol + +# Deployment instances used to generate chain specific bytecode. +instances: + - chain: + name: mainnet + id: 0 + arguments: + - "0xBA12222222228d8Ba445958a75a0704d566BF2C8" + +# Specify some automatic test cases in case getPoolIds and +# getTokens are not implemented. +tests: + instances: + - pool_id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc" + sell_token: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + buy_token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + block: 17000000 + chain: + id: 0 + name: mainnet diff --git a/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol b/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol index 52d4771..fde236a 100644 --- a/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol +++ b/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol @@ -3,7 +3,8 @@ pragma solidity ^0.8.13; import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; -uint256 constant RESERVE_LIMIT_FACTOR = 2; // TODO why is the factor so high? +// Uniswap handles arbirary amounts, but we limit the amount to 10x just in case +uint256 constant RESERVE_LIMIT_FACTOR = 10; contract UniswapV2SwapAdapter is ISwapAdapter { IUniswapV2Factory immutable factory; @@ -12,6 +13,7 @@ contract UniswapV2SwapAdapter is ISwapAdapter { factory = IUniswapV2Factory(factory_); } + /// @inheritdoc ISwapAdapter function price( bytes32 poolId, IERC20 sellToken, @@ -33,6 +35,11 @@ contract UniswapV2SwapAdapter is ISwapAdapter { } } + /// @notice Calculates pool prices for specified amounts + /// @param amountIn The amount of the token being sold. + /// @param reserveIn The reserve of the token being sold. + /// @param reserveOut The reserve of the token being bought. + /// @return The price as a fraction corresponding to the provided amount. function getPriceAt(uint256 amountIn, uint256 reserveIn, uint256 reserveOut) internal pure @@ -50,6 +57,7 @@ contract UniswapV2SwapAdapter is ISwapAdapter { return Fraction(newReserveOut * 1000, newReserveIn * 997); } + /// @inheritdoc ISwapAdapter function swap( bytes32 poolId, IERC20 sellToken, @@ -58,8 +66,7 @@ contract UniswapV2SwapAdapter is ISwapAdapter { uint256 specifiedAmount ) external override returns (Trade memory trade) { if (specifiedAmount == 0) { - return trade; // TODO: This returns Fraction(0, 0) instead of the - // expected zero Fraction(0, 1) + return trade; } IUniswapV2Pair pair = IUniswapV2Pair(address(bytes20(poolId))); @@ -83,6 +90,14 @@ contract UniswapV2SwapAdapter is ISwapAdapter { trade.price = getPriceAt(specifiedAmount, r0, r1); } + /// @notice Executes a sell order on a given pool. + /// @param pair The pair to trade on. + /// @param sellToken The token being sold. + /// @param zero2one Whether the sell token is token0 or token1. + /// @param reserveIn The reserve of the token being sold. + /// @param reserveOut The reserve of the token being bought. + /// @param amount The amount to be traded. + /// @return calculatedAmount The amount of tokens received. function sell( IUniswapV2Pair pair, IERC20 sellToken, @@ -104,8 +119,11 @@ contract UniswapV2SwapAdapter is ISwapAdapter { return amountOut; } - // Given an input amount of an asset and pair reserves, returns the maximum - // output amount of the other asset + /// @notice Given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset + /// @param amountIn The amount of the token being sold. + /// @param reserveIn The reserve of the token being sold. + /// @param reserveOut The reserve of the token being bought. + /// @return amountOut The amount of tokens received. function getAmountOut( uint256 amountIn, uint256 reserveIn, @@ -123,6 +141,14 @@ contract UniswapV2SwapAdapter is ISwapAdapter { amountOut = numerator / denominator; } + /// @notice Execute a buy order on a given pool. + /// @param pair The pair to trade on. + /// @param sellToken The token being sold. + /// @param zero2one Whether the sell token is token0 or token1. + /// @param reserveIn The reserve of the token being sold. + /// @param reserveOut The reserve of the token being bought. + /// @param amountOut The amount of tokens to be bought. + /// @return calculatedAmount The amount of tokens sold. function buy( IUniswapV2Pair pair, IERC20 sellToken, @@ -147,8 +173,10 @@ contract UniswapV2SwapAdapter is ISwapAdapter { return amount; } - // given an output amount of an asset and pair reserves, returns a required - // input amount of the other asset + /// @notice Given an output amount of an asset and pair reserves, returns a required input amount of the other asset + /// @param amountOut The amount of the token being bought. + /// @param reserveIn The reserve of the token being sold. + /// @param reserveOut The reserve of the token being bought. function getAmountIn( uint256 amountOut, uint256 reserveIn, @@ -168,6 +196,7 @@ contract UniswapV2SwapAdapter is ISwapAdapter { amountIn = (numerator / denominator) + 1; } + /// @inheritdoc ISwapAdapter function getLimits(bytes32 poolId, IERC20 sellToken, IERC20 buyToken) external view @@ -186,6 +215,7 @@ contract UniswapV2SwapAdapter is ISwapAdapter { } } + /// @inheritdoc ISwapAdapter function getCapabilities(bytes32, IERC20, IERC20) external pure @@ -198,6 +228,7 @@ contract UniswapV2SwapAdapter is ISwapAdapter { capabilities[2] = Capability.PriceFunction; } + /// @inheritdoc ISwapAdapter function getTokens(bytes32 poolId) external view @@ -210,6 +241,7 @@ contract UniswapV2SwapAdapter is ISwapAdapter { tokens[1] = IERC20(pair.token1()); } + /// @inheritdoc ISwapAdapter function getPoolIds(uint256 offset, uint256 limit) external view diff --git a/evm/test/BalancerV2SwapAdapter.t.sol b/evm/test/BalancerV2SwapAdapter.t.sol index 7fb4564..554e660 100644 --- a/evm/test/BalancerV2SwapAdapter.t.sol +++ b/evm/test/BalancerV2SwapAdapter.t.sol @@ -73,12 +73,9 @@ contract BalancerV2SwapAdapterTest is Test, ISwapAdapterTypes { for (uint256 i = 0; i < TEST_ITERATIONS; i++) { amounts[i] = 1000 * (i + 1) * 10 ** 18; - console.log("i = ", i); - console.log("amounts[i] = ", amounts[i]); prices[i] = adapter.priceSingle( B_80BAL_20WETH_POOL_ID, BAL, WETH, amounts[i] ); - console.log("prices = ", prices[i].numerator, prices[i].denominator); } for (uint256 i = 0; i < TEST_ITERATIONS - 1; i++) { @@ -98,7 +95,7 @@ contract BalancerV2SwapAdapterTest is Test, ISwapAdapterTypes { if (side == OrderSide.Buy) { vm.assume(specifiedAmount < limits[1]); - // sellAmount is not specified for buy orders + // TODO calculate the amountIn by using price function as in testPriceDecreasing deal(address(BAL), address(this), type(uint256).max); BAL.approve(address(adapter), type(uint256).max); } else { diff --git a/evm/test/UniswapV2SwapAdapter.t.sol b/evm/test/UniswapV2SwapAdapter.t.sol index 78d007c..0c4762f 100644 --- a/evm/test/UniswapV2SwapAdapter.t.sol +++ b/evm/test/UniswapV2SwapAdapter.t.sol @@ -73,7 +73,7 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { if (side == OrderSide.Buy) { vm.assume(specifiedAmount < limits[1]); - // sellAmount is not specified for buy orders + // TODO calculate the amountIn by using price function as in BalancerV2 testPriceDecreasing deal(address(USDC), address(this), type(uint256).max); USDC.approve(address(adapter), type(uint256).max); } else { From 194e7caa156e1060aa53948ed917299dd3005f5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20D?= Date: Fri, 8 Dec 2023 16:59:09 +0100 Subject: [PATCH 14/16] Small improvements to the docs --- docs/README.md | 12 ++--- docs/logic/vm-integration/README.md | 46 ++++++++++++------- .../logic/vm-integration/ethereum-solidity.md | 30 ++++++------ evm/src/interfaces/ISwapAdapter.sol | 18 ++++---- evm/src/interfaces/ISwapAdapterTypes.sol | 4 +- 5 files changed, 63 insertions(+), 47 deletions(-) diff --git a/docs/README.md b/docs/README.md index 6881e7a..fc8340e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,25 +2,25 @@ Protocol lib is a library used by Propellerheads.xyz solvers to integrate decentralized protocols. Currently, only swap/exchange protocols are supported. -### Integration Process +## Integration Process To integrate with PropellerHeads solvers, two components need to be provided: -* **Protocol logic:** Provides simulations, of the protocols logic. +* **Protocol logic:** Provides simulations of the protocols logic. * **Indexing**: Provides access to the protocol state used by the simulation. This component is optional if your protocol is stateless. -#### Protocol Logic +### Protocol Logic PropellerHeads currently exposes two integration modes to specify the protocols' underlying logic: -* **VM Integration:** This integration type requires implementing an adapter interface in any language that compiles to the respective vm byte code. Currently, only Solidity is supported. +* **VM Integration:** This integration type requires implementing an adapter interface in any language that compiles to the respective vm byte code. This SDK provides the interface only in Solidity. **[Read more here.](logic/vm-integration/README.md)** * **Native Rust Integration:** Coming soon, this integration type requires implementing a Rust trait that describes the protocol logic. While VM integration is certainly the quickest and probably most accessible one for protocol developers, native implementations are much faster and allow us to consider the protocol for more time-sensitive use cases - e.g. quoting. -#### Indexing +### Indexing -For indexing purposes, it is required that you provide a [substreams](https://thegraph.com/docs/en/substreams/) package that emits a specified set of messages. Most new protocols will already have a [substreams](https://thegraph.com/docs/en/substreams/) package for indexing implemented this will only need to be adjusted to emit the required messages. +For indexing purposes, it is required that you provide a [substreams](https://thegraph.com/docs/en/substreams/) package that emits a specified set of messages. If your protocol already has a [substreams](https://thegraph.com/docs/en/substreams/) package for indexing implemented, you can adjust it to emit the required messages. _Specifications coming soon._ diff --git a/docs/logic/vm-integration/README.md b/docs/logic/vm-integration/README.md index 3571c5a..0ccd34a 100644 --- a/docs/logic/vm-integration/README.md +++ b/docs/logic/vm-integration/README.md @@ -2,41 +2,53 @@ This page describes the interface required to implement protocol logic component. -To create a VM implementation, it is required two provide a manifest file as well as a implementation of the corresponding adapter interface. +To create a VM integration, it is required to provide a manifest file as well as an implementation of the corresponding adapter interface. + +## Examples + +Following exchanges have been integrated using VM approach: + +- Uniswap V2 (see `/evm/src/uniswap-v2`) +- Balancer V2 (see `/evm/src/balancer-v2`) ## Step by step ### Prerequisites 1. Install [Foundry](https://book.getfoundry.sh/getting-started/installation#using-foundryup). -```bash -curl -L https://foundry.paradigm.xyz | bash -``` + ```bash + curl -L https://foundry.paradigm.xyz | bash + ``` + then start a new terminal session and run + ```bash + foundryup + ``` 2. Start by making a local copy of the Propeller Protocol Lib repository: -```bash -git clone https://github.com/propeller-heads/propeller-protocol-lib -``` + ```bash + git clone https://github.com/propeller-heads/propeller-protocol-lib + ``` 3. Install forge dependencies: -```bash -cd ./propeller-protocol-lib/evm/ -forge install -``` + ```bash + cd ./propeller-protocol-lib/evm/ + forge install + ``` ### Understanding the ISwapAdapter 1. Read the the documentation of the [Ethereum Solidity interface](ethereum-solidity.md). It describes the functions that need to be implemented as well as the manifest file. 2. Additionally read through the docstring of the [ISwapAdapter.sol](../../../evm/src/interfaces/ISwapAdapter.sol) interface and the [ISwapAdapterTypes.sol](../../../evm/src/interfaces/ISwapAdapterTypes.sol) interface which defines the data types and errors used by the adapter interface. 3. You can also generate the documentation locally and the look at the generated documentation in the `./docs` folder: -```bash -forge doc -``` + ```bash + cd ./propeller-protocol-lib/evm/ + forge doc + ``` ### Implementing the ISwapAdapter interface 1. Your integration should be in a separate directory in the `evm/src` folder. Start by cloning the template directory: -```bash -cp -r ./evm/src/template ./evm/src/ -``` + ```bash + cp -r ./evm/src/template ./evm/src/ + ``` 2. Implement the `ISwapAdapter` interface in the `./evm/src/.sol` file. 3. Create tests for your implementation in the `./evm/test/.t.sol` file, again based on the template `./evm/test/TemplateSwapAdapter.t.sol`. diff --git a/docs/logic/vm-integration/ethereum-solidity.md b/docs/logic/vm-integration/ethereum-solidity.md index 6f894e1..0d6133a 100644 --- a/docs/logic/vm-integration/ethereum-solidity.md +++ b/docs/logic/vm-integration/ethereum-solidity.md @@ -4,12 +4,12 @@ description: Provide protocol logic using the ethereum virtual machine # Ethereum: Solidity -### Swap +## Swap/exchange protocol To integrate an EVM exchange protocol the [ISwapAdapter.sol ](https://github.com/propeller-heads/propeller-protocol-lib/blob/main/evm/interfaces/ISwapAdapter.sol)should be implemented. Additionally a manifest file is required that summarises some metadata about the protocol. {% hint style="info" %} -Although the interface is specified for Solidity, you are not limited to writing the adapater contract in solidity. We can use any compiled evm bytecode. So if you prefer e.g. Vyper you are welcome to implement the interface using vyper. Unfortunately we do not provide all the tooling for vyper contracts yet, but you can certainly submit compiled vyper byte code. +Although the interface is specified for Solidity, you are not limited to writing the adapater contract in Solidity. We can use any compiled evm bytecode. So if you prefer e.g. Vyper, you are welcome to implement the interface using Vyper. Unfortunately we do not provide all the tooling for Vyper contracts yet, but you can certainly submit compiled Vyper byte code. {% endhint %} The manifest file contains information about the author, as well as additional static information about the protocol and how to test the current implementation. The file below lists all valid keys. @@ -22,9 +22,10 @@ author: # Protocol Constants constants: - # The minimum gas usage of the protocol, excluding any token transfers + # The minimum gas usage of a swap using the protocol, excluding any token transfers protocol_gas: 30000 - # Minimum capabilities we can expect, individual pools may extend these + # Minimum capabilities we can expect, individual pools may extend these. + # To learn about Capabilities, see ISwapAdapter.sol capabilities: - SellSide - BuySide @@ -32,17 +33,18 @@ constants: # The files containing the adapter contract (byte)code contract: - # The contract bytecode (required if no source is provided) + # The contract runtime (i.e. deployed) bytecode (required if no source is provided) runtime: UniswapV2SwapAdapter.bin # If you submit the source our CI can generate the bytecode source: UniswapV2SwapAdapter.sol -# Deployment instances used to generate chain specific bytecode. +# Deployment instances used to generate chain-specific bytecode. # Used by the runtime bytecode build script. instances: - chain: name: mainnet id: 0 + # Arguments passed to the constructor when building the contract arguments: - "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f" @@ -63,9 +65,11 @@ tests: Calculates pool prices for specified amounts (optional). -The returned prices should include all protocol fees, in case the fee is dynamic, the returned price is expected to include the minimum fee. +The returned prices should be in `buyToken/sellToken` units. -Ideally this method should be implemented, although it is optional as the price function can be numerically estimated from the swap function. In case it is not available it should be flagged accordingly via capabilities and calling it should revert using the NotImplemented error. +The returned prices should include all protocol fees. In case the fee is dynamic, the returned price is expected to include the minimum fee. + +Ideally this method should be implemented, although it is optional as the price function can be numerically estimated from the swap function. In case it is not available, it should be flagged accordingly via capabilities, and calling it should revert using the NotImplemented error. The method needs to be implemented as view as this is usually more efficient and can be run in parallel. @@ -82,11 +86,11 @@ function price( Simulates swapping tokens on a given pool. -This function should be state modifying meaning it should actually execute the swap and change the state of the vm accordingly. +This function should be state modifying, meaning it should actually execute the swap and change the state of the VM accordingly. -Please include a gas usage estimate for each amount. This can be achieved e.g. by using the gasleft() function. +Please include a gas usage estimate for each amount. This can be achieved e.g. by using the `gasleft()` function. -The return type Trade, has a price attribute which should contain the value of price(specifiedAmount). As previously mentioned, the price function support is optional, it is valid to return a zero value for this price in that case it will be estimated numerically. To return zero please use Fraction(0, 1). +The return type `Trade` has a price attribute which should contain the value of `price(specifiedAmount)`. As previously mentioned, the price function support is optional, it is valid to return a zero value for this price (in that case it will be estimated numerically). To return zero please use `Fraction(0, 1)`. ```solidity function swap( @@ -102,7 +106,7 @@ function swap( Retrieves the limits for each token. -This method returns the maximum limits of a token that can be traded. The limit is reached when the change in the received amounts is zero or close to zero. If in doubt over estimate the limit. The swap function should not error with LimitExceeded if called with any amounts below the limit. +This method returns the maximum amount of a token that can be traded. The limit is reached when the change in the received amounts is zero or close to zero. If in doubt, overestimate the limit. The swap function should not error with LimitExceeded if called with any amounts below the limit. ```solidity function getLimits(bytes32 poolId, OrderSide side) @@ -136,7 +140,7 @@ function getTokens(bytes32 poolId) Retrieves a range of pool IDs. -_Mainly used for testing it is alright to not return all available pools here. Nevertheless this is useful to test against the substreams implementation. If implemented it safes time writing custom tests._ +_Mainly used for testing. It is alright to not return all available pools here. Nevertheless, this is useful to test against the substreams implementation. If implemented, it saves time writing custom tests._ ```solidity function getPoolIds(uint256 offset, uint256 limit) diff --git a/evm/src/interfaces/ISwapAdapter.sol b/evm/src/interfaces/ISwapAdapter.sol index 5482fb8..430ec98 100644 --- a/evm/src/interfaces/ISwapAdapter.sol +++ b/evm/src/interfaces/ISwapAdapter.sol @@ -8,7 +8,7 @@ import {ISwapAdapterTypes} from "src/interfaces/ISwapAdapterTypes.sol"; /// @dev Implement this interface to support Propeller routing through your /// pools. Before implementing the interface we need to introduce some function /// for a given pool. The main one, the swap(x) function, implements a sell -/// order of a specified . +/// order of a specified token. /// The gas function simply /// returns the estimated gas cost given a specified amount x. Last but not /// least, the price function is the derivative of the swap function. It @@ -18,14 +18,14 @@ import {ISwapAdapterTypes} from "src/interfaces/ISwapAdapterTypes.sol"; /// unlimited approvals to this contract. interface ISwapAdapter is ISwapAdapterTypes { /// @notice Calculates pool prices for specified amounts (optional). - /// @dev The returned prices should include all dex fees, in case the fee is + /// @dev The returned prices should include all dex fees. In case the fee is /// dynamic, the returned price is expected to include the minimum fee. /// Ideally this method should be implemented, although it is optional as /// the price function can be numerically estimated from the swap function. - /// In case it is not available it should be flagged via capabilities and + /// In case it is not available, it should be flagged via capabilities and /// calling it should revert using the `NotImplemented` error. The method /// needs to be implemented as view as this is usually more efficient and - /// can be run in parallel. all. + /// can be run in parallel. /// @param poolId The ID of the trading pool. /// @param sellToken The token being sold. /// @param buyToken The token being bought. @@ -42,8 +42,8 @@ interface ISwapAdapter is ISwapAdapterTypes { /** * @notice Simulates swapping tokens on a given pool. - * @dev This function should be state modifying meaning it should actually - * execute the swap and change the state of the evm accordingly. Please + * @dev This function should be state modifying, meaning it should actually + * execute the swap and change the state of the EVM accordingly. Please * include a gas usage estimate for each amount. This can be achieved e.g. by * using the `gasleft()` function. The return type `Trade` has an attribute * called price which should contain the value of `price(specifiedAmount)`. @@ -97,9 +97,9 @@ interface ISwapAdapter is ISwapAdapterTypes { returns (IERC20[] memory tokens); /// @notice Retrieves a range of pool IDs. - /// @dev Mainly used for testing it is alright to not return all available - /// pools here. Nevertheless this is useful to test against the substreams - /// implementation. If implemented it saves time writing custom tests. + /// @dev Mainly used for testing. It is alright to not return all available + /// pools here. Nevertheless, this is useful to test against the substreams + /// implementation. If implemented, it saves time writing custom tests. /// @param offset The starting index from which to retrieve pool IDs. /// @param limit The maximum number of pool IDs to retrieve. /// @return ids An array of pool IDs. diff --git a/evm/src/interfaces/ISwapAdapterTypes.sol b/evm/src/interfaces/ISwapAdapterTypes.sol index e3d7462..13993a2 100644 --- a/evm/src/interfaces/ISwapAdapterTypes.sol +++ b/evm/src/interfaces/ISwapAdapterTypes.sol @@ -27,8 +27,8 @@ interface ISwapAdapterTypes { // The pool does not suffer from price impact and maintains a constant // price for increasingly larger specified amounts. (optional) ConstantPrice, - // Indicates that the pool does not read it's own token balances while - // swapping. (optional) + // Indicates that the pool does not read its own token balances + // from token contracts while swapping. (optional) TokenBalanceIndependent, // Indicates that prices are returned scaled, else it is assumed prices // still require scaling by token decimals. (required) From 1b919108da8cecde9e00d3bd53f79ae35fb53850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20D?= Date: Fri, 8 Dec 2023 17:02:42 +0100 Subject: [PATCH 15/16] Small improvements to the docs --- docs/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/README.md b/docs/README.md index fc8340e..fa597fa 100644 --- a/docs/README.md +++ b/docs/README.md @@ -9,6 +9,8 @@ To integrate with PropellerHeads solvers, two components need to be provided: * **Protocol logic:** Provides simulations of the protocols logic. * **Indexing**: Provides access to the protocol state used by the simulation. This component is optional if your protocol is stateless. +To propose an integration, create a pull request in this repository with the above components implemented. + ### Protocol Logic PropellerHeads currently exposes two integration modes to specify the protocols' underlying logic: From 1118502162651f697dc505d239e4365e3625128d Mon Sep 17 00:00:00 2001 From: pistomat Date: Sat, 9 Dec 2023 19:46:02 +0100 Subject: [PATCH 16/16] Implement adapter and test templates --- docs/logic/vm-integration/README.md | 27 ++++++--- evm/src/balancer-v2/BalancerV2SwapAdapter.sol | 3 +- evm/src/interfaces/ISwapAdapter.sol | 2 +- evm/src/template/TemplateSwapAdapter.sol | 58 +++++++++++++++++++ evm/src/uniswap-v2/UniswapV2SwapAdapter.sol | 8 ++- evm/test/BalancerV2SwapAdapter.t.sol | 3 +- evm/test/TemplateSwapAdapter.t.sol | 18 ++++++ evm/test/UniswapV2SwapAdapter.t.sol | 3 +- 8 files changed, 105 insertions(+), 17 deletions(-) create mode 100644 evm/test/TemplateSwapAdapter.t.sol diff --git a/docs/logic/vm-integration/README.md b/docs/logic/vm-integration/README.md index 0ccd34a..bcc5e58 100644 --- a/docs/logic/vm-integration/README.md +++ b/docs/logic/vm-integration/README.md @@ -15,7 +15,7 @@ Following exchanges have been integrated using VM approach: ### Prerequisites -1. Install [Foundry](https://book.getfoundry.sh/getting-started/installation#using-foundryup). +1. Install [Foundry](https://book.getfoundry.sh/getting-started/installation#using-foundryup), start by downloading and installing the Foundry installer: ```bash curl -L https://foundry.paradigm.xyz | bash ``` @@ -37,18 +37,27 @@ Following exchanges have been integrated using VM approach: ### Understanding the ISwapAdapter -1. Read the the documentation of the [Ethereum Solidity interface](ethereum-solidity.md). It describes the functions that need to be implemented as well as the manifest file. -2. Additionally read through the docstring of the [ISwapAdapter.sol](../../../evm/src/interfaces/ISwapAdapter.sol) interface and the [ISwapAdapterTypes.sol](../../../evm/src/interfaces/ISwapAdapterTypes.sol) interface which defines the data types and errors used by the adapter interface. -3. You can also generate the documentation locally and the look at the generated documentation in the `./docs` folder: +Read the the documentation of the [Ethereum Solidity interface](ethereum-solidity.md). It describes the functions that need to be implemented as well as the manifest file. +Additionally read through the docstring of the [ISwapAdapter.sol](../../../evm/src/interfaces/ISwapAdapter.sol) interface and the [ISwapAdapterTypes.sol](../../../evm/src/interfaces/ISwapAdapterTypes.sol) interface which defines the data types and errors used by the adapter interface. +You can also generate the documentation locally and the look at the generated documentation in the `./docs` folder: ```bash - cd ./propeller-protocol-lib/evm/ + cd ./evm/ forge doc ``` ### Implementing the ISwapAdapter interface -1. Your integration should be in a separate directory in the `evm/src` folder. Start by cloning the template directory: +Your integration should be in a separate directory in the `evm/src` folder. Start by cloning the template directory: ```bash - cp -r ./evm/src/template ./evm/src/ + cp ./evm/src/template ./evm/src/ ``` -2. Implement the `ISwapAdapter` interface in the `./evm/src/.sol` file. -3. Create tests for your implementation in the `./evm/test/.t.sol` file, again based on the template `./evm/test/TemplateSwapAdapter.t.sol`. +Implement the `ISwapAdapter` interface in the `./evm/src/.sol` file. There are two reference implementations, one for Uniswap V2 and the other for Balancer V2. +### Testing your implementation +Clone the `evm/test/TemplateSwapAdapter.t.sol` file and rename it to `.t.sol`. Implement the tests for your adapter, make sure all implemented functions are tested and working correctly. Look at the examples of `UniswapV2SwapAdapter.t.sol` and `BalancerV2SwapAdapter.t.sol` for reference. The [Foundry test guide](https://book.getfoundry.sh/forge/tests) is a good reference, especially the chapter for [Fuzz testing](https://book.getfoundry.sh/forge/fuzz-testing), which is used in both the Uniswap and Balancer tests. + +We are using fork testing, i.e. we are running a local Ethereum node and fork the mainnet state. This allows us to test the integration against the real contracts and real data. To run the tests, you need to set the `ETH_RPC_URL` environment variable to the URL of an ethereum RPC. It can be your own node or a public one, like [Alchemy](https://www.alchemy.com/) or [Infura](https://infura.io/). + +Finally, run the tests with: + ```bash + cd ./evm + forge test + ``` diff --git a/evm/src/balancer-v2/BalancerV2SwapAdapter.sol b/evm/src/balancer-v2/BalancerV2SwapAdapter.sol index 1b37799..56d1d6e 100644 --- a/evm/src/balancer-v2/BalancerV2SwapAdapter.sol +++ b/evm/src/balancer-v2/BalancerV2SwapAdapter.sol @@ -3,14 +3,13 @@ pragma experimental ABIEncoderV2; pragma solidity ^0.8.13; import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; -import "forge-std/Test.sol"; // Maximum Swap In/Out Ratio - 0.3 // https://balancer.gitbook.io/balancer/core-concepts/protocol/limitations#v2-limits uint256 constant RESERVE_LIMIT_FACTOR = 4; uint256 constant SWAP_DEADLINE_SEC = 1000; -contract BalancerV2SwapAdapter is ISwapAdapter, Test { +contract BalancerV2SwapAdapter is ISwapAdapter { IVault immutable vault; constructor(address payable vault_) { diff --git a/evm/src/interfaces/ISwapAdapter.sol b/evm/src/interfaces/ISwapAdapter.sol index 430ec98..178f2c6 100644 --- a/evm/src/interfaces/ISwapAdapter.sol +++ b/evm/src/interfaces/ISwapAdapter.sol @@ -68,7 +68,7 @@ interface ISwapAdapter is ISwapAdapterTypes { /// @notice Retrieves the limits for each token. /// @dev Retrieve the maximum limits of a token that can be traded. The /// limit is reached when the change in the received amounts is zero or - /// close to zero or when the swap fails because of the pools restrictions. + /// close to zero or when the swap fails because of the pools restrictions. /// Overestimate if in doubt rather than underestimate. The /// swap function should not error with `LimitExceeded` if called with /// amounts below the limit. diff --git a/evm/src/template/TemplateSwapAdapter.sol b/evm/src/template/TemplateSwapAdapter.sol index e69de29..5c8d638 100644 --- a/evm/src/template/TemplateSwapAdapter.sol +++ b/evm/src/template/TemplateSwapAdapter.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma experimental ABIEncoderV2; +pragma solidity ^0.8.13; + +import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; + +/// @title TemplateSwapAdapter +/// @dev This is a template for a swap adapter. +/// Rename it to your own protocol's name and implement it according to the +/// specification. +contract TemplateSwapAdapter is ISwapAdapter { + function price( + bytes32 _poolId, + IERC20 _sellToken, + IERC20 _buyToken, + uint256[] memory _specifiedAmounts + ) external view override returns (Fraction[] memory _prices) { + revert NotImplemented("TemplateSwapAdapter.price"); + } + + function swap( + bytes32 poolId, + IERC20 sellToken, + IERC20 buyToken, + OrderSide side, + uint256 specifiedAmount + ) external returns (Trade memory trade) { + revert NotImplemented("TemplateSwapAdapter.swap"); + } + + function getLimits(bytes32 poolId, IERC20 sellToken, IERC20 buyToken) + external + returns (uint256[] memory limits) + { + revert NotImplemented("TemplateSwapAdapter.getLimits"); + } + + function getCapabilities(bytes32 poolId, IERC20 sellToken, IERC20 buyToken) + external + returns (Capability[] memory capabilities) + { + revert NotImplemented("TemplateSwapAdapter.getCapabilities"); + } + + function getTokens(bytes32 poolId) + external + returns (IERC20[] memory tokens) + { + revert NotImplemented("TemplateSwapAdapter.getTokens"); + } + + function getPoolIds(uint256 offset, uint256 limit) + external + returns (bytes32[] memory ids) + { + revert NotImplemented("TemplateSwapAdapter.getPoolIds"); + } +} diff --git a/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol b/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol index fde236a..cd07683 100644 --- a/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol +++ b/evm/src/uniswap-v2/UniswapV2SwapAdapter.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.13; import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; -// Uniswap handles arbirary amounts, but we limit the amount to 10x just in case +// Uniswap handles arbirary amounts, but we limit the amount to 10x just in case uint256 constant RESERVE_LIMIT_FACTOR = 10; contract UniswapV2SwapAdapter is ISwapAdapter { @@ -119,7 +119,8 @@ contract UniswapV2SwapAdapter is ISwapAdapter { return amountOut; } - /// @notice Given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset + /// @notice Given an input amount of an asset and pair reserves, returns the + /// maximum output amount of the other asset /// @param amountIn The amount of the token being sold. /// @param reserveIn The reserve of the token being sold. /// @param reserveOut The reserve of the token being bought. @@ -173,7 +174,8 @@ contract UniswapV2SwapAdapter is ISwapAdapter { return amount; } - /// @notice Given an output amount of an asset and pair reserves, returns a required input amount of the other asset + /// @notice Given an output amount of an asset and pair reserves, returns a + /// required input amount of the other asset /// @param amountOut The amount of the token being bought. /// @param reserveIn The reserve of the token being sold. /// @param reserveOut The reserve of the token being bought. diff --git a/evm/test/BalancerV2SwapAdapter.t.sol b/evm/test/BalancerV2SwapAdapter.t.sol index 554e660..fcb6328 100644 --- a/evm/test/BalancerV2SwapAdapter.t.sol +++ b/evm/test/BalancerV2SwapAdapter.t.sol @@ -95,7 +95,8 @@ contract BalancerV2SwapAdapterTest is Test, ISwapAdapterTypes { if (side == OrderSide.Buy) { vm.assume(specifiedAmount < limits[1]); - // TODO calculate the amountIn by using price function as in testPriceDecreasing + // TODO calculate the amountIn by using price function as in + // testPriceDecreasing deal(address(BAL), address(this), type(uint256).max); BAL.approve(address(adapter), type(uint256).max); } else { diff --git a/evm/test/TemplateSwapAdapter.t.sol b/evm/test/TemplateSwapAdapter.t.sol new file mode 100644 index 0000000..8ace710 --- /dev/null +++ b/evm/test/TemplateSwapAdapter.t.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import "src/interfaces/ISwapAdapterTypes.sol"; +import "src/libraries/FractionMath.sol"; + +/// @title TemplateSwapAdapterTest +/// @dev This is a template for a swap adapter test. +/// Test all functions that are implemented in your swap adapter, the two test included here are just an example. +/// Feel free to use UniswapV2SwapAdapterTest and BalancerV2SwapAdapterTest as a reference. +contract TemplateSwapAdapterTest is Test, ISwapAdapterTypes { + using FractionMath for Fraction; + + function testPriceFuzz(uint256 amount0, uint256 amount1) public {} + + function testSwapFuzz(uint256 specifiedAmount) public {} +} \ No newline at end of file diff --git a/evm/test/UniswapV2SwapAdapter.t.sol b/evm/test/UniswapV2SwapAdapter.t.sol index 0c4762f..960d0e1 100644 --- a/evm/test/UniswapV2SwapAdapter.t.sol +++ b/evm/test/UniswapV2SwapAdapter.t.sol @@ -73,7 +73,8 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes { if (side == OrderSide.Buy) { vm.assume(specifiedAmount < limits[1]); - // TODO calculate the amountIn by using price function as in BalancerV2 testPriceDecreasing + // TODO calculate the amountIn by using price function as in + // BalancerV2 testPriceDecreasing deal(address(USDC), address(this), type(uint256).max); USDC.approve(address(adapter), type(uint256).max); } else {