Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
42 changes: 24 additions & 18 deletions lib/ethers/utils.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand All @@ -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::binary-20>>, :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))
Expand Down Expand Up @@ -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(<<address_bin::binary-20>>, chain_id),
do: hex_encode(address_bin, false) |> to_checksum_address(chain_id)
Expand Down Expand Up @@ -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(<<pre, _::binary-32>> = compressed, _use_checksum_address)
if Code.ensure_loaded?(Ethers.secp256k1_module()) do
def public_key_to_address(<<pre, _::binary-32>> = 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(<<pre, _::binary-32>> = _compressed, _use_checksum_address)
when pre in [2, 3],
do: raise("secp256k1 module not loaded")
end

def public_key_to_address(<<pre, _::binary-32>> = 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!()
Expand Down
6 changes: 3 additions & 3 deletions test/ethers/event_mixed_index_contract_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -76,15 +76,15 @@ defmodule Ethers.EventMixedIndexContractTest do
end

test "inspect returns correct value" do
assert ~s'#Ethers.EventFilter<event Transfer(uint256 amount, address indexed sender "0x90f8bf6a479f320ead074411a4b0e7944ea80000", bool isFinal, address indexed receiver "0x90f8bf6a479f320ead074411a4b0e7944ea80001")>' ==
assert ~s'#Ethers.EventFilter<event Transfer(uint256 amount, address indexed sender "0x90F8BF6A479f320EAd074411a4b0E7944ea80000", bool isFinal, address indexed receiver "0x90f8Bf6A479f320eaD074411a4B0e7944ea80001")>' ==
inspect(
EventMixedIndexContract.EventFilters.transfer(
"0x90f8bf6a479f320ead074411a4b0e7944ea80000",
"0x90f8bf6a479f320ead074411a4b0e7944ea80001"
)
)

assert ~s'#Ethers.EventFilter<event Transfer(uint256 amount, address indexed sender any, bool isFinal, address indexed receiver "0x90f8bf6a479f320ead074411a4b0e7944ea80001")>' ==
assert ~s'#Ethers.EventFilter<event Transfer(uint256 amount, address indexed sender any, bool isFinal, address indexed receiver "0x90f8Bf6A479f320eaD074411a4B0e7944ea80001")>' ==
inspect(
EventMixedIndexContract.EventFilters.transfer(
nil,
Expand Down
2 changes: 1 addition & 1 deletion test/ethers/owner_contract_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion test/ethers/registry_contract_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ defmodule Ethers.RegistryContractTest do

alias Ethers.Contract.Test.RegistryContract

@from "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266"
@from "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
@from1 "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"
@from2 "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"

Expand Down
2 changes: 1 addition & 1 deletion test/ethers/types_contract_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
42 changes: 42 additions & 0 deletions test/ethers/utils_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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