diff --git a/CHANGELOG.md b/CHANGELOG.md
index f34df27..93c3efc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
 # Changelog
 
+## Unreleased
+
+### Enhancements
+
+- Improve `Ethers.deploy/2` error handling
+- Implement `Ethers.CcipRead` to support EIP-3668
+
 ## v0.5.5 (2024-12-03)
 
 ### Enhancements
diff --git a/config/prod.exs b/config/prod.exs
index e69de29..becde76 100644
--- a/config/prod.exs
+++ b/config/prod.exs
@@ -0,0 +1 @@
+import Config
diff --git a/config/test.exs b/config/test.exs
index c855b21..d01e49a 100644
--- a/config/test.exs
+++ b/config/test.exs
@@ -2,4 +2,6 @@ import Config
 
 config :ethereumex, url: "http://localhost:8545"
 
-config :ethers, :ignore_error_consolidation?, true
+config :ethers, ignore_error_consolidation?: true
+
+config :ethers, ccip_req_opts: [plug: {Req.Test, Ethers.CcipReq}, retry: false]
diff --git a/lib/ethers.ex b/lib/ethers.ex
index d2f72f1..462b57e 100644
--- a/lib/ethers.ex
+++ b/lib/ethers.ex
@@ -221,11 +221,11 @@ defmodule Ethers do
   def deploy(contract_module_or_binary, overrides \\ [])
 
   def deploy(contract_module, overrides) when is_atom(contract_module) do
-    with true <- function_exported?(contract_module, :__contract_binary__, 0),
-         bin when not is_nil(bin) <- contract_module.__contract_binary__() do
-      deploy(bin, overrides)
-    else
-      _error ->
+    case contract_module.__contract_binary__() do
+      bin when is_binary(bin) ->
+        deploy(bin, overrides)
+
+      nil ->
         {:error, :binary_not_found}
     end
   end
