diff --git a/Cargo.lock b/Cargo.lock index aff7bcb9c6..30af1e1192 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2204,9 +2204,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "h2" -version = "0.3.20" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" +checksum = "4fbd2820c5e49886948654ab546d0688ff24530286bdcf8fca3cefb16d4618eb" dependencies = [ "bytes", "fnv", @@ -2214,7 +2214,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 1.9.3", + "indexmap 2.2.5", "slab", "tokio", "tokio-util 0.7.8", @@ -2678,9 +2678,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.147" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libm" @@ -2808,9 +2808,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.8" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "wasi", @@ -3081,11 +3081,11 @@ checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "openssl" -version = "0.10.56" +version = "0.10.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "729b745ad4a5575dd06a3e1af1414bd330ee561c01b3899eb584baeaa8def17e" +checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.3.3", "cfg-if", "foreign-types", "libc", @@ -3113,9 +3113,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.91" +version = "0.9.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "866b5f16f90776b9bb8dc1e1802ac6f0513de3a7a7465867bfbc563dc737faac" +checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" dependencies = [ "cc", "libc", @@ -3239,7 +3239,7 @@ checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.3.5", "smallvec", "windows-targets", ] @@ -3536,6 +3536,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "refunder" version = "0.1.0" @@ -3779,9 +3788,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.101.3" +version = "0.101.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "261e9e0888cba427c3316e6322805653c9425240b6fd96cee7cb671ab70ab8d0" +checksum = "7d93931baf2d282fff8d3a532bbfd7653f734643161b87e3e01e59a04439bf0d" dependencies = [ "ring", "untrusted", @@ -4572,7 +4581,7 @@ checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ "cfg-if", "fastrand 2.0.0", - "redox_syscall", + "redox_syscall 0.3.5", "rustix", "windows-sys", ] @@ -5106,6 +5115,12 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.87" @@ -5215,9 +5230,9 @@ dependencies = [ [[package]] name = "webpki" -version = "0.22.0" +version = "0.22.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +checksum = "07ecc0cd7cac091bf682ec5efa18b1cff79d617b84181f38b3951dbe135f607f" dependencies = [ "ring", "untrusted", @@ -5225,9 +5240,13 @@ dependencies = [ [[package]] name = "whoami" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" +checksum = "0fec781d48b41f8163426ed18e8fc2864c12937df9ce54c88ede7bd47270893e" +dependencies = [ + "redox_syscall 0.4.1", + "wasite", +] [[package]] name = "winapi" diff --git a/crates/contracts/artifacts/GasHog.json b/crates/contracts/artifacts/GasHog.json new file mode 100644 index 0000000000..7cce24e6d4 --- /dev/null +++ b/crates/contracts/artifacts/GasHog.json @@ -0,0 +1 @@ +{"abi":[{"inputs":[{"internalType":"contract ERC20","name":"token","type":"address"},{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"approve","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"order","type":"bytes32"},{"internalType":"bytes","name":"signature","type":"bytes"}],"name":"isValidSignature","outputs":[{"internalType":"bytes4","name":"","type":"bytes4"}],"stateMutability":"view","type":"function"}],"bytecode":"0x608060405234801561001057600080fd5b50610318806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80631626ba7e1461003b578063e1f21c6714610083575b600080fd5b61004e6100493660046101d0565b610098565b6040517fffffffff00000000000000000000000000000000000000000000000000000000909116815260200160405180910390f35b610096610091366004610271565b610143565b005b6000805a905060006100ac848601866102b2565b90507fce7d7369855be79904099402d83db6d6ab8840dcd5c086e062cd1ca0c8111dfc5b815a6100dc90856102cb565b101561010b576040805160208101839052016040516020818303038152906040528051906020012090506100d0565b86810361011757600080fd5b507f1626ba7e000000000000000000000000000000000000000000000000000000009695505050505050565b6040517f095ea7b300000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff83811660048301526024820183905284169063095ea7b390604401600060405180830381600087803b1580156101b357600080fd5b505af11580156101c7573d6000803e3d6000fd5b50505050505050565b6000806000604084860312156101e557600080fd5b83359250602084013567ffffffffffffffff8082111561020457600080fd5b818601915086601f83011261021857600080fd5b81358181111561022757600080fd5b87602082850101111561023957600080fd5b6020830194508093505050509250925092565b73ffffffffffffffffffffffffffffffffffffffff8116811461026e57600080fd5b50565b60008060006060848603121561028657600080fd5b83356102918161024c565b925060208401356102a18161024c565b929592945050506040919091013590565b6000602082840312156102c457600080fd5b5035919050565b81810381811115610305577f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b9291505056fea164736f6c6343000811000a","deployedBytecode":"0x608060405234801561001057600080fd5b50600436106100365760003560e01c80631626ba7e1461003b578063e1f21c6714610083575b600080fd5b61004e6100493660046101d0565b610098565b6040517fffffffff00000000000000000000000000000000000000000000000000000000909116815260200160405180910390f35b610096610091366004610271565b610143565b005b6000805a905060006100ac848601866102b2565b90507fce7d7369855be79904099402d83db6d6ab8840dcd5c086e062cd1ca0c8111dfc5b815a6100dc90856102cb565b101561010b576040805160208101839052016040516020818303038152906040528051906020012090506100d0565b86810361011757600080fd5b507f1626ba7e000000000000000000000000000000000000000000000000000000009695505050505050565b6040517f095ea7b300000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff83811660048301526024820183905284169063095ea7b390604401600060405180830381600087803b1580156101b357600080fd5b505af11580156101c7573d6000803e3d6000fd5b50505050505050565b6000806000604084860312156101e557600080fd5b83359250602084013567ffffffffffffffff8082111561020457600080fd5b818601915086601f83011261021857600080fd5b81358181111561022757600080fd5b87602082850101111561023957600080fd5b6020830194508093505050509250925092565b73ffffffffffffffffffffffffffffffffffffffff8116811461026e57600080fd5b50565b60008060006060848603121561028657600080fd5b83356102918161024c565b925060208401356102a18161024c565b929592945050506040919091013590565b6000602082840312156102c457600080fd5b5035919050565b81810381811115610305577f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b9291505056fea164736f6c6343000811000a","devdoc":{"methods":{}},"userdoc":{"methods":{}}} diff --git a/crates/contracts/build.rs b/crates/contracts/build.rs index 1b0efa8316..8715c9aa10 100644 --- a/crates/contracts/build.rs +++ b/crates/contracts/build.rs @@ -691,6 +691,9 @@ fn main() { // Test Contract for incrementing arbitrary counters. generate_contract("Counter"); + + // Test Contract for using up a specified amount of gas. + generate_contract("GasHog"); } fn generate_contract(name: &str) { diff --git a/crates/contracts/solidity/Makefile b/crates/contracts/solidity/Makefile index e7845b64de..c0d94e220e 100644 --- a/crates/contracts/solidity/Makefile +++ b/crates/contracts/solidity/Makefile @@ -19,7 +19,7 @@ CONTRACTS := \ Trader.sol ARTIFACTS := $(patsubst %.sol,$(ARTIFACTDIR)/%.json,$(CONTRACTS)) -TEST_CONTRACTS := Counter.sol +TEST_CONTRACTS := Counter.sol GasHog.sol TEST_ARTIFACTS := $(patsubst %.sol,$(ARTIFACTDIR)/%.json,$(TEST_CONTRACTS)) .PHONY: artifacts @@ -58,11 +58,11 @@ $(TARGETDIR)/%.abi: %.sol $(SOLC) \ $(SOLFLAGS) -o /target $< -$(TARGETDIR)/%.abi: test/%.sol +$(TARGETDIR)/%.abi: tests/%.sol @mkdir -p $(TARGETDIR) @echo solc $(SOLFLAGS) -o /target $(notdir $<) @$(DOCKER) run -it --rm \ - -v "$(abspath .)/test:/contracts" -w "/contracts" \ + -v "$(abspath .)/tests:/contracts" -w "/contracts" \ -v "$(abspath $(TARGETDIR)):/target" \ $(SOLC) \ $(SOLFLAGS) -o /target $(notdir $<) diff --git a/crates/contracts/solidity/tests/GasHog.sol b/crates/contracts/solidity/tests/GasHog.sol new file mode 100644 index 0000000000..edab6f6bbc --- /dev/null +++ b/crates/contracts/solidity/tests/GasHog.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +interface ERC20 { + function approve(address spender, uint amount) external; +} + +/// @title Helper contract to simulate gas intensive ERC1271 signatures +contract GasHog { + function isValidSignature(bytes32 order, bytes calldata signature) public view returns (bytes4) { + uint start = gasleft(); + uint target = abi.decode(signature, (uint)); + bytes32 hash = keccak256("go"); + while (start - gasleft() < target) { + hash = keccak256(abi.encode(hash)); + } + // Assert the impossible so that the compiler doesn't optimise the loop away + require(hash != order); + + // ERC1271 Magic Value + return 0x1626ba7e; + } + + function approve(ERC20 token, address spender, uint amount) external { + token.approve(spender, amount); + } +} diff --git a/crates/contracts/src/lib.rs b/crates/contracts/src/lib.rs index 8dc2e55d34..c7b095b03d 100644 --- a/crates/contracts/src/lib.rs +++ b/crates/contracts/src/lib.rs @@ -86,6 +86,7 @@ pub mod support { pub mod test { include_contracts! { Counter; + GasHog; } } diff --git a/crates/e2e/src/setup/services.rs b/crates/e2e/src/setup/services.rs index 54d39548d3..0a91f2a00a 100644 --- a/crates/e2e/src/setup/services.rs +++ b/crates/e2e/src/setup/services.rs @@ -62,6 +62,12 @@ impl ServicesBuilder { } } +#[derive(Default)] +pub struct ExtraServiceArgs { + pub api: Vec, + pub autopilot: Vec, +} + /// Wrapper over offchain services. /// Exposes various utility methods for tests. pub struct Services<'a> { @@ -103,11 +109,6 @@ impl<'a> Services<'a> { "--baseline-sources=None".to_string(), "--network-block-interval=1s".to_string(), "--solver-competition-auth=super_secret_key".to_string(), - format!( - "--custom-univ2-baseline-sources={:?}|{:?}", - self.contracts.uniswap_v2_router.address(), - self.contracts.default_pool_code(), - ), format!( "--settlement-contract-address={:?}", self.contracts.gp_settlement.address() @@ -168,6 +169,11 @@ impl<'a> Services<'a> { /// Starts a basic version of the protocol with a single baseline solver. pub async fn start_protocol(&self, solver: TestAccount) { + self.start_protocol_with_args(Default::default(), solver) + .await; + } + + pub async fn start_protocol_with_args(&self, args: ExtraServiceArgs, solver: TestAccount) { let solver_endpoint = colocation::start_baseline_solver(self.contracts.weth.address()).await; colocation::start_driver( @@ -180,15 +186,26 @@ impl<'a> Services<'a> { ); self.start_autopilot( None, - vec![ - "--drivers=test_solver|http://localhost:11088/test_solver".to_string(), - "--price-estimation-drivers=test_quoter|http://localhost:11088/test_solver" - .to_string(), - ], + [ + vec![ + "--drivers=test_solver|http://localhost:11088/test_solver".to_string(), + "--price-estimation-drivers=test_quoter|http://localhost:11088/test_solver" + .to_string(), + ], + args.autopilot, + ] + .concat(), ); - self.start_api(vec![ - "--price-estimation-drivers=test_quoter|http://localhost:11088/test_solver".to_string(), - ]) + self.start_api( + [ + vec![ + "--price-estimation-drivers=test_quoter|http://localhost:11088/test_solver" + .to_string(), + ], + args.api, + ] + .concat(), + ) .await; } diff --git a/crates/e2e/tests/e2e/hooks.rs b/crates/e2e/tests/e2e/hooks.rs index 709f623c4c..519bcdae25 100644 --- a/crates/e2e/tests/e2e/hooks.rs +++ b/crates/e2e/tests/e2e/hooks.rs @@ -11,6 +11,7 @@ use { order::{OrderCreation, OrderCreationAppData, OrderKind}, signature::{hashed_eip712_message, EcdsaSigningScheme, Signature}, }, + reqwest::StatusCode, secp256k1::SecretKey, serde_json::json, shared::ethrpc::Web3, @@ -35,6 +36,65 @@ async fn local_node_partial_fills() { run_test(partial_fills).await; } +#[tokio::test] +#[ignore] +async fn local_node_gas_limit() { + run_test(gas_limit).await; +} + +async fn gas_limit(web3: Web3) { + let mut onchain = OnchainComponents::deploy(web3).await; + + let [solver] = onchain.make_solvers(to_wei(1)).await; + let [trader] = onchain.make_accounts(to_wei(1)).await; + let cow = onchain + .deploy_cow_weth_pool(to_wei(1_000_000), to_wei(1_000), to_wei(1_000)) + .await; + + // Fund trader accounts and approve relayer + cow.fund(trader.address(), to_wei(5)).await; + tx!( + trader.account(), + cow.approve(onchain.contracts().allowance, to_wei(5)) + ); + + let services = Services::new(onchain.contracts()).await; + services.start_protocol(solver).await; + + let order = OrderCreation { + sell_token: cow.address(), + sell_amount: to_wei(4), + buy_token: onchain.contracts().weth.address(), + buy_amount: to_wei(3), + valid_to: model::time::now_in_epoch_seconds() + 300, + kind: OrderKind::Sell, + app_data: OrderCreationAppData::Full { + full: json!({ + "metadata": { + "hooks": { + "pre": [Hook { + target: trader.address(), + call_data: Default::default(), + gas_limit: 10_000_000, + }], + "post": [], + }, + }, + }) + .to_string(), + }, + ..Default::default() + } + .sign( + EcdsaSigningScheme::Eip712, + &onchain.contracts().domain_separator, + SecretKeyRef::from(&SecretKey::from_slice(trader.private_key()).unwrap()), + ); + let error = services.create_order(&order).await.unwrap_err(); + assert_eq!(error.0, StatusCode::BAD_REQUEST); + assert!(error.1.contains("TooMuchGas")); +} + async fn allowance(web3: Web3) { let mut onchain = OnchainComponents::deploy(web3).await; diff --git a/crates/e2e/tests/e2e/main.rs b/crates/e2e/tests/e2e/main.rs index ec4b2dd5e2..fab03c65a5 100644 --- a/crates/e2e/tests/e2e/main.rs +++ b/crates/e2e/tests/e2e/main.rs @@ -27,5 +27,6 @@ mod smart_contract_orders; mod solver_competition; mod submission; mod tracking_insufficient_funds; +mod uncovered_order; mod univ2; mod vault_balances; diff --git a/crates/e2e/tests/e2e/smart_contract_orders.rs b/crates/e2e/tests/e2e/smart_contract_orders.rs index 30d000d192..d8c80e30b2 100644 --- a/crates/e2e/tests/e2e/smart_contract_orders.rs +++ b/crates/e2e/tests/e2e/smart_contract_orders.rs @@ -1,10 +1,14 @@ use { - e2e::setup::{safe::Safe, *}, + e2e::{ + setup::{safe::Safe, *}, + tx, + }, ethcontract::{Bytes, H160, U256}, model::{ order::{OrderCreation, OrderCreationAppData, OrderKind, OrderStatus, OrderUid}, signature::Signature, }, + reqwest::StatusCode, shared::ethrpc::Web3, }; @@ -14,6 +18,12 @@ async fn local_node_smart_contract_orders() { run_test(smart_contract_orders).await; } +#[tokio::test] +#[ignore] +async fn local_node_max_gas_limit() { + run_test(erc1271_gas_limit).await; +} + async fn smart_contract_orders(web3: Web3) { let mut onchain = OnchainComponents::deploy(web3.clone()).await; @@ -149,3 +159,55 @@ async fn smart_contract_orders(web3: Web3) { .expect("Couldn't fetch native token balance"); assert_eq!(balance, U256::from(7_975_363_406_512_003_608_u128)); } + +async fn erc1271_gas_limit(web3: Web3) { + let mut onchain = OnchainComponents::deploy(web3.clone()).await; + + let [solver] = onchain.make_solvers(to_wei(1)).await; + let trader = contracts::test::GasHog::builder(&web3) + .deploy() + .await + .unwrap(); + + let cow = onchain + .deploy_cow_weth_pool(to_wei(1_000_000), to_wei(1_000), to_wei(1_000)) + .await; + + // Fund trader accounts and approve relayer + cow.fund(trader.address(), to_wei(5)).await; + tx!( + solver.account(), + trader.approve(cow.address(), onchain.contracts().allowance, to_wei(10)) + ); + + let services = Services::new(onchain.contracts()).await; + services + .start_protocol_with_args( + ExtraServiceArgs { + api: vec!["--max-gas-per-order=1000000".to_string()], + ..Default::default() + }, + solver, + ) + .await; + + // Use 1M gas units during signature verification + let mut signature = [0; 32]; + U256::exp10(6).to_big_endian(&mut signature); + + let order = OrderCreation { + sell_token: cow.address(), + sell_amount: to_wei(4), + buy_token: onchain.contracts().weth.address(), + buy_amount: to_wei(3), + valid_to: model::time::now_in_epoch_seconds() + 300, + kind: OrderKind::Sell, + signature: Signature::Eip1271(signature.to_vec()), + from: Some(trader.address()), + ..Default::default() + }; + + let error = services.create_order(&order).await.unwrap_err(); + assert_eq!(error.0, StatusCode::BAD_REQUEST); + assert!(error.1.contains("TooMuchGas")); +} diff --git a/crates/e2e/tests/e2e/uncovered_order.rs b/crates/e2e/tests/e2e/uncovered_order.rs new file mode 100644 index 0000000000..4d82a65778 --- /dev/null +++ b/crates/e2e/tests/e2e/uncovered_order.rs @@ -0,0 +1,84 @@ +use { + e2e::{setup::*, tx, tx_value}, + ethcontract::U256, + model::{ + order::{OrderCreation, OrderKind}, + signature::EcdsaSigningScheme, + }, + secp256k1::SecretKey, + shared::ethrpc::Web3, + web3::signing::SecretKeyRef, +}; + +#[tokio::test] +#[ignore] +async fn local_node_uncovered_order() { + run_test(test).await; +} + +/// Tests that a user can already create an order if they only have +/// 1 wei of the sell token and later fund their account to get the +/// order executed. +async fn test(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(10)).await; + let [token] = onchain + .deploy_tokens_with_weth_uni_v2_pools(to_wei(1_000), to_wei(1_000)) + .await; + let weth = &onchain.contracts().weth; + + tx!( + trader.account(), + weth.approve(onchain.contracts().allowance, to_wei(3)) + ); + + tracing::info!("Starting services."); + let services = Services::new(onchain.contracts()).await; + services.start_protocol(solver).await; + + tracing::info!("Placing order with 0 sell tokens"); + let order = OrderCreation { + sell_token: weth.address(), + sell_amount: to_wei(2), + fee_amount: 0.into(), + buy_token: token.address(), + buy_amount: to_wei(1), + valid_to: model::time::now_in_epoch_seconds() + 300, + kind: OrderKind::Sell, + partially_fillable: false, + ..Default::default() + } + .sign( + EcdsaSigningScheme::Eip712, + &onchain.contracts().domain_separator, + SecretKeyRef::from(&SecretKey::from_slice(trader.private_key()).unwrap()), + ); + // This order can't be created because we require the trader + // to have at least 1 wei of sell tokens. + services.create_order(&order).await.unwrap_err(); + + tracing::info!("Placing order with 1 wei of sell_tokens"); + tx_value!(trader.account(), 1.into(), weth.deposit()); + // Now that the trader has some funds they are able to create + // an order (even if it exceeds their current balance). + services.create_order(&order).await.unwrap(); + + tracing::info!("Deposit ETH to make order executable"); + tx_value!(trader.account(), to_wei(2), weth.deposit()); + + tracing::info!("Waiting for order to show up in auction"); + wait_for_condition(TIMEOUT, || async { services.solvable_orders().await == 1 }) + .await + .unwrap(); + + // Drive solution + tracing::info!("Waiting for trade."); + wait_for_condition(TIMEOUT, || async { services.solvable_orders().await == 0 }) + .await + .unwrap(); + let balance_after = weth.balance_of(trader.address()).call().await.unwrap(); + assert_eq!(U256::one(), balance_after); +} diff --git a/crates/observe/Cargo.toml b/crates/observe/Cargo.toml index 5bb442d49b..4ae1864f4d 100644 --- a/crates/observe/Cargo.toml +++ b/crates/observe/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" license = "MIT OR Apache-2.0" [dependencies] -atty = "0.2" +atty = "0.2.14" futures = { workspace = true } once_cell = { workspace = true } pin-project-lite = "0.2" diff --git a/crates/orderbook/openapi.yml b/crates/orderbook/openapi.yml index a6e5cab86a..8fc203b863 100644 --- a/crates/orderbook/openapi.yml +++ b/crates/orderbook/openapi.yml @@ -1187,6 +1187,7 @@ components: ZeroAmount, IncompatibleSigningScheme, TooManyLimitOrders, + TooMuchGas, UnsupportedBuyTokenDestination, UnsupportedSellTokenSource, UnsupportedOrderType, diff --git a/crates/orderbook/src/api/post_order.rs b/crates/orderbook/src/api/post_order.rs index cf68214b49..23a5a4de9f 100644 --- a/crates/orderbook/src/api/post_order.rs +++ b/crates/orderbook/src/api/post_order.rs @@ -229,6 +229,10 @@ impl IntoWarpReply for ValidationErrorWrapper { error("TooManyLimitOrders", "Too many limit orders"), StatusCode::BAD_REQUEST, ), + ValidationError::TooMuchGas => with_status( + error("TooMuchGas", "Executing order requires too many gas units"), + StatusCode::BAD_REQUEST, + ), ValidationError::Other(err) => { tracing::error!(?err, "ValidationErrorWrapper"); diff --git a/crates/orderbook/src/arguments.rs b/crates/orderbook/src/arguments.rs index 0ba214617f..c33442c178 100644 --- a/crates/orderbook/src/arguments.rs +++ b/crates/orderbook/src/arguments.rs @@ -143,6 +143,10 @@ pub struct Arguments { /// Set the maximum size in bytes of order app data. #[clap(long, env, default_value = "8192")] pub app_data_size_limit: usize, + + /// The maximum gas amount a single order can use for getting settled. + #[clap(long, env, default_value = "8000000")] + pub max_gas_per_order: u64, } impl std::fmt::Display for Arguments { @@ -173,6 +177,7 @@ impl std::fmt::Display for Arguments { hooks_contract_address, app_data_size_limit, db_url, + max_gas_per_order, } = self; write!(f, "{}", shared)?; @@ -237,6 +242,7 @@ impl std::fmt::Display for Arguments { &hooks_contract_address.map(|a| format!("{a:?}")), )?; writeln!(f, "app_data_size_limit: {}", app_data_size_limit)?; + writeln!(f, "max_gas_per_order: {}", max_gas_per_order)?; Ok(()) } diff --git a/crates/orderbook/src/run.rs b/crates/orderbook/src/run.rs index e4dc44c04f..ae0d41b37c 100644 --- a/crates/orderbook/src/run.rs +++ b/crates/orderbook/src/run.rs @@ -463,6 +463,7 @@ pub async fn run(args: Arguments) { Arc::new(CachedCodeFetcher::new(Arc::new(web3.clone()))), app_data_validator.clone(), args.shared.market_orders_deprecation_date, + args.max_gas_per_order, ) .with_verified_quotes(args.price_estimation.trade_simulator.is_some()), ); diff --git a/crates/shared/src/order_quoting.rs b/crates/shared/src/order_quoting.rs index 7644026f25..d668cd0af7 100644 --- a/crates/shared/src/order_quoting.rs +++ b/crates/shared/src/order_quoting.rs @@ -60,7 +60,7 @@ impl QuoteParameters { } } - fn additional_cost(&self) -> u64 { + pub fn additional_cost(&self) -> u64 { self.signing_scheme .additional_gas_amount() .saturating_add(self.additional_gas) @@ -279,7 +279,7 @@ impl QuoteSearchParameters { } /// Returns additional gas costs incurred by the quote. - fn additional_cost(&self) -> u64 { + pub fn additional_cost(&self) -> u64 { self.signing_scheme .additional_gas_amount() .saturating_add(self.additional_gas) diff --git a/crates/shared/src/order_validation.rs b/crates/shared/src/order_validation.rs index 26b9c59041..7c3e363e77 100644 --- a/crates/shared/src/order_validation.rs +++ b/crates/shared/src/order_validation.rs @@ -162,6 +162,7 @@ pub enum ValidationError { ZeroAmount, IncompatibleSigningScheme, TooManyLimitOrders, + TooMuchGas, Other(anyhow::Error), } @@ -254,6 +255,7 @@ pub struct OrderValidator { app_data_validator: Validator, request_verified_quotes: bool, market_orders_deprecation_date: Option>, + max_gas_per_order: u64, } #[derive(Debug, Eq, PartialEq, Default)] @@ -325,6 +327,7 @@ impl OrderValidator { code_fetcher: Arc, app_data_validator: Validator, market_orders_deprecation_date: Option>, + max_gas_per_order: u64, ) -> Self { Self { native_token, @@ -342,6 +345,7 @@ impl OrderValidator { app_data_validator, request_verified_quotes: false, market_orders_deprecation_date, + max_gas_per_order, } } @@ -587,8 +591,6 @@ impl OrderValidating for OrderValidator { verification, }; - let min_balance = minimum_balance(&data).ok_or(ValidationError::SellAmountOverflow)?; - // Fast path to check if transfer is possible with a single node query. // If not, run extra queries for additional information. match self @@ -600,7 +602,7 @@ impl OrderValidating for OrderValidator { source: data.sell_token_balance, interactions: app_data.interactions.pre.clone(), }, - min_balance, + MINIMUM_BALANCE, ) .await { @@ -727,6 +729,14 @@ impl OrderValidating for OrderValidator { } }; + if quote.as_ref().is_some_and(|quote| { + // Quoted gas does not include additional gas for hooks nor ERC1271 signatures + quote.data.fee_parameters.gas_amount as u64 + quote_parameters.additional_cost() + > self.max_gas_per_order + }) { + return Err(ValidationError::TooMuchGas); + } + let order = Order { metadata: OrderMetadata { owner, @@ -814,17 +824,11 @@ fn has_same_buy_and_sell_token(order: &PreOrderData, native_token: &WETH9) -> bo } /// Min balance user must have in sell token for order to be accepted. -/// -/// None when addition overflows. -fn minimum_balance(order: &OrderData) -> Option { - // TODO: We might even want to allow 0 balance for partially fillable but we - // require balance for fok limit orders too so this make some sense and protects - // against accidentally creating order for token without balance. - if order.partially_fillable { - return Some(1.into()); - } - order.sell_amount.checked_add(order.fee_amount) -} +// All orders can be placed without having the full sell balance. +// A minimum, of 1 atom is still required as a spam protection measure. +// TODO: ideally, we should keep the full balance enforcement for SWAPs, +// but given all orders are LIMIT now, this is harder to do. +const MINIMUM_BALANCE: U256 = U256::one(); // 1 atom of a token /// Retrieves the quote for an order that is being created and verify that its /// fee is sufficient. @@ -976,22 +980,6 @@ mod tests { std::str::FromStr, }; - #[test] - fn minimum_balance_() { - let order = OrderData { - sell_amount: U256::MAX, - fee_amount: U256::from(1), - ..Default::default() - }; - assert_eq!(minimum_balance(&order), None); - let order = OrderData { - sell_amount: U256::from(1), - fee_amount: U256::from(1), - ..Default::default() - }; - assert_eq!(minimum_balance(&order), Some(U256::from(2))); - } - #[test] fn detects_orders_with_same_buy_and_sell_token() { let native_token = dummy_contract!(WETH9, [0xef; 20]); @@ -1060,6 +1048,7 @@ mod tests { Arc::new(MockCodeFetching::new()), Default::default(), None, + u64::MAX, ); let result = validator .partial_validate(PreOrderData { @@ -1207,6 +1196,7 @@ mod tests { Arc::new(MockCodeFetching::new()), Default::default(), None, + u64::MAX, ); let order = || PreOrderData { valid_to: time::now_in_epoch_seconds() @@ -1295,6 +1285,7 @@ mod tests { Arc::new(MockCodeFetching::new()), Default::default(), None, + u64::MAX, ); let creation = OrderCreation { @@ -1499,6 +1490,7 @@ mod tests { Arc::new(MockCodeFetching::new()), Default::default(), None, + u64::MAX, ); let creation = OrderCreation { @@ -1570,6 +1562,7 @@ mod tests { Arc::new(MockCodeFetching::new()), Default::default(), None, + u64::MAX, ); let creation = OrderCreation { @@ -1626,6 +1619,7 @@ mod tests { Arc::new(MockCodeFetching::new()), Default::default(), None, + u64::MAX, ); let order = OrderCreation { valid_to: time::now_in_epoch_seconds() + 2, @@ -1684,6 +1678,7 @@ mod tests { Arc::new(MockCodeFetching::new()), Default::default(), None, + u64::MAX, ); let order = OrderCreation { valid_to: time::now_in_epoch_seconds() + 2, @@ -1741,6 +1736,7 @@ mod tests { Arc::new(MockCodeFetching::new()), Default::default(), None, + u64::MAX, ); let order = OrderCreation { valid_to: time::now_in_epoch_seconds() + 2, @@ -1793,6 +1789,7 @@ mod tests { Arc::new(MockCodeFetching::new()), Default::default(), None, + u64::MAX, ); let order = OrderCreation { valid_to: time::now_in_epoch_seconds() + 2, @@ -1847,6 +1844,7 @@ mod tests { Arc::new(MockCodeFetching::new()), Default::default(), None, + u64::MAX, ); let order = OrderCreation { valid_to: time::now_in_epoch_seconds() + 2, @@ -1873,59 +1871,6 @@ mod tests { )); } - #[tokio::test] - async fn post_validate_err_sell_amount_overflow() { - let mut order_quoter = MockOrderQuoting::new(); - let mut bad_token_detector = MockBadTokenDetecting::new(); - let mut balance_fetcher = MockBalanceFetching::new(); - order_quoter - .expect_find_quote() - .returning(|_, _| Ok(Default::default())); - order_quoter.expect_store_quote().returning(Ok); - bad_token_detector - .expect_detect() - .returning(|_| Ok(TokenQuality::Good)); - balance_fetcher - .expect_can_transfer() - .returning(|_, _| Ok(())); - let mut limit_order_counter = MockLimitOrderCounting::new(); - limit_order_counter.expect_count().returning(|_| Ok(0u64)); - let validator = OrderValidator::new( - dummy_contract!(WETH9, [0xef; 20]), - Arc::new(order_validation::banned::Users::none()), - OrderValidPeriodConfiguration::any(), - false, - Arc::new(bad_token_detector), - dummy_contract!(HooksTrampoline, [0xcf; 20]), - Arc::new(order_quoter), - Arc::new(balance_fetcher), - Arc::new(MockSignatureValidating::new()), - Arc::new(limit_order_counter), - 0, - Arc::new(MockCodeFetching::new()), - Default::default(), - None, - ); - let order = OrderCreation { - valid_to: time::now_in_epoch_seconds() + 2, - sell_token: H160::from_low_u64_be(1), - buy_token: H160::from_low_u64_be(2), - buy_amount: U256::from(1), - sell_amount: U256::MAX, - fee_amount: U256::from(1), - signature: Signature::Eip712(EcdsaSignature::non_zero()), - app_data: OrderCreationAppData::Full { - full: "{}".to_string(), - }, - ..Default::default() - }; - let result = validator - .validate_and_construct_order(order, &Default::default(), Default::default(), None) - .await; - dbg!(&result); - assert!(matches!(result, Err(ValidationError::SellAmountOverflow))); - } - #[tokio::test] async fn post_validate_err_insufficient_balance() { let mut order_quoter = MockOrderQuoting::new(); @@ -1957,6 +1902,7 @@ mod tests { Arc::new(MockCodeFetching::new()), Default::default(), None, + u64::MAX, ); let order = OrderCreation { valid_to: time::now_in_epoch_seconds() + 2, @@ -2013,6 +1959,7 @@ mod tests { Arc::new(MockCodeFetching::new()), Default::default(), None, + u64::MAX, ); let creation = OrderCreation { @@ -2076,6 +2023,7 @@ mod tests { Arc::new(MockCodeFetching::new()), Default::default(), None, + u64::MAX, ); let order = OrderCreation {