Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simulate Trades in Solver #1960

Merged
merged 14 commits into from
Oct 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
MartinquaXD marked this conversation as resolved.
Show resolved Hide resolved

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,
MartinquaXD marked this conversation as resolved.
Show resolved Hide resolved
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
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I did not review the PR before merging.

Is it possible to move out gas estimation at the callsite so we can use the accurate gas estimate for success_probability calculation also?


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");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, would like to use simulated gas estimate for success_probability calculation.

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
Loading