diff --git a/lib/ethers/ccip_read.ex b/lib/ethers/ccip_read.ex
new file mode 100644
index 0000000..991a481
--- /dev/null
+++ b/lib/ethers/ccip_read.ex
@@ -0,0 +1,117 @@
+defmodule Ethers.CcipRead do
+  @moduledoc """
+  CCIP Read ([EIP-3668](https://eips.ethereum.org/EIPS/eip-3668)) implementation
+
+  NOTE: Currently supports URLs with "https" scheme only
+  """
+
+  require Logger
+
+  alias Ethers.Contracts.CcipRead.Errors.OffchainLookup
+  alias Ethers.TxData
+  alias Ethers.Utils
+
+  @error_selector "0x556f1830"
+  @error_selector_bin Utils.hex_decode!(@error_selector)
+  @supported_schemas ["https"]
+
+  @doc """
+  Same as `Ethers.call/2` but will handle `Ethers.Contacts.CcipRead.Errors.OffchainLookup` errors
+  and performs an offchain lookup as per [EIP-3668](https://eips.ethereum.org/EIPS/eip-3668) specs.
+
+  ## Options and Overrides
+  Accepts same options as `Ethers.call/2`
+  """
+  @spec call(TxData.t(), Keyword.t()) :: {:ok, [term()] | term()} | {:error, term()}
+  def call(tx_data, opts) do
+    case Ethers.call(tx_data, opts) do
+      {:ok, result} ->
+        {:ok, result}
+
+      {:error, %_{} = error} ->
+        if offchain_lookup_error?(error) do
+          ccip_resolve(error, tx_data, opts)
+        else
+          {:error, error}
+        end
+
+      {:error, %{"data" => <<@error_selector, _::binary>> = error_data}} ->
+        with {:ok, decoded_error} <- Utils.hex_decode(error_data),
+             {:ok, lookup_error} <- OffchainLookup.decode(decoded_error) do
+          ccip_resolve(lookup_error, tx_data, opts)
+        end
+
+      {:error, reason} ->
+        {:error, reason}
+    end
+  end
+
+  defp ccip_resolve(error, tx_data, opts) do
+    with {:ok, data} <-
+           error.urls
+           |> Enum.filter(fn url ->
+             url |> String.downcase() |> String.starts_with?(@supported_schemas)
+           end)
+           |> resolve_first(error) do
+      data = ABI.TypeEncoder.encode([data, error.extra_data], [:bytes, :bytes])
+      tx_data = %{tx_data | data: Utils.hex_encode(error.callback_function <> data)}
+      Ethers.call(tx_data, opts)
+    end
+  end
+
+  defp resolve_first([], _), do: {:error, :ccip_read_failed}
+
+  defp resolve_first([url | rest], error) do
+    case do_resolve_single(url, error) do
+      {:ok, data} ->
+        {:ok, data}
+
+      {:error, reason} ->
+        Logger.error("CCIP READ: failed resolving #{url} error: #{inspect(reason)}")
+
+        resolve_first(rest, error)
+    end
+  end
+
+  defp do_resolve_single(url_template, error) do
+    sender = Ethers.Utils.hex_encode(error.sender)
+    data = Ethers.Utils.hex_encode(error.call_data)
+
+    req_opts =
+      if String.contains?(url_template, "{data}") do
+        [method: :get]
+      else
+        [method: :post, json: %{data: data, sender: sender}]
+      end
+
+    url = url_template |> String.replace("{sender}", sender) |> String.replace("{data}", data)
+    req_opts = req_opts |> Keyword.put(:url, url) |> Keyword.merge(ccip_req_opts())
+
+    Logger.debug("CCIP READ: trying #{url}")
+
+    case Req.request(req_opts) do
+      {:ok, %Req.Response{status: 200, body: %{"data" => data}}} ->
+        case Utils.hex_decode(data) do
+          {:ok, hex} -> {:ok, hex}
+          :error -> {:error, :hex_decode_failed}
+        end
+
+      {:ok, resp} ->
+        {:error, resp}
+
+      {:error, reason} ->
+        {:error, reason}
+    end
+  end
+
+  defp offchain_lookup_error?(%mod{}) do
+    mod.function_selector().method_id == @error_selector_bin
+  rescue
+    UndefinedFunctionError ->
+      false
+  end
+
+  defp ccip_req_opts do
+    Application.get_env(:ethers, :ccip_req_opts, [])
+  end
+end
diff --git a/lib/ethers/contracts/ccip_read.ex b/lib/ethers/contracts/ccip_read.ex
new file mode 100644
index 0000000..6d3fddf
--- /dev/null
+++ b/lib/ethers/contracts/ccip_read.ex
@@ -0,0 +1,7 @@
+defmodule Ethers.Contracts.CcipRead do
+  @moduledoc """
+  CCIP Read ([EIP-3668](https://eips.ethereum.org/EIPS/eip-3668)) contract
+  """
+
+  use Ethers.Contract, abi: :ccip_read
+end
diff --git a/mix.exs b/mix.exs
index 5902690..77cdf39 100644
--- a/mix.exs
+++ b/mix.exs
@@ -108,7 +108,9 @@ defmodule Ethers.MixProject do
       {:ex_secp256k1, "~> 0.7.2", optional: true},
       {:excoveralls, "~> 0.10", only: :test},
       {:idna, "~> 6.1"},
-      {:jason, "~> 1.4"}
+      {:jason, "~> 1.4"},
+      {:plug, ">= 1.0.0", only: :test},
+      {:req, "~> 0.5"}
     ]
   end
 
diff --git a/mix.lock b/mix.lock
index a33ccc1..d0561f6 100644
--- a/mix.lock
+++ b/mix.lock
@@ -25,7 +25,10 @@
   "nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"},
   "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
   "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
+  "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"},
+  "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
   "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
+  "req": {:hex, :req, "0.5.1", "90584216d064389a4ff2d4279fe2c11ff6c812ab00fa01a9fb9d15457f65ba70", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "7ea96a1a95388eb0fefa92d89466cdfedba24032794e5c1147d78ec90db7edca"},
   "rustler_precompiled": {:hex, :rustler_precompiled, "0.8.1", "8afe0b6f3a9a677ada046cdd23e3f4c6399618b91a6122289324774961281e1e", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "90b8c2297bf7959cfa1c927b2881faad7bb0707183124955369991b76177a166"},
   "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
   "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
