From bbebb5e72ec1843ebadae5faa36790c1164ea5a0 Mon Sep 17 00:00:00 2001
From: MartinquaXD <martin.beckmann@protonmail.com>
Date: Sat, 1 Jun 2024 08:45:28 +0200
Subject: [PATCH] Squashed commit of the following:

commit ab487f460effe5d5e85d915a2a2ed64ff26701ac
Author: MartinquaXD <martin.beckmann@protonmail.com>
Date:   Sat Jun 1 08:44:04 2024 +0200

    Fix clippy warning

commit 576cf19cfe3465e338b06172a5af99fa35c89af2
Author: MartinquaXD <martin.beckmann@protonmail.com>
Date:   Sat Jun 1 08:39:19 2024 +0200

    Implemented suggested changes

commit 60650dadd2c96cedcad3ca4572ca54f6d3818bcd
Author: Mateo <mateo@cow.fi>
Date:   Fri May 31 18:22:47 2024 +0200

    Remove log from the mock solver

commit 859380a34e00cec102fb6df9695356969d15a6c5
Author: Mateo <mateo@cow.fi>
Date:   Fri May 31 15:37:34 2024 +0200

    Linter

commit 4a67faf9d9f464c2f09c764a2928a2acfb5fc2eb
Author: Mateo <mateo@cow.fi>
Date:   Fri May 31 15:22:14 2024 +0200

    Implement e2e tests for JIT orders

