Skip to content

Commit

Permalink
Simulate Trades in Solver (#1960)
Browse files Browse the repository at this point in the history
# Description

Supersedes #1531

#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 <[email protected]>
Co-authored-by: Martin Beckmann <[email protected]>
  • Loading branch information
3 people authored Oct 18, 2023
1 parent 53b77c1 commit 7aeb32b
Show file tree
Hide file tree
Showing 40 changed files with 490 additions and 65 deletions.
2 changes: 1 addition & 1 deletion crates/autopilot/src/on_settlement_event_updater.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
1 change: 1 addition & 0 deletions crates/contracts/artifacts/AnyoneAuthenticator.json
Original file line number Diff line number Diff line change
@@ -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":{}}}
1 change: 1 addition & 0 deletions crates/contracts/artifacts/Swapper.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions crates/contracts/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down
8 changes: 8 additions & 0 deletions crates/contracts/solidity/AnyoneAuthenticator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract AnyoneAuthenticator {
function isSolver(address) external pure returns (bool) {
return true;
}
}
2 changes: 2 additions & 0 deletions crates/contracts/solidity/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down
117 changes: 117 additions & 0 deletions crates/contracts/solidity/Swapper.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}

2 changes: 2 additions & 0 deletions crates/contracts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,14 @@ include_contracts! {

pub mod support {
include_contracts! {
AnyoneAuthenticator;
Balances;
FetchBlock;
Multicall;
Signatures;
SimulateCode;
Solver;
Swapper;
Trader;
}
}
Expand Down
3 changes: 2 additions & 1 deletion crates/solvers/config/example.balancer.toml
Original file line number Diff line number Diff line change
@@ -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]
Expand Down
4 changes: 3 additions & 1 deletion crates/solvers/config/example.oneinch.toml
Original file line number Diff line number Diff line change
@@ -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"
4 changes: 3 additions & 1 deletion crates/solvers/config/example.paraswap.toml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
3 changes: 2 additions & 1 deletion crates/solvers/config/example.zeroex.toml
Original file line number Diff line number Diff line change
@@ -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]
Expand Down
20 changes: 18 additions & 2 deletions crates/solvers/src/domain/dex/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use {
crate::{
domain::{auction, eth, order, solution},
infra,
util,
},
ethereum_types::U256,
Expand Down Expand Up @@ -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<auction::Price>,
score: solution::Score,
simulator: &infra::dex::Simulator,
) -> Option<solution::Solution> {
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,
Expand All @@ -124,7 +140,7 @@ impl Swap {
input: self.input,
output: self.output,
interactions,
gas: self.gas,
gas,
}
.into_solution(gas_price, sell_token, score)
}
Expand Down
7 changes: 7 additions & 0 deletions crates/solvers/src/domain/order.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
13 changes: 12 additions & 1 deletion crates/solvers/src/domain/solver/dex/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand All @@ -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),
Expand Down Expand Up @@ -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;
};
Expand Down
13 changes: 13 additions & 0 deletions crates/solvers/src/infra/blockchain.rs
Original file line number Diff line number Diff line change
@@ -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",
)
}
9 changes: 1 addition & 8 deletions crates/solvers/src/infra/config/dex/balancer/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<H160>,

/// Optional CoW Protocol Settlement contract address. If not specified,
/// the default Settlement contract address will be used.
settlement: Option<H160>,
}

/// Load the driver configuration from a TOML file.
Expand All @@ -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,
}
Expand Down
Loading

0 comments on commit 7aeb32b

Please sign in to comment.