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

Baseline solver limit orders support #2326

Merged
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
2e18b36
Baseline LimitOrder support
squadgazzz Jan 24, 2024
4b42119
Precision fix
squadgazzz Jan 24, 2024
c3d45a2
Bfp -> U256
squadgazzz Jan 25, 2024
2a2a8aa
Refactoring
squadgazzz Jan 25, 2024
cffd9c0
More tests
squadgazzz Jan 25, 2024
381416d
Naming
squadgazzz Jan 25, 2024
0eb67d2
Redundant function
squadgazzz Jan 25, 2024
dce04b6
Adjusted value
squadgazzz Jan 25, 2024
a830885
Clean-up
squadgazzz Jan 25, 2024
621ca08
Align calculations with 0x smart contract
squadgazzz Jan 26, 2024
ec9887d
Naming
squadgazzz Jan 26, 2024
67dba54
Adjust formula
squadgazzz Jan 26, 2024
c86ae17
Minor test fixes
squadgazzz Feb 1, 2024
b8cfde1
Domain separator fix
squadgazzz Feb 1, 2024
bea923c
Fixed hash structure
squadgazzz Feb 2, 2024
f848362
Whitelist solvers and fix values
squadgazzz Feb 21, 2024
fca90f4
Merge branch 'e2e/0x-liquidity-fetching' into baseline-solver-limit-o…
squadgazzz Feb 21, 2024
9ad39cd
Redundant log
squadgazzz Feb 21, 2024
a3167b1
Fork url env name
squadgazzz Feb 21, 2024
3a6b13e
Config fix
squadgazzz Feb 21, 2024
de418ea
Merge branch 'e2e/0x-liquidity-fetching' into baseline-solver-limit-o…
squadgazzz Apr 1, 2024
e818b6d
Missing app_data field
squadgazzz Apr 1, 2024
49df1fb
Merge branch 'e2e/0x-liquidity-fetching' into baseline-solver-limit-o…
squadgazzz Apr 1, 2024
f10fed0
Redundant import
squadgazzz Apr 1, 2024
6bcfbac
Private field
squadgazzz Apr 1, 2024
c27296d
Fix after merge
squadgazzz Apr 1, 2024
fd7880f
Native 0x liquidity orders
squadgazzz Apr 2, 2024
68e7da3
Comments
squadgazzz Apr 2, 2024
4de0d47
Redundant mock
squadgazzz Apr 2, 2024
7e061d8
Review fixes
squadgazzz Apr 3, 2024
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: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/solvers/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,5 @@ tracing = { workspace = true }
[dev-dependencies]
glob = "0.3"
tempfile = "3"
hex-literal = { workspace = true }
ethcontract = { workspace = true }
77 changes: 49 additions & 28 deletions crates/solvers/src/boundary/baseline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use {

pub struct Solver<'a> {
base_tokens: BaseTokens,
amms: HashMap<TokenPair, Vec<Amm>>,
amms: HashMap<TokenPair, Vec<OnchainLiquidity>>,
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the difference between amms and liquidity? Does it make sense to combine the two somehow (maybe in a separate PR)? Also amms should probably be renamed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Renamed and created an issue #2332

liquidity: HashMap<liquidity::Id, &'a liquidity::Liquidity>,
}

Expand Down Expand Up @@ -100,7 +100,7 @@ impl<'a> Solver<'a> {

fn traverse_path(
&self,
path: &[&Amm],
path: &[&OnchainLiquidity],
mut sell_token: H160,
mut sell_amount: U256,
) -> Option<Vec<baseline::Segment<'a>>> {
Expand Down Expand Up @@ -137,7 +137,9 @@ impl<'a> Solver<'a> {
}
}

