From 7aeb32b46bb3706982b1ec95913df4ddf0f5c9eb Mon Sep 17 00:00:00 2001 From: Nicholas Rodrigues Lordello Date: Wed, 18 Oct 2023 13:29:38 +0200 Subject: [PATCH] Simulate Trades in Solver (#1960) # Description Supersedes #1531 https://github.com/cowprotocol/services/pull/1469 introduced simulations to the single order solvers so that we can produce accurate fees when solving for partially fillable limit orders. This PR ports some of that logic to our solvers binary for the DEX aggregator solvers in the co-located world. In particular, it introduces a new `Swapper` helper contract for setting up simulations on-chain for DEX aggregator swaps. We also only do the simulation for limit orders (to avoid additional roudtrips for market orders, which don't use this gas information anyway, so the heuristic value is more than good enough). # Changes - [x] Introduce new simulation helper contract. - [x] Execute simulations for limit orders and use the simulated gas amount for computing accurate solver fees. - [x] : Mock node request/responses for executing the simulation in the `partial_fill` tests. ## How to test Adjusted some existing tests to work. ## Related Issues Fixes #1959 --------- Co-authored-by: Martin Beckmann Co-authored-by: Martin Beckmann --- .../src/on_settlement_event_updater.rs | 2 +- .../artifacts/AnyoneAuthenticator.json | 1 + crates/contracts/artifacts/Swapper.json | 1 + crates/contracts/build.rs | 4 + .../solidity/AnyoneAuthenticator.sol | 8 ++ crates/contracts/solidity/Makefile | 2 + crates/contracts/solidity/Swapper.sol | 117 ++++++++++++++++ crates/contracts/src/lib.rs | 2 + crates/solvers/config/example.balancer.toml | 3 +- crates/solvers/config/example.oneinch.toml | 4 +- crates/solvers/config/example.paraswap.toml | 4 +- crates/solvers/config/example.zeroex.toml | 3 +- crates/solvers/src/domain/dex/mod.rs | 20 ++- crates/solvers/src/domain/order.rs | 7 + crates/solvers/src/domain/solver/dex/mod.rs | 13 +- crates/solvers/src/infra/blockchain.rs | 13 ++ .../src/infra/config/dex/balancer/file.rs | 9 +- crates/solvers/src/infra/config/dex/file.rs | 34 ++++- crates/solvers/src/infra/config/dex/mod.rs | 7 + crates/solvers/src/infra/contracts.rs | 2 + crates/solvers/src/infra/dex/mod.rs | 3 + crates/solvers/src/infra/dex/simulator.rs | 128 ++++++++++++++++++ crates/solvers/src/infra/mod.rs | 1 + .../src/tests/balancer/market_order.rs | 8 +- crates/solvers/src/tests/balancer/mod.rs | 1 + .../solvers/src/tests/balancer/not_found.rs | 4 +- .../src/tests/balancer/out_of_price.rs | 8 +- crates/solvers/src/tests/dex/partial_fill.rs | 79 +++++++++-- .../solvers/src/tests/dex/wrong_execution.rs | 4 +- .../src/tests/legacy/attaching_approvals.rs | 4 +- .../tests/legacy/concentrated_liquidity.rs | 4 +- crates/solvers/src/tests/legacy/jit_order.rs | 4 +- .../solvers/src/tests/legacy/market_order.rs | 8 +- crates/solvers/src/tests/mock/http.rs | 21 ++- crates/solvers/src/tests/oneinch/mod.rs | 1 + .../src/tests/paraswap/market_order.rs | 8 +- crates/solvers/src/tests/paraswap/mod.rs | 1 + .../src/tests/paraswap/out_of_price.rs | 8 +- crates/solvers/src/tests/zeroex/mod.rs | 1 + crates/solvers/src/tests/zeroex/options.rs | 3 +- 40 files changed, 490 insertions(+), 65 deletions(-) create mode 100644 crates/contracts/artifacts/AnyoneAuthenticator.json create mode 100644 crates/contracts/artifacts/Swapper.json create mode 100644 crates/contracts/solidity/AnyoneAuthenticator.sol create mode 100644 crates/contracts/solidity/Swapper.sol create mode 100644 crates/solvers/src/infra/blockchain.rs create mode 100644 crates/solvers/src/infra/dex/simulator.rs diff --git a/crates/autopilot/src/on_settlement_event_updater.rs b/crates/autopilot/src/on_settlement_event_updater.rs index e6091d2ddb..6a96c8eb15 100644 --- a/crates/autopilot/src/on_settlement_event_updater.rs +++ b/crates/autopilot/src/on_settlement_event_updater.rs @@ -195,7 +195,7 @@ impl OnSettlementEventUpdater { .collect(), }); } - Err(err) if matches!(err, DecodingError::InvalidSelector) => { + Err(DecodingError::InvalidSelector) => { // we indexed a transaction initiated by solver, that was not a settlement // for this case we want to have the entry in observations table but with zeros update.auction_data = Some(Default::default()); diff --git a/crates/contracts/artifacts/AnyoneAuthenticator.json b/crates/contracts/artifacts/AnyoneAuthenticator.json new file mode 100644 index 0000000000..6924aeb37a --- /dev/null +++ b/crates/contracts/artifacts/AnyoneAuthenticator.json @@ -0,0 +1 @@ +{"abi":[{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"isSolver","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"pure","type":"function"}],"bytecode":"0x608060405234801561001057600080fd5b50609a8061001f6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c806302cc250d14602d575b600080fd5b603e60383660046052565b50600190565b604051901515815260200160405180910390f35b600060208284031215606357600080fd5b813573ffffffffffffffffffffffffffffffffffffffff81168114608657600080fd5b939250505056fea164736f6c6343000811000a","deployedBytecode":"0x6080604052348015600f57600080fd5b506004361060285760003560e01c806302cc250d14602d575b600080fd5b603e60383660046052565b50600190565b604051901515815260200160405180910390f35b600060208284031215606357600080fd5b813573ffffffffffffffffffffffffffffffffffffffff81168114608657600080fd5b939250505056fea164736f6c6343000811000a","devdoc":{"methods":{}},"userdoc":{"methods":{}}} diff --git a/crates/contracts/artifacts/Swapper.json b/crates/contracts/artifacts/Swapper.json new file mode 100644 index 0000000000..3b4420dd93 --- /dev/null +++ b/crates/contracts/artifacts/Swapper.json @@ -0,0 +1 @@ +{"abi":[{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"},{"internalType":"bytes","name":"","type":"bytes"}],"name":"isValidSignature","outputs":[{"internalType":"bytes4","name":"","type":"bytes4"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"contract ISettlement","name":"settlement","type":"address"},{"components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"internalType":"struct Asset","name":"sell","type":"tuple"},{"components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"internalType":"struct Asset","name":"buy","type":"tuple"},{"components":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"internalType":"struct Allowance","name":"allowance","type":"tuple"},{"components":[{"internalType":"address","name":"target","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"callData","type":"bytes"}],"internalType":"struct Interaction","name":"call","type":"tuple"}],"name":"swap","outputs":[{"internalType":"uint256","name":"gasUsed","type":"uint256"}],"stateMutability":"nonpayable","type":"function"}],"bytecode":"0x608060405234801561001057600080fd5b506111c8806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80631626ba7e1461003b5780639ed280dd146100a7575b600080fd5b610071610049366004610b03565b7f1626ba7e000000000000000000000000000000000000000000000000000000009392505050565b6040517fffffffff0000000000000000000000000000000000000000000000000000000090911681526020015b60405180910390f35b6100ba6100b5366004610bbc565b6100c8565b60405190815260200161009e565b6000602085018035906100db9087610c49565b6040517f70a0823100000000000000000000000000000000000000000000000000000000815230600482015273ffffffffffffffffffffffffffffffffffffffff91909116906370a0823190602401602060405180830381865afa158015610147573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061016b9190610c66565b1015610179575060006108b8565b6102178673ffffffffffffffffffffffffffffffffffffffff16639b552cc26040518163ffffffff1660e01b8152600401602060405180830381865afa1580156101c7573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101eb9190610c7f565b60006101fa6020890189610c49565b73ffffffffffffffffffffffffffffffffffffffff1691906108c1565b61029a8673ffffffffffffffffffffffffffffffffffffffff16639b552cc26040518163ffffffff1660e01b8152600401602060405180830381865afa158015610265573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906102899190610c7f565b602087018035906101fa9089610c49565b6040805160028082526060820183526000926020830190803683370190505090506102c86020870187610c49565b816000815181106102db576102db610ccb565b73ffffffffffffffffffffffffffffffffffffffff90921660209283029190910182015261030b90860186610c49565b8160018151811061031e5761031e610ccb565b73ffffffffffffffffffffffffffffffffffffffff92909216602092830291909101820152604080516002808252606082018352600093919290918301908036833701905050905085602001358160008151811061037e5761037e610ccb565b6020026020010181815250508660200135816001815181106103a2576103a2610ccb565b6020908102919091010152604080516001808252818301909252600091816020015b6104406040518061016001604052806000815260200160008152602001600073ffffffffffffffffffffffffffffffffffffffff1681526020016000815260200160008152602001600063ffffffff16815260200160008019168152602001600081526020016000815260200160008152602001606081525090565b8152602001906001900390816103c45790505090506040518061016001604052806000815260200160018152602001600073ffffffffffffffffffffffffffffffffffffffff168152602001896020013581526020018860200135815260200163ffffffff801681526020016000801b815260200160008152602001604081526020016000815260200130604051602001610506919060609190911b7fffffffffffffffffffffffffffffffffffffffff00000000000000000000000016815260140190565b6040516020818303038152906040528152508160008151811061052b5761052b610ccb565b602002602001018190525061053e610adc565b60208088013590610551908b018b610c49565b73ffffffffffffffffffffffffffffffffffffffff1663dd62ed3e8c61057a60208c018c610c49565b6040517fffffffff0000000000000000000000000000000000000000000000000000000060e085901b16815273ffffffffffffffffffffffffffffffffffffffff928316600482015291166024820152604401602060405180830381865afa1580156105ea573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061060e9190610c66565b101561078c5760408051600180825281830190925290816020015b60408051606080820183526000808352602083015291810191909152815260200190600190039081610629575050815261066660208a018a610c49565b8151805160009061067957610679610ccb565b60209081029190910181015173ffffffffffffffffffffffffffffffffffffffff9092169091526106ac908a018a610c49565b73ffffffffffffffffffffffffffffffffffffffff1663095ea7b36106d460208a018a610c49565b60405173ffffffffffffffffffffffffffffffffffffffff909116602482015260208a01356044820152606401604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08184030181529190526020810180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1660e09390931b929092179091529050816000602002015160008151811061077c5761077c610ccb565b6020026020010151604001819052505b60408051600180825281830190925290816020015b604080516060808201835260008083526020830152918101919091528152602001906001900390816107a157505060208201526107dd86610d72565b602082015180516000906107f3576107f3610ccb565b60200260200101819052506108b18a73ffffffffffffffffffffffffffffffffffffffff166313d79a0b868686866040516024016108349493929190611071565b604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08184030181529190526020810180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1660e09390931b9290921790915273ffffffffffffffffffffffffffffffffffffffff8d16915061099c565b9450505050505b95945050505050565b6040805173ffffffffffffffffffffffffffffffffffffffff848116602483015260448083018590528351808403909101815260649092019092526020810180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167f095ea7b30000000000000000000000000000000000000000000000000000000017905290600090610954908616836109b1565b9050610995816040518060400160405280601a81526020017f5361666545524332303a20617070726f76616c206661696c65640000000000008152506109bf565b5050505050565b60006109aa83600084610a23565b9392505050565b60606109aa83600084610a56565b815115806109dc5750818060200190518101906109dc919061112a565b8190610a1e576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610a15919061114c565b60405180910390fd5b505050565b60005a905060008083516020850186885af1610a43573d6000803e3d6000fd5b5a610a4e908261115f565b949350505050565b606060008473ffffffffffffffffffffffffffffffffffffffff168484604051610a80919061119f565b60006040518083038185875af1925050503d8060008114610abd576040519150601f19603f3d011682016040523d82523d6000602084013e610ac2565b606091505b509250905080610ad457815160208301fd5b509392505050565b60405180606001604052806003905b6060815260200190600190039081610aeb5790505090565b600080600060408486031215610b1857600080fd5b83359250602084013567ffffffffffffffff80821115610b3757600080fd5b818601915086601f830112610b4b57600080fd5b813581811115610b5a57600080fd5b876020828501011115610b6c57600080fd5b6020830194508093505050509250925092565b73ffffffffffffffffffffffffffffffffffffffff81168114610ba157600080fd5b50565b600060408284031215610bb657600080fd5b50919050565b60008060008060006101008688031215610bd557600080fd5b8535610be081610b7f565b9450610bef8760208801610ba4565b9350610bfe8760608801610ba4565b9250610c0d8760a08801610ba4565b915060e086013567ffffffffffffffff811115610c2957600080fd5b860160608189031215610c3b57600080fd5b809150509295509295909350565b600060208284031215610c5b57600080fd5b81356109aa81610b7f565b600060208284031215610c7857600080fd5b5051919050565b600060208284031215610c9157600080fd5b81516109aa81610b7f565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b6040516060810167ffffffffffffffff81118282101715610d1d57610d1d610c9c565b60405290565b604051601f82017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016810167ffffffffffffffff81118282101715610d6a57610d6a610c9c565b604052919050565b600060608236031215610d8457600080fd5b610d8c610cfa565b8235610d9781610b7f565b815260208381013581830152604084013567ffffffffffffffff80821115610dbe57600080fd5b9085019036601f830112610dd157600080fd5b813581811115610de357610de3610c9c565b610e13847fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f84011601610d23565b91508082523684828501011115610e2957600080fd5b808484018584013760009082019093019290925250604082015292915050565b60005b83811015610e64578181015183820152602001610e4c565b50506000910152565b60008151808452610e85816020860160208601610e49565b601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0169290920160200192915050565b600081518084526020808501808196508360051b8101915082860160005b85811015610f975782840389528151610160815186528682015187870152604080830151610f1a8289018273ffffffffffffffffffffffffffffffffffffffff169052565b5050606082810151908701526080808301519087015260a08083015163ffffffff169087015260c0808301519087015260e080830151908701526101008083015190870152610120808301519087015261014091820151918601819052610f8381870183610e6d565b9a87019a9550505090840190600101610ed5565b5091979650505050505050565b6000826060808201846000805b6003811015610f97578584038952825180518086526020918201918087019190600582901b88018101865b8381101561105a578982037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe00185528551805173ffffffffffffffffffffffffffffffffffffffff16835283810151848401526040908101519083018c90526110478c840182610e6d565b9684019695840195925050600101610fdc565b509c81019c97509590950194505050600101610fb1565b6080808252855190820181905260009060209060a0840190828901845b828110156110c057815173ffffffffffffffffffffffffffffffffffffffff168452928401929084019060010161108e565b5050508381038285015286518082528783019183019060005b818110156110f5578351835292840192918401916001016110d9565b505084810360408601526111098188610eb7565b92505050828103606084015261111f8185610fa4565b979650505050505050565b60006020828403121561113c57600080fd5b815180151581146109aa57600080fd5b6020815260006109aa6020830184610e6d565b81810381811115611199577f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b92915050565b600082516111b1818460208701610e49565b919091019291505056fea164736f6c6343000811000a","deployedBytecode":"0x608060405234801561001057600080fd5b50600436106100365760003560e01c80631626ba7e1461003b5780639ed280dd146100a7575b600080fd5b610071610049366004610b03565b7f1626ba7e000000000000000000000000000000000000000000000000000000009392505050565b6040517fffffffff0000000000000000000000000000000000000000000000000000000090911681526020015b60405180910390f35b6100ba6100b5366004610bbc565b6100c8565b60405190815260200161009e565b6000602085018035906100db9087610c49565b6040517f70a0823100000000000000000000000000000000000000000000000000000000815230600482015273ffffffffffffffffffffffffffffffffffffffff91909116906370a0823190602401602060405180830381865afa158015610147573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061016b9190610c66565b1015610179575060006108b8565b6102178673ffffffffffffffffffffffffffffffffffffffff16639b552cc26040518163ffffffff1660e01b8152600401602060405180830381865afa1580156101c7573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101eb9190610c7f565b60006101fa6020890189610c49565b73ffffffffffffffffffffffffffffffffffffffff1691906108c1565b61029a8673ffffffffffffffffffffffffffffffffffffffff16639b552cc26040518163ffffffff1660e01b8152600401602060405180830381865afa158015610265573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906102899190610c7f565b602087018035906101fa9089610c49565b6040805160028082526060820183526000926020830190803683370190505090506102c86020870187610c49565b816000815181106102db576102db610ccb565b73ffffffffffffffffffffffffffffffffffffffff90921660209283029190910182015261030b90860186610c49565b8160018151811061031e5761031e610ccb565b73ffffffffffffffffffffffffffffffffffffffff92909216602092830291909101820152604080516002808252606082018352600093919290918301908036833701905050905085602001358160008151811061037e5761037e610ccb565b6020026020010181815250508660200135816001815181106103a2576103a2610ccb565b6020908102919091010152604080516001808252818301909252600091816020015b6104406040518061016001604052806000815260200160008152602001600073ffffffffffffffffffffffffffffffffffffffff1681526020016000815260200160008152602001600063ffffffff16815260200160008019168152602001600081526020016000815260200160008152602001606081525090565b8152602001906001900390816103c45790505090506040518061016001604052806000815260200160018152602001600073ffffffffffffffffffffffffffffffffffffffff168152602001896020013581526020018860200135815260200163ffffffff801681526020016000801b815260200160008152602001604081526020016000815260200130604051602001610506919060609190911b7fffffffffffffffffffffffffffffffffffffffff00000000000000000000000016815260140190565b6040516020818303038152906040528152508160008151811061052b5761052b610ccb565b602002602001018190525061053e610adc565b60208088013590610551908b018b610c49565b73ffffffffffffffffffffffffffffffffffffffff1663dd62ed3e8c61057a60208c018c610c49565b6040517fffffffff0000000000000000000000000000000000000000000000000000000060e085901b16815273ffffffffffffffffffffffffffffffffffffffff928316600482015291166024820152604401602060405180830381865afa1580156105ea573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061060e9190610c66565b101561078c5760408051600180825281830190925290816020015b60408051606080820183526000808352602083015291810191909152815260200190600190039081610629575050815261066660208a018a610c49565b8151805160009061067957610679610ccb565b60209081029190910181015173ffffffffffffffffffffffffffffffffffffffff9092169091526106ac908a018a610c49565b73ffffffffffffffffffffffffffffffffffffffff1663095ea7b36106d460208a018a610c49565b60405173ffffffffffffffffffffffffffffffffffffffff909116602482015260208a01356044820152606401604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08184030181529190526020810180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1660e09390931b929092179091529050816000602002015160008151811061077c5761077c610ccb565b6020026020010151604001819052505b60408051600180825281830190925290816020015b604080516060808201835260008083526020830152918101919091528152602001906001900390816107a157505060208201526107dd86610d72565b602082015180516000906107f3576107f3610ccb565b60200260200101819052506108b18a73ffffffffffffffffffffffffffffffffffffffff166313d79a0b868686866040516024016108349493929190611071565b604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08184030181529190526020810180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1660e09390931b9290921790915273ffffffffffffffffffffffffffffffffffffffff8d16915061099c565b9450505050505b95945050505050565b6040805173ffffffffffffffffffffffffffffffffffffffff848116602483015260448083018590528351808403909101815260649092019092526020810180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167f095ea7b30000000000000000000000000000000000000000000000000000000017905290600090610954908616836109b1565b9050610995816040518060400160405280601a81526020017f5361666545524332303a20617070726f76616c206661696c65640000000000008152506109bf565b5050505050565b60006109aa83600084610a23565b9392505050565b60606109aa83600084610a56565b815115806109dc5750818060200190518101906109dc919061112a565b8190610a1e576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610a15919061114c565b60405180910390fd5b505050565b60005a905060008083516020850186885af1610a43573d6000803e3d6000fd5b5a610a4e908261115f565b949350505050565b606060008473ffffffffffffffffffffffffffffffffffffffff168484604051610a80919061119f565b60006040518083038185875af1925050503d8060008114610abd576040519150601f19603f3d011682016040523d82523d6000602084013e610ac2565b606091505b509250905080610ad457815160208301fd5b509392505050565b60405180606001604052806003905b6060815260200190600190039081610aeb5790505090565b600080600060408486031215610b1857600080fd5b83359250602084013567ffffffffffffffff80821115610b3757600080fd5b818601915086601f830112610b4b57600080fd5b813581811115610b5a57600080fd5b876020828501011115610b6c57600080fd5b6020830194508093505050509250925092565b73ffffffffffffffffffffffffffffffffffffffff81168114610ba157600080fd5b50565b600060408284031215610bb657600080fd5b50919050565b60008060008060006101008688031215610bd557600080fd5b8535610be081610b7f565b9450610bef8760208801610ba4565b9350610bfe8760608801610ba4565b9250610c0d8760a08801610ba4565b915060e086013567ffffffffffffffff811115610c2957600080fd5b860160608189031215610c3b57600080fd5b809150509295509295909350565b600060208284031215610c5b57600080fd5b81356109aa81610b7f565b600060208284031215610c7857600080fd5b5051919050565b600060208284031215610c9157600080fd5b81516109aa81610b7f565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b6040516060810167ffffffffffffffff81118282101715610d1d57610d1d610c9c565b60405290565b604051601f82017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016810167ffffffffffffffff81118282101715610d6a57610d6a610c9c565b604052919050565b600060608236031215610d8457600080fd5b610d8c610cfa565b8235610d9781610b7f565b815260208381013581830152604084013567ffffffffffffffff80821115610dbe57600080fd5b9085019036601f830112610dd157600080fd5b813581811115610de357610de3610c9c565b610e13847fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f84011601610d23565b91508082523684828501011115610e2957600080fd5b808484018584013760009082019093019290925250604082015292915050565b60005b83811015610e64578181015183820152602001610e4c565b50506000910152565b60008151808452610e85816020860160208601610e49565b601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0169290920160200192915050565b600081518084526020808501808196508360051b8101915082860160005b85811015610f975782840389528151610160815186528682015187870152604080830151610f1a8289018273ffffffffffffffffffffffffffffffffffffffff169052565b5050606082810151908701526080808301519087015260a08083015163ffffffff169087015260c0808301519087015260e080830151908701526101008083015190870152610120808301519087015261014091820151918601819052610f8381870183610e6d565b9a87019a9550505090840190600101610ed5565b5091979650505050505050565b6000826060808201846000805b6003811015610f97578584038952825180518086526020918201918087019190600582901b88018101865b8381101561105a578982037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe00185528551805173ffffffffffffffffffffffffffffffffffffffff16835283810151848401526040908101519083018c90526110478c840182610e6d565b9684019695840195925050600101610fdc565b509c81019c97509590950194505050600101610fb1565b6080808252855190820181905260009060209060a0840190828901845b828110156110c057815173ffffffffffffffffffffffffffffffffffffffff168452928401929084019060010161108e565b5050508381038285015286518082528783019183019060005b818110156110f5578351835292840192918401916001016110d9565b505084810360408601526111098188610eb7565b92505050828103606084015261111f8185610fa4565b979650505050505050565b60006020828403121561113c57600080fd5b815180151581146109aa57600080fd5b6020815260006109aa6020830184610e6d565b81810381811115611199577f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b92915050565b600082516111b1818460208701610e49565b919091019291505056fea164736f6c6343000811000a","devdoc":{"methods":{}},"userdoc":{"methods":{}}} diff --git a/crates/contracts/build.rs b/crates/contracts/build.rs index c0af4b113b..a430246f5f 100644 --- a/crates/contracts/build.rs +++ b/crates/contracts/build.rs @@ -529,6 +529,10 @@ fn main() { generate_contract("Signatures"); generate_contract("SimulateCode"); + // Support contract used for solver fee simulations. + generate_contract("AnyoneAuthenticator"); + generate_contract("Swapper"); + // Support contract used for global block stream. generate_contract("FetchBlock"); diff --git a/crates/contracts/solidity/AnyoneAuthenticator.sol b/crates/contracts/solidity/AnyoneAuthenticator.sol new file mode 100644 index 0000000000..7602ffb1a9 --- /dev/null +++ b/crates/contracts/solidity/AnyoneAuthenticator.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +contract AnyoneAuthenticator { + function isSolver(address) external pure returns (bool) { + return true; + } +} diff --git a/crates/contracts/solidity/Makefile b/crates/contracts/solidity/Makefile index 19ee35a44e..e7845b64de 100644 --- a/crates/contracts/solidity/Makefile +++ b/crates/contracts/solidity/Makefile @@ -8,12 +8,14 @@ TARGETDIR := ../../../target/solidity ARTIFACTDIR := ../artifacts CONTRACTS := \ + AnyoneAuthenticator.sol \ Balances.sol \ FetchBlock.sol \ Multicall.sol \ Signatures.sol \ SimulateCode.sol \ Solver.sol \ + Swapper.sol \ Trader.sol ARTIFACTS := $(patsubst %.sol,$(ARTIFACTDIR)/%.json,$(CONTRACTS)) diff --git a/crates/contracts/solidity/Swapper.sol b/crates/contracts/solidity/Swapper.sol new file mode 100644 index 0000000000..c9f224401c --- /dev/null +++ b/crates/contracts/solidity/Swapper.sol @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import { IERC20 } from "./interfaces/IERC20.sol"; +import { ISettlement, Interaction, Trade } from "./interfaces/ISettlement.sol"; +import { Caller } from "./libraries/Caller.sol"; +import { SafeERC20 } from "./libraries/SafeERC20.sol"; + +struct Asset { + address token; + uint256 amount; +} + +struct Allowance { + address spender; + uint256 amount; +} + +/// @title A contract for verifying DEX aggregator swaps for solving. +contract Swapper { + using Caller for *; + using SafeERC20 for *; + + /// @dev Simulates the execution of a single DEX swap over the CoW Protocol + /// settlement contract. This is used for accurately simulating gas costs + /// for orders with solver-computed fees. + /// + /// @param settlement - the CoW Protocol settlement contract. + /// @param sell - the asset being sold in the swap. + /// @param buy - the asset being bought in the swap. + /// @param allowance - the required ERC-20 allowance for the swap; the + /// approval will be me made on behalf of the settlement contract. + /// @param call - the call for executing the swap. + /// + /// @return gasUsed - the cumulative gas used for executing the simulated + /// settlement. + function swap( + ISettlement settlement, + Asset calldata sell, + Asset calldata buy, + Allowance calldata allowance, + Interaction calldata call + ) external returns ( + uint256 gasUsed + ) { + if (IERC20(sell.token).balanceOf(address(this)) < sell.amount) { + // The swapper does not have sufficient balance. This can happen + // when hooks set up required balance for a trade. This is currently + // not supported by this simulation, so return "0" to indicate that + // no simulation was possible and that heuristic gas estimates + // should be used instead. + return 0; + } + + // We first reset the allowance to 0 because some ERC20 tokens (e.g. USDT) + // require that due to this attack: + // https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + // Before approving the amount we actually need. + IERC20(sell.token).safeApprove(address(settlement.vaultRelayer()), 0); + IERC20(sell.token).safeApprove(address(settlement.vaultRelayer()), sell.amount); + + address[] memory tokens = new address[](2); + tokens[0] = sell.token; + tokens[1] = buy.token; + + uint256[] memory clearingPrices = new uint256[](2); + clearingPrices[0] = buy.amount; + clearingPrices[1] = sell.amount; + + Trade[] memory trades = new Trade[](1); + trades[0] = Trade({ + sellTokenIndex: 0, + buyTokenIndex: 1, + receiver: address(0), + sellAmount: sell.amount, + buyAmount: buy.amount, + validTo: type(uint32).max, + appData: bytes32(0), + feeAmount: 0, + flags: 0x40, // EIP-1271 + // Actual amount is irrelevant because we configure a fill-or-kill + // order for which the settlement contract determines the + // `executedAmount` automatically. + executedAmount: 0, + signature: abi.encodePacked(address(this)) + }); + + Interaction[][3] memory interactions; + if ( + IERC20(sell.token).allowance(address(settlement), allowance.spender) + < allowance.amount + ) { + interactions[0] = new Interaction[](1); + interactions[0][0].target = sell.token; + interactions[0][0].callData = abi.encodeCall( + IERC20(sell.token).approve, + (allowance.spender, allowance.amount) + ); + } + interactions[1] = new Interaction[](1); + interactions[1][0] = call; + + gasUsed = address(settlement).doMeteredCallNoReturn( + abi.encodeCall( + settlement.settle, + (tokens, clearingPrices, trades, interactions) + ) + ); + } + + /// @dev Validate all signature requests. This makes "signing" CoW protocol + /// orders trivial. + function isValidSignature(bytes32, bytes calldata) external pure returns (bytes4) { + return 0x1626ba7e; + } +} + diff --git a/crates/contracts/src/lib.rs b/crates/contracts/src/lib.rs index c8d664d1fe..e4a7caec0e 100644 --- a/crates/contracts/src/lib.rs +++ b/crates/contracts/src/lib.rs @@ -70,12 +70,14 @@ include_contracts! { pub mod support { include_contracts! { + AnyoneAuthenticator; Balances; FetchBlock; Multicall; Signatures; SimulateCode; Solver; + Swapper; Trader; } } diff --git a/crates/solvers/config/example.balancer.toml b/crates/solvers/config/example.balancer.toml index 0d66a50dfb..f2f9f80742 100644 --- a/crates/solvers/config/example.balancer.toml +++ b/crates/solvers/config/example.balancer.toml @@ -1,5 +1,6 @@ +node-url = "http://localhost:8545" absolute-slippage = "40000000000000000" # Denominated in wei, optional -relative-slippage = "0.1" # Percentage in the [0, 1] range +relative-slippage = "0.001" # Percentage in the [0, 1] range risk-parameters = [0,0,0,0] [dex] diff --git a/crates/solvers/config/example.oneinch.toml b/crates/solvers/config/example.oneinch.toml index 41bda0395d..74dce21a9e 100644 --- a/crates/solvers/config/example.oneinch.toml +++ b/crates/solvers/config/example.oneinch.toml @@ -1,5 +1,7 @@ +node-url = "http://localhost:8545" absolute-slippage = "40000000000000000" # Denominated in wei, optional -relative-slippage = "0.1" # Percentage in the [0, 1] range +relative-slippage = "0.001" # Percentage in the [0, 1] range risk-parameters = [0,0,0,0] + [dex] chain-id = "1" diff --git a/crates/solvers/config/example.paraswap.toml b/crates/solvers/config/example.paraswap.toml index 43d66c134a..4e8c8b116d 100644 --- a/crates/solvers/config/example.paraswap.toml +++ b/crates/solvers/config/example.paraswap.toml @@ -1,6 +1,8 @@ +node-url = "http://localhost:8545" absolute-slippage = "40000000000000000" # Denominated in wei, optional -relative-slippage = "0.1" # Percentage in the [0, 1] range +relative-slippage = "0.001" # Percentage in the [0, 1] range risk-parameters = [0,0,0,0] + [dex] exclude-dexs = [] # which dexs to ignore as liquidity sources address = "0xdd2e786980CD58ACc5F64807b354c981f4094936" # public address of the solver diff --git a/crates/solvers/config/example.zeroex.toml b/crates/solvers/config/example.zeroex.toml index 8850f71b5e..6884a602ed 100644 --- a/crates/solvers/config/example.zeroex.toml +++ b/crates/solvers/config/example.zeroex.toml @@ -1,5 +1,6 @@ +node-url = "http://localhost:8545" absolute-slippage = "40000000000000000" # Denominated in wei, optional -relative-slippage = "0.1" # Percentage in the [0, 1] range +relative-slippage = "0.001" # Percentage in the [0, 1] range risk-parameters = [0,0,0,0] [dex] diff --git a/crates/solvers/src/domain/dex/mod.rs b/crates/solvers/src/domain/dex/mod.rs index de53869c2d..80ce2573a2 100644 --- a/crates/solvers/src/domain/dex/mod.rs +++ b/crates/solvers/src/domain/dex/mod.rs @@ -5,6 +5,7 @@ use { crate::{ domain::{auction, eth, order, solution}, + infra, util, }, ethereum_types::U256, @@ -101,13 +102,28 @@ impl Swap { /// Constructs a single order `solution::Solution` for this swap. Returns /// `None` if the swap is not valid for the specified order. - pub fn into_solution( + pub async fn into_solution( self, order: order::Order, gas_price: auction::GasPrice, sell_token: Option, score: solution::Score, + simulator: &infra::dex::Simulator, ) -> Option { + let gas = if order.class == order::Class::Limit { + match simulator.gas(order.owner(), &self).await { + Ok(value) => value, + Err(err) => { + tracing::warn!(?err, "gas simulation failed"); + return None; + } + } + } else { + // We are fine with just using heuristic gas for market orders, + // since it doesn't really play a role in the final solution. + self.gas + }; + let allowance = self.allowance(); let interactions = vec![solution::Interaction::Custom(solution::CustomInteraction { target: self.call.to.0, @@ -124,7 +140,7 @@ impl Swap { input: self.input, output: self.output, interactions, - gas: self.gas, + gas, } .into_solution(gas_price, sell_token, score) } diff --git a/crates/solvers/src/domain/order.rs b/crates/solvers/src/domain/order.rs index 17044f1fbd..2a686e3b98 100644 --- a/crates/solvers/src/domain/order.rs +++ b/crates/solvers/src/domain/order.rs @@ -19,6 +19,13 @@ pub struct Order { } impl Order { + /// Returns the order's owner address. + pub fn owner(&self) -> Address { + let mut bytes = [0_u8; 20]; + bytes.copy_from_slice(&self.uid.0[32..52]); + bytes.into() + } + /// Returns the order's fee amount as an asset. pub fn fee(&self) -> eth::Asset { eth::Asset { diff --git a/crates/solvers/src/domain/solver/dex/mod.rs b/crates/solvers/src/domain/solver/dex/mod.rs index eb3b455633..c2321fc97d 100644 --- a/crates/solvers/src/domain/solver/dex/mod.rs +++ b/crates/solvers/src/domain/solver/dex/mod.rs @@ -18,6 +18,9 @@ pub struct Dex { /// The DEX API client. dex: infra::dex::Dex, + /// A DEX swap gas simulator for computing limit order fees. + simulator: infra::dex::Simulator, + /// The slippage configuration to use for the solver. slippage: slippage::Limits, @@ -36,6 +39,11 @@ impl Dex { pub fn new(dex: infra::dex::Dex, config: infra::config::dex::Config) -> Self { Self { dex, + simulator: infra::dex::Simulator::new( + &config.node_url, + config.contracts.settlement, + config.contracts.authenticator, + ), slippage: config.slippage, concurrent_requests: config.concurrent_requests, fills: Fills::new(config.smallest_partial_fill), @@ -115,7 +123,10 @@ impl Dex { let sell = tokens.reference_price(&order.sell.token); let score = solution::Score::RiskAdjusted(self.risk.success_probability(swap.gas, gas_price, 1)); - let Some(solution) = swap.into_solution(order.clone(), gas_price, sell, score) else { + let Some(solution) = swap + .into_solution(order.clone(), gas_price, sell, score, &self.simulator) + .await + else { tracing::debug!("no solution for swap"); return None; }; diff --git a/crates/solvers/src/infra/blockchain.rs b/crates/solvers/src/infra/blockchain.rs new file mode 100644 index 0000000000..9bed81083b --- /dev/null +++ b/crates/solvers/src/infra/blockchain.rs @@ -0,0 +1,13 @@ +use std::time::Duration; + +/// Creates a node RPC instance. +pub fn rpc(url: &reqwest::Url) -> ethrpc::Web3 { + ethrpc::web3( + Default::default(), + reqwest::ClientBuilder::new() + .timeout(Duration::from_secs(10)) + .user_agent("cowprotocol-solver-engine/1.0.0"), + url, + "base", + ) +} diff --git a/crates/solvers/src/infra/config/dex/balancer/file.rs b/crates/solvers/src/infra/config/dex/balancer/file.rs index 48d6a26bcb..49a9f23b8a 100644 --- a/crates/solvers/src/infra/config/dex/balancer/file.rs +++ b/crates/solvers/src/infra/config/dex/balancer/file.rs @@ -20,10 +20,6 @@ struct Config { /// Optional Balancer V2 Vault contract address. If not specified, the /// default Vault contract address will be used. vault: Option, - - /// Optional CoW Protocol Settlement contract address. If not specified, - /// the default Settlement contract address will be used. - settlement: Option, } /// Load the driver configuration from a TOML file. @@ -44,10 +40,7 @@ pub async fn load(path: &Path) -> super::Config { .vault .map(eth::ContractAddress) .unwrap_or(contracts.balancer_vault), - settlement: config - .settlement - .map(eth::ContractAddress) - .unwrap_or(contracts.settlement), + settlement: base.contracts.settlement, }, base, } diff --git a/crates/solvers/src/infra/config/dex/file.rs b/crates/solvers/src/infra/config/dex/file.rs index e8f1b688a6..6b101e96b8 100644 --- a/crates/solvers/src/infra/config/dex/file.rs +++ b/crates/solvers/src/infra/config/dex/file.rs @@ -3,7 +3,7 @@ use { crate::{ domain::{dex::slippage, eth, Risk}, - infra::config::unwrap_or_log, + infra::{blockchain, config::unwrap_or_log, contracts}, util::serialize, }, bigdecimal::BigDecimal, @@ -17,6 +17,14 @@ use { #[derive(Debug, Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] struct Config { + /// The node URL to use for simulations. + #[serde_as(as = "serde_with::DisplayFromStr")] + node_url: reqwest::Url, + + /// Optional CoW Protocol Settlement contract address. If not specified, + /// the default Settlement contract address will be used. + settlement: Option, + /// The relative slippage allowed by the solver. #[serde(default = "default_relative_slippage")] #[serde_as(as = "serde_with::DisplayFromStr")] @@ -71,7 +79,31 @@ pub async fn load(path: &Path) -> (super::Config, T) { let dex: T = unwrap_or_log(config.dex.try_into(), &path); + // Take advantage of the fact that deterministic deployment means that all + // CoW Protocol contracts have the same address. + let contracts = contracts::Contracts::for_chain(eth::ChainId::Mainnet); + let (settlement, authenticator) = if let Some(settlement) = config.settlement { + let authenticator = eth::ContractAddress({ + let web3 = blockchain::rpc(&config.node_url); + let settlement = ::contracts::GPv2Settlement::at(&web3, settlement); + settlement + .methods() + .authenticator() + .call() + .await + .unwrap_or_else(|e| panic!("error reading authenticator contract address: {e:?}")) + }); + (eth::ContractAddress(settlement), authenticator) + } else { + (contracts.settlement, contracts.authenticator) + }; + let config = super::Config { + node_url: config.node_url, + contracts: super::Contracts { + settlement, + authenticator, + }, slippage: slippage::Limits::new( config.relative_slippage, config.absolute_slippage.map(eth::Ether), diff --git a/crates/solvers/src/infra/config/dex/mod.rs b/crates/solvers/src/infra/config/dex/mod.rs index ae462c20c3..ac8cb2354d 100644 --- a/crates/solvers/src/infra/config/dex/mod.rs +++ b/crates/solvers/src/infra/config/dex/mod.rs @@ -9,7 +9,14 @@ use { std::num::NonZeroUsize, }; +pub struct Contracts { + pub settlement: eth::ContractAddress, + pub authenticator: eth::ContractAddress, +} + pub struct Config { + pub node_url: reqwest::Url, + pub contracts: Contracts, pub slippage: slippage::Limits, pub concurrent_requests: NonZeroUsize, pub smallest_partial_fill: eth::Ether, diff --git a/crates/solvers/src/infra/contracts.rs b/crates/solvers/src/infra/contracts.rs index 2633bd9da2..c2266143b9 100644 --- a/crates/solvers/src/infra/contracts.rs +++ b/crates/solvers/src/infra/contracts.rs @@ -4,6 +4,7 @@ use crate::domain::eth; pub struct Contracts { pub weth: eth::WethAddress, pub settlement: eth::ContractAddress, + pub authenticator: eth::ContractAddress, pub balancer_vault: eth::ContractAddress, } @@ -21,6 +22,7 @@ impl Contracts { Self { weth: eth::WethAddress(a(contracts::WETH9::raw_contract()).0), settlement: a(contracts::GPv2Settlement::raw_contract()), + authenticator: a(contracts::GPv2AllowListAuthentication::raw_contract()), balancer_vault: a(contracts::BalancerV2Vault::raw_contract()), } } diff --git a/crates/solvers/src/infra/dex/mod.rs b/crates/solvers/src/infra/dex/mod.rs index 5d073b5f9c..32dacaad4e 100644 --- a/crates/solvers/src/infra/dex/mod.rs +++ b/crates/solvers/src/infra/dex/mod.rs @@ -3,8 +3,11 @@ use crate::domain::{auction, dex}; pub mod balancer; pub mod oneinch; pub mod paraswap; +pub mod simulator; pub mod zeroex; +pub use self::simulator::Simulator; + /// A supported external DEX/DEX aggregator API. pub enum Dex { Balancer(balancer::Sor), diff --git a/crates/solvers/src/infra/dex/simulator.rs b/crates/solvers/src/infra/dex/simulator.rs new file mode 100644 index 0000000000..d17c2bfff0 --- /dev/null +++ b/crates/solvers/src/infra/dex/simulator.rs @@ -0,0 +1,128 @@ +use { + crate::{ + domain::{dex, eth}, + infra::blockchain, + }, + contracts::ethcontract::{self, web3}, + ethereum_types::{Address, U256}, + ethrpc::extensions::EthExt, + std::collections::HashMap, +}; + +/// A DEX swap simulator. +#[derive(Debug, Clone)] +pub struct Simulator { + web3: ethrpc::Web3, + settlement: eth::ContractAddress, + authenticator: eth::ContractAddress, +} + +impl Simulator { + /// Create a new simulator for computing DEX swap gas usage. + pub fn new( + url: &reqwest::Url, + settlement: eth::ContractAddress, + authenticator: eth::ContractAddress, + ) -> Self { + Self { + web3: blockchain::rpc(url), + settlement, + authenticator, + } + } + + /// Simulate the gas needed by a single order DEX swap. + /// + /// This will return a `None` if the gas simulation is unavailable. + pub async fn gas(&self, owner: Address, swap: &dex::Swap) -> Result { + let swapper = contracts::support::Swapper::at(&self.web3, owner); + let tx = swapper + .methods() + .swap( + self.settlement.0, + (swap.input.token.0, swap.input.amount), + (swap.output.token.0, swap.output.amount), + (swap.allowance.spender.0, swap.allowance.amount.get()), + ( + swap.call.to.0, + U256::zero(), + ethcontract::Bytes(swap.call.calldata.clone()), + ), + ) + .tx; + + let call = web3::types::CallRequest { + to: tx.to, + data: tx.data, + ..Default::default() + }; + + let code = |contract: &contracts::ethcontract::Contract| { + contract + .deployed_bytecode + .to_bytes() + .expect("contract bytecode is available") + }; + let overrides = HashMap::<_, _>::from_iter([ + // Setup up our trader code that actually executes the settlement + ( + swapper.address(), + ethrpc::extensions::StateOverride { + code: Some(code(contracts::support::Swapper::raw_contract())), + ..Default::default() + }, + ), + // Override the CoW protocol solver authenticator with one that + // allows any address to solve + ( + self.authenticator.0, + ethrpc::extensions::StateOverride { + code: Some(code(contracts::support::AnyoneAuthenticator::raw_contract())), + ..Default::default() + }, + ), + ]); + + let return_data = self + .web3 + .eth() + .call_with_state_overrides(call, web3::types::BlockNumber::Latest.into(), overrides) + .await? + .0; + + let gas = { + if return_data.len() != 32 { + return Err(Error::InvalidReturnData); + } + + U256::from_big_endian(&return_data) + }; + + // `gas == 0` means that the simulation is not possible. See + // `Swapper.sol` contract for more details. In this case, use the + // heuristic gas amount from the swap. + Ok(if gas.is_zero() { + tracing::info!( + gas = ?swap.gas, + "could not simulate dex swap to get gas used; fall back to gas estimate provided \ + by dex API" + ); + swap.gas + } else { + eth::Gas(gas) + }) + } +} + +#[derive(Debug, thiserror::Error)] +#[error("error initializing simulator: {0}")] +pub struct InitializationError(#[from] ethcontract::errors::MethodError); + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("web3 error: {0:?}")] + Web3(#[from] web3::error::Error), + + #[error("invalid return data")] + InvalidReturnData, +} diff --git a/crates/solvers/src/infra/mod.rs b/crates/solvers/src/infra/mod.rs index 65261b1a83..e938aba499 100644 --- a/crates/solvers/src/infra/mod.rs +++ b/crates/solvers/src/infra/mod.rs @@ -1,3 +1,4 @@ +pub mod blockchain; pub mod cli; pub mod config; pub mod contracts; diff --git a/crates/solvers/src/tests/balancer/market_order.rs b/crates/solvers/src/tests/balancer/market_order.rs index 9d1d92d609..b9c96d6195 100644 --- a/crates/solvers/src/tests/balancer/market_order.rs +++ b/crates/solvers/src/tests/balancer/market_order.rs @@ -10,13 +10,13 @@ use { async fn sell() { let api = mock::http::setup(vec![mock::http::Expectation::Post { path: mock::http::Path::exact("sor"), - req: json!({ + req: mock::http::RequestBody::Exact(json!({ "sellToken": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "buyToken": "0xba100000625a3754423978a60c9317c58a424e3d", "orderKind": "sell", "amount": "1000000000000000000", "gasPrice": "15000000000", - }), + })), res: json!({ "tokenAddresses": [ "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", @@ -167,13 +167,13 @@ async fn sell() { async fn buy() { let api = mock::http::setup(vec![mock::http::Expectation::Post { path: mock::http::Path::exact("sor"), - req: json!({ + req: mock::http::RequestBody::Exact(json!({ "sellToken": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "buyToken": "0xba100000625a3754423978a60c9317c58a424e3d", "orderKind": "buy", "amount": "100000000000000000000", "gasPrice": "15000000000", - }), + })), res: json!({ "tokenAddresses": [ "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", diff --git a/crates/solvers/src/tests/balancer/mod.rs b/crates/solvers/src/tests/balancer/mod.rs index 543cab7c8b..4371c185d5 100644 --- a/crates/solvers/src/tests/balancer/mod.rs +++ b/crates/solvers/src/tests/balancer/mod.rs @@ -8,6 +8,7 @@ mod out_of_price; pub fn config(solver_addr: &SocketAddr) -> tests::Config { tests::Config::String(format!( r" +node-url = 'http://localhost:8545' risk-parameters = [0,0,0,0] [dex] endpoint = 'http://{solver_addr}/sor' diff --git a/crates/solvers/src/tests/balancer/not_found.rs b/crates/solvers/src/tests/balancer/not_found.rs index d3e2ae1788..03228a5adb 100644 --- a/crates/solvers/src/tests/balancer/not_found.rs +++ b/crates/solvers/src/tests/balancer/not_found.rs @@ -11,13 +11,13 @@ use { async fn test() { let api = mock::http::setup(vec![mock::http::Expectation::Post { path: mock::http::Path::Any, - req: json!({ + req: mock::http::RequestBody::Exact(json!({ "sellToken": "0x1111111111111111111111111111111111111111", "buyToken": "0x2222222222222222222222222222222222222222", "orderKind": "sell", "amount": "1000000000000000000", "gasPrice": "15000000000", - }), + })), res: json!({ "tokenAddresses": [], "swaps": [], diff --git a/crates/solvers/src/tests/balancer/out_of_price.rs b/crates/solvers/src/tests/balancer/out_of_price.rs index 4bc5f4c4ba..eae7be3fe8 100644 --- a/crates/solvers/src/tests/balancer/out_of_price.rs +++ b/crates/solvers/src/tests/balancer/out_of_price.rs @@ -13,13 +13,13 @@ use { async fn sell() { let api = mock::http::setup(vec![mock::http::Expectation::Post { path: mock::http::Path::Any, - req: json!({ + req: mock::http::RequestBody::Exact(json!({ "sellToken": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "buyToken": "0xba100000625a3754423978a60c9317c58a424e3d", "orderKind": "sell", "amount": "1000000000000000000", "gasPrice": "15000000000", - }), + })), res: json!({ "tokenAddresses": [ "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", @@ -102,13 +102,13 @@ async fn sell() { async fn buy() { let api = mock::http::setup(vec![mock::http::Expectation::Post { path: mock::http::Path::Any, - req: json!({ + req: mock::http::RequestBody::Exact(json!({ "sellToken": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "buyToken": "0xba100000625a3754423978a60c9317c58a424e3d", "orderKind": "buy", "amount": "100000000000000000000", "gasPrice": "15000000000", - }), + })), res: json!({ "tokenAddresses": [ "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", diff --git a/crates/solvers/src/tests/dex/partial_fill.rs b/crates/solvers/src/tests/dex/partial_fill.rs index 7ca6a6c303..c575926592 100644 --- a/crates/solvers/src/tests/dex/partial_fill.rs +++ b/crates/solvers/src/tests/dex/partial_fill.rs @@ -13,13 +13,13 @@ use { async fn tested_amounts_adjust_depending_on_response() { // observe::tracing::initialize_reentrant("solvers=trace"); let inner_request = |amount| { - json!({ + mock::http::RequestBody::Exact(json!({ "sellToken": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "buyToken": "0xba100000625a3754423978a60c9317c58a424e3d", "orderKind": "sell", "amount": amount, "gasPrice": "15000000000", - }) + })) }; let no_swap_found_response = json!({ @@ -94,7 +94,30 @@ async fn tested_amounts_adjust_depending_on_response() { ]) .await; - let engine = tests::SolverEngine::new("balancer", balancer::config(&api.address)).await; + let simulation_node = mock::http::setup(vec![mock::http::Expectation::Post { + path: mock::http::Path::Any, + req: mock::http::RequestBody::Any, + res: { + json!({ + "id": 1, + "jsonrpc": "2.0", + "result": "0x0000000000000000000000000000000000000000000000000000000000015B3C" + }) + }, + }]) + .await; + + let config = tests::Config::String(format!( + r" +node-url = 'http://{}' +risk-parameters = [0,0,0,0] +[dex] +endpoint = 'http://{}/sor' + ", + simulation_node.address, api.address, + )); + + let engine = tests::SolverEngine::new("balancer", config).await; let auction = json!({ "id": "1", @@ -251,13 +274,13 @@ async fn tested_amounts_wrap_around() { .into_iter() .map(|amount| mock::http::Expectation::Post { path: mock::http::Path::Any, - req: json!({ + req: mock::http::RequestBody::Exact(json!({ "sellToken": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "buyToken": "0xba100000625a3754423978a60c9317c58a424e3d", "orderKind": "buy", "amount": amount, "gasPrice": "15000000000", - }), + })), res: json!({ "tokenAddresses": [], "swaps": [], @@ -344,13 +367,13 @@ async fn moves_surplus_fee_to_buy_token() { let api = mock::http::setup(vec![ mock::http::Expectation::Post { path: mock::http::Path::Any, - req: json!({ + req: mock::http::RequestBody::Exact(json!({ "sellToken": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "buyToken": "0xba100000625a3754423978a60c9317c58a424e3d", "orderKind": "sell", "amount": "2000000000000000000", "gasPrice": "6000000000000", - }), + })), res: json!({ "tokenAddresses": [], "swaps": [], @@ -366,13 +389,13 @@ async fn moves_surplus_fee_to_buy_token() { }, mock::http::Expectation::Post { path: mock::http::Path::Any, - req: json!({ + req: mock::http::RequestBody::Exact(json!({ "sellToken": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "buyToken": "0xba100000625a3754423978a60c9317c58a424e3d", "orderKind": "sell", "amount": "1000000000000000000", "gasPrice": "6000000000000", - }), + })), res: json!({ "tokenAddresses": [ "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", @@ -402,7 +425,35 @@ async fn moves_surplus_fee_to_buy_token() { ]) .await; - let engine = tests::SolverEngine::new("balancer", balancer::config(&api.address)).await; + let simulation_node = mock::http::setup(vec![mock::http::Expectation::Post { + path: mock::http::Path::Any, + req: mock::http::RequestBody::Any, + res: { + json!({ + "id": 1, + "jsonrpc": "2.0", + // If the simulation logic returns 0 it means that the user did not have the + // required balance. This could be caused by a pre-interaction that acquires the + // necessary sell_token before the trade which is currently not supported by the + // simulation loic. + // In that case we fall back to the heuristic gas price we had in the past. + "result": "0x0000000000000000000000000000000000000000000000000000000000000000" + }) + }, + }]) + .await; + + let config = tests::Config::String(format!( + r" +node-url = 'http://{}' +risk-parameters = [0,0,0,0] +[dex] +endpoint = 'http://{}/sor' + ", + simulation_node.address, api.address, + )); + + let engine = tests::SolverEngine::new("balancer", config).await; let auction = json!({ "id": "1", @@ -542,13 +593,13 @@ async fn moves_surplus_fee_to_buy_token() { async fn insufficient_room_for_surplus_fee() { let api = mock::http::setup(vec![mock::http::Expectation::Post { path: mock::http::Path::Any, - req: json!({ + req: mock::http::RequestBody::Exact(json!({ "sellToken": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "buyToken": "0xba100000625a3754423978a60c9317c58a424e3d", "orderKind": "sell", "amount": "1000000000000000000", "gasPrice": "15000000000", - }), + })), res: json!({ "tokenAddresses": [ "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", @@ -643,13 +694,13 @@ async fn insufficient_room_for_surplus_fee() { async fn market() { let api = mock::http::setup(vec![mock::http::Expectation::Post { path: mock::http::Path::Any, - req: json!({ + req: mock::http::RequestBody::Exact(json!({ "sellToken": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "buyToken": "0xba100000625a3754423978a60c9317c58a424e3d", "orderKind": "sell", "amount": "1000000000000000000", "gasPrice": "15000000000", - }), + })), res: json!({ "tokenAddresses": [ "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", diff --git a/crates/solvers/src/tests/dex/wrong_execution.rs b/crates/solvers/src/tests/dex/wrong_execution.rs index 164783cfdf..91a479e67b 100644 --- a/crates/solvers/src/tests/dex/wrong_execution.rs +++ b/crates/solvers/src/tests/dex/wrong_execution.rs @@ -42,7 +42,7 @@ async fn test() { ] { let api = mock::http::setup(vec![mock::http::Expectation::Post { path: mock::http::Path::Any, - req: json!({ + req: mock::http::RequestBody::Exact(json!({ "sellToken": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "buyToken": "0xba100000625a3754423978a60c9317c58a424e3d", "orderKind": side, @@ -52,7 +52,7 @@ async fn test() { "227598784442065388110" }, "gasPrice": "15000000000", - }), + })), res: json!({ "tokenAddresses": [ "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", diff --git a/crates/solvers/src/tests/legacy/attaching_approvals.rs b/crates/solvers/src/tests/legacy/attaching_approvals.rs index 31b9f36e26..f157705459 100644 --- a/crates/solvers/src/tests/legacy/attaching_approvals.rs +++ b/crates/solvers/src/tests/legacy/attaching_approvals.rs @@ -10,7 +10,7 @@ use { async fn test() { let legacy_solver = mock::http::setup(vec![mock::http::Expectation::Post { path: mock::http::Path::Any, - req: json!({ + req: mock::http::RequestBody::Exact(json!({ "amms": {}, "metadata": { "auction_id": 1234, @@ -38,7 +38,7 @@ async fn test() { "normalize_priority": 0, } } - }), + })), res: json!({ "orders": {}, "prices": {}, diff --git a/crates/solvers/src/tests/legacy/concentrated_liquidity.rs b/crates/solvers/src/tests/legacy/concentrated_liquidity.rs index 84d3202f20..1221312907 100644 --- a/crates/solvers/src/tests/legacy/concentrated_liquidity.rs +++ b/crates/solvers/src/tests/legacy/concentrated_liquidity.rs @@ -10,7 +10,7 @@ use { async fn test() { let legacy_solver = mock::http::setup(vec![mock::http::Expectation::Post { path: mock::http::Path::Any, - req: json!({ + req: mock::http::RequestBody::Exact(json!({ "amms": { "0x97b744df0b59d93a866304f97431d8efad29a08d": { "address": "0x97b744df0b59d93a866304f97431d8efad29a08d", @@ -65,7 +65,7 @@ async fn test() { "external_price": null, } } - }), + })), res: json!({ "orders": {}, "prices": {}, diff --git a/crates/solvers/src/tests/legacy/jit_order.rs b/crates/solvers/src/tests/legacy/jit_order.rs index 60955c5f25..18a1c8a4b5 100644 --- a/crates/solvers/src/tests/legacy/jit_order.rs +++ b/crates/solvers/src/tests/legacy/jit_order.rs @@ -10,7 +10,7 @@ use { async fn test() { let legacy_solver = mock::http::setup(vec![mock::http::Expectation::Post { path: mock::http::Path::Any, - req: json!({ + req: mock::http::RequestBody::Exact(json!({ "amms": {}, "metadata": { "auction_id": 1, @@ -30,7 +30,7 @@ async fn test() { "external_price": null, } } - }), + })), res: json!({ "orders": {}, "prices": {}, diff --git a/crates/solvers/src/tests/legacy/market_order.rs b/crates/solvers/src/tests/legacy/market_order.rs index fca0a1c693..68988df844 100644 --- a/crates/solvers/src/tests/legacy/market_order.rs +++ b/crates/solvers/src/tests/legacy/market_order.rs @@ -19,7 +19,7 @@ async fn quote() { &auction_id=1\ &request_id=0" ), - req: json!({ + req: mock::http::RequestBody::Exact(json!({ "amms": { "0x97b744df0b59d93a866304f97431d8efad29a08d": { "address": "0x97b744df0b59d93a866304f97431d8efad29a08d", @@ -85,7 +85,7 @@ async fn quote() { "normalize_priority": 0 } } - }), + })), res: json!({ "orders": { "0": { @@ -223,7 +223,7 @@ async fn solve() { &auction_id=1234\ &request_id=0", ), - req: json!({ + req: mock::http::RequestBody::Exact(json!({ "amms": { "0x97b744df0b59d93a866304f97431d8efad29a08d": { "address": "0x97b744df0b59d93a866304f97431d8efad29a08d", @@ -289,7 +289,7 @@ async fn solve() { "normalize_priority": 0 } } - }), + })), res: json!({ "orders": { "0": { diff --git a/crates/solvers/src/tests/mock/http.rs b/crates/solvers/src/tests/mock/http.rs index e5bdcd7788..f91f59d797 100644 --- a/crates/solvers/src/tests/mock/http.rs +++ b/crates/solvers/src/tests/mock/http.rs @@ -71,11 +71,19 @@ pub enum Expectation { }, Post { path: Path, - req: serde_json::Value, + req: RequestBody, res: serde_json::Value, }, } +#[derive(Clone, Debug)] +pub enum RequestBody { + /// The received `[RequestBody]` has to match the provided value exactly. + Exact(serde_json::Value), + /// Any `[RequestBody]` will be accepted. + Any, +} + /// Drop handle that will verify that the server task didn't panic throughout /// the test and that all the expectations have been met. pub struct ServerHandle { @@ -105,7 +113,11 @@ impl Drop for ServerHandle { !self.handle.is_finished(), "mock http server terminated before test ended" ); - assert_eq!(self.expectations.lock().unwrap().len(), 0); + assert_eq!( + self.expectations.lock().unwrap().len(), + 0, + "mock server did not receive enough requests" + ); self.handle.abort(); } } @@ -226,7 +238,10 @@ fn post( let full_path = full_path(path, query); assert_eq!(full_path, expected_path, "POST request has unexpected path"); - assert_eq!(req, expected_req, "POST request has unexpected body"); + match expected_req { + RequestBody::Exact(value) => assert_eq!(req, value, "POST request has unexpected body"), + RequestBody::Any => (), + } res }; diff --git a/crates/solvers/src/tests/oneinch/mod.rs b/crates/solvers/src/tests/oneinch/mod.rs index aadc863724..72257dc172 100644 --- a/crates/solvers/src/tests/oneinch/mod.rs +++ b/crates/solvers/src/tests/oneinch/mod.rs @@ -8,6 +8,7 @@ mod out_of_price; pub fn config(solver_addr: &SocketAddr) -> tests::Config { tests::Config::String(format!( r" +node-url = 'http://localhost:8545' risk-parameters = [0,0,0,0] [dex] chain-id = '1' diff --git a/crates/solvers/src/tests/paraswap/market_order.rs b/crates/solvers/src/tests/paraswap/market_order.rs index 96fbcf02f5..01922b7d39 100644 --- a/crates/solvers/src/tests/paraswap/market_order.rs +++ b/crates/solvers/src/tests/paraswap/market_order.rs @@ -72,7 +72,7 @@ async fn sell() { }, mock::http::Expectation::Post { path: mock::http::Path::exact("transactions/1?ignoreChecks=true"), - req: json!({ + req: mock::http::RequestBody::Exact(json!({ "srcToken": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "destToken": "0xe41d2489571d322189246dafa5ebde1f4699f498", "srcAmount": "1000000000000000000", @@ -135,7 +135,7 @@ async fn sell() { }, "userAddress": "0xe0b3700e0aadcb18ed8d4bff648bc99896a18ad1", "partner": "cow" - }), + })), res: json!({ "from": "0xe0b3700e0aadcb18ed8d4bff648bc99896a18ad1", "to": "0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57", @@ -318,7 +318,7 @@ async fn buy() { }, mock::http::Expectation::Post { path: mock::http::Path::exact("transactions/1?ignoreChecks=true"), - req: json!({ + req: mock::http::RequestBody::Exact(json!({ "srcToken": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "destToken": "0xe41d2489571d322189246dafa5ebde1f4699f498", "srcAmount": "124940475326949378", @@ -389,7 +389,7 @@ async fn buy() { }, "userAddress": "0xe0b3700e0aadcb18ed8d4bff648bc99896a18ad1", "partner": "cow" - }), + })), res: json!({ "from": "0xe0b3700e0aadcb18ed8d4bff648bc99896a18ad1", "to": "0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57", diff --git a/crates/solvers/src/tests/paraswap/mod.rs b/crates/solvers/src/tests/paraswap/mod.rs index f3152acdd5..4edaf265bc 100644 --- a/crates/solvers/src/tests/paraswap/mod.rs +++ b/crates/solvers/src/tests/paraswap/mod.rs @@ -8,6 +8,7 @@ mod out_of_price; pub fn config(solver_addr: &SocketAddr) -> tests::Config { tests::Config::String(format!( r" +node-url = 'http://localhost:8545' risk-parameters = [0,0,0,0] [dex] endpoint = 'http://{solver_addr}' diff --git a/crates/solvers/src/tests/paraswap/out_of_price.rs b/crates/solvers/src/tests/paraswap/out_of_price.rs index fe78fdb580..ab25bad137 100644 --- a/crates/solvers/src/tests/paraswap/out_of_price.rs +++ b/crates/solvers/src/tests/paraswap/out_of_price.rs @@ -75,7 +75,7 @@ async fn sell() { }, mock::http::Expectation::Post { path: mock::http::Path::exact("transactions/1?ignoreChecks=true"), - req: json!({ + req: mock::http::RequestBody::Exact(json!({ "srcToken": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "destToken": "0xe41d2489571d322189246dafa5ebde1f4699f498", "srcAmount": "1000000000000000000", @@ -138,7 +138,7 @@ async fn sell() { }, "userAddress": "0xe0b3700e0aadcb18ed8d4bff648bc99896a18ad1", "partner": "cow" - }), + })), res: json!({ "from": "0xe0b3700e0aadcb18ed8d4bff648bc99896a18ad1", "to": "0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57", @@ -276,7 +276,7 @@ async fn buy() { }, mock::http::Expectation::Post { path: mock::http::Path::exact("transactions/1?ignoreChecks=true"), - req: json!({ + req: mock::http::RequestBody::Exact(json!({ "srcToken": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "destToken": "0xe41d2489571d322189246dafa5ebde1f4699f498", "srcAmount": "124940475326949378", @@ -347,7 +347,7 @@ async fn buy() { }, "userAddress": "0xe0b3700e0aadcb18ed8d4bff648bc99896a18ad1", "partner": "cow" - }), + })), res: json!({ "from": "0xe0b3700e0aadcb18ed8d4bff648bc99896a18ad1", "to": "0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57", diff --git a/crates/solvers/src/tests/zeroex/mod.rs b/crates/solvers/src/tests/zeroex/mod.rs index 342a2f1b66..34dc8a74a7 100644 --- a/crates/solvers/src/tests/zeroex/mod.rs +++ b/crates/solvers/src/tests/zeroex/mod.rs @@ -9,6 +9,7 @@ mod out_of_price; pub fn config(solver_addr: &SocketAddr) -> tests::Config { tests::Config::String(format!( r" +node-url = 'http://localhost:8545' risk-parameters = [0,0,0,0] [dex] endpoint = 'http://{solver_addr}/swap/v1/' diff --git a/crates/solvers/src/tests/zeroex/options.rs b/crates/solvers/src/tests/zeroex/options.rs index fb04c76682..c0b2a009d8 100644 --- a/crates/solvers/src/tests/zeroex/options.rs +++ b/crates/solvers/src/tests/zeroex/options.rs @@ -181,7 +181,8 @@ async fn test() { .await; let config = tests::Config::String(format!( - " + r" +node-url = 'http://localhost:8545' relative-slippage = '0.1' risk-parameters = [0,0,0,0] [dex]