From 41f20f14b0c8dcda787f78de21d4c84c45938059 Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Thu, 29 Aug 2024 13:11:07 +0100 Subject: [PATCH] feat: Add propeller swap encoders - Add setup for package - Add docs - Add balancer implementation and test - Add CI: - Add setup action - Add test and format CI - Add CD: Publish python package to AWS --- .github/actions/setup_env/action.yaml | 32 +++++++++++ .../publish-swap-encoders-package.yaml | 29 ++++++++++ .github/workflows/python-tests.yaml | 32 +++++++++++ .github/workflows/swap-encoders.yaml | 32 +++++++++++ docs/README.md | 8 ++- docs/execution/swap-encoder.md | 57 +++++++++++++++++++ docs/execution/swap-executor.md | 6 +- propeller-swap-encoders/environment_dev.yaml | 5 ++ .../propeller_swap_encoders/__init__.py | 0 .../propeller_swap_encoders/balancer.py | 33 +++++++++++ .../tests/test_balancer.py | 48 ++++++++++++++++ propeller-swap-encoders/pyproject.toml | 35 ++++++++++++ propeller-swap-encoders/requirements.txt | 4 ++ 13 files changed, 318 insertions(+), 3 deletions(-) create mode 100644 .github/actions/setup_env/action.yaml create mode 100644 .github/workflows/publish-swap-encoders-package.yaml create mode 100644 .github/workflows/python-tests.yaml create mode 100644 .github/workflows/swap-encoders.yaml create mode 100644 docs/execution/swap-encoder.md create mode 100644 propeller-swap-encoders/environment_dev.yaml create mode 100644 propeller-swap-encoders/propeller_swap_encoders/__init__.py create mode 100644 propeller-swap-encoders/propeller_swap_encoders/balancer.py create mode 100644 propeller-swap-encoders/propeller_swap_encoders/tests/test_balancer.py create mode 100644 propeller-swap-encoders/pyproject.toml create mode 100644 propeller-swap-encoders/requirements.txt diff --git a/.github/actions/setup_env/action.yaml b/.github/actions/setup_env/action.yaml new file mode 100644 index 0000000..4ab1828 --- /dev/null +++ b/.github/actions/setup_env/action.yaml @@ -0,0 +1,32 @@ +name: Setup/Cache Env +description: 'Sets up and caches a python env. Will only install dependencies if no cache was hit.' + +runs: + using: composite + steps: + - name: Set up Python 3.9 + uses: actions/setup-python@v4 + with: + python-version: "3.9" + + - name: Cache Env + uses: actions/cache@v3 + id: env-cache + with: + path: ${{ env.pythonLocation }} + key: ${{ env.pythonLocation }}-${{ hashFiles('./requirements/requirements_dev.txt') }}-${{ hashFiles('./requirements/requirements_data.txt') }}-${{ hashFiles('./requirements/requirements_api.txt') }}-${{ hashFiles('./requirements/requirements.txt') }}-${{ hashFiles('./requirements/requirements_internal.txt') }} + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4.0.1 + with: + role-to-assume: arn:aws:iam::827659017777:role/github-actions + audience: sts.amazonaws.com + aws-region: eu-central-1 + + - name: Install Dependencies + if: ${{ steps.env-cache.outputs.cache-hit != 'true' }} + run: | + aws codeartifact login --tool pip --domain propeller --domain-owner 827659017777 --repository protosim + python -m pip install --upgrade pip + pip install -r propeller-swap-encoders/requirements.txt + shell: bash diff --git a/.github/workflows/publish-swap-encoders-package.yaml b/.github/workflows/publish-swap-encoders-package.yaml new file mode 100644 index 0000000..55f8c39 --- /dev/null +++ b/.github/workflows/publish-swap-encoders-package.yaml @@ -0,0 +1,29 @@ +name: Publish Propeller Swap Encoders Python Packages to AWS CodeArtifact + +on: + release: + types: + - prereleased + - released + workflow_dispatch: { } + +permissions: + id-token: write + contents: read + +jobs: + publish_propeller_solver_core: + uses: propeller-heads/ci-cd-templates/.github/workflows/release-python-package.yaml@main + permissions: + id-token: write + contents: read + with: + package_root: "propeller-solver-core" + + publish_propeller_swap_encoders: + uses: propeller-heads/ci-cd-templates/.github/workflows/release-python-package.yaml@main + permissions: + id-token: write + contents: read + with: + package_root: "propeller-swap-encoders" diff --git a/.github/workflows/python-tests.yaml b/.github/workflows/python-tests.yaml new file mode 100644 index 0000000..ab7b12a --- /dev/null +++ b/.github/workflows/python-tests.yaml @@ -0,0 +1,32 @@ +name: Test code using pytest + +on: + workflow_call: + inputs: + runs_on: + required: false + type: string + default: ubuntu-latest + timeout_minutes: + required: false + type: number + default: 15 + +jobs: + test-python: + runs-on: "${{ inputs.runs_on }}" + timeout-minutes: "${{ inputs.timeout_minutes }}" + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Env + uses: ./.github/actions/setup_env + + - name: Test with pytest + id: tests + run: | + export PYTHONPATH=$PYTHONPATH:$GITHUB_WORKSPACE/propeller-swap-encoders + pytest --disable-warnings ./propeller-swap-encoders diff --git a/.github/workflows/swap-encoders.yaml b/.github/workflows/swap-encoders.yaml new file mode 100644 index 0000000..5378523 --- /dev/null +++ b/.github/workflows/swap-encoders.yaml @@ -0,0 +1,32 @@ +name: Swap encoders CI + +on: + pull_request: + +permissions: + id-token: write + contents: read + +env: + PYTEST_ADDOPTS: "--color=yes" + +jobs: + tests: + uses: propeller-heads/propeller-protocol-lib/.github/workflows/python-tests.yaml@dc/ENG-3545-make-encoders-lib + + formatting: + name: Formatting + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Check out Repo + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Env + uses: ./.github/actions/setup_env + + - name: Black Formatting + run: | + black ./propeller-swap-encoders --check --skip-magic-trailing-comma diff --git a/docs/README.md b/docs/README.md index ae0f704..20dd4a5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -28,5 +28,9 @@ For indexing purposes, it is required that you provide a [substreams](https://su ### Execution -For execution purposes, the implementation of the `SwapExecutor` interface is required. Without this component, trades cannot be executed on-chain, making it a critical part of the integration. -The `SwapExecutor` is responsible for performing swaps by interacting with the underlying liquidity pools, handling token approvals, managing input/output amounts, and ensuring gas-efficient and secure execution. Each protocol must implement its own `SwapExecutor`, tailored to its specific logic and requirements. +For execution purposes, the implementation of the `SwapExecutor` and `SwapStructEncoder` interfaces is required. Without these components, trades cannot be executed on-chain, making them critical parts of the integration. + +**SwapExecutor**: The SwapExecutor is responsible for performing swaps by interacting with the underlying liquidity pools, handling token approvals, managing input/output amounts, and ensuring gas-efficient and secure execution. Each protocol must implement its own `SwapExecutor`, tailored to its specific logic and requirements. + +**SwapStructEncoder**: The `SwapStructEncoder` encodes the necessary data structures required for the `SwapExecutor` to perform swaps. It ensures that the swap details, including input/output tokens, pool addresses, and other protocol-specific parameters, are correctly formatted and encoded before being passed to the `SwapExecutor`. Each protocol must implement its own `SwapStructEncoder` and ensure compatibility with its `SwapExecutor`. + diff --git a/docs/execution/swap-encoder.md b/docs/execution/swap-encoder.md new file mode 100644 index 0000000..8d8b8ba --- /dev/null +++ b/docs/execution/swap-encoder.md @@ -0,0 +1,57 @@ +# Implementing a SwapExecutor for a Protocol + +## Overview + +The `SwapStructEncoder` interface is designed to encode the necessary data for a swap, which will be used by the `SwapExecutor` to interact with a liquidity pool. The encoder is responsible for structuring the swap details, including the input/output tokens, pool addresses, and any additional protocol-specific parameters. + +Each protocol must implement its own `SwapStructEncoder` and ensure that the swap data is correctly encoded for the `SwapExecutor`. + +### Dev environment + +- Run `aws codeartifact login --tool pip --repository protosim --domain propeller` so you can access the propeller packages. +- Create the dev environment `conda env create -f propeller-swap-encoders/environment_dev.yaml` +- Activate it with `conda activate propeller-swap-encoders` +- Install dependencies with `pip install -r propeller-swap-encoders/requirements.txt` +- Should you get a pyyaml installation error execute the following command: `pip install "cython<3.0.0" && pip install --no-build-isolation pyyaml==5.4.1` + +You can import the abstract class `SwapStructEncoder` from `propeller-solver-core` in your python code like: +```python +from core.encoding.interface import SwapStructEncoder +``` + +## Key Methods + +This is the `SwapStructEncoder` interface: + +```python +class SwapStructEncoder(ABC): + """Encodes a PercentageSwap of a certain protocol to be used in our SwapRouterV2 + Should be subclassed for each protocol that we support. + """ + + @abstractmethod + def encode_swap_struct( + self, swap: dict[str, Any], receiver: Address, **kwargs + ) -> bytes: + pass +``` + +- **encode_swap_struct** + - **Purpose**: To encode the swap details into a bytes object that the SwapExecutor can use to execute the swap. + - **Parameters**: + - `swap`: A dictionary containing the swap details, such as input/output token addresses, amounts, and pool information. + - `receiver`: The address that will receive the output tokens from the swap. + - `**kwargs`: Any additional protocol-specific parameters that need to be included in the encoding. + - **Returns**: A bytes object containing the encoded swap data. + +## Implementation Steps + +1. **Define Protocol-Specific Encoding Logic**: Implement the `encode_swap_struct` function to encode the swap details specific to the protocol. This may include encoding token addresses, pool addresses, and other necessary parameters into a bytes format. +2. **Compatibility with SwapExecutor**: Ensure that the encoded data is compatible with the `SwapExecutor` implementation for the protocol. The `SwapExecutor` will rely on this data to perform the swap accurately. +3. **Testing**: Thoroughly test the encoding process with various swap scenarios to ensure that the encoded data is correct and that the `SwapExecutor` can process it without errors. + + + +## Example Implementation + +See the example implementation of a `SwapExecutor` for Balancer [here](../../propeller-swap-encoders/propeller_swap_encoders/balancer.py) and test [here](../../propeller-swap-encoders/propeller_swap_encoders/tests/test_balancer.py). \ No newline at end of file diff --git a/docs/execution/swap-executor.md b/docs/execution/swap-executor.md index 87f2556..5d065c2 100644 --- a/docs/execution/swap-executor.md +++ b/docs/execution/swap-executor.md @@ -7,13 +7,17 @@ It allows for flexible interaction by accepting either the amount of the input t as parameters, returning the corresponding swapped amount. This interface is essential for creating a `SwapExecutor` specific to a protocol. +The `SwapExecutor` works in conjunction with the `SwapStructEncoder`, which encodes the necessary data required for the swap. +This encoded data is passed to the `SwapExecutor`, enabling it to perform the swap according to the protocol's specific logic. + ## Key Methods - **swap(uint256 givenAmount, bytes calldata data)** - **Purpose**: To perform a token swap, either specifying the input amount to get the output amount or vice versa. - **Parameters**: - `givenAmount`: The amount of the token (input or output) for the swap. - - `data`: Encoded information necessary for the swap (e.g., pool address, token addresses - depends on the protocol). + - `data`: Encoded information necessary for the swap (e.g., pool address, token addresses - depends on the protocol), +provided by the `SwapStructEncoder`. - **Returns**: The amount of the token swapped. ## Implementation Steps diff --git a/propeller-swap-encoders/environment_dev.yaml b/propeller-swap-encoders/environment_dev.yaml new file mode 100644 index 0000000..041f2cc --- /dev/null +++ b/propeller-swap-encoders/environment_dev.yaml @@ -0,0 +1,5 @@ +name: propeller-swap-encoders +channels: + - conda-forge +dependencies: + - python=3.9 diff --git a/propeller-swap-encoders/propeller_swap_encoders/__init__.py b/propeller-swap-encoders/propeller_swap_encoders/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/propeller-swap-encoders/propeller_swap_encoders/balancer.py b/propeller-swap-encoders/propeller_swap_encoders/balancer.py new file mode 100644 index 0000000..1b9a2b2 --- /dev/null +++ b/propeller-swap-encoders/propeller_swap_encoders/balancer.py @@ -0,0 +1,33 @@ +from typing import Any + +from core.encoding.interface import SwapStructEncoder +from core.type_aliases import Address +from eth_abi.packed import encode_abi_packed +from hexbytes import HexBytes + + +class BalancerSwapStructEncoder(SwapStructEncoder): + def encode_swap_struct( + self, swap: dict[str, Any], receiver: Address, exact_out: bool, **kwargs + ) -> bytes: + """ + Parameters: + ---------- + swap + The swap to encode + receiver + The receiver of the buy token + exact_out + Whether the amount encoded is the exact amount out + """ + return encode_abi_packed( + ["address", "address", "bytes32", "address", "bool", "bool"], + [ + swap["sell_token"].address, + swap["buy_token"].address, + HexBytes(swap["pool_id"]), + receiver, + exact_out, + swap["token_approval_needed"], + ], + ) diff --git a/propeller-swap-encoders/propeller_swap_encoders/tests/test_balancer.py b/propeller-swap-encoders/propeller_swap_encoders/tests/test_balancer.py new file mode 100644 index 0000000..6a226ad --- /dev/null +++ b/propeller-swap-encoders/propeller_swap_encoders/tests/test_balancer.py @@ -0,0 +1,48 @@ +from core.models.evm.ethereum_token import EthereumToken +from propeller_swap_encoders.balancer import BalancerSwapStructEncoder + + +def test_encode_balancer(): + WETH = EthereumToken( + symbol="WETH", + address="0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + decimals=18, + gas=0, + ) + DAI = EthereumToken( + symbol="DAI", address="0x6b175474e89094c44da98b954eedeac495271d0f", decimals=18 + ) + bob = "0x000000000000000000000000000000000000007B" + swap = { + "pool_id": "0x06df3b2bbb68adc8b0e302443692037ed9f91b42000000000000000000000063", + "sell_token": DAI, + "buy_token": WETH, + "split": 0, + "sell_amount": 0, + "buy_amount": 100, + "token_approval_needed": False, + "pool_tokens": (), + "pool_type": "BalancerStablePoolState", + "curve_v2_pool_type": None, + "is_curve_tricrypto": None, + "quote": None, + "pool_fee": None, + } + balancer_encoder = BalancerSwapStructEncoder() + encoded = balancer_encoder.encode_swap_struct(swap, receiver=bob, exact_out=False) + assert ( + encoded.hex() + == + # sell token + "6b175474e89094c44da98b954eedeac495271d0f" + # buy token + "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" + # pool address + "06df3b2bbb68adc8b0e302443692037ed9f91b42000000000000000000000063" + # receiver + "000000000000000000000000000000000000007b" + # exact_out + "00" + # token_approval_needed + "00" + ) diff --git a/propeller-swap-encoders/pyproject.toml b/propeller-swap-encoders/pyproject.toml new file mode 100644 index 0000000..a187468 --- /dev/null +++ b/propeller-swap-encoders/pyproject.toml @@ -0,0 +1,35 @@ +[build-system] +requires = ["setuptools>=60", "setuptools-scm>=8.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "propeller-swap-encoders" +version = "0.1.0" +description = "" +authors = [ + { name = "Propeller Heads" } +] +keywords = ["propeller-swap-encoders"] +classifiers = [ + "Intended Audience :: Developers", + "Natural Language :: English", + "Programming Language :: Python :: 3.7", +] +readme = "README.md" +requires-python = ">=3.7" +dependencies = [ + "propeller-solver-core==0.1.0", +] + +[tool.setuptools.packages.find] +include = ["propeller_swap_encoders"] + +[project.optional-dependencies] +testing = [ + "pytest", + "pytest-runner" +] + +[project.urls] +homepage = "https://github.com/propeller-heads/propeller-protocol-lib" + diff --git a/propeller-swap-encoders/requirements.txt b/propeller-swap-encoders/requirements.txt new file mode 100644 index 0000000..a8b2dac --- /dev/null +++ b/propeller-swap-encoders/requirements.txt @@ -0,0 +1,4 @@ +propeller-solver-core==0.1.0 +pyyaml==5.4.1 +pytest==6.2.4 +black==24.4.2 \ No newline at end of file