commit debfa394b8ddd96e2a123d6ca07d4ec7ff5b6db4
Author: Dusan Stanivukovic <dusan.stanivukovic@gmail.com>
Date:   Fri May 31 09:37:51 2024 +0200

    Circuit breaker remove solver (#2705)

    # Description
    Related to https://github.com/cowprotocol/services/issues/2667

    POC implementation for using "Roles" safe module to grant special role
    to an EOA to sign and execute "removeSolver" function on behalf of the
    gpv2_authenticator manager/owner safe.

    Need to add tests to see if this actually works.

    # Changes
    <!-- List of detailed changes (how the change is accomplished) -->

    - [ ] Added `Roles` smart contract
    - [ ] Added EOA account as configuration
    - [ ] Implemented `remove_solver` function

    ## How to test
    todo

    ---------

    Co-authored-by: Mateo <mateo@cow.fi>
    Co-authored-by: Mateo-mro <160488334+Mateo-mro@users.noreply.github.com>
---
 Cargo.lock                                    |   9 +
 .../src/infra/blockchain/authenticator.rs     |  87 +++++++
 .../src/infra/blockchain/contracts.rs         |   6 +-
 crates/autopilot/src/infra/blockchain/mod.rs  |  15 +-
 crates/autopilot/src/run.rs                   |  19 +-
 crates/contracts/artifacts/Roles.json         |   1 +
 crates/contracts/build.rs                     |  36 +++
 crates/contracts/src/lib.rs                   |   1 +
 crates/e2e/Cargo.toml                         |   6 +
 crates/e2e/src/setup/colocation.rs            |   7 +-
 crates/e2e/src/setup/mod.rs                   |   3 +-
 crates/e2e/src/setup/solver.rs                | 175 ++++++++++++++
 crates/e2e/tests/e2e/jit_orders.rs            | 227 ++++++++++++++++++
 crates/e2e/tests/e2e/main.rs                  |   1 +
 crates/solvers-dto/src/solution.rs            |  30 +--
 15 files changed, 591 insertions(+), 32 deletions(-)
 create mode 100644 crates/autopilot/src/infra/blockchain/authenticator.rs
 create mode 100644 crates/contracts/artifacts/Roles.json
 create mode 100644 crates/e2e/src/setup/solver.rs
 create mode 100644 crates/e2e/tests/e2e/jit_orders.rs

diff --git a/Cargo.lock b/Cargo.lock
index 855af1a2a7..3f7e0d9166 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1691,6 +1691,7 @@ dependencies = [
  "app-data-hash",
  "async-trait",
  "autopilot",
+ "axum",
  "chrono",
  "clap",
  "contracts",
@@ -1709,14 +1710,19 @@ dependencies = [
  "refunder",
  "reqwest",
  "secp256k1",
+ "serde",
  "serde_json",
  "shared",
  "solver",
  "solvers",
+ "solvers-dto",
  "sqlx",
  "tempfile",
  "tokio",
+ "tower",
+ "tower-http",
  "tracing",
+ "uuid",
  "warp",
  "web3",
 ]
@@ -5145,6 +5151,9 @@ name = "uuid"
 version = "1.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d"
+dependencies = [
+ "getrandom",
+]
 
 [[package]]
 name = "valuable"
diff --git a/crates/autopilot/src/infra/blockchain/authenticator.rs b/crates/autopilot/src/infra/blockchain/authenticator.rs
new file mode 100644
index 0000000000..3b09718089
--- /dev/null
+++ b/crates/autopilot/src/infra/blockchain/authenticator.rs
@@ -0,0 +1,87 @@
+use {
+    crate::{
+        domain::{self, eth},
+        infra::blockchain::{
+            contracts::{deployment_address, Contracts},
+            ChainId,
+        },
+    },
+    ethcontract::{dyns::DynWeb3, GasPrice},
+};
+
+#[allow(dead_code)]
+#[derive(Debug, Clone)]
+pub struct Manager {
+    /// The authenticator contract that decides which solver is allowed to
+    /// submit settlements.
+    authenticator: contracts::GPv2AllowListAuthentication,
+    /// The safe module that is used to provide special role to EOA.
+    authenticator_role: contracts::Roles,
+    /// The EOA that is allowed to remove solvers.
+    authenticator_eoa: ethcontract::Account,
+}
+
+impl Manager {
+    /// Creates an authenticator which can remove solvers from the allow-list
+    pub async fn new(
+        web3: DynWeb3,
+        chain: ChainId,
+        contracts: Contracts,
+        authenticator_pk: eth::H256,
+    ) -> Self {
+        let authenticator_role = contracts::Roles::at(
+            &web3,
+            deployment_address(contracts::Roles::raw_contract(), &chain).expect("roles address"),
+        );
+
+        Self {
+            authenticator: contracts.authenticator().clone(),
+            authenticator_role,
+            authenticator_eoa: ethcontract::Account::Offline(
+                ethcontract::PrivateKey::from_raw(authenticator_pk.0).unwrap(),
+                None,
+            ),
+        }
+    }
+
+    /// Fire and forget: Removes solver from the allow-list in the authenticator
+    /// contract. This solver will no longer be able to settle.
+    #[allow(dead_code)]
+    fn remove_solver(&self, solver: domain::eth::Address) {
+        let calldata = self
+            .authenticator
+            .methods()
+            .remove_solver(solver.into())
+            .tx
+            .data
+            .expect("missing calldata");
+        let authenticator_eoa = self.authenticator_eoa.clone();
+        let authenticator_address = self.authenticator.address();
+        let authenticator_role = self.authenticator_role.clone();
+        tokio::task::spawn(async move {
+            // This value comes from the TX posted in the issue: https://github.com/cowprotocol/services/issues/2667
+            let mut byte_array = [0u8; 32];
+            byte_array[31] = 1;
+            authenticator_role
+                .methods()
+                .exec_transaction_with_role(
+                    authenticator_address,
+                    0.into(),
+                    ethcontract::Bytes(calldata.0),
+                    0,
+                    ethcontract::Bytes(byte_array),
+                    true,
+                )
+                .from(authenticator_eoa)
+                .gas_price(GasPrice::Eip1559 {
+                    // These are arbitrary high numbers that should be enough for a tx to be settled
+                    // anytime.
+                    max_fee_per_gas: 1000.into(),
+                    max_priority_fee_per_gas: 5.into(),
+                })
+                .send()
+                .await
+                .inspect_err(|err| tracing::error!(?solver, ?err, "failed to remove the solver"))
+        });
+    }
+}
diff --git a/crates/autopilot/src/infra/blockchain/contracts.rs b/crates/autopilot/src/infra/blockchain/contracts.rs
index 7d855377cf..b86e872d70 100644
--- a/crates/autopilot/src/infra/blockchain/contracts.rs
+++ b/crates/autopilot/src/infra/blockchain/contracts.rs
@@ -5,13 +5,15 @@ pub struct Contracts {
     settlement: contracts::GPv2Settlement,
     weth: contracts::WETH9,
     chainalysis_oracle: Option<contracts::ChainalysisOracle>,
-    authenticator: contracts::GPv2AllowListAuthentication,
 
+    /// The authenticator contract that decides which solver is allowed to
+    /// submit settlements.
+    authenticator: contracts::GPv2AllowListAuthentication,
     /// The domain separator for settlement contract used for signing orders.
     settlement_domain_separator: domain::eth::DomainSeparator,
 }
 
-#[derive(Debug, Default, Clone, Copy)]
+#[derive(Debug, Clone)]
 pub struct Addresses {
     pub settlement: Option<H160>,
     pub weth: Option<H160>,
diff --git a/crates/autopilot/src/infra/blockchain/mod.rs b/crates/autopilot/src/infra/blockchain/mod.rs
index 4ce255f33c..8e9a72b6a9 100644
--- a/crates/autopilot/src/infra/blockchain/mod.rs
+++ b/crates/autopilot/src/infra/blockchain/mod.rs
@@ -12,6 +12,7 @@ use {
     url::Url,
 };
 
+pub mod authenticator;
 pub mod contracts;
 
 /// Chain ID as defined by EIP-155.
@@ -62,6 +63,11 @@ impl Rpc {
     pub fn web3(&self) -> &DynWeb3 {
         &self.web3
     }
+
+    /// Returns a reference to the underlying RPC URL.
+    pub fn url(&self) -> &Url {
+        &self.url
+    }
 }
 
 /// The Ethereum blockchain.
@@ -80,8 +86,13 @@ impl Ethereum {
     ///
     /// Since this type is essential for the program this method will panic on
     /// any initialization error.
-    pub async fn new(rpc: Rpc, addresses: contracts::Addresses, poll_interval: Duration) -> Self {
-        let Rpc { web3, chain, url } = rpc;
+    pub async fn new(
+        web3: DynWeb3,
+        chain: ChainId,
+        url: Url,
+        addresses: contracts::Addresses,
+        poll_interval: Duration,
+    ) -> Self {
         let contracts = Contracts::new(&web3, &chain, addresses).await;
 
         Self {
diff --git a/crates/autopilot/src/run.rs b/crates/autopilot/src/run.rs
index 1e0c2e29da..f8fb54fd7f 100644
--- a/crates/autopilot/src/run.rs
+++ b/crates/autopilot/src/run.rs
@@ -13,14 +13,14 @@ use {
         },
         domain,
         event_updater::EventUpdater,
-        infra::{self},
+        infra::{self, blockchain::ChainId},
         run_loop::RunLoop,
         shadow,
         solvable_orders::SolvableOrdersCache,
     },
     clap::Parser,
     contracts::{BalancerV2Vault, IUniswapV3Factory},
-    ethcontract::{errors::DeployError, BlockNumber},
+    ethcontract::{dyns::DynWeb3, errors::DeployError, BlockNumber},
     ethrpc::current_block::block_number_to_block_number_hash,
     futures::StreamExt,
     model::DomainSeparator,
@@ -87,11 +87,13 @@ async fn ethrpc(url: &Url) -> infra::blockchain::Rpc {
 }
 
 async fn ethereum(
-    ethrpc: infra::blockchain::Rpc,
+    web3: DynWeb3,
+    chain: ChainId,
+    url: Url,
     contracts: infra::blockchain::contracts::Addresses,
     poll_interval: Duration,
 ) -> infra::Ethereum {
-    infra::Ethereum::new(ethrpc, contracts, poll_interval).await
+    infra::Ethereum::new(web3, chain, url, contracts, poll_interval).await
 }
 
 pub async fn start(args: impl Iterator<Item = String>) {
@@ -149,13 +151,18 @@ pub async fn run(args: Arguments) {
     }
 
     let ethrpc = ethrpc(&args.shared.node_url).await;
+    let chain = ethrpc.chain();
+    let web3 = ethrpc.web3().clone();
+    let url = ethrpc.url().clone();
     let contracts = infra::blockchain::contracts::Addresses {
         settlement: args.shared.settlement_contract_address,
         weth: args.shared.native_token_address,
     };
     let eth = ethereum(
-        ethrpc,
-        contracts,
+        web3.clone(),
+        chain,
+        url,
+        contracts.clone(),
         args.shared.current_block.block_stream_poll_interval,
     )
     .await;
diff --git a/crates/contracts/artifacts/Roles.json b/crates/contracts/artifacts/Roles.json
new file mode 100644
index 0000000000..e0e46ba6ac
--- /dev/null
+++ b/crates/contracts/artifacts/Roles.json
@@ -0,0 +1 @@
+{"abi":[{"inputs":[{"internalType":"address","name":"_owner","type":"address"},{"internalType":"address","name":"_avatar","type":"address"},{"internalType":"address","name":"_target","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"address","name":"module","type":"address"}],"name":"AlreadyDisabledModule","type":"error"},{"inputs":[{"internalType":"address","name":"module","type":"address"}],"name":"AlreadyEnabledModule","type":"error"},{"inputs":[],"name":"ArraysDifferentLength","type":"error"},{"inputs":[],"name":"CalldataOutOfBounds","type":"error"},{"inputs":[{"internalType":"enum PermissionChecker.Status","name":"status","type":"uint8"},{"internalType":"bytes32","name":"info","type":"bytes32"}],"name":"ConditionViolation","type":"error"},{"inputs":[],"name":"FunctionSignatureTooShort","type":"error"},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"HashAlreadyConsumed","type":"error"},{"inputs":[],"name":"InvalidInitialization","type":"error"},{"inputs":[{"internalType":"address","name":"module","type":"address"}],"name":"InvalidModule","type":"error"},{"inputs":[],"name":"InvalidPageSize","type":"error"},{"inputs":[],"name":"MalformedMultiEntrypoint","type":"error"},{"inputs":[],"name":"ModuleTransactionFailed","type":"error"},{"inputs":[],"name":"NoMembership","type":"error"},{"inputs":[{"internalType":"address","name":"sender","type":"address"}],"name":"NotAuthorized","type":"error"},{"inputs":[],"name":"NotInitializing","type":"error"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"OwnableInvalidOwner","type":"error"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"OwnableUnauthorizedAccount","type":"error"},{"inputs":[],"name":"SetupModulesAlreadyCalled","type":"error"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bytes32","name":"roleKey","type":"bytes32"},{"indexed":false,"internalType":"address","name":"targetAddress","type":"address"},{"indexed":false,"internalType":"bytes4","name":"selector","type":"bytes4"},{"indexed":false,"internalType":"enum ExecutionOptions","name":"options","type":"uint8"}],"name":"AllowFunction","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bytes32","name":"roleKey","type":"bytes32"},{"indexed":false,"internalType":"address","name":"targetAddress","type":"address"},{"indexed":false,"internalType":"enum ExecutionOptions","name":"options","type":"uint8"}],"name":"AllowTarget","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"module","type":"address"},{"indexed":false,"internalType":"bytes32[]","name":"roleKeys","type":"bytes32[]"},{"indexed":false,"internalType":"bool[]","name":"memberOf","type":"bool[]"}],"name":"AssignRoles","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousAvatar","type":"address"},{"indexed":true,"internalType":"address","name":"newAvatar","type":"address"}],"name":"AvatarSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bytes32","name":"allowanceKey","type":"bytes32"},{"indexed":false,"internalType":"uint128","name":"consumed","type":"uint128"},{"indexed":false,"internalType":"uint128","name":"newBalance","type":"uint128"}],"name":"ConsumeAllowance","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"module","type":"address"}],"name":"DisabledModule","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"module","type":"address"}],"name":"EnabledModule","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"module","type":"address"}],"name":"ExecutionFromModuleFailure","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"module","type":"address"}],"name":"ExecutionFromModuleSuccess","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bytes32","name":"","type":"bytes32"}],"name":"HashExecuted","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bytes32","name":"","type":"bytes32"}],"name":"HashInvalidated","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint64","name":"version","type":"uint64"}],"name":"Initialized","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bytes32","name":"roleKey","type":"bytes32"},{"indexed":false,"internalType":"address","name":"targetAddress","type":"address"},{"indexed":false,"internalType":"bytes4","name":"selector","type":"bytes4"}],"name":"RevokeFunction","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bytes32","name":"roleKey","type":"bytes32"},{"indexed":false,"internalType":"address","name":"targetAddress","type":"address"}],"name":"RevokeTarget","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"initiator","type":"address"},{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"avatar","type":"address"},{"indexed":false,"internalType":"address","name":"target","type":"address"}],"name":"RolesModSetup","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bytes32","name":"roleKey","type":"bytes32"},{"indexed":false,"internalType":"address","name":"targetAddress","type":"address"},{"indexed":false,"internalType":"bytes4","name":"selector","type":"bytes4"},{"components":[{"internalType":"uint8","name":"parent","type":"uint8"},{"internalType":"enum ParameterType","name":"paramType","type":"uint8"},{"internalType":"enum Operator","name":"operator","type":"uint8"},{"internalType":"bytes","name":"compValue","type":"bytes"}],"indexed":false,"internalType":"struct ConditionFlat[]","name":"conditions","type":"tuple[]"},{"indexed":false,"internalType":"enum ExecutionOptions","name":"options","type":"uint8"}],"name":"ScopeFunction","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bytes32","name":"roleKey","type":"bytes32"},{"indexed":false,"internalType":"address","name":"targetAddress","type":"address"}],"name":"ScopeTarget","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bytes32","name":"allowanceKey","type":"bytes32"},{"indexed":false,"internalType":"uint128","name":"balance","type":"uint128"},{"indexed":false,"internalType":"uint128","name":"maxRefill","type":"uint128"},{"indexed":false,"internalType":"uint128","name":"refill","type":"uint128"},{"indexed":false,"internalType":"uint64","name":"period","type":"uint64"},{"indexed":false,"internalType":"uint64","name":"timestamp","type":"uint64"}],"name":"SetAllowance","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"module","type":"address"},{"indexed":false,"internalType":"bytes32","name":"defaultRoleKey","type":"bytes32"}],"name":"SetDefaultRole","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"bytes4","name":"selector","type":"bytes4"},{"indexed":false,"internalType":"contract ITransactionUnwrapper","name":"adapter","type":"address"}],"name":"SetUnwrapAdapter","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousTarget","type":"address"},{"indexed":true,"internalType":"address","name":"newTarget","type":"address"}],"name":"TargetSet","type":"event"},{"inputs":[{"internalType":"bytes32","name":"roleKey","type":"bytes32"},{"internalType":"address","name":"targetAddress","type":"address"},{"internalType":"bytes4","name":"selector","type":"bytes4"},{"internalType":"enum ExecutionOptions","name":"options","type":"uint8"}],"name":"allowFunction","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"roleKey","type":"bytes32"},{"internalType":"address","name":"targetAddress","type":"address"},{"internalType":"enum ExecutionOptions","name":"options","type":"uint8"}],"name":"allowTarget","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"allowances","outputs":[{"internalType":"uint128","name":"refill","type":"uint128"},{"internalType":"uint128","name":"maxRefill","type":"uint128"},{"internalType":"uint64","name":"period","type":"uint64"},{"internalType":"uint128","name":"balance","type":"uint128"},{"internalType":"uint64","name":"timestamp","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"module","type":"address"},{"internalType":"bytes32[]","name":"roleKeys","type":"bytes32[]"},{"internalType":"bool[]","name":"memberOf","type":"bool[]"}],"name":"assignRoles","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"avatar","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"consumed","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"defaultRoles","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"prevModule","type":"address"},{"internalType":"address","name":"module","type":"address"}],"name":"disableModule","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"module","type":"address"}],"name":"enableModule","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"enum Enum.Operation","name":"operation","type":"uint8"}],"name":"execTransactionFromModule","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"enum Enum.Operation","name":"operation","type":"uint8"}],"name":"execTransactionFromModuleReturnData","outputs":[{"internalType":"bool","name":"success","type":"bool"},{"internalType":"bytes","name":"returnData","type":"bytes"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"enum Enum.Operation","name":"operation","type":"uint8"},{"internalType":"bytes32","name":"roleKey","type":"bytes32"},{"internalType":"bool","name":"shouldRevert","type":"bool"}],"name":"execTransactionWithRole","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"enum Enum.Operation","name":"operation","type":"uint8"},{"internalType":"bytes32","name":"roleKey","type":"bytes32"},{"internalType":"bool","name":"shouldRevert","type":"bool"}],"name":"execTransactionWithRoleReturnData","outputs":[{"internalType":"bool","name":"success","type":"bool"},{"internalType":"bytes","name":"returnData","type":"bytes"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"start","type":"address"},{"internalType":"uint256","name":"pageSize","type":"uint256"}],"name":"getModulesPaginated","outputs":[{"internalType":"address[]","name":"array","type":"address[]"},{"internalType":"address","name":"next","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"hash","type":"bytes32"}],"name":"invalidate","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_module","type":"address"}],"name":"isModuleEnabled","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"bytes32","name":"salt","type":"bytes32"}],"name":"moduleTxHash","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"roleKey","type":"bytes32"},{"internalType":"address","name":"targetAddress","type":"address"},{"internalType":"bytes4","name":"selector","type":"bytes4"}],"name":"revokeFunction","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"roleKey","type":"bytes32"},{"internalType":"address","name":"targetAddress","type":"address"}],"name":"revokeTarget","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"roleKey","type":"bytes32"},{"internalType":"address","name":"targetAddress","type":"address"},{"internalType":"bytes4","name":"selector","type":"bytes4"},{"components":[{"internalType":"uint8","name":"parent","type":"uint8"},{"internalType":"enum ParameterType","name":"paramType","type":"uint8"},{"internalType":"enum Operator","name":"operator","type":"uint8"},{"internalType":"bytes","name":"compValue","type":"bytes"}],"internalType":"struct ConditionFlat[]","name":"conditions","type":"tuple[]"},{"internalType":"enum ExecutionOptions","name":"options","type":"uint8"}],"name":"scopeFunction","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"roleKey","type":"bytes32"},{"internalType":"address","name":"targetAddress","type":"address"}],"name":"scopeTarget","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"key","type":"bytes32"},{"internalType":"uint128","name":"balance","type":"uint128"},{"internalType":"uint128","name":"maxRefill","type":"uint128"},{"internalType":"uint128","name":"refill","type":"uint128"},{"internalType":"uint64","name":"period","type":"uint64"},{"internalType":"uint64","name":"timestamp","type":"uint64"}],"name":"setAllowance","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_avatar","type":"address"}],"name":"setAvatar","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"module","type":"address"},{"internalType":"bytes32","name":"roleKey","type":"bytes32"}],"name":"setDefaultRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_target","type":"address"}],"name":"setTarget","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"bytes4","name":"selector","type":"bytes4"},{"internalType":"contract ITransactionUnwrapper","name":"adapter","type":"address"}],"name":"setTransactionUnwrapper","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes","name":"initParams","type":"bytes"}],"name":"setUp","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"target","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"unwrappers","outputs":[{"internalType":"contract ITransactionUnwrapper","name":"","type":"address"}],"stateMutability":"view","type":"function"}]}
\ No newline at end of file
diff --git a/crates/contracts/build.rs b/crates/contracts/build.rs
index b2313ce210..64f6867bc4 100644
--- a/crates/contracts/build.rs
+++ b/crates/contracts/build.rs
@@ -641,6 +641,42 @@ fn main() {
     });
     generate_contract("GnosisSafeProxy");
     generate_contract("GnosisSafeProxyFactory");
