From 589bdb6fc8c63f79ba3ae6f35c00472352602b81 Mon Sep 17 00:00:00 2001 From: Nicholas Rodrigues Lordello Date: Tue, 31 Jan 2023 10:11:50 +0100 Subject: [PATCH 01/21] Fix Clippy Lints --- src/configuration.rs | 18 ++++++++-------- src/cow_api_client.rs | 43 +++++++++++++++++++++++++------------ src/encoder.rs | 36 ++++++++++++++++++++++--------- src/ethereum_client.rs | 39 +++++++++++++++++----------------- src/main.rs | 48 +++++++++++++++++++++--------------------- 5 files changed, 108 insertions(+), 76 deletions(-) diff --git a/src/configuration.rs b/src/configuration.rs index 6f0436f..dcf04ee 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -1,4 +1,4 @@ -use anyhow::{Context, Result, anyhow}; +use anyhow::{anyhow, Result}; use ethers::types::Address; use log::debug; use std::env; @@ -22,13 +22,16 @@ impl Configuration { let node_base_url = collect_optional_environment_variable("NODE_BASE_URL")?; if infura_api_key.is_none() && node_base_url.is_none() { - return Err(anyhow!("either `infura_api_key` or `node_base_url` must be set")) + return Err(anyhow!( + "either `infura_api_key` or `node_base_url` must be set" + )); } let network = collect_optional_environment_variable("MILKMAN_NETWORK")? - .unwrap_or("mainnet".to_string()); + .unwrap_or_else(|| "mainnet".to_string()); let milkman_address = collect_optional_environment_variable("MILKMAN_ADDRESS")? - .unwrap_or(PROD_MILKMAN_ADDRESS.to_string()) + .as_deref() + .unwrap_or(PROD_MILKMAN_ADDRESS) .parse()?; let polling_frequency_secs = collect_optional_environment_variable("POLLING_FREQUENCY_SECS")? @@ -36,7 +39,8 @@ impl Configuration { .transpose()? .unwrap_or(10); let hash_helper_address = collect_optional_environment_variable("HASH_HELPER_ADDRESS")? - .unwrap_or(MAINNET_HASH_HELPER_ADDRESS.to_string()) + .as_deref() + .unwrap_or(MAINNET_HASH_HELPER_ADDRESS) .parse()?; let starting_block_number = @@ -57,10 +61,6 @@ impl Configuration { } } -fn collect_required_environment_variable(key: &str) -> Result { - Ok(env::var(key).context(format!("required environment variable {} not set", key))?) -} - fn collect_optional_environment_variable(key: &str) -> Result> { match env::var(key) { Ok(value) => Ok(Some(value)), diff --git a/src/cow_api_client.rs b/src/cow_api_client.rs index 6114cd6..31ea372 100644 --- a/src/cow_api_client.rs +++ b/src/cow_api_client.rs @@ -1,5 +1,5 @@ use crate::configuration::Configuration; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context, Result}; use ethers::abi::Address; use ethers::types::{Bytes, U256}; use log::{debug, info}; @@ -14,6 +14,19 @@ pub struct Quote { pub valid_to: u64, } +#[derive(Debug)] +pub struct Order<'a> { + pub order_contract: Address, + pub sell_token: Address, + pub buy_token: Address, + pub sell_amount: U256, + pub buy_amount: U256, + pub valid_to: u64, + pub fee_amount: U256, + pub receiver: Address, + pub eip_1271_signature: &'a Bytes, +} + pub struct CowAPIClient { pub base_url: String, pub milkman_address: String, @@ -73,15 +86,15 @@ impl CowAPIClient { let quote = &response_body["quote"]; let fee_amount = quote["feeAmount"] .as_str() - .ok_or(anyhow!("unable to get `feeAmount` on quote"))? + .context("unable to get `feeAmount` on quote")? .to_owned(); let buy_amount_after_fee = quote["buyAmount"] .as_str() - .ok_or(anyhow!("unable to get `buyAmountAfterFee` from quote"))? + .context("unable to get `buyAmountAfterFee` from quote")? .to_owned(); let valid_to = quote["validTo"] .as_u64() - .ok_or(anyhow!("unable to get `validTo` from quote"))?; + .context("unable to get `validTo` from quote")?; Ok(Quote { fee_amount: fee_amount.parse::()?.into(), @@ -92,15 +105,17 @@ impl CowAPIClient { pub async fn create_order( &self, - order_contract: Address, - sell_token: Address, - buy_token: Address, - sell_amount: U256, - buy_amount: U256, - valid_to: u64, - fee_amount: U256, - receiver: Address, - eip_1271_signature: &Bytes, + Order { + order_contract, + sell_token, + buy_token, + sell_amount, + buy_amount, + valid_to, + fee_amount, + receiver, + eip_1271_signature, + }: Order<'_>, ) -> Result { let http_client = reqwest::Client::new(); let response = http_client @@ -130,7 +145,7 @@ impl CowAPIClient { .json::() .await? .as_str() - .ok_or(anyhow!("Unable to retrieve UID from POST order response"))? + .context("Unable to retrieve UID from POST order response")? .to_string(), Err(err) => { debug!("POST order failed with body: {:?}", response.text().await?); diff --git a/src/encoder.rs b/src/encoder.rs index 4f16b31..09ab592 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -4,17 +4,33 @@ use hex::FromHex; use crate::constants::{APP_DATA, ERC20_BALANCE, KIND_SELL}; +#[derive(Debug)] +pub struct SignatureData<'a> { + pub from_token: Address, + pub to_token: Address, + pub receiver: Address, + pub sell_amount_after_fees: U256, + pub buy_amount_after_fees_and_slippage: U256, + pub valid_to: u64, + pub fee_amount: U256, + pub order_creator: Address, + pub price_checker: Address, + pub price_checker_data: &'a Bytes, +} + pub fn get_eip_1271_signature( - from_token: Address, - to_token: Address, - receiver: Address, - sell_amount_after_fees: U256, - buy_amount_after_fees_and_slippage: U256, - valid_to: u64, - fee_amount: U256, - order_creator: Address, - price_checker: Address, - price_checker_data: &Bytes, + SignatureData { + from_token, + to_token, + receiver, + sell_amount_after_fees, + buy_amount_after_fees_and_slippage, + valid_to, + fee_amount, + order_creator, + price_checker, + price_checker_data, + }: SignatureData<'_>, ) -> Bytes { abi::encode(&vec![ Token::Address(from_token), diff --git a/src/ethereum_client.rs b/src/ethereum_client.rs index a8e9a1f..7e0839f 100644 --- a/src/ethereum_client.rs +++ b/src/ethereum_client.rs @@ -1,5 +1,4 @@ -use crate::types::{BlockNumber, Swap}; -use anyhow::{anyhow, Result}; +use anyhow::{Context, Result}; use ethers::prelude::*; use hex::FromHex; use log::debug; @@ -10,7 +9,8 @@ use std::sync::Arc; use crate::configuration::Configuration; use crate::constants::{APP_DATA, ERC20_BALANCE, KIND_SELL}; -use crate::encoder; +use crate::encoder::{self, SignatureData}; +use crate::types::{BlockNumber, Swap}; abigen!( RawMilkman, @@ -46,7 +46,8 @@ impl EthereumClient { } else { format!( "https://{}.infura.io/v3/{}", - config.network, config.infura_api_key.clone().unwrap() + config.network, + config.infura_api_key.clone().unwrap() ) }; let provider = Arc::new(Provider::::try_from(node_url)?); @@ -61,10 +62,11 @@ impl EthereumClient { self.get_latest_block() .await? .number - .ok_or(anyhow!("Error extracting number from latest block.")) + .context("Error extracting number from latest block.") .map(|block_num: U64| block_num.try_into().unwrap()) // U64 -> u64 should always work } + #[cfg(test)] pub async fn get_chain_timestamp(&self) -> Result { Ok(self.get_latest_block().await?.timestamp.as_u64()) } @@ -73,7 +75,7 @@ impl EthereumClient { self.inner_client .get_block(ethers::core::types::BlockNumber::Latest) .await? - .ok_or(anyhow!("Error fetching latest block.")) + .context("Error fetching latest block.") } pub async fn get_requested_swaps( @@ -135,18 +137,18 @@ impl EthereumClient { .call() .await?; - let mock_signature = encoder::get_eip_1271_signature( - swap_request.from_token, - swap_request.to_token, - swap_request.receiver, - swap_request.amount_in, - U256::MAX, - u32::MAX as u64, - U256::zero(), - swap_request.order_creator, - swap_request.price_checker, - &swap_request.price_checker_data, - ); + let mock_signature = encoder::get_eip_1271_signature(SignatureData { + from_token: swap_request.from_token, + to_token: swap_request.to_token, + receiver: swap_request.receiver, + sell_amount_after_fees: swap_request.amount_in, + buy_amount_after_fees_and_slippage: U256::MAX, + valid_to: u32::MAX as u64, + fee_amount: U256::zero(), + order_creator: swap_request.order_creator, + price_checker: swap_request.price_checker, + price_checker_data: &swap_request.price_checker_data, + }); debug!( "Is valid sig? {:?}", @@ -161,7 +163,6 @@ impl EthereumClient { .estimate_gas() .await?) } - } impl From<&SwapRequestedFilter> for Swap { diff --git a/src/main.rs b/src/main.rs index dc0bce1..d668607 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,9 +11,10 @@ mod ethereum_client; use crate::ethereum_client::EthereumClient; mod cow_api_client; -use crate::cow_api_client::CowAPIClient; +use crate::cow_api_client::{CowAPIClient, Order}; mod encoder; +use crate::encoder::SignatureData; mod types; use crate::types::Swap; @@ -77,7 +78,7 @@ async fn main() { } }; - if requested_swaps.len() > 0 { + if !requested_swaps.is_empty() { info!( "Found {} requested swaps between blocks {} and {}", requested_swaps.len(), @@ -151,32 +152,31 @@ async fn main() { let sell_amount_after_fees = requested_swap.amount_in - quote.fee_amount; let buy_amount_after_fees_and_slippage = quote.buy_amount_after_fee * 995 / 1000; - let eip_1271_signature = encoder::get_eip_1271_signature( - requested_swap.from_token, - requested_swap.to_token, - requested_swap.receiver, + let eip_1271_signature = encoder::get_eip_1271_signature(SignatureData { + from_token: requested_swap.from_token, + to_token: requested_swap.to_token, + receiver: requested_swap.receiver, sell_amount_after_fees, buy_amount_after_fees_and_slippage, - quote.valid_to, - quote.fee_amount, - requested_swap.order_creator, - requested_swap.price_checker, - &requested_swap.price_checker_data, - ); - + valid_to: quote.valid_to, + fee_amount: quote.fee_amount, + order_creator: requested_swap.order_creator, + price_checker: requested_swap.price_checker, + price_checker_data: &requested_swap.price_checker_data, + }); match cow_api_client - .create_order( - requested_swap.order_contract, - requested_swap.from_token, - requested_swap.to_token, - sell_amount_after_fees, - buy_amount_after_fees_and_slippage, - quote.valid_to, - quote.fee_amount, - requested_swap.receiver, - &eip_1271_signature, - ) + .create_order(Order { + order_contract: requested_swap.order_contract, + sell_token: requested_swap.from_token, + buy_token: requested_swap.to_token, + sell_amount: sell_amount_after_fees, + buy_amount: buy_amount_after_fees_and_slippage, + valid_to: quote.valid_to, + fee_amount: quote.fee_amount, + receiver: requested_swap.receiver, + eip_1271_signature: &eip_1271_signature, + }) .await { Ok(_) => (), From 6607430ce280dd62f6634b75e958dcc5c6d1cbb9 Mon Sep 17 00:00:00 2001 From: Nicholas Rodrigues Lordello Date: Tue, 31 Jan 2023 10:17:36 +0100 Subject: [PATCH 02/21] Fail CI on build warnings --- .github/workflows/test.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 71e0c2c..50a5b43 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -11,7 +11,10 @@ jobs: # Shrink artifact size by not including debug info. Makes build faster and shrinks cache. CARGO_PROFILE_DEV_DEBUG: 0 CARGO_PROFILE_TEST_DEBUG: 0 + # Error build on warning (including clippy lints) + RUSTFLAGS: "-Dwarnings" steps: - uses: actions/checkout@v2 - uses: Swatinem/rust-cache@v1 - - run: cargo test \ No newline at end of file + - run: cargo test + - run: cargo clippy From 7c1ce7b0e0d19c8ba5c5934b5727a12f1873db19 Mon Sep 17 00:00:00 2001 From: Nicholas Rodrigues Lordello Date: Tue, 31 Jan 2023 10:43:07 +0100 Subject: [PATCH 03/21] Add Docker Deployment Workflow --- .github/workflows/deploy.yaml | 37 +++++++++++++++++++++++++++++++++++ Dockerfile | 16 ++++++++------- 2 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/deploy.yaml diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml new file mode 100644 index 0000000..5813a91 --- /dev/null +++ b/.github/workflows/deploy.yaml @@ -0,0 +1,37 @@ +name: deploy +on: + push: + branches: [main] + tags: [v*] + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v3 + + - uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Metadata + id: meta_services + uses: docker/metadata-action@v3 + with: + images: ghcr.io/${{ github.repository }} + labels: | + org.opencontainers.image.licenses=UNLICENSED + - name: Build + uses: docker/build-push-action@v2 + with: + context: . + file: Dockerfile + push: true + tags: ${{ steps.meta_services.outputs.tags }} + labels: ${{ steps.meta_services.outputs.labels }} diff --git a/Dockerfile b/Dockerfile index 8553b39..afe832d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,23 +8,25 @@ RUN apt-get update && apt-get install -y git libssl-dev pkg-config COPY Cargo.toml /tmp/milkman-bot COPY Cargo.lock /tmp/milkman-bot +ENV CARGO_PROFILE_RELEASE_DEBUG=1 + # To cache dependencies, create a layer that compiles dependencies and some rust src that won't change, # and then another layer that compiles our source. RUN echo 'fn main() {}' >> /tmp/milkman-bot/dummy.rs RUN sed -i 's|src/main.rs|dummy.rs|g' Cargo.toml -RUN env CARGO_PROFILE_RELEASE_DEBUG=1 cargo build --release +RUN cargo build --release RUN sed -i 's|dummy.rs|src/main.rs|g' Cargo.toml COPY . /tmp/milkman-bot -RUN env CARGO_PROFILE_RELEASE_DEBUG=1 cargo build --release +RUN cargo build --release -FROM docker.io/debian:stable +FROM docker.io/debian:bullseye-slim -COPY --from=cargo-build /tmp/milkman-bot/target/release/milkman-bot / -COPY --from=cargo-build /tmp/milkman-bot /project/ +COPY --from=cargo-build /tmp/milkman-bot/target/release/milkman-bot /usr/bin/ WORKDIR / -RUN apt-get update && apt-get install -y libssl-dev git +RUN apt-get update && apt-get install -y ca-certificates tini -CMD ["./milkman-bot"] +ENTRYPOINT ["tini", "--"] +CMD ["milkman-bot"] From 6b1c62342d06ede023483a92b07dd6a6851d31d2 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Wed, 1 Feb 2023 08:27:29 +0800 Subject: [PATCH 04/21] chore: test bullseye slim --- .circleci/config.yml | 4 ++-- Dockerfile | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index ca85a61..025f3ea 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -19,10 +19,10 @@ jobs: - run: "docker login -u $DOCKER_HUB_USERNAME -p $DOCKER_HUB_PASSWORD" - run: name: "Build image" - command: "docker build . -t charlesdalton/milkman-bot:latest" + command: "docker build . -t charlesdalton/milkman-bot:slim-latest" - run: name: "Push image to DockerHub" - command: "docker push charlesdalton/milkman-bot:latest" + command: "docker push charlesdalton/milkman-bot:slim-latest" # Invoke jobs via workflows # See: https://circleci.com/docs/2.0/configuration-reference/#workflows diff --git a/Dockerfile b/Dockerfile index 8553b39..dda043c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,12 +19,12 @@ RUN sed -i 's|dummy.rs|src/main.rs|g' Cargo.toml COPY . /tmp/milkman-bot RUN env CARGO_PROFILE_RELEASE_DEBUG=1 cargo build --release -FROM docker.io/debian:stable +FROM docker.io/debian:bullseye-slim COPY --from=cargo-build /tmp/milkman-bot/target/release/milkman-bot / COPY --from=cargo-build /tmp/milkman-bot /project/ WORKDIR / -RUN apt-get update && apt-get install -y libssl-dev git +RUN apt-get update && apt-get install -y libssl-dev ca-certificates CMD ["./milkman-bot"] From 114c855f4959a5bfe3b6ca719e32848f4b0f92b7 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Wed, 1 Feb 2023 08:46:01 +0800 Subject: [PATCH 05/21] Revert "chore: test bullseye slim" This reverts commit 6b1c62342d06ede023483a92b07dd6a6851d31d2. --- .circleci/config.yml | 4 ++-- Dockerfile | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 025f3ea..ca85a61 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -19,10 +19,10 @@ jobs: - run: "docker login -u $DOCKER_HUB_USERNAME -p $DOCKER_HUB_PASSWORD" - run: name: "Build image" - command: "docker build . -t charlesdalton/milkman-bot:slim-latest" + command: "docker build . -t charlesdalton/milkman-bot:latest" - run: name: "Push image to DockerHub" - command: "docker push charlesdalton/milkman-bot:slim-latest" + command: "docker push charlesdalton/milkman-bot:latest" # Invoke jobs via workflows # See: https://circleci.com/docs/2.0/configuration-reference/#workflows diff --git a/Dockerfile b/Dockerfile index dda043c..8553b39 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,12 +19,12 @@ RUN sed -i 's|dummy.rs|src/main.rs|g' Cargo.toml COPY . /tmp/milkman-bot RUN env CARGO_PROFILE_RELEASE_DEBUG=1 cargo build --release -FROM docker.io/debian:bullseye-slim +FROM docker.io/debian:stable COPY --from=cargo-build /tmp/milkman-bot/target/release/milkman-bot / COPY --from=cargo-build /tmp/milkman-bot /project/ WORKDIR / -RUN apt-get update && apt-get install -y libssl-dev ca-certificates +RUN apt-get update && apt-get install -y libssl-dev git CMD ["./milkman-bot"] From a37a2c5538b50f2fa1b10d335f0bd111b22cfde8 Mon Sep 17 00:00:00 2001 From: Charles <95452384+charlesndalton@users.noreply.github.com> Date: Wed, 1 Feb 2023 09:00:16 +0800 Subject: [PATCH 06/21] chore: add license --- LICENSE | 504 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 504 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8000a6f --- /dev/null +++ b/LICENSE @@ -0,0 +1,504 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random + Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! From 3a05cc28c5f13f1ddf846d896c55c89fe81a40fa Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Wed, 1 Feb 2023 09:13:49 +0800 Subject: [PATCH 07/21] chore: try alpine instead of debian --- .gitignore | 1 + Dockerfile | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index ea8c4bf..0592392 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +.DS_Store diff --git a/Dockerfile b/Dockerfile index afe832d..9e9a1a8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,12 +21,12 @@ RUN sed -i 's|dummy.rs|src/main.rs|g' Cargo.toml COPY . /tmp/milkman-bot RUN cargo build --release -FROM docker.io/debian:bullseye-slim +FROM docker.io/alpine:3.17 COPY --from=cargo-build /tmp/milkman-bot/target/release/milkman-bot /usr/bin/ WORKDIR / -RUN apt-get update && apt-get install -y ca-certificates tini +RUN apk --no-cache add ca-certificates tini ENTRYPOINT ["tini", "--"] CMD ["milkman-bot"] From 54ef83c49a643b6b79b6af85f74d9eff7241b919 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Wed, 1 Feb 2023 09:43:12 +0800 Subject: [PATCH 08/21] chore: add license to deploy.yaml --- .github/workflows/deploy.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 5813a91..3026e57 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -26,7 +26,7 @@ jobs: with: images: ghcr.io/${{ github.repository }} labels: | - org.opencontainers.image.licenses=UNLICENSED + org.opencontainers.image.licenses=LGPL-2.0-or-later - name: Build uses: docker/build-push-action@v2 with: From 23fde0f9e1b2e0b287ef5f2a201d32ec35348f40 Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Wed, 1 Feb 2023 11:36:24 +0800 Subject: [PATCH 09/21] fix: revert back to bullseye --- Dockerfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9e9a1a8..f62a2a9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ COPY Cargo.lock /tmp/milkman-bot ENV CARGO_PROFILE_RELEASE_DEBUG=1 -# To cache dependencies, create a layer that compiles dependencies and some rust src that won't change, +# To cache dependencies, create a layer that compiles dependencies and some rust src that won't change, # and then another layer that compiles our source. RUN echo 'fn main() {}' >> /tmp/milkman-bot/dummy.rs @@ -21,12 +21,12 @@ RUN sed -i 's|dummy.rs|src/main.rs|g' Cargo.toml COPY . /tmp/milkman-bot RUN cargo build --release -FROM docker.io/alpine:3.17 +FROM docker.io/debian:bullseye-slim COPY --from=cargo-build /tmp/milkman-bot/target/release/milkman-bot /usr/bin/ WORKDIR / -RUN apk --no-cache add ca-certificates tini +RUN apt-get update && apt-get install -y ca-certificates tini ENTRYPOINT ["tini", "--"] -CMD ["milkman-bot"] +CMD ["milkman-bot"] \ No newline at end of file From 6c0487936ad9fd92925dd651ddbb405e75d9ea3d Mon Sep 17 00:00:00 2001 From: charlesndalton Date: Wed, 1 Feb 2023 11:37:30 +0800 Subject: [PATCH 10/21] chore: update deployment yaml to use ghcr --- milkman-bot-deployment.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/milkman-bot-deployment.yaml b/milkman-bot-deployment.yaml index fb2bcb2..b51700d 100644 --- a/milkman-bot-deployment.yaml +++ b/milkman-bot-deployment.yaml @@ -16,7 +16,7 @@ spec: spec: containers: - name: milkman-bot - image: charlesdalton/milkman-bot:latest + image: ghcr.io/charlesndalton/milkman-bot:main env: - name: RUST_LOG value: "INFO" From 1748cbcf00691220b11ceda4226c9fc20ccdaaa1 Mon Sep 17 00:00:00 2001 From: Felix Leupold Date: Thu, 1 Jun 2023 21:51:00 +0200 Subject: [PATCH 11/21] Make slippage tolerance configurable --- src/configuration.rs | 8 ++++++++ src/main.rs | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/configuration.rs b/src/configuration.rs index dcf04ee..e3e13a7 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -14,6 +14,7 @@ pub struct Configuration { pub starting_block_number: Option, pub polling_frequency_secs: u64, pub node_base_url: Option, + pub slippage_tolerance_bps: u16, } impl Configuration { @@ -49,6 +50,12 @@ impl Configuration { None => None, }; + let slippage_tolerance_bps = + match collect_optional_environment_variable("SLIPPAGE_TOLERANCE_BPS")? { + Some(block_num) => block_num.parse::().expect("Unable to parse slippage tolerance factor"), + None => 50, + }; + Ok(Self { infura_api_key, network, @@ -57,6 +64,7 @@ impl Configuration { starting_block_number, polling_frequency_secs, node_base_url, + slippage_tolerance_bps }) } } diff --git a/src/main.rs b/src/main.rs index 019e994..7689abf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -151,7 +151,7 @@ async fn main() { }; let sell_amount_after_fees = requested_swap.amount_in - quote.fee_amount; - let buy_amount_after_fees_and_slippage = quote.buy_amount_after_fee * 995 / 1000; + let buy_amount_after_fees_and_slippage = quote.buy_amount_after_fee * (10000 - config.slippage_tolerance_bps / 10000); let eip_1271_signature = encoder::get_eip_1271_signature(SignatureData { from_token: requested_swap.from_token, From f00078819b4b1a02fa5522537007f2c06ab10181 Mon Sep 17 00:00:00 2001 From: Felix Leupold Date: Sun, 25 Jun 2023 20:12:14 +0200 Subject: [PATCH 12/21] Use quoteId when placing orders --- src/cow_api_client.rs | 9 ++++++++- src/main.rs | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/cow_api_client.rs b/src/cow_api_client.rs index 31ea372..123427e 100644 --- a/src/cow_api_client.rs +++ b/src/cow_api_client.rs @@ -12,6 +12,7 @@ pub struct Quote { pub fee_amount: U256, pub buy_amount_after_fee: U256, pub valid_to: u64, + pub id: u64, } #[derive(Debug)] @@ -95,11 +96,15 @@ impl CowAPIClient { let valid_to = quote["validTo"] .as_u64() .context("unable to get `validTo` from quote")?; + let id = response_body["id"] + .as_u64() + .context("unable to get `id` from quote")?; Ok(Quote { fee_amount: fee_amount.parse::()?.into(), buy_amount_after_fee: buy_amount_after_fee.parse::()?.into(), valid_to, + id }) } @@ -116,6 +121,7 @@ impl CowAPIClient { receiver, eip_1271_signature, }: Order<'_>, + quote_id: u64 ) -> Result { let http_client = reqwest::Client::new(); let response = http_client @@ -135,7 +141,8 @@ impl CowAPIClient { "from": order_contract, "sellTokenBalance": "erc20", "buyTokenBalance": "erc20", - "signingScheme": "eip1271" + "signingScheme": "eip1271", + "quoteId": quote_id, })) .send() .await?; diff --git a/src/main.rs b/src/main.rs index 019e994..7c73e2d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -177,7 +177,7 @@ async fn main() { fee_amount: quote.fee_amount, receiver: requested_swap.receiver, eip_1271_signature: &eip_1271_signature, - }) + }, quote.id) .await { Ok(_) => (), From 8bedb014faf7a8ad402f2483fd114922db26b050 Mon Sep 17 00:00:00 2001 From: Felix Leupold Date: Sun, 25 Jun 2023 20:11:11 +0200 Subject: [PATCH 13/21] Debug print isValidSignature call --- src/ethereum_client.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ethereum_client.rs b/src/ethereum_client.rs index 7e0839f..8432715 100644 --- a/src/ethereum_client.rs +++ b/src/ethereum_client.rs @@ -150,6 +150,7 @@ impl EthereumClient { price_checker_data: &swap_request.price_checker_data, }); + debug!("isValidSignature({:?},{:?})", hex::encode(&mock_order_digest), hex::encode(&mock_signature.0)); debug!( "Is valid sig? {:?}", order_contract From 6a32f50e1ee39f552e614e9e0033ee634f92d490 Mon Sep 17 00:00:00 2001 From: Felix Leupold Date: Tue, 27 Jun 2023 09:48:13 +0200 Subject: [PATCH 14/21] make quote_id part of the order struct --- src/cow_api_client.rs | 5 +++-- src/main.rs | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/cow_api_client.rs b/src/cow_api_client.rs index 123427e..f7037b1 100644 --- a/src/cow_api_client.rs +++ b/src/cow_api_client.rs @@ -26,6 +26,7 @@ pub struct Order<'a> { pub fee_amount: U256, pub receiver: Address, pub eip_1271_signature: &'a Bytes, + pub quote_id: u64, } pub struct CowAPIClient { @@ -104,7 +105,7 @@ impl CowAPIClient { fee_amount: fee_amount.parse::()?.into(), buy_amount_after_fee: buy_amount_after_fee.parse::()?.into(), valid_to, - id + id, }) } @@ -120,8 +121,8 @@ impl CowAPIClient { fee_amount, receiver, eip_1271_signature, + quote_id, }: Order<'_>, - quote_id: u64 ) -> Result { let http_client = reqwest::Client::new(); let response = http_client diff --git a/src/main.rs b/src/main.rs index 7c73e2d..91fbf50 100644 --- a/src/main.rs +++ b/src/main.rs @@ -177,7 +177,8 @@ async fn main() { fee_amount: quote.fee_amount, receiver: requested_swap.receiver, eip_1271_signature: &eip_1271_signature, - }, quote.id) + quote_id: quote.id, + }) .await { Ok(_) => (), From bf822410c09522d491b0ca7c61d89306e9b793c2 Mon Sep 17 00:00:00 2001 From: Felix Leupold Date: Tue, 27 Jun 2023 12:23:50 +0200 Subject: [PATCH 15/21] fix tests --- src/ethereum_client.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/ethereum_client.rs b/src/ethereum_client.rs index 8432715..b6a7cfe 100644 --- a/src/ethereum_client.rs +++ b/src/ethereum_client.rs @@ -150,7 +150,11 @@ impl EthereumClient { price_checker_data: &swap_request.price_checker_data, }); - debug!("isValidSignature({:?},{:?})", hex::encode(&mock_order_digest), hex::encode(&mock_signature.0)); + debug!( + "isValidSignature({:?},{:?})", + hex::encode(&mock_order_digest), + hex::encode(&mock_signature.0) + ); debug!( "Is valid sig? {:?}", order_contract @@ -199,6 +203,7 @@ mod tests { starting_block_number: None, polling_frequency_secs: 15, node_base_url: None, + slippage_tolerance_bps: 50, }; let eth_client = EthereumClient::new(&config).expect("Unable to create Ethereum client"); From 2fae0d466682450b92e07b7e2e109e5d6e0c0d96 Mon Sep 17 00:00:00 2001 From: Felix Leupold Date: Tue, 27 Jun 2023 13:54:06 +0200 Subject: [PATCH 16/21] fix clippy as well --- src/ethereum_client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ethereum_client.rs b/src/ethereum_client.rs index b6a7cfe..939a46f 100644 --- a/src/ethereum_client.rs +++ b/src/ethereum_client.rs @@ -152,7 +152,7 @@ impl EthereumClient { debug!( "isValidSignature({:?},{:?})", - hex::encode(&mock_order_digest), + hex::encode(mock_order_digest), hex::encode(&mock_signature.0) ); debug!( From 267e84c6c2b7322132e0e0dceb0fd72b264c35a8 Mon Sep 17 00:00:00 2001 From: Felix Leupold Date: Thu, 29 Jun 2023 10:53:58 +0200 Subject: [PATCH 17/21] Fix operator precedence in slippage computation --- src/main.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 041d6be..00b22cf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -71,7 +71,10 @@ async fn main() { debug!("range end: {}", range_end); // add the - 100 to cast a wider net since Infura sometimes doesn't reply - let requested_swaps = match eth_client.get_requested_swaps(range_start - 100, range_end).await { + let requested_swaps = match eth_client + .get_requested_swaps(range_start - 100, range_end) + .await + { Ok(swaps) => swaps, Err(err) => { error!("unable to get requested swaps – {:?}", err); @@ -151,7 +154,8 @@ async fn main() { }; let sell_amount_after_fees = requested_swap.amount_in - quote.fee_amount; - let buy_amount_after_fees_and_slippage = quote.buy_amount_after_fee * (10000 - config.slippage_tolerance_bps / 10000); + let buy_amount_after_fees_and_slippage = + quote.buy_amount_after_fee * (10000 - config.slippage_tolerance_bps) / 10000; let eip_1271_signature = encoder::get_eip_1271_signature(SignatureData { from_token: requested_swap.from_token, From 628f5d34dceea373c33e02664f43e4f569084cd0 Mon Sep 17 00:00:00 2001 From: Felix Leupold Date: Sat, 26 Aug 2023 22:18:30 +0200 Subject: [PATCH 18/21] Switch to tracing logs to allow for log spans per order --- Cargo.lock | 111 ++++++++++++++++++++++++++------ Cargo.toml | 3 +- src/main.rs | 182 ++++++++++++++++++++++++++++------------------------ 3 files changed, 191 insertions(+), 105 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ef4864d..9e16e73 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -797,19 +797,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "env_logger" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" -dependencies = [ - "atty", - "humantime", - "log", - "regex", - "termcolor", -] - [[package]] name = "eth-keystore" version = "0.4.2" @@ -1526,12 +1513,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - [[package]] name = "hyper" version = "0.14.20" @@ -1791,6 +1772,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata", +] + [[package]] name = "md-5" version = "0.10.5" @@ -1820,7 +1810,6 @@ name = "milkman-bot" version = "0.1.0" dependencies = [ "anyhow", - "env_logger", "ethers", "hex", "log", @@ -1829,6 +1818,8 @@ dependencies = [ "serde", "serde_json", "tokio", + "tracing", + "tracing-subscriber", "url", ] @@ -1883,6 +1874,16 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -1996,6 +1997,12 @@ version = "6.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "parity-scale-codec" version = "3.1.5" @@ -2427,6 +2434,15 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax", +] + [[package]] name = "regex-syntax" version = "0.6.27" @@ -2829,6 +2845,15 @@ dependencies = [ "keccak", ] +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -3079,6 +3104,16 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "time" version = "0.3.13" @@ -3246,6 +3281,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" dependencies = [ "once_cell", + "valuable", ] [[package]] @@ -3258,6 +3294,35 @@ dependencies = [ "tracing", ] +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + [[package]] name = "try-lock" version = "0.2.3" @@ -3368,6 +3433,12 @@ dependencies = [ "serde", ] +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index 85fdc67..25f49b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,10 +17,11 @@ serde = "1.0" serde_json = "1.0" hex = "0.4.3" log = "0.4" -env_logger = "0.8.4" anyhow = "1.0.61" ethers = { version = "0.17.0", features = ["abigen", "openssl"] } url = { version = "2.2.2" } +tracing = "0.1.37" +tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } [dev-dependencies] rand = { version = "0.8.4", features = ["min_const_gen"] } diff --git a/src/main.rs b/src/main.rs index 00b22cf..38704c6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,8 @@ -use anyhow::Result; +use anyhow::{bail, Context, Result}; use hex::ToHex; -use log::{debug, error, info}; use std::{collections::HashMap, time::Duration}; use tokio::time::sleep; +use tracing::Instrument; mod configuration; use crate::configuration::Configuration; @@ -32,9 +32,8 @@ mod constants; /// marginal cloud compute cost to CoW is likely to be very small. #[tokio::main] async fn main() { - env_logger::init(); - - info!("=== MILKMAN BOT STARTING ==="); + tracing_subscriber::fmt::init(); + tracing::info!("=== MILKMAN BOT STARTING ==="); let config = Configuration::get_from_environment() .expect("Unable to get configuration from the environment variables."); // .expect() because every decision to panic should be conscious, not just triggered by a `?` that we didn't think about @@ -56,7 +55,7 @@ async fn main() { .expect("Unable to get latest block number before starting."), ); - debug!("range start: {}", range_start); + tracing::debug!("range start: {}", range_start); let mut swap_queue = HashMap::new(); @@ -68,7 +67,7 @@ async fn main() { .await .expect("Unable to get latest block number."); // should we panic here if we can't get it? another option would be continuing in the loop, but then we might not observe that the bot is really `down` - debug!("range end: {}", range_end); + tracing::debug!("range end: {}", range_end); // add the - 100 to cast a wider net since Infura sometimes doesn't reply let requested_swaps = match eth_client @@ -77,13 +76,13 @@ async fn main() { { Ok(swaps) => swaps, Err(err) => { - error!("unable to get requested swaps – {:?}", err); + tracing::error!("unable to get requested swaps – {:?}", err); continue; } }; if !requested_swaps.is_empty() { - info!( + tracing::info!( "Found {} requested swaps between blocks {} and {}", requested_swaps.len(), range_start, @@ -92,8 +91,8 @@ async fn main() { } for requested_swap in requested_swaps { - info!("Inserting following swap in queue: {:?}", requested_swap); - debug!( + tracing::info!("Inserting following swap in queue: {:?}", requested_swap); + tracing::debug!( "Price checker data hex: 0x{}", requested_swap.price_checker_data.encode_hex::() ); @@ -104,90 +103,28 @@ async fn main() { let is_swap_fulfilled = match is_swap_fulfilled(requested_swap, ð_client).await { Ok(res) => res, Err(err) => { - error!("unable to determine if swap was fulfilled – {:?}", err); + tracing::error!("unable to determine if swap was fulfilled – {:?}", err); continue; } }; if is_swap_fulfilled { - info!( + tracing::info!( "Swap with order contract ({}) was fulfilled, removing from queue.", requested_swap.order_contract ); swap_queue.remove(&requested_swap.order_contract); } else { - info!( - "Handling swap with order contract ({})", - requested_swap.order_contract - ); - let mut verification_gas_limit = match eth_client - .get_estimated_order_contract_gas(&config, requested_swap) - .await - { - Ok(res) => res, - Err(err) => { - error!("unable to estimate verification gas – {:?}", err); - continue; + let contract = format!("{:#x}", requested_swap.order_contract); + async { + if let Err(err) = + handle_swap(requested_swap, ð_client, &cow_api_client, &config).await + { + tracing::error!("unable to handle swap {:?}", err); } - }; - verification_gas_limit = (verification_gas_limit * 11) / 10; // extra padding - debug!( - "verification gas limit to use - {:?}", - verification_gas_limit - ); - - let quote = match cow_api_client - .get_quote( - requested_swap.order_contract, - requested_swap.from_token, - requested_swap.to_token, - requested_swap.amount_in, - verification_gas_limit.as_u64(), - ) - .await - { - Ok(res) => res, - Err(err) => { - error!("unable to fetch quote - {:?}", err); - continue; - } - }; - - let sell_amount_after_fees = requested_swap.amount_in - quote.fee_amount; - let buy_amount_after_fees_and_slippage = - quote.buy_amount_after_fee * (10000 - config.slippage_tolerance_bps) / 10000; - - let eip_1271_signature = encoder::get_eip_1271_signature(SignatureData { - from_token: requested_swap.from_token, - to_token: requested_swap.to_token, - receiver: requested_swap.receiver, - sell_amount_after_fees, - buy_amount_after_fees_and_slippage, - valid_to: quote.valid_to, - fee_amount: quote.fee_amount, - order_creator: requested_swap.order_creator, - price_checker: requested_swap.price_checker, - price_checker_data: &requested_swap.price_checker_data, - }); - - match cow_api_client - .create_order(Order { - order_contract: requested_swap.order_contract, - sell_token: requested_swap.from_token, - buy_token: requested_swap.to_token, - sell_amount: sell_amount_after_fees, - buy_amount: buy_amount_after_fees_and_slippage, - valid_to: quote.valid_to, - fee_amount: quote.fee_amount, - receiver: requested_swap.receiver, - eip_1271_signature: &eip_1271_signature, - quote_id: quote.id, - }) - .await - { - Ok(_) => (), - Err(err) => error!("unable to create order via CoW API – {:?}", err), - }; + } + .instrument(tracing::info_span!("handle_swap", contract)) + .await } } @@ -195,6 +132,83 @@ async fn main() { } } +async fn handle_swap( + requested_swap: &Swap, + eth_client: &EthereumClient, + cow_api_client: &CowAPIClient, + config: &Configuration, +) -> Result<()> { + tracing::info!( + "Handling swap with order contract ({})", + requested_swap.order_contract + ); + let mut verification_gas_limit = match eth_client + .get_estimated_order_contract_gas(&config, requested_swap) + .await + { + Ok(res) => res, + Err(err) => { + bail!("unable to estimate verification gas – {:?}", err); + } + }; + verification_gas_limit = (verification_gas_limit * 11) / 10; // extra padding + tracing::debug!( + "verification gas limit to use - {:?}", + verification_gas_limit + ); + + let quote = match cow_api_client + .get_quote( + requested_swap.order_contract, + requested_swap.from_token, + requested_swap.to_token, + requested_swap.amount_in, + verification_gas_limit.as_u64(), + ) + .await + { + Ok(res) => res, + Err(err) => { + bail!("unable to fetch quote - {:?}", err); + } + }; + + let sell_amount_after_fees = requested_swap.amount_in - quote.fee_amount; + let buy_amount_after_fees_and_slippage = + quote.buy_amount_after_fee * (10000 - config.slippage_tolerance_bps) / 10000; + + let eip_1271_signature = encoder::get_eip_1271_signature(SignatureData { + from_token: requested_swap.from_token, + to_token: requested_swap.to_token, + receiver: requested_swap.receiver, + sell_amount_after_fees, + buy_amount_after_fees_and_slippage, + valid_to: quote.valid_to, + fee_amount: quote.fee_amount, + order_creator: requested_swap.order_creator, + price_checker: requested_swap.price_checker, + price_checker_data: &requested_swap.price_checker_data, + }); + tracing::debug!(signature = ?eip_1271_signature.to_string()); + + cow_api_client + .create_order(Order { + order_contract: requested_swap.order_contract, + sell_token: requested_swap.from_token, + buy_token: requested_swap.to_token, + sell_amount: sell_amount_after_fees, + buy_amount: buy_amount_after_fees_and_slippage, + valid_to: quote.valid_to, + fee_amount: quote.fee_amount, + receiver: requested_swap.receiver, + eip_1271_signature: &eip_1271_signature, + quote_id: quote.id, + }) + .await + .context("unable to create order via CoW API")?; + Ok(()) +} + async fn is_swap_fulfilled(swap: &Swap, eth_client: &EthereumClient) -> Result { // if all `from` tokens are gone, the swap must have been completed or cancelled Ok(eth_client From 74155bdb4e637f8e12862285aa40ca9b7e130428 Mon Sep 17 00:00:00 2001 From: Felix Leupold Date: Sun, 27 Aug 2023 14:40:35 +0200 Subject: [PATCH 19/21] clippy --- src/ethereum_client.rs | 2 +- src/main.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ethereum_client.rs b/src/ethereum_client.rs index 939a46f..08b6238 100644 --- a/src/ethereum_client.rs +++ b/src/ethereum_client.rs @@ -241,7 +241,7 @@ mod tests { .await .expect("Unable to get requested swaps"); - assert!(requested_swaps.len() > 0); + assert!(!requested_swaps.is_empty()); } #[test] diff --git a/src/main.rs b/src/main.rs index 38704c6..175351d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -143,7 +143,7 @@ async fn handle_swap( requested_swap.order_contract ); let mut verification_gas_limit = match eth_client - .get_estimated_order_contract_gas(&config, requested_swap) + .get_estimated_order_contract_gas(config, requested_swap) .await { Ok(res) => res, From 0f0f953af864c8f2fd5dd08956300a6398ef1e73 Mon Sep 17 00:00:00 2001 From: Felix Leupold Date: Sat, 7 Oct 2023 10:40:41 +0200 Subject: [PATCH 20/21] Update README.md --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 0e33d93..9b436b9 100644 --- a/README.md +++ b/README.md @@ -88,3 +88,10 @@ N/A *Description*: If you want to use something other than Infura. Needs to be JSON-RPC compatible. +### SLIPPAGE_TOLERANCE_BPS + +*Default*: +50 + +*Description*: +The slippage tolerance that is set on the orders the bot places (compared to the quoted amount). Reducing this may make a price checker that is "just" not passing accept the order, however it may make it more difficult for solvers to settle. From 623695193620078cad59d00b98d2fe95c303f516 Mon Sep 17 00:00:00 2001 From: ilya Date: Fri, 15 Mar 2024 18:02:14 +0000 Subject: [PATCH 21/21] Formatting --- src/configuration.rs | 6 ++++-- src/main.rs | 7 ++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/configuration.rs b/src/configuration.rs index e3e13a7..a449c42 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -52,7 +52,9 @@ impl Configuration { let slippage_tolerance_bps = match collect_optional_environment_variable("SLIPPAGE_TOLERANCE_BPS")? { - Some(block_num) => block_num.parse::().expect("Unable to parse slippage tolerance factor"), + Some(block_num) => block_num + .parse::() + .expect("Unable to parse slippage tolerance factor"), None => 50, }; @@ -64,7 +66,7 @@ impl Configuration { starting_block_number, polling_frequency_secs, node_base_url, - slippage_tolerance_bps + slippage_tolerance_bps, }) } } diff --git a/src/main.rs b/src/main.rs index 966ed06..e71adb7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,18 +6,23 @@ use tokio::time::sleep; use tracing::Instrument; mod configuration; + use crate::configuration::Configuration; mod ethereum_client; + use crate::ethereum_client::EthereumClient; mod cow_api_client; + use crate::cow_api_client::{CowAPIClient, Order}; mod encoder; + use crate::encoder::SignatureData; mod types; + use crate::types::Swap; mod constants; @@ -200,7 +205,7 @@ async fn handle_swap( sell_amount: sell_amount_after_fees, buy_amount: buy_amount_after_fees_and_slippage, valid_to: quote.valid_to, - fee_amount: U256::zero(), + fee_amount: U256::zero(), receiver: requested_swap.receiver, eip_1271_signature: &eip_1271_signature, quote_id: quote.id,