Skip to content

Commit

Permalink
Magic: StorageAccessbile Based Simulations (#1831)
Browse files Browse the repository at this point in the history
Follow up to
#1827 (comment)
- thanks @fleupold for the wonderful idea!

This PR changes instances of `eth_call` with state override simulations
**where we are only overriding the Settlement contract code** to make
use of `StorageAccessible`. It does this with an additional support
`Reader` contract in order to avoid needing to manage deployments
on-chain for the `StorageAccessible` target (i.e. the reader logic). See
the comment in the `Reader.sol` file for more details on how this works.

Specifically, we changed the following two simulations to no longer
require state overrides (i.e. we do not need a special node to handle
those requests, and it will Just Work™ on Gnosis Chain):
* Account balance simulations with pre-hooks
* ERC-1271 signature validation with pre-hooks

In a follow up PRs I will:
1. #1832
2. Remove the redundant `Web3` strategy for both account balance and
signature verification simulation

### Release notes

Hooks E2E test continues to pass **without the nasty
`----account-balances-optimistic-pre-interaction-handling=true` hack**
which used to disable balance checks for orders with pre-hooks. This
means that not only are we E2E testing that executing orders with
pre-hooks are working, we are also checking that balance simulations
work 🎉.

You can double check this is actually doing something by applying a
patch like this:

```diff
diff --git a/crates/e2e/tests/e2e/hooks.rs b/crates/e2e/tests/e2e/hooks.rs
index 42d8d072..84d978b5 100644
--- a/crates/e2e/tests/e2e/hooks.rs
+++ b/crates/e2e/tests/e2e/hooks.rs
@@ -66,7 +66,7 @@ async fn test(web3: Web3) {
             full: json!({
                 "metadata": {
                     "hooks": {
-                        "pre": [permit, steal_cow],
+                        "pre": [/*permit, */steal_cow],
                         "post": [steal_weth],
                     },
                 },
```

And see the E2E test fail as expected with:

```
thread 'hooks::local_node_test' panicked at 'called `Result::unwrap()` on an `Err` value: (400, "{\"errorType\":\"InsufficientAllowance\",\"description\":\"order owner must give allowance to VaultRelayer\"}")', crates/e2e/tests/e2e/hooks.rs:83:41
```

I also ran the `cargo test -p shared -- --nocapture --ignored
account_balances::simulation` manual test which continues to work.
  • Loading branch information
Nicholas Rodrigues Lordello authored Sep 6, 2023
1 parent 27fa754 commit fab0fe4
Show file tree
Hide file tree
Showing 16 changed files with 158 additions and 215 deletions.
10 changes: 0 additions & 10 deletions crates/autopilot/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,11 +172,6 @@ pub async fn main(args: arguments::Arguments) {
vault_relayer,
},
web3.clone(),
simulation_web3.clone(),
args.shared
.tenderly
.get_api_instance(&http_factory, "signature_validating".into())
.unwrap(),
);

let balance_fetcher = args.shared.balances.cached(
Expand All @@ -187,11 +182,6 @@ pub async fn main(args: arguments::Arguments) {
vault: vault.as_ref().map(|contract| contract.address()),
},
web3.clone(),
simulation_web3.clone(),
args.shared
.tenderly
.get_api_instance(&http_factory, "balance_fetching".into())
.unwrap(),
current_block_stream.clone(),
);

