Repository: 0xSpaceShard/starknet-devnet (language: python)

Issue:
Mainnet forking
As I mentioned in the community call last week, it would be amazing to have mainnet forking. The use case it's to test how our dapp interacts with existing dapps deployed to the network. I know that now it's not possible to implement this, so this is more of a question to know what is needed to make this happen?


Discussion / hints:
Thanks once again for the proposal. I'm sharing this issue to more places.
Any updates on this important feature? @FabijanC 
> Any updates on this important feature? @FabijanC

I agree that it is important. It is in plans to be done, but not in the very near future.
On hold until: "_we have an independent Cairo VM + a StarkNet runner with a more lenient API (I'm guessing that this will be available in O(3 months))._"
> On hold until: "_we have an independent Cairo VM + a StarkNet runner with a more lenient API (I'm guessing that this will be available in O(3 months))._"

do you plan to use the Cleopatra VM (rust) ?
Hi @0xSidius ! Could you give more insight how using Cleopatra on our side would help with forking?
> Hi @0xSidius ! Could you give more insight how using Cleopatra on our side would help with forking?

it wouldn't w.r.t. to forking, but it would significantly improve DevEx, slashing down the current main Starknet bottleneck (performance)

Did you take a hard look at the developer experience on Starknet today? Even a modest project, using the fastest toolchains (Protostar) with Devnet will see hours-long test runs
Ok, but this is  for another issue... Btw. do you have any estimate how much would using Cleopatra potentially speed up the devnet? 
> Ok, but this is for another issue... Btw. do you have any estimate how much would using Cleopatra potentially speed up the devnet?

if our Ape/Nile vs Protostar comparison is any guide, we are looking at an average of anywhere in between of 1.5x to over 3x

Fix patch:
diff --git a/page/docs/guide/development.md b/page/docs/guide/development.md
index 995de456b..6cdfb911f 100644
--- a/page/docs/guide/development.md
+++ b/page/docs/guide/development.md
@@ -1,5 +1,5 @@
 ---
-sidebar_position: 17
+sidebar_position: 18
 ---
 
 # Development
diff --git a/page/docs/guide/devnet-speed-up.md b/page/docs/guide/devnet-speed-up.md
index 836cef7da..34effe943 100644
--- a/page/docs/guide/devnet-speed-up.md
+++ b/page/docs/guide/devnet-speed-up.md
@@ -1,5 +1,5 @@
 ---
-sidebar_position: 16
+sidebar_position: 17
 ---
 # Devnet speed-up troubleshooting
 
diff --git a/page/docs/guide/fork.md b/page/docs/guide/fork.md
new file mode 100644
index 000000000..757023169
--- /dev/null
+++ b/page/docs/guide/fork.md
@@ -0,0 +1,19 @@
+---
+sidebar_position: 16
+---
+
+# Fork
+
+To interact with contracts deployed on mainnet or testnet, you can use the forking feature to copy the remote origin and experiment with it locally with no changes to the origin.
+
+```
+starknet-devnet --fork-network <NAME|URL> [--fork-block <BLOCK_NUMBER>]
+```
+
+The value of `--fork-network` can either be a network name (`alpha-goerli`, `alpha-goerli2`, or `alpha-mainnet`) or a URL (e.g. `https://alpha4.starknet.io`).
+
+The `--fork-block` parameter is optional and its value should be the block number from which the forking is done. If none is provided, defaults to the `"latest"` block at the time of Devnet's start-up.
+
+All calls will first try Devnet's state and then fall back to the forking block.
+
+If you are forking another Devnet instance, keep in mind that it doesn't support polling specific blocks, but will always fall back to the currently latest block.
diff --git a/page/docs/guide/mint-token.md b/page/docs/guide/mint-token.md
index ee5ad5f51..921c6f446 100644
--- a/page/docs/guide/mint-token.md
+++ b/page/docs/guide/mint-token.md
@@ -6,7 +6,7 @@ sidebar_position: 14
 
 Other than using prefunded predeployed accounts, you can also add funds to an account that you deployed yourself.
 
-The ERC20 contract used for minting ETH tokens and charging fees is at: `0x62230ea046a9a5fbc261ac77d03c8d41e5d442db2284587570ab46455fd2488`
+The ERC20 contract used for minting ETH tokens and charging fees is at: `0x49D36570D4E46F48E99674BD3FCC84644DDD6B96F7C741B1562B82F9E004DC7`
 
 ## Query fee token address
 
@@ -19,7 +19,7 @@ Response:
 ```
 {
   "symbol":"ETH",
-  "address":"0x62230ea046a9a5fbc261ac77d03c8d41e5d442db2284587570ab46455fd2488",
+  "address":"0x...",
 }
 ```
 
diff --git a/page/docs/guide/run.md b/page/docs/guide/run.md
index ecdc78647..57781f8b1 100644
--- a/page/docs/guide/run.md
+++ b/page/docs/guide/run.md
@@ -9,7 +9,7 @@ Installing the package adds the `starknet-devnet` command.
 ```text
 usage: starknet-devnet [-h] [-v] [--host HOST] [--port PORT] [--load-path LOAD_PATH] [--dump-path DUMP_PATH] [--dump-on DUMP_ON] [--lite-mode] [--accounts ACCOUNTS]
                        [--initial-balance INITIAL_BALANCE] [--seed SEED] [--hide-predeployed-accounts] [--start-time START_TIME] [--gas-price GAS_PRICE] [--timeout TIMEOUT]
-                       [--account-class ACCOUNT_CLASS]
+                       [--account-class ACCOUNT_CLASS] [--fork-network FORK_NETWORK] [--fork-block FORK_BLOCK]
 
 Run a local instance of StarkNet Devnet
 
@@ -23,7 +23,7 @@ optional arguments:
   --dump-path DUMP_PATH
                         Specify the path to dump to
   --dump-on DUMP_ON     Specify when to dump; can dump on: exit, transaction
-  --lite-mode           Introduces speed-up by skipping block hash and deploy transaction hash calculation - applies sequential numbering instead (0x0, 0x1, 0x2, ...).
+  --lite-mode           Introduces speed-up by skipping block hash calculation - applies sequential numbering instead (0x0, 0x1, 0x2, ...).
   --accounts ACCOUNTS   Specify the number of accounts to be predeployed; defaults to 10
   --initial-balance INITIAL_BALANCE, -e INITIAL_BALANCE
                         Specify the initial balance of accounts to be predeployed; defaults to 1e+21
@@ -37,9 +37,11 @@ optional arguments:
   --timeout TIMEOUT, -t TIMEOUT
                         Specify the server timeout in seconds; defaults to 60
   --account-class ACCOUNT_CLASS
-                        Specify the account implementation to be used for predeploying;
-                        should be a path to the compiled JSON artifact;
-                        defaults to OpenZeppelin v0.5.0
+                        Specify the account implementation to be used for predeploying; should be a path to the compiled JSON artifact; defaults to OpenZeppelin v0.5.0
+  --fork-network FORK_NETWORK
+                        Specify the network to fork: can be a URL (e.g. https://alpha-mainnet.starknet.io) or network name (valid names: alpha-goerli, alpha-goerli2, alpha-mainnet)
+  --fork-block FORK_BLOCK
+                        Specify the block number where the --fork-network is forked; defaults to latest
 ```
 
 You can run `starknet-devnet` in a separate shell, or you can run it in background with `starknet-devnet &`.
diff --git a/pyproject.toml b/pyproject.toml
index 5f0e3e62b..8666fc1db 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -63,4 +63,5 @@ asyncio_mode="strict"
 filterwarnings=[
     "ignore::DeprecationWarning:lark.*:",
     "ignore::DeprecationWarning:frozendict.*:",
+    "ignore::DeprecationWarning:eth_abi.codec.*:",
 ]
diff --git a/scripts/mint.sh b/scripts/mint.sh
new file mode 100755
index 000000000..9a6995d3d
--- /dev/null
+++ b/scripts/mint.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+set -eu
+
+address=$1
+
+curl localhost:5050/mint \
+    -H "Content-Type: application/json" \
+    -d "{ \"address\": \"$address\", \"amount\": 1000000000000000000, \"lite\": false }"
diff --git a/starknet_devnet/account.py b/starknet_devnet/account.py
index 339f7cebb..20e6d9b44 100644
--- a/starknet_devnet/account.py
+++ b/starknet_devnet/account.py
@@ -76,14 +76,3 @@ async def deploy(self) -> StarknetContract:
         await starknet.state.state.set_storage_at(
             fee_token_address, balance_address + 1, initial_balance_uint256.high
         )
-
-        contract = StarknetContract(
-            state=starknet.state,
-            abi=contract_class.abi,
-            contract_address=self.address,
-            deploy_call_info=None,
-        )
-
-        await self.starknet_wrapper.store_contract(
-            self.address, contract, contract_class
-        )
diff --git a/starknet_devnet/accounts.py b/starknet_devnet/accounts.py
index e02ee9951..d8b6d0b94 100644
--- a/starknet_devnet/accounts.py
+++ b/starknet_devnet/accounts.py
@@ -9,6 +9,7 @@
 from starkware.crypto.signature.signature import private_to_stark_key
 
 from .account import Account
+from .util import warn
 
 
 class Accounts:
@@ -77,7 +78,7 @@ def __print(self):
 
         print(f"Initial balance of each account: {self.__initial_balance} WEI")
         print("Seed to replicate this account sequence:", self.__seed)