+    generate_contract_with_config("Roles", |builder| {
+        builder
+            .contract_mod_override("roles")
+            .add_network(
+                MAINNET,
+                Network {
+                    address: addr("0x9646fDAD06d3e24444381f44362a3B0eB343D337"),
+                    // <https://etherscan.io/tx/0x351ecf2966f8cdd54e1de1d4cb326217fa89f6064231dfc1fe56417b9b48e942>
+                    deployment_information: Some(DeploymentInformation::BlockNumber(18692162)),
+                },
+            )
+            .add_network(
+                GNOSIS,
+                Network {
+                    address: addr("0x9646fDAD06d3e24444381f44362a3B0eB343D337"),
+                    // <https://gnosisscan.io/tx/0x4b1ec57c4048afd40904ea9b91dad38ec18d69ea0db965d624ffdd4abd284c96>
+                    deployment_information: Some(DeploymentInformation::BlockNumber(31222929)),
+                },
+            )
+            .add_network(
+                SEPOLIA,
+                Network {
+                    address: addr("0x9646fDAD06d3e24444381f44362a3B0eB343D337"),
+                    // <https://sepolia.etherscan.io/tx/0x516f0f6b8ac669cb5ca3962833e520274169c1463da354be9faa2cb0e6afa8a6>
+                    deployment_information: Some(DeploymentInformation::BlockNumber(4884885)),
+                },
+            )
+            .add_network(
+                ARBITRUM_ONE,
+                Network {
+                    address: addr("0x9646fDAD06d3e24444381f44362a3B0eB343D337"),
+                    // <https://arbiscan.io/tx/0x3860d6091e1baf8a9ba16e58ec437ec3644db2f4c0d9e2ba7fe37cfa0a4fa748>
+                    deployment_information: Some(DeploymentInformation::BlockNumber(176504820)),
+                },
+            )
+    });
     generate_contract_with_config("HoneyswapRouter", |builder| {
         builder.add_network_str(GNOSIS, "0x1C232F01118CB8B424793ae03F870aa7D0ac7f77")
     });
