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

Simulated Token Balances for Verified Quotes #3147

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
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/contracts/artifacts/Solver.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions crates/contracts/artifacts/Spardose.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"abi":[{"inputs":[{"internalType":"address","name":"trader","type":"address"},{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"ensureBalance","outputs":[],"stateMutability":"nonpayable","type":"function"}],"bytecode":"0x608060405234801561001057600080fd5b506103ee806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063c56cca8314610030575b600080fd5b61004361003e3660046102fa565b610045565b005b6040517f70a0823100000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff8481166004830152600091908416906370a0823190602401602060405180830381865afa1580156100b5573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100d99190610336565b90508181106100e85750505050565b60006100f4828461034f565b905061011773ffffffffffffffffffffffffffffffffffffffff851686836101ae565b6101a7576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f74726164657220646f6573206e6f74206861766520656e6f7567682073656c6c60448201527f5f746f6b656e0000000000000000000000000000000000000000000000000000606482015260840160405180910390fd5b5050505050565b6040805173ffffffffffffffffffffffffffffffffffffffff8481166024830152604480830185905283518084039091018152606490920183526020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167fa9059cbb00000000000000000000000000000000000000000000000000000000179052915160009260609190871690610247908490610389565b6000604051808303816000865af19150503d8060008114610284576040519150601f19603f3d011682016040523d82523d6000602084013e610289565b606091505b5090935090508280156102a057506102a0816102aa565b9695505050505050565b60008151600014806102cb5750818060200190518101906102cb91906103b8565b92915050565b803573ffffffffffffffffffffffffffffffffffffffff811681146102f557600080fd5b919050565b60008060006060848603121561030f57600080fd5b610318846102d1565b9250610326602085016102d1565b9150604084013590509250925092565b60006020828403121561034857600080fd5b5051919050565b818103818111156102cb577f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000825160005b818110156103aa5760208186018101518583015201610390565b506000920191825250919050565b6000602082840312156103ca57600080fd5b815180151581146103da57600080fd5b939250505056fea164736f6c6343000811000a","deployedBytecode":"0x608060405234801561001057600080fd5b506004361061002b5760003560e01c8063c56cca8314610030575b600080fd5b61004361003e3660046102fa565b610045565b005b6040517f70a0823100000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff8481166004830152600091908416906370a0823190602401602060405180830381865afa1580156100b5573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100d99190610336565b90508181106100e85750505050565b60006100f4828461034f565b905061011773ffffffffffffffffffffffffffffffffffffffff851686836101ae565b6101a7576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f74726164657220646f6573206e6f74206861766520656e6f7567682073656c6c60448201527f5f746f6b656e0000000000000000000000000000000000000000000000000000606482015260840160405180910390fd5b5050505050565b6040805173ffffffffffffffffffffffffffffffffffffffff8481166024830152604480830185905283518084039091018152606490920183526020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167fa9059cbb00000000000000000000000000000000000000000000000000000000179052915160009260609190871690610247908490610389565b6000604051808303816000865af19150503d8060008114610284576040519150601f19603f3d011682016040523d82523d6000602084013e610289565b606091505b5090935090508280156102a057506102a0816102aa565b9695505050505050565b60008151600014806102cb5750818060200190518101906102cb91906103b8565b92915050565b803573ffffffffffffffffffffffffffffffffffffffff811681146102f557600080fd5b919050565b60008060006060848603121561030f57600080fd5b610318846102d1565b9250610326602085016102d1565b9150604084013590509250925092565b60006020828403121561034857600080fd5b5051919050565b818103818111156102cb577f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000825160005b818110156103aa5760208186018101518583015201610390565b506000920191825250919050565b6000602082840312156103ca57600080fd5b815180151581146103da57600080fd5b939250505056fea164736f6c6343000811000a","devdoc":{"methods":{}},"userdoc":{"methods":{}}}
2 changes: 1 addition & 1 deletion crates/contracts/artifacts/Swapper.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.

3 changes: 2 additions & 1 deletion crates/contracts/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -990,8 +990,9 @@ fn main() {
generate_contract("CowAmmUniswapV2PriceOracle");

// Support contracts used for trade and token simulations.
generate_contract("Trader");
generate_contract("Solver");
generate_contract("Spardose");
generate_contract("Trader");

// Support contracts used for various order simulations.
generate_contract("Balances");
Expand Down
1 change: 1 addition & 0 deletions crates/contracts/solidity/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ CONTRACTS := \
Signatures.sol \
SimulateCode.sol \
Solver.sol \
Spardose.sol \
Swapper.sol \
Trader.sol
ARTIFACTS := $(patsubst %.sol,$(ARTIFACTDIR)/%.json,$(CONTRACTS))
Expand Down
32 changes: 22 additions & 10 deletions crates/contracts/solidity/Solver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Interaction, Trade, ISettlement } from "./interfaces/ISettlement.sol";
import { Caller } from "./libraries/Caller.sol";
import { Math } from "./libraries/Math.sol";
import { SafeERC20 } from "./libraries/SafeERC20.sol";
import { Spardose } from "./Spardose.sol";
import { Trader } from "./Trader.sol";

/// @title A contract for impersonating a solver. This contract
Expand All @@ -16,7 +17,11 @@ import { Trader } from "./Trader.sol";
contract Solver {
using Caller for *;
using Math for *;
using SafeERC20 for *;

struct Mock {
bool enabled;
address spardose;
}

uint256 private _simulationOverhead;
uint256[] private _queriedBalances;
Expand All @@ -35,10 +40,10 @@ contract Solver {
/// @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 mockPreconditions - controls whether things like ETH wrapping
/// or setting allowance should be done on behalf of the
/// user to support quote verification even if the user didn't
/// wrap their ETH or set the necessary allowances yet.
/// @param mock - mocking configuration for the simulation; this controls
/// whether things like ETH wrapping, setting allowance and
/// pre-funding should be done on behalf of the user to support
/// quote verification for users who aren't ready to swap.
///
/// @return gasUsed - gas used for the `settle()` call
/// @return queriedBalances - list of balances stored during the simulation
Expand All @@ -51,30 +56,37 @@ contract Solver {
address[] calldata tokens,
address payable receiver,
bytes calldata settlementCall,
bool mockPreconditions
Mock memory mock
) 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.
if (mockPreconditions) {
if (mock.enabled) {
// 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
);

// Ensure that the user has sufficient sell token balance for the
// swap using balance overrides.
Spardose(mock.spardose).ensureBalance(trader, sellToken, sellAmount);
}

// 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
// contract.
receiver.call{value: 0}("");
{
(bool success,) = receiver.call{value: 0}("");
success;
}
Comment on lines +90 to +93
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 is just to silence a Solidity compiler warning.


// Store pre-settlement balances
_storeSettlementBalances(tokens, settlementContract);
Expand Down
34 changes: 34 additions & 0 deletions crates/contracts/solidity/Spardose.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import { IERC20 } from "./interfaces/IERC20.sol";
import { SafeERC20 } from "./libraries/SafeERC20.sol";

/// @title A piggy bank contract (Spardose is piggy bank in German)
/// @notice This contract account is used for pre-funding traders with tokens
/// for quote simulations. A separate contract is used (instead of overriding
/// the balance of the solver or trader directly) in order to interfere as
/// little as possible with the settlement.
contract Spardose {
using SafeERC20 for *;

/// @dev Ensures that the trader has at least `amount` tokens. If not, it
/// will transfer the difference to the trader.
///
/// @param trader - the address of the trader
/// @param token - the token to ensure a balance for
/// @param amount - the amount of `token` that the `trader` must hold.
function ensureBalance(address trader, address token, uint256 amount) external {
uint256 traderBalance = IERC20(token).balanceOf(trader);
if (traderBalance >= amount) {
// Nothing to do.
return;
}

uint256 difference = amount - traderBalance;
require(
IERC20(token).trySafeTransfer(trader, difference),
"trader does not have enough sell_token"
);
}
}
3 changes: 0 additions & 3 deletions crates/contracts/solidity/Trader.sol
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,6 @@ contract Trader {
IERC20(sellToken).safeApprove(address(settlementContract.vaultRelayer()), 0);
IERC20(sellToken).safeApprove(address(settlementContract.vaultRelayer()), type(uint256).max);
}

uint256 availableSellToken = IERC20(sellToken).balanceOf(address(this));
require(availableSellToken >= sellAmount, "trader does not have enough sell_token");
}

/// @dev Validate all signature requests. This makes "signing" CoW protocol
Expand Down
17 changes: 14 additions & 3 deletions crates/contracts/solidity/libraries/SafeERC20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,24 @@ import { Caller } from "./Caller.sol";
library SafeERC20 {
using Caller for *;

function trySafeTransfer(IERC20 self, address target, uint256 amount) internal returns (bool success) {
bytes memory cdata = abi.encodeCall(self.transfer, (target, amount));
bytes memory rdata;
(success, rdata) = address(self).call(cdata);
return success && check(rdata);
}

function safeApprove(IERC20 self, address target, uint256 amount) internal {
bytes memory cdata = abi.encodeCall(self.approve, (target, amount));
bytes memory rdata = address(self).doCall(cdata);
check(rdata, "SafeERC20: approval failed");
ensure(rdata, "SafeERC20: approval failed");
}

function check(bytes memory rdata) internal pure returns (bool ok) {
return rdata.length == 0 || abi.decode(rdata, (bool));
}

function check(bytes memory self, string memory message) internal pure {
require(self.length == 0 || abi.decode(self, (bool)), message);
function ensure(bytes memory rdata, string memory message) internal pure {
require(check(rdata), message);
}
}
1 change: 1 addition & 0 deletions crates/contracts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ pub mod support {
Signatures;
SimulateCode;
Solver;
Spardose;
Swapper;
Trader;
}
Expand Down
102 changes: 100 additions & 2 deletions crates/e2e/tests/e2e/quote_verification.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use {
bigdecimal::{BigDecimal, Zero},
e2e::setup::*,
ethcontract::H160,
e2e::{setup::*, tx},
ethcontract::{H160, U256},
ethrpc::Web3,
model::{
order::{BuyTokenDestination, OrderKind, SellTokenSource},
Expand All @@ -19,6 +19,12 @@ use {
std::{str::FromStr, sync::Arc},
};

#[tokio::test]
#[ignore]
async fn local_node_standard_verified_quote() {
run_test(standard_verified_quote).await;
}

#[tokio::test]
#[ignore]
async fn forked_node_bypass_verification_for_rfq_quotes() {
Expand All @@ -37,6 +43,51 @@ async fn local_node_verified_quote_for_settlement_contract() {
run_test(verified_quote_for_settlement_contract).await;
}

#[tokio::test]
#[ignore]
async fn local_node_verified_quote_with_simulated_balance() {
run_test(verified_quote_with_simulated_balance).await;
}

/// Verified quotes work as expected.
async fn standard_verified_quote(web3: Web3) {
tracing::info!("Setting up chain state.");
let mut onchain = OnchainComponents::deploy(web3).await;

let [solver] = onchain.make_solvers(to_wei(10)).await;
let [trader] = onchain.make_accounts(to_wei(1)).await;
let [token] = onchain
.deploy_tokens_with_weth_uni_v2_pools(to_wei(1_000), to_wei(1_000))
.await;

token.mint(trader.address(), to_wei(1)).await;
tx!(
trader.account(),
token.approve(onchain.contracts().allowance, to_wei(1))
);

tracing::info!("Starting services.");
let services = Services::new(&onchain).await;
services.start_protocol(solver).await;

// quote where the trader has no balances or approval set.
let response = services
.submit_quote(&OrderQuoteRequest {
from: trader.address(),
sell_token: token.address(),
buy_token: onchain.contracts().weth.address(),
side: OrderQuoteSide::Sell {
sell_amount: SellAmount::BeforeFee {
value: to_wei(1).try_into().unwrap(),
},
},
..Default::default()
})
.await
.unwrap();
assert!(response.verified);
}

/// The block number from which we will fetch state for the forked tests.
const FORK_BLOCK_MAINNET: u64 = 19796077;

Expand Down Expand Up @@ -208,3 +259,50 @@ async fn verified_quote_for_settlement_contract(web3: Web3) {
.unwrap();
assert!(response.verified);
}

/// Test that asserts that we can verify quotes for traders with simulated
/// balances.
async fn verified_quote_with_simulated_balance(web3: Web3) {
tracing::info!("Setting up chain state.");
let mut onchain = OnchainComponents::deploy(web3).await;

let [solver] = onchain.make_solvers(to_wei(10)).await;
let [trader] = onchain.make_accounts(to_wei(0)).await;
let [token] = onchain
.deploy_tokens_with_weth_uni_v2_pools(to_wei(1_000), to_wei(1_000))
.await;

tracing::info!("Starting services.");
let services = Services::new(&onchain).await;
services
.start_protocol_with_args(
ExtraServiceArgs {
api: vec![format!(
// The OpenZeppelin `ERC20Mintable` token uses a mapping in
// the first (0'th) storage slot for balances.
"--quote-token-balance-overrides={:?}@0",
token.address()
)],
..Default::default()
},
solver,
)
.await;

// quote where the trader has no balances or approval set.
let response = services
.submit_quote(&OrderQuoteRequest {
from: trader.address(),
sell_token: token.address(),
buy_token: onchain.contracts().weth.address(),
side: OrderQuoteSide::Sell {
sell_amount: SellAmount::BeforeFee {
value: to_wei(1).try_into().unwrap(),
},
},
..Default::default()
})
.await
.unwrap();
assert!(response.verified);
}
4 changes: 2 additions & 2 deletions crates/ethrpc/src/extensions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,12 @@ pub struct StateOverride {
/// Fake key-value mapping to override **all** slots in the account storage
/// before executing the call.
#[serde(skip_serializing_if = "Option::is_none")]
pub state: Option<HashMap<H256, U256>>,
pub state: Option<HashMap<H256, H256>>,

/// Fake key-value mapping to override **individual** slots in the account
/// storage before executing the call.
#[serde(skip_serializing_if = "Option::is_none")]
pub state_diff: Option<HashMap<H256, U256>>,
pub state_diff: Option<HashMap<H256, H256>>,
}

#[cfg(test)]
Expand Down
17 changes: 5 additions & 12 deletions crates/shared/src/code_simulation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use {
crate::tenderly_api::{SimulationKind, SimulationRequest, StateObject, TenderlyApi},
anyhow::{ensure, Context as _, Result},
contracts::errors::EthcontractErrorType,
ethcontract::{errors::ExecutionError, H256},
ethcontract::errors::ExecutionError,
ethrpc::{
extensions::{EthExt as _, StateOverride, StateOverrides},
Web3,
Expand Down Expand Up @@ -206,16 +206,7 @@ impl TryFrom<StateOverride> for StateObject {
Ok(StateObject {
balance: value.balance,
code: value.code,
storage: value.state_diff.map(|state_diff| {
state_diff
.into_iter()
.map(|(key, uint)| {
let mut value = H256::default();
uint.to_big_endian(&mut value.0);
(key, value)
})
.collect()
}),
storage: value.state_diff,
})
}
}
Expand Down Expand Up @@ -262,6 +253,7 @@ mod tests {
use {
super::*,
crate::{ethrpc::create_env_test_transport, tenderly_api::TenderlyHttpApi},
ethcontract::H256,
hex_literal::hex,
maplit::hashmap,
std::time::Duration,
Expand Down Expand Up @@ -393,7 +385,8 @@ mod tests {
hashmap! {
addr!("D533a949740bb3306d119CC777fa900bA034cd52") => StateOverride {
state_diff: Some(hashmap! {
H256(balance_slot) => 1.into()
H256(balance_slot) =>
H256(hex!("0000000000000000000000000000000000000000000000000000000000000001")),
}),
..Default::default()
},
Expand Down
24 changes: 14 additions & 10 deletions crates/shared/src/price_estimation/factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,17 +113,21 @@ impl<'a> PriceEstimatorFactory<'a> {
None => Arc::new(web3.clone()),
};

let balance_overrides = Arc::new(args.quote_token_balance_overrides.clone());

let verifier = TradeVerifier::new(
web3,
simulator,
components.code_fetcher.clone(),
network.block_stream.clone(),
network.settlement,
network.native_token,
args.quote_inaccuracy_limit.clone(),
)
.await?;

Ok(Some(Arc::new(
TradeVerifier::new(
web3,
simulator,
components.code_fetcher.clone(),
network.block_stream.clone(),
network.settlement,
network.native_token,
args.quote_inaccuracy_limit.clone(),
)
.await?,
verifier.with_balance_overrides(balance_overrides),
)))
}

Expand Down
Loading
Loading