Skip to content

Commit

Permalink
Simulated Token Balances for Verified Quotes (#3147)
Browse files Browse the repository at this point in the history
# Description

This PR is a follow up to #3125 and uses the component introduced in the
aforementioned PR for setting up a simulated token balance using state
overrides in order for quote verification to work even when the trader
does not have sufficient balance.

# Changes

The way it works is by configuring known mapping slots for the
`mapping(address => uint256) balances` in ERC20 token contract
implementations and using this to compute the slot for overriding a
separate account's token balance (the `Spardose`), which can prefund the
trader during quote simualtions. We intentionally do not override the
trader's or solver's balance in order to not interfere with the
settlement process:

> Technically a solver could have private inventory they would like to
> use for the solution instead of transfering it to the trader.

_Posted by @MartinquaXD_

Note that the type of the state override changed slightly. This is
because it was wrong to begin with. Node implementations I tested with
(Geth and Anvil) expect both the slot and the value for state overrides
to be exactly 32-bytes long (so `H256`). I guess this feature of the
state override in the `ethrpc` crate was not used in the past and
therefore no one noticed 🤷.

- [x] Adds command line configuration for token balance overrides
- [x] Adds logic to the trades verifier to setup state overrides for
take token balances for the configured tokens
- [x] Adds logic in the settlement simulation to try and fund the trader
if they are missing balances and balance overrides are enabled
- [x] Fixes to the `StateOverride` type and serialization

### Test Plan

Added an E2E test that uses the new token balance override feature in
order to produce a verified quote for a trader with no balances. Note
that commenting out the API arguments causes the test to fail as
expected.


> This PR is the combination of two original PRs mentioned in #3125:
> * nlordell#1
> * nlordell#2.
  • Loading branch information
nlordell authored Dec 5, 2024
1 parent 546fa70 commit e75a277
Show file tree
Hide file tree
Showing 18 changed files with 353 additions and 64 deletions.
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":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"}],"bytecode":"0x608060405234801561001057600080fd5b50610364806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063c56cca8314610030575b600080fd5b61004361003e366004610277565b610057565b604051901515815260200160405180910390f35b6040517f70a0823100000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff848116600483015260009182918516906370a0823190602401602060405180830381865afa1580156100c8573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100ec91906102b3565b90508281106100ff576001915050610133565b600061010b82856102cc565b905061012e73ffffffffffffffffffffffffffffffffffffffff8616878361013a565b925050505b9392505050565b6040805173ffffffffffffffffffffffffffffffffffffffff8481166024830152604480830185905283518084039091018152606490920183526020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167fa9059cbb000000000000000000000000000000000000000000000000000000001790529151600092606091908716906101d3908490610306565b6000604051808303816000865af19150503d8060008114610210576040519150601f19603f3d011682016040523d82523d6000602084013e610215565b606091505b50909350905082801561012e575061012e8160008151600014806102485750818060200190518101906102489190610335565b92915050565b803573ffffffffffffffffffffffffffffffffffffffff8116811461027257600080fd5b919050565b60008060006060848603121561028c57600080fd5b6102958461024e565b92506102a36020850161024e565b9150604084013590509250925092565b6000602082840312156102c557600080fd5b5051919050565b81810381811115610248577f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000825160005b81811015610327576020818601810151858301520161030d565b506000920191825250919050565b60006020828403121561034757600080fd5b8151801515811461013357600080fdfea164736f6c6343000811000a","deployedBytecode":"0x608060405234801561001057600080fd5b506004361061002b5760003560e01c8063c56cca8314610030575b600080fd5b61004361003e366004610277565b610057565b604051901515815260200160405180910390f35b6040517f70a0823100000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff848116600483015260009182918516906370a0823190602401602060405180830381865afa1580156100c8573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100ec91906102b3565b90508281106100ff576001915050610133565b600061010b82856102cc565b905061012e73ffffffffffffffffffffffffffffffffffffffff8616878361013a565b925050505b9392505050565b6040805173ffffffffffffffffffffffffffffffffffffffff8481166024830152604480830185905283518084039091018152606490920183526020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167fa9059cbb000000000000000000000000000000000000000000000000000000001790529151600092606091908716906101d3908490610306565b6000604051808303816000865af19150503d8060008114610210576040519150601f19603f3d011682016040523d82523d6000602084013e610215565b606091505b50909350905082801561012e575061012e8160008151600014806102485750818060200190518101906102489190610335565b92915050565b803573ffffffffffffffffffffffffffffffffffffffff8116811461027257600080fd5b919050565b60008060006060848603121561028c57600080fd5b6102958461024e565b92506102a36020850161024e565b9150604084013590509250925092565b6000602082840312156102c557600080fd5b5051919050565b81810381811115610248577f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000825160005b81811015610327576020818601810151858301520161030d565b506000920191825250919050565b60006020828403121561034757600080fd5b8151801515811461013357600080fdfea164736f6c6343000811000a","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
36 changes: 26 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,41 @@ 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. In case
// balance overrides are enabled, the Spardose will fund the trader
// with simulated balances.
require(
Spardose(mock.spardose).ensureBalance(trader, sellToken, sellAmount),
"trader does not have enough sell token"
);
}

// 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;
}

// Store pre-settlement balances
_storeSettlementBalances(tokens, settlementContract);
Expand Down
32 changes: 32 additions & 0 deletions crates/contracts/solidity/Spardose.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// 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 try and 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
///
/// @return success - the `trader`'s `token` balance is more than `amount`
function ensureBalance(address trader, address token, uint256 amount) external returns (bool success) {
uint256 traderBalance = IERC20(token).balanceOf(trader);
if (traderBalance >= amount) {
return true;
}

uint256 difference = amount - traderBalance;
return IERC20(token).trySafeTransfer(trader, difference);
}
}
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
Loading

0 comments on commit e75a277

Please sign in to comment.