Skip to content

Commit

Permalink
feat: impl hardhat_impersonateAccount and `hardhat_stopImpersonatio…
Browse files Browse the repository at this point in the history
…nAccount` (#125)
  • Loading branch information
grw-ms authored Sep 21, 2023
1 parent 2b645a3 commit e2f07b2
Show file tree
Hide file tree
Showing 4 changed files with 263 additions and 7 deletions.
58 changes: 56 additions & 2 deletions SUPPORTED_APIS.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ The `status` options are:
| `EVM` | `evm_snapshot` | `NOT IMPLEMENTED`<br />[GitHub Issue #69](https://github.com/matter-labs/era-test-node/issues/69) | Snapshot the state of the blockchain at the current block |
| `HARDHAT` | `hardhat_addCompilationResult` | `NOT IMPLEMENTED` | Add information about compiled contracts |
| `HARDHAT` | `hardhat_dropTransaction` | `NOT IMPLEMENTED` | Remove a transaction from the mempool |
| `HARDHAT` | `hardhat_impersonateAccount` | `NOT IMPLEMENTED`<br />[GitHub Issue #73](https://github.com/matter-labs/era-test-node/issues/73) | Impersonate an account |
| [`HARDHAT`](#hardhat-namespace) | [`hardhat_impersonateAccount`](#hardhat_impersonateaccount) | `SUPPORTED` | Impersonate an account |
| `HARDHAT` | `hardhat_getAutomine` | `NOT IMPLEMENTED` | Returns `true` if automatic mining is enabled, and `false` otherwise |
| `HARDHAT` | `hardhat_metadata` | `NOT IMPLEMENTED` | Returns the metadata of the current network |
| [`HARDHAT`](#hardhat-namespace) | [`hardhat_mine`](#hardhat_mine) | Mine any number of blocks at once, in constant time |
Expand All @@ -104,7 +104,7 @@ The `status` options are:
| `HARDHAT` | `hardhat_setPrevRandao` | `NOT IMPLEMENTED` | Sets the PREVRANDAO value of the next block |
| [`HARDHAT`](#hardhat-namespace) | [`hardhat_setNonce`](#hardhat_setnonce) | `SUPPORTED` | Sets the nonce of a given account |
| `HARDHAT` | `hardhat_setStorageAt` | `NOT IMPLEMENTED` | Sets the storage value at a given key for a given account |
| `HARDHAT` | `hardhat_stopImpersonatingAccount` | `NOT IMPLEMENTED`<br />[GitHub Issue #74](https://github.com/matter-labs/era-test-node/issues/74) | Stop impersonating an account after having previously used `hardhat_impersonateAccount` |
| [`HARDHAT`](#hardhat-namespace) | [`hardhat_stopImpersonatingAccount`](#hardhat_stopimpersonatingaccount) | `SUPPORTED` | Stop impersonating an account after having previously used `hardhat_impersonateAccount` |
| [`NETWORK`](#network-namespace) | [`net_version`](#net_version) | `SUPPORTED` | Returns the current network id <br />_(default is `260`)_ |
| [`NETWORK`](#network-namespace) | [`net_peerCount`](#net_peercount) | `SUPPORTED` | Returns the number of peers currently connected to the client <br/>_(hard-coded to `0`)_ |
| [`NETWORK`](#network-namespace) | [`net_listening`](#net_listening) | `SUPPORTED` | Returns `true` if the client is actively listening for network connections <br />_(hard-coded to `false`)_ |
Expand Down Expand Up @@ -1053,6 +1053,60 @@ curl --request POST \
"0x100"
]
}'

```
### `hardhat_impersonateAccount`

[source](src/hardhat.rs)

Begin impersonating account- subsequent transactions sent to the node will be committed as if they were initiated by the supplied address.

#### Arguments

- `address: Address` - The address to begin impersonating

#### Example

```bash
curl --request POST \
--url http://localhost:8011/ \
--header 'content-type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": "2",
"method": "hardhat_impersonateAccount",
"params": [
"0x364d6D0333432C3Ac016Ca832fb8594A8cE43Ca6"
]
}'
```

### `hardhat_stopImpersonatingAccount`

[source](src/hardhat.rs)

Stop impersonating account, should be used after calling `hardhat_impersonateAccount`.
Since we only impersonate one account at a time, the `address` argument is ignored and the current
impersonated account (if any) is cleared.

#### Arguments

- `address: Address` - (Optional) Argument accepted for compatibility and will be ignored

#### Example

```bash
curl --request POST \
--url http://localhost:8011/ \
--header 'content-type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": "2",
"method": "hardhat_stopImpersonatingAccount",
"params": [
"0x364d6D0333432C3Ac016Ca832fb8594A8cE43Ca6"
]
}'
```

## `EVM NAMESPACE`
Expand Down
148 changes: 146 additions & 2 deletions src/hardhat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,34 @@ pub trait HardhatNamespaceT {
num_blocks: Option<U64>,
interval: Option<U64>,
) -> BoxFuture<Result<bool>>;

/// Hardhat Network allows you to send transactions impersonating specific account and contract addresses.
/// To impersonate an account use this method, passing the address to impersonate as its parameter.
/// After calling this method, any transactions with this sender will be executed without verification.
/// Multiple addresses can be impersonated at once.
///
/// # Arguments
///
/// * `address` - The address to impersonate
///
/// # Returns
///
/// A `BoxFuture` containing a `Result` with a `bool` representing the success of the operation.
#[rpc(name = "hardhat_impersonateAccount")]
fn impersonate_account(&self, address: Address) -> BoxFuture<Result<bool>>;

/// Use this method to stop impersonating an account after having previously used `hardhat_impersonateAccount`
/// The method returns `true` if the account was being impersonated and `false` otherwise.
///
/// # Arguments
///
/// * `address` - The address to stop impersonating.
///
/// # Returns
///
/// A `BoxFuture` containing a `Result` with a `bool` representing the success of the operation.
#[rpc(name = "hardhat_stopImpersonatingAccount")]
fn stop_impersonating_account(&self, address: Address) -> BoxFuture<Result<bool>>;
}

impl<S: Send + Sync + 'static + ForkSource + std::fmt::Debug> HardhatNamespaceT
Expand Down Expand Up @@ -174,15 +202,55 @@ impl<S: Send + Sync + 'static + ForkSource + std::fmt::Debug> HardhatNamespaceT
}
})
}

fn impersonate_account(&self, address: Address) -> BoxFuture<jsonrpc_core::Result<bool>> {
let inner = Arc::clone(&self.node);
Box::pin(async move {
match inner.write() {
Ok(mut inner) => {
if inner.set_impersonated_account(address) {
log::info!("🕵️ Account {:?} has been impersonated", address);
Ok(true)
} else {
log::info!("🕵️ Account {:?} was already impersonated", address);
Ok(false)
}
}
Err(_) => Err(into_jsrpc_error(Web3Error::InternalError)),
}
})
}

fn stop_impersonating_account(&self, address: Address) -> BoxFuture<Result<bool>> {
let inner = Arc::clone(&self.node);
Box::pin(async move {
match inner.write() {
Ok(mut inner) => {
if inner.stop_impersonating_account(address) {
log::info!("🕵️ Stopped impersonating account {:?}", address);
Ok(true)
} else {
log::info!(
"🕵️ Account {:?} was not impersonated, nothing to stop",
address
);
Ok(false)
}
}
Err(_) => Err(into_jsrpc_error(Web3Error::InternalError)),
}
})
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::{http_fork_source::HttpForkSource, node::InMemoryNode};
use std::str::FromStr;
use zksync_basic_types::{Nonce, H256};
use zksync_core::api_server::web3::backend_jsonrpc::namespaces::eth::EthNamespaceT;
use zksync_types::api::BlockNumber;
use zksync_types::{api::BlockNumber, fee::Fee, l2::L2Tx};

#[tokio::test]
async fn test_set_balance() {
Expand Down Expand Up @@ -268,7 +336,8 @@ mod tests {
#[tokio::test]
async fn test_hardhat_mine_custom() {
let node = InMemoryNode::<HttpForkSource>::default();
let hardhat = HardhatNamespaceImpl::new(node.get_inner());
let hardhat: HardhatNamespaceImpl<HttpForkSource> =
HardhatNamespaceImpl::new(node.get_inner());

let start_block = node
.get_block_by_number(zksync_types::api::BlockNumber::Latest, false)
Expand Down Expand Up @@ -299,4 +368,79 @@ mod tests {
);
}
}

#[tokio::test]
async fn test_impersonate_account() {
let node = InMemoryNode::<HttpForkSource>::default();
let hardhat: HardhatNamespaceImpl<HttpForkSource> =
HardhatNamespaceImpl::new(node.get_inner());
let to_impersonate =
Address::from_str("0xd8da6bf26964af9d7eed9e03e53415d37aa96045").unwrap();

// give impersonated account some balance
let result = hardhat
.set_balance(to_impersonate, U256::exp10(18))
.await
.unwrap();
assert!(result);

// construct a tx
let mut tx = L2Tx::new(
Address::random(),
vec![],
Nonce(0),
Fee {
gas_limit: U256::from(1_000_000),
max_fee_per_gas: U256::from(250_000_000),
max_priority_fee_per_gas: U256::from(250_000_000),
gas_per_pubdata_limit: U256::from(20000),
},
to_impersonate,
U256::one(),
None,
Default::default(),
);
tx.set_input(vec![], H256::random());

// try to execute the tx- should fail without signature
assert!(node.apply_txs(vec![tx.clone()]).is_err());

// impersonate the account
let result = hardhat
.impersonate_account(to_impersonate)
.await
.expect("impersonate_account");

// result should be true
assert!(result);

// impersonating the same account again should return false
let result = hardhat
.impersonate_account(to_impersonate)
.await
.expect("impersonate_account");
assert!(!result);

// execution should now succeed
assert!(node.apply_txs(vec![tx.clone()]).is_ok());

// stop impersonating the account
let result = hardhat
.stop_impersonating_account(to_impersonate)
.await
.expect("stop_impersonating_account");

// result should be true
assert!(result);

// stop impersonating the same account again should return false
let result = hardhat
.stop_impersonating_account(to_impersonate)
.await
.expect("stop_impersonating_account");
assert!(!result);

// execution should now fail again
assert!(node.apply_txs(vec![tx]).is_err());
}
}
38 changes: 35 additions & 3 deletions src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::{
filters::EthFilters,
fork::{ForkDetails, ForkSource, ForkStorage},
formatter,
system_contracts::{self, SystemContracts},
system_contracts::{self, Options, SystemContracts},
utils::{
self, adjust_l1_gas_price_for_tx, derive_gas_estimation_overhead, to_human_size,
IntoBoxedFuture,
Expand Down Expand Up @@ -36,7 +36,7 @@ use vm::{
};
use zksync_basic_types::{
web3::{self, signing::keccak256},
AccountTreeId, Bytes, H160, H256, U256, U64,
AccountTreeId, Address, Bytes, H160, H256, U256, U64,
};
use zksync_contracts::BaseSystemContracts;
use zksync_core::api_server::web3::backend_jsonrpc::{
Expand Down Expand Up @@ -255,6 +255,7 @@ pub struct InMemoryNodeInner<S> {
pub resolve_hashes: bool,
pub console_log_handler: ConsoleLogHandler,
pub system_contracts: SystemContracts,
pub impersonated_accounts: HashSet<Address>,
}

type L2TxResult = (
Expand Down Expand Up @@ -566,6 +567,17 @@ impl<S: std::fmt::Debug + ForkSource> InMemoryNodeInner<S> {
Some(revert) => Err(revert.revert_reason),
}
}

/// Sets the `impersonated_account` field of the node.
/// This field is used to override the `tx.initiator_account` field of the transaction in the `run_l2_tx` method.
pub fn set_impersonated_account(&mut self, address: Address) -> bool {
self.impersonated_accounts.insert(address)
}

/// Clears the `impersonated_account` field of the node.
pub fn stop_impersonating_account(&mut self, address: Address) -> bool {
self.impersonated_accounts.remove(&address)
}
}

fn not_implemented<T: Send + 'static>(
Expand Down Expand Up @@ -645,6 +657,7 @@ impl<S: ForkSource + std::fmt::Debug> InMemoryNode<S> {
resolve_hashes,
console_log_handler: ConsoleLogHandler::default(),
system_contracts: SystemContracts::from_options(system_contracts_options),
impersonated_accounts: Default::default(),
}
} else {
let mut block_hashes = HashMap::<u64, H256>::new();
Expand Down Expand Up @@ -675,6 +688,7 @@ impl<S: ForkSource + std::fmt::Debug> InMemoryNode<S> {
resolve_hashes,
console_log_handler: ConsoleLogHandler::default(),
system_contracts: SystemContracts::from_options(system_contracts_options),
impersonated_accounts: Default::default(),
}
};

Expand Down Expand Up @@ -946,7 +960,24 @@ impl<S: ForkSource + std::fmt::Debug> InMemoryNode<S> {

let mut oracle_tools = OracleTools::new(&mut storage_view, HistoryEnabled);

let bootloader_code = inner.system_contracts.contracts(execution_mode);
// if we are impersonating an account, we need to use non-verifying system contracts
let nonverifying_contracts;
let bootloader_code = {
if inner
.impersonated_accounts
.contains(&l2_tx.common_data.initiator_address)
{
tracing::info!(
"🕵️ Executing tx from impersonated account {:?}",
l2_tx.common_data.initiator_address
);
nonverifying_contracts =
SystemContracts::from_options(&Options::BuiltInWithoutSecurity);
nonverifying_contracts.contracts(execution_mode)
} else {
inner.system_contracts.contracts(execution_mode)
}
};

let block_context = inner.create_block_context();
let block_properties = InMemoryNodeInner::<S>::create_block_properties(bootloader_code);
Expand All @@ -963,6 +994,7 @@ impl<S: ForkSource + std::fmt::Debug> InMemoryNode<S> {
let spent_on_pubdata_before = vm.state.local_state.spent_pubdata_counter;

let tx: Transaction = l2_tx.clone().into();

push_transaction_to_bootloader_memory(&mut vm, &tx, execution_mode, None);
let tx_result = vm
.execute_next_tx(u32::MAX, true)
Expand Down
26 changes: 26 additions & 0 deletions test_endpoints.http
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,32 @@ content-type: application/json
POST http://localhost:8011
content-type: application/json

{
"jsonrpc": "2.0",
"id": "2",
"method": "hardhat_impersonateAccount",
"params": [
"0x364d6D0333432C3Ac016Ca832fb8594A8cE43Ca6"
]
}

###
POST http://localhost:8011
content-type: application/json

{
"jsonrpc": "2.0",
"id": "2",
"method": "hardhat_stopImpersonatingAccount",
"params": [
"0x364d6D0333432C3Ac016Ca832fb8594A8cE43Ca6"
]
}

###
POST http://localhost:8011
content-type: application/json

{
"jsonrpc": "2.0",
"id": "1",
Expand Down

0 comments on commit e2f07b2

Please sign in to comment.