From debfa394b8ddd96e2a123d6ca07d4ec7ff5b6db4 Mon Sep 17 00:00:00 2001 From: Dusan Stanivukovic Date: Fri, 31 May 2024 09:37:51 +0200 Subject: [PATCH 1/7] 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 - [ ] Added `Roles` smart contract - [ ] Added EOA account as configuration - [ ] Implemented `remove_solver` function ## How to test todo --------- Co-authored-by: Mateo Co-authored-by: Mateo-mro <160488334+Mateo-mro@users.noreply.github.com> --- .../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 + 7 files changed, 155 insertions(+), 10 deletions(-) create mode 100644 crates/autopilot/src/infra/blockchain/authenticator.rs create mode 100644 crates/contracts/artifacts/Roles.json 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, - 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, pub weth: Option, 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) { @@ -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 e7ad747000..1d4f99e7df 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"), + // + deployment_information: Some(DeploymentInformation::BlockNumber(18692162)), + }, + ) + .add_network( + GNOSIS, + Network { + address: addr("0x9646fDAD06d3e24444381f44362a3B0eB343D337"), + // + deployment_information: Some(DeploymentInformation::BlockNumber(31222929)), + }, + ) + .add_network( + SEPOLIA, + Network { + address: addr("0x9646fDAD06d3e24444381f44362a3B0eB343D337"), + // + deployment_information: Some(DeploymentInformation::BlockNumber(4884885)), + }, + ) + .add_network( + ARBITRUM_ONE, + Network { + address: addr("0x9646fDAD06d3e24444381f44362a3B0eB343D337"), + // + 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 c8a3dff096..e124ef5bc3 100644 --- a/crates/contracts/src/lib.rs +++ b/crates/contracts/src/lib.rs @@ -50,6 +50,7 @@ include_contracts! { GnosisSafeCompatibilityFallbackHandler; GnosisSafeProxy; GnosisSafeProxyFactory; + Roles; HoneyswapRouter; HooksTrampoline; ISwaprPair; From 84fe9c6d423128fa1f6feab43f9a18b54589e5fa Mon Sep 17 00:00:00 2001 From: Martin Beckmann Date: Mon, 3 Jun 2024 13:50:41 +0200 Subject: [PATCH 2/7] Cow amm encoding (#2764) # Description Currently surplus capturing JIT orders do not get encoded such that they get surplus. Instead they always trade at their limit price. This is problematic for CoW AMM orders since there is no reason to rebalance CoW AMMs if these trades can't get surplus. # Changes Since we are short on time we hackily convert surplus capturing JIT orders into Fulfillments (aka regular orders) when building the solution in the driver. That way they will get encoded the same way regular orders will. Quite a few additional changes were needed to support solvers setting an execution fee for JIT orders. Now regular orders and JIT orders are even more similar which increases the hackiness of this PR. :/ Also a small fix for converting EIP-1271 signatures was necessary to have them encoded correctly in the final settlement calldata. - removed signed `fee` from JIT orders (driver enforces a value of 0 when parsing them) - removed signed `partiallyFillable` flag from JIT orders (driver enforces fill-or-kill semantics when parsing them) ## How to test Test in https://github.com/cowprotocol/services/pull/2766 --------- Co-authored-by: sunce86 --- .github/workflows/pull-request.yaml | 2 + crates/driver/src/boundary/settlement.rs | 9 +- .../src/domain/competition/order/mod.rs | 17 +- .../domain/competition/solution/encoding.rs | 5 +- .../src/domain/competition/solution/mod.rs | 47 ++++- .../src/domain/competition/solution/trade.rs | 42 ++++- .../driver/src/infra/solver/dto/solution.rs | 165 ++++++++++-------- crates/solvers-dto/src/solution.rs | 5 +- crates/solvers/openapi.yml | 18 +- .../src/api/routes/solve/dto/solution.rs | 3 +- crates/solvers/src/domain/solution.rs | 1 + 11 files changed, 214 insertions(+), 100 deletions(-) diff --git a/.github/workflows/pull-request.yaml b/.github/workflows/pull-request.yaml index 4c9f763f27..c7596338d1 100644 --- a/.github/workflows/pull-request.yaml +++ b/.github/workflows/pull-request.yaml @@ -108,6 +108,8 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - run: rustup toolchain install stable --profile minimal - uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly-f479e945c6be78bb902df12f9d683c3bb55e3fb0 - uses: Swatinem/rust-cache@v2 # Start the build process in the background. The following cargo test command will automatically # wait for the build process to be done before proceeding. diff --git a/crates/driver/src/boundary/settlement.rs b/crates/driver/src/boundary/settlement.rs index 75d0ee5780..79ac6b6eee 100644 --- a/crates/driver/src/boundary/settlement.rs +++ b/crates/driver/src/boundary/settlement.rs @@ -297,12 +297,15 @@ fn to_boundary_jit_order(domain: &DomainSeparator, order: &order::Jit) -> Order buy_amount: order.buy.amount.into(), valid_to: order.valid_to.into(), app_data: AppDataHash(order.app_data.into()), - fee_amount: order.fee.into(), + fee_amount: order.fee().0, kind: match order.side { competition::order::Side::Buy => OrderKind::Buy, competition::order::Side::Sell => OrderKind::Sell, }, - partially_fillable: order.partially_fillable, + partially_fillable: match order.partially_fillable() { + order::Partial::No => false, + order::Partial::Yes { .. } => true, + }, sell_token_balance: match order.sell_token_balance { competition::order::SellTokenBalance::Erc20 => SellTokenSource::Erc20, competition::order::SellTokenBalance::Internal => SellTokenSource::Internal, @@ -315,7 +318,7 @@ fn to_boundary_jit_order(domain: &DomainSeparator, order: &order::Jit) -> Order }; let metadata = OrderMetadata { owner: order.signature.signer.into(), - full_fee_amount: order.fee.into(), + full_fee_amount: order.fee().into(), // All foreign orders **MUST** be liquidity, this is // important so they cannot be used to affect the objective. class: OrderClass::Liquidity, diff --git a/crates/driver/src/domain/competition/order/mod.rs b/crates/driver/src/domain/competition/order/mod.rs index bf5cb5c77e..dfc6321074 100644 --- a/crates/driver/src/domain/competition/order/mod.rs +++ b/crates/driver/src/domain/competition/order/mod.rs @@ -361,15 +361,14 @@ pub struct Jit { /// The amount this order wants to buy when completely filled. /// The actual executed amount depends on partial fills and the order side. pub buy: eth::Asset, - pub fee: SellAmount, pub receiver: eth::Address, pub valid_to: util::Timestamp, pub app_data: AppData, pub side: Side, - pub partially_fillable: bool, pub sell_token_balance: SellTokenBalance, pub buy_token_balance: BuyTokenBalance, pub signature: Signature, + pub uid: Uid, } impl Jit { @@ -381,6 +380,20 @@ impl Jit { Side::Sell => self.sell.amount.into(), } } + + /// Returns the signed fee of the order. You can't set this field in + /// the API so it's enforced to be 0. This function only exists to + /// not have magic values scattered everywhere. + pub fn fee(&self) -> SellAmount { + SellAmount(0.into()) + } + + /// Returns the signed partially fillable property of the order. You can't + /// set this field in the API so it's enforced to be fill-or-kill. This + /// function only exists to not have magic values scattered everywhere. + pub fn partially_fillable(&self) -> Partial { + Partial::No + } } #[cfg(test)] diff --git a/crates/driver/src/domain/competition/solution/encoding.rs b/crates/driver/src/domain/competition/solution/encoding.rs index 5d90797d86..c82bb46e16 100644 --- a/crates/driver/src/domain/competition/solution/encoding.rs +++ b/crates/driver/src/domain/competition/solution/encoding.rs @@ -144,7 +144,10 @@ pub fn tx( fee_amount: eth::U256::zero(), flags: Flags { side: trade.order().side, - partially_fillable: trade.order().partially_fillable, + partially_fillable: matches!( + trade.order().partially_fillable(), + order::Partial::Yes { .. } + ), signing_scheme: trade.order().signature.scheme, sell_token_balance: trade.order().sell_token_balance, buy_token_balance: trade.order().buy_token_balance, diff --git a/crates/driver/src/domain/competition/solution/mod.rs b/crates/driver/src/domain/competition/solution/mod.rs index e07f771468..ae89c00702 100644 --- a/crates/driver/src/domain/competition/solution/mod.rs +++ b/crates/driver/src/domain/competition/solution/mod.rs @@ -1,5 +1,5 @@ use { - self::trade::ClearingPrices, + self::trade::{ClearingPrices, Fee, Fulfillment}, super::auction, crate::{ boundary, @@ -56,7 +56,7 @@ impl Solution { #[allow(clippy::too_many_arguments)] pub fn new( id: Id, - trades: Vec, + mut trades: Vec, prices: Prices, pre_interactions: Vec, interactions: Vec, @@ -65,7 +65,48 @@ impl Solution { weth: eth::WethAddress, gas: Option, fee_handler: FeeHandler, + surplus_capturing_jit_order_owners: &HashSet, ) -> Result { + // Surplus capturing JIT orders behave like Fulfillment orders. They capture + // surplus, pay network fees and contribute to score of a solution. + // To make sure that all the same logic and checks get applied we convert them + // right away. + for trade in &mut trades { + let Trade::Jit(jit) = trade else { continue }; + if !surplus_capturing_jit_order_owners.contains(&jit.order().signature.signer) { + continue; + } + + *trade = Trade::Fulfillment( + Fulfillment::new( + competition::Order { + uid: jit.order().uid, + kind: order::Kind::Limit, + side: jit.order().side, + sell: jit.order().sell, + buy: jit.order().buy, + signature: jit.order().signature.clone(), + receiver: Some(jit.order().receiver), + valid_to: jit.order().valid_to, + app_data: jit.order().app_data, + partial: jit.order().partially_fillable(), + pre_interactions: vec![], + post_interactions: vec![], + sell_token_balance: jit.order().sell_token_balance, + buy_token_balance: jit.order().buy_token_balance, + protocol_fees: vec![], + }, + jit.executed(), + Fee::Dynamic(jit.fee()), + ) + .map_err(error::Solution::InvalidJitTrade)?, + ); + tracing::debug!( + fulfillment = ?trade, + "converted surplus capturing JIT trade into fulfillment" + ); + } + let solution = Self { id, trades, @@ -555,6 +596,8 @@ pub mod error { InvalidClearingPrices, #[error(transparent)] ProtocolFee(#[from] fee::Error), + #[error("invalid JIT trade")] + InvalidJitTrade(Trade), } #[derive(Debug, thiserror::Error)] diff --git a/crates/driver/src/domain/competition/solution/trade.rs b/crates/driver/src/domain/competition/solution/trade.rs index 373606b71f..0cc9a7e475 100644 --- a/crates/driver/src/domain/competition/solution/trade.rs +++ b/crates/driver/src/domain/competition/solution/trade.rs @@ -43,7 +43,7 @@ impl Trade { pub fn fee(&self) -> SellAmount { match self { Trade::Fulfillment(fulfillment) => fulfillment.fee(), - Trade::Jit(jit) => jit.order().fee, + Trade::Jit(jit) => jit.fee, } } @@ -349,20 +349,44 @@ pub struct Jit { /// partially fillable, the executed amount must equal the amount from the /// order. executed: order::TargetAmount, + fee: order::SellAmount, } impl Jit { - pub fn new(order: order::Jit, executed: order::TargetAmount) -> Result { + pub fn new( + order: order::Jit, + executed: order::TargetAmount, + fee: order::SellAmount, + ) -> Result { + // If the order is partial, the total executed amount can be smaller than + // the target amount. Otherwise, the executed amount must be equal to the target + // amount. + let fee_target_amount = match order.side { + order::Side::Buy => order::TargetAmount::default(), + order::Side::Sell => fee.0.into(), + }; + + let executed_with_fee = order::TargetAmount( + executed + .0 + .checked_add(fee_target_amount.into()) + .ok_or(error::Trade::InvalidExecutedAmount)?, + ); + // If the order is partially fillable, the executed amount can be smaller than // the target amount. Otherwise, the executed amount must be equal to the target // amount. - let is_valid = if order.partially_fillable { - executed <= order.target() - } else { - executed == order.target() + let is_valid = match order.partially_fillable() { + order::Partial::Yes { available } => executed_with_fee <= available, + order::Partial::No => executed_with_fee == order.target(), }; + if is_valid { - Ok(Self { order, executed }) + Ok(Self { + order, + executed, + fee, + }) } else { Err(error::Trade::InvalidExecutedAmount) } @@ -375,6 +399,10 @@ impl Jit { pub fn executed(&self) -> order::TargetAmount { self.executed } + + pub fn fee(&self) -> order::SellAmount { + self.fee + } } /// The amounts executed by a trade. diff --git a/crates/driver/src/infra/solver/dto/solution.rs b/crates/driver/src/infra/solver/dto/solution.rs index ac9cd7c16e..90b77d19cb 100644 --- a/crates/driver/src/infra/solver/dto/solution.rs +++ b/crates/driver/src/infra/solver/dto/solution.rs @@ -61,6 +61,9 @@ impl Solutions { Trade::Jit(jit) => Ok(competition::solution::Trade::Jit( competition::solution::trade::Jit::new( competition::order::Jit { + uid: jit.order.uid( + solver.eth.contracts().settlement_domain_separator(), + )?, sell: eth::Asset { amount: jit.order.sell_amount.into(), token: jit.order.sell_token.into(), @@ -69,7 +72,6 @@ impl Solutions { amount: jit.order.buy_amount.into(), token: jit.order.buy_token.into(), }, - fee: jit.order.fee_amount.into(), receiver: jit.order.receiver.into(), valid_to: jit.order.valid_to.into(), app_data: jit.order.app_data.into(), @@ -77,7 +79,6 @@ impl Solutions { Kind::Sell => competition::order::Side::Sell, Kind::Buy => competition::order::Side::Buy, }, - partially_fillable: jit.order.partially_fillable, sell_token_balance: match jit.order.sell_token_balance { SellTokenBalance::Erc20 => { competition::order::SellTokenBalance::Erc20 @@ -97,34 +98,12 @@ impl Solutions { competition::order::BuyTokenBalance::Internal } }, - signature: { - let mut signature = competition::order::Signature { - scheme: match jit.order.signing_scheme { - SigningScheme::Eip712 => { - competition::order::signature::Scheme::Eip712 - } - SigningScheme::EthSign => { - competition::order::signature::Scheme::EthSign - } - SigningScheme::PreSign => { - competition::order::signature::Scheme::PreSign - } - SigningScheme::Eip1271 => { - competition::order::signature::Scheme::Eip1271 - } - }, - data: jit.order.signature.clone().into(), - signer: Default::default(), - }; - - // Recover the signer from the order signature - let signer = Self::recover_signer_from_jit_trade_order(&jit, &signature, solver.eth.contracts().settlement_domain_separator())?; - signature.signer = signer; - - signature - }, + signature: jit.order.signature( + solver.eth.contracts().settlement_domain_separator(), + )?, }, jit.executed_amount.into(), + jit.fee.into(), ) .map_err(|err| super::Error(format!("invalid JIT trade: {err}")))?, )), @@ -224,6 +203,7 @@ impl Solutions { weth, solution.gas.map(|gas| eth::Gas(gas.into())), solver_config.fee_handler, + auction.surplus_capturing_jit_order_owners(), ) .map_err(|err| match err { competition::solution::error::Solution::InvalidClearingPrices => { @@ -232,52 +212,13 @@ impl Solutions { competition::solution::error::Solution::ProtocolFee(err) => { super::Error(format!("could not incorporate protocol fee: {err}")) } + competition::solution::error::Solution::InvalidJitTrade(err) => { + super::Error(format!("invalid jit trade: {err}")) + } }) }) .collect() } - - /// Function to recover the signer of a JIT order - fn recover_signer_from_jit_trade_order( - jit: &JitTrade, - signature: &competition::order::Signature, - domain: ð::DomainSeparator, - ) -> Result { - let order_data = OrderData { - sell_token: jit.order.sell_token, - buy_token: jit.order.buy_token, - receiver: Some(jit.order.receiver), - sell_amount: jit.order.sell_amount, - buy_amount: jit.order.buy_amount, - valid_to: jit.order.valid_to, - app_data: AppDataHash(jit.order.app_data), - fee_amount: jit.order.fee_amount, - kind: match jit.order.kind { - Kind::Sell => OrderKind::Sell, - Kind::Buy => OrderKind::Buy, - }, - partially_fillable: jit.order.partially_fillable, - sell_token_balance: match jit.order.sell_token_balance { - SellTokenBalance::Erc20 => SellTokenSource::Erc20, - SellTokenBalance::Internal => SellTokenSource::Internal, - SellTokenBalance::External => SellTokenSource::External, - }, - buy_token_balance: match jit.order.buy_token_balance { - BuyTokenBalance::Erc20 => BuyTokenDestination::Erc20, - BuyTokenBalance::Internal => BuyTokenDestination::Internal, - }, - }; - - signature - .to_boundary_signature() - .recover_owner( - jit.order.signature.as_slice(), - &DomainSeparator(domain.0), - &order_data.hash_struct(), - ) - .map_err(|e| super::Error(e.to_string())) - .map(Into::into) - } } #[serde_as] @@ -333,6 +274,9 @@ struct JitTrade { order: JitOrder, #[serde_as(as = "serialize::U256")] executed_amount: eth::U256, + #[serde(default)] + #[serde_as(as = "serialize::U256")] + fee: eth::U256, } #[serde_as] @@ -349,10 +293,7 @@ struct JitOrder { valid_to: u32, #[serde_as(as = "serialize::Hex")] app_data: [u8; order::APP_DATA_LEN], - #[serde_as(as = "serialize::U256")] - fee_amount: eth::U256, kind: Kind, - partially_fillable: bool, sell_token_balance: SellTokenBalance, buy_token_balance: BuyTokenBalance, signing_scheme: SigningScheme, @@ -360,6 +301,84 @@ struct JitOrder { signature: Vec, } +impl JitOrder { + fn raw_order_data(&self) -> OrderData { + OrderData { + sell_token: self.sell_token, + buy_token: self.buy_token, + receiver: Some(self.receiver), + sell_amount: self.sell_amount, + buy_amount: self.buy_amount, + valid_to: self.valid_to, + app_data: AppDataHash(self.app_data), + fee_amount: 0.into(), + kind: match self.kind { + Kind::Sell => OrderKind::Sell, + Kind::Buy => OrderKind::Buy, + }, + partially_fillable: false, + sell_token_balance: match self.sell_token_balance { + SellTokenBalance::Erc20 => SellTokenSource::Erc20, + SellTokenBalance::Internal => SellTokenSource::Internal, + SellTokenBalance::External => SellTokenSource::External, + }, + buy_token_balance: match self.buy_token_balance { + BuyTokenBalance::Erc20 => BuyTokenDestination::Erc20, + BuyTokenBalance::Internal => BuyTokenDestination::Internal, + }, + } + } + + fn signature( + &self, + domain_separator: ð::DomainSeparator, + ) -> Result { + let mut signature = competition::order::Signature { + scheme: match self.signing_scheme { + SigningScheme::Eip712 => competition::order::signature::Scheme::Eip712, + SigningScheme::EthSign => competition::order::signature::Scheme::EthSign, + SigningScheme::PreSign => competition::order::signature::Scheme::PreSign, + SigningScheme::Eip1271 => competition::order::signature::Scheme::Eip1271, + }, + data: self.signature.clone().into(), + signer: Default::default(), + }; + + let signer = signature + .to_boundary_signature() + .recover_owner( + self.signature.as_slice(), + &DomainSeparator(domain_separator.0), + &self.raw_order_data().hash_struct(), + ) + .map_err(|e| super::Error(e.to_string()))?; + + if matches!(self.signing_scheme, SigningScheme::Eip1271) { + // For EIP-1271 signatures the encoding logic prepends the signer to the raw + // signature bytes. This leads to the owner being encoded twice in + // the final settlement calldata unless we remove that from the raw + // data. + signature.data = Bytes(self.signature[20..].to_vec()); + } + + signature.signer = signer.into(); + + Ok(signature) + } + + fn uid( + &self, + domain: ð::DomainSeparator, + ) -> Result { + let order_data = self.raw_order_data(); + let signature = self.signature(domain)?; + Ok(order_data + .uid(&DomainSeparator(domain.0), &signature.signer.into()) + .0 + .into()) + } +} + #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] enum Kind { diff --git a/crates/solvers-dto/src/solution.rs b/crates/solvers-dto/src/solution.rs index e2a7111f52..6375c9b451 100644 --- a/crates/solvers-dto/src/solution.rs +++ b/crates/solvers-dto/src/solution.rs @@ -57,6 +57,8 @@ pub struct JitTrade { pub order: JitOrder, #[serde_as(as = "HexOrDecimalU256")] pub executed_amount: U256, + #[serde_as(as = "Option")] + pub fee: Option, } #[serde_as] @@ -73,10 +75,7 @@ pub struct JitOrder { pub valid_to: u32, #[serde_as(as = "serialize::Hex")] pub app_data: [u8; 32], - #[serde_as(as = "HexOrDecimalU256")] - pub fee_amount: U256, pub kind: Kind, - pub partially_fillable: bool, pub sell_token_balance: SellTokenBalance, pub buy_token_balance: BuyTokenBalance, pub signing_scheme: SigningScheme, diff --git a/crates/solvers/openapi.yml b/crates/solvers/openapi.yml index 423b92516f..2526bb9bd2 100644 --- a/crates/solvers/openapi.yml +++ b/crates/solvers/openapi.yml @@ -668,7 +668,8 @@ components: JitOrder: description: | - A just-in-time liquidity order included in a settlement. + A just-in-time liquidity order included in a settlement. These will + be assumed to be fill-or-kill orders with a signed fee of 0. type: object required: - sellToken @@ -678,9 +679,7 @@ components: - buyAmount - validTo - appData - - feeAmount - kind - - partiallyFillable - sellTokenBalance - buyTokenBalance - signingScheme @@ -700,12 +699,8 @@ components: type: integer appData: $ref: "#/components/schemas/AppData" - feeAmount: - $ref: "#/components/schemas/TokenAmount" kind: $ref: "#/components/schemas/OrderKind" - partiallyFillable: - type: boolean sellTokenBalance: $ref: "#/components/schemas/SellTokenBalance" buyTokenBalance: @@ -751,6 +746,7 @@ components: - kind - order - executedAmount + - fee properties: kind: type: string @@ -761,6 +757,14 @@ components: "sellToken" for sell orders, and "buyToken" for buy orders. allOf: - $ref: "#/components/schemas/TokenAmount" + fee: + description: | + The amount of sell token which should be kept to cover the gas + cost for this JIT trade. If a fee is set on a sell order the + "executedAmount" needs to be reduced accordingly to not "overfill" + the order. + allOf: + - $ref: "#/components/schemas/TokenAmount" order: description: | The just-in-time liquidity order to execute in a solution. diff --git a/crates/solvers/src/api/routes/solve/dto/solution.rs b/crates/solvers/src/api/routes/solve/dto/solution.rs index 495b947e97..9ff7f82c32 100644 --- a/crates/solvers/src/api/routes/solve/dto/solution.rs +++ b/crates/solvers/src/api/routes/solve/dto/solution.rs @@ -48,18 +48,17 @@ pub fn from_domain(solutions: &[solution::Solution]) -> super::Solutions { receiver: trade.order.receiver, valid_to: trade.order.valid_to, app_data: trade.order.app_data.0, - fee_amount: 0.into(), kind: match trade.order.side { crate::domain::order::Side::Buy => Kind::Buy, crate::domain::order::Side::Sell => Kind::Sell, }, - partially_fillable: trade.order.partially_fillable, sell_token_balance: SellTokenBalance::Erc20, buy_token_balance: BuyTokenBalance::Erc20, signing_scheme, signature, }, executed_amount: trade.executed, + fee: Some(trade.fee.0), }) } }) diff --git a/crates/solvers/src/domain/solution.rs b/crates/solvers/src/domain/solution.rs index 93a2ff0eb0..ea4e323c0f 100644 --- a/crates/solvers/src/domain/solution.rs +++ b/crates/solvers/src/domain/solution.rs @@ -318,6 +318,7 @@ impl Fee { pub struct JitTrade { pub order: order::JitOrder, pub executed: U256, + pub fee: eth::SellTokenAmount, } /// An interaction that is required to execute a solution by acquiring liquidity From 861d1a5d203e5a034f08b579d4000c9d8a8fd823 Mon Sep 17 00:00:00 2001 From: ilya Date: Mon, 3 Jun 2024 17:01:12 +0100 Subject: [PATCH 3/7] Uniswap V2 contract addresses fix (#2767) --- crates/contracts/build.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/contracts/build.rs b/crates/contracts/build.rs index 1d4f99e7df..ae2af17ec4 100644 --- a/crates/contracts/build.rs +++ b/crates/contracts/build.rs @@ -720,7 +720,7 @@ fn main() { .add_network_str(MAINNET, "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f") .add_network_str(GOERLI, "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f") .add_network_str(GNOSIS, "0xA818b4F111Ccac7AA31D0BCc0806d64F2E0737D7") - .add_network_str(ARBITRUM_ONE, "0x6554AD1Afaa3f4ce16dc31030403590F467417A6") + .add_network_str(ARBITRUM_ONE, "0xf1D7CC64Fb4452F05c498126312eBE29f30Fbcf9") // Not available on Sepolia }); generate_contract_with_config("UniswapV2Router02", |builder| { @@ -729,7 +729,7 @@ fn main() { .add_network_str(MAINNET, "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D") .add_network_str(GOERLI, "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D") .add_network_str(GNOSIS, "0x1C232F01118CB8B424793ae03F870aa7D0ac7f77") - .add_network_str(ARBITRUM_ONE, "0xaedE1EFe768bD8A1663A7608c63290C60B85e71c") + .add_network_str(ARBITRUM_ONE, "0x4752ba5dbc23f44d87826276bf6fd6b1c372ad24") // Not available on Sepolia }); generate_contract_with_config("UniswapV3SwapRouter", |builder| { From c3d392d4367e4d0f0aa363bf3d397e18eb530866 Mon Sep 17 00:00:00 2001 From: Felix Leupold Date: Tue, 4 Jun 2024 15:56:37 +0200 Subject: [PATCH 4/7] Remove boundary encoding (#2769) # Description Now that domain based settlement encoding is stable we can remove the boundary encoding and configuration variable # Changes - [x] Remove boundary encoding logic - [x] Remove config param This will also require a infrastructure PR to remove the domain encoding config variable ## How to test CI --- crates/driver/src/boundary/mod.rs | 2 - crates/driver/src/boundary/settlement.rs | 403 ------------------ crates/driver/src/domain/competition/mod.rs | 4 +- .../domain/competition/solution/encoding.rs | 9 - .../src/domain/competition/solution/mod.rs | 3 +- .../domain/competition/solution/settlement.rs | 81 +--- crates/driver/src/infra/api/mod.rs | 2 - crates/driver/src/infra/config/file/load.rs | 1 - crates/driver/src/infra/config/file/mod.rs | 27 -- crates/driver/src/infra/config/mod.rs | 3 - crates/driver/src/run.rs | 1 - crates/e2e/src/setup/mod.rs | 3 +- crates/e2e/src/setup/services.rs | 1 + crates/e2e/tests/e2e/liquidity.rs | 20 +- 14 files changed, 35 insertions(+), 525 deletions(-) delete mode 100644 crates/driver/src/boundary/settlement.rs diff --git a/crates/driver/src/boundary/mod.rs b/crates/driver/src/boundary/mod.rs index db03539d4f..981b8a896b 100644 --- a/crates/driver/src/boundary/mod.rs +++ b/crates/driver/src/boundary/mod.rs @@ -23,7 +23,6 @@ //! Software (2014) pub mod liquidity; -pub mod settlement; // The [`anyhow::Error`] type is re-exported because the legacy code mostly // returns that error. This will change as the legacy code gets refactored away. @@ -32,7 +31,6 @@ pub use { anyhow::{Error, Result}, contracts, model::order::OrderData, - settlement::Settlement, shared::ethrpc::Web3, }; diff --git a/crates/driver/src/boundary/settlement.rs b/crates/driver/src/boundary/settlement.rs deleted file mode 100644 index 79ac6b6eee..0000000000 --- a/crates/driver/src/boundary/settlement.rs +++ /dev/null @@ -1,403 +0,0 @@ -use { - crate::{ - domain::{ - competition::{ - self, - auction, - order, - solution::settlement::{self, Internalization}, - }, - eth, - liquidity, - }, - infra::{solver::ManageNativeToken, Ethereum}, - }, - anyhow::{anyhow, Context, Ok, Result}, - app_data::AppDataHash, - model::{ - interaction::InteractionData, - order::{ - BuyTokenDestination, - Interactions, - Order, - OrderClass, - OrderData, - OrderKind, - OrderMetadata, - OrderUid, - SellTokenSource, - }, - DomainSeparator, - }, - shared::{ - external_prices::ExternalPrices, - http_solver::model::{InternalizationStrategy, TokenAmount}, - }, - solver::{ - interactions::Erc20ApproveInteraction, - liquidity::{ - order_converter::OrderConverter, - slippage::{SlippageCalculator, SlippageContext}, - AmmOrderExecution, - LimitOrderExecution, - }, - settlement::Revertable, - }, - std::{collections::HashMap, sync::Arc}, -}; - -#[derive(Debug, Clone)] -pub struct Settlement { - pub(super) inner: solver::settlement::Settlement, - pub solver: eth::Address, -} - -impl Settlement { - pub async fn encode( - eth: &Ethereum, - solution: &competition::Solution, - auction: &competition::Auction, - manage_native_token: ManageNativeToken, - ) -> Result { - let native_token = eth.contracts().weth(); - let order_converter = OrderConverter { - native_token: native_token.clone(), - }; - - let settlement_contract = eth.contracts().settlement(); - let domain = order::signature::domain_separator( - eth.network(), - settlement_contract.clone().address().into(), - ); - - let mut settlement = solver::settlement::Settlement::new( - solution - .clearing_prices() - .into_iter() - .map(|asset| (asset.token.into(), asset.amount.into())) - .collect(), - ); - - for trade in solution.trades() { - let (boundary_order, execution) = match trade { - competition::solution::Trade::Fulfillment(trade) => { - // TODO: The `http_solver` module filters out orders with 0 - // executed amounts which seems weird to me... why is a - // solver specifying trades with 0 executed amounts? - if eth::U256::from(trade.executed()).is_zero() { - return Err(anyhow!("unexpected empty execution")); - } - - ( - to_boundary_order(trade.order()), - LimitOrderExecution { - filled: trade.executed().into(), - fee: trade.fee().into(), - }, - ) - } - competition::solution::Trade::Jit(trade) => ( - to_boundary_jit_order(&DomainSeparator(domain.0), trade.order()), - LimitOrderExecution { - filled: trade.executed().into(), - fee: 0.into(), - }, - ), - }; - - let boundary_limit_order = order_converter.normalize_limit_order( - solver::liquidity::BalancedOrder::full(boundary_order), - manage_native_token.insert_unwraps, - )?; - settlement.with_liquidity(&boundary_limit_order, execution)?; - } - - let approvals = solution - .approvals(eth, settlement::Internalization::Disable) - .await?; - for approval in approvals { - settlement - .encoder - .append_to_execution_plan(Arc::new(Erc20ApproveInteraction { - token: eth.contract_at(approval.0.token.into()), - spender: approval.0.spender.into(), - amount: approval.0.amount, - })); - } - - let slippage_calculator = SlippageCalculator { - relative: solution.solver().slippage().relative.clone(), - absolute: solution.solver().slippage().absolute.map(Into::into), - }; - let external_prices = ExternalPrices::try_from_auction_prices( - native_token.address(), - auction - .tokens() - .iter() - .filter_map(|token| { - token - .price - .map(|price| (token.address.into(), price.into())) - }) - .collect(), - )?; - let slippage_context = slippage_calculator.context(&external_prices); - - for interaction in solution.interactions() { - let boundary_interaction = to_boundary_interaction( - &slippage_context, - settlement_contract.address().into(), - interaction, - )?; - settlement.encoder.append_to_execution_plan_internalizable( - Arc::new(boundary_interaction), - interaction.internalize(), - ); - } - - Ok(Self { - inner: settlement, - solver: solution.solver().address(), - }) - } - - pub fn tx( - &self, - auction_id: auction::Id, - contract: &contracts::GPv2Settlement, - internalization: Internalization, - ) -> eth::Tx { - let encoded_settlement = self.inner.clone().encode(match internalization { - settlement::Internalization::Enable => { - InternalizationStrategy::SkipInternalizableInteraction - } - settlement::Internalization::Disable => InternalizationStrategy::EncodeAllInteractions, - }); - - let account = ethcontract::Account::Local(self.solver.into(), None); - let tx = contract - .settle( - encoded_settlement.tokens, - encoded_settlement.clearing_prices, - encoded_settlement.trades, - encoded_settlement.interactions, - ) - .from(account) - .into_inner(); - - let mut input = tx.data.unwrap().0; - input.extend(auction_id.to_be_bytes()); - eth::Tx { - from: self.solver, - to: tx.to.unwrap().into(), - value: tx.value.unwrap_or_default().into(), - input: input.into(), - access_list: Default::default(), - } - } - - pub fn clearing_prices(&self) -> HashMap { - self.inner - .clearing_prices() - .iter() - .map(|(&token, &amount)| (token.into(), amount.into())) - .collect() - } - - pub fn revertable(&self) -> bool { - self.inner.revertable() != Revertable::NoRisk - } -} - -fn to_boundary_order(order: &competition::Order) -> Order { - Order { - data: OrderData { - sell_token: order.sell.token.into(), - buy_token: order.buy.token.into(), - sell_amount: order.sell.amount.into(), - buy_amount: order.buy.amount.into(), - // The fee amount is guaranteed to be 0 and it no longer exists in the domain, but for - // the proper encoding of the order where the `model::OrderData` struct is used, we must - // set it to 0. - fee_amount: 0.into(), - receiver: order.receiver.map(Into::into), - valid_to: order.valid_to.into(), - app_data: AppDataHash(order.app_data.into()), - kind: match order.side { - competition::order::Side::Buy => OrderKind::Buy, - competition::order::Side::Sell => OrderKind::Sell, - }, - partially_fillable: order.is_partial(), - sell_token_balance: match order.sell_token_balance { - competition::order::SellTokenBalance::Erc20 => SellTokenSource::Erc20, - competition::order::SellTokenBalance::Internal => SellTokenSource::Internal, - competition::order::SellTokenBalance::External => SellTokenSource::External, - }, - buy_token_balance: match order.buy_token_balance { - competition::order::BuyTokenBalance::Erc20 => BuyTokenDestination::Erc20, - competition::order::BuyTokenBalance::Internal => BuyTokenDestination::Internal, - }, - }, - metadata: OrderMetadata { - full_fee_amount: Default::default(), - solver_fee: 0.into(), - class: match order.kind { - competition::order::Kind::Market => OrderClass::Market, - competition::order::Kind::Liquidity => OrderClass::Liquidity, - competition::order::Kind::Limit => OrderClass::Limit, - }, - creation_date: Default::default(), - owner: order.signature.signer.into(), - uid: OrderUid(order.uid.into()), - available_balance: Default::default(), - executed_buy_amount: Default::default(), - executed_sell_amount: Default::default(), - executed_sell_amount_before_fees: Default::default(), - executed_fee_amount: Default::default(), - executed_surplus_fee: Default::default(), - invalidated: Default::default(), - status: Default::default(), - settlement_contract: Default::default(), - ethflow_data: Default::default(), - onchain_user: Default::default(), - onchain_order_data: Default::default(), - is_liquidity_order: order.is_liquidity(), - full_app_data: Default::default(), - }, - signature: order.signature.to_boundary_signature(), - interactions: Interactions { - pre: order - .pre_interactions - .iter() - .map(|interaction| model::interaction::InteractionData { - target: interaction.target.into(), - value: interaction.value.into(), - call_data: interaction.call_data.clone().into(), - }) - .collect(), - post: order - .post_interactions - .iter() - .map(|interaction| model::interaction::InteractionData { - target: interaction.target.into(), - value: interaction.value.into(), - call_data: interaction.call_data.clone().into(), - }) - .collect(), - }, - } -} - -fn to_boundary_jit_order(domain: &DomainSeparator, order: &order::Jit) -> Order { - let data = OrderData { - sell_token: order.sell.token.into(), - buy_token: order.buy.token.into(), - receiver: Some(order.receiver.into()), - sell_amount: order.sell.amount.into(), - buy_amount: order.buy.amount.into(), - valid_to: order.valid_to.into(), - app_data: AppDataHash(order.app_data.into()), - fee_amount: order.fee().0, - kind: match order.side { - competition::order::Side::Buy => OrderKind::Buy, - competition::order::Side::Sell => OrderKind::Sell, - }, - partially_fillable: match order.partially_fillable() { - order::Partial::No => false, - order::Partial::Yes { .. } => true, - }, - sell_token_balance: match order.sell_token_balance { - competition::order::SellTokenBalance::Erc20 => SellTokenSource::Erc20, - competition::order::SellTokenBalance::Internal => SellTokenSource::Internal, - competition::order::SellTokenBalance::External => SellTokenSource::External, - }, - buy_token_balance: match order.buy_token_balance { - competition::order::BuyTokenBalance::Erc20 => BuyTokenDestination::Erc20, - competition::order::BuyTokenBalance::Internal => BuyTokenDestination::Internal, - }, - }; - let metadata = OrderMetadata { - owner: order.signature.signer.into(), - full_fee_amount: order.fee().into(), - // All foreign orders **MUST** be liquidity, this is - // important so they cannot be used to affect the objective. - class: OrderClass::Liquidity, - // Not needed for encoding but nice to have for logs and competition info. - uid: data.uid(domain, &order.signature.signer.into()), - // These fields do not seem to be used at all for order - // encoding, so we just use the default values. - ..Default::default() - }; - let signature = order.signature.to_boundary_signature(); - - Order { - data, - metadata, - signature, - interactions: Interactions::default(), - } -} - -pub fn to_boundary_interaction( - slippage_context: &SlippageContext, - settlement_contract: eth::ContractAddress, - interaction: &competition::solution::Interaction, -) -> Result { - match interaction { - competition::solution::Interaction::Custom(custom) => Ok(InteractionData { - target: custom.target.into(), - value: custom.value.into(), - call_data: custom.call_data.clone().into(), - }), - competition::solution::Interaction::Liquidity(liquidity) => { - let boundary_execution = - slippage_context.apply_to_amm_execution(AmmOrderExecution { - input_max: TokenAmount::new( - liquidity.input.token.into(), - liquidity.input.amount, - ), - output: TokenAmount::new( - liquidity.output.token.into(), - liquidity.output.amount, - ), - internalizable: interaction.internalize(), - })?; - - let input = liquidity::MaxInput(eth::Asset { - token: boundary_execution.input_max.token.into(), - amount: boundary_execution.input_max.amount.into(), - }); - let output = liquidity::ExactOutput(eth::Asset { - token: boundary_execution.output.token.into(), - amount: boundary_execution.output.amount.into(), - }); - - let interaction = match &liquidity.liquidity.kind { - liquidity::Kind::UniswapV2(pool) => pool - .swap(&input, &output, &settlement_contract.into()) - .context("invalid uniswap V2 execution")?, - liquidity::Kind::UniswapV3(pool) => pool - .swap(&input, &output, &settlement_contract.into()) - .context("invalid uniswap v3 execution")?, - liquidity::Kind::BalancerV2Stable(pool) => pool - .swap(&input, &output, &settlement_contract.into()) - .context("invalid balancer v2 stable execution")?, - liquidity::Kind::BalancerV2Weighted(pool) => pool - .swap(&input, &output, &settlement_contract.into()) - .context("invalid balancer v2 weighted execution")?, - liquidity::Kind::Swapr(pool) => pool - .swap(&input, &output, &settlement_contract.into()) - .context("invalid swapr execution")?, - liquidity::Kind::ZeroEx(limit_order) => limit_order - .to_interaction(&input) - .context("invalid zeroex execution")?, - }; - - Ok(InteractionData { - target: interaction.target.into(), - value: interaction.value.into(), - call_data: interaction.call_data.into(), - }) - } - } -} diff --git a/crates/driver/src/domain/competition/mod.rs b/crates/driver/src/domain/competition/mod.rs index c24ac90a03..6c2a8a43c1 100644 --- a/crates/driver/src/domain/competition/mod.rs +++ b/crates/driver/src/domain/competition/mod.rs @@ -1,5 +1,5 @@ use { - self::solution::{encoding, settlement}, + self::solution::settlement, super::{ time::{self, Remaining}, Mempools, @@ -50,7 +50,6 @@ pub struct Competition { pub simulator: Simulator, pub mempools: Mempools, pub settlement: Mutex>, - pub encoding: encoding::Strategy, } impl Competition { @@ -120,7 +119,6 @@ impl Competition { auction, &self.eth, &self.simulator, - self.encoding, self.solver.solver_native_token(), ) .await; diff --git a/crates/driver/src/domain/competition/solution/encoding.rs b/crates/driver/src/domain/competition/solution/encoding.rs index c82bb46e16..9e610e10a5 100644 --- a/crates/driver/src/domain/competition/solution/encoding.rs +++ b/crates/driver/src/domain/competition/solution/encoding.rs @@ -16,15 +16,6 @@ use { itertools::Itertools, }; -/// The type of strategy used to encode the solution. -#[derive(Debug, Copy, Clone)] -pub enum Strategy { - /// Use logic from the legacy solver crate - Boundary, - /// Use logic from this module for encoding - Domain, -} - #[derive(Debug, thiserror::Error)] pub enum Error { #[error("invalid interaction: {0:?}")] diff --git a/crates/driver/src/domain/competition/solution/mod.rs b/crates/driver/src/domain/competition/solution/mod.rs index ae89c00702..26d133d112 100644 --- a/crates/driver/src/domain/competition/solution/mod.rs +++ b/crates/driver/src/domain/competition/solution/mod.rs @@ -393,10 +393,9 @@ impl Solution { auction: &competition::Auction, eth: &Ethereum, simulator: &Simulator, - encoding: encoding::Strategy, solver_native_token: ManageNativeToken, ) -> Result { - Settlement::encode(self, auction, eth, simulator, encoding, solver_native_token).await + Settlement::encode(self, auction, eth, simulator, solver_native_token).await } /// Token prices settled by this solution, expressed using an arbitrary diff --git a/crates/driver/src/domain/competition/solution/settlement.rs b/crates/driver/src/domain/competition/solution/settlement.rs index 5d1a89811f..67a936c657 100644 --- a/crates/driver/src/domain/competition/solution/settlement.rs +++ b/crates/driver/src/domain/competition/solution/settlement.rs @@ -1,7 +1,6 @@ use { super::{encoding, trade::ClearingPrices, Error, Solution}, crate::{ - boundary, domain::{ competition::{self, auction, order, solution}, eth, @@ -69,7 +68,6 @@ impl Settlement { auction: &competition::Auction, eth: &Ethereum, simulator: &Simulator, - encoding: encoding::Strategy, solver_native_token: ManageNativeToken, ) -> Result { // For a settlement to be valid, the solution has to respect some rules which @@ -90,67 +88,24 @@ impl Settlement { } // Encode the solution into a settlement. - let tx = match encoding { - encoding::Strategy::Boundary => { - let boundary = - boundary::Settlement::encode(eth, &solution, auction, solver_native_token) - .await?; - let tx = SettlementTx { - internalized: boundary.tx( - auction.id().unwrap(), - eth.contracts().settlement(), - Internalization::Enable, - ), - uninternalized: boundary.tx( - auction.id().unwrap(), - eth.contracts().settlement(), - Internalization::Disable, - ), - may_revert: boundary.revertable(), - }; - - // To prepare rollout, ensure that the domain settlement encoding works and - // matches the boundary settlement encoding - match encoding::tx( - auction, - &solution, - eth.contracts(), - solution.approvals(eth, Internalization::Enable).await?, - Internalization::Enable, - solver_native_token, - ) { - Ok(domain) => { - if domain.input != tx.internalized.input { - tracing::warn!( - ?domain, - boundary = ?tx.internalized, - "boundary settlement does not match domain settlement" - ); - } - } - Err(err) => tracing::warn!(?err, "failed to encode domain settlement"), - }; - tx - } - encoding::Strategy::Domain => SettlementTx { - internalized: encoding::tx( - auction, - &solution, - eth.contracts(), - solution.approvals(eth, Internalization::Enable).await?, - Internalization::Enable, - solver_native_token, - )?, - uninternalized: encoding::tx( - auction, - &solution, - eth.contracts(), - solution.approvals(eth, Internalization::Disable).await?, - Internalization::Disable, - solver_native_token, - )?, - may_revert: solution.revertable(), - }, + let tx = SettlementTx { + internalized: encoding::tx( + auction, + &solution, + eth.contracts(), + solution.approvals(eth, Internalization::Enable).await?, + Internalization::Enable, + solver_native_token, + )?, + uninternalized: encoding::tx( + auction, + &solution, + eth.contracts(), + solution.approvals(eth, Internalization::Disable).await?, + Internalization::Disable, + solver_native_token, + )?, + may_revert: solution.revertable(), }; Self::new(auction.id().unwrap(), solution, tx, eth, simulator).await } diff --git a/crates/driver/src/infra/api/mod.rs b/crates/driver/src/infra/api/mod.rs index 9eec96536b..7ad6061c31 100644 --- a/crates/driver/src/infra/api/mod.rs +++ b/crates/driver/src/infra/api/mod.rs @@ -31,7 +31,6 @@ pub struct Api { /// If this channel is specified, the bound address will be sent to it. This /// allows the driver to bind to 0.0.0.0:0 during testing. pub addr_sender: Option>, - pub encoding: infra::config::encoding::Strategy, } impl Api { @@ -77,7 +76,6 @@ impl Api { simulator: self.simulator.clone(), mempools: self.mempools.clone(), settlement: Default::default(), - encoding: self.encoding.to_domain(), }, liquidity: self.liquidity.clone(), tokens: tokens.clone(), diff --git a/crates/driver/src/infra/config/file/load.rs b/crates/driver/src/infra/config/file/load.rs index 7a6a495e8e..6b4910b310 100644 --- a/crates/driver/src/infra/config/file/load.rs +++ b/crates/driver/src/infra/config/file/load.rs @@ -320,7 +320,6 @@ pub async fn load(chain: eth::ChainId, path: &Path) -> infra::Config { }, disable_access_list_simulation: config.disable_access_list_simulation, disable_gas_simulation: config.disable_gas_simulation.map(Into::into), - encoding: config.encoding, gas_estimator: config.gas_estimator, } } diff --git a/crates/driver/src/infra/config/file/mod.rs b/crates/driver/src/infra/config/file/mod.rs index 8e539c5066..e9b56ea1e7 100644 --- a/crates/driver/src/infra/config/file/mod.rs +++ b/crates/driver/src/infra/config/file/mod.rs @@ -54,9 +54,6 @@ struct Config { #[serde(default)] liquidity: LiquidityConfig, - - #[serde(default)] - encoding: encoding::Strategy, } #[serde_as] @@ -153,30 +150,6 @@ impl ManageNativeToken { } } -pub mod encoding { - use {crate::domain::competition, serde::Deserialize}; - - /// Which logic to use to encode solutions into settlement transactions. - #[derive(Debug, Deserialize, Default)] - #[serde(rename_all = "kebab-case")] - pub enum Strategy { - /// Legacy solver crate strategy - #[default] - Boundary, - /// New encoding strategy - Domain, - } - - impl Strategy { - pub fn to_domain(&self) -> competition::solution::encoding::Strategy { - match self { - Self::Boundary => competition::solution::encoding::Strategy::Boundary, - Self::Domain => competition::solution::encoding::Strategy::Domain, - } - } - } -} - fn default_additional_tip_percentage() -> f64 { 0.05 } diff --git a/crates/driver/src/infra/config/mod.rs b/crates/driver/src/infra/config/mod.rs index 7cc809976c..a70632af61 100644 --- a/crates/driver/src/infra/config/mod.rs +++ b/crates/driver/src/infra/config/mod.rs @@ -1,5 +1,3 @@ -pub use file::encoding; - use crate::{ domain::eth, infra::{blockchain, config::file::GasEstimatorType, liquidity, mempool, simulator, solver}, @@ -18,5 +16,4 @@ pub struct Config { pub gas_estimator: GasEstimatorType, pub mempools: Vec, pub contracts: blockchain::contracts::Addresses, - pub encoding: encoding::Strategy, } diff --git a/crates/driver/src/run.rs b/crates/driver/src/run.rs index 331fea8bca..8301321d37 100644 --- a/crates/driver/src/run.rs +++ b/crates/driver/src/run.rs @@ -69,7 +69,6 @@ async fn run_with(args: cli::Args, addr_sender: Option( macro_rules! assert_approximately_eq { ($executed_value:expr, $expected_value:expr) => {{ let lower = $expected_value * U256::from(99999999999u128) / U256::from(100000000000u128); - let upper = $expected_value * U256::from(100000000001u128) / U256::from(100000000000u128); + let upper = + ($expected_value * U256::from(100000000001u128) / U256::from(100000000000u128)) + 1; assert!( $executed_value >= lower && $executed_value <= upper, "Expected: ~{}, got: {}", diff --git a/crates/e2e/src/setup/services.rs b/crates/e2e/src/setup/services.rs index d29248590d..d10cbfd120 100644 --- a/crates/e2e/src/setup/services.rs +++ b/crates/e2e/src/setup/services.rs @@ -381,6 +381,7 @@ impl<'a> Services<'a> { &self, order: &OrderCreation, ) -> Result { + tracing::info!("Creating order: {order:?}"); let placement = self .http .post(format!("{API_HOST}{ORDERS_ENDPOINT}")) diff --git a/crates/e2e/tests/e2e/liquidity.rs b/crates/e2e/tests/e2e/liquidity.rs index e6fa744ff3..710587a149 100644 --- a/crates/e2e/tests/e2e/liquidity.rs +++ b/crates/e2e/tests/e2e/liquidity.rs @@ -4,6 +4,7 @@ use { driver::domain::eth::H160, e2e::{ api::zeroex::{Eip712TypedZeroExOrder, ZeroExApi}, + assert_approximately_eq, nodes::forked_node::ForkedNodeApi, setup::{ colocation::{self, SolverEngine}, @@ -202,11 +203,14 @@ async fn zero_ex_liquidity(web3: Web3) { // crates/e2e/src/setup/colocation.rs:110 which is then applied to the // original filled amount crates/solver/src/liquidity/slippage.rs:110 let expected_filled_amount = amount.as_u128() + amount.as_u128() / 10u128; - assert_eq!(zeroex_order_amounts.filled, expected_filled_amount); + assert_approximately_eq!( + U256::from(zeroex_order_amounts.filled), + U256::from(expected_filled_amount) + ); assert!(zeroex_order_amounts.fillable > 0u128); - assert_eq!( - zeroex_order_amounts.fillable, - amount.as_u128() * 2 - expected_filled_amount + assert_approximately_eq!( + U256::from(zeroex_order_amounts.fillable), + U256::from(amount.as_u128() * 2 - expected_filled_amount) ); // Fill the remaining part of the 0x order @@ -233,11 +237,11 @@ async fn zero_ex_liquidity(web3: Web3) { let zeroex_order_amounts = get_zeroex_order_amounts(&zeroex, &zeroex_order) .await .unwrap(); - assert_eq!( - zeroex_order_amounts.filled, - amount.as_u128() * 2 - expected_filled_amount + assert_approximately_eq!( + U256::from(zeroex_order_amounts.filled), + U256::from(amount.as_u128() * 2 - expected_filled_amount) ); - assert_eq!(zeroex_order_amounts.fillable, 0u128); + assert_approximately_eq!(U256::from(zeroex_order_amounts.fillable), U256::zero()); } fn create_zeroex_liquidity_orders( From 85cdee74f6bb56e96ee4e27802f473be6e4d5eb8 Mon Sep 17 00:00:00 2001 From: Martin Beckmann Date: Thu, 6 Jun 2024 09:51:05 +0200 Subject: [PATCH 5/7] Fix CI after foundry update (#2771) # Description https://github.com/foundry-rs/foundry/issues/8035 changed `anvil` to compute the gas price differently. This seems like a breaking change since under some conditions the new logic computes a different gas price than the original one. This broke 2 of our tests which both involve waiting for the system to flush out cached liquidity data to generate new quotes. For `e2e::e2e quoting::local_node_test` it was enough to just bump the `--base-fee` to `1` (without breaking any other test) and for `e2e::e2e protocol_fee::local_node_combined_protocol_fees` I needed to make the condition to detect flushed liquidity stricter. Instead of waiting for any change in quotes (which are now triggered too early because of tiny changes in the gas price) we now wait for the quote to be at least twice as good as the old quote. # Changes - bump `--base-fee` to `1` in `anvil` args - stricter synchronization logic for `e2e::e2e protocol_fee::local_node_combined_protocol_fees` - use nightly version of foundry again in CI --- .github/workflows/pull-request.yaml | 2 -- crates/e2e/src/nodes/mod.rs | 2 +- crates/e2e/src/setup/mod.rs | 4 ++-- crates/e2e/tests/e2e/protocol_fee.rs | 5 ++++- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pull-request.yaml b/.github/workflows/pull-request.yaml index c7596338d1..4c9f763f27 100644 --- a/.github/workflows/pull-request.yaml +++ b/.github/workflows/pull-request.yaml @@ -108,8 +108,6 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - run: rustup toolchain install stable --profile minimal - uses: foundry-rs/foundry-toolchain@v1 - with: - version: nightly-f479e945c6be78bb902df12f9d683c3bb55e3fb0 - uses: Swatinem/rust-cache@v2 # Start the build process in the background. The following cargo test command will automatically # wait for the build process to be done before proceeding. diff --git a/crates/e2e/src/nodes/mod.rs b/crates/e2e/src/nodes/mod.rs index ac7aaf2baf..6e1646c849 100644 --- a/crates/e2e/src/nodes/mod.rs +++ b/crates/e2e/src/nodes/mod.rs @@ -36,7 +36,7 @@ impl Node { "--gas-limit", "10000000", "--base-fee", - "0", + "1", "--balance", "1000000", "--chain-id", diff --git a/crates/e2e/src/setup/mod.rs b/crates/e2e/src/setup/mod.rs index bb1150e2dc..7abff0627b 100644 --- a/crates/e2e/src/setup/mod.rs +++ b/crates/e2e/src/setup/mod.rs @@ -58,7 +58,7 @@ where static NODE_MUTEX: Mutex<()> = Mutex::new(()); -const DEFAULT_FILTERS: [&str; 9] = [ +const DEFAULT_FILTERS: &[&str] = &[ "warn", "autopilot=debug", "driver=debug", @@ -74,7 +74,7 @@ fn with_default_filters(custom_filters: impl IntoIterator) -> Vec, { - let mut default_filters: Vec<_> = DEFAULT_FILTERS.into_iter().map(String::from).collect(); + let mut default_filters: Vec<_> = DEFAULT_FILTERS.iter().map(|s| s.to_string()).collect(); default_filters.extend(custom_filters.into_iter().map(|f| f.as_ref().to_owned())); default_filters diff --git a/crates/e2e/tests/e2e/protocol_fee.rs b/crates/e2e/tests/e2e/protocol_fee.rs index 0eeb1eb8d8..d668bb343a 100644 --- a/crates/e2e/tests/e2e/protocol_fee.rs +++ b/crates/e2e/tests/e2e/protocol_fee.rs @@ -231,7 +231,10 @@ async fn combined_protocol_fees(web3: Web3) { ) .await .unwrap(); - new_market_order_quote.quote.buy_amount != market_quote_before.quote.buy_amount + // Only proceed with test once the quote changes significantly (2x) to avoid + // progressing due to tiny fluctuations in gas price which would lead to + // errors down the line. + new_market_order_quote.quote.buy_amount > market_quote_before.quote.buy_amount * 2 }) .await .expect("Timeout waiting for eviction of the cached liquidity"); From 93519e1198c5fc75bce466f2a44925be002d0fc9 Mon Sep 17 00:00:00 2001 From: Dean Eigenmann <7621705+decanus@users.noreply.github.com> Date: Thu, 6 Jun 2024 10:43:50 +0200 Subject: [PATCH 6/7] auctionId -> solutionId as it is in the code (#2743) updates the api to match the code --------- Co-authored-by: Martin Beckmann --- crates/driver/openapi.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/driver/openapi.yml b/crates/driver/openapi.yml index d939b80172..79596b260f 100644 --- a/crates/driver/openapi.yml +++ b/crates/driver/openapi.yml @@ -390,9 +390,10 @@ components: description: Request to the settle and reveal endpoint. type: object properties: - auctionId: - description: Id of the auction that should be executed. - type: integer + solutionId: + description: Id of the solution that should be executed. + type: string + example: "123" RevealedResponse: description: Response of the reveal endpoint. type: object From baf9a1a3432d56bff9dd35ca1dc3c220fa3bc261 Mon Sep 17 00:00:00 2001 From: Martin Beckmann Date: Thu, 6 Jun 2024 16:14:28 +0200 Subject: [PATCH 7/7] [TRIVIAL] Simplify estimator logs (#2772) # Changes When quote verification was fresh it made sense to have special logging for verified estimators to debug things. But now all estimators are always verified and the `_verified` postfix makes it so that you don't get to see all the logs in kibana when you just search for `` (`_verified` logs would not show up). --- crates/shared/src/price_estimation/factory.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/shared/src/price_estimation/factory.rs b/crates/shared/src/price_estimation/factory.rs index 2c7b3f6957..e26afd58c1 100644 --- a/crates/shared/src/price_estimation/factory.rs +++ b/crates/shared/src/price_estimation/factory.rs @@ -156,7 +156,7 @@ impl<'a> PriceEstimatorFactory<'a> { let fast = instrument(estimator, name); let optimal = match verified { - Some(verified) => instrument(verified, format!("{name}_verified")), + Some(verified) => instrument(verified, name), None => fast.clone(), };