diff --git a/priv/abi/ccip_read.json b/priv/abi/ccip_read.json
new file mode 100644
index 0000000..490940b
--- /dev/null
+++ b/priv/abi/ccip_read.json
@@ -0,0 +1,33 @@
+[
+  {
+    "inputs": [
+      {
+        "internalType": "address",
+        "name": "sender",
+        "type": "address"
+      },
+      {
+        "internalType": "string[]",
+        "name": "urls",
+        "type": "string[]"
+      },
+      {
+        "internalType": "bytes",
+        "name": "callData",
+        "type": "bytes"
+      },
+      {
+        "internalType": "bytes4",
+        "name": "callbackFunction",
+        "type": "bytes4"
+      },
+      {
+        "internalType": "bytes",
+        "name": "extraData",
+        "type": "bytes"
+      }
+    ],
+    "name": "OffchainLookup",
+    "type": "error"
+  }
+]
diff --git a/test/ethers/ccip_read_test.exs b/test/ethers/ccip_read_test.exs
new file mode 100644
index 0000000..09a2534
--- /dev/null
+++ b/test/ethers/ccip_read_test.exs
@@ -0,0 +1,131 @@
+defmodule Ethers.CcipReadTest do
+  use ExUnit.Case
+
+  import Ethers.TestHelpers
+
+  alias Ethers.CcipRead
+  alias Ethers.Contract.Test.CcipReadTestContract
+  alias Ethers.Utils
+
+  @from "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
+
+  setup do
+    address = deploy(CcipReadTestContract, from: @from)
+    [address: address]
+  end
+
+  describe "call/2" do
+    test "returns successful result when no offchain lookup is needed", %{address: address} do
+      assert {:ok, "direct value"} =
+               CcipReadTestContract.get_direct_value()
+               |> CcipRead.call(to: address)
+    end
+
+    test "handles OffchainLookup error and performs offchain lookup", %{address: address} do
+      Req.Test.expect(Ethers.CcipReq, fn conn ->
+        assert ["ccip", sender, data] = conn.path_info
+
+        # Verify the request parameters
+        assert String.starts_with?(sender, "0x")
+        assert String.starts_with?(data, "0x")
+
+        Req.Test.json(conn, %{data: data})
+      end)
+
+      assert {:ok, 100} =
+               CcipReadTestContract.get_value(100)
+               |> CcipRead.call(to: address)
+    end
+
+    test "filters out non-https URLs from the lookup list", %{address: address} do
+      # The contract provides both https and non-https URLs
+      # Our implementation should only try the https ones
+      Req.Test.expect(Ethers.CcipReq, fn conn ->
+        assert conn.scheme == :https
+        assert ["ccip", _sender, data] = conn.path_info
+        Req.Test.json(conn, %{data: data})
+      end)
+
+      assert {:ok, 300} =
+               CcipReadTestContract.get_value(300)
+               |> CcipRead.call(to: address)
+    end
+
+    test "tries next URL when first URL fails", %{address: address} do
+      # First request fails
+      Req.Test.expect(Ethers.CcipReq, 2, fn conn ->
+        if conn.host == "example.com" do
+          conn
+          |> Plug.Conn.put_status(500)
+          |> Req.Test.json(%{data: "0x"})
+        else
+          # Second URL succeeds
+          Req.Test.json(conn, %{
+            data: ABI.TypeEncoder.encode([700], [{:uint, 256}]) |> Utils.hex_encode()
+          })
+        end
+      end)
+
+      assert {:ok, 700} =
+               CcipReadTestContract.get_value(400)
+               |> CcipRead.call(to: address)
+    end
+
+    test "returns error when all URLs fail", %{address: address} do
+      # Both URLs fail
+      Req.Test.stub(Ethers.CcipReq, fn conn ->
+        Plug.Conn.put_status(conn, 500)
+        |> Req.Test.text("Failed")
+      end)
+
+      assert {:error, :ccip_read_failed} =
+               CcipReadTestContract.get_value(500)
+               |> CcipRead.call(to: address)
+    end
+
+    test "returns error when response is not 200", %{address: address} do
+      Req.Test.stub(Ethers.CcipReq, fn conn ->
+        conn
+        |> Plug.Conn.put_status(404)
+        |> Req.Test.json(%{error: "Not found"})
+      end)
+
+      assert {:error, :ccip_read_failed} =
+               CcipReadTestContract.get_value(600)
+               |> CcipRead.call(to: address)
+    end
+
+    test "returns error when response body is invalid", %{address: address} do
+      Req.Test.stub(Ethers.CcipReq, fn conn ->
+        conn
+        |> Plug.Conn.put_status(200)
+        |> Req.Test.text("invalid json")
+      end)
+
+      assert {:error, :ccip_read_failed} =
+               CcipReadTestContract.get_value(700)
+               |> CcipRead.call(to: address)
+    end
+
+    test "returns error when hex decoding fails", %{address: address} do
+      Req.Test.stub(Ethers.CcipReq, fn conn ->
+        Req.Test.json(conn, %{data: "invalid hex"})
+      end)
+
+      assert {:error, :ccip_read_failed} =
+               CcipReadTestContract.get_value(800)
+               |> CcipRead.call(to: address)
+    end
+
+    test "returns original error when it's not an OffchainLookup error", %{address: address} do
+      assert {:error, %Ethers.Contract.Test.CcipReadTestContract.Errors.InvalidValue{}} =
+               CcipReadTestContract.get_value(0)
+               |> CcipRead.call(to: address)
+
+      # Sending value to a non-payable function should return the original error
+      assert {:error, %{"code" => 3}} =
+               CcipReadTestContract.get_value(1)
+               |> CcipRead.call(to: address, value: 1000)
+    end
+  end
+end
diff --git a/test/ethers_test.exs b/test/ethers_test.exs
index 54a8cea..8de5f2f 100644
--- a/test/ethers_test.exs
+++ b/test/ethers_test.exs
@@ -210,7 +210,9 @@ defmodule EthersTest do
     end
 
     test "returns error if the module does not include the binary" do