fn to_boundary_amms(liquidity: &[liquidity::Liquidity]) -> HashMap<TokenPair, Vec<Amm>> {
fn to_boundary_amms(
liquidity: &[liquidity::Liquidity],
) -> HashMap<TokenPair, Vec<OnchainLiquidity>> {
liquidity
.iter()
.fold(HashMap::new(), |mut amms, liquidity| {
Expand All @@ -149,11 +151,13 @@ fn to_boundary_amms(liquidity: &[liquidity::Liquidity]) -> HashMap<TokenPair, Ve
pool,
)
{
amms.entry(boundary_pool.tokens).or_default().push(Amm {
id: liquidity.id.clone(),
token_pair: boundary_pool.tokens,
pool: Pool::ConstantProduct(boundary_pool),
});
amms.entry(boundary_pool.tokens)
.or_default()
.push(OnchainLiquidity {
id: liquidity.id.clone(),
token_pair: boundary_pool.tokens,
source: LiquiditySource::ConstantProduct(boundary_pool),
});
}
}
liquidity::State::WeightedProduct(pool) => {
Expand All @@ -165,10 +169,10 @@ fn to_boundary_amms(liquidity: &[liquidity::Liquidity]) -> HashMap<TokenPair, Ve
{
for pair in pool.reserves.token_pairs() {
let token_pair = to_boundary_token_pair(&pair);
amms.entry(token_pair).or_default().push(Amm {
amms.entry(token_pair).or_default().push(OnchainLiquidity {
id: liquidity.id.clone(),
token_pair,
pool: Pool::WeightedProduct(boundary_pool.clone()),
source: LiquiditySource::WeightedProduct(boundary_pool.clone()),
});
}
}
Expand All @@ -179,14 +183,25 @@ fn to_boundary_amms(liquidity: &[liquidity::Liquidity]) -> HashMap<TokenPair, Ve
{
for pair in pool.reserves.token_pairs() {
let token_pair = to_boundary_token_pair(&pair);
amms.entry(token_pair).or_default().push(Amm {
amms.entry(token_pair).or_default().push(OnchainLiquidity {
id: liquidity.id.clone(),
token_pair,
pool: Pool::Stable(boundary_pool.clone()),
source: LiquiditySource::Stable(boundary_pool.clone()),
});
}
}
}
liquidity::State::LimitOrder(limit_order) => {
if let Some(token_pair) =
TokenPair::new(limit_order.maker.token.0, limit_order.taker.token.0)
{
amms.entry(token_pair).or_default().push(OnchainLiquidity {
id: liquidity.id.clone(),
token_pair,
source: LiquiditySource::LimitOrder(limit_order.clone()),
})
}
}
// The baseline solver does not currently support other AMMs.
_ => {}
};
Expand All @@ -195,41 +210,47 @@ fn to_boundary_amms(liquidity: &[liquidity::Liquidity]) -> HashMap<TokenPair, Ve
}

#[derive(Debug)]
struct Amm {
struct OnchainLiquidity {
id: liquidity::Id,
token_pair: TokenPair,
pool: Pool,
source: LiquiditySource,
}

#[derive(Debug)]
enum Pool {
enum LiquiditySource {
ConstantProduct(boundary::liquidity::constant_product::Pool),
WeightedProduct(boundary::liquidity::weighted_product::Pool),
Stable(boundary::liquidity::stable::Pool),
LimitOrder(liquidity::limit_order::LimitOrder),
}

impl BaselineSolvable for Amm {
impl BaselineSolvable for OnchainLiquidity {
fn get_amount_out(&self, out_token: H160, input: (U256, H160)) -> Option<U256> {
match &self.pool {
Pool::ConstantProduct(pool) => pool.get_amount_out(out_token, input),
Pool::WeightedProduct(pool) => pool.get_amount_out(out_token, input),
Pool::Stable(pool) => pool.get_amount_out(out_token, input),
match &self.source {
LiquiditySource::ConstantProduct(pool) => pool.get_amount_out(out_token, input),
LiquiditySource::WeightedProduct(pool) => pool.get_amount_out(out_token, input),
LiquiditySource::Stable(pool) => pool.get_amount_out(out_token, input),
LiquiditySource::LimitOrder(limit_order) => {
limit_order.get_amount_out(out_token, input)
}
}
}

fn get_amount_in(&self, in_token: H160, out: (U256, H160)) -> Option<U256> {
match &self.pool {
Pool::ConstantProduct(pool) => pool.get_amount_in(in_token, out),
Pool::WeightedProduct(pool) => pool.get_amount_in(in_token, out),
Pool::Stable(pool) => pool.get_amount_in(in_token, out),
match &self.source {
LiquiditySource::ConstantProduct(pool) => pool.get_amount_in(in_token, out),
LiquiditySource::WeightedProduct(pool) => pool.get_amount_in(in_token, out),
LiquiditySource::Stable(pool) => pool.get_amount_in(in_token, out),
LiquiditySource::LimitOrder(limit_order) => limit_order.get_amount_in(in_token, out),
}
}

fn gas_cost(&self) -> usize {
match &self.pool {
Pool::ConstantProduct(pool) => pool.gas_cost(),
Pool::WeightedProduct(pool) => pool.gas_cost(),
Pool::Stable(pool) => pool.gas_cost(),
match &self.source {
LiquiditySource::ConstantProduct(pool) => pool.gas_cost(),
LiquiditySource::WeightedProduct(pool) => pool.gas_cost(),
LiquiditySource::Stable(pool) => pool.gas_cost(),
LiquiditySource::LimitOrder(limit_order) => limit_order.gas_cost(),
}
}
}
Expand Down
159 changes: 159 additions & 0 deletions crates/solvers/src/boundary/liquidity/limit_order.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
use {
crate::domain::liquidity::limit_order::{LimitOrder, TakerAmount},
contracts::ethcontract::{H160, U256},
shared::{baseline_solver::BaselineSolvable, price_estimation::gas::GAS_PER_ZEROEX_ORDER},
};

impl BaselineSolvable for LimitOrder {
fn get_amount_out(&self, out_token: H160, (in_amount, in_token): (U256, H160)) -> Option<U256> {
if in_token != self.taker.token.0
|| out_token != self.maker.token.0
|| in_amount > self.taker.amount
{
return None;
}

let fee_adjusted_amount = in_amount.checked_sub(self.fee.0)?;
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like this fee scales with gas price. I'm not sure we have taken that into account anywhere: https://docs.0xprotocol.org/en/latest/basics/protocol_fees.html#protocol-fees

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, does it mean we have to fetch the gas price each time we fetch 0x's limit orders from the orderbook?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Btw, taker's token fee is not scaled AFAIU. And the protocol fee should not be considered while calculating in/out amounts.

Copy link
Contributor

@MartinquaXD MartinquaXD Jan 26, 2024

Choose a reason for hiding this comment

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

If I understand the contract code correctly the takerTokenFeeAmount gets deducted after the order gets partially filled and does not reduce the available fill amount so I think it does not even have any influence on the math here.
Additionally the protocol fee was decided to be 0 according to the page I linked so I believe the logic is actually just in_amount * maker_amount / taker_amount.

That being said I always find solidity code particularly hard to reason about so I might be wrong here. If there is still doubt about this checking out a tenderly simulation for a 0x fill would help. Also if this math is off we should detect that with the e2e tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, simplified the formula to align with the one used in the smart contract. Added this temp test with the values from this tx. The second assertion fails due to precision issue, which is expected, I believe.

if self.maker.amount > self.taker.amount {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why does it make a difference here whether taker.amount or maker.amount amount is bigger?
My understanding is that from our perspective makerToken == buyToken and takerToken == sellToken.
get_amount_out() basically computes given some in_amount how much outToken would the liquidity source be willing to sell.
If I'm not mistaken it should not have any influence on the math whether takerAmount or makerAmount is bigger.

Same thought on the other function.

Copy link
Contributor Author

@squadgazzz squadgazzz Jan 25, 2024

Choose a reason for hiding this comment

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

We do not operate with floating point numbers here. Consider an example where the ratio is 0.5, where in fact, we will get 0. As an alternative to avoid that, we should multiply the amounts that could lead to U256 overflow.

Copy link
Contributor

@MartinquaXD MartinquaXD Jan 25, 2024

Choose a reason for hiding this comment

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

I see. 👍
You could also get around that by not computing the ratio first and instead do all operations in order which is what we usually do:

fee_adjusted_amount * make.amount / taker.amount

If you want to be super careful in order to avoid overflows on the multiplication you could even do a.full_mul(b) which promotes the result to U512.
One thing to note throughout all this is that we should try to mimic what the actual smart contract does as closely as possible. Imagine they have an error in their equation or even some rounding issues. Then it does us no good if we have a theoretically better implementation if a solution generated by using it ends up reverting.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also let's add a small comment in the code explaining this non trivial rationale...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

let price_ratio = self.maker.amount.checked_div(self.taker.amount)?;
fee_adjusted_amount.checked_mul(price_ratio)
} else {
let inverse_price_ratio = self.taker.amount.checked_div(self.maker.amount)?;
fee_adjusted_amount.checked_div(inverse_price_ratio)
}
}

fn get_amount_in(&self, in_token: H160, (out_amount, out_token): (U256, H160)) -> Option<U256> {
if out_token != self.maker.token.0
|| in_token != self.taker.token.0
|| out_amount > self.maker.amount
{
return None;
}

let amount_before_fee = if self.maker.amount > self.taker.amount {
let inverse_price_ratio = self.maker.amount.checked_div(self.taker.amount)?;
out_amount.checked_div(inverse_price_ratio)?
} else {
let price_ratio = self.taker.amount.checked_div(self.maker.amount)?;
out_amount.checked_mul(price_ratio)?
};
amount_before_fee.checked_add(self.fee.0)
}

fn gas_cost(&self) -> usize {
GAS_PER_ZEROEX_ORDER as usize
}
}

#[cfg(test)]
mod tests {
use {super::*, crate::domain::eth, contracts::ethcontract::U256, shared::addr};

fn create_limit_order(maker_amount: U256, taker_amount: U256, fee_amount: U256) -> LimitOrder {
let maker = eth::Asset {
amount: maker_amount,
token: eth::TokenAddress(addr!("a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48")),
};
let taker = eth::Asset {
amount: taker_amount,
token: eth::TokenAddress(addr!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2")),
};
let fee = TakerAmount(fee_amount);

LimitOrder { maker, taker, fee }
}

#[test]
fn amount_out_in_round_trip() {
let maker_amount = to_wei(321);
let taker_amount = to_wei(123);
let fee_amount = to_wei(10);
let desired_in_amount = to_wei(50);

let order = create_limit_order(maker_amount, taker_amount, fee_amount);
let out_token = order.maker.token.0;
let in_token = order.taker.token.0;

let amount_out = order
.get_amount_out(out_token, (desired_in_amount, in_token))
.unwrap();
let amount_in = order
.get_amount_in(in_token, (amount_out, out_token))
.unwrap();

assert_eq!(amount_in, desired_in_amount);
}

#[test]
fn amount_in_out_round_trip() {
let maker_amount = to_wei(123);
let taker_amount = to_wei(321);
let fee_amount = to_wei(10);
let desired_out_amount = to_wei(50);

let order = create_limit_order(maker_amount, taker_amount, fee_amount);
let out_token = order.maker.token.0;
let in_token = order.taker.token.0;

let amount_in = order
.get_amount_in(in_token, (desired_out_amount, out_token))
.unwrap();
let amount_out = order
.get_amount_out(out_token, (amount_in, in_token))
.unwrap();

assert_eq!(amount_out, desired_out_amount);
}

#[test]
fn too_high_in_amount() {
let maker_amount = to_wei(300);
let taker_amount = to_wei(100);
let fee_amount = to_wei(10);

let order = create_limit_order(maker_amount, taker_amount, fee_amount);
let out_token = order.maker.token.0;
let in_token = order.taker.token.0;
let amount_in = taker_amount.checked_mul(U256::from(2)).unwrap();
let amount_out = order.get_amount_out(out_token, (amount_in, in_token));

assert!(amount_out.is_none());
}

#[test]
fn too_high_out_amount() {
let maker_amount = to_wei(100);
let taker_amount = to_wei(300);
let fee_amount = to_wei(10);

let order = create_limit_order(maker_amount, taker_amount, fee_amount);
let out_token = order.maker.token.0;
let in_token = order.taker.token.0;
let amount_out = maker_amount.checked_mul(U256::from(2)).unwrap();
let amount_in = order.get_amount_in(in_token, (amount_out, out_token));

assert!(amount_in.is_none());
}

#[test]
fn wrong_tokens() {
let maker_amount = to_wei(100);
let taker_amount = to_wei(100);
let fee_amount = to_wei(10);

let order = create_limit_order(maker_amount, taker_amount, fee_amount);
let out_token = order.maker.token.0;
let in_token = order.taker.token.0;
let amount = to_wei(1);
let amount_in = order.get_amount_in(out_token, (amount, in_token));
let amount_out = order.get_amount_out(in_token, (amount, out_token));

assert!(amount_in.is_none());
assert!(amount_out.is_none());
}

fn to_wei(base: u32) -> U256 {
U256::from(base) * U256::exp10(18)
}
}
1 change: 1 addition & 0 deletions crates/solvers/src/boundary/liquidity/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod constant_product;
mod limit_order;
pub mod stable;
pub mod weighted_product;
Loading