Skip to content

Commit

Permalink
Enable Auto-detection of Token Balance Overrides (#3149)
Browse files Browse the repository at this point in the history
# Description

This PR is a follow up to #3148 and the final PR in the stack. It adds
configuration and glue code for enabling the automatic token balance
override detection introduced in the aforementioned PR.

# Changes

- A new `Arguments` type was created for setting balance override
configuration and creating the balance overrides instance.
- The old implementation of the `BalanceOverriding` trait was superseded
by a new one that accepts both a static configuration from the
`Arguments` as well as allows optionally for auto-detection
- The `Trader` Solidity implementation was changed to only wrap ETH if
the trader has sufficient balance. Attempting wrap more than the
trader's balance this would cause the simulation to revert without a
helpful revert message; and generally means that balance override
feature doesn't work for WETH. With the modified implementation, the
`Trader.prepareSwap` no longer reverts attempting to wrap more ETH than
the balance holds. Now, the `Spardose.requestFunds` will always get
called if the `Trader` does not have sufficient `sell_token` balance,
and it provides a more helpful message when balance overrides are
disabled:
    ```
2024-12-04T16:09:19.002Z WARN
request{id="2"}:estimator{name="test_quoter"}:
shared::price_estimation::trade_verifier: failed verification; returning
unverified estimate err=failed to simulate quote

    Caused by:
simulation reverted Some("revert: trader does not have enough sell
token") ...
    ```

## How to Test

An E2E test was modified to include balance override tests for both
configured and auto-detected tokens. Additionally, I added a test to
ensure that the ETH -> WETH wrapping functionality that previously
existed in quote verification simulation did not regress with this
change.

## Additional Notes

@MartinquaXD commented that the changes to `Trader` did not make much
sense. In order to try and address the comment, I moved some code
around. In particular, the `Trader` now calls `requestFunds` from the
`Spardose` during the `prepareSwap` call (instead of the `Solver`
calling `ensureBalance` for the trader when setting up the settlement).
IMO this makes a bit more sense conceptually (it is like the trader had
a pre-hook that would provide the required funds for the swap). The
contract changes are part of this commit:
7518ea9.

## Configuration

The balance overriding feature is **disabled by default**. There are two
relevant configuration options that need to be changed in order to
enable it:
- `QUOTE_TOKEN_BALANCE_OVERRIDES`: a set of `${ADDR}@${SLOT}`
configurations for manually enabling token balance overrides for
specific tokens (for example, for enabling WETH and USDC on Ethereum
Mainnet:
`0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2@3,0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48@9`).
- `QUOTE_AUTODETECT_TOKEN_BALANCE_OVERRIDES`: Set to `true` in order to
enable auto-detection of balance override slots.
  • Loading branch information
nlordell authored Dec 6, 2024
1 parent a88bfbd commit d58ffd7
Show file tree
Hide file tree
Showing 14 changed files with 399 additions and 177 deletions.
2 changes: 1 addition & 1 deletion crates/contracts/artifacts/Solver.json

Large diffs are not rendered by default.

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

12 changes: 2 additions & 10 deletions crates/contracts/solidity/Solver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ 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 Down Expand Up @@ -71,16 +70,9 @@ contract Solver {
settlementContract,
sellToken,
sellAmount,
nativeToken
nativeToken,
mock.spardose
);

// 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.
Expand Down
21 changes: 6 additions & 15 deletions crates/contracts/solidity/Spardose.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,12 @@ import { SafeERC20 } from "./libraries/SafeERC20.sol";
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.
/// @dev Request funds from the piggy bank to be transferred to the caller.
/// Reverts if the transfer fails.
///
/// @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);
/// @param token - the token request funds for
/// @param amount - the amount of `token` to transfer
function requestFunds(address token, uint256 amount) external {
IERC20(token).safeTransfer(msg.sender, amount);
}
}
36 changes: 29 additions & 7 deletions crates/contracts/solidity/Trader.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";

/// @title A contract for impersonating a trader.
contract Trader {
Expand Down Expand Up @@ -62,29 +63,36 @@ contract Trader {
receive() external payable {}

/// @dev Executes needed actions on behalf of the trader to make the trade possible.
/// (e.g. wrapping ETH and setting approvals)
/// (e.g. wrapping ETH, setting approvals, and funding the account)
/// @param settlementContract - pass in settlement contract because it does not have
/// a stable address in tests.
/// @param sellToken - token being sold by the trade
/// @param sellAmount - expected amount to be sold according to the quote
/// @param nativeToken - ERC20 version of the chain's native token
/// @param spardose - piggy bank for requesting additional funds
function prepareSwap(
ISettlement settlementContract,
address sellToken,
uint256 sellAmount,
address nativeToken
address nativeToken,
address spardose
) external {
require(!alreadyCalled(), "prepareSwap can only be called once");

if (sellToken == nativeToken) {
uint256 availableNativeToken = IERC20(sellToken).balanceOf(address(this));
if (availableNativeToken < sellAmount) {
uint256 amountToWrap = sellAmount - availableNativeToken;
require(address(this).balance >= amountToWrap, "not enough ETH to wrap");
// Simulate wrapping the missing `ETH` so the user doesn't have to spend gas
// on that just to get a quote. If they are happy with the quote and want to
// create an order they will actually have to do the wrapping, though.
INativeERC20(nativeToken).deposit{value: amountToWrap}();
// If the user has sufficient balance, simulate the wrapping the missing
// `ETH` so the user doesn't have to spend gas on that just to get a quote.
// If they are happy with the quote and want to create an order they will
// actually have to do the wrapping, though. Note that we don't attempt to
// wrap if the user doesn't have sufficient `ETH` balance, since that would
// revert. Instead, we fall-through so that we handle insufficient sell
// token balances uniformly for all tokens.
if (address(this).balance >= amountToWrap) {
INativeERC20(nativeToken).deposit{value: amountToWrap}();
}
}
}

Expand All @@ -99,6 +107,20 @@ contract Trader {
IERC20(sellToken).safeApprove(address(settlementContract.vaultRelayer()), 0);
IERC20(sellToken).safeApprove(address(settlementContract.vaultRelayer()), type(uint256).max);
}

// Ensure that the user has sufficient sell token balance. If not, request some
// funds from the Spardose (piggy bank) which will be available if balance
// overrides are enabled.
uint256 sellBalance = IERC20(sellToken).balanceOf(address(this));
if (sellBalance < sellAmount) {
try Spardose(spardose).requestFunds(sellToken, sellAmount - sellBalance) {}
catch {
// The trader does not have sufficient sell token balance, and the
// piggy bank pre-fund failed, as balance overrides are not available.
// Revert with a helpful message.
revert("trader does not have enough sell token");
}
}
}

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

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

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);
ensure(rdata, "SafeERC20: approval failed");
require(check(rdata), "SafeERC20: approval failed");
}

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

function ensure(bytes memory rdata, string memory message) internal pure {
require(check(rdata), message);
}
}
Loading

0 comments on commit d58ffd7

Please sign in to comment.