diff --git a/crates/contracts/src/lib.rs b/crates/contracts/src/lib.rs
index 9478f654b2..dfbb7ffc79 100644
--- a/crates/contracts/src/lib.rs
+++ b/crates/contracts/src/lib.rs
@@ -53,6 +53,7 @@ include_contracts! {
     GnosisSafeCompatibilityFallbackHandler;
     GnosisSafeProxy;
     GnosisSafeProxyFactory;
+    Roles;
     HoneyswapRouter;
     HooksTrampoline;
     ISwaprPair;
diff --git a/crates/e2e/Cargo.toml b/crates/e2e/Cargo.toml
index 99371fca66..efff0e88fa 100644
--- a/crates/e2e/Cargo.toml
+++ b/crates/e2e/Cargo.toml
@@ -10,6 +10,7 @@ app-data = { path = "../app-data" }
 anyhow = { workspace = true }
 async-trait = { workspace = true }
 autopilot = { path = "../autopilot" }
+axum = { workspace = true }
 chrono = { workspace = true }
 clap = { workspace = true }
 contracts = { path = "../contracts" }
@@ -25,16 +26,21 @@ observe = { path = "../observe" }
 orderbook = { path = "../orderbook" }
 reqwest = { workspace = true, features = ["blocking"] }
 secp256k1 = { workspace = true }
+serde = { workspace = true }
 serde_json = { workspace = true }
 shared = { path = "../shared" }
 solver = { path = "../solver" }
 solvers = { path = "../solvers" }
+solvers-dto = { path = "../solvers-dto" }
 sqlx = { workspace = true }
 tempfile = { workspace = true }
 tokio = { workspace = true, features = ["macros", "process"] }
+tower = "0.4"
+tower-http = { version = "0.4", features = ["limit", "trace"] }
 tracing = { workspace = true }
 warp = { workspace = true }
 web3 = { workspace = true, features = ["http"] }
+uuid = { version = "1.4.1", features = ["v4"] }
 
 [dev-dependencies]
 app-data-hash = { path = "../app-data-hash" }
diff --git a/crates/e2e/src/setup/colocation.rs b/crates/e2e/src/setup/colocation.rs
index 11895d6ad5..1454a60560 100644
--- a/crates/e2e/src/setup/colocation.rs
+++ b/crates/e2e/src/setup/colocation.rs
@@ -1,9 +1,4 @@
-use {
-    crate::{nodes::NODE_HOST, setup::*},
-    ethcontract::H160,
-    reqwest::Url,
-    tokio::task::JoinHandle,
-};
+use {crate::setup::*, ethcontract::H160, reqwest::Url, tokio::task::JoinHandle};
 
 pub async fn start_baseline_solver(weth: H160) -> Url {
     let config_file = config_tmp_file(format!(
diff --git a/crates/e2e/src/setup/mod.rs b/crates/e2e/src/setup/mod.rs
index 9aeea810dc..8c30e7fe55 100644
--- a/crates/e2e/src/setup/mod.rs
+++ b/crates/e2e/src/setup/mod.rs
@@ -3,6 +3,7 @@ mod deploy;
 #[macro_use]
 pub mod onchain_components;
 mod services;
+mod solver;
 
 use {
     crate::nodes::{Node, NODE_HOST},
@@ -19,7 +20,7 @@ use {
     },
     tempfile::TempPath,
 };
-pub use {deploy::*, onchain_components::*, services::*};
+pub use {deploy::*, onchain_components::*, services::*, solver::*};
 
 /// Create a temporary file with the given content.
 pub fn config_tmp_file<C: AsRef<[u8]>>(content: C) -> TempPath {
diff --git a/crates/e2e/src/setup/solver.rs b/crates/e2e/src/setup/solver.rs
new file mode 100644
index 0000000000..eafdcf3c3e
--- /dev/null
+++ b/crates/e2e/src/setup/solver.rs
@@ -0,0 +1,175 @@
+//! Mock solver for testing purposes. It returns a custom solution.
+
+use {
+    app_data::AppDataHash,
+    axum::Json,
+    ethcontract::common::abi::ethereum_types::Address,
+    model::{
+        order::{BuyTokenDestination, OrderData, OrderKind, SellTokenSource},
+        signature::EcdsaSigningScheme,
+        DomainSeparator,
+    },
+    reqwest::Url,
+    solvers_dto::{
+        auction::Auction,
+        solution::{Asset, Kind, Solution, Solutions},
+    },
+    std::sync::{Arc, Mutex},
+    tokio::signal::{unix, unix::SignalKind},
+    warp::hyper,
+    web3::signing::SecretKeyRef,
+};
+
+/// A solver that does not implement any solving logic itself and instead simply
+/// forwards a single hardcoded solution.
+pub struct Mock {
+    /// The currently configured solution to return.
+    solution: Arc<Mutex<Option<Solution>>>,
+    /// Under which URL the solver is reachable by a driver.
+    pub url: Url,
+}
+
+impl Mock {
+    /// Instructs the solver to return a new solution from now on.
+    pub fn configure_solution(&self, solution: Option<Solution>) {
+        *self.solution.lock().unwrap() = solution;
+    }
+}
+
+impl Default for Mock {
+    fn default() -> Self {
+        let solution = Arc::new(Mutex::new(None));
+
+        let app = axum::Router::new()
+            .layer(tower::ServiceBuilder::new().layer(
+                tower_http::limit::RequestBodyLimitLayer::new(REQUEST_BODY_LIMIT),
+            ))
+            .route("/solve", axum::routing::post(solve))
+            .layer(
+                tower::ServiceBuilder::new().layer(tower_http::trace::TraceLayer::new_for_http()),
+            )
+            .with_state(solution.clone())
+            // axum's default body limit needs to be disabled to not have the default limit on top of our custom limit
+            .layer(axum::extract::DefaultBodyLimit::disable());
+
+        let make_svc = observe::make_service_with_task_local_storage!(app);
+
+        let server = axum::Server::bind(&"0.0.0.0:0".parse().unwrap()).serve(make_svc);
+
+        let mock = Mock {
+            solution,
+            url: format!("http://{}", server.local_addr()).parse().unwrap(),
+        };
+
+        tokio::task::spawn(server.with_graceful_shutdown(shutdown_signal()));
+
+        mock
+    }
+}
+
+async fn solve(
+    state: axum::extract::State<Arc<Mutex<Option<Solution>>>>,
+    Json(auction): Json<Auction>,
+) -> (axum::http::StatusCode, Json<Solutions>) {
+    let auction_id = auction.id.unwrap_or_default();
+    let solutions = state.lock().unwrap().iter().cloned().collect();
+    let solutions = Solutions { solutions };
+    tracing::trace!(?auction_id, ?solutions, "/solve");
+    (axum::http::StatusCode::OK, Json(solutions))
+}
+
+const REQUEST_BODY_LIMIT: usize = 10 * 1024 * 1024;
+
+#[cfg(unix)]
+async fn shutdown_signal() {
+    // Intercept main signals for graceful shutdown.
+    // Kubernetes sends sigterm, whereas locally sigint (ctrl-c) is most common.
+    let mut interrupt = unix::signal(SignalKind::interrupt()).unwrap();
+    let mut terminate = unix::signal(SignalKind::terminate()).unwrap();
+    tokio::select! {
+        _ = interrupt.recv() => (),
+        _ = terminate.recv() => (),
+    };
+}
+
+#[derive(Clone, Debug)]
+pub struct JitOrder {
+    pub owner: Address,
+    pub sell: Asset,
+    pub buy: Asset,
+    pub kind: OrderKind,
+    pub partially_fillable: bool,
+    pub valid_to: u32,
+    pub app_data: AppDataHash,
+    pub receiver: Address,
+}
+
+impl JitOrder {
+    fn data(&self) -> OrderData {
+        OrderData {
+            sell_token: self.sell.token,
+            buy_token: self.buy.token,
+            receiver: self.receiver.into(),
+            sell_amount: self.sell.amount,
+            buy_amount: self.buy.amount,
+            valid_to: self.valid_to,
+            app_data: AppDataHash(self.app_data.0),
+            fee_amount: 0.into(),
+            kind: self.kind,
+            partially_fillable: self.partially_fillable,
+            sell_token_balance: Default::default(),
+            buy_token_balance: Default::default(),
+        }
+    }
+
+    pub fn sign(
+        self,
+        signing_scheme: EcdsaSigningScheme,
+        domain: &DomainSeparator,
+        key: SecretKeyRef,
+    ) -> solvers_dto::solution::JitOrder {
+        let data = self.data();
+        let signature = match model::signature::EcdsaSignature::sign(
+            signing_scheme,
+            domain,
+            &data.hash_struct(),
+            key,
+        )
+        .to_signature(signing_scheme)
+        {
+            model::signature::Signature::Eip712(signature) => signature.to_bytes().to_vec(),
+            model::signature::Signature::EthSign(signature) => signature.to_bytes().to_vec(),
+            model::signature::Signature::Eip1271(signature) => signature,
+            model::signature::Signature::PreSign => panic!("Not supported PreSigned JIT orders"),
+        };
+        solvers_dto::solution::JitOrder {
+            sell_token: data.sell_token,
+            buy_token: data.buy_token,
+            receiver: data.receiver.unwrap_or_default(),
+            sell_amount: data.sell_amount,
+            buy_amount: data.buy_amount,
+            valid_to: data.valid_to,
+            app_data: data.app_data.0,
+            fee_amount: data.fee_amount,
+            kind: match data.kind {
+                OrderKind::Buy => Kind::Buy,
+                OrderKind::Sell => Kind::Sell,
+            },
+            partially_fillable: data.partially_fillable,
+            sell_token_balance: match data.sell_token_balance {
+                SellTokenSource::Erc20 => solvers_dto::solution::SellTokenBalance::Erc20,
+                SellTokenSource::External => solvers_dto::solution::SellTokenBalance::External,
+                SellTokenSource::Internal => solvers_dto::solution::SellTokenBalance::Internal,
+            },
+            buy_token_balance: match data.buy_token_balance {
+                BuyTokenDestination::Erc20 => solvers_dto::solution::BuyTokenBalance::Erc20,
+                BuyTokenDestination::Internal => solvers_dto::solution::BuyTokenBalance::Internal,
+            },
+            signing_scheme: match signing_scheme {
+                EcdsaSigningScheme::Eip712 => solvers_dto::solution::SigningScheme::Eip712,
+                EcdsaSigningScheme::EthSign => solvers_dto::solution::SigningScheme::EthSign,
+            },
+            signature,
+        }
+    }
+}
diff --git a/crates/e2e/tests/e2e/jit_orders.rs b/crates/e2e/tests/e2e/jit_orders.rs
new file mode 100644
index 0000000000..c094d30848
--- /dev/null
+++ b/crates/e2e/tests/e2e/jit_orders.rs
@@ -0,0 +1,227 @@
+use {
+    e2e::{
+        setup::{colocation::SolverEngine, *},
+        tx,
+    },
+    ethcontract::prelude::U256,
+    model::{
+        order::{OrderClass, OrderCreation, OrderKind},
+        signature::EcdsaSigningScheme,
+    },
+    secp256k1::SecretKey,
+    shared::ethrpc::Web3,
+    solvers_dto::solution::{Asset, Solution},
+    std::collections::HashMap,
+    web3::signing::SecretKeyRef,
+};
+
+#[tokio::test]
+#[ignore]
+async fn local_node_single_limit_order() {
+    run_test(single_limit_order_test).await;
+}
+
+async fn single_limit_order_test(web3: Web3) {
+    let mut onchain = OnchainComponents::deploy(web3.clone()).await;
+
+    let [solver] = onchain.make_solvers(to_wei(100)).await;
+    let [trader_a] = onchain.make_accounts(to_wei(100)).await;
+    let [token_a, token_b] = onchain
+        .deploy_tokens_with_weth_uni_v2_pools(to_wei(1_000), to_wei(1_000))
+        .await;
+
+    // Fund trader account
+    token_a.mint(trader_a.address(), to_wei(100)).await;
+    token_b.mint(trader_a.address(), to_wei(100)).await;
+
+    // Fund solver account
+    token_a.mint(solver.address(), to_wei(1000)).await;
+    token_b.mint(solver.address(), to_wei(1000)).await;
+
+    // Create and fund Uniswap pool
+    tx!(
+        solver.account(),
+        onchain
+            .contracts()
+            .uniswap_v2_factory
+            .create_pair(token_a.address(), token_b.address())
+    );
+    tx!(
+        solver.account(),
+        token_a.approve(
+            onchain.contracts().uniswap_v2_router.address(),
+            to_wei(1000)
+        )
+    );
+    tx!(
+        solver.account(),
+        token_b.approve(
+            onchain.contracts().uniswap_v2_router.address(),
+            to_wei(1000)
+        )
+    );
+    tx!(
+        solver.account(),
+        onchain.contracts().uniswap_v2_router.add_liquidity(
+            token_a.address(),
+            token_b.address(),
+            to_wei(100),
+            to_wei(100),
+            0_u64.into(),
+            0_u64.into(),
+            solver.address(),
+            U256::max_value(),
+        )
+    );
+
+    // Approve GPv2 for trading
+    tx!(
+        trader_a.account(),
+        token_a.approve(onchain.contracts().allowance, to_wei(100))
+    );
+    tx!(
+        trader_a.account(),
+        token_b.approve(onchain.contracts().allowance, to_wei(100))
+    );
+    tx!(
+        solver.account(),
+        token_a.approve(onchain.contracts().allowance, to_wei(100))
+    );
+    tx!(
+        solver.account(),
+        token_b.approve(onchain.contracts().allowance, to_wei(100))
+    );
+
+    let services = Services::new(onchain.contracts()).await;
+
+    let mock_solver = Mock::default();
+
+    // Start system
+    colocation::start_driver(
+        onchain.contracts(),
+        vec![
+            SolverEngine {
+                name: "test_solver".into(),
+                account: solver.clone(),
+                endpoint: colocation::start_baseline_solver(onchain.contracts().weth.address())
+                    .await,
+            },
+            SolverEngine {
+                name: "mock_solver".into(),
+                account: solver.clone(),
+                endpoint: mock_solver.url.clone(),
+            },
+        ],
+        colocation::LiquidityProvider::UniswapV2,
+    );
+
+    // We start the quoter as the baseline solver, and the mock solver as the one
+    // returning the solution
+    services
+        .start_autopilot(
+            None,
+            vec![
+                "--drivers=mock_solver|http://localhost:11088/mock_solver".to_string(),
+                "--price-estimation-drivers=test_solver|http://localhost:11088/test_solver"
+                    .to_string(),
+            ],
+        )
+        .await;
+    services
+        .start_api(vec![
+            "--price-estimation-drivers=test_solver|http://localhost:11088/test_solver".to_string(),
+        ])
+        .await;
+
+    // Place order
+    let order = OrderCreation {
+        sell_token: token_a.address(),
+        sell_amount: to_wei(10),
+        buy_token: token_b.address(),
+        buy_amount: to_wei(5),
+        valid_to: model::time::now_in_epoch_seconds() + 300,
+        kind: OrderKind::Sell,
+        ..Default::default()
+    }
+    .sign(
+        EcdsaSigningScheme::Eip712,
+        &onchain.contracts().domain_separator,
+        SecretKeyRef::from(&SecretKey::from_slice(trader_a.private_key()).unwrap()),
+    );
+    let order_id = services.create_order(&order).await.unwrap();
+    let limit_order = services.get_order(&order_id).await.unwrap();
+    assert_eq!(limit_order.metadata.class, OrderClass::Limit);
+
+    mock_solver.configure_solution(Some(Solution {
+        id: 0,
+        prices: HashMap::from([
+            (token_a.address(), to_wei(1)),
+            (token_b.address(), to_wei(1)),
+        ]),
+        trades: vec![
+            solvers_dto::solution::Trade::Jit(solvers_dto::solution::JitTrade {
+                order: JitOrder {
+                    owner: trader_a.address(),
+                    sell: Asset {
+                        amount: to_wei(10),
+                        token: token_b.address(),
+                    },
+                    buy: Asset {
+                        amount: to_wei(1),
+                        token: token_a.address(),
+                    },
+                    kind: OrderKind::Sell,
+                    partially_fillable: false,
+                    valid_to: model::time::now_in_epoch_seconds() + 300,
+                    app_data: Default::default(),
+                    receiver: solver.address(),
+                }
+                .sign(
+                    EcdsaSigningScheme::Eip712,
+                    &onchain.contracts().domain_separator,
+                    SecretKeyRef::from(&SecretKey::from_slice(solver.private_key()).unwrap()),
+                ),
+                executed_amount: to_wei(10),
+            }),
+            solvers_dto::solution::Trade::Fulfillment(solvers_dto::solution::Fulfillment {
+                executed_amount: to_wei(10),
+                fee: Some(0.into()),
+                order: order_id.0,
+            }),
+        ],
+        pre_interactions: vec![],
+        interactions: vec![],
+        post_interactions: vec![],
+        gas: None,
+    }));
+
+    // Drive solution
+    tracing::info!("Waiting for trade.");
+    let trader_balance_before = token_b.balance_of(trader_a.address()).call().await.unwrap();
+    let solver_balance_before = token_b.balance_of(solver.address()).call().await.unwrap();
+    wait_for_condition(TIMEOUT, || async { services.solvable_orders().await == 1 })
+        .await
+        .unwrap();
+
+    wait_for_condition(TIMEOUT, || async { services.solvable_orders().await == 0 })
+        .await
+        .unwrap();
+
+    let trader_balance_after = token_b.balance_of(trader_a.address()).call().await.unwrap();
+    let solver_balance_after = token_b.balance_of(solver.address()).call().await.unwrap();
+
+    assert!(
+        trader_balance_after
+            .checked_sub(trader_balance_before)
+            .unwrap()
+            >= to_wei(5)
+    );
+    // Since the fee is 0 in the custom solution, the balance difference has to be
+    // exactly 10 wei
+    assert_eq!(
+        solver_balance_before
+            .checked_sub(solver_balance_after)
+            .unwrap(),
+        to_wei(10)
+    );
+}
diff --git a/crates/e2e/tests/e2e/main.rs b/crates/e2e/tests/e2e/main.rs
index 29f90f3933..efa2a389fc 100644
--- a/crates/e2e/tests/e2e/main.rs
+++ b/crates/e2e/tests/e2e/main.rs
@@ -13,6 +13,7 @@ mod eth_integration;
 mod eth_safe;
 mod ethflow;
 mod hooks;
+mod jit_orders;
 mod limit_orders;
 mod liquidity;
 mod order_cancellation;
diff --git a/crates/solvers-dto/src/solution.rs b/crates/solvers-dto/src/solution.rs
index e2a7111f52..760394bb94 100644
--- a/crates/solvers-dto/src/solution.rs
+++ b/crates/solvers-dto/src/solution.rs
@@ -14,7 +14,7 @@ pub struct Solutions {
 }
 
 #[serde_as]
-#[derive(Debug, Serialize)]
+#[derive(Clone, Debug, Serialize)]
 #[serde(rename_all = "camelCase")]
 pub struct Solution {
     pub id: u64,
@@ -30,7 +30,7 @@ pub struct Solution {
     pub gas: Option<u64>,
 }
 
-#[derive(Debug, Serialize)]
+#[derive(Clone, Debug, Serialize)]
 #[serde(tag = "kind", rename_all = "camelCase")]
 pub enum Trade {
     Fulfillment(Fulfillment),
@@ -38,7 +38,7 @@ pub enum Trade {
 }
 
 #[serde_as]
-#[derive(Debug, Serialize)]
+#[derive(Clone, Debug, Serialize)]
 #[serde(rename_all = "camelCase")]
 pub struct Fulfillment {
     #[serde_as(as = "serialize::Hex")]
@@ -51,7 +51,7 @@ pub struct Fulfillment {
 }
 
 #[serde_as]
-#[derive(Debug, Serialize)]
+#[derive(Clone, Debug, Serialize)]
 #[serde(rename_all = "camelCase")]
 pub struct JitTrade {
     pub order: JitOrder,
@@ -60,7 +60,7 @@ pub struct JitTrade {
 }
 
 #[serde_as]
-#[derive(Debug, Serialize)]
+#[derive(Clone, Debug, Serialize)]
 #[serde(rename_all = "camelCase")]
 pub struct JitOrder {
     pub sell_token: H160,
@@ -84,21 +84,21 @@ pub struct JitOrder {
     pub signature: Vec<u8>,
 }
 
-#[derive(Debug, Serialize)]
+#[derive(Clone, Debug, Serialize)]
 #[serde(rename_all = "camelCase")]
 pub enum Kind {
     Sell,
     Buy,
 }
 
-#[derive(Debug, Serialize)]
+#[derive(Clone, Debug, Serialize)]
 #[serde(tag = "kind", rename_all = "camelCase")]
 pub enum Interaction {
     Liquidity(LiquidityInteraction),
     Custom(CustomInteraction),
 }
 
-#[derive(Debug, Serialize)]
+#[derive(Clone, Debug, Serialize)]
 #[serde(tag = "kind", rename_all = "camelCase")]
 pub struct Call {
     pub target: H160,
@@ -107,7 +107,7 @@ pub struct Call {
 }
 
 #[serde_as]
-#[derive(Debug, Serialize)]
+#[derive(Clone, Debug, Serialize)]
 #[serde(rename_all = "camelCase")]
 pub struct LiquidityInteraction {
     pub internalize: bool,
@@ -121,7 +121,7 @@ pub struct LiquidityInteraction {
 }
 
 #[serde_as]
-#[derive(Debug, Serialize)]
+#[derive(Clone, Debug, Serialize)]
 #[serde(rename_all = "camelCase")]
 pub struct CustomInteraction {
     pub internalize: bool,
@@ -151,7 +151,7 @@ pub struct OrderInteraction {
 }
 
 #[serde_as]
-#[derive(Debug, Serialize)]
+#[derive(Clone, Debug, Serialize)]
 #[serde(rename_all = "camelCase")]
 pub struct Asset {
     pub token: H160,
@@ -160,7 +160,7 @@ pub struct Asset {
 }
 
 #[serde_as]
-#[derive(Debug, Serialize)]
+#[derive(Clone, Debug, Serialize)]
 #[serde(rename_all = "camelCase")]
 pub struct Allowance {
     pub token: H160,
@@ -169,7 +169,7 @@ pub struct Allowance {
     pub amount: U256,
 }
 
-#[derive(Debug, Default, Serialize)]
+#[derive(Clone, Debug, Default, Serialize)]
 #[serde(rename_all = "camelCase")]
 pub enum SellTokenBalance {
     #[default]
@@ -178,7 +178,7 @@ pub enum SellTokenBalance {
     External,
 }
 
-#[derive(Debug, Default, Serialize)]
+#[derive(Clone, Debug, Default, Serialize)]
 #[serde(rename_all = "camelCase")]
 pub enum BuyTokenBalance {
     #[default]
@@ -186,7 +186,7 @@ pub enum BuyTokenBalance {
     Internal,
 }
 
-#[derive(Debug, Serialize)]
+#[derive(Clone, Debug, Serialize)]
 #[serde(rename_all = "camelCase")]
 pub enum SigningScheme {
     Eip712,