Expand Down
1 change: 1 addition & 0 deletions crates/contracts/artifacts/SimulateCode.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"abi":[{"inputs":[{"internalType":"contract IStorageAccessible","name":"target","type":"address"},{"internalType":"bytes","name":"code","type":"bytes"},{"internalType":"bytes","name":"call","type":"bytes"}],"stateMutability":"nonpayable","type":"constructor"}],"bytecode":"0x608060405234801561001057600080fd5b5060405161027e38038061027e83398101604081905261002f9161017d565b600082516020840134f090506000846001600160a01b031663f84436bd83856040518363ffffffff1660e01b815260040161006b9291906101ff565b6000604051808303816000875af115801561008a573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526100b29190810190610241565b9050805181602001f35b634e487b7160e01b600052604160045260246000fd5b60005b838110156100ed5781810151838201526020016100d5565b50506000910152565b600082601f83011261010757600080fd5b81516001600160401b0380821115610121576101216100bc565b604051601f8301601f19908116603f01168101908282118183101715610149576101496100bc565b8160405283815286602085880101111561016257600080fd5b6101738460208301602089016100d2565b9695505050505050565b60008060006060848603121561019257600080fd5b83516001600160a01b03811681146101a957600080fd5b60208501519093506001600160401b03808211156101c657600080fd5b6101d2878388016100f6565b935060408601519150808211156101e857600080fd5b506101f5868287016100f6565b9150509250925092565b60018060a01b0383168152604060208201526000825180604084015261022c8160608501602087016100d2565b601f01601f1916919091016060019392505050565b60006020828403121561025357600080fd5b81516001600160401b0381111561026957600080fd5b610275848285016100f6565b94935050505056fe","deployedBytecode":"0x6080604052600080fdfea164736f6c6343000811000a","devdoc":{"methods":{}},"userdoc":{"methods":{}}}
5 changes: 2 additions & 3 deletions crates/contracts/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -523,11 +523,10 @@ fn main() {
generate_contract("Trader");
generate_contract("Solver");

// Support contract used for balance simulation.
// Support contracts used for various order simulations.
generate_contract("Balances");

// Support contract used for ERC-1271 signature verification simulation.
generate_contract("Signatures");
generate_contract("SimulateCode");

// Support contract used for global block stream.
generate_contract("FetchBlock");
Expand Down
9 changes: 8 additions & 1 deletion crates/contracts/solidity/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@ SOLFLAGS := --overwrite --abi --bin --bin-runtime --metadata-hash none --optimiz
TARGETDIR := ../../../target/solidity
ARTIFACTDIR := ../artifacts

CONTRACTS := Balances.sol FetchBlock.sol Multicall.sol Trader.sol Signatures.sol Solver.sol
CONTRACTS := \
Balances.sol \
FetchBlock.sol \
Multicall.sol \
Signatures.sol \
SimulateCode.sol \
Solver.sol \
Trader.sol
ARTIFACTS := $(patsubst %.sol,$(ARTIFACTDIR)/%.json,$(CONTRACTS))

.PHONY: artifacts
Expand Down
46 changes: 46 additions & 0 deletions crates/contracts/solidity/SimulateCode.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import { IStorageAccessible } from "./interfaces/IStorageAccessible.sol";

/// @title A contract for simulating arbitrary code in the context of a
/// `StorageAccessible` contract.
contract SimulateCode {
/// @dev This looks like a constructor but it is not... In fact, nodes
/// support `eth_call`s for contract creation and **return the code of the
/// contract that would be created**. This means we can use constructors to
/// execute arbitrary code on the current state of the EVM, and "manually"
/// return with some inline assembly that data (as this is the mechanism
/// used for contract creation). See the `FetchBlock.sol` contract for
/// another application of this trick.
///
/// The contract does this to:
/// 1. Deploy some arbitrary contract code
/// 2. Use the `StorageAccessible` pattern to execute the contract code
/// deployed in step 1. within the another contract context (usually the
/// settlement contract - which implements this pattern)
///
/// This allows us to make use of `StorageAccessible` without actually
/// deploying a contract :).
///
/// Returns the return data from the simulation code.
///
/// @param target - The `StorageAccessible` implementation.
/// @param code - Creation code for the reader contract.
/// @param call - The calldata to pass in the `DELEGATECALL` simulation.
constructor(
IStorageAccessible target,
bytes memory code,
bytes memory call
) {
address implementation;
assembly {
implementation := create(callvalue(), add(code, 32), mload(code))
}

bytes memory result = target.simulateDelegatecall(implementation, call);
assembly {
return(add(32, result), mload(result))
}
}
}
7 changes: 7 additions & 0 deletions crates/contracts/solidity/interfaces/IStorageAccessible.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

/// @title Storage accessible interface.
interface IStorageAccessible {
function simulateDelegatecall(address reader, bytes memory call) external returns (bytes memory result);
}
4 changes: 3 additions & 1 deletion crates/contracts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ mod macros;

#[cfg(feature = "bin")]
pub mod paths;
pub mod storage_accessible;
pub mod vault;
pub mod web3;

Expand Down Expand Up @@ -71,9 +72,10 @@ pub mod support {
Balances;
FetchBlock;
Multicall;
Trader;
Signatures;
SimulateCode;
Solver;
Trader;
}
}

