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

Verify quote with pre interactions #3160

Merged
merged 9 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
11 changes: 0 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,6 @@ Concretely, it is responsible for "cutting" new auctions (i.e. determining aucti

The `autopilot` connects to the same PostgreSQL database as the `orderbook` and uses it to query orders as well as storing the most recent auction and settlement competition.

## Solver

The `solver` crate is responsible for submitting on-chain settlements based on the orders it gets from the order book and other liquidity sources like Balancer or Uniswap pools.

It implements a few settlement strategies directly in Rust:

- Naive Solver: Can match to overlapping opposing orders (e.g. DAI for WETH & WETH for DAI) with one another settling the excess with Uniswap
- Uniswap Baseline: Same path finding as used by the Uniswap frontend (settling orders individually instead of batching them together)

It can also interact with a more advanced, Gnosis internal, closed source solver which tries to settle all orders using the combinatorial optimization formulations described in [Multi-Token Batch Auctions with Uniform Clearing Price](https://github.com/gnosis/dex-research/blob/master/BatchAuctionOptimization/batchauctions.pdf)

## Other Crates

There are additional crates that live in the cargo workspace.
Expand Down
2 changes: 1 addition & 1 deletion crates/contracts/artifacts/Solver.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion crates/contracts/artifacts/Trader.json

Large diffs are not rendered by default.

46 changes: 24 additions & 22 deletions crates/contracts/solidity/Solver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,44 +27,23 @@ contract Solver {
///
/// @param settlementContract - address of the settlement contract because
/// it does not have a stable address in tests.
/// @param trader - address of the order owner doing the trade
/// @param sellToken - address of the token being sold
/// @param sellAmount - amount being sold
/// @param nativeToken - ERC20 version of the chain's token
/// @param tokens - list of tokens used in the trade
/// @param receiver - address receiving the bought tokens
/// @param settlementCall - the calldata of the `settle()` call
/// @param spardose - contract to provide missing funds with
///
/// @return gasUsed - gas used for the `settle()` call
/// @return queriedBalances - list of balances stored during the simulation
function swap(
ISettlement settlementContract,
address payable trader,
address sellToken,
uint256 sellAmount,
address nativeToken,
address[] calldata tokens,
address payable receiver,
bytes calldata settlementCall,
address spardose
bytes calldata settlementCall
) external returns (
uint256 gasUsed,
uint256[] memory queriedBalances
) {
require(msg.sender == address(this), "only simulation logic is allowed to call 'swap' function");

// Prepare the trade in the context of the trader so we are allowed
// to set approvals and things like that.
Trader(trader)
.prepareSwap(
settlementContract,
sellToken,
sellAmount,
nativeToken,
spardose
);

// Warm the storage for sending ETH to smart contract addresses.
// We allow this call to revert becaues it was either unnecessary in the first place
// or failing to send `ETH` to the `receiver` will cause a revert in the settlement
Expand Down Expand Up @@ -125,4 +104,27 @@ contract Solver {
address(settlementContract).doCall(settlementCall);
gasUsed = gasStart - gasleft() - _simulationOverhead;
}

/// @dev Simple wrapper around `Trader.ensureTradePreconditions()` that
/// discounts the gas used to prepare the swap (setting up approvals
/// and balances) from the total gas cost since that would normally
/// not happen during the settlement.
function ensureTradePreconditions(
Trader trader,
ISettlement settlementContract,
address sellToken,
uint256 sellAmount,
address nativeToken,
address spardose
) external {
uint256 gasStart = gasleft();
trader.ensureTradePreconditions(
settlementContract,
sellToken,
sellAmount,
nativeToken,
spardose
);
_simulationOverhead += gasStart - gasleft();
MartinquaXD marked this conversation as resolved.
Show resolved Hide resolved
}
}
17 changes: 9 additions & 8 deletions crates/contracts/solidity/Trader.sol
Original file line number Diff line number Diff line change
Expand Up @@ -64,19 +64,13 @@ contract Trader {

/// @dev Executes needed actions on behalf of the trader to make the trade possible.
/// (e.g. wrapping ETH, setting approvals, and funding the account)
/// To support cases where the user's pre-interactions set up the trade
/// themselves (e.g. get tokens by unstaking) AND cases where we need to
/// set up everything for the trade we catch all relevant reverts here.
/// At the end of the function we can assume that all necessary pre-conditions
/// are met. If that is not the case the simulation will simply fail at a later
/// point in time anyway.
/// @param settlementContract - pass in settlement contract because it does not have
/// a stable address in tests.
/// @param sellToken - token being sold by the trade
/// @param sellAmount - expected amount to be sold according to the quote
/// @param nativeToken - ERC20 version of the chain's native token
/// @param spardose - piggy bank for requesting additional funds
function prepareSwap(
function ensureTradePreconditions(
ISettlement settlementContract,
address sellToken,
uint256 sellAmount,
Expand Down Expand Up @@ -115,6 +109,8 @@ contract Trader {
catch {}
try IERC20(sellToken).approve(address(settlementContract.vaultRelayer()), type(uint256).max) {}
catch {}
uint256 allowance = IERC20(sellToken).allowance(address(this), address(settlementContract.vaultRelayer()));
require(allowance >= sellAmount, "trader did not give the required approvals");
}

// Ensure that the user has sufficient sell token balance. If not, request some
Expand All @@ -123,7 +119,12 @@ contract Trader {
uint256 sellBalance = IERC20(sellToken).balanceOf(address(this));
if (sellBalance < sellAmount) {
try Spardose(spardose).requestFunds(sellToken, sellAmount - sellBalance) {}
catch {}
catch {
// The trader does not have sufficient sell token balance, and the
// piggy bank pre-fund failed, as balance overrides are not available.
// Revert with a helpful message.
revert("trader does not have enough sell token");
}
}
}

Expand Down
48 changes: 33 additions & 15 deletions crates/shared/src/price_estimation/trade_verifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ impl TradeVerifier {
out_amount,
self.native_token,
&self.domain_separator,
self.settlement.address(),
)?;

let settlement = add_balance_queries(settlement, query, &verification, &solver);
Expand All @@ -158,23 +159,13 @@ impl TradeVerifier {
)
.tx;

let sell_amount = match query.kind {
OrderKind::Sell => query.in_amount.get(),
OrderKind::Buy => *out_amount,
};

let simulation = solver
.methods()
.swap(
self.settlement.address(),
verification.from,
query.sell_token,
sell_amount,
self.native_token,
tokens.clone(),
verification.receiver,
Bytes(settlement.data.unwrap().0),
Self::SPARDOSE,
)
.tx;

Expand Down Expand Up @@ -466,6 +457,7 @@ fn encode_settlement(
out_amount: &U256,
native_token: H160,
domain_separator: &DomainSeparator,
settlement: H160,
) -> Result<EncodedSettlement> {
let mut trade_interactions = encode_interactions(&trade.interactions());
if query.buy_token == BUY_ETH_ADDRESS {
Expand Down Expand Up @@ -493,11 +485,37 @@ fn encode_settlement(
)?);
}

let pre_interactions = [
verification.pre_interactions.clone(),
trade.pre_interactions(),
]
.concat();
// Execute interaction to set up trade right before transfering funds.
// This interaction does nothing if the user-provided pre-interactions
// already set everything up (e.g. approvals, balances). That way we can
// correctly verify quotes with or without these user pre-interactions
// with helpful error messages.
let trade_setup_interaction = {
let sell_amount = match query.kind {
OrderKind::Sell => query.in_amount.get(),
OrderKind::Buy => *out_amount,
};
let solver = dummy_contract!(Solver, trade.solver());
let setup_step = solver.ensure_trade_preconditions(
verification.from,
settlement,
query.sell_token,
sell_amount,
native_token,
TradeVerifier::SPARDOSE,
);
Interaction {
target: solver.address(),
value: 0.into(),
data: setup_step.tx.data.unwrap().0,
MartinquaXD marked this conversation as resolved.
Show resolved Hide resolved
}
};

let user_interactions = verification.pre_interactions.iter().cloned();
let pre_interactions: Vec<_> = user_interactions
.chain(trade.pre_interactions())
.chain([trade_setup_interaction])
.collect();

Ok(EncodedSettlement {
tokens: tokens.to_vec(),
Expand Down
8 changes: 0 additions & 8 deletions crates/solver/src/settlement/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,14 +131,6 @@ impl Settlement {
self.encoder.clearing_prices()
}

/// Returns the clearing price for the specified token.
///
/// Returns `None` if the token is not part of the settlement.
#[cfg(test)]
pub(crate) fn clearing_price(&self, token: H160) -> Option<U256> {
self.clearing_prices().get(&token).copied()
}

/// Returns all orders included in the settlement.
pub fn traded_orders(&self) -> impl Iterator<Item = &Order> + '_ {
self.encoder.all_trades().map(|trade| &trade.data.order)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,32 @@
use {
crate::liquidity::{ConstantProductOrder, WeightedProductOrder},
anyhow::anyhow,
ethcontract::{H160, U256},
shared::{
baseline_solver::BaselineSolvable,
sources::{balancer_v2::swap::WeightedPoolRef, uniswap_v2::pool_fetching::Pool},
},
std::{fmt::Debug, str::FromStr},
};

// Wrapper type for AWS ARN identifiers
#[derive(Debug, Clone)]
pub struct Arn(pub String);

impl FromStr for Arn {
type Err = anyhow::Error;

fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
// Could be more strict here, but this should suffice to catch unintended
// configuration mistakes
if s.starts_with("arn:aws:kms:") {
Ok(Self(s.to_string()))
} else {
Err(anyhow!("Invalid ARN identifier: {}", s))
}
}
}

impl BaselineSolvable for ConstantProductOrder {
fn get_amount_out(&self, out_token: H160, input: (U256, H160)) -> Option<U256> {
amm_to_pool(self).get_amount_out(out_token, input)
Expand Down
25 changes: 0 additions & 25 deletions crates/solver/src/solver/mod.rs

This file was deleted.

Loading
Loading