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..ae2af17ec4 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") }); @@ -684,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| { @@ -693,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| { 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; 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 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 75d0ee5780..0000000000 --- a/crates/driver/src/boundary/settlement.rs +++ /dev/null @@ -1,400 +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.into(), - kind: match order.side { - competition::order::Side::Buy => OrderKind::Buy, - competition::order::Side::Sell => OrderKind::Sell, - }, - partially_fillable: order.partially_fillable, - 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/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..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:?}")] @@ -144,7 +135,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..26d133d112 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, @@ -352,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 @@ -555,6 +595,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/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/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/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/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/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 = 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 @@ -205,7 +205,8 @@ async fn run( 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( 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"); 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(), }; 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