Expand Down
41 changes: 41 additions & 0 deletions crates/contracts/src/storage_accessible.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//! Module to help encoding `eth_call`s for the `StorageAccessible` contracts.
use {
crate::support::SimulateCode,
ethcontract::{
common::abi,
tokens::Tokenize,
web3::types::{Bytes, CallRequest},
H160,
},
};

/// Encode a call to a `StorageAccessible` `target` to execute `call` with the
/// contract created with `code`
pub fn call(target: H160, code: Bytes, call: Bytes) -> CallRequest {
// Unfortunately, the `ethcontract` crate does not expose the logic to build
// creation code for a contract. Luckily, it isn't complicated - you just
// append the ABI-encoded constructor arguments.
let args = abi::encode(&[
target.into_token(),
ethcontract::Bytes(code.0).into_token(),
ethcontract::Bytes(call.0).into_token(),
]);

CallRequest {
data: Some(
[
SimulateCode::raw_contract()
.bytecode
.to_bytes()
.unwrap()
.0
.as_slice(),
&args,
]
.concat()
.into(),
),
..Default::default()
}
}
4 changes: 2 additions & 2 deletions crates/e2e/tests/e2e/colocation_hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,14 @@ async fn test(web3: Web3) {

let services = Services::new(onchain.contracts()).await;
services.start_autopilot(vec![
"--account-balances-optimistic-pre-interaction-handling=true".to_string(),
"--account-balances=simulation".to_string(),
"--enable-colocation=true".to_string(),
"--drivers=http://localhost:11088/test_solver".to_string(),
]);
services
.start_api(vec![
"--account-balances=simulation".to_string(),
"--enable-custom-interactions=true".to_string(),
"--account-balances-optimistic-pre-interaction-handling=true".to_string(),
])
.await;

Expand Down
8 changes: 3 additions & 5 deletions crates/e2e/tests/e2e/hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,11 @@ async fn test(web3: Web3) {
.await;

let services = Services::new(onchain.contracts()).await;
services.start_autopilot(vec![
"--account-balances-optimistic-pre-interaction-handling=true".to_string(),
]);
services.start_autopilot(vec!["--account-balances=simulation".to_string()]);
services
.start_api(vec![
"--account-balances=simulation".to_string(),
"--enable-custom-interactions=true".to_string(),
"--account-balances-optimistic-pre-interaction-handling=true".to_string(),
])
.await;

Expand Down Expand Up @@ -90,7 +88,7 @@ async fn test(web3: Web3) {
tracing::info!("Waiting for trade.");
services.start_old_driver(
solver.private_key(),
vec!["--account-balances-optimistic-pre-interaction-handling=true".to_string()],
vec!["--account-balances=simulation".to_string()],
);
let trade_happened = || async {
cow.balance_of(trader.address())
Expand Down
10 changes: 0 additions & 10 deletions crates/orderbook/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,6 @@ pub async fn run(args: Arguments) {
vault_relayer,
},
web3.clone(),
simulation_web3.clone(),
args.shared
.tenderly
.get_api_instance(&http_factory, "signature_validating".into())
.unwrap(),
);

let vault = match args.shared.balancer_v2_vault_address {
Expand Down Expand Up @@ -158,11 +153,6 @@ pub async fn run(args: Arguments) {
vault: vault.as_ref().map(|contract| contract.address()),
},
web3.clone(),
simulation_web3.clone(),
args.shared
.tenderly
.get_api_instance(&http_factory, "balance_fetching".into())
.unwrap(),
);

let gas_price_estimator = Arc::new(InstrumentedGasEstimator::new(
Expand Down
72 changes: 8 additions & 64 deletions crates/shared/src/account_balances/arguments.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
use {
super::{BalanceFetching, CachingBalanceFetcher, SimulationBalanceFetcher, Web3BalanceFetcher},
crate::{
arguments::{display_option, CodeSimulatorKind},
code_simulation::{CodeSimulating, TenderlyCodeSimulator, Web3ThenTenderly},
tenderly_api::TenderlyApi,
},
ethcontract::H160,
ethrpc::{current_block::CurrentBlockStream, Web3},
std::{
Expand All @@ -21,11 +16,6 @@ pub struct Arguments {
#[clap(long, env, default_value = "web3", value_enum)]
pub account_balances: Strategy,

/// The code simulation implementation to use. Can be one of `Web3`,
/// `Tenderly` or `Web3ThenTenderly`.
#[clap(long, env, value_enum)]
pub account_balances_simulator: Option<CodeSimulatorKind>,

/// Whether or not to optimistically treat account balance queries with
/// pre-interactions as if sufficient token balance and allowance is always
/// available. Useful for partially supporting pre-interactions in
Expand Down Expand Up @@ -60,13 +50,7 @@ pub struct Contracts {
}

impl Arguments {
pub fn fetcher(
&self,
contracts: Contracts,
web3: Web3,
simulation_web3: Option<Web3>,
tenderly: Option<Arc<dyn TenderlyApi>>,
) -> Arc<dyn BalanceFetching> {
pub fn fetcher(&self, contracts: Contracts, web3: Web3) -> Arc<dyn BalanceFetching> {
match self.account_balances {
Strategy::Web3 => Arc::new(Web3BalanceFetcher::new(
web3,
Expand All @@ -75,54 +59,22 @@ impl Arguments {
contracts.settlement,
self.account_balances_optimistic_pre_interaction_handling,
)),
Strategy::Simulation => {
let web3_simulator =
move || simulation_web3.expect("simulation web3 not configured");
let tenderly_simulator = move || {
TenderlyCodeSimulator::new(
tenderly.expect("tenderly api not configured"),
contracts.chain_id,
)
};

let simulator = match self
.account_balances_simulator
.expect("account balances simulator not configured")
{
CodeSimulatorKind::Web3 => {
Arc::new(web3_simulator()) as Arc<dyn CodeSimulating>
}
CodeSimulatorKind::Tenderly => Arc::new(tenderly_simulator()),
CodeSimulatorKind::Web3ThenTenderly => Arc::new(Web3ThenTenderly::new(
web3_simulator(),
tenderly_simulator(),
)),
};

Arc::new(SimulationBalanceFetcher::new(
simulator,
contracts.settlement,
contracts.vault_relayer,
contracts.vault,
))
}
Strategy::Simulation => Arc::new(SimulationBalanceFetcher::new(
web3,
contracts.settlement,
contracts.vault_relayer,
contracts.vault,
)),
}
}

pub fn cached(
&self,
contracts: Contracts,
web3: Web3,
simulation_web3: Option<Web3>,
tenderly: Option<Arc<dyn TenderlyApi>>,
blocks: CurrentBlockStream,
) -> Arc<CachingBalanceFetcher> {
let cached = Arc::new(CachingBalanceFetcher::new(self.fetcher(
contracts,
web3,
simulation_web3,
tenderly,
)));
let cached = Arc::new(CachingBalanceFetcher::new(self.fetcher(contracts, web3)));
cached.spawn_background_task(blocks);
cached
}
Expand All @@ -131,14 +83,6 @@ impl Arguments {
impl Display for Arguments {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
writeln!(f, "account_balances: {:?}", self.account_balances)?;
display_option(
f,
"account_balances_simulator",
&self
.account_balances_simulator
.as_ref()
.map(|value| format!("{value:?}")),
)?;

Ok(())
}
Expand Down
Loading

0 comments on commit fab0fe4

Please sign in to comment.