diff --git a/CHANGELOG.md b/CHANGELOG.md index 74f539d..5b4ac58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## UNRELEASED + +### Breaking Changes + +- `Ethers.Utils.human_arg/2` now returns checksummed addresses instead of lowercase addresses for better EIP-55 compliance + +### Bug Fixes + +- Fix `Ethers.Utils.human_arg/2` incorrectly handling 20-byte binary addresses that start with bytes `0x30` and `0x78` (which represent the string "0x") + ## 0.6.9 (2025-10-16) ### Bug Fixes diff --git a/lib/ethers/utils.ex b/lib/ethers/utils.ex index 847fb22..b7cefe2 100644 --- a/lib/ethers/utils.ex +++ b/lib/ethers/utils.ex @@ -192,7 +192,7 @@ defmodule Ethers.Utils do <<192, 42, 170, 57, 178, 35, 254, 141, 10, 14, 92, 79, 39, 234, 217, 8, 60, 117, 108, 194>> """ @spec prepare_arg(term(), ABI.FunctionSelector.type()) :: term() - def prepare_arg("0x" <> _ = argument, :address), do: hex_decode!(argument) + def prepare_arg(<<"0x", address::binary-40>>, :address), do: hex_decode!(address) def prepare_arg(arguments, {:array, type}), do: Enum.map(arguments, &prepare_arg(&1, type)) def prepare_arg(arguments, {:array, type, _}), do: Enum.map(arguments, &prepare_arg(&1, type)) @@ -212,14 +212,17 @@ defmodule Ethers.Utils do ## Examples iex> Ethers.Utils.human_arg(<<192, 42, 170, 57, 178, 35, 254, 141, 10, 14, 92, 79, 39, ...> 234, 217, 8, 60, 117, 108, 194>>, :address) - "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" + "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" iex> Ethers.Utils.human_arg("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", :address) - "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" + "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" """ @spec human_arg(term(), ABI.FunctionSelector.type()) :: term() - def human_arg("0x" <> _ = argument, :address), do: argument - def human_arg(argument, :address), do: hex_encode(argument) + def human_arg(<<"0x", _::binary-40>> = address, :address), do: to_checksum_address(address) + def human_arg(<>, :address), do: to_checksum_address(address) + + def human_arg(invalid, :address), + do: raise(ArgumentError, "Invalid address: #{inspect(invalid)}") def human_arg(arguments, {:array, type}), do: Enum.map(arguments, &human_arg(&1, type)) def human_arg(arguments, {:array, type, _}), do: Enum.map(arguments, &human_arg(&1, type)) @@ -255,12 +258,15 @@ defmodule Ethers.Utils do iex> Ethers.Utils.to_checksum_address("0XDE709F2102306220921060314715629080e2Fb77", 30) "0xDe709F2102306220921060314715629080e2FB77" """ - @spec to_checksum_address(Ethers.Types.t_address(), pos_integer() | nil) :: + @spec to_checksum_address(Ethers.Types.t_address() | <<_::320>>, pos_integer() | nil) :: Ethers.Types.t_address() def to_checksum_address(address, chain_id \\ nil) - def to_checksum_address("0x" <> address, chain_id), do: to_checksum_address(address, chain_id) - def to_checksum_address("0X" <> address, chain_id), do: to_checksum_address(address, chain_id) + def to_checksum_address(<<"0x", address::binary-40>>, chain_id), + do: to_checksum_address(address, chain_id) + + def to_checksum_address(<<"0X", address::binary-40>>, chain_id), + do: to_checksum_address(address, chain_id) def to_checksum_address(<>, chain_id), do: hex_encode(address_bin, false) |> to_checksum_address(chain_id) @@ -341,20 +347,20 @@ defmodule Ethers.Utils do public_key_to_address(public_key, use_checksum_address) end - unless Code.ensure_loaded?(Ethers.secp256k1_module()) do - def public_key_to_address(<> = compressed, _use_checksum_address) + if Code.ensure_loaded?(Ethers.secp256k1_module()) do + def public_key_to_address(<> = compressed, use_checksum_address) + when pre in [2, 3] do + case Ethers.secp256k1_module().public_key_decompress(compressed) do + {:ok, public_key} -> public_key_to_address(public_key, use_checksum_address) + error -> raise ArgumentError, "Invalid compressed public key #{inspect(error)}" + end + end + else + def public_key_to_address(<> = _compressed, _use_checksum_address) when pre in [2, 3], do: raise("secp256k1 module not loaded") end - def public_key_to_address(<> = compressed, use_checksum_address) - when pre in [2, 3] do - case Ethers.secp256k1_module().public_key_decompress(compressed) do - {:ok, public_key} -> public_key_to_address(public_key, use_checksum_address) - error -> raise ArgumentError, "Invalid compressed public key #{inspect(error)}" - end - end - def public_key_to_address("0x" <> _ = key, use_checksum_address) do key |> hex_decode!() diff --git a/test/ethers/event_mixed_index_contract_test.exs b/test/ethers/event_mixed_index_contract_test.exs index 0da7ca4..8f7524a 100644 --- a/test/ethers/event_mixed_index_contract_test.exs +++ b/test/ethers/event_mixed_index_contract_test.exs @@ -10,7 +10,7 @@ defmodule Ethers.EventMixedIndexContractTest do alias Ethers.Contract.Test.EventMixedIndexContract - @from "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266" + @from "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" describe "event filters" do test "works with mixed indexed events" do @@ -76,7 +76,7 @@ defmodule Ethers.EventMixedIndexContractTest do end test "inspect returns correct value" do - assert ~s'#Ethers.EventFilter' == + assert ~s'#Ethers.EventFilter' == inspect( EventMixedIndexContract.EventFilters.transfer( "0x90f8bf6a479f320ead074411a4b0e7944ea80000", @@ -84,7 +84,7 @@ defmodule Ethers.EventMixedIndexContractTest do ) ) - assert ~s'#Ethers.EventFilter' == + assert ~s'#Ethers.EventFilter' == inspect( EventMixedIndexContract.EventFilters.transfer( nil, diff --git a/test/ethers/owner_contract_test.exs b/test/ethers/owner_contract_test.exs index 0dbd4d6..2913f4b 100644 --- a/test/ethers/owner_contract_test.exs +++ b/test/ethers/owner_contract_test.exs @@ -12,7 +12,7 @@ defmodule Ethers.OwnerContractTest do alias Ethers.Contract.Test.OwnerContract @from "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266" - @sample_address "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" + @sample_address "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" test "can deploy and get owner" do encoded_constructor = OwnerContract.constructor(@sample_address) diff --git a/test/ethers/registry_contract_test.exs b/test/ethers/registry_contract_test.exs index f6dd480..bc7f73e 100644 --- a/test/ethers/registry_contract_test.exs +++ b/test/ethers/registry_contract_test.exs @@ -11,7 +11,7 @@ defmodule Ethers.RegistryContractTest do alias Ethers.Contract.Test.RegistryContract - @from "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266" + @from "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" @from1 "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" @from2 "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC" diff --git a/test/ethers/types_contract_test.exs b/test/ethers/types_contract_test.exs index 18b5616..6489818 100644 --- a/test/ethers/types_contract_test.exs +++ b/test/ethers/types_contract_test.exs @@ -12,7 +12,7 @@ defmodule Ethers.TypesContractTest do alias Ethers.Contract.Test.TypesContract @from "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" - @sample_address "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" + @sample_address "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" setup_all :deploy_types_contract diff --git a/test/ethers/utils_test.exs b/test/ethers/utils_test.exs index b1ef33e..685a8de 100644 --- a/test/ethers/utils_test.exs +++ b/test/ethers/utils_test.exs @@ -161,4 +161,46 @@ defmodule Ethers.UtilsTest do "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1" end end + + describe "human_arg/2" do + test "handles 20-byte binary address" do + # Regression test: ensure binary addresses are properly converted + binary_address = + <<48, 120, 170, 17, 101, 240, 156, 228, 62, 76, 75, 122, 119, 72, 248, 105, 128, 216, 172, + 54>> + + result = Ethers.Utils.human_arg(binary_address, :address) + + assert result == "0x3078aA1165F09ce43E4C4B7a7748f86980d8AC36" + assert String.starts_with?(result, "0x") + assert String.length(result) == 42 + end + + test "handles random 20-byte binary address" do + # Regression test: ensure any 20-byte binary is properly converted + binary_address = + <<123, 45, 67, 89, 12, 34, 56, 78, 90, 11, 22, 33, 44, 55, 66, 77, 88, 99, 111, 222>> + + result = Ethers.Utils.human_arg(binary_address, :address) + + assert result == "0x7B2d43590c22384e5A0B16212c37424D58636FDE" + assert String.starts_with?(result, "0x") + assert String.length(result) == 42 + end + + test "handles hex string address" do + # Ensure hex string addresses are checksummed + hex_address = "0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1" + + result = Ethers.Utils.human_arg(hex_address, :address) + + assert result == "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1" + end + + test "raises on invalid address" do + assert_raise ArgumentError, ~r/Invalid address/, fn -> + Ethers.Utils.human_arg("invalid_address", :address) + end + end + end end