From 092fce79f683159ca072f8558ddc2ca5467f335b Mon Sep 17 00:00:00 2001 From: pistomat Date: Fri, 1 Dec 2023 13:09:02 +0100 Subject: [PATCH] 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);