diff --git a/SUPPORTED_APIS.md b/SUPPORTED_APIS.md
index 8b5a8de3..f1bc609b 100644
--- a/SUPPORTED_APIS.md
+++ b/SUPPORTED_APIS.md
@@ -90,7 +90,7 @@ The `status` options are:
| `EVM` | `evm_snapshot` | `NOT IMPLEMENTED`
[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`
[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 |
@@ -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`
[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
_(default is `260`)_ |
| [`NETWORK`](#network-namespace) | [`net_peerCount`](#net_peercount) | `SUPPORTED` | Returns the number of peers currently connected to the client
_(hard-coded to `0`)_ |
| [`NETWORK`](#network-namespace) | [`net_listening`](#net_listening) | `SUPPORTED` | Returns `true` if the client is actively listening for network connections
_(hard-coded to `false`)_ |
@@ -909,6 +909,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`
diff --git a/src/hardhat.rs b/src/hardhat.rs
index 52063870..0413128e 100644
--- a/src/hardhat.rs
+++ b/src/hardhat.rs
@@ -71,6 +71,34 @@ pub trait HardhatNamespaceT {
num_blocks: Option,
interval: Option,
) -> BoxFuture>;
+
+ /// 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>;
+
+ /// 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>;
}
impl HardhatNamespaceT
@@ -174,6 +202,45 @@ impl HardhatNamespaceT
}
})
}
+
+ fn impersonate_account(&self, address: Address) -> BoxFuture> {
+ 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> {
+ 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)]
@@ -181,8 +248,9 @@ 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() {
@@ -268,7 +336,8 @@ mod tests {
#[tokio::test]
async fn test_hardhat_mine_custom() {
let node = InMemoryNode::::default();
- let hardhat = HardhatNamespaceImpl::new(node.get_inner());
+ let hardhat: HardhatNamespaceImpl =
+ HardhatNamespaceImpl::new(node.get_inner());
let start_block = node
.get_block_by_number(zksync_types::api::BlockNumber::Latest, false)
@@ -299,4 +368,79 @@ mod tests {
);
}
}
+
+ #[tokio::test]
+ async fn test_impersonate_account() {
+ let node = InMemoryNode::::default();
+ let hardhat: HardhatNamespaceImpl =
+ 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());
+ }
}
diff --git a/src/node.rs b/src/node.rs
index e7d3553a..3af49148 100644
--- a/src/node.rs
+++ b/src/node.rs
@@ -4,7 +4,7 @@ use crate::{
console_log::ConsoleLogHandler,
fork::{ForkDetails, ForkSource, ForkStorage},
formatter,
- system_contracts::{self, SystemContracts},
+ system_contracts::{self, Options, SystemContracts},
utils::{
adjust_l1_gas_price_for_tx, derive_gas_estimation_overhead, to_human_size, IntoBoxedFuture,
},
@@ -16,7 +16,7 @@ use futures::FutureExt;
use jsonrpc_core::BoxFuture;
use std::{
cmp::{self},
- collections::HashMap,
+ collections::{HashMap, HashSet},
str::FromStr,
sync::{Arc, RwLock},
};
@@ -34,7 +34,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::{
@@ -246,6 +246,7 @@ pub struct InMemoryNodeInner {
pub resolve_hashes: bool,
pub console_log_handler: ConsoleLogHandler,
pub system_contracts: SystemContracts,
+ pub impersonated_accounts: HashSet,
}
type L2TxResult = (
@@ -557,6 +558,17 @@ impl InMemoryNodeInner {
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(
@@ -635,6 +647,7 @@ impl InMemoryNode {
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::::new();
@@ -664,6 +677,7 @@ impl InMemoryNode {
resolve_hashes,
console_log_handler: ConsoleLogHandler::default(),
system_contracts: SystemContracts::from_options(system_contracts_options),
+ impersonated_accounts: Default::default(),
}
};
@@ -935,7 +949,24 @@ impl InMemoryNode {
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::::create_block_properties(bootloader_code);
@@ -952,6 +983,7 @@ impl InMemoryNode {
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)
diff --git a/test_endpoints.http b/test_endpoints.http
index 51c8ece4..68d38b3d 100644
--- a/test_endpoints.http
+++ b/test_endpoints.http
@@ -452,6 +452,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",