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

Encode /quote response JIT orders and pre-interactions #3038

Open
wants to merge 14 commits into
base: 3027/driver-quote-response-jit-orders
Choose a base branch
from

Conversation

squadgazzz
Copy link
Contributor

@squadgazzz squadgazzz commented Oct 3, 2024

Description

Follow up to #3033, which uses the newly added /quote response fields with pre-interaction and JIT orders, encodes them into the settlement in order to verify the provided quote with this data.

How to test

New e2e tests:

  1. Ensures that an order can be fulfilled using the CoW AMM liquidity only.
  2. Tests the quote encoding logic.

@squadgazzz squadgazzz force-pushed the 3027/encode-jit-orders branch 2 times, most recently from 89b9a9d to 8ab3ebb Compare October 4, 2024 11:52
Comment on lines 1207 to 1209
// todo: figure out why this is even required.
dai.mint(onchain.contracts().gp_settlement.address(), to_wei(30))
.await;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The PR shouldn't be merged until this is resolved.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can't reproduce the revert when the settlement contract is not funded enough using a manual rpc call with either anvil or forked tenderly node. So weird. Tried different options with/without state overrides, different block numbers. All the same: the simulation passes, but not in the test...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

17292a6 (#3038) fixes this 🤷‍♂️

Comment on lines +1245 to +1250
// todo: this should be removed by providing a surplus capturing owners list(https://github.com/cowprotocol/services/pull/3048)
solvers_dto::solution::Trade::Fulfillment(solvers_dto::solution::Fulfillment {
order: [0u8; 56],
executed_amount: to_wei(230) - fee_user,
fee: None,
}),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This dummy trade is required to pass the following check in order to accept the solution:

Quote::new(
eth,
self,
// TODO(#1468): choose the best solution in the future, but for now just pick the
// first solution
solutions
.into_iter()
.find(|solution| !solution.is_empty(auction.surplus_capturing_jit_order_owners()))
.ok_or(QuotingFailed::NoSolutions)?,
)

All the regular trades are still skipped as it was before when building a quote response:

impl Quote {
fn new(eth: &Ethereum, order: &Order, solution: competition::Solution) -> Result<Self, Error> {
let sell_price = solution
.clearing_price(order.tokens.sell)
.ok_or(QuotingFailed::ClearingSellMissing)?
.to_big_rational();
let buy_price = solution
.clearing_price(order.tokens.buy)
.ok_or(QuotingFailed::ClearingBuyMissing)?
.to_big_rational();
let order_amount = order.amount.0.to_big_rational();
let amount = match order.side {
order::Side::Sell => order_amount
.mul(sell_price)
.checked_div(&buy_price)
.context("div by zero: buy price")?,
order::Side::Buy => order_amount
.mul(&buy_price)
.checked_div(&sell_price)
.context("div by zero: sell price")?,
};
Ok(Self {
amount: eth::U256::from_big_rational(&amount)?,
pre_interactions: solution.pre_interactions().to_vec(),
interactions: solution
.interactions()
.iter()
.map(|i| encode::interaction(i, eth.contracts().settlement()))
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.flatten()
.collect(),
solver: solution.solver().address(),
gas: solution.gas(),
tx_origin: *solution.solver().quote_tx_origin(),
jit_orders: solution
.trades()
.iter()
.filter_map(|trade| match trade {
solution::Trade::Jit(jit) => Some(jit.clone()),
_ => None,
})
.collect(),
})
}
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Is this actually a dummy trade? Isn't this how a real solver would actually indicate that it wants to settle the fake order generated for the quote?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't really see this trade used in the quote response at all.

onchain.contracts().weth.deposit()
);

// Fund the settlement contract with WETH so it can pay out the user order.
Copy link
Contributor

Choose a reason for hiding this comment

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

This should not be needed if the liquidity actually comes from the cow amm order.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Then, I assume there is still something that needs to be corrected since the simulation fails without this funding for both quote and settlement e2e tests.

colocation::start_driver(
onchain.contracts(),
vec![
colocation::start_baseline_solver(
Copy link
Contributor

Choose a reason for hiding this comment

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

We are not using the regular baseline solver for anything anymore, right? Then this can be removed and the comment above be amended.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is used for native price fetching.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fn api_autopilot_arguments() -> impl Iterator<Item = String> {
[
"--native-price-estimators=test_quoter|http://localhost:11088/test_solver".to_string(),

&onchain.contracts().domain_separator,
SecretKeyRef::from(&SecretKey::from_slice(bob.private_key()).unwrap()),
);
let user_order_id = services.create_order(&user_order).await.unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

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

To check that the quote actually gets verified it would be good to first get a quote from the API and assert that it has verified: true. Then you can call create_order() and pass the generated quote id.
Should effectively not change anything but it would make it clear in the test that checking that the quote was verified is important.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I forgot to mention that in the description. I wanted to merge these two e2e tests into a single one, but that also requires changes in the mocked solver. The PR is already huge, maybe it makes sense to accomplish this as a follow-up.

crates/e2e/tests/e2e/cow_amm.rs Outdated Show resolved Hide resolved
Comment on lines +965 to +966
/// Tests quotes verification that contains CoW AMM JIT orders.
async fn cow_amm_quoting(web3: Web3) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I see there is a separate test for quoting specifically now. Can this not be merged with the other new test?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

dto::Side::Buy => OrderKind::Buy,
dto::Side::Sell => OrderKind::Sell,
},
partially_fillable: false,
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a slightly problematic detail I didn't consider before. The JIT order API for the /settle call doesn't allow for partially fillable orders because that assumes only truly new JIT orders being proposed there and for those fill-or-kill should be sufficient.
But for the /quote endpoint the JIT orders returned could also be user orders (masked as JIT orders to not introduce even more enums). So if a solver would like to return a quote based on a partially fillable user order in the book this would not work here.
I think the API needs to allow sending the partially_fillable flag with the JIT orders.

Copy link
Contributor Author

@squadgazzz squadgazzz Oct 11, 2024

Choose a reason for hiding this comment

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

That means, the solvers API needs to be also changed since now it doesn't contain this field in the JitOrder json, right?

#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct JitOrder {
sell_token: eth::H160,
buy_token: eth::H160,
receiver: eth::H160,
#[serde_as(as = "serialize::U256")]
sell_amount: eth::U256,
#[serde_as(as = "serialize::U256")]
buy_amount: eth::U256,
valid_to: u32,
#[serde_as(as = "serialize::Hex")]
app_data: [u8; order::APP_DATA_LEN],
kind: Kind,
sell_token_balance: SellTokenBalance,
buy_token_balance: BuyTokenBalance,
signing_scheme: SigningScheme,
#[serde_as(as = "serialize::Hex")]
signature: Vec<u8>,
}

Copy link
Contributor

Choose a reason for hiding this comment

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

It was intended like this in the /solve API but I guess it makes sense to keep these 2 APIs the same.

crates/shared/src/price_estimation/trade_verifier.rs Outdated Show resolved Hide resolved
Comment on lines +466 to +467
clearing_prices.push(jit_order.buy_amount);
clearing_prices.push(jit_order.executed_amount);
Copy link
Contributor

Choose a reason for hiding this comment

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

@harisang in this PR we are testing that quotes can be settled (and verified) against JIT orders. We only introduced the concept of JIT orders in the /quote response because solvers could also settled against user orders by indexing the orderbook and pretending the user order is a JIT order.
In this particular part this would cause JIT orders to be settled at the limit price in the settlement. So if a solver returns a regular user order as a JIT order which would not reflect what would happen in a real settlement entirely correctly. I assumed this should be fine but I actually never asked the solver team about it.
Do you foresee any significant issues from this detail? Asking because we'd otherwise have to be able to know which JIT orders are actually surplus capturing in the quote verification which would be a huge PITA.

Copy link
Contributor Author

@squadgazzz squadgazzz Oct 11, 2024

Choose a reason for hiding this comment

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

In the competition encoding, executed amount is not used here:

super::Trade::Jit(trade) => {
(
Price {
// Jit orders are matched at limit price, so the sell token is worth
// buy.amount and vice versa
sell_token: trade.order().sell.token.into(),
sell_price: trade.order().buy.amount.into(),
buy_token: trade.order().buy.token.into(),
buy_price: trade.order().sell.amount.into(),
},

Maybe that is why the e2e test requires settlement contract to be funded with DAI:

// todo: figure out why this is even required.
dai.mint(onchain.contracts().gp_settlement.address(), to_wei(30))
.await;

But when using plain sell/buy amounts, the simulation gets reverted with a vague error, so I couldn't understand what exactly is wrong with that case.

Comment on lines -235 to -236
#[serde_as(as = "BytesHex")]
pub app_data: Vec<u8>,
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 reason for this change? The original version is more correct, no?

Copy link
Contributor Author

@squadgazzz squadgazzz Oct 11, 2024

Choose a reason for hiding this comment

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

The reason is that the AppDataHash needs to be constructed later here:

app_data: AppDataHash::from_str(&jit_order.app_data)?,

Which means that the Vec needs to be encoded back to String and only then to the AppDataHash. The change avoids back and forth decoding/encoding of the original String from the response.

Comment on lines +1245 to +1250
// todo: this should be removed by providing a surplus capturing owners list(https://github.com/cowprotocol/services/pull/3048)
solvers_dto::solution::Trade::Fulfillment(solvers_dto::solution::Fulfillment {
order: [0u8; 56],
executed_amount: to_wei(230) - fee_user,
fee: None,
}),
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this actually a dummy trade? Isn't this how a real solver would actually indicate that it wants to settle the fake order generated for the quote?


// Compensate a delay between the `CurrentBlockStream` and the actual onchain
// data.
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't fully understand why these sleeps are needed 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Autopilot receives onchain data using CurrentBlockStream, which has a delay between the actual onchain operations making the test fail since autopilot doesn't get informed about all the operations above.

Copy link
Contributor

Choose a reason for hiding this comment

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

I see, is there a way we can check this by DB query polling? mostly asking because it could be faster/more precise

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants