Skip to content

Commit

Permalink
Domain Settlement Encoding (#2661)
Browse files Browse the repository at this point in the history
# Description
Main part of #2215. This PR ports the settlement encoding logic which is
currently contained in the legacy solver crate's
[`SettlemenEncoder`](https://github.com/cowprotocol/services/blob/81831022cc41a05e76547b90e87694dc349a0388/crates/solver/src/settlement/settlement_encoder.rs#L37)
to our domain.

It is importantly still missing application of slippage tolerance (which
is contained in
[`SlippageContext`](https://github.com/cowprotocol/services/blob/81831022cc41a05e76547b90e87694dc349a0388/crates/solver/src/liquidity/slippage.rs#L123),
porting which would have made this PR even larger.

# Changes
- [x] Introduce encoding module which exposes a function to take a
solution and returns a settlement tx object
- [x] Encode clearing prices, trades & interactions the way the
settlement contract expects (guided by the boundary implementation)
- [x] Implement `revertable` logic on `Solutions` (this also fixes
#2640)
- [x] Make allowance computation internalisation aware (currently
allowances for internalised interactions are added to the execution plan
even if we are building an internalised one)
- [x] Unify `swap` implementation of the liquidity types to always
return `Result` (one returned `Option`)

## How to test
- [ ] Change default `encoding::Strategy` from Boundary to Domain and
run tests.

> [!IMPORTANT]  
> We still need to implement the slippage computation to make this
strategy fully working


Fixes #2215 & #2640

---------

Co-authored-by: Martin Beckmann <[email protected]>
  • Loading branch information
fleupold and MartinquaXD authored Apr 29, 2024
1 parent 16d0963 commit 6bb059b
Show file tree
Hide file tree
Showing 6 changed files with 438 additions and 21 deletions.
4 changes: 3 additions & 1 deletion crates/driver/src/boundary/settlement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,9 @@ impl Settlement {
settlement.with_liquidity(&boundary_limit_order, execution)?;
}

let approvals = solution.approvals(eth).await?;
let approvals = solution
.approvals(eth, settlement::Internalization::Disable)
.await?;
for approval in approvals {
settlement
.encoder
Expand Down
368 changes: 368 additions & 0 deletions crates/driver/src/domain/competition/solution/encoding.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
use {
super::{error::Math, interaction::Liquidity, settlement, trade::ClearingPrices},
crate::{
domain::{
competition::{
self,
order::{self, Partial},
},
eth::{self, allowance, Ether},
liquidity::{self, ExactOutput, MaxInput},
},
util::Bytes,
},
allowance::Allowance,
};

/// The type of strategy used to encode the solution.
#[derive(Debug, Copy, Clone)]
pub enum Strategy {
Expand All @@ -6,3 +22,355 @@ pub enum Strategy {
/// Use logic from this module for encoding
Domain,
}

#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("invalid interaction: {0:?}")]
InvalidInteractionExecution(competition::solution::interaction::Liquidity),
#[error("invalid clearing price: {0:?}")]
InvalidClearingPrice(eth::TokenAddress),
#[error(transparent)]
Math(#[from] Math),
}

pub fn tx(
auction_id: competition::auction::Id,
solution: &super::Solution,
contract: &contracts::GPv2Settlement,
approvals: impl Iterator<Item = eth::allowance::Approval>,
internalization: settlement::Internalization,
) -> Result<eth::Tx, Error> {
let mut tokens = Vec::with_capacity(solution.prices.len() + (solution.trades().len() * 2));
let mut clearing_prices =
Vec::with_capacity(solution.prices.len() + (solution.trades().len() * 2));
let mut trades: Vec<Trade> = Vec::with_capacity(solution.trades().len());
let mut pre_interactions = Vec::new();
let mut interactions =
Vec::with_capacity(approvals.size_hint().0 + solution.interactions().len());
let mut post_interactions = Vec::new();

// Encode uniform clearing price vector
for (token, price) in solution.prices.clone() {
tokens.push(token.into());
clearing_prices.push(price);
}

// Encode trades with custom clearing prices
for trade in solution.trades() {
let (price, trade) = match trade {
super::Trade::Fulfillment(trade) => {
pre_interactions.extend(trade.order().pre_interactions.clone());
post_interactions.extend(trade.order().post_interactions.clone());

let uniform_prices = ClearingPrices {
sell: solution
.clearing_price(trade.order().sell.token)
.ok_or(Error::InvalidClearingPrice(trade.order().sell.token))?,
buy: solution
.clearing_price(trade.order().buy.token)
.ok_or(Error::InvalidClearingPrice(trade.order().buy.token))?,
};
let custom_prices = trade.custom_prices(&uniform_prices)?;
(
Price {
sell_token: trade.order().sell.token.into(),
sell_price: custom_prices.sell,
buy_token: trade.order().buy.token.into(),
buy_price: custom_prices.buy,
},
Trade {
sell_token_index: (tokens.len() - 2).into(),
buy_token_index: (tokens.len() - 1).into(),
receiver: trade.order().receiver.unwrap_or_default().into(),
sell_amount: trade.order().sell.amount.into(),
buy_amount: trade.order().buy.amount.into(),
valid_to: trade.order().valid_to.into(),
app_data: trade.order().app_data.0 .0.into(),
fee_amount: eth::U256::zero(),
flags: Flags {
side: trade.order().side,
partially_fillable: matches!(
trade.order().partial,
Partial::Yes { .. }
),
signing_scheme: trade.order().signature.scheme,
sell_token_balance: trade.order().sell_token_balance,
buy_token_balance: trade.order().buy_token_balance,
},
executed_amount: match trade.order().side {
order::Side::Sell => trade.executed().0 + trade.fee().0,
order::Side::Buy => trade.executed().into(),
},
signature: trade.order().signature.data.clone(),
},
)
}
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.0.into(),
sell_price: trade.order().buy.amount.into(),
buy_token: trade.order().buy.token.0.into(),
buy_price: trade.order().sell.amount.into(),
},
Trade {
sell_token_index: (tokens.len() - 2).into(),
buy_token_index: (tokens.len() - 1).into(),
receiver: trade.order().receiver.into(),
sell_amount: trade.order().sell.amount.into(),
buy_amount: trade.order().buy.amount.into(),
valid_to: trade.order().valid_to.into(),
app_data: trade.order().app_data.0 .0.into(),
fee_amount: eth::U256::zero(),
flags: Flags {
side: trade.order().side,
partially_fillable: trade.order().partially_fillable,
signing_scheme: trade.order().signature.scheme,
sell_token_balance: trade.order().sell_token_balance,
buy_token_balance: trade.order().buy_token_balance,
},
executed_amount: trade.executed().0,
signature: trade.order().signature.data.clone(),
},
)
}
};
tokens.push(price.sell_token);
tokens.push(price.buy_token);
clearing_prices.push(price.sell_price);
clearing_prices.push(price.buy_price);

trades.push(trade);
}

// Encode allowances
for approval in approvals {
interactions.push(approve(&approval.0))
}

// Encode interaction
for interaction in solution.interactions() {
if matches!(internalization, settlement::Internalization::Enable)
&& interaction.internalize()
{
continue;
}

interactions.push(match interaction {
competition::solution::Interaction::Custom(interaction) => eth::Interaction {
value: interaction.value,
target: interaction.target.0.into(),
call_data: interaction.call_data.clone(),
},
competition::solution::Interaction::Liquidity(liquidity) => {
liquidity_interaction(liquidity, contract)?
}
})
}

let tx = contract
.settle(
tokens,
clearing_prices,
trades.iter().map(codec::trade).collect(),
[
pre_interactions.iter().map(codec::interaction).collect(),
interactions.iter().map(codec::interaction).collect(),
post_interactions.iter().map(codec::interaction).collect(),
],
)
.into_inner();

// Encode the auction id into the calldata
let mut calldata = tx.data.unwrap().0;
calldata.extend(auction_id.to_be_bytes());

Ok(eth::Tx {
from: solution.solver().address(),
to: contract.address().into(),
input: calldata.into(),
value: Ether(0.into()),
access_list: Default::default(),
})
}

fn liquidity_interaction(
liquidity: &Liquidity,
settlement: &contracts::GPv2Settlement,
) -> Result<eth::Interaction, Error> {
// Todo account for slippage
let input = MaxInput(liquidity.input);
let output = ExactOutput(liquidity.output);

match liquidity.liquidity.kind.clone() {
liquidity::Kind::UniswapV2(pool) => pool
.swap(&input, &output, &settlement.address().into())
.ok(),
liquidity::Kind::UniswapV3(pool) => pool
.swap(&input, &output, &settlement.address().into())
.ok(),
liquidity::Kind::BalancerV2Stable(pool) => pool
.swap(&input, &output, &settlement.address().into())
.ok(),
liquidity::Kind::BalancerV2Weighted(pool) => pool
.swap(&input, &output, &settlement.address().into())
.ok(),
liquidity::Kind::Swapr(pool) => pool
.swap(&input, &output, &settlement.address().into())
.ok(),
liquidity::Kind::ZeroEx(limit_order) => limit_order.to_interaction(&input).ok(),
}
.ok_or(Error::InvalidInteractionExecution(liquidity.clone()))
}

fn approve(allowance: &Allowance) -> eth::Interaction {
let mut amount = [0u8; 32];
let selector = hex_literal::hex!("095ea7b3");
allowance.amount.to_big_endian(&mut amount);
eth::Interaction {
target: allowance.token.0.into(),
value: eth::U256::zero().into(),
// selector (4 bytes) + spender (20 byte address padded to 32 bytes) + amount (32 bytes)
call_data: [
selector.as_slice(),
[0; 12].as_slice(),
allowance.spender.0.as_bytes(),
&amount,
]
.concat()
.into(),
}
}

struct Trade {
sell_token_index: eth::U256,
buy_token_index: eth::U256,
receiver: eth::H160,
sell_amount: eth::U256,
buy_amount: eth::U256,
valid_to: u32,
app_data: Bytes<[u8; 32]>,
fee_amount: eth::U256,
flags: Flags,
executed_amount: eth::U256,
signature: Bytes<Vec<u8>>,
}

struct Price {
sell_token: eth::H160,
sell_price: eth::U256,
buy_token: eth::H160,
buy_price: eth::U256,
}

struct Flags {
side: order::Side,
partially_fillable: bool,
signing_scheme: order::signature::Scheme,
sell_token_balance: order::SellTokenBalance,
buy_token_balance: order::BuyTokenBalance,
}

mod codec {
use crate::domain::{competition::order, eth};

// cf. https://github.com/cowprotocol/contracts/blob/v1.5.0/src/contracts/libraries/GPv2Trade.sol#L16
type Trade = (
eth::U256, // sellTokenIndex
eth::U256, // buyTokenIndex
eth::H160, // receiver
eth::U256, // sellAmount
eth::U256, // buyAmount
u32, // validTo
ethcontract::Bytes<[u8; 32]>, // appData
eth::U256, // feeAmount
eth::U256, // flags
eth::U256, // executedAmount
ethcontract::Bytes<Vec<u8>>, // signature
);

pub fn trade(trade: &super::Trade) -> Trade {
(
trade.sell_token_index,
trade.buy_token_index,
trade.receiver,
trade.sell_amount,
trade.buy_amount,
trade.valid_to,
ethcontract::Bytes(trade.app_data.into()),
trade.fee_amount,
flags(&trade.flags),
trade.executed_amount,
ethcontract::Bytes(trade.signature.0.clone()),
)
}

// cf. https://github.com/cowprotocol/contracts/blob/v1.5.0/src/contracts/libraries/GPv2Trade.sol#L58
fn flags(flags: &super::Flags) -> eth::U256 {
let mut result = 0u8;
// The kind is encoded as 1 bit in position 0.
result |= match flags.side {
order::Side::Sell => 0b0,
order::Side::Buy => 0b1,
};
// The order fill kind is encoded as 1 bit in position 1.
result |= (flags.partially_fillable as u8) << 1;
// The order sell token balance is encoded as 2 bits in position 2.
result |= match flags.sell_token_balance {
order::SellTokenBalance::Erc20 => 0b00,
order::SellTokenBalance::External => 0b10,
order::SellTokenBalance::Internal => 0b11,
} << 2;
// The order buy token balance is encoded as 1 bit in position 4.
result |= match flags.buy_token_balance {
order::BuyTokenBalance::Erc20 => 0b0,
order::BuyTokenBalance::Internal => 0b1,
} << 4;
// The signing scheme is encoded as a 2 bits in position 5.
result |= match flags.signing_scheme {
order::signature::Scheme::Eip712 => 0b00,
order::signature::Scheme::EthSign => 0b01,
order::signature::Scheme::Eip1271 => 0b10,
order::signature::Scheme::PreSign => 0b11,
} << 5;
result.into()
}

// cf. https://github.com/cowprotocol/contracts/blob/v1.5.0/src/contracts/libraries/GPv2Interaction.sol#L9
type Interaction = (
eth::H160, // target
eth::U256, // value
ethcontract::Bytes<Vec<u8>>, // signature
);

pub fn interaction(interaction: &eth::Interaction) -> Interaction {
(
interaction.target.0,
interaction.value.0,
ethcontract::Bytes(interaction.call_data.0.clone()),
)
}
}

#[cfg(test)]
mod test {
use {super::*, hex_literal::hex};

#[test]
fn test_approve() {
let allowance = Allowance {
token: eth::H160::from_slice(&hex!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")).into(),
spender: eth::H160::from_slice(&hex!("000000000022D473030F116dDEE9F6B43aC78BA3"))
.into(),
amount: eth::U256::max_value(),
};
let interaction = approve(&allowance);
assert_eq!(
interaction.target,
eth::H160::from_slice(&hex!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")).into(),
);
assert_eq!(interaction.call_data.0.as_slice(), hex!("095ea7b3000000000000000000000000000000000022d473030f116ddee9f6b43ac78ba3ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"));
}
}
Loading

0 comments on commit 6bb059b

Please sign in to comment.