From a7798374e1cc3cf949242581703d86986c98ba16 Mon Sep 17 00:00:00 2001 From: pistomat Date: Thu, 7 Dec 2023 17:44:47 +0100 Subject: [PATCH] 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 {