-      assert {:error, :binary_not_found} = Ethers.deploy(NotFoundContract, from: @from)
+      assert_raise UndefinedFunctionError, fn ->
+        assert {:error, :binary_not_found} = Ethers.deploy(NotFoundContract, from: @from)
+      end
 
       assert {:error, :binary_not_found} =
                Ethers.deploy(Ethers.Contracts.ERC20, from: @from)
diff --git a/test/support/contracts.ex b/test/support/contracts.ex
index 4afb568..43f00ab 100644
--- a/test/support/contracts.ex
+++ b/test/support/contracts.ex
@@ -5,3 +5,8 @@ defmodule Ethers.Contract.Test.RevertContract do
   @moduledoc false
   use Ethers.Contract, abi_file: "tmp/revert_abi.json"
 end
+
+defmodule Ethers.Contract.Test.CcipReadTestContract do
+  @moduledoc false
+  use Ethers.Contract, abi_file: "tmp/ccip_read_abi.json"
+end
diff --git a/test/support/contracts/ccip_read.sol b/test/support/contracts/ccip_read.sol
new file mode 100644
index 0000000..5d42846
--- /dev/null
+++ b/test/support/contracts/ccip_read.sol
@@ -0,0 +1,50 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.17;
+
+contract CcipReadTest {
+    error OffchainLookup(
+        address sender,
+        string[] urls,
+        bytes callData,
+        bytes4 callbackFunction,
+        bytes extraData
+    );
+
+    error InvalidValue();
+
+    function getValue(uint256 value) external view returns (uint256) {
+        string[] memory urls = new string[](3);
+        urls[0] = "invalid://example.com/ccip/{sender}/{data}";
+        urls[1] = "https://example.com/ccip/{sender}/{data}";
+        urls[2] = "https://backup.example.com/ccip";
+
+        if (value == 0) {
+            revert InvalidValue();
+        }
+
+        revert OffchainLookup(
+            address(this),
+            urls,
+            abi.encode(value),
+            this.handleResponse.selector,
+            bytes("testing")
+        );
+    }
+
+    function handleResponse(bytes calldata response, bytes calldata extraData) 
+        external 
+        pure 
+        returns (uint256) 
+    {
+        // Validate extraData
+        require(keccak256(abi.encodePacked(extraData)) == keccak256(bytes("testing")));
+        
+        // Decode the response - in real contract you'd validate this
+        return abi.decode(response, (uint256));
+    }
+
+    // Helper function to test non-CCIP functionality
+    function getDirectValue() external pure returns (string memory) {
+        return "direct value";
+    }
+} 
diff --git a/test/test_helper.exs b/test/test_helper.exs
index 869559e..6a0af57 100644
--- a/test/test_helper.exs
+++ b/test/test_helper.exs
@@ -1 +1 @@
-ExUnit.start()
+ExUnit.start(capture_log: true)