-        print(
+        warn(
             "WARNING: Use these accounts and their keys ONLY for local testing. "
             "DO NOT use them on mainnet or other live networks because you will LOSE FUNDS.\n",
             file=sys.stderr,
diff --git a/starknet_devnet/blocks.py b/starknet_devnet/blocks.py
index cb0cdf919..88b519f72 100644
--- a/starknet_devnet/blocks.py
+++ b/starknet_devnet/blocks.py
@@ -12,6 +12,7 @@
     BlockStatus,
 )
 from starkware.starknet.services.api.feeder_gateway.response_objects import (
+    BlockIdentifier,
     BlockStateUpdate,
 )
 from starkware.starkware_utils.error_handling import StarkErrorCode
@@ -33,22 +34,16 @@ def __init__(self, origin: Origin, lite=False) -> None:
         self.__state_updates: Dict[int, BlockStateUpdate] = {}
         self.__hash2num: Dict[str, int] = {}
 
-    def get_last_block(self) -> StarknetBlock:
+    async def get_last_block(self) -> StarknetBlock:
         """Returns the last block stored so far."""
         number_of_blocks = self.get_number_of_blocks()
-        return self.get_by_number(number_of_blocks - 1)
+        return await self.get_by_number(number_of_blocks - 1)
 
     def get_number_of_blocks(self) -> int:
         """Returns the number of blocks stored so far."""
         return len(self.__num2block) + self.origin.get_number_of_blocks()
 
-    def get_by_number(self, block_number: int) -> StarknetBlock:
-        """Returns the block whose block_number is provided"""
-        if block_number is None:
-            if self.__num2block:
-                return self.get_last_block()
-            return self.origin.get_block_by_number(block_number)
-
+    def __assert_block_number_in_range(self, block_number: BlockIdentifier):
         if block_number < 0:
             message = (
                 f"Block number must be a non-negative integer; got: {block_number}."
@@ -63,12 +58,20 @@ def get_by_number(self, block_number: int) -> StarknetBlock:
                 code=StarknetErrorCode.BLOCK_NOT_FOUND, message=message
             )
 
+    async def get_by_number(self, block_number: BlockIdentifier) -> StarknetBlock:
+        """Returns the block whose block_number is provided"""
+        if block_number is None:
+            if self.__num2block:
+                return await self.get_last_block()
+            return await self.origin.get_block_by_number(block_number)
+
+        self.__assert_block_number_in_range(block_number)
         if block_number in self.__num2block:
             return self.__num2block[block_number]
 
-        return self.origin.get_block_by_number(block_number)
+        return await self.origin.get_block_by_number(block_number)
 
-    def get_by_hash(self, block_hash: str) -> StarknetBlock:
+    async def get_by_hash(self, block_hash: str) -> StarknetBlock:
         """
         Returns the block with the given block hash.
         """
@@ -76,11 +79,13 @@ def get_by_hash(self, block_hash: str) -> StarknetBlock:
 
         if numeric_hash in self.__hash2num:
             block_number = self.__hash2num[int(block_hash, 16)]
-            return self.get_by_number(block_number)
+            return await self.get_by_number(block_number)
 
-        return self.origin.get_block_by_hash(block_hash)
+        return await self.origin.get_block_by_hash(block_hash)
 
-    def get_state_update(self, block_hash=None, block_number=None) -> BlockStateUpdate:
+    async def get_state_update(
+        self, block_hash=None, block_number=None
+    ) -> BlockStateUpdate:
         """
         Returns state update for the provided block hash or block number.
         It will return the last state update if block is not provided.
@@ -89,19 +94,20 @@ def get_state_update(self, block_hash=None, block_number=None) -> BlockStateUpda
             numeric_hash = int(block_hash, 16)
 
             if numeric_hash not in self.__hash2num:
-                return self.origin.get_state_update(block_hash=block_hash)
+                return await self.origin.get_state_update(block_hash=block_hash)
 
             block_number = self.__hash2num[numeric_hash]
 
         if block_number is not None:
-            if block_number not in self.__state_updates:
-                return self.origin.get_state_update(block_number=block_number)
+            self.__assert_block_number_in_range(block_number)
+            if block_number in self.__state_updates:
+                return self.__state_updates[block_number]
 
-            return self.__state_updates[block_number]
+            return await self.origin.get_state_update(block_number=block_number)
 
         return (
             self.__state_updates.get(self.get_number_of_blocks() - 1)
-            or self.origin.get_state_update()
+            or await self.origin.get_state_update()
         )
 
     async def generate(
@@ -122,7 +128,7 @@ async def generate(
         if block_number == 0:
             parent_block_hash = 0
         else:
-            last_block = self.get_last_block()
+            last_block = await self.get_last_block()
             parent_block_hash = last_block.block_hash
 
         if is_empty_block:
diff --git a/starknet_devnet/blueprints/feeder_gateway.py b/starknet_devnet/blueprints/feeder_gateway.py
index 4891149f6..e50014c6e 100644
--- a/starknet_devnet/blueprints/feeder_gateway.py
+++ b/starknet_devnet/blueprints/feeder_gateway.py
@@ -17,11 +17,13 @@
     CallL1Handler,
 )
 from starkware.starknet.services.api.feeder_gateway.response_objects import (
+    StarknetBlock,
     TransactionSimulationInfo,
 )
 from starkware.starkware_utils.error_handling import StarkErrorCode
 from werkzeug.datastructures import MultiDict
 
+
 from starknet_devnet.state import state
 from starknet_devnet.util import StarknetDevnetException, custom_int, fixed_length_hex
 
@@ -68,25 +70,25 @@ def _check_block_arguments(block_hash, block_number):
         )
 
 
-def _get_block_object(block_hash: str, block_number: int):
+async def _get_block_object(block_hash: str, block_number: int):
     """Returns the block object"""
 
     _check_block_arguments(block_hash, block_number)
 
     if block_hash is not None:
-        block = state.starknet_wrapper.blocks.get_by_hash(block_hash)
-    else:
-        block = state.starknet_wrapper.blocks.get_by_number(block_number)
+        return await state.starknet_wrapper.blocks.get_by_hash(block_hash)
 
-    return block
+    return await state.starknet_wrapper.blocks.get_by_number(block_number)
 
 
-def _get_block_transaction_traces(block):
+async def _get_block_transaction_traces(block: StarknetBlock):
     traces = []
     if block.transaction_receipts:
         for transaction in block.transaction_receipts:
             tx_hash = hex(transaction.transaction_hash)
-            trace = state.starknet_wrapper.transactions.get_transaction_trace(tx_hash)
+            trace = await state.starknet_wrapper.transactions.get_transaction_trace(
+                tx_hash
+            )
 
             # expected trace is equal to response of get_transaction, but with the hash property
             trace_dict = trace.dump()
@@ -121,32 +123,32 @@ async def call_contract():
 
 
 @feeder_gateway.route("/get_block", methods=["GET"])
-def get_block():
+async def get_block():
     """Endpoint for retrieving a block identified by its hash or number."""
 
     block_hash = request.args.get("blockHash")
     block_number = request.args.get("blockNumber", type=custom_int)
 
-    block = _get_block_object(block_hash=block_hash, block_number=block_number)
+    block = await _get_block_object(block_hash=block_hash, block_number=block_number)
 
     return Response(block.dumps(), status=200, mimetype="application/json")
 
 
 @feeder_gateway.route("/get_block_traces", methods=["GET"])
-def get_block_traces():
+async def get_block_traces():
     """Returns the traces of the transactions in the specified block."""
 
     block_hash = request.args.get("blockHash")
     block_number = request.args.get("blockNumber", type=custom_int)
 
-    block = _get_block_object(block_hash=block_hash, block_number=block_number)
-    block_transaction_traces = _get_block_transaction_traces(block)
+    block = await _get_block_object(block_hash=block_hash, block_number=block_number)
+    block_transaction_traces = await _get_block_transaction_traces(block)
 
     return jsonify(block_transaction_traces.dump())
 
 
 @feeder_gateway.route("/get_code", methods=["GET"])
-def get_code():
+async def get_code():
     """
     Returns the ABI and bytecode of the contract whose contractAddress is provided.
     """
@@ -154,12 +156,12 @@ def get_code():
     _check_block_hash(request.args)
 
     contract_address = request.args.get("contractAddress", type=custom_int)
-    result_dict = state.starknet_wrapper.contracts.get_code(contract_address)
-    return jsonify(result_dict)
+    code_dict = await state.starknet_wrapper.get_code(contract_address)
+    return jsonify(code_dict)
 
 
 @feeder_gateway.route("/get_full_contract", methods=["GET"])
-def get_full_contract():
+async def get_full_contract():
     """
     Returns the contract class of the contract whose contractAddress is provided.
     """
@@ -167,29 +169,26 @@ def get_full_contract():
 
     contract_address = request.args.get("contractAddress", type=custom_int)
 
-    contract_class = state.starknet_wrapper.contracts.get_full_contract(
-        contract_address
-    )
-
-    return jsonify(contract_class.dump())
+    contract_class = await state.starknet_wrapper.get_class_by_address(contract_address)
+    return jsonify(contract_class.remove_debug_info().dump())
 
 
 @feeder_gateway.route("/get_class_hash_at", methods=["GET"])
-def get_class_hash_at():
+async def get_class_hash_at():
     """Get contract class hash by contract address"""
 
     contract_address = request.args.get("contractAddress", type=custom_int)
-    class_hash = state.starknet_wrapper.contracts.get_class_hash_at(contract_address)
+    class_hash = await state.starknet_wrapper.get_class_hash_at(contract_address)
     return jsonify(fixed_length_hex(class_hash))
 
 
 @feeder_gateway.route("/get_class_by_hash", methods=["GET"])
-def get_class_by_hash():
+async def get_class_by_hash():
     """Get contract class by class hash"""
 
     class_hash = request.args.get("classHash", type=custom_int)
-    contract_class = state.starknet_wrapper.contracts.get_class_by_hash(class_hash)
-    return jsonify(contract_class.dump())
+    contract_class = await state.starknet_wrapper.get_class_by_hash(class_hash)
+    return jsonify(contract_class.remove_debug_info().dump())
 
 
 @feeder_gateway.route("/get_storage_at", methods=["GET"])
@@ -205,26 +204,26 @@ async def get_storage_at():
 
 
 @feeder_gateway.route("/get_transaction_status", methods=["GET"])
-def get_transaction_status():
+async def get_transaction_status():
     """
     Returns the status of the transaction identified by the transactionHash argument in the GET request.
     """
 
     transaction_hash = request.args.get("transactionHash")
-    transaction_status = state.starknet_wrapper.transactions.get_transaction_status(
+    tx_status = await state.starknet_wrapper.transactions.get_transaction_status(
         transaction_hash
     )
-    return jsonify(transaction_status)
+    return jsonify(tx_status)
 
 
 @feeder_gateway.route("/get_transaction", methods=["GET"])
-def get_transaction():
+async def get_transaction():
     """
     Returns the transaction identified by the transactionHash argument in the GET request.
     """
 
     transaction_hash = request.args.get("transactionHash")
-    transaction_info = state.starknet_wrapper.transactions.get_transaction(
+    transaction_info = await state.starknet_wrapper.transactions.get_transaction(
         transaction_hash
     )
     return Response(
@@ -233,28 +232,28 @@ def get_transaction():
 
 
 @feeder_gateway.route("/get_transaction_receipt", methods=["GET"])
-def get_transaction_receipt():
+async def get_transaction_receipt():
     """
     Returns the transaction receipt identified by the transactionHash argument in the GET request.
     """
 
     transaction_hash = request.args.get("transactionHash")
-    transaction_receipt = state.starknet_wrapper.transactions.get_transaction_receipt(
+    tx_receipt = await state.starknet_wrapper.transactions.get_transaction_receipt(
         transaction_hash
     )
     return Response(
-        response=transaction_receipt.dumps(), status=200, mimetype="application/json"
+        response=tx_receipt.dumps(), status=200, mimetype="application/json"
     )
 
 
 @feeder_gateway.route("/get_transaction_trace", methods=["GET"])
-def get_transaction_trace():
+async def get_transaction_trace():
     """
     Returns the trace of the transaction identified by the transactionHash argument in the GET request.
     """
 
     transaction_hash = request.args.get("transactionHash")
-    transaction_trace = state.starknet_wrapper.transactions.get_transaction_trace(
+    transaction_trace = await state.starknet_wrapper.transactions.get_transaction_trace(
         transaction_hash
     )
 
@@ -264,7 +263,7 @@ def get_transaction_trace():
 
 
 @feeder_gateway.route("/get_state_update", methods=["GET"])
-def get_state_update():
+async def get_state_update():
     """
     Returns the status update from the block identified by the blockHash argument in the GET request.
     If no block hash was provided it will default to the last block.
@@ -273,7 +272,7 @@ def get_state_update():
     block_hash = request.args.get("blockHash")
     block_number = request.args.get("blockNumber", type=custom_int)
 
-    state_update = state.starknet_wrapper.blocks.get_state_update(
+    state_update = await state.starknet_wrapper.blocks.get_state_update(
         block_hash=block_hash, block_number=block_number
     )
 
diff --git a/starknet_devnet/blueprints/rpc/blocks.py b/starknet_devnet/blueprints/rpc/blocks.py
index b4fb47d01..5d3f4dd70 100644
--- a/starknet_devnet/blueprints/rpc/blocks.py
+++ b/starknet_devnet/blueprints/rpc/blocks.py
@@ -12,7 +12,7 @@ async def get_block_with_tx_hashes(block_id: BlockId) -> dict:
     """
     Get block information with transaction hashes given the block id
     """
-    block = get_block_by_block_id(block_id)
+    block = await get_block_by_block_id(block_id)
     return await rpc_block(block=block)
 
 
@@ -20,7 +20,7 @@ async def get_block_with_txs(block_id: BlockId) -> dict:
     """
     Get block information with full transactions given the block id
     """
-    block = get_block_by_block_id(block_id)
+    block = await get_block_by_block_id(block_id)
     return await rpc_block(block=block, tx_type="FULL_TXNS")
 
 
@@ -44,7 +44,7 @@ async def block_hash_and_number() -> dict:
         raise RpcError(code=32, message="There are no blocks")
 
     last_block_number = number_of_blocks - 1
-    last_block = state.starknet_wrapper.blocks.get_by_number(last_block_number)
+    last_block = await state.starknet_wrapper.blocks.get_by_number(last_block_number)
 
     result = {
         "block_hash": rpc_felt(last_block.block_hash),
@@ -57,5 +57,5 @@ async def get_block_transaction_count(block_id: BlockId) -> int:
     """
     Get the number of transactions in a block given a block id
     """
-    block = get_block_by_block_id(block_id)
+    block = await get_block_by_block_id(block_id)
     return len(block.transactions)
diff --git a/starknet_devnet/blueprints/rpc/call.py b/starknet_devnet/blueprints/rpc/call.py
index b62523479..d0c208130 100644
--- a/starknet_devnet/blueprints/rpc/call.py
+++ b/starknet_devnet/blueprints/rpc/call.py
@@ -36,9 +36,9 @@ async def call(request: RpcFunctionCall, block_id: BlockId) -> List[Felt]:
     """
     Call a starknet function without creating a StarkNet transaction
     """
-    assert_block_id_is_latest_or_pending(block_id)
+    await assert_block_id_is_latest_or_pending(block_id)
 
-    if not state.starknet_wrapper.contracts.is_deployed(
+    if not await state.starknet_wrapper.is_deployed(
         int(request["contract_address"], 16)
     ):
         raise RpcError(code=20, message="Contract not found")
diff --git a/starknet_devnet/blueprints/rpc/classes.py b/starknet_devnet/blueprints/rpc/classes.py
index 388003282..3e073716b 100644
--- a/starknet_devnet/blueprints/rpc/classes.py
+++ b/starknet_devnet/blueprints/rpc/classes.py
@@ -2,6 +2,8 @@
 RPC classes endpoints
 """
 
+from starkware.starkware_utils.error_handling import StarkException
+
 from starknet_devnet.blueprints.rpc.utils import (
     assert_block_id_is_latest_or_pending,
     rpc_felt,
@@ -21,10 +23,10 @@ async def get_class(block_id: BlockId, class_hash: Felt) -> dict:
     """
     Get the contract class definition in the given block associated with the given hash
     """
-    assert_block_id_is_latest_or_pending(block_id)
+    await assert_block_id_is_latest_or_pending(block_id)
 
     try:
-        result = state.starknet_wrapper.contracts.get_class_by_hash(
+        result = await state.starknet_wrapper.get_class_by_hash(
             class_hash=int(class_hash, 16)
         )
     except StarknetDevnetException as ex:
@@ -37,13 +39,13 @@ async def get_class_hash_at(block_id: BlockId, contract_address: Address) -> Fel
     """
     Get the contract class hash in the given block for the contract deployed at the given address
     """
-    assert_block_id_is_latest_or_pending(block_id)
+    await assert_block_id_is_latest_or_pending(block_id)
 
     try:
-        result = state.starknet_wrapper.contracts.get_class_hash_at(
-            address=int(contract_address, 16)
+        result = await state.starknet_wrapper.get_class_hash_at(
+            int(contract_address, 16)
         )
-    except StarknetDevnetException as ex:
+    except StarkException as ex:
         raise RpcError(code=28, message="Class hash not found") from ex
 
     return rpc_felt(result)
@@ -53,16 +55,13 @@ async def get_class_at(block_id: BlockId, contract_address: Address) -> dict:
     """
     Get the contract class definition in the given block at the given address
     """
-    assert_block_id_is_latest_or_pending(block_id)
+    await assert_block_id_is_latest_or_pending(block_id)
 
     try:
-        class_hash = state.starknet_wrapper.contracts.get_class_hash_at(
-            address=int(contract_address, 16)
+        result = await state.starknet_wrapper.get_class_by_address(
+            int(contract_address, 16)
         )
-        result = state.starknet_wrapper.contracts.get_class_by_hash(
-            class_hash=class_hash
-        )
-    except StarknetDevnetException as ex:
+    except StarkException as ex:
         raise RpcError(code=20, message="Contract not found") from ex
 
     return rpc_contract_class(result)
diff --git a/starknet_devnet/blueprints/rpc/misc.py b/starknet_devnet/blueprints/rpc/misc.py
index df4a92dcc..b6b716c31 100644
--- a/starknet_devnet/blueprints/rpc/misc.py
+++ b/starknet_devnet/blueprints/rpc/misc.py
@@ -89,7 +89,7 @@ async def get_events(
         else int(to_block) + 1
     )
     for block_number in range(int(from_block), to_block):
-        block = state.starknet_wrapper.blocks.get_by_number(block_number)
+        block = await state.starknet_wrapper.blocks.get_by_number(block_number)
         if block.transaction_receipts:
             events.extend(get_events_from_block(block, address, keys))
 
@@ -109,9 +109,9 @@ async def get_nonce(block_id: BlockId, contract_address: Address) -> Felt:
     """
     Get the nonce associated with the given address in the given block
     """
-    assert_block_id_is_latest_or_pending(block_id)
+    await assert_block_id_is_latest_or_pending(block_id)
 
-    if not state.starknet_wrapper.contracts.is_deployed(int(contract_address, 16)):
+    if not await state.starknet_wrapper.is_deployed(int(contract_address, 16)):
         raise RpcError(code=20, message="Contract not found")
 
     result = await state.starknet_wrapper.get_nonce(
diff --git a/starknet_devnet/blueprints/rpc/routes.py b/starknet_devnet/blueprints/rpc/routes.py
index 3dd4fe030..508c29421 100644
--- a/starknet_devnet/blueprints/rpc/routes.py
+++ b/starknet_devnet/blueprints/rpc/routes.py
@@ -6,6 +6,7 @@
 
 from __future__ import annotations
 
+import inspect
 from typing import Callable, Dict, Union, List, Tuple
 
 from flask import Blueprint
@@ -83,6 +84,8 @@ async def base_route():
         result = await (
             method(*params) if isinstance(params, list) else method(**params)
         )
+        if inspect.iscoroutinefunction(result):
+            result = await result
     except TypeError as type_error:
         return rpc_error(message_id=message_id, code=22, message=str(type_error))
     except RpcError as error:
diff --git a/starknet_devnet/blueprints/rpc/state.py b/starknet_devnet/blueprints/rpc/state.py
index 50c649ec7..015e2cdab 100644
--- a/starknet_devnet/blueprints/rpc/state.py
+++ b/starknet_devnet/blueprints/rpc/state.py
@@ -17,11 +17,11 @@ async def get_state_update(block_id: BlockId) -> dict:
 
     try:
         if "block_hash" in block_id:
-            result = state.starknet_wrapper.blocks.get_state_update(
+            result = await state.starknet_wrapper.blocks.get_state_update(
                 block_hash=block_id["block_hash"]
             )
         else:
-            result = state.starknet_wrapper.blocks.get_state_update(
+            result = await state.starknet_wrapper.blocks.get_state_update(
                 block_number=block_id["block_number"]
             )
     except StarknetDevnetException as ex:
diff --git a/starknet_devnet/blueprints/rpc/storage.py b/starknet_devnet/blueprints/rpc/storage.py
index 91ae4e88c..072d67ddb 100644
--- a/starknet_devnet/blueprints/rpc/storage.py
+++ b/starknet_devnet/blueprints/rpc/storage.py
@@ -21,9 +21,9 @@ async def get_storage_at(
     """
     Get the value of the storage at the given address and key
     """
-    assert_block_id_is_latest_or_pending(block_id)
+    await assert_block_id_is_latest_or_pending(block_id)
 
-    if not state.starknet_wrapper.contracts.is_deployed(int(contract_address, 16)):
+    if not await state.starknet_wrapper.is_deployed(int(contract_address, 16)):
         raise RpcError(code=20, message="Contract not found")
 
     storage = await state.starknet_wrapper.get_storage_at(
diff --git a/starknet_devnet/blueprints/rpc/structures/responses.py b/starknet_devnet/blueprints/rpc/structures/responses.py
index be9ba1e4b..73d051ae2 100644
--- a/starknet_devnet/blueprints/rpc/structures/responses.py
+++ b/starknet_devnet/blueprints/rpc/structures/responses.py
@@ -105,51 +105,53 @@ class RpcDeployAccountReceipt(RpcBaseTransactionReceipt):
     contract_address: Felt
 
 
-def rpc_invoke_receipt(txr: TransactionReceipt) -> RpcInvokeReceipt:
+async def rpc_invoke_receipt(txr: TransactionReceipt) -> RpcInvokeReceipt:
     """
     Convert gateway invoke transaction receipt to rpc format
     """
-    return rpc_base_transaction_receipt(txr)
+    return await rpc_base_transaction_receipt(txr)
 
 
-def rpc_declare_receipt(txr: TransactionReceipt) -> RpcDeclareReceipt:
+async def rpc_declare_receipt(txr: TransactionReceipt) -> RpcDeclareReceipt:
     """
     Convert gateway declare transaction receipt to rpc format
     """
-    return rpc_base_transaction_receipt(txr)
+    return await rpc_base_transaction_receipt(txr)
 
 
-def rpc_deploy_receipt(txr: TransactionReceipt) -> RpcDeployReceipt:
+async def rpc_deploy_receipt(txr: TransactionReceipt) -> RpcDeployReceipt:
     """
     Convert gateway deploy transaction receipt to rpc format
     """
-    base_receipt = rpc_base_transaction_receipt(txr)
-    transaction = state.starknet_wrapper.transactions.get_transaction(
+    base_receipt = await rpc_base_transaction_receipt(txr)
+    transaction = await state.starknet_wrapper.transactions.get_transaction(
         hex(txr.transaction_hash)
-    ).transaction
+    )
 
     receipt: RpcDeployReceipt = {
-        "contract_address": rpc_felt(transaction.contract_address),
+        "contract_address": rpc_felt(transaction.transaction.contract_address),
         **base_receipt,
     }
     return receipt
 
 
-def rpc_deploy_account_receipt(txr: TransactionReceipt) -> RpcDeployAccountReceipt:
+async def rpc_deploy_account_receipt(
+    txr: TransactionReceipt,
+) -> RpcDeployAccountReceipt:
     """
     Convert gateway deploy account transaction receipt to rpc format
     """
-    return rpc_deploy_receipt(txr)
+    return await rpc_deploy_receipt(txr)
 
 
-def rpc_l1_handler_receipt(txr: TransactionReceipt) -> RpcL1HandlerReceipt:
+async def rpc_l1_handler_receipt(txr: TransactionReceipt) -> RpcL1HandlerReceipt:
     """
     Convert gateway l1 handler transaction receipt to rpc format
     """
-    return rpc_base_transaction_receipt(txr)
+    return await rpc_base_transaction_receipt(txr)
 
 
-def rpc_base_transaction_receipt(txr: TransactionReceipt) -> dict:
+async def rpc_base_transaction_receipt(txr: TransactionReceipt) -> dict:
     """
     Convert gateway transaction receipt to rpc base transaction receipt
     """
@@ -188,11 +190,11 @@ def status() -> str:
         }
         return mapping[txr.status]
 
-    def txn_type() -> RpcTxnType:
-        transaction = state.starknet_wrapper.transactions.get_transaction(
+    async def txn_type() -> RpcTxnType:
+        transaction = await state.starknet_wrapper.transactions.get_transaction(
             hex(txr.transaction_hash)
-        ).transaction
-        return rpc_txn_type(transaction.tx_type.name)
+        )
+        return rpc_txn_type(transaction.transaction.tx_type.name)
 
     receipt: RpcBaseTransactionReceipt = {
         "transaction_hash": rpc_felt(txr.transaction_hash),
@@ -202,12 +204,12 @@ def txn_type() -> RpcTxnType:
         "block_number": txr.block_number or None,
         "messages_sent": messages_sent(),
         "events": events(),
-        "type": txn_type(),
+        "type": await txn_type(),
     }
     return receipt
 
 
-def rpc_transaction_receipt(txr: TransactionReceipt) -> dict:
+async def rpc_transaction_receipt(txr: TransactionReceipt) -> dict:
     """
     Convert gateway transaction receipt to rpc format
     """
@@ -218,8 +220,8 @@ def rpc_transaction_receipt(txr: TransactionReceipt) -> dict:
         TransactionType.L1_HANDLER: rpc_l1_handler_receipt,
         TransactionType.DEPLOY_ACCOUNT: rpc_deploy_account_receipt,
     }
-    transaction = state.starknet_wrapper.transactions.get_transaction(
+    transaction = await state.starknet_wrapper.transactions.get_transaction(
         hex(txr.transaction_hash)
-    ).transaction
-    tx_type = transaction.tx_type
-    return tx_mapping[tx_type](txr)
+    )
+    tx_type = transaction.transaction.tx_type
+    return await tx_mapping[tx_type](txr)
diff --git a/starknet_devnet/blueprints/rpc/transactions.py b/starknet_devnet/blueprints/rpc/transactions.py
index 3ff430e89..db7145efe 100644
--- a/starknet_devnet/blueprints/rpc/transactions.py
+++ b/starknet_devnet/blueprints/rpc/transactions.py
@@ -52,7 +52,9 @@ async def get_transaction_by_hash(transaction_hash: TxnHash) -> dict:
     Get the details and status of a submitted transaction
     """
     try:
-        result = state.starknet_wrapper.transactions.get_transaction(transaction_hash)
+        result = await state.starknet_wrapper.transactions.get_transaction(
+            transaction_hash
+        )
     except StarknetDevnetException as ex:
         raise RpcError(code=25, message="Transaction hash not found") from ex
 
@@ -66,7 +68,7 @@ async def get_transaction_by_block_id_and_index(block_id: BlockId, index: int) -
     """
     Get the details of a transaction by a given block id and index
     """
-    block = get_block_by_block_id(block_id)
+    block = await get_block_by_block_id(block_id)
 
     try:
         transaction_hash: int = block.transactions[index].transaction_hash
@@ -81,7 +83,7 @@ async def get_transaction_receipt(transaction_hash: TxnHash) -> dict:
     Get the transaction receipt by the transaction hash
     """
     try:
-        result = state.starknet_wrapper.transactions.get_transaction_receipt(
+        result = await state.starknet_wrapper.transactions.get_transaction_receipt(
             tx_hash=transaction_hash
         )
     except StarknetDevnetException as ex:
@@ -90,7 +92,7 @@ async def get_transaction_receipt(transaction_hash: TxnHash) -> dict:
     if result.status == TransactionStatus.NOT_RECEIVED:
         raise RpcError(code=25, message="Transaction hash not found")
 
-    return rpc_transaction_receipt(result)
+    return await rpc_transaction_receipt(result)
 
 
 async def pending_transactions() -> List[RpcTransaction]:
@@ -158,7 +160,7 @@ async def add_deploy_account_transaction(
         external_tx=deploy_account_tx
     )
 
-    status_response = state.starknet_wrapper.transactions.get_transaction_status(
+    status_response = await state.starknet_wrapper.transactions.get_transaction_status(
         hex(transaction_hash)
     )
     if (
@@ -193,7 +195,7 @@ async def estimate_fee(request: RpcBroadcastedTxn, block_id: BlockId) -> dict:
     """
     Estimate the fee for a given StarkNet transaction
     """
-    assert_block_id_is_latest_or_pending(block_id)
+    await assert_block_id_is_latest_or_pending(block_id)
     transaction = make_transaction(request)
     try:
         _, fee_response = await state.starknet_wrapper.calculate_trace_and_fee(
diff --git a/starknet_devnet/blueprints/rpc/utils.py b/starknet_devnet/blueprints/rpc/utils.py
index b51cc0e51..938f0527a 100644
--- a/starknet_devnet/blueprints/rpc/utils.py
+++ b/starknet_devnet/blueprints/rpc/utils.py
@@ -33,7 +33,7 @@ def block_tag_to_block_number(block_id: BlockId) -> BlockId:
     return block_id
 
 
-def get_block_by_block_id(block_id: BlockId) -> dict:
+async def get_block_by_block_id(block_id: BlockId) -> dict:
     """
     Get block using different method depending on block_id type
     """
@@ -42,22 +42,22 @@ def get_block_by_block_id(block_id: BlockId) -> dict:
 
     try:
         if "block_hash" in block_id:
-            return state.starknet_wrapper.blocks.get_by_hash(
+            return await state.starknet_wrapper.blocks.get_by_hash(
                 block_hash=block_id["block_hash"]
             )
-        return state.starknet_wrapper.blocks.get_by_number(
+        return await state.starknet_wrapper.blocks.get_by_number(
             block_number=block_id["block_number"]
         )
     except StarknetDevnetException as ex:
         raise RpcError(code=24, message="Block not found") from ex
 
 
-def assert_block_id_is_latest_or_pending(block_id: BlockId) -> None:
+async def assert_block_id_is_latest_or_pending(block_id: BlockId) -> None:
     """
     Assert block_id is "latest"/"pending" or a block hash or number of "latest"/"pending" block and throw RpcError otherwise
     """
     if isinstance(block_id, dict):
-        last_block = state.starknet_wrapper.blocks.get_last_block()
+        last_block = await state.starknet_wrapper.blocks.get_last_block()
 
         if "block_hash" in block_id and "block_number" in block_id:
             raise RpcError(
diff --git a/starknet_devnet/contract_wrapper.py b/starknet_devnet/contract_wrapper.py
deleted file mode 100644
index 3f2d3141a..000000000
--- a/starknet_devnet/contract_wrapper.py
+++ /dev/null
@@ -1,51 +0,0 @@
-"""
-Contains code for wrapping StarknetContract instances.
-"""
-
-from typing import List
-
-from starkware.starknet.services.api.contract_class import ContractClass
-from starkware.starknet.testing.contract import StarknetContract
-
-
-class ContractWrapper:
-    """
-    Wraps a StarknetContract, storing its types and code for later use.
-    """
-
-    def __init__(
-        self,
-        contract: StarknetContract,
-        contract_class: ContractClass,
-        deployment_tx_hash: int = None,
-    ):
-        self.contract: StarknetContract = contract
-        self.contract_class = contract_class.remove_debug_info()
-        self.deployment_tx_hash = deployment_tx_hash
-
-        self.code: dict = {
-            "abi": contract_class.abi,
-            "bytecode": self.contract_class.dump()["program"]["data"],
-        }
-
-    # pylint: disable=too-many-arguments
-    async def call(
-        self,
-        entry_point_selector: int,
-        calldata: List[int],
-        caller_address: int,
-    ):
-        """
-        Calls the function identified with `entry_point_selector`, potentially passing in `calldata` and `signature`.
-        """
-
-        call_info = await self.contract.state.copy().execute_entry_point_raw(
-            contract_address=self.contract.contract_address,
-            selector=entry_point_selector,
-            calldata=calldata,
-            caller_address=caller_address,
-        )
-
-        result = list(map(hex, call_info.retdata))
-
-        return result
diff --git a/starknet_devnet/contracts.py b/starknet_devnet/contracts.py
deleted file mode 100644
index 24db4d715..000000000
--- a/starknet_devnet/contracts.py
+++ /dev/null
@@ -1,87 +0,0 @@
-"""
-Class for storing and handling contracts
-"""
-
-from typing import Dict
-
-from starkware.starknet.definitions.error_codes import StarknetErrorCode
-from starkware.starknet.services.api.contract_class import ContractClass
-
-from .origin import Origin
-from .util import StarknetDevnetException
-from .contract_wrapper import ContractWrapper
-
-
-class DevnetContracts:
-    """
-    This class is used to store the deployed contracts of the devnet.
-    """
-
-    def __init__(self, origin: Origin):
-        self.origin = origin
-        self.__instances: Dict[int, ContractWrapper] = {}
-        self.__classes: Dict[int, ContractClass] = {}
-        self.__class_hashes: Dict[int, int] = {}
-
-    def store(
-        self, address: int, class_hash: int, contract_wrapper: ContractWrapper
-    ) -> None:
-        """
-        Store the contract wrapper.
-        """
-        self.__instances[address] = contract_wrapper
-
-        self.__classes[class_hash] = contract_wrapper.contract_class
-        self.__class_hashes[address] = class_hash
-
-    def store_class(self, class_hash: int, contract_class: ContractClass) -> None:
-        """Store contract class."""
-        self.__classes[class_hash] = contract_class.remove_debug_info()
-
-    def is_deployed(self, address: int) -> bool:
-        """
-        Check if the contract is deployed.
-        """
-        return address in self.__instances
-
-    def get_by_address(self, address: int) -> ContractWrapper:
-        """
-        Get the contract wrapper by address.
-        """
-        if not self.is_deployed(address):
-            message = f"Requested contract address {hex(address)} is not deployed."
-            raise StarknetDevnetException(
-                code=StarknetErrorCode.UNINITIALIZED_CONTRACT, message=message
-            )
-
-        return self.__instances[address]
-
-    def get_code(self, address: int) -> str:
-        """
-        Get the contract code by address.
-        """
-        if not self.is_deployed(address):
-            return self.origin.get_code(address)
-
-        return self.__instances[address].code
-
-    def get_full_contract(self, address: int) -> ContractClass:
-        """
-        Get the contract wrapper by address.
-        """
-        contract_wrapper = self.get_by_address(address)
-        return contract_wrapper.contract_class
-
-    def get_class_by_hash(self, class_hash: int) -> ContractClass:
-        """Gets the class from the provided class_hash."""
-        if class_hash not in self.__classes:
-            return self.origin.get_class_by_hash(class_hash)
-
-        return self.__classes[class_hash]
-
-    def get_class_hash_at(self, address: int) -> int:
-        """Gets the class hash at the provided address."""
-        if not self.is_deployed(address):
-            return self.origin.get_class_hash_at(address)
-
-        return self.__class_hashes[address]
diff --git a/starknet_devnet/devnet_config.py b/starknet_devnet/devnet_config.py
index b63374a1d..74481a306 100644
--- a/starknet_devnet/devnet_config.py
+++ b/starknet_devnet/devnet_config.py
@@ -1,16 +1,23 @@
 """Module for configuration specified by user"""
 
 import argparse
+import asyncio
+import contextlib
 from enum import Enum, auto
 import json
 import os
 import sys
 from typing import List
 
+from aiohttp.client_exceptions import ClientConnectorError, InvalidURL
 from marshmallow.exceptions import ValidationError
+from services.external_api.client import BadRequest, RetryConfig
 from starkware.python.utils import to_bytes
 from starkware.starknet.core.os.class_hash import compute_class_hash
 from starkware.starknet.services.api.contract_class import ContractClass
+from starkware.starknet.services.api.feeder_gateway.feeder_gateway_client import (
+    FeederGatewayClient,
+)
 
 from .contract_class_wrapper import (
     ContractClassWrapper,
@@ -28,18 +35,36 @@
 )
 
 
-# Uncomment this once fork support is added
-# def _fork_url(name: str):
-#     """
-#     Return the URL corresponding to the provided name.
-#     If it's not one of predefined names, assumes it is already a URL.
-#     """
-#     if name in ["alpha", "alpha-goerli"]:
-#         return "https://alpha4.starknet.io"
-#     if name == "alpha-mainnet":
-#         return "https://alpha-mainnet.starknet.io"
-#     # otherwise a URL; perhaps check validity
-#     return name
+NETWORK_TO_URL = {
+    "alpha-goerli": "https://alpha4.starknet.io",
+    "alpha-goerli2": "https://alpha4-2.starknet.io",
+    "alpha-mainnet": "https://alpha-mainnet.starknet.io",
+}
+NETWORK_NAMES = ", ".join(NETWORK_TO_URL.keys())
+
+
+def _fork_network(network_id: str):
+    """
+    Return the URL corresponding to the provided name.
+    If it's not one of predefined names, assumes it is already a URL.
+    """
+    return NETWORK_TO_URL.get(network_id, network_id)
+
+
+def _fork_block(specifier: str):
+    """Parse block specifier; allows int and 'latest'"""
+    if specifier == "latest":
+        return specifier
+
+    try:
+        parsed = int(specifier)
+        assert parsed > 0
+    except (AssertionError, ValueError):
+        sys.exit(
+            f"The value of --fork-block must be a non-negative integer or 'latest', got: {specifier}"
+        )
+
+    return parsed
 
 
 class DumpOn(Enum):
@@ -99,6 +124,37 @@ def _parse_account_class(class_path: str) -> ContractClassWrapper:
     return ContractClassWrapper(contract_class, class_hash_bytes)
 
 
+def _get_feeder_gateway_client(url: str, block_id: str):
+    """Construct a feeder gateway client at url and block"""
+
+    feeder_gateway_client = FeederGatewayClient(
+        url=url,
+        retry_config=RetryConfig(n_retries=1),
+    )
+
+    try:
+        # FeederGatewayClient is implemented in such a way that it logs and raises;
+        # this suppreses the logging
+        with contextlib.redirect_stderr(None):
+            block = asyncio.run(feeder_gateway_client.get_block(block_number=block_id))
+            block_number = block.block_number
+    except InvalidURL:
+        sys.exit(
+            f"Error: Invalid fork-network (must be a URL or one of {{{NETWORK_NAMES}}}). Received: {url}"
+        )
+    except BadRequest as bad_request:
+        if bad_request.status_code == 404:
+            msg = f"Error: {url} is not a valid StarkNet sequencer"
+        else:
+            msg = f"Error: {bad_request}"
+
+        sys.exit(msg)
+    except ClientConnectorError as error:
+        sys.exit(f"Error: {error}")
+
+    return feeder_gateway_client, block_number
+
+
 class NonNegativeAction(argparse.Action):
     """
     Action for parsing the non negative int argument.
@@ -122,7 +178,7 @@ def parse_args(raw_args: List[str]):
     Parses CLI arguments.
     """
     parser = argparse.ArgumentParser(
-        description="Run a local instance of Starknet Devnet"
+        description="Run a local instance of StarkNet Devnet"
     )
     parser.add_argument(
         "-v",
@@ -134,7 +190,7 @@ def parse_args(raw_args: List[str]):
     parser.add_argument(
         "--host",
         help=f"Specify the address to listen at; defaults to {DEFAULT_HOST} "
-        + "(use the address the program outputs on start)",
+        "(use the address the program outputs on start)",
         default=DEFAULT_HOST,
     )
     parser.add_argument(
@@ -156,7 +212,7 @@ def parse_args(raw_args: List[str]):
     parser.add_argument(
         "--lite-mode",
         action="store_true",
-        help="Introduces speed-up by skipping block hash and deploy transaction hash calculation"
+        help="Introduces speed-up by skipping block hash calculation"
         " - applies sequential numbering instead (0x0, 0x1, 0x2, ...).",
     )
     parser.add_argument(
@@ -170,7 +226,7 @@ def parse_args(raw_args: List[str]):
         "-e",
         action=NonNegativeAction,
         help="Specify the initial balance of accounts to be predeployed; "
-        + f"defaults to {DEFAULT_INITIAL_BALANCE:g}",
+        f"defaults to {DEFAULT_INITIAL_BALANCE:g}",
         default=DEFAULT_INITIAL_BALANCE,
     )
     parser.add_argument(
@@ -210,18 +266,31 @@ def parse_args(raw_args: List[str]):
         type=_parse_account_class,
         default=DEFAULT_ACCOUNT_PATH,
     )
-    # Uncomment this once fork support is added
-    # parser.add_argument(
-    #     "--fork", "-f",
-    #     type=_fork_url,
-    #     help="Specify the network to fork: can be a URL (e.g. https://alpha-mainnet.starknet.io) " +
-    #          "or network name (alpha or alpha-mainnet)",
-    # )
+    parser.add_argument(
+        "--fork-network",
+        type=_fork_network,
+        help="Specify the network to fork: can be a URL (e.g. https://alpha-mainnet.starknet.io) "
+        f"or network name (valid names: {', '.join(NETWORK_TO_URL.keys())})",
+    )
+    parser.add_argument(
+        "--fork-block",
+        type=_fork_block,
+        help="Specify the block number where the --fork-network is forked; defaults to latest",
+    )
 
     parsed_args = parser.parse_args(raw_args)
     if parsed_args.dump_on and not parsed_args.dump_path:
         sys.exit("Error: --dump-path required if --dump-on present")
 
+    if parsed_args.fork_block and not parsed_args.fork_network:
+        sys.exit("Error: --fork-network required if --fork-block present")
+
+    if parsed_args.fork_network:
+        parsed_args.fork_block = parsed_args.fork_block or "latest"
+        parsed_args.fork_network, parsed_args.fork_block = _get_feeder_gateway_client(
+            parsed_args.fork_network, parsed_args.fork_block
+        )
+
     return parsed_args
 
 
@@ -241,3 +310,5 @@ def __init__(self, args: argparse.Namespace = None):
         self.lite_mode = self.args.lite_mode
         self.account_class = self.args.account_class
         self.hide_predeployed_accounts = self.args.hide_predeployed_accounts
+        self.fork_network = self.args.fork_network
+        self.fork_block = self.args.fork_block
diff --git a/starknet_devnet/fee_token.py b/starknet_devnet/fee_token.py
index 7df330814..c9162ec23 100644
--- a/starknet_devnet/fee_token.py
+++ b/starknet_devnet/fee_token.py
@@ -2,15 +2,15 @@
 Fee token and its predefined constants.
 """
 
+from starkware.python.utils import to_bytes
 from starkware.solidity.utils import load_nearby_contract
 from starkware.starknet.services.api.contract_class import ContractClass
 from starkware.starknet.services.api.gateway.transaction import InvokeFunction
 from starkware.starknet.testing.contract import StarknetContract
-from starkware.python.utils import to_bytes
 from starkware.starknet.compiler.compile import get_selector_from_name
 from starkware.starknet.testing.starknet import Starknet
-from starknet_devnet.sequencer_api_utils import InternalInvokeFunction
 
+from starknet_devnet.sequencer_api_utils import InternalInvokeFunction
 from starknet_devnet.util import Uint256, str_to_felt
 
 
@@ -24,10 +24,9 @@ class FeeToken:
     HASH = 3000409729603134799471314790024123407246450023546294072844903167350593031855
     HASH_BYTES = to_bytes(HASH)
 
-    # Precalculated to fixed address
-    # ADDRESS = calculate_contract_address_from_hash(salt=10, class_hash=HASH,
-    # constructor_calldata=[], deployer_address=0)
-    ADDRESS = 0x62230EA046A9A5FBC261AC77D03C8D41E5D442DB2284587570AB46455FD2488
+    # Taken from
+    # https://github.com/starknet-community-libs/starknet-addresses/blob/df19b17d2c83f11c30e65e2373e8a0c65446f17c/bridged_tokens/goerli.json
+    ADDRESS = 0x49D36570D4E46F48E99674BD3FCC84644DDD6B96F7C741B1562B82F9E004DC7
     SYMBOL = "ETH"
     NAME = "ether"
 
@@ -53,10 +52,14 @@ async def deploy(self):
         await starknet.state.state.set_contract_class(
             FeeToken.HASH_BYTES, contract_class
         )
-        await starknet.state.state.deploy_contract(
-            FeeToken.ADDRESS, FeeToken.HASH_BYTES
-        )
 
+        # pylint: disable=protected-access
+        starknet.state.state.cache._class_hash_writes[
+            FeeToken.ADDRESS
+        ] = FeeToken.HASH_BYTES
+        # replace with await starknet.state.state.deploy_contract
+
+        # mimic constructor
         await starknet.state.state.set_storage_at(
             FeeToken.ADDRESS,
             get_selector_from_name("ERC20_name"),
@@ -68,7 +71,9 @@ async def deploy(self):
             str_to_felt(FeeToken.SYMBOL),
         )
         await starknet.state.state.set_storage_at(
-            FeeToken.ADDRESS, get_selector_from_name("ERC20_decimals"), 18
+            FeeToken.ADDRESS,
+            get_selector_from_name("ERC20_decimals"),
+            18,
         )
 
         self.contract = StarknetContract(
@@ -78,10 +83,6 @@ async def deploy(self):
             deploy_call_info=None,
         )
 
-        await self.starknet_wrapper.store_contract(
-            FeeToken.ADDRESS, self.contract, contract_class
-        )
-
     async def get_balance(self, address: int) -> int:
         """Return the balance of the contract under `address`."""
         response = await self.contract.balanceOf(address).call()
diff --git a/starknet_devnet/forked_state.py b/starknet_devnet/forked_state.py
new file mode 100644
index 000000000..3de5739ea
--- /dev/null
+++ b/starknet_devnet/forked_state.py
@@ -0,0 +1,111 @@
+"""Forked state"""
+
+import contextlib
+import json
+
+from services.external_api.client import BadRequest
+from starkware.python.utils import to_bytes
+from starkware.starknet.business_logic.state.state import BlockInfo, CachedState
+from starkware.starknet.business_logic.state.state_api import StateReader
+from starkware.starknet.definitions.constants import UNINITIALIZED_CLASS_HASH
+from starkware.starknet.services.api.contract_class import ContractClass
+from starkware.starknet.services.api.feeder_gateway.feeder_gateway_client import (
+    FeederGatewayClient,
+)
+from starkware.starknet.testing.starknet import Starknet
+from starkware.starknet.testing.state import StarknetState
+from starkware.starkware_utils.error_handling import StarkException
+
+from .block_info_generator import now
+from .general_config import DEFAULT_GENERAL_CONFIG
+
+
+def is_originally_starknet_exception(exc: BadRequest):
+    """
+    Return `True` if `exc` matches scheme of a Starknet exception.
+    Oterhwise return `False`.
+    """
+    try:
+        loaded = json.loads(exc.text)
+        assert loaded["code"]
+        assert loaded["message"]
+        return True
+    except (AssertionError, json.decoder.JSONDecodeError):
+        return False
+
+
+class ForkedStateReader(StateReader):
+    """State with a fallback to a forked origin"""
+
+    def __init__(
+        self,
+        feeder_gateway_client: FeederGatewayClient,
+        block_number: int,
+    ):
+        self.__feeder_gateway_client = feeder_gateway_client
+        self.__block_number = block_number
+
+    async def get_contract_class(self, class_hash: bytes) -> ContractClass:
+        try:
+            with contextlib.redirect_stderr(None):
+                class_hash_hex = "0x" + class_hash.hex()
+                contract_class_dict = (
+                    await self.__feeder_gateway_client.get_class_by_hash(class_hash_hex)
+                )
+                return ContractClass.load(contract_class_dict)
+        except BadRequest as bad_request:
+            original_error = StarkException(**json.loads(bad_request.text))
+            raise original_error from bad_request
+
+    async def _get_raw_contract_class(self, class_hash: bytes) -> bytes:
+        raise NotImplementedError
+
+    async def get_class_hash_at(self, contract_address: int) -> bytes:
+        try:
+            with contextlib.redirect_stderr(None):
+                class_hash_hex = await self.__feeder_gateway_client.get_class_hash_at(
+                    contract_address=contract_address,
+                    block_number=self.__block_number,
+                )
+            return to_bytes(int(class_hash_hex, 16))
+        except BadRequest as bad_request:
+            if is_originally_starknet_exception(bad_request):
+                return UNINITIALIZED_CLASS_HASH
+            raise
+
+    async def get_nonce_at(self, contract_address: int) -> int:
+        return await self.__feeder_gateway_client.get_nonce(
+            contract_address=contract_address,
+            block_number=self.__block_number,
+        )
+
+    async def get_storage_at(self, contract_address: int, key: int) -> int:
+        storage_hex = await self.__feeder_gateway_client.get_storage_at(
+            contract_address=contract_address,
+            key=key,
+            block_number=self.__block_number,
+        )
+        return int(storage_hex, 16)
+
+
+def get_forked_starknet(
+    feeder_gateway_client: FeederGatewayClient, block_number: int, gas_price: int
+) -> Starknet:
+    """Return a forked Starknet"""
+    state_reader = ForkedStateReader(
+        feeder_gateway_client=feeder_gateway_client,
+        block_number=block_number,
+    )
+    return Starknet(
+        state=StarknetState(
+            state=CachedState(
+                block_info=BlockInfo.create_for_testing(
+                    block_number=block_number,
+                    block_timestamp=now(),
+                    gas_price=gas_price,
+                ),
+                state_reader=state_reader,
+            ),
+            general_config=DEFAULT_GENERAL_CONFIG,
+        )
+    )
diff --git a/starknet_devnet/lite_mode/__init__.py b/starknet_devnet/lite_mode/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/starknet_devnet/lite_mode/lite_internal_deploy.py b/starknet_devnet/lite_mode/lite_internal_deploy.py
deleted file mode 100644
index bd752f9af..000000000
--- a/starknet_devnet/lite_mode/lite_internal_deploy.py
+++ /dev/null
@@ -1,101 +0,0 @@
-"""
-This module introduces `LiteInternalDeploy`, optimized lite-mode version of InternalDeploy.
-"""
-from typing import List
-
-from starkware.starknet.business_logic.transaction.objects import InternalDeploy
-from starkware.starknet.services.api.gateway.transaction import (
-    Deploy,
-    Transaction,
-    EverestTransaction,
-)
-from starkware.starknet.services.api.contract_class import ContractClass
-from starkware.starknet.business_logic.utils import verify_version
-from starkware.starknet.core.os.contract_address.contract_address import (
-    calculate_contract_address_from_hash,
-)
-from starkware.starknet.core.os.class_hash import compute_class_hash
-from starkware.starknet.business_logic.transaction.objects import InternalTransaction
-from starkware.python.utils import to_bytes
-
-from starknet_devnet.constants import OLD_SUPPORTED_VERSIONS
-
-# pylint: disable=too-many-ancestors, arguments-renamed, too-many-arguments
-class LiteInternalDeploy(InternalDeploy):
-    """
-    The lite version of InternalDeploy which avoid transaction hash a calculation in deploy.
-    """
-
-    @classmethod
-    def _specific_from_external(
-        cls,
-        external_tx: Transaction,
-        tx_number: int,
-    ) -> "LiteInternalDeploy":
-        """
-        Lite version of _specific_from_external method.
-        """
-        assert isinstance(external_tx, Deploy)
-        return cls.lite_create(
-            contract_address_salt=external_tx.contract_address_salt,
-            contract_class=external_tx.contract_definition,
-            constructor_calldata=external_tx.constructor_calldata,
-            version=external_tx.version,
-            tx_number=tx_number,
-        )
-
-    @classmethod
-    def from_external(
-        cls, external_tx: EverestTransaction, tx_number: int
-    ) -> InternalTransaction:
-        """
-        Returns an internal transaction genearated based on an external one.
-        """
-        # Downcast arguments to application-specific types.
-        assert isinstance(external_tx, Transaction)
-
-        internal_cls = LiteInternalDeploy.external_to_internal_cls.get(
-            type(external_tx)
-        )
-        if internal_cls is None:
-            raise NotImplementedError(
-                f"Unsupported transaction type {type(external_tx).__name__}."
-            )
-
-        return LiteInternalDeploy._specific_from_external(
-            external_tx=external_tx, tx_number=tx_number
-        )
-
-    @classmethod
-    def lite_create(
-        cls,
-        contract_address_salt: int,
-        contract_class: ContractClass,
-        constructor_calldata: List[int],
-        version: int,
-        tx_number: int,
-    ):
-        """
-        Lite version of create method without hash a calculation.
-        """
-        verify_version(
-            version=version,
-            only_query=False,
-            old_supported_versions=OLD_SUPPORTED_VERSIONS,
-        )
-        class_hash = compute_class_hash(contract_class=contract_class)
-        contract_address = calculate_contract_address_from_hash(
-            salt=contract_address_salt,
-            class_hash=class_hash,
-            constructor_calldata=constructor_calldata,
-            deployer_address=0,
-        )
-
-        return cls(
-            contract_address=contract_address,
-            contract_address_salt=contract_address_salt,
-            contract_hash=to_bytes(class_hash),
-            constructor_calldata=constructor_calldata,
-            version=version,
-            hash_value=tx_number,
-        )
diff --git a/starknet_devnet/lite_mode/lite_starknet.py b/starknet_devnet/lite_mode/lite_starknet.py
deleted file mode 100644
index 37b8f4fb3..000000000
--- a/starknet_devnet/lite_mode/lite_starknet.py
+++ /dev/null
@@ -1,66 +0,0 @@
-"""
-This module introduces `LiteStarknet`, optimized lite-mode version of Starknet.
-"""
-from typing import List, Union, Optional
-
-from starkware.python.utils import as_non_optional
-from starkware.starknet.testing.starknet import Starknet
-from starkware.starknet.testing.objects import StarknetCallInfo
-from starkware.starknet.services.api.contract_class import ContractClass
-from starkware.starknet.testing.contract import StarknetContract
-from starkware.starknet.testing.contract_utils import (
-    get_abi,
-    get_contract_class,
-)
-
-from .lite_starknet_state import LiteStarknetState
-
-CastableToAddressSalt = Union[str, int]
-
-# pylint: disable=too-many-arguments, arguments-differ)
-class LiteStarknet(Starknet):
-    """
-    The lite version of Starknet which avoid transaction hash calculation in deploy.
-    """
-
-    async def deploy(
-        self,
-        starknet: Starknet,
-        tx_number: int,
-        source: Optional[str] = None,
-        contract_class: Optional[ContractClass] = None,
-        contract_address_salt: Optional[CastableToAddressSalt] = None,
-        cairo_path: Optional[List[str]] = None,
-        constructor_calldata: Optional[List[int]] = None,
-        disable_hint_validation: bool = False,
-    ) -> StarknetContract:
-        contract_class = get_contract_class(
-            source=source,
-            contract_class=contract_class,
-            cairo_path=cairo_path,
-            disable_hint_validation=disable_hint_validation,
-        )
-
-        address, execution_info = await LiteStarknetState.deploy(
-            self,
-            contract_class=contract_class,
-            contract_address_salt=contract_address_salt,
-            constructor_calldata=[]
-            if constructor_calldata is None
-            else constructor_calldata,
-            starknet=starknet,
-            tx_number=tx_number,
-        )
-
-        deploy_call_info = StarknetCallInfo.from_internal(
-            call_info=as_non_optional(execution_info.call_info),
-            result=(),
-            main_call_events=[],
-        )
-
-        return StarknetContract(
-            state=starknet.state,
-            abi=get_abi(contract_class=contract_class),
-            contract_address=address,
-            deploy_call_info=deploy_call_info,
-        )
diff --git a/starknet_devnet/lite_mode/lite_starknet_state.py b/starknet_devnet/lite_mode/lite_starknet_state.py
deleted file mode 100644
index 182ae5f23..000000000
--- a/starknet_devnet/lite_mode/lite_starknet_state.py
+++ /dev/null
@@ -1,51 +0,0 @@
-"""
-This module introduces `LiteStarknetState`, optimized lite-mode version of StarknetState.
-"""
-from typing import List, Tuple, Union, Optional
-
-from starkware.starknet.definitions import constants, fields
-from starkware.starknet.testing.starknet import (
-    Starknet,
-    StarknetState,
-)
-from starkware.starknet.services.api.contract_class import ContractClass
-from starkware.starknet.business_logic.execution.objects import TransactionExecutionInfo
-
-from .lite_internal_deploy import LiteInternalDeploy
-
-CastableToAddressSalt = Union[str, int]
-
-# pylint: disable=arguments-differ, too-many-arguments
-class LiteStarknetState(StarknetState):
-    """
-    The lite version of StarknetState which avoid transaction hash calculation in deploy.
-    """
-
-    async def deploy(
-        self,
-        contract_class: ContractClass,
-        constructor_calldata: List[int],
-        starknet: Starknet,
-        tx_number: int,
-        contract_address_salt: Optional[CastableToAddressSalt] = None,
-    ) -> Tuple[int, TransactionExecutionInfo]:
-        if contract_address_salt is None:
-            contract_address_salt = fields.ContractAddressSalt.get_random_value()
-        if isinstance(contract_address_salt, str):
-            contract_address_salt = int(contract_address_salt, 16)
-        assert isinstance(contract_address_salt, int)
-
-        transaction = LiteInternalDeploy.lite_create(
-            contract_address_salt=contract_address_salt,
-            constructor_calldata=constructor_calldata,
-            contract_class=contract_class,
-            version=constants.TRANSACTION_VERSION,
-            tx_number=tx_number,
-        )
-
-        await starknet.state.state.set_contract_class(
-            class_hash=transaction.contract_hash, contract_class=contract_class
-        )
-        tx_execution_info = await starknet.state.execute_tx(tx=transaction)
-
-        return transaction.contract_address, tx_execution_info
diff --git a/starknet_devnet/origin.py b/starknet_devnet/origin.py
index 7b8c2c9f3..d404597e7 100644
--- a/starknet_devnet/origin.py
+++ b/starknet_devnet/origin.py
@@ -2,8 +2,11 @@
 Contains classes that provide the abstraction of L2 blockchain.
 """
 
+from services.external_api.client import BadRequest
 from starkware.starknet.definitions.error_codes import StarknetErrorCode
-from starkware.starknet.services.api.contract_class import ContractClass
+from starkware.starknet.services.api.feeder_gateway.feeder_gateway_client import (
+    FeederGatewayClient,
+)
 from starkware.starknet.services.api.feeder_gateway.response_objects import (
     TransactionStatus,
     TransactionInfo,
@@ -11,6 +14,7 @@
     TransactionTrace,
     StarknetBlock,
 )
+from starknet_devnet.forked_state import is_originally_starknet_exception
 
 from starknet_devnet.util import StarknetDevnetException
 
@@ -20,55 +24,37 @@ class Origin:
     Abstraction of an L2 blockchain.
     """
 
-    def get_transaction_status(self, transaction_hash: str):
+    async def get_transaction_status(self, transaction_hash: str):
         """Returns the status of the transaction."""
         raise NotImplementedError
 
-    def get_transaction(self, transaction_hash: str) -> TransactionInfo:
+    async def get_transaction(self, transaction_hash: str) -> TransactionInfo:
         """Returns the transaction object."""
         raise NotImplementedError
 
-    def get_transaction_receipt(self, transaction_hash: str) -> TransactionReceipt:
+    async def get_transaction_receipt(
+        self, transaction_hash: str
+    ) -> TransactionReceipt:
         """Returns the transaction receipt object."""
         raise NotImplementedError
 
-    def get_transaction_trace(self, transaction_hash: str) -> TransactionTrace:
+    async def get_transaction_trace(self, transaction_hash: str) -> TransactionTrace:
         """Returns the transaction trace object."""
         raise NotImplementedError
 
-    def get_block_by_hash(self, block_hash: str) -> StarknetBlock:
+    async def get_block_by_hash(self, block_hash: str) -> StarknetBlock:
         """Returns the block identified with either its hash."""
         raise NotImplementedError
 
-    def get_block_by_number(self, block_number: int) -> StarknetBlock:
+    async def get_block_by_number(self, block_number: int) -> StarknetBlock:
         """Returns the block identified with either its number or the latest block if no number provided."""
         raise NotImplementedError
 
-    def get_code(self, contract_address: int) -> dict:
-        """Returns the code of the contract."""
-        raise NotImplementedError
-
-    def get_full_contract(self, contract_address: int) -> dict:
-        """Returns the contract class"""
-        raise NotImplementedError
-
-    def get_class_by_hash(self, class_hash: int) -> ContractClass:
-        """Returns the contract class from its hash"""
-        raise NotImplementedError
-
-    def get_class_hash_at(self, contract_address: int) -> int:
-        """Returns the class hash at the provided address"""
-        raise NotImplementedError
-
-    def get_storage_at(self, contract_address: int, key: int) -> str:
-        """Returns the storage identified with `key` at `contract_address`."""
-        raise NotImplementedError
-
     def get_number_of_blocks(self):
         """Returns the number of blocks stored so far"""
         raise NotImplementedError
 
-    def get_state_update(
+    async def get_state_update(
         self, block_hash: str = None, block_number: int = None
     ) -> dict or None:
         """
@@ -83,18 +69,20 @@ class NullOrigin(Origin):
     A default class to comply with the Origin interface.
     """
 
-    def get_transaction_status(self, transaction_hash: str):
+    async def get_transaction_status(self, transaction_hash: str):
         return {"tx_status": TransactionStatus.NOT_RECEIVED.name}
 
-    def get_transaction(self, transaction_hash: str) -> TransactionInfo:
+    async def get_transaction(self, transaction_hash: str) -> TransactionInfo:
         return TransactionInfo.create(
             status=TransactionStatus.NOT_RECEIVED,
         )
 
-    def get_transaction_receipt(self, transaction_hash: str) -> TransactionReceipt:
+    async def get_transaction_receipt(
+        self, transaction_hash: str
+    ) -> TransactionReceipt:
         return TransactionReceipt(
             status=TransactionStatus.NOT_RECEIVED,
-            transaction_hash=int(transaction_hash, 16),
+            transaction_hash=0,  # testnet returns 0 instead of received hash
             events=[],
             l2_to_l1_messages=[],
             block_hash=None,
@@ -106,50 +94,29 @@ def get_transaction_receipt(self, transaction_hash: str) -> TransactionReceipt:
             l1_to_l2_consumed_message=None,
         )
 
-    def get_transaction_trace(self, transaction_hash: str):
+    async def get_transaction_trace(self, transaction_hash: str):
         tx_hash_int = int(transaction_hash, 16)
         message = f"Transaction corresponding to hash {tx_hash_int} is not found."
         raise StarknetDevnetException(
             code=StarknetErrorCode.INVALID_TRANSACTION_HASH, message=message
         )
 
-    def get_block_by_hash(self, block_hash: str):
+    async def get_block_by_hash(self, block_hash: str):
         message = f"Block hash not found; got: {block_hash}."
         raise StarknetDevnetException(
             code=StarknetErrorCode.BLOCK_NOT_FOUND, message=message
         )
 
-    def get_block_by_number(self, block_number: int):
+    async def get_block_by_number(self, block_number: int):
         message = "Requested the latest block, but there are no blocks so far."
         raise StarknetDevnetException(
             code=StarknetErrorCode.BLOCK_NOT_FOUND, message=message
         )
 
-    def get_code(self, contract_address: int):
-        return {"abi": {}, "bytecode": []}
-
-    def get_full_contract(self, contract_address: int) -> dict:
-        return {"abi": {}, "entry_points_by_type": {}, "program": {}}
-
-    def get_class_by_hash(self, class_hash: int) -> ContractClass:
-        message = f"Class with hash {hex(class_hash)} is not declared."
-        raise StarknetDevnetException(
-            code=StarknetErrorCode.UNDECLARED_CLASS, message=message
-        )
-
-    def get_class_hash_at(self, contract_address: int) -> int:
-        message = f"Contract with address {hex(contract_address)} is not deployed."
-        raise StarknetDevnetException(
-            code=StarknetErrorCode.UNINITIALIZED_CONTRACT, message=message
-        )
-
-    def get_storage_at(self, contract_address: int, key: int) -> str:
-        return hex(0)
-
     def get_number_of_blocks(self):
         return 0
 
-    def get_state_update(
+    async def get_state_update(
         self, block_hash: str = None, block_number: int = None
     ) -> dict or None:
         if block_hash:
@@ -174,44 +141,70 @@ class ForkedOrigin(Origin):
     Abstracts an origin that the devnet was forked from.
     """
 
-    def __init__(self, url):
-        self.url = url
-        self.number_of_blocks = ...
+    def __init__(
+        self, feeder_gateway_client: FeederGatewayClient, last_block_number: int
+    ):
+        self.__feeder_gateway_client = feeder_gateway_client
+        self.__number_of_blocks = last_block_number + 1
 
-    def get_transaction_status(self, transaction_hash: str):
-        raise NotImplementedError
-
-    def get_transaction(self, transaction_hash: str):
-        raise NotImplementedError
-
-    def get_transaction_trace(self, transaction_hash: str):
-        raise NotImplementedError
-
-    def get_block_by_hash(self, block_hash: str):
-        raise NotImplementedError
-
-    def get_block_by_number(self, block_number: int):
-        raise NotImplementedError
-
-    def get_code(self, contract_address: int) -> dict:
-        raise NotImplementedError
+    async def get_transaction_status(self, transaction_hash: str):
+        return await self.__feeder_gateway_client.get_transaction_status(
+            transaction_hash
+        )
 
-    def get_full_contract(self, contract_address: int) -> dict:
-        raise NotImplementedError
+    async def get_transaction(self, transaction_hash: str):
+        return await self.__feeder_gateway_client.get_transaction(transaction_hash)
 
-    def get_class_by_hash(self, class_hash: int) -> ContractClass:
-        raise NotImplementedError
+    async def get_transaction_receipt(
+        self, transaction_hash: str
+    ) -> TransactionReceipt:
+        return await self.__feeder_gateway_client.get_transaction_receipt(
+            transaction_hash
+        )
 
-    def get_class_hash_at(self, contract_address: int) -> int:
-        raise NotImplementedError
+    async def get_transaction_trace(self, transaction_hash: str):
+        try:
+            return await self.__feeder_gateway_client.get_transaction_trace(
+                transaction_hash
+            )
+        except BadRequest as bad_request:
+            if is_originally_starknet_exception(bad_request):
+                raise StarknetDevnetException(
+                    code=StarknetErrorCode.INVALID_TRANSACTION_HASH,
+                    message=f"Transaction corresponding to hash {transaction_hash} is not found.",
+                ) from bad_request
+            raise
+
+    async def get_block_by_hash(self, block_hash: str):
+        custom_exception = StarknetDevnetException(
+            code=StarknetErrorCode.BLOCK_NOT_FOUND,
+            message=f"Block hash {block_hash} does not exist.",
+        )
+        try:
+            block = await self.__feeder_gateway_client.get_block(block_hash=block_hash)
+            if block.block_number > self.get_number_of_blocks():
+                raise custom_exception
+        except BadRequest as bad_request:
+            if is_originally_starknet_exception(bad_request):
+                raise custom_exception from bad_request
 
-    def get_storage_at(self, contract_address: int, key: int) -> str:
-        raise NotImplementedError
+    async def get_block_by_number(self, block_number: int):
+        return await self.__feeder_gateway_client.get_block(block_number=block_number)
 
     def get_number_of_blocks(self):
-        return self.number_of_blocks
+        return self.__number_of_blocks
 
-    def get_state_update(
+    async def get_state_update(
         self, block_hash: str = None, block_number: int = None
     ) -> dict or None:
-        raise NotImplementedError
+        try:
+            return await self.__feeder_gateway_client.get_state_update(
+                block_hash=block_hash,
+                block_number=block_number,
+            )
+        except BadRequest as bad_request:
+            if is_originally_starknet_exception(bad_request):
+                raise StarknetDevnetException(
+                    code=StarknetErrorCode.BLOCK_NOT_FOUND,
+                    message=f"Block hash {block_hash} does not exist.",
+                ) from bad_request
diff --git a/starknet_devnet/server.py b/starknet_devnet/server.py
index 625d0ee3a..fb496970f 100644
--- a/starknet_devnet/server.py
+++ b/starknet_devnet/server.py
@@ -82,10 +82,6 @@ def load(self):
 def main():
     """Runs the server."""
 
-    # Uncomment this once fork support is added
-    # origin = Origin(args.fork) if args.fork else NullOrigin()
-    # starknet_wrapper.origin = origin
-
     args = parse_args(sys.argv[1:])
 
     try:
diff --git a/starknet_devnet/starknet_wrapper.py b/starknet_devnet/starknet_wrapper.py
index 271649595..a0079a253 100644
--- a/starknet_devnet/starknet_wrapper.py
+++ b/starknet_devnet/starknet_wrapper.py
@@ -7,7 +7,6 @@
 from typing import Dict, List, Optional, Set, Tuple, Type, Union
 
 import cloudpickle as pickle
-from starkware.python.utils import as_non_optional
 from starkware.starknet.business_logic.transaction.fee import calculate_tx_fee
 from starkware.starknet.business_logic.transaction.objects import (
     CallInfo,
@@ -23,13 +22,17 @@
 from starkware.starknet.core.os.contract_address.contract_address import (
     calculate_contract_address_from_hash,
 )
+from starkware.starknet.core.os.transaction_hash.transaction_hash import (
+    calculate_deploy_transaction_hash,
+)
+from starkware.starknet.definitions.error_codes import StarknetErrorCode
 from starkware.starknet.services.api.gateway.transaction import (
     InvokeFunction,
     Deploy,
     DeployAccount,
     Declare,
 )
-from starkware.starknet.testing.contract_utils import get_abi
+from starkware.starknet.testing.objects import FunctionInvocation
 from starkware.starknet.testing.starknet import Starknet
 from starkware.starkware_utils.error_handling import StarkException
 from starkware.starknet.services.api.contract_class import EntryPointType, ContractClass
@@ -40,8 +43,6 @@
 from starkware.starknet.services.api.feeder_gateway.response_objects import (
     TransactionStatus,
 )
-from starkware.starknet.testing.contract import StarknetContract
-from starkware.starknet.testing.objects import FunctionInvocation, StarknetCallInfo
 from starkware.starknet.third_party.open_zeppelin.starknet_contracts import (
     account_contract as oz_account_class,
 )
@@ -56,28 +57,23 @@
 )
 from starkware.starkware_utils.error_handling import StarkErrorCode
 
-from starknet_devnet.util import to_bytes, get_fee_estimation_info
-from starknet_devnet.constants import DUMMY_STATE_ROOT, OZ_ACCOUNT_CLASS_HASH
-
-from .lite_mode.lite_internal_deploy import LiteInternalDeploy
-from .lite_mode.lite_starknet import LiteStarknet
-
 from .accounts import Accounts
 from .blueprints.rpc.structures.types import Felt
-from .fee_token import FeeToken
+from .constants import DUMMY_STATE_ROOT, OZ_ACCOUNT_CLASS_HASH
 from .general_config import DEFAULT_GENERAL_CONFIG
-from .origin import NullOrigin, Origin
+from .fee_token import FeeToken
+from .forked_state import get_forked_starknet
+from .origin import ForkedOrigin, NullOrigin
 from .udc import UDC
+from .util import to_bytes, get_fee_estimation_info
 from .util import (
     StarknetDevnetException,
     enable_pickling,
     get_storage_diffs,
     get_all_declared_contracts,
 )
-from .contract_wrapper import ContractWrapper
 from .postman_wrapper import DevnetL1L2
 from .transactions import DevnetTransactions, DevnetTransaction
-from .contracts import DevnetContracts
 from .blocks import DevnetBlocks
 from .block_info_generator import BlockInfoGenerator
 from .devnet_config import DevnetConfig
@@ -86,6 +82,7 @@
 enable_pickling()
 
 # pylint: disable=too-many-instance-attributes
+# pylint: disable=too-many-public-methods
 class StarknetWrapper:
     """
     Wraps a Starknet instance and stores data to be returned by the server:
@@ -93,13 +90,16 @@ class StarknetWrapper:
     """
 
     def __init__(self, config: DevnetConfig):
-        self.origin: Origin = NullOrigin()
+        self.origin = (
+            ForkedOrigin(config.fork_network, config.fork_block)
+            if config.fork_network
+            else NullOrigin()
+        )
         """Origin chain that this devnet was forked from."""
 
         self.block_info_generator = BlockInfoGenerator()
         self.blocks = DevnetBlocks(self.origin, lite=config.lite_mode)
         self.config = config
-        self.contracts = DevnetContracts(self.origin)
         self.l1l2 = DevnetL1L2()
         self.transactions = DevnetTransactions(self.origin)
         self.starknet: Starknet = None
@@ -112,7 +112,7 @@ def __init__(self, config: DevnetConfig):
         if config.start_time is not None:
             self.set_block_time(config.start_time)
 
-        self.set_gas_price(config.gas_price)
+        self.__set_gas_price(config.gas_price)
 
     @staticmethod
     def load(path: str) -> "StarknetWrapper":
@@ -151,10 +151,26 @@ async def __init_starknet(self):
         Create and return underlying Starknet instance
         """
         if not self.starknet:
-            self.starknet = await Starknet.empty(general_config=DEFAULT_GENERAL_CONFIG)
+            if self.__is_fork():
+                print(
+                    f"Forking {self.config.fork_network.url} from block {self.config.fork_block}"
+                )
+
+                self.starknet = get_forked_starknet(
+                    feeder_gateway_client=self.config.fork_network,
+                    block_number=self.config.fork_block,
+                    gas_price=self.block_info_generator.gas_price,
+                )
+            else:
+                self.starknet = await Starknet.empty(
+                    general_config=DEFAULT_GENERAL_CONFIG
+                )
 
         return self.starknet
 
+    def __is_fork(self):
+        return bool(self.config.fork_network)
+
     def get_state(self):
         """
         Returns the StarknetState of the underlying Starknet instance.
@@ -204,22 +220,6 @@ async def _update_state(
             state_diff=state_diff,
         )
 
-    async def store_contract(
-        self,
-        address: int,
-        contract: StarknetContract,
-        contract_class: ContractClass,
-        tx_hash: int = None,
-    ):
-        """Store the provided data sa wrapped contract"""
-        class_hash_bytes = await self.starknet.state.state.get_class_hash_at(address)
-        class_hash = int.from_bytes(class_hash_bytes, "big")
-        self.contracts.store(
-            address=address,
-            class_hash=class_hash,
-            contract_wrapper=ContractWrapper(contract, contract_class, tx_hash),
-        )
-
     async def _store_transaction(
         self,
         transaction: DevnetTransaction,
@@ -246,13 +246,6 @@ async def _store_transaction(
 
         self.transactions.store(tx_hash, transaction)
 
-    def set_config(self, config: DevnetConfig):
-        """
-        Sets the configuration of the devnet.
-        """
-        self.config = config
-        self.blocks.lite = config.lite_mode
-
     async def declare(self, external_tx: Declare) -> Tuple[int, int]:
         """
         Declares the class specified with `declare_transaction`
@@ -272,10 +265,11 @@ async def declare(self, external_tx: Declare) -> Tuple[int, int]:
 
             tx_handler.explicitly_declared.append(class_hash_int)
 
-            # alpha-goerli allows multiple declarations of the same class
-            self.contracts.store_class(class_hash_int, external_tx.contract_class)
+            # alpha-goerli allows multiple declarations of the same class.
+            # Even though execute_tx is performed, class needs to be set explicitly
             await self.get_state().state.set_contract_class(
-                tx_handler.internal_tx.class_hash, external_tx.contract_class
+                class_hash=tx_handler.internal_tx.class_hash,
+                contract_class=external_tx.contract_class,
             )
 
         return class_hash_int, tx_handler.internal_tx.hash_value
@@ -382,26 +376,12 @@ async def deploy_account(self, external_tx: DeployAccount):
             tx_handler.internal_tx = InternalDeployAccount.from_external(
                 external_tx, state.general_config
             )
-            tx_handler.execution_info = await state.execute_tx(tx_handler.internal_tx)
+
+            tx_handler.execution_info = await self.__deploy(tx_handler.internal_tx)
             tx_handler.internal_calls = (
                 tx_handler.execution_info.call_info.internal_calls
             )
 
-            contract_class = await state.state.get_contract_class(
-                to_bytes(external_tx.class_hash)
-            )
-            contract = self.__create_contract(
-                contract_class,
-                tx_handler.execution_info.call_info,
-                address=account_address,
-            )
-            await self.store_contract(
-                address=account_address,
-                contract=contract,
-                contract_class=contract_class,
-                tx_hash=tx_handler.internal_tx.hash_value,
-            )
-
         return (
             account_address,
             tx_handler.internal_tx.hash_value,
@@ -413,57 +393,39 @@ async def deploy(self, deploy_transaction: Deploy) -> Tuple[int, int]:
         Returns (contract_address, transaction_hash).
         """
 
-        transactions_count = self.transactions.get_count()
         contract_class = deploy_transaction.contract_definition
 
-        if self.config.lite_mode:
-            internal_tx: LiteInternalDeploy = LiteInternalDeploy.from_external(
-                deploy_transaction, tx_number=transactions_count
-            )
-        else:
-            internal_tx: InternalDeploy = InternalDeploy.from_external(
-                deploy_transaction, self.get_state().general_config
-            )
+        internal_tx: InternalDeploy = InternalDeploy.from_external(
+            deploy_transaction, self.get_state().general_config
+        )
 
         contract_address = internal_tx.contract_address
 
-        if self.contracts.is_deployed(contract_address):
-            tx_hash = self.contracts.get_by_address(contract_address).deployment_tx_hash
+        if await self.is_deployed(contract_address):
+            tx_hash = calculate_deploy_transaction_hash(
+                version=deploy_transaction.version,
+                contract_address=contract_address,
+                constructor_calldata=deploy_transaction.constructor_calldata,
+                chain_id=self.get_state().general_config.chain_id.value,
+            )
             return contract_address, tx_hash
 
         tx_hash = internal_tx.hash_value
 
         async with self.__get_transaction_handler() as tx_handler:
             tx_handler.internal_tx = internal_tx
-
-            if self.config.lite_mode:
-                contract = await LiteStarknet.deploy(
-                    self,
-                    contract_class=contract_class,
-                    constructor_calldata=deploy_transaction.constructor_calldata,
-                    contract_address_salt=deploy_transaction.contract_address_salt,
-                    starknet=self.starknet,
-                    tx_number=transactions_count,
-                )
-            else:
-                contract = await self.__deploy(internal_tx, contract_class)
-
-            tx_handler.execution_info = contract.deploy_call_info
-            tx_handler.internal_calls = (
-                contract.deploy_call_info.call_info.internal_calls
+            await self.get_state().state.set_contract_class(
+                class_hash=internal_tx.class_hash, contract_class=contract_class
             )
-
-            await self.store_contract(
-                contract.contract_address, contract, contract_class, tx_hash
+            tx_handler.execution_info = await self.__deploy(internal_tx)
+            tx_handler.internal_calls = (
+                tx_handler.execution_info.call_info.internal_calls
             )
 
-            class_hash_bytes = await self.starknet.state.state.get_class_hash_at(
-                contract_address
-            )
-            class_hash_int = int.from_bytes(class_hash_bytes, "big")
             tx_handler.deployed_contracts.append(
                 DeployedContract(
-                    address=contract.contract_address, class_hash=class_hash_int
+                    address=contract_address,
+                    class_hash=int.from_bytes(internal_tx.class_hash, "big"),
                 )
             )
 
@@ -489,50 +451,26 @@ async def invoke(self, external_tx: InvokeFunction):
 
     async def call(self, transaction: CallFunction):
         """Perform call according to specifications in `transaction`."""
-        contract_wrapper = self.contracts.get_by_address(transaction.contract_address)
 
-        adapted_result = await contract_wrapper.call(
-            entry_point_selector=transaction.entry_point_selector,
+        state_copy = self.get_state().copy()
+        call_info = await state_copy.execute_entry_point_raw(
+            contract_address=transaction.contract_address,
+            selector=transaction.entry_point_selector,
             calldata=transaction.calldata,
             caller_address=0,
         )
 
-        return {"result": adapted_result}
+        result = list(map(hex, call_info.retdata))
+        return {"result": result}
 
-    async def __deploy(self, deploy_tx: InternalDeploy, contract_class: ContractClass):
+    async def __deploy(self, deploy_tx: Union[InternalDeploy, InternalDeployAccount]):
         """
         Replacement for self.starknet.deploy that allows usage of InternalDeploy right away.
         This way InternalDeploy doesn't have to be created twice, calculating hash every time.
         """
         state = self.get_state()
-        await state.state.set_contract_class(
-            class_hash=deploy_tx.contract_hash, contract_class=contract_class
-        )
-
         tx_execution_info = await state.execute_tx(tx=deploy_tx)
-
-        return self.__create_contract(
-            contract_class=contract_class,
-            call_info=tx_execution_info.call_info,
-            address=deploy_tx.contract_address,
-        )
-
-    def __create_contract(
-        self, contract_class: ContractClass, call_info: CallInfo, address: int
-    ) -> StarknetContract:
-
-        deploy_call_info = StarknetCallInfo.from_internal(
-            call_info=as_non_optional(call_info),
-            result=(),
-            main_call_events=[],
-        )
-
-        return StarknetContract(
-            state=self.get_state(),
-            abi=get_abi(contract_class=contract_class),
-            contract_address=address,
-            deploy_call_info=deploy_call_info,
-        )
+        return tx_execution_info
 
     async def _register_new_contracts(
         self,
@@ -542,35 +480,67 @@ async def _register_new_contracts(
     ):
         for internal_call in internal_calls:
             if internal_call.entry_point_type == EntryPointType.CONSTRUCTOR:
-                state = self.get_state()
                 class_hash_bytes = to_bytes(internal_call.class_hash)
                 class_hash_int = int.from_bytes(class_hash_bytes, "big")
-                contract_class = await state.state.get_contract_class(class_hash_bytes)
 
-                contract = StarknetContract(
-                    state, contract_class.abi, internal_call.contract_address, None
-                )
-                await self.store_contract(
-                    internal_call.contract_address, contract, contract_class, tx_hash
-                )
                 deployed_contracts.append(
                     DeployedContract(
-                        address=contract.contract_address, class_hash=class_hash_int
+                        address=internal_call.contract_address,
+                        class_hash=class_hash_int,
                     )
                 )
             await self._register_new_contracts(
                 internal_call.internal_calls, tx_hash, deployed_contracts
             )
 
+    async def get_class_by_hash(self, class_hash: int) -> ContractClass:
+        """Return contract class given class hash"""
+        cached_state = self.get_state().state
+        class_hash_bytes = to_bytes(class_hash)
+        return await cached_state.get_contract_class(class_hash_bytes)
+
+    async def get_class_hash_at(self, contract_address: int) -> int:
+        """Return class hash give the contract address"""
+        cached_state = self.get_state().state
+        class_hash_bytes = await cached_state.get_class_hash_at(contract_address)
+        class_hash_int = int.from_bytes(class_hash_bytes, "big")
+
+        if not class_hash_int:
+            raise StarknetDevnetException(
+                code=StarknetErrorCode.UNINITIALIZED_CONTRACT,
+                message=f"Contract with address {contract_address} is not deployed.",
+            )
+        return class_hash_int
+
+    async def get_class_by_address(self, contract_address: int) -> ContractClass:
+        """Return contract class given the contract address"""
+        cached_state = self.get_state().state
+        class_hash_int = await self.get_class_hash_at(contract_address)
+        class_hash_bytes = to_bytes(class_hash_int)
+        return await cached_state.get_contract_class(class_hash_bytes)
+
+    async def get_code(self, contract_address: int) -> dict:
+        """Return code dict given the contract address"""
+        try:
+            contract_class = await self.get_class_by_address(contract_address)
+            result_dict = {
+                "abi": contract_class.abi,
+                "bytecode": contract_class.dump()["program"]["data"],
+            }
+        except StarkException as err:
+            if err.code != StarknetErrorCode.UNINITIALIZED_CONTRACT:
+                raise
+            result_dict = {"abi": {}, "bytecode": []}
+
+        return result_dict
+
     async def get_storage_at(self, contract_address: int, key: int) -> Felt:
         """
         Returns the storage identified by `key`
         from the contract at `contract_address`.
         """
         state = self.get_state().state
-        if self.contracts.is_deployed(contract_address):
-            return hex(await state.get_storage_at(contract_address, key))
-        return self.origin.get_storage_at(contract_address, key)
+        return hex(await state.get_storage_at(contract_address, key))
 
     async def load_messaging_contract_in_l1(
         self, network_url: str, contract_address: str, network_id: str
@@ -671,7 +641,7 @@ def set_block_time(self, time_s: int):
         """Sets the block time to `time_s`."""
         self.block_info_generator.set_next_block_time(time_s)
 
-    def set_gas_price(self, gas_price: int):
+    def __set_gas_price(self, gas_price: int):
         """Sets gas price to `gas_price`."""
         self.block_info_generator.set_gas_price(gas_price)
 
@@ -683,3 +653,13 @@ async def __predeclare_oz_account(self):
         await self.get_state().state.set_contract_class(
             to_bytes(OZ_ACCOUNT_CLASS_HASH), oz_account_class
         )
+
+    async def is_deployed(self, address: int) -> bool:
+        """
+        Check if the contract is deployed.
+        """
+        assert isinstance(address, int)
+        cached_state = self.get_state().state
+        class_hash_bytes = await cached_state.get_class_hash_at(address)
+        class_hash_int = int.from_bytes(class_hash_bytes, "big")
+        return bool(class_hash_int)
diff --git a/starknet_devnet/transactions.py b/starknet_devnet/transactions.py
index 91c393510..97741a011 100644
--- a/starknet_devnet/transactions.py
+++ b/starknet_devnet/transactions.py
@@ -206,25 +206,25 @@ def store(self, tx_hash: int, transaction: DevnetTransaction):
         """
         self.__instances[tx_hash] = transaction
 
-    def get_transaction(self, tx_hash: str):
+    async def get_transaction(self, tx_hash: str):
         """
         Get a transaction info.
         """
         transaction = self.__get_transaction_by_hash(tx_hash)
 
         if transaction is None:
-            return self.origin.get_transaction(tx_hash)
+            return await self.origin.get_transaction(tx_hash)
 
         return transaction.get_tx_info()
 
-    def get_transaction_trace(self, tx_hash: str):
+    async def get_transaction_trace(self, tx_hash: str):
         """
         Get a transaction trace.
         """
         transaction = self.__get_transaction_by_hash(tx_hash)
 
         if transaction is None:
-            return self.origin.get_transaction_trace(tx_hash)
+            return await self.origin.get_transaction_trace(tx_hash)
 
         if transaction.status == TransactionStatus.REJECTED:
             raise StarknetDevnetException(
@@ -234,25 +234,25 @@ def get_transaction_trace(self, tx_hash: str):
 
         return transaction.get_trace()
 
-    def get_transaction_receipt(self, tx_hash: str):
+    async def get_transaction_receipt(self, tx_hash: str):
         """
         Get a transaction receipt.
         """
         transaction = self.__get_transaction_by_hash(tx_hash)
 
         if transaction is None:
-            return self.origin.get_transaction_receipt(tx_hash)
+            return await self.origin.get_transaction_receipt(tx_hash)
 
         return transaction.get_receipt()
 
-    def get_transaction_status(self, tx_hash: str):
+    async def get_transaction_status(self, tx_hash: str):
         """
         Get a transaction status.
         """
         transaction = self.__get_transaction_by_hash(tx_hash)
 
         if transaction is None:
-            return self.origin.get_transaction_status(tx_hash)
+            return await self.origin.get_transaction_status(tx_hash)
 
         tx_info = transaction.get_tx_info()
 
diff --git a/starknet_devnet/udc.py b/starknet_devnet/udc.py
index d3ec12165..fe5c385d9 100644
--- a/starknet_devnet/udc.py
+++ b/starknet_devnet/udc.py
@@ -3,7 +3,6 @@
 from starkware.python.utils import to_bytes
 from starkware.solidity.utils import load_nearby_contract
 from starkware.starknet.services.api.contract_class import ContractClass
-from starkware.starknet.testing.contract import StarknetContract
 from starkware.starknet.testing.starknet import Starknet
 
 
@@ -40,15 +39,7 @@ async def deploy(self):
         contract_class = UDC.get_contract_class()
 
         await starknet.state.state.set_contract_class(UDC.HASH_BYTES, contract_class)
-        await starknet.state.state.deploy_contract(UDC.ADDRESS, UDC.HASH_BYTES)
-
-        contract = StarknetContract(
-            state=starknet.state,
-            abi=contract_class.abi,
-            contract_address=UDC.ADDRESS,
-            deploy_call_info=None,
-        )
-
-        await self.starknet_wrapper.store_contract(
-            UDC.ADDRESS, contract, contract_class
-        )
+
+        # pylint: disable=protected-access
+        starknet.state.state.cache._class_hash_writes[UDC.ADDRESS] = UDC.HASH_BYTES
+        # replace with await starknet.state.state.deploy_contract
diff --git a/starknet_devnet/util.py b/starknet_devnet/util.py
index f346dc4ce..76eadfb8f 100644
--- a/starknet_devnet/util.py
+++ b/starknet_devnet/util.py
@@ -4,6 +4,7 @@
 
 from dataclasses import dataclass
 import os
+import sys
 from typing import Dict, Union, List, Set
 
 from starkware.starknet.definitions.error_codes import StarknetErrorCode
@@ -157,3 +158,8 @@ def get_fee_estimation_info(tx_fee: int, gas_price: int):
             "gas_usage": gas_usage,
         }
     )
+
+
+def warn(msg: str, file=sys.stderr):
+    """Log a warning"""
+    print(f"\033[93m{msg}\033[0m", file=file)


Test patch:
diff --git a/scripts/test_fork.sh b/scripts/test_fork.sh
new file mode 100755
index 000000000..5a81719b8
--- /dev/null
+++ b/scripts/test_fork.sh
@@ -0,0 +1,88 @@
+#!/bin/bash
+
+set -eu
+
+trap 'for killable in $(jobs -p); do kill $killable; done' EXIT
+
+HOST=localhost
+PORT1=5049
+DEVNET1_URL="http://$HOST:$PORT1"
+PORT2=5050
+DEVNET2_URL="http://$HOST:$PORT2"
+
+
+poetry run starknet-devnet --host "$HOST" --port "$PORT1" --seed 42 --accounts 1 --hide-predeployed-accounts &
+DEVNET1_PID=$!
+curl --retry 20 --retry-delay 1 --retry-connrefused -s -o /dev/null "$DEVNET1_URL/is_alive"
+echo "Started up devnet1; pid: $DEVNET1_PID"
+
+poetry run starknet-devnet --host "$HOST" --port "$PORT2" --fork-network "$DEVNET1_URL" --accounts 0 &
+DEVNET2_PID=$!
+curl --retry 20 --retry-delay 1 --retry-connrefused -s -o /dev/null "$DEVNET2_URL/is_alive"
+echo "Started up devnet2; pid: $DEVNET2_PID"
+
+# # get public key of predeployed account
+# for port in "$PORT1" "$PORT2"; do
+#     echo "Polling devnet at :$port"
+#     poetry run starknet get_storage_at \
+#         --feeder_gateway_url "http://$HOST:$port" \
+#         --contract_address 0x347be35996a21f6bf0623e75dbce52baba918ad5ae8d83b6f416045ab22961a \
+#         --key 550557492744938365112574611882025123252567779123164597803728068558738016655
+# done
+
+source ~/venvs/cairo_venv-0.10.1-pre/bin/activate
+DEPLOYMENT_URL="$DEVNET1_URL"
+starknet deploy \
+    --contract test/artifacts/contracts/cairo/contract.cairo/contract.json \
+    --inputs 10 \
+    --gateway_url "$DEPLOYMENT_URL" \
+    --feeder_gateway_url "$DEPLOYMENT_URL" \
+    --salt 0x99 \
+    --no_wallet
+echo "Deployed contract"
+CONTRACT_ADDRESS=0x07c80f5573d4c636960b56b02a01514d487c6e6a2c6f9242490280c932a32f71
+
+export STARKNET_WALLET="starkware.starknet.wallets.open_zeppelin.OpenZeppelinAccount"
+ACCOUNT_DIR="."
+rm -rf ./starknet_open_zeppelin_accounts.json*
+
+starknet new_account --network alpha-goerli --account_dir "$ACCOUNT_DIR"
+
+ACCOUNT_DEPLOYMENT_URL="$DEVNET1_URL"
+starknet deploy_account \
+    --gateway_url "$ACCOUNT_DEPLOYMENT_URL" \
+    --feeder_gateway_url "$ACCOUNT_DEPLOYMENT_URL" \
+    --network alpha-goerli \
+    --account_dir "$ACCOUNT_DIR" \
+    --max_fee 0
+echo "Deployed account"
+
+INVOKE_URL="$DEVNET1_URL"
+INVOKE_HASH=$(starknet invoke \
+    --abi test/artifacts/contracts/cairo/contract.cairo/contract_abi.json \
+    --function increase_balance \
+    --inputs 10 20 \
+    --address "$CONTRACT_ADDRESS" \
+    --gateway_url "$INVOKE_URL" \
+    --feeder_gateway_url "$INVOKE_URL" \
+    --max_fee 0 \
+    --network_id alpha-goerli \
+    --chain_id 0x534e5f474f45524c49 \
+    --account_dir "$ACCOUNT_DIR" | sed -rn 's/^Transaction hash: (.*)$/\1/p'
+)
+echo "Invoked contract on $INVOKE_URL"
+
+echo "Transaction on $DEVNET1_URL"
+starknet get_transaction --hash "$INVOKE_HASH" --feeder_gateway_url "$DEVNET1_URL"
+echo "Transaction on $DEVNET2_URL"
+starknet get_transaction --hash "$INVOKE_HASH" --feeder_gateway_url "$DEVNET2_URL"
+echo "Block on $DEVNET2_URL"
+starknet get_block --feeder_gateway "$DEVNET2_URL"
+
+for url in "$DEVNET1_URL" "$DEVNET2_URL"; do
+    starknet call \
+        --abi test/artifacts/contracts/cairo/contract.cairo/contract_abi.json \
+        --feeder_gateway_url "$url" \
+        --address "$CONTRACT_ADDRESS" \
+        --function get_balance
+done
diff --git a/test/account.py b/test/account.py
index 297aa948f..ea55d5296 100644
--- a/test/account.py
+++ b/test/account.py
@@ -43,10 +43,10 @@ def deploy_account_contract(salt=None):
     return deploy(ACCOUNT_PATH, inputs=[str(PUBLIC_KEY)], salt=salt)
 
 
-def get_nonce(account_address: str) -> int:
+def get_nonce(account_address: str, feeder_gateway_url=APP_URL) -> int:
     """Get nonce."""
     resp = requests.get(
-        f"{APP_URL}/feeder_gateway/get_nonce?contractAddress={account_address}"
+        f"{feeder_gateway_url}/feeder_gateway/get_nonce?contractAddress={account_address}"
     )
     return int(resp.json(), 16)
 
@@ -151,7 +151,11 @@ def _get_transaction_hash(
 
 
 def get_estimated_fee(
-    calls: List[AccountCall], account_address: str, private_key: str, nonce=None
+    calls: List[AccountCall],
+    account_address: str,
+    private_key: str,
+    nonce=None,
+    feeder_gateway_url=APP_URL,
 ):
     """Get estimated fee through account."""
 
@@ -174,6 +178,7 @@ def get_estimated_fee(
         abi_path=ACCOUNT_ABI_PATH,
         signature=signature,
         nonce=nonce,
+        feeder_gateway_url=feeder_gateway_url,
     )
 
 
@@ -183,11 +188,12 @@ def invoke(
     private_key: int,
     nonce=None,
     max_fee=None,
+    gateway_url=APP_URL,
 ):
     """Invoke __execute__ with correct calldata and signature."""
 
     if nonce is None:
-        nonce = get_nonce(account_address)
+        nonce = get_nonce(account_address, feeder_gateway_url=gateway_url)
 
     if max_fee is None:
         max_fee = get_estimated_fee(
@@ -195,6 +201,7 @@ def invoke(
             account_address=account_address,
             private_key=private_key,
             nonce=nonce,
+            feeder_gateway_url=gateway_url,
         )
 
     signature, execute_calldata = _get_execute_args(
@@ -222,7 +229,8 @@ def invoke(
             *signature,
             "--max_fee",
             str(max_fee),
-        ]
+        ],
+        gateway_url=gateway_url,
     )
 
     print("Invoke sent!")
diff --git a/test/expected/invoke_receipt.json b/test/expected/invoke_receipt.json
index 9fd17226b..6de317a02 100644
--- a/test/expected/invoke_receipt.json
+++ b/test/expected/invoke_receipt.json
@@ -10,7 +10,7 @@
                 "0x16010bdfe1800",
                 "0x0"
             ],
-            "from_address": "0x62230ea046a9a5fbc261ac77d03c8d41e5d442db2284587570ab46455fd2488",
+            "from_address": "0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
             "keys": [
                 "0x99cd8bde557814842a3121e8ddfd433a539b8c9f14bf31ebf108d12e6196e9"
             ]
diff --git a/test/expected/invoke_receipt_account_event.json b/test/expected/invoke_receipt_account_event.json
index dc662d03e..b53a35ba2 100644
--- a/test/expected/invoke_receipt_account_event.json
+++ b/test/expected/invoke_receipt_account_event.json
@@ -17,7 +17,7 @@
                 "0x5a569a2800",
                 "0x0"
             ],
-            "from_address": "0x62230ea046a9a5fbc261ac77d03c8d41e5d442db2284587570ab46455fd2488",
+            "from_address": "0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
             "keys": [
                 "0x99cd8bde557814842a3121e8ddfd433a539b8c9f14bf31ebf108d12e6196e9"
             ]
diff --git a/test/expected/invoke_receipt_event.json b/test/expected/invoke_receipt_event.json
index d445fc0c1..78a42ad5f 100644
--- a/test/expected/invoke_receipt_event.json
+++ b/test/expected/invoke_receipt_event.json
@@ -17,7 +17,7 @@
                 "0x160e24a2c4000",
                 "0x0"
             ],
-            "from_address": "0x62230ea046a9a5fbc261ac77d03c8d41e5d442db2284587570ab46455fd2488",
+            "from_address": "0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
             "keys": [
                 "0x99cd8bde557814842a3121e8ddfd433a539b8c9f14bf31ebf108d12e6196e9"
             ]
diff --git a/test/shared.py b/test/shared.py
index 976d68b93..1b8914b2e 100644
--- a/test/shared.py
+++ b/test/shared.py
@@ -48,7 +48,6 @@
 GENESIS_BLOCK_NUMBER = 0
 GENESIS_BLOCK_HASH = "0x0"
 INCORRECT_GENESIS_BLOCK_HASH = "0x1"
-DEFAULT_GAS_PRICE = int(1e11)
 
 SUPPORTED_TX_VERSION = 1
 SUPPORTED_RPC_TX_VERSION = 1
@@ -60,8 +59,12 @@
 PREDEPLOYED_ACCOUNT_PRIVATE_KEY = 0xBDD640FB06671AD11C80317FA3B1799D
 
 EXPECTED_FEE_TOKEN_ADDRESS = (
-    "0x62230ea046a9a5fbc261ac77d03c8d41e5d442db2284587570ab46455fd2488"
+    "0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7"
 )
 EXPECTED_UDC_ADDRESS = (
     "0x41a78e741e5af2fec34b695679bc6891742439f7afb8484ecd7766661ad02bf"
 )
+
+ALPHA_MAINNET_URL = "https://alpha-mainnet.starknet.io"
+ALPHA_GOERLI_URL = "https://alpha4.starknet.io"
+ALPHA_GOERLI2_URL = "https://alpha4-2.starknet.io"
diff --git a/test/test_account.py b/test/test_account.py
index 424964e6a..f4f0a851f 100644
--- a/test/test_account.py
+++ b/test/test_account.py
@@ -62,9 +62,9 @@ def deploy_events_contract():
     return deploy(EVENTS_CONTRACT_PATH, salt=SALT)
 
 
-def get_account_balance(address: str) -> int:
+def get_account_balance(address: str, server_url=APP_URL) -> int:
     """Get balance (wei) of account with `address` (hex)."""
-    resp = requests.get(f"{APP_URL}/account_balance?address={address}")
+    resp = requests.get(f"{server_url}/account_balance?address={address}")
     assert resp.status_code == 200
     return int(resp.json()["amount"])
 
diff --git a/test/test_block_number.py b/test/test_block_number.py
index 570afbb11..ebb0caccf 100644
--- a/test/test_block_number.py
+++ b/test/test_block_number.py
@@ -31,25 +31,24 @@ def my_get_block_number(address: str):
 
 @pytest.mark.usefixtures("run_devnet_in_background")
 @pytest.mark.parametrize(
-    "run_devnet_in_background, expected_tx_hash",
+    "run_devnet_in_background",
     [
-        ([*PREDEPLOY_ACCOUNT_CLI_ARGS], EXPECTED_TX_HASH),
-        ([*PREDEPLOY_ACCOUNT_CLI_ARGS, "--lite-mode"], "0x0"),
+        PREDEPLOY_ACCOUNT_CLI_ARGS,
+        [*PREDEPLOY_ACCOUNT_CLI_ARGS, "--lite-mode"],
     ],
     indirect=True,
 )
-def test_block_number_incremented(expected_tx_hash):
+def test_block_number_incremented():
     """
     Tests how block number is incremented in regular mode and lite mode.
-    In regular mode with salt "0x42" our expected hash is
-    0x4506fb016a309c8694a5c862625ba743a3ed2e248bca1ba5aa174ca06381f0f.
+    In regular mode with salt "0x42" our expected hash is {EXPECTED_TX_HASH}.
     """
 
     deploy_info = deploy(BLOCK_NUMBER_CONTRACT_PATH, salt="0x42")
     block_number_before = my_get_block_number(deploy_info["address"])
 
     assert int(block_number_before) == GENESIS_BLOCK_NUMBER + 1
-    assert expected_tx_hash == deploy_info["tx_hash"]
+    assert deploy_info["tx_hash"] == EXPECTED_TX_HASH
 
     invoke(
         calls=[(deploy_info["address"], "write_block_number", [])],
diff --git a/test/test_declare.py b/test/test_declare.py
index 64a007a14..f32162c89 100644
--- a/test/test_declare.py
+++ b/test/test_declare.py
@@ -16,11 +16,10 @@
     PREDEPLOYED_ACCOUNT_PRIVATE_KEY,
 )
 from .util import (
-    assert_contract_class,
+    assert_class_by_hash,
     assert_hex_equal,
     assert_tx_status,
     devnet_in_background,
-    get_class_by_hash,
 )
 
 
@@ -60,6 +59,4 @@ def test_declare_happy_path():
     class_hash = declare_info["class_hash"]
     assert_hex_equal(class_hash, EXPECTED_CLASS_HASH)
     assert_tx_status(declare_info["tx_hash"], "ACCEPTED_ON_L2")
-
-    declared_class = get_class_by_hash(class_hash)
-    assert_contract_class(declared_class, CONTRACT_PATH)
+    assert_class_by_hash(class_hash, CONTRACT_PATH)
diff --git a/test/test_deploy.py b/test/test_deploy.py
index 1c37ce735..c3075037c 100644
--- a/test/test_deploy.py
+++ b/test/test_deploy.py
@@ -42,14 +42,13 @@
 
 from .account import declare, invoke
 from .util import (
-    assert_contract_class,
+    assert_class_by_hash,
     assert_equal,
     assert_hex_equal,
     assert_tx_status,
     call,
     deploy,
     devnet_in_background,
-    get_class_by_hash,
     get_class_hash_at,
     get_transaction_receipt,
     load_contract_class,
@@ -85,23 +84,21 @@ def fixture_starknet_wrapper_args(request):
 
 
 @pytest.mark.parametrize(
-    "starknet_wrapper_args, expected_tx_hash, expected_block_hash",
+    "starknet_wrapper_args, expected_block_hash",
     [
         (
             [*PREDEPLOY_ACCOUNT_CLI_ARGS],
-            "0x13d4b9f765587296a4f40591efe235a8caf24f0496230f0b13a87f2e4c8150a",
             "",
         ),
         (
             [*PREDEPLOY_ACCOUNT_CLI_ARGS, "--lite-mode"],
-            "0x0",
             "0x1",
         ),
     ],
     indirect=True,
 )
 @pytest.mark.asyncio
-async def test_deploy(starknet_wrapper_args, expected_tx_hash, expected_block_hash):
+async def test_deploy(starknet_wrapper_args, expected_block_hash):
     """
     Test the deployment of a contract.
     """
@@ -121,11 +118,11 @@ async def test_deploy(starknet_wrapper_args, expected_tx_hash, expected_block_ha
 
     assert_hex_equal(
         hex(tx_hash),
-        expected_tx_hash,
+        "0x13D4B9F765587296A4F40591EFE235A8CAF24F0496230F0B13A87F2E4C8150A",
     )
     assert contract_address == expected_contract_address
 
-    tx_status = devnet.transactions.get_transaction_status(hex(tx_hash))
+    tx_status = await devnet.transactions.get_transaction_status(hex(tx_hash))
     assert tx_status["tx_status"] == TransactionStatus.ACCEPTED_ON_L2.name
 
     if "--lite-mode" in starknet_wrapper_args:
@@ -140,6 +137,11 @@ def test_predeployed_oz_account():
 @devnet_in_background()
 def test_deploy_account():
     """Test the deployment of an account."""
+    deploy_account_test_body()
+
+
+def deploy_account_test_body():
+    """The body of account deployment test."""
 
     # the account class should already be declared
 
@@ -251,8 +253,7 @@ def test_deploy_through_deployer_constructor():
     class_hash = declare_info["class_hash"]
     assert_hex_equal(class_hash, EXPECTED_CLASS_HASH)
 
-    contract_class = get_class_by_hash(class_hash=class_hash)
-    assert_contract_class(contract_class, CONTRACT_PATH)
+    assert_class_by_hash(class_hash, CONTRACT_PATH)
 
     # Deploy the deployer - also deploys a contract of the declared class using the deploy syscall
     initial_balance_in_constructor = "5"
@@ -284,7 +285,11 @@ def test_precomputed_udc_address():
 @devnet_in_background(*PREDEPLOY_ACCOUNT_CLI_ARGS)
 def test_deploy_with_udc():
     """Test if deploying through UDC works."""
+    deploy_with_udc_test_body()
+
 
+def deploy_with_udc_test_body():
+    """The body of udc deployment test."""
     # Declare the class to be deployed
     declare_info = declare(
         contract_path=CONTRACT_PATH,
@@ -294,8 +299,7 @@ def test_deploy_with_udc():
     class_hash = declare_info["class_hash"]
     assert_hex_equal(class_hash, EXPECTED_CLASS_HASH)
 
-    contract_class = get_class_by_hash(class_hash=class_hash)
-    assert_contract_class(contract_class, CONTRACT_PATH)
+    assert_class_by_hash(class_hash, CONTRACT_PATH)
 
     # Deploy a contract of the declared class through the deployer
     initial_balance = "10"
diff --git a/test/test_endpoints.py b/test/test_endpoints.py
index 50189cb09..57992eacc 100644
--- a/test/test_endpoints.py
+++ b/test/test_endpoints.py
@@ -274,7 +274,10 @@ def test_error_response_class_hash_at():
     error_message = resp.json()["message"]
 
     assert resp.status_code == 500
-    expected_message = f"Contract with address {INVALID_ADDRESS} is not deployed."
+    expected_message = (
+        # alpha-goerli reports a decimal address
+        f"Contract with address {int(INVALID_ADDRESS, 16)} is not deployed."
+    )
     assert expected_message == error_message
 
 
diff --git a/test/test_fork.py b/test/test_fork.py
new file mode 100644
index 000000000..6aa95a02f
--- /dev/null
+++ b/test/test_fork.py
@@ -0,0 +1,302 @@
+"""
+Test the forking feature.
+Relying on the fact that devnet doesn't support specifying which block to query
+"""
+
+import pytest
+
+from starknet_devnet.constants import DEFAULT_INITIAL_BALANCE
+
+from .account import get_nonce, invoke
+from .shared import (
+    ABI_PATH,
+    CONTRACT_PATH,
+    ALPHA_MAINNET_URL,
+    PREDEPLOY_ACCOUNT_CLI_ARGS,
+    PREDEPLOYED_ACCOUNT_ADDRESS,
+    PREDEPLOYED_ACCOUNT_PRIVATE_KEY,
+)
+from .settings import APP_URL, bind_free_port, HOST
+from .test_account import get_account_balance
+from .test_deploy import deploy_account_test_body, deploy_with_udc_test_body
+from .testnet_deployment import (
+    TESTNET_CONTRACT_ADDRESS,
+    TESTNET_DEPLOYMENT_BLOCK,
+    TESTNET_FORK_PARAMS,
+    TESTNET_URL,
+)
+from .util import (
+    assert_address_has_no_class_hash,
+    assert_tx_status,
+    call,
+    deploy,
+    devnet_in_background,
+    mint,
+)
+
+ORIGIN_PORT, ORIGIN_URL = bind_free_port(HOST)
+FORK_PORT, FORK_URL = bind_free_port(HOST)
+
+
+def _invoke_on_fork_and_assert_only_fork_changed(
+    contract_address: str,
+    initial_balance: str,
+    fork_url: str,
+    origin_url: str,
+):
+
+    # account nonce - before
+    origin_nonce_before = get_nonce(
+        account_address=PREDEPLOYED_ACCOUNT_ADDRESS, feeder_gateway_url=origin_url
+    )
+    assert origin_nonce_before == 0
+    fork_nonce_before = get_nonce(
+        account_address=PREDEPLOYED_ACCOUNT_ADDRESS, feeder_gateway_url=fork_url
+    )
+    assert fork_nonce_before == 0
+
+    # do the invoke and implicitly estimate fee before that
+    increase_args = [1, 2]
+    invoke_tx_hash = invoke(
+        calls=[(contract_address, "increase_balance", increase_args)],
+        account_address=PREDEPLOYED_ACCOUNT_ADDRESS,
+        private_key=PREDEPLOYED_ACCOUNT_PRIVATE_KEY,
+        gateway_url=fork_url,
+    )
+    # assert only received on fork
+    assert_tx_status(invoke_tx_hash, "NOT_RECEIVED", feeder_gateway_url=origin_url)
+    assert_tx_status(invoke_tx_hash, "ACCEPTED_ON_L2", feeder_gateway_url=fork_url)
+    # assert only callable
+    origin_balance_after = call(
+        function="get_balance",
+        abi_path=ABI_PATH,
+        address=contract_address,
+        feeder_gateway_url=origin_url,
+    )
+    assert origin_balance_after == initial_balance
+
+    fork_balance_after = call(
+        function="get_balance",
+        abi_path=ABI_PATH,
+        address=contract_address,
+        feeder_gateway_url=fork_url,
+    )
+    expected_balancer_after = str(int(initial_balance) + sum(increase_args))
+    assert fork_balance_after == expected_balancer_after
+
+    # account nonce - after
+    origin_nonce_after = get_nonce(
+        account_address=PREDEPLOYED_ACCOUNT_ADDRESS, feeder_gateway_url=origin_url
+    )
+    assert origin_nonce_after == 0
+    fork_nonce_after = get_nonce(
+        account_address=PREDEPLOYED_ACCOUNT_ADDRESS, feeder_gateway_url=fork_url
+    )
+    assert fork_nonce_after == 1
+
+
+def _deploy_on_origin_invoke_on_fork_assert_only_fork_changed(
+    fork_url: str,
+    origin_url: str,
+    initial_balance="10",
+):
+
+    deploy_info = deploy(
+        contract=CONTRACT_PATH,
+        inputs=[initial_balance],
+        gateway_url=origin_url,
+    )
+
+    _invoke_on_fork_and_assert_only_fork_changed(
+        contract_address=deploy_info["address"],
+        initial_balance=initial_balance,
+        fork_url=fork_url,
+        origin_url=origin_url,
+    )
+
+
+@devnet_in_background("--port", ORIGIN_PORT, *PREDEPLOY_ACCOUNT_CLI_ARGS)
+@devnet_in_background(
+    "--port", FORK_PORT, "--fork-network", ORIGIN_URL, "--accounts", "0"
+)
+def test_forking_devnet_with_account_on_origin():
+    """
+    Deploy contract on origin, invoke on fork, rely on account on origin.
+    Assert only fork changed
+    """
+
+    # account balance
+    origin_balance_before = get_account_balance(
+        address=PREDEPLOYED_ACCOUNT_ADDRESS, server_url=ORIGIN_URL
+    )
+    assert origin_balance_before == DEFAULT_INITIAL_BALANCE
+
+    fork_balance_before = get_account_balance(
+        # fork has access to balances on origin
+        address=PREDEPLOYED_ACCOUNT_ADDRESS,
+        server_url=FORK_URL,
+    )
+    assert fork_balance_before == DEFAULT_INITIAL_BALANCE
+
+    # with goerli, forking would be done here, but having it done beforehand is ok with devnet
+    _deploy_on_origin_invoke_on_fork_assert_only_fork_changed(
+        fork_url=FORK_URL,
+        origin_url=ORIGIN_URL,
+    )
+
+    # account balance
+    origin_balance_after = get_account_balance(
+        address=PREDEPLOYED_ACCOUNT_ADDRESS, server_url=ORIGIN_URL
+    )
+    assert origin_balance_after == DEFAULT_INITIAL_BALANCE
+
+    fork_balance_after = get_account_balance(
+        address=PREDEPLOYED_ACCOUNT_ADDRESS, server_url=FORK_URL
+    )
+    assert fork_balance_after < DEFAULT_INITIAL_BALANCE
+
+
+@devnet_in_background("--port", ORIGIN_PORT, "--accounts", "0")
+@devnet_in_background(
+    "--port", FORK_PORT, "--fork-network", ORIGIN_URL, *PREDEPLOY_ACCOUNT_CLI_ARGS
+)
+def test_forking_devnet_with_account_on_fork():
+    """
+    Deploy contract on origin, invoke on fork, rely on account on fork.
+    Assert only fork changed
+    """
+
+    # account balance
+    origin_balance_before = get_account_balance(
+        address=PREDEPLOYED_ACCOUNT_ADDRESS, server_url=ORIGIN_URL
+    )
+    assert origin_balance_before == 0
+
+    fork_balance_before = get_account_balance(
+        address=PREDEPLOYED_ACCOUNT_ADDRESS, server_url=FORK_URL
+    )
+    assert fork_balance_before == DEFAULT_INITIAL_BALANCE
+
+    # with goerli, forking would be done here, but having it done beforehand is ok with devnet
+    _deploy_on_origin_invoke_on_fork_assert_only_fork_changed(
+        fork_url=FORK_URL,
+        origin_url=ORIGIN_URL,
+    )
+
+    # account balance
+    origin_balance_after = get_account_balance(
+        address=PREDEPLOYED_ACCOUNT_ADDRESS, server_url=ORIGIN_URL
+    )
+    assert origin_balance_after == 0
+
+    fork_balance_after = get_account_balance(
+        address=PREDEPLOYED_ACCOUNT_ADDRESS, server_url=FORK_URL
+    )
+    assert fork_balance_after < DEFAULT_INITIAL_BALANCE
+
+
+@pytest.mark.usefixtures("run_devnet_in_background")
+@pytest.mark.parametrize(
+    "run_devnet_in_background",
+    [
+        [*TESTNET_FORK_PARAMS, "--fork-block", str(TESTNET_DEPLOYMENT_BLOCK)],
+        [*TESTNET_FORK_PARAMS, "--fork-block", str(TESTNET_DEPLOYMENT_BLOCK + 1)],
+        [*TESTNET_FORK_PARAMS, "--fork-block", "latest"],
+        [*TESTNET_FORK_PARAMS],  # should default to latest
+    ],
+    indirect=True,
+)
+def test_forking_testnet_from_valid_block():
+    """Test forking from various happy path blocks"""
+
+    _invoke_on_fork_and_assert_only_fork_changed(
+        contract_address=TESTNET_CONTRACT_ADDRESS,
+        initial_balance="10",
+        fork_url=APP_URL,
+        origin_url=TESTNET_URL,
+    )
+
+
+@pytest.mark.usefixtures("run_devnet_in_background")
+@pytest.mark.parametrize(
+    "run_devnet_in_background, origin_url",
+    [
+        (
+            [*PREDEPLOY_ACCOUNT_CLI_ARGS, "--fork-network", "alpha-mainnet"],
+            ALPHA_MAINNET_URL,
+        ),
+        ([*TESTNET_FORK_PARAMS], TESTNET_URL),
+    ],
+    indirect=["run_devnet_in_background"],
+)
+def test_deploy_on_fork(origin_url):
+    """
+    Deploy on fork, invoke on fork.
+    Assert usability on fork. Assert no change on origin.
+    """
+
+    deploy_info = deploy(contract=CONTRACT_PATH, inputs=["10"])
+    contract_address = deploy_info["address"]
+
+    invoke_tx_hash = invoke(
+        calls=[(contract_address, "increase_balance", [1, 2])],
+        account_address=PREDEPLOYED_ACCOUNT_ADDRESS,
+        private_key=PREDEPLOYED_ACCOUNT_PRIVATE_KEY,
+    )
+    assert_tx_status(invoke_tx_hash, "ACCEPTED_ON_L2")
+
+    balance_after = call(
+        function="get_balance",
+        address=contract_address,
+        abi_path=ABI_PATH,
+    )
+    assert balance_after == "13"
+
+    assert_address_has_no_class_hash(contract_address, origin_url)
+
+
+@devnet_in_background(
+    *TESTNET_FORK_PARAMS, "--fork-block", str(TESTNET_DEPLOYMENT_BLOCK - 1)
+)
+def test_forking_testnet_from_too_early_block():
+    """Test forking testnet if not yet deployed"""
+
+    invoke_tx_hash = invoke(
+        calls=[(TESTNET_CONTRACT_ADDRESS, "increase_balance", [2, 3])],  # random values
+        account_address=PREDEPLOYED_ACCOUNT_ADDRESS,
+        private_key=PREDEPLOYED_ACCOUNT_PRIVATE_KEY,
+        max_fee=int(1e8),  # to prevent implicit fee estimation
+    )
+
+    # assertions on fork (devnet)
+    assert_tx_status(invoke_tx_hash, "REJECTED")
+    assert_address_has_no_class_hash(TESTNET_CONTRACT_ADDRESS)
+
+    # assertions on origin (testnet)
+    # this will fail if someone invokes `increase_balance(2, 3)` because it will then be REJECTED instead of NOT_RECEIVED
+    assert_tx_status(invoke_tx_hash, "NOT_RECEIVED", feeder_gateway_url=TESTNET_URL)
+
+
+@devnet_in_background(*TESTNET_FORK_PARAMS)
+@pytest.mark.parametrize("lite", [True, False])
+def test_minting(lite: bool):
+    """Test minting"""
+    dummy_address = "0x123"
+    dummy_amount = 100
+
+    resp = mint(dummy_address, dummy_amount, lite=lite)
+    assert resp["new_balance"] == dummy_amount
+    resp = mint(dummy_address, dummy_amount, lite=lite)
+    assert resp["new_balance"] == dummy_amount * 2
+
+
+@devnet_in_background(*TESTNET_FORK_PARAMS)
+def test_deploy_account():
+    """Test that deploy account functionality works when forking"""
+    deploy_account_test_body()
+
+
+@devnet_in_background(*TESTNET_FORK_PARAMS)
+def test_deploy_with_udc():
+    """Test that deploying with udc works when forking"""
+    deploy_with_udc_test_body()
diff --git a/test/test_fork_cli_params.py b/test/test_fork_cli_params.py
new file mode 100644
index 000000000..cb1b51656
--- /dev/null
+++ b/test/test_fork_cli_params.py
@@ -0,0 +1,141 @@
+"""Testing fork CLI params"""
+
+import subprocess
+
+import pytest
+
+from .shared import ALPHA_GOERLI_URL, ALPHA_GOERLI2_URL, ALPHA_MAINNET_URL
+from .util import DevnetBackgroundProc, read_stream, terminate_and_wait
+
+
+ACTIVE_DEVNET = DevnetBackgroundProc()
+
+
+def test_invalid_fork_network():
+    """Test if fork network invalid"""
+    invalid_name = "alpha-goerli-invalid"
+    proc = ACTIVE_DEVNET.start(
+        "--fork-network",
+        invalid_name,
+        stderr=subprocess.PIPE,
+        stdout=subprocess.PIPE,
+    )
+    assert read_stream(proc.stdout) == ""
+    assert (
+        read_stream(proc.stderr)
+        == f"Error: Invalid fork-network (must be a URL or one of {{alpha-goerli, alpha-goerli2, alpha-mainnet}}). Received: {invalid_name}\n"
+    )
+    assert proc.returncode == 1
+
+
+def test_url_not_sequencer():
+    """Pass a valid url but not of a StarkNet sequencer"""
+    invalid_url = "http://google.com"
+    proc = ACTIVE_DEVNET.start(
+        "--fork-network",
+        invalid_url,
+        stderr=subprocess.PIPE,
+        stdout=subprocess.PIPE,
+    )
+    assert read_stream(proc.stdout) == ""
+    assert (
+        read_stream(proc.stderr)
+        == f"Error: {invalid_url} is not a valid StarkNet sequencer\n"
+    )
+    assert proc.returncode == 1
+
+
+@pytest.mark.parametrize(
+    "fork_network, expected_stdout",
+    [
+        ("alpha-mainnet", f"Forking {ALPHA_MAINNET_URL}"),
+        (ALPHA_MAINNET_URL, f"Forking {ALPHA_MAINNET_URL}"),
+        ("alpha-goerli", f"Forking {ALPHA_GOERLI_URL}"),
+        (ALPHA_GOERLI_URL, f"Forking {ALPHA_GOERLI_URL}"),
+        ("alpha-goerli2", f"Forking {ALPHA_GOERLI2_URL}"),
+        (ALPHA_GOERLI2_URL, f"Forking {ALPHA_GOERLI2_URL}"),
+    ],
+)
+def test_predefined_fork_network_specification(
+    fork_network: str,
+    expected_stdout: str,
+):
+    """Test various happy path fork network specification scenarios"""
+    proc = ACTIVE_DEVNET.start(
+        "--accounts",
+        "0",  # to reduce output
+        "--fork-network",
+        fork_network,
+        stdout=subprocess.PIPE,
+    )
+    terminate_and_wait(proc)
+    assert expected_stdout in read_stream(proc.stdout)
+    assert proc.returncode == 0
+
+
+def test_block_provided_without_network():
+    """Should fail if block provided and network not"""
+    proc = ACTIVE_DEVNET.start(
+        "--fork-block", "123", stderr=subprocess.PIPE, stdout=subprocess.PIPE
+    )
+    assert read_stream(proc.stdout) == ""
+    assert (
+        read_stream(proc.stderr)
+        == "Error: --fork-network required if --fork-block present\n"
+    )
+    assert proc.returncode == 1
+
+
+@pytest.mark.parametrize("fork_block", ["-1", "piece of invalid text"])
+def test_malformed_block_id(fork_block: str):
+    """Should exit if provided with a negative block number"""
+    proc = ACTIVE_DEVNET.start(
+        "--fork-network",
+        "alpha-goerli",
+        "--fork-block",
+        fork_block,
+        stderr=subprocess.PIPE,
+        stdout=subprocess.PIPE,
+    )
+    assert read_stream(proc.stdout) == ""
+    assert (
+        read_stream(proc.stderr)
+        == f"The value of --fork-block must be a non-negative integer or 'latest', got: {fork_block}\n"
+    )
+    assert proc.returncode == 1
+
+
+def test_too_big_block_id():
+    """Should exit if fork block number too big"""
+    too_big_block_id = str(int(1e9))
+    proc = ACTIVE_DEVNET.start(
+        "--fork-network",
+        "alpha-goerli2",
+        "--fork-block",
+        too_big_block_id,
+        stderr=subprocess.PIPE,
+        stdout=subprocess.PIPE,
+    )
+    assert read_stream(proc.stdout) == ""
+    assert f"Block number {too_big_block_id} was not found." in read_stream(proc.stderr)
+
+
+@pytest.mark.parametrize(
+    "fork_block",
+    [
+        "latest",  # would be hard to assert the block number is correct
+        "1",  # small enough, every chain should have it
+    ],
+)
+def test_valid_block_ids(fork_block: str):
+    """Test some happy path fork block ids"""
+    proc = ACTIVE_DEVNET.start(
+        "--fork-network",
+        "alpha-goerli2",
+        "--fork-block",
+        fork_block,
+        stdout=subprocess.PIPE,
+    )
+    terminate_and_wait(proc)
+    assert f"Forking {ALPHA_GOERLI2_URL}" in read_stream(proc.stdout)
+    assert proc.returncode == 0
diff --git a/test/test_fork_feeder_gateway.py b/test/test_fork_feeder_gateway.py
new file mode 100644
index 000000000..ffdbdb2d4
--- /dev/null
+++ b/test/test_fork_feeder_gateway.py
@@ -0,0 +1,377 @@
+"""Test feeder gateway responses of origin and fork"""
+
+import json
+
+import requests
+from starkware.starknet.definitions.error_codes import StarknetErrorCode
+from starkware.starknet.services.api.feeder_gateway.response_objects import (
+    BlockIdentifier,
+)
+
+from .account import declare, invoke
+from .shared import (
+    BALANCE_KEY,
+    CONTRACT_PATH,
+    EXPECTED_CLASS_HASH,
+    PREDEPLOYED_ACCOUNT_ADDRESS,
+    PREDEPLOYED_ACCOUNT_PRIVATE_KEY,
+)
+from .settings import APP_URL
+from .test_state_update import get_state_update
+from .test_transaction_trace import (
+    assert_get_block_traces_response,
+    get_block_traces,
+    get_transaction_trace_response,
+)
+from .testnet_deployment import (
+    TESTNET_CONTRACT_ADDRESS,
+    TESTNET_DEPLOYMENT_BLOCK,
+    TESTNET_FORK_PARAMS,
+    TESTNET_URL,
+)
+from .util import (
+    assert_address_has_no_class_hash,
+    assert_class_by_hash,
+    assert_class_by_hash_not_present,
+    assert_class_hash_at_address,
+    assert_contract_code_not_present,
+    assert_contract_code_present,
+    assert_full_contract,
+    assert_full_contract_not_present,
+    assert_receipt_present,
+    assert_storage,
+    assert_transaction,
+    assert_transaction_not_received,
+    assert_transaction_receipt_not_received,
+    assert_tx_status,
+    deploy,
+    devnet_in_background,
+    get_block,
+)
+
+
+DEPLOYMENT_INPUT = "10"
+EXPECTED_DEPLOYMENT_ADDRESS = (
+    "0x007e723b33e317c604b36e17d0a8e7064b08eda39aa9a4d6a94a7626ec432d8a"
+)
+EXPECTED_INVOKE_HASH = (
+    "0x51b501687e77d5433c7fc00b3a6dd25c2f6edf95f506dd9cba2251a4ce9ed43"
+)
+
+
+def _deploy_to_expected_address(contract=CONTRACT_PATH):
+    deploy_info = deploy(
+        contract=contract,
+        inputs=[DEPLOYMENT_INPUT],
+        salt="0x42",
+    )
+    assert int(deploy_info["address"], 16) == int(EXPECTED_DEPLOYMENT_ADDRESS, 16)
+
+
+def _make_expected_invoke(gateway_url=APP_URL):
+    invoke_tx_hash = invoke(
+        calls=[(TESTNET_CONTRACT_ADDRESS, "increase_balance", [1, 2])],
+        account_address=PREDEPLOYED_ACCOUNT_ADDRESS,
+        private_key=PREDEPLOYED_ACCOUNT_PRIVATE_KEY,
+        gateway_url=gateway_url,
+    )
+    assert int(invoke_tx_hash, 16) == int(EXPECTED_INVOKE_HASH, 16)
+
+
+@devnet_in_background(
+    *TESTNET_FORK_PARAMS,
+    "--fork-block",
+    # starting from an earlier block; otherwise the contract class is already present
+    str(TESTNET_DEPLOYMENT_BLOCK - 1),
+)
+def test_contract_responses():
+    """Assert that get_full_contract only makes sense on fork after deployment"""
+    # full contract
+    assert_full_contract_not_present(
+        address=EXPECTED_DEPLOYMENT_ADDRESS, feeder_gateway_url=TESTNET_URL
+    )
+    assert_full_contract_not_present(
+        address=EXPECTED_DEPLOYMENT_ADDRESS, feeder_gateway_url=APP_URL
+    )
+
+    # code
+    assert_contract_code_not_present(
+        address=EXPECTED_DEPLOYMENT_ADDRESS, feeder_gateway_url=TESTNET_URL
+    )
+    assert_contract_code_not_present(
+        address=EXPECTED_DEPLOYMENT_ADDRESS, feeder_gateway_url=APP_URL
+    )
+
+    # class hash
+    assert_address_has_no_class_hash(
+        contract_address=EXPECTED_DEPLOYMENT_ADDRESS, feeder_gateway_url=TESTNET_URL
+    )
+    assert_address_has_no_class_hash(
+        contract_address=EXPECTED_DEPLOYMENT_ADDRESS, feeder_gateway_url=APP_URL
+    )
+
+    # storage
+    assert_storage(
+        address=EXPECTED_DEPLOYMENT_ADDRESS,
+        key=BALANCE_KEY,
+        expected_value="0x0",
+        feeder_gateway_url=TESTNET_URL,
+    )
+    assert_storage(
+        address=EXPECTED_DEPLOYMENT_ADDRESS,
+        key=BALANCE_KEY,
+        expected_value="0x0",
+        feeder_gateway_url=APP_URL,
+    )
+
+    _deploy_to_expected_address()
+
+    # full contract
+    assert_full_contract_not_present(
+        address=EXPECTED_DEPLOYMENT_ADDRESS, feeder_gateway_url=TESTNET_URL
+    )
+    assert_full_contract(
+        address=EXPECTED_DEPLOYMENT_ADDRESS,
+        expected_path=CONTRACT_PATH,
+        feeder_gateway_url=APP_URL,
+    )
+
+    # code
+    assert_contract_code_not_present(
+        address=EXPECTED_DEPLOYMENT_ADDRESS, feeder_gateway_url=TESTNET_URL
+    )
+    assert_contract_code_present(
+        address=EXPECTED_DEPLOYMENT_ADDRESS, feeder_gateway_url=APP_URL
+    )
+
+    assert_address_has_no_class_hash(
+        contract_address=EXPECTED_DEPLOYMENT_ADDRESS, feeder_gateway_url=TESTNET_URL
+    )
+    assert_class_hash_at_address(
+        contract_address=EXPECTED_DEPLOYMENT_ADDRESS,
+        expected_class_hash=EXPECTED_CLASS_HASH,
+        feeder_gateway_url=APP_URL,
+    )
+
+    # storage
+    assert_storage(
+        address=EXPECTED_DEPLOYMENT_ADDRESS,
+        key=BALANCE_KEY,
+        expected_value="0x0",
+        feeder_gateway_url=TESTNET_URL,
+    )
+    assert_storage(
+        address=EXPECTED_DEPLOYMENT_ADDRESS,
+        key=BALANCE_KEY,
+        expected_value=hex(int(DEPLOYMENT_INPUT, 10)),
+        feeder_gateway_url=APP_URL,
+    )
+
+
+@devnet_in_background(
+    *TESTNET_FORK_PARAMS, "--fork-block", str(TESTNET_DEPLOYMENT_BLOCK - 1)
+)
+def test_declare_and_get_class_by_hash():
+    """Test class declaration and class getting by hash"""
+
+    assert_class_by_hash_not_present(
+        class_hash=EXPECTED_CLASS_HASH, feeder_gateway_url=TESTNET_URL
+    )
+    assert_class_by_hash_not_present(
+        class_hash=EXPECTED_CLASS_HASH, feeder_gateway_url=APP_URL
+    )
+
+    declare_info = declare(
+        contract_path=CONTRACT_PATH,
+        account_address=PREDEPLOYED_ACCOUNT_ADDRESS,
+        private_key=PREDEPLOYED_ACCOUNT_PRIVATE_KEY,
+    )
+    assert int(declare_info["class_hash"], 16) == int(EXPECTED_CLASS_HASH, 16)
+    assert_tx_status(declare_info["tx_hash"], "ACCEPTED_ON_L2")
+
+    assert_class_by_hash_not_present(
+        class_hash=EXPECTED_CLASS_HASH, feeder_gateway_url=TESTNET_URL
+    )
+    assert_class_by_hash(
+        class_hash=EXPECTED_CLASS_HASH,
+        expected_path=CONTRACT_PATH,
+        feeder_gateway_url=APP_URL,
+    )
+
+
+def _assert_transaction_trace_not_present(tx_hash: str, feeder_gateway_url=APP_URL):
+    resp = get_transaction_trace_response(tx_hash, server_url=feeder_gateway_url)
+    assert resp.json()["code"] == str(StarknetErrorCode.INVALID_TRANSACTION_HASH)
+    assert resp.status_code == 500
+
+
+def _assert_transaction_trace_present(
+    tx_hash: str, expected_address: str, feeder_gateway_url=APP_URL
+):
+    resp = get_transaction_trace_response(tx_hash, server_url=feeder_gateway_url)
+    body = resp.json()
+    assert body["function_invocation"]["contract_address"] == expected_address
+    assert resp.status_code == 200
+
+
+@devnet_in_background(
+    *TESTNET_FORK_PARAMS, "--fork-block", str(TESTNET_DEPLOYMENT_BLOCK)
+)
+def test_transaction_responses():
+    """Assert transaction only present on fork after invoking"""
+
+    # tx status
+    assert_tx_status(
+        tx_hash=EXPECTED_INVOKE_HASH,
+        expected_tx_status="NOT_RECEIVED",
+        feeder_gateway_url=TESTNET_URL,
+    )
+    assert_tx_status(
+        tx_hash=EXPECTED_INVOKE_HASH,
+        expected_tx_status="NOT_RECEIVED",
+        feeder_gateway_url=APP_URL,
+    )
+
+    # tx
+    assert_transaction_not_received(
+        tx_hash=EXPECTED_INVOKE_HASH, feeder_gateway_url=TESTNET_URL
+    )
+    assert_transaction_not_received(
+        tx_hash=EXPECTED_INVOKE_HASH, feeder_gateway_url=APP_URL
+    )
+
+    # tx receipt
+    assert_transaction_receipt_not_received(
+        tx_hash=EXPECTED_INVOKE_HASH, feeder_gateway_url=TESTNET_URL
+    )
+    assert_transaction_receipt_not_received(
+        tx_hash=EXPECTED_INVOKE_HASH, feeder_gateway_url=APP_URL
+    )
+
+    # tx trace
+    _assert_transaction_trace_not_present(
+        tx_hash=EXPECTED_INVOKE_HASH, feeder_gateway_url=TESTNET_URL
+    )
+    _assert_transaction_trace_not_present(
+        tx_hash=EXPECTED_INVOKE_HASH, feeder_gateway_url=APP_URL
+    )
+
+    _make_expected_invoke()
+
+    # tx status
+    assert_tx_status(
+        tx_hash=EXPECTED_INVOKE_HASH,
+        expected_tx_status="NOT_RECEIVED",
+        feeder_gateway_url=TESTNET_URL,
+    )
+    assert_tx_status(
+        tx_hash=EXPECTED_INVOKE_HASH,
+        expected_tx_status="ACCEPTED_ON_L2",
+        feeder_gateway_url=APP_URL,
+    )
+
+    # tx
+    assert_transaction_not_received(
+        tx_hash=EXPECTED_INVOKE_HASH, feeder_gateway_url=TESTNET_URL
+    )
+    assert_transaction(
+        tx_hash=EXPECTED_INVOKE_HASH,
+        expected_status="ACCEPTED_ON_L2",
+        feeder_gateway_url=APP_URL,
+    )
+
+    # tx receipt
+    assert_transaction_receipt_not_received(
+        tx_hash=EXPECTED_INVOKE_HASH, feeder_gateway_url=TESTNET_URL
+    )
+    assert_receipt_present(
+        tx_hash=EXPECTED_INVOKE_HASH,
+        expected_status="ACCEPTED_ON_L2",
+        feeder_gateway_url=APP_URL,
+    )
+
+    # tx trace
+    _assert_transaction_trace_not_present(
+        tx_hash=EXPECTED_INVOKE_HASH, feeder_gateway_url=TESTNET_URL
+    )
+    _assert_transaction_trace_present(
+        tx_hash=EXPECTED_INVOKE_HASH,
+        expected_address=PREDEPLOYED_ACCOUNT_ADDRESS,
+        feeder_gateway_url=APP_URL,
+    )
+
+
+def _assert_block_artifact_not_found(
+    method: str,
+    block_number: BlockIdentifier = None,
+    block_hash: str = None,
+    feeder_gateway_url=APP_URL,
+):
+    resp = requests.get(
+        f"{feeder_gateway_url}/feeder_gateway/{method}",
+        {"blockNumber": block_number, "blockHash": block_hash},
+    )
+    assert json.loads(resp.text)["code"] == str(StarknetErrorCode.BLOCK_NOT_FOUND)
+    assert resp.status_code == 500
+
+
+@devnet_in_background(
+    *TESTNET_FORK_PARAMS, "--fork-block", str(TESTNET_DEPLOYMENT_BLOCK)
+)
+def test_block_responses():
+    """Test how block responses are handled when forking."""
+
+    origin_block = get_block(
+        block_number=TESTNET_DEPLOYMENT_BLOCK,
+        parse=True,
+        feeder_gateway_url=TESTNET_URL,
+    )
+    fork_block = get_block(block_number=TESTNET_DEPLOYMENT_BLOCK, parse=True)
+    assert origin_block == fork_block
+
+    # assert block count incremented by one (due to genesis block)
+    latest_fork_block_before = get_block(block_number="latest", parse=True)
+    assert latest_fork_block_before["block_number"] == fork_block["block_number"] + 1
+
+    # assert next block not yet present
+    next_block_number = str(TESTNET_DEPLOYMENT_BLOCK + 2)
+    for method in "get_block", "get_block_traces", "get_state_update":
+        _assert_block_artifact_not_found(method, block_number=next_block_number)
+
+    # invoke
+    _make_expected_invoke(gateway_url=APP_URL)
+
+    # assert block count incremented by one
+    latest_fork_block_after = get_block(block_number="latest", parse=True)
+    assert (
+        latest_fork_block_after["block_number"]
+        == latest_fork_block_before["block_number"] + 1
+    )
+
+    # assert block trace
+    assert_get_block_traces_response({"blockNumber": "latest"}, EXPECTED_INVOKE_HASH)
+
+    # assert state update
+    state_update = get_state_update(block_number="latest")
+    assert TESTNET_CONTRACT_ADDRESS in state_update["state_diff"]["storage_diffs"]
+
+
+@devnet_in_background(*TESTNET_FORK_PARAMS)
+def test_block_responses_by_hash():
+    """Test error is internally properly handled and that a json is sent"""
+    dummy_hash = "0x1"
+    for method in "get_block", "get_block_traces", "get_state_update":
+        _assert_block_artifact_not_found(method, block_hash=dummy_hash)
+
+    latest_block_by_number = get_block(block_number="latest", parse=True)
+    latest_block_hash = latest_block_by_number["block_hash"]
+    latest_block_by_hash = get_block(block_hash=latest_block_hash, parse=True)
+    assert latest_block_by_number == latest_block_by_hash
+
+    block_traces_by_hash = get_block_traces({"blockHash": latest_block_hash})
+    block_traces_by_number = get_block_traces({"blockNumber": "latest"})
+    assert block_traces_by_hash == block_traces_by_number
+
+    state_update_by_hash = get_block_traces({"blockHash": latest_block_hash})
+    state_update_by_number = get_block_traces({"blockNumber": "latest"})
+    assert state_update_by_hash == state_update_by_number
diff --git a/test/test_general_workflow.py b/test/test_general_workflow.py
index f790732dc..4a2155f32 100644
--- a/test/test_general_workflow.py
+++ b/test/test_general_workflow.py
@@ -6,12 +6,13 @@
 
 from .account import invoke
 from .util import (
-    assert_contract_class,
+    assert_class_by_hash,
+    assert_full_contract,
     assert_negative_block_input,
     assert_transaction_not_received,
     assert_transaction_receipt_not_received,
     assert_block,
-    assert_contract_code,
+    assert_contract_code_present,
     assert_equal,
     assert_failing_deploy,
     assert_receipt,
@@ -22,9 +23,7 @@
     assert_events,
     call,
     deploy,
-    get_class_by_hash,
     get_class_hash_at,
-    get_full_contract,
     get_block,
 )
 
@@ -43,28 +42,25 @@
     PREDEPLOYED_ACCOUNT_PRIVATE_KEY,
 )
 
-EXPECTED_SALTY_DEPLOY_HASH_LITE_MODE = "0x2"
 EXPECTED_SALTY_DEPLOY_BLOCK_HASH_LITE_MODE = "0x1"
 
 
 @pytest.mark.usefixtures("run_devnet_in_background")
 @pytest.mark.parametrize(
-    "run_devnet_in_background, expected_tx_hash, expected_block_hash",
+    "run_devnet_in_background, expected_block_hash",
     [
         (
             [*PREDEPLOY_ACCOUNT_CLI_ARGS],
-            EXPECTED_SALTY_DEPLOY_HASH,
             "",
         ),
         (
             [*PREDEPLOY_ACCOUNT_CLI_ARGS, "--lite-mode"],
-            EXPECTED_SALTY_DEPLOY_HASH_LITE_MODE,
             EXPECTED_SALTY_DEPLOY_BLOCK_HASH_LITE_MODE,
         ),
     ],
     indirect=True,
 )
-def test_general_workflow(expected_tx_hash, expected_block_hash):
+def test_general_workflow(expected_block_hash):
     """Test devnet with CLI"""
     deploy_info = deploy(CONTRACT_PATH, inputs=["0"])
 
@@ -87,16 +83,14 @@ def test_general_workflow(expected_tx_hash, expected_block_hash):
     assert_transaction_receipt_not_received(NONEXISTENT_TX_HASH)
 
     # check code
-    assert_contract_code(deploy_info["address"])
+    assert_contract_code_present(deploy_info["address"])
 
     # check contract class
-    class_by_address = get_full_contract(deploy_info["address"])
-    assert_contract_class(class_by_address, CONTRACT_PATH)
+    assert_full_contract(address=deploy_info["address"], expected_path=CONTRACT_PATH)
 
     # check contract class through class hash
     class_hash = get_class_hash_at(deploy_info["address"])
-    class_by_hash = get_class_by_hash(class_hash)
-    assert_equal(class_by_address, class_by_hash)
+    assert_class_by_hash(class_hash, expected_path=CONTRACT_PATH)
 
     # increase and assert balance
     invoke_tx_hash = invoke(
@@ -132,7 +126,7 @@ def test_general_workflow(expected_tx_hash, expected_block_hash):
             inputs=None,
             expected_status="ACCEPTED_ON_L2",
             expected_address=EXPECTED_SALTY_DEPLOY_ADDRESS,
-            expected_tx_hash=expected_tx_hash,
+            expected_tx_hash=EXPECTED_SALTY_DEPLOY_HASH,
         )
 
     salty_invoke_tx_hash = invoke(
diff --git a/test/test_transaction_trace.py b/test/test_transaction_trace.py
index cbe38203d..8598ec638 100644
--- a/test/test_transaction_trace.py
+++ b/test/test_transaction_trace.py
@@ -26,13 +26,15 @@
 )
 
 
-def get_transaction_trace_response(tx_hash=None):
+def get_transaction_trace_response(tx_hash=None, server_url=APP_URL):
     """Get transaction trace response"""
     params = {
         "transactionHash": tx_hash,
     }
 
-    res = requests.get(f"{APP_URL}/feeder_gateway/get_transaction_trace", params=params)
+    res = requests.get(
+        f"{server_url}/feeder_gateway/get_transaction_trace", params=params
+    )
 
     return res
 
@@ -100,18 +102,23 @@ def test_nonexistent_transaction_hash():
     assert res.status_code == 500
 
 
-def assert_get_block_traces_response(params, expected_tx_hash):
-    """Assert response of get_block_traces"""
+def get_block_traces(params: dict):
+    """Get block traces"""
     block_traces = requests.get(
         f"{APP_URL}/feeder_gateway/get_block_traces", params=params
     ).json()
 
     # loading to assert valid structure
-    BlockTransactionTraces.load(block_traces)
+    return BlockTransactionTraces.load(block_traces)
+
+
+def assert_get_block_traces_response(params: dict, expected_tx_hash: str):
+    """Assert response of get_block_traces"""
+    block_traces = get_block_traces(params=params)
 
     # index 0 assuming it's the only tx in the response
-    actual_tx_hash = block_traces["traces"][0]["transaction_hash"]
-    assert actual_tx_hash == expected_tx_hash
+    actual_tx_hash = block_traces.traces[0].transaction_hash
+    assert actual_tx_hash == int(expected_tx_hash, 16)
 
 
 @pytest.mark.transaction_trace
diff --git a/test/testnet_deployment.py b/test/testnet_deployment.py
new file mode 100644
index 000000000..805ee0761
--- /dev/null
+++ b/test/testnet_deployment.py
@@ -0,0 +1,16 @@
+"""Contains info about deployment of a test contract on alpha-goerli-2"""
+
+from .shared import ALPHA_GOERLI2_URL, PREDEPLOY_ACCOUNT_CLI_ARGS
+
+TESTNET_URL = ALPHA_GOERLI2_URL
+TESTNET_CONTRACT_ADDRESS = (
+    "0x32320dbdff79639db4ac0ff1f9f8b7450d31fee8ca1bccea7cfa0d7765fe0b2"
+)
+TESTNET_CONTRACT_SALT = (
+    "0x10477367a9748e55196ab3c9ce04be74253cdb974e35a1d52ccda74d6d0e76b"
+)
+TESTNET_CONTRACT_CLASS_HASH = (
+    "0x028c7d54caa154d29953a26857c200623fd185bffa178a185d0ff247d22127a9"
+)
+TESTNET_DEPLOYMENT_BLOCK = 8827  # this is when the contract was deployed
+TESTNET_FORK_PARAMS = [*PREDEPLOY_ACCOUNT_CLI_ARGS, "--fork-network", "alpha-goerli2"]
diff --git a/test/util.py b/test/util.py
index 518df5ab2..97905da07 100644
--- a/test/util.py
+++ b/test/util.py
@@ -8,10 +8,11 @@
 import re
 import subprocess
 import time
-from typing import List
+from typing import IO, List
 import requests
 
 from starkware.starknet.cli.starknet_cli import get_salt
+from starkware.starknet.definitions.error_codes import StarknetErrorCode
 from starkware.starknet.definitions.transaction_type import TransactionType
 from starkware.starknet.services.api.contract_class import ContractClass
 from starkware.starknet.services.api.gateway.transaction import Deploy
@@ -37,6 +38,8 @@ def run_devnet_in_background(*args, stderr=None, stdout=None):
     if "--accounts" not in args:
         args = [*args, "--accounts", "1"]
 
+    port = args[args.index("--port") + 1] if "--port" in args else PORT
+
     command = [
         "poetry",
         "run",
@@ -44,13 +47,14 @@ def run_devnet_in_background(*args, stderr=None, stdout=None):
         "--host",
         HOST,
         "--port",
-        PORT,
+        port,
         *args,
     ]
     # pylint: disable=consider-using-with
     proc = subprocess.Popen(command, close_fds=True, stderr=stderr, stdout=stdout)
 
-    ensure_server_alive(f"{APP_URL}/is_alive", proc)
+    healthcheck_url = f"http://{HOST}:{port}/is_alive"
+    ensure_server_alive(healthcheck_url, proc)
     return proc
 
 
@@ -147,11 +151,12 @@ def extract_address(stdout):
     return extract(r"Contract address: (\w*)", stdout)
 
 
-def run_starknet(args, raise_on_nonzero=True, add_gateway_urls=True):
+def run_starknet(args, raise_on_nonzero=True, gateway_url=APP_URL):
     """Wrapper around subprocess.run"""
     my_args = ["poetry", "run", "starknet", *args, "--no_wallet"]
-    if add_gateway_urls:
-        my_args.extend(["--gateway_url", APP_URL, "--feeder_gateway_url", APP_URL])
+    # there is no case when gateway should not be equal to feeder gateway
+    my_args.extend(["--gateway_url", gateway_url, "--feeder_gateway_url", gateway_url])
+
     output = subprocess.run(my_args, encoding="utf-8", check=False, capture_output=True)
     if output.returncode != 0 and raise_on_nonzero:
         if output.stderr:
@@ -160,20 +165,20 @@ def run_starknet(args, raise_on_nonzero=True, add_gateway_urls=True):
     return output
 
 
-def send_tx(transaction: dict, tx_type: TransactionType) -> dict:
+def send_tx(transaction: dict, tx_type: TransactionType, gateway_url=APP_URL) -> dict:
     """
     Send transaction.
     Returns tx hash
     """
     resp = requests.post(
-        url=f"{APP_URL}/gateway/add_transaction",
+        url=f"{gateway_url}/gateway/add_transaction",
         json={**transaction, "type": tx_type.name},
     )
     assert resp.status_code == 200
     return resp.json()
 
 
-def deploy(contract, inputs=None, salt=None):
+def deploy(contract, inputs=None, salt=None, gateway_url=APP_URL):
     """Wrapper around starknet deploy"""
 
     inputs = inputs or []
@@ -185,7 +190,7 @@ def deploy(contract, inputs=None, salt=None):
         version=SUPPORTED_TX_VERSION,
     ).dump()
 
-    resp = send_tx(deploy_tx, TransactionType.DEPLOY)
+    resp = send_tx(deploy_tx, TransactionType.DEPLOY, gateway_url)
 
     return {
         "tx_hash": resp["transaction_hash"],
@@ -216,9 +221,13 @@ def estimate_message_fee(
     return extract_fee(output.stdout)
 
 
-def assert_transaction(tx_hash, expected_status, expected_signature=None):
+def assert_transaction(
+    tx_hash, expected_status, expected_signature=None, feeder_gateway_url=APP_URL
+):
     """Wrapper around starknet get_transaction"""
-    output = run_starknet(["get_transaction", "--hash", tx_hash])
+    output = run_starknet(
+        ["get_transaction", "--hash", tx_hash], gateway_url=feeder_gateway_url
+    )
     transaction = json.loads(output.stdout)
     assert_equal(transaction["status"], expected_status, transaction)
     if expected_signature:
@@ -267,29 +276,39 @@ def assert_keys(dictionary, keys):
     assert dictionary.keys() == expected_set, f"{dictionary.keys()} != {expected_set}"
 
 
-def assert_transaction_not_received(tx_hash):
+def assert_transaction_not_received(tx_hash: str, feeder_gateway_url=APP_URL):
     """Assert correct tx response when there is no tx with `tx_hash`."""
-    output = run_starknet(["get_transaction", "--hash", tx_hash])
+    output = run_starknet(
+        ["get_transaction", "--hash", tx_hash], gateway_url=feeder_gateway_url
+    )
     transaction = json.loads(output.stdout)
     assert_equal(transaction, {"status": "NOT_RECEIVED"})
 
 
-def assert_transaction_receipt_not_received(tx_hash):
+def assert_transaction_receipt_not_received(tx_hash: str, feeder_gateway_url=APP_URL):
     """Assert correct tx receipt response when there is no tx with `tx_hash`."""
-    receipt = get_transaction_receipt(tx_hash)
+    receipt = get_transaction_receipt(tx_hash, feeder_gateway_url=feeder_gateway_url)
     assert_equal(
         receipt,
         {
             "events": [],
             "l2_to_l1_messages": [],
             "status": "NOT_RECEIVED",
-            "transaction_hash": tx_hash,
+            "transaction_hash": "0x0",
         },
     )
 
 
 # pylint: disable=too-many-arguments
-def estimate_fee(function, inputs, address, abi_path, signature=None, nonce=None):
+def estimate_fee(
+    function,
+    inputs,
+    address,
+    abi_path,
+    signature=None,
+    nonce=None,
+    feeder_gateway_url=APP_URL,
+):
     """Wrapper around starknet estimate_fee. Returns fee in wei."""
     args = [
         "invoke",
@@ -310,13 +329,15 @@ def estimate_fee(function, inputs, address, abi_path, signature=None, nonce=None
     if nonce is not None:
         args.extend(["--nonce", str(nonce)])
 
-    output = run_starknet(args)
+    output = run_starknet(args, gateway_url=feeder_gateway_url)
 
     print("Estimate fee successful!")
     return extract_fee(output.stdout)
 
 
-def call(function: str, address: str, abi_path: str, inputs=None):
+def call(
+    function: str, address: str, abi_path: str, inputs=None, feeder_gateway_url=APP_URL
+):
     """Wrapper around starknet call"""
     args = [
         "call",
@@ -330,7 +351,7 @@ def call(function: str, address: str, abi_path: str, inputs=None):
     if inputs:
         args.extend(["--inputs", *inputs])
 
-    output = run_starknet(args)
+    output = run_starknet(args, gateway_url=feeder_gateway_url)
 
     print("Call successful!")
     return output.stdout.rstrip()
@@ -343,9 +364,11 @@ def load_contract_class(contract_path: str):
     return ContractClass.load(loaded_contract)
 
 
-def assert_tx_status(tx_hash, expected_tx_status):
+def assert_tx_status(tx_hash, expected_tx_status: str, feeder_gateway_url=APP_URL):
     """Asserts the tx_status of the tx with tx_hash."""
-    output = run_starknet(["tx_status", "--hash", tx_hash])
+    output = run_starknet(
+        ["tx_status", "--hash", tx_hash], gateway_url=feeder_gateway_url
+    )
     response = json.loads(output.stdout)
     tx_status = response["tx_status"]
     assert_equal(tx_status, expected_tx_status, response)
@@ -354,14 +377,33 @@ def assert_tx_status(tx_hash, expected_tx_status):
         assert "tx_failure_reason" in response, f"Key not found in {response}"
 
 
-def assert_contract_code(address):
-    """Asserts the content of the code of a contract at address."""
-    output = run_starknet(["get_code", "--contract_address", address])
+def assert_contract_code_present(address: str, feeder_gateway_url=APP_URL):
+    """Asserts the content of the code of a contract at `address`."""
+    output = run_starknet(
+        ["get_code", "--contract_address", address], gateway_url=feeder_gateway_url
+    )
     code = json.loads(output.stdout)
-    # just checking key equality
+
+    assert code["abi"]  # assert non-empty
+    assert code["bytecode"]  # assert non-empty
+
+    # assert no other keys
     assert_equal(sorted(code.keys()), ["abi", "bytecode"])
 
 
+def assert_contract_code_not_present(address: str, feeder_gateway_url=APP_URL):
+    """Assert abi and bytecode empty"""
+    resp = requests.get(
+        f"{feeder_gateway_url}/feeder_gateway/get_code?contractAddress={address}"
+    )
+
+    code = resp.json()
+    assert code["abi"] == {}
+    assert code["bytecode"] == []
+
+    assert resp.status_code == 200
+
+
 def assert_contract_class(actual_class: ContractClass, expected_class_path: str):
     """Asserts equality between `actual_class` and class at `expected_class_path`."""
 
@@ -369,10 +411,13 @@ def assert_contract_class(actual_class: ContractClass, expected_class_path: str)
     assert_equal(actual_class, loaded_contract_class.remove_debug_info())
 
 
-def assert_storage(address, key, expected_value):
+def assert_storage(
+    address: str, key: str, expected_value: str, feeder_gateway_url=APP_URL
+):
     """Asserts the storage value stored at (address, key)."""
     output = run_starknet(
-        ["get_storage_at", "--contract_address", address, "--key", key]
+        ["get_storage_at", "--contract_address", address, "--key", key],
+        gateway_url=feeder_gateway_url,
     )
     assert_equal(output.stdout.rstrip(), expected_value)
 
@@ -383,28 +428,96 @@ def load_json_from_path(path):
         return json.load(expected_file)
 
 
-def get_transaction_receipt(tx_hash: str):
+def get_transaction_receipt(tx_hash: str, feeder_gateway_url=APP_URL):
     """Fetches the transaction receipt of transaction with tx_hash"""
-    output = run_starknet(["get_transaction_receipt", "--hash", tx_hash])
+    output = run_starknet(
+        ["get_transaction_receipt", "--hash", tx_hash], gateway_url=feeder_gateway_url
+    )
     return json.loads(output.stdout)
 
 
-def get_full_contract(contract_address: str) -> ContractClass:
+def get_full_contract(
+    contract_address: str, feeder_gateway_url=APP_URL
+) -> ContractClass:
     """Gets contract class by contract address"""
-    output = run_starknet(["get_full_contract", "--contract_address", contract_address])
+    output = run_starknet(
+        ["get_full_contract", "--contract_address", contract_address],
+        gateway_url=feeder_gateway_url,
+    )
     return ContractClass.loads(output.stdout)
 
 
+def assert_full_contract_not_present(address: str, feeder_gateway_url=APP_URL):
+    """Assert that get_full_contract fails due to uninitialized contract"""
+    resp = requests.get(
+        f"{feeder_gateway_url}/feeder_gateway/get_full_contract",
+        {"contractAddress": address},
+    )
+
+    assert resp.json()["code"] == str(StarknetErrorCode.UNINITIALIZED_CONTRACT)
+    assert resp.status_code == 500
+
+
+def assert_full_contract(address: str, expected_path: str, feeder_gateway_url=APP_URL):
+    """Assert that the provided address has contract from `expected_path` deployed at it."""
+    class_by_address = get_full_contract(
+        contract_address=address, feeder_gateway_url=feeder_gateway_url
+    )
+    assert_contract_class(class_by_address, expected_class_path=expected_path)
+
+
 def get_class_hash_at(contract_address: str) -> str:
     """Gets class hash at given contract address"""
     output = run_starknet(["get_class_hash_at", "--contract_address", contract_address])
     return output.stdout
 
 
-def get_class_by_hash(class_hash: str):
+def assert_address_has_no_class_hash(contract_address: str, feeder_gateway_url=APP_URL):
+    """There should be no class hash at `contract_address`."""
+    resp = requests.get(
+        f"{feeder_gateway_url}/feeder_gateway/get_class_hash_at",
+        {"contractAddress": contract_address},
+    )
+    assert resp.json()["code"] == str(StarknetErrorCode.UNINITIALIZED_CONTRACT)
+    assert resp.status_code == 500
+
+
+def assert_class_hash_at_address(
+    contract_address: str, expected_class_hash: str, feeder_gateway_url=APP_URL
+):
+    """The class hash at `contract_address` should be `expected_class_hash`."""
+    resp = requests.get(
+        f"{feeder_gateway_url}/feeder_gateway/get_class_hash_at",
+        {"contractAddress": contract_address},
+    )
+    received_class_hash = int(json.loads(resp.text), 16)
+    assert received_class_hash == int(expected_class_hash, 16)
+    assert resp.status_code == 200
+
+
+def get_class_by_hash(class_hash: str, feeder_gateway_url=APP_URL):
     """Gets contract class by contract hash"""
-    output = run_starknet(["get_class_by_hash", "--class_hash", class_hash])
-    return ContractClass.loads(output.stdout)
+    return requests.get(
+        f"{feeder_gateway_url}/feeder_gateway/get_class_by_hash",
+        {"classHash": class_hash},
+    )
+
+
+def assert_class_by_hash(
+    class_hash: str, expected_path: str, feeder_gateway_url=APP_URL
+):
+    """Assert the class at `class_hash` matches what is at `expected_path`."""
+    resp = get_class_by_hash(class_hash, feeder_gateway_url=feeder_gateway_url)
+    class_by_hash = ContractClass.loads(resp.text)
+    assert_contract_class(class_by_hash, expected_class_path=expected_path)
+    assert resp.status_code == 200
+
+
+def assert_class_by_hash_not_present(class_hash: str, feeder_gateway_url=APP_URL):
+    """Assert the server holds no class at provided `class_hash`."""
+    resp = get_class_by_hash(class_hash, feeder_gateway_url=feeder_gateway_url)
+    assert resp.json()["code"] == str(StarknetErrorCode.UNDECLARED_CLASS)
+    assert resp.status_code == 500
 
 
 def assert_receipt(tx_hash, expected_path):
@@ -420,6 +533,15 @@ def assert_receipt(tx_hash, expected_path):
     assert_equal(receipt, expected_receipt)
 
 
+def assert_receipt_present(
+    tx_hash: str, expected_status: str, feeder_gateway_url=APP_URL
+):
+    """Asserts the content of the receipt of tx with tx_hash is non-empty"""
+    receipt = get_transaction_receipt(tx_hash, feeder_gateway_url=feeder_gateway_url)
+    assert receipt["transaction_hash"] == tx_hash
+    assert receipt["status"] == expected_status
+
+
 def assert_events(tx_hash, expected_path):
     """Asserts the content of the events element of the receipt of tx with tx_hash."""
     receipt = get_transaction_receipt(tx_hash)
@@ -427,16 +549,23 @@ def assert_events(tx_hash, expected_path):
     assert_equal(receipt["events"], expected_receipt["events"])
 
 
-def get_block(block_number=None, parse=False):
+def get_block(
+    block_number=None, block_hash=None, parse=False, feeder_gateway_url=APP_URL
+):
     """Get the block with block_number. If no number provided, return the last."""
     args = ["get_block"]
     if block_number:
         args.extend(["--number", str(block_number)])
+    if block_hash:
+        args.extend(["--hash", str(block_hash)])
+
     if parse:
-        output = run_starknet(args, raise_on_nonzero=True)
+        output = run_starknet(
+            args, raise_on_nonzero=True, gateway_url=feeder_gateway_url
+        )
         return json.loads(output.stdout)
 
-    return run_starknet(args, raise_on_nonzero=False)
+    return run_starknet(args, raise_on_nonzero=False, gateway_url=feeder_gateway_url)
 
 
 def assert_negative_block_input():
@@ -478,14 +607,6 @@ def assert_block(latest_block_number, latest_tx_hash):
     assert re.match(r"^[a-fA-F0-9]{64}$", latest_block["state_root"])
 
 
-def assert_block_hash(latest_block_number, expected_block_hash):
-    """Asserts the content of the block with block_number."""
-
-    block = get_block(block_number=latest_block_number, parse=True)
-    assert_equal(block["block_hash"], expected_block_hash)
-    assert_equal(block["status"], "ACCEPTED_ON_L2")
-
-
 def assert_salty_deploy(
     contract_path, inputs, salt, expected_status, expected_address, expected_tx_hash
 ):
@@ -544,3 +665,8 @@ def stop(self):
         if self.proc:
             terminate_and_wait(self.proc)
             self.proc = None
+
+
+def read_stream(stream: IO, encoding="utf-8") -> str:
+    """Return stdout and stderr of `proc`"""
+    return stream.read().decode(encoding)