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,