diff --git a/lib/eth/abi.rb b/lib/eth/abi.rb index 99f1154d..5fa36e6f 100644 --- a/lib/eth/abi.rb +++ b/lib/eth/abi.rb @@ -38,8 +38,12 @@ class ValueOutOfBounds < StandardError; end # # @param types [Array] types to be ABI-encoded. # @param args [Array] values to be ABI-encoded. + # @param packed [Bool] set true to return packed encoding (default: `false`). # @return [String] the encoded ABI data. - def encode(types, args) + def encode(types, args, packed = false) + return encode_packed(types, args) if packed + types = [types] unless types.instance_of? Array + args = [args] unless args.instance_of? Array # parse all types parsed_types = types.map { |t| Type === t ? t : Type.parse(t) } @@ -64,6 +68,20 @@ def encode(types, args) "#{head}#{tail}" end + # Encodes Application Binary Interface (ABI) data in non-standard packed mode. + # It accepts multiple arguments and encodes using the head/tail mechanism. + # + # @param types [Array] types to be ABI-encoded. + # @param args [Array] values to be ABI-encoded. + # @return [String] the encoded packed ABI data. + def encode_packed(types, args) + raise ArgumentError, "Types and values must be the same length" if types.length != args.length + packed = types.zip(args).map do |type, arg| + Abi::Packed::Encoder.type(type, arg) + end.join + packed.force_encoding(Encoding::ASCII_8BIT) + end + # Decodes Application Binary Interface (ABI) data. It accepts multiple # arguments and decodes using the head/tail mechanism. # diff --git a/lib/eth/abi/decoder.rb b/lib/eth/abi/decoder.rb index 171e2263..01673831 100644 --- a/lib/eth/abi/decoder.rb +++ b/lib/eth/abi/decoder.rb @@ -102,7 +102,7 @@ def primitive_type(type, data) when "address" # decoded address with 0x-prefix - "0x#{Util.bin_to_hex data[12..-1]}" + Address.new(Util.bin_to_hex data[12..-1]).to_s.downcase when "string", "bytes" if type.sub_type.empty? size = Util.deserialize_big_endian_to_int data[0, 32] diff --git a/lib/eth/abi/encoder.rb b/lib/eth/abi/encoder.rb index f2ee06d8..d60e01bb 100644 --- a/lib/eth/abi/encoder.rb +++ b/lib/eth/abi/encoder.rb @@ -49,7 +49,6 @@ def type(type, arg) head, tail = "", "" head += type(Type.size_type, arg.size) nested_sub = type.nested_sub - nested_sub_size = type.nested_sub.size # calculate offsets if %w(string bytes).include?(type.base_type) && type.sub_type.empty? @@ -93,15 +92,15 @@ def type(type, arg) # @return [String] the encoded primitive type. # @raise [EncodingError] if value does not match type. # @raise [ValueOutOfBounds] if value is out of bounds for type. - # @raise [EncodingError] if encoding fails for type. + # @raise [ArgumentError] if encoding fails for type. def primitive_type(type, arg) case type.base_type when "uint" uint arg, type - when "bool" - bool arg when "int" int arg, type + when "bool" + bool arg when "ureal", "ufixed" ufixed arg, type when "real", "fixed" diff --git a/lib/eth/abi/packed/decoder.rb b/lib/eth/abi/packed/decoder.rb index 0cf06b0f..ed23b902 100644 --- a/lib/eth/abi/packed/decoder.rb +++ b/lib/eth/abi/packed/decoder.rb @@ -23,6 +23,15 @@ module Packed # Provides a utility module to assist decoding ABIs. module Decoder + + # Since the encoding is ambiguous, there is no decoding function. + # + # @param types [Array] the ABI to be decoded. + # @param data [String] ABI data to be decoded. + # @raise [DecodingError] if you try to decode packed ABI data. + # def decode_packed(types, data) + # raise DecodingError, "Since the encoding is ambiguous, there is no decoding function." + # end end end end diff --git a/lib/eth/abi/packed/encoder.rb b/lib/eth/abi/packed/encoder.rb index 17d0c727..9f577e45 100644 --- a/lib/eth/abi/packed/encoder.rb +++ b/lib/eth/abi/packed/encoder.rb @@ -19,10 +19,175 @@ module Eth # Provides a Ruby implementation of the Ethereum Application Binary Interface (ABI). module Abi + + # Encapsulates the module for non-standard packed encoding used in Solidity. module Packed # Provides a utility module to assist encoding ABIs. module Encoder + extend self + + # Encodes a specific value, either static or dynamic in non-standard + # packed encoding mode. + # + # @param type [Eth::Abi::Type] type to be encoded. + # @param arg [String|Number] value to be encoded. + # @return [String] the packed encoded type. + # @raise [EncodingError] if value does not match type. + # @raise [ArgumentError] if encoding fails for type. + def type(type, arg) + case type + when /^uint(\d+)$/ + uint(arg, $1.to_i / 8) + when /^int(\d+)$/ + int(arg, $1.to_i / 8) + when "bool" + bool(arg) + when /^ureal(\d+)x(\d+)$/, /^ufixed(\d+)x(\d+)$/ + ufixed(arg, $1.to_i / 8, $2.to_i) + when /^real(\d+)x(\d+)$/, /^fixed(\d+)x(\d+)$/ + fixed(arg, $1.to_i / 8, $2.to_i) + when "string" + string(arg) + when /^bytes(\d+)$/ + bytes(arg, $1.to_i) + when "bytes" + string(arg) + when /^tuple\((.+)\)$/ + tuple($1.split(","), arg) + when /^hash(\d+)$/ + hash(arg, $1.to_i) + when "address" + address(arg) + when /^(.+)\[\]$/ + array($1, arg) + when /^(.+)\[(\d+)\]$/ + fixed_array($1, arg, $2.to_i) + else + raise EncodingError, "Unhandled type: #{type}" + end + end + + private + + # Properly encodes signed integers. + def uint(value, byte_size) + raise ArgumentError, "Don't know how to handle this input." unless value.is_a? Numeric + raise ValueOutOfBounds, "Number out of range: #{value}" if value > Constant::UINT_MAX or value < Constant::UINT_MIN + [value].pack("Q>")[-1, byte_size].rjust(byte_size, "\x00".b).b + end + + # Properly encodes signed integers. + def int(value, byte_size) + raise ArgumentError, "Don't know how to handle this input." unless value.is_a? Numeric + raise ValueOutOfBounds, "Number out of range: #{value}" if value > Constant::INT_MAX or value < Constant::INT_MIN + [value].pack("q>")[-1, byte_size].rjust(byte_size, value < 0 ? "\xFF".b : "\x00".b).b + end + + # Properly encodes booleans. + def bool(value) + raise EncodingError, "Argument is not bool: #{value}" unless value.instance_of? TrueClass or value.instance_of? FalseClass + (value ? "\x01".b : "\x00".b).b + end + + # Properly encodes unsigned fixed-point numbers. + def ufixed(value, byte_size, decimals) + raise ArgumentError, "Don't know how to handle this input." unless value.is_a? Numeric + raise ValueOutOfBounds, value unless value >= 0 and value < 2 ** decimals + scaled_value = (value * (10 ** decimals)).to_i + uint(scaled_value, byte_size) + end + + # Properly encodes signed fixed-point numbers. + def fixed(value, byte_size, decimals) + raise ArgumentError, "Don't know how to handle this input." unless value.is_a? Numeric + raise ValueOutOfBounds, value unless value >= -2 ** (decimals - 1) and value < 2 ** (decimals - 1) + scaled_value = (value * (10 ** decimals)).to_i + int(scaled_value, byte_size) + end + + # Properly encodes byte(-string)s. + def bytes(value, length) + raise EncodingError, "Expecting String: #{value}" unless value.instance_of? String + value = handle_hex_string value, length + raise ArgumentError, "Value must be a string of length #{length}" unless value.is_a?(String) && value.bytesize == length + value.b + end + + # Properly encodes (byte-)strings. + def string(value) + raise ArgumentError, "Value must be a string" unless value.is_a?(String) + value.b + end + + # Properly encodes tuples. + def tuple(types, values) + encode(types, values) + end + + # Properly encodes hash-strings. + def hash(value, byte_size) + raise EncodingError, "Argument too long: #{value}" unless byte_size > 0 and byte_size <= 32 + hash_bytes = handle_hex_string value, byte_size + raise ArgumentError, "Hash value must be #{byte_size} bytes" unless hash_bytes.bytesize == byte_size + hash_bytes.b + end + + # Properly encodes addresses. + def address(value) + if value.is_a? Address + + # from checksummed address with 0x prefix + Util.zpad_hex value.to_s[2..-1], 20 + elsif value.is_a? Integer + + # address from integer + Util.zpad_int value, 20 + elsif value.size == 20 + + # address from encoded address + Util.zpad value, 20 + elsif value.size == 40 + + # address from hexadecimal address + Util.zpad_hex value, 20 + elsif value.size == 42 and value[0, 2] == "0x" + + # address from hexadecimal address with 0x prefix + Util.zpad_hex value[2..-1], 20 + else + raise EncodingError, "Could not parse address: #{value}" + end + end + + # Properly encodes dynamic-sized arrays. + def array(type, values) + values.map { |value| type(type, value) }.join.b + end + + # Properly encodes fixed-size arrays. + def fixed_array(type, values, size) + raise ArgumentError, "Array size does not match" unless values.size == size + array(type, values) + end + + # The ABI encoder needs to be able to determine between a hex `"123"` + # and a binary `"123"` string. + def handle_hex_string(val, len) + if Util.prefixed? val or + (len === val.size * 2 and Util.hex? val) + + # There is no way telling whether a string is hex or binary with certainty + # in Ruby. Therefore, we assume a `0x` prefix to indicate a hex string. + # Additionally, if the string size is exactly the double of the expected + # binary size, we can assume a hex value. + Util.hex_to_bin val + else + + # Everything else will be assumed binary or raw string. + val.b + end + end end end end diff --git a/spec/eth/abi/packed/encoder_spec.rb b/spec/eth/abi/packed/encoder_spec.rb index f5597173..8693ed54 100644 --- a/spec/eth/abi/packed/encoder_spec.rb +++ b/spec/eth/abi/packed/encoder_spec.rb @@ -3,4 +3,188 @@ require "spec_helper" describe Abi::Packed::Encoder do + it "encodes packed types" do + expect(Util.bin_to_hex Abi.encode_packed(["uint8[]"], [[1, 2, 3]])).to eq "010203" + expect(Util.bin_to_hex Abi.encode_packed(["uint16[]"], [[1, 2, 3]])).to eq "000100020003" + expect(Util.bin_to_hex Abi.encode_packed(["uint32"], [17])).to eq "00000011" + expect(Util.bin_to_hex Abi.encode_packed(["uint64"], [17])).to eq "0000000000000011" + expect(Util.bin_to_hex Abi.encode_packed(["bool[]"], [[true, false]])).to eq "0100" + expect(Util.bin_to_hex Abi.encode_packed(["bool"], [true])).to eq "01" + expect(Util.bin_to_hex Abi.encode_packed(["int32[]"], [[1, 2, 3]])).to eq "000000010000000200000003" + expect(Util.bin_to_hex Abi.encode_packed(["int64[]"], [[1, 2, 3]])).to eq "000000000000000100000000000000020000000000000003" + expect(Util.bin_to_hex Abi.encode_packed(["int64"], [17])).to eq "0000000000000011" + expect(Util.bin_to_hex Abi.encode_packed(["int128"], [17])).to eq "00000000000000000000000000000011" + expect(Util.bin_to_hex Abi.encode_packed(["bytes1"], ["0x42"])).to eq "42" + expect(Util.bin_to_hex Abi.encode_packed(["bytes"], ["dave".b])).to eq "64617665" + expect(Util.bin_to_hex Abi.encode_packed(["string"], ["dave"])).to eq "64617665" + expect(Util.bin_to_hex Abi.encode_packed(["string"], ["Hello, World"])).to eq "48656c6c6f2c20576f726c64" + expect(Abi.encode_packed(["address"], ["\xff" * 20])).to eq "\xff" * 20 + expect(Abi.encode_packed(["address"], ["ff" * 20])).to eq "\xff" * 20 + expect(Abi.encode_packed(["address"], ["0x" + "ff" * 20])).to eq "\xff" * 20 + expect(Abi.encode_packed(["address"], [Address.new("0x" + "ff" * 20)])).to eq "\xff" * 20 + expect(Abi.encode_packed(["address"], ["0xA1B28f84a836142b6cB1cf003Ee3B113745268c0"])).to eq "\xA1\xB2\x8F\x84\xA86\x14+l\xB1\xCF\x00>\xE3\xB1\x13tRh\xC0" + expect(Util.bin_to_hex Abi.encode_packed(["hash32"], ["8<\xAE\xB6pn\x00\xE2\fr\x05XH\x88\xBAW\xBFV\xEA\xFFMDe\xA8<\x9C{\e!GH\xA6"])).to eq "383caeb6706e00e20c7205584888ba57bf56eaff4d4465a83c9c7b1b214748a6" + expect(Util.bin_to_hex Abi.encode_packed(["hash20"], ["H\x88\xBAW\xBFV\xEA\xFFMDe\xA8<\x9C{\e!GH\xA6"])).to eq "4888ba57bf56eaff4d4465a83c9c7b1b214748a6" + end + + it "encodes non-standard packed mode (solidity 0.8.28)" do + #ref https://docs.soliditylang.org/en/v0.8.28/abi-spec.html#non-standard-packed-mode + # 0xffff42000348656c6c6f2c20776f726c6421 + # ^^^^ int16(-1) + # ^^ bytes1(0x42) + # ^^^^ uint16(0x03) + # ^^^^^^^^^^^^^^^^^^^^^^^^^^ string("Hello, world!") without a length field + + expect(Util.bin_to_hex Abi.encode_packed(["int16"], [-1])).to eq "ffff" + expect(Util.bin_to_hex Abi.encode_packed(["bytes1"], ["0x42"])).to eq "42" + expect(Util.bin_to_hex Abi.encode_packed(["bytes1"], ["\B"])).to eq "42" + expect(Util.bin_to_hex Abi.encode_packed(["uint16"], [0x03])).to eq "0003" + expect(Util.bin_to_hex Abi.encode_packed(["string"], ["Hello, world!"])).to eq "48656c6c6f2c20776f726c6421" + types = ["int16", "bytes1", "uint16", "string"] + values = [ + -1, "\B", 0x03, "Hello, world!", + ] + expect(Util.bin_to_hex Abi.encode_packed(types, values)).to eq "ffff42000348656c6c6f2c20776f726c6421" + end + + context "wuminzhe's tests" do + # ref https://github.com/wuminzhe/abi_coder_rb/blob/701af2315cfc94a94872beb6c639ece400fca589/spec/packed_encoding_spec.rb + + it "bool" do + types = ["bool"] + values = [true] + data = "01" + expect(Util.bin_to_hex Abi.encode_packed(types, values)).to eq data + end + + it "bytes" do + types = ["bytes"] + values = ["dave".b] + data = "64617665" + expect(Util.bin_to_hex Abi.encode_packed(types, values)).to eq data + end + + it "bytes4" do + types = ["bytes4"] + values = ["dave"] + data = "64617665" + expect(Util.bin_to_hex Abi.encode_packed(types, values)).to eq data + end + + it "string" do + types = ["string"] + values = ["dave"] + data = "64617665" + expect(Util.bin_to_hex Abi.encode_packed(types, values)).to eq data + end + + it "address1" do + types = ["address"] + values = ["cd2a3d9f938e13cd947ec05abc7fe734df8dd826"] + data = "cd2a3d9f938e13cd947ec05abc7fe734df8dd826" + expect(Util.bin_to_hex Abi.encode_packed(types, values)).to eq data + end + + it "address2" do + types = ["address"] + values = ["cd2a3d9f938e13cd947ec05abc7fe734df8dd826"] + data = "cd2a3d9f938e13cd947ec05abc7fe734df8dd826" + expect(Util.bin_to_hex Abi.encode_packed(types, values)).to eq data + end + + it "address3" do + types = ["address"] + values = [0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826] + data = "cd2a3d9f938e13cd947ec05abc7fe734df8dd826" + expect(Util.bin_to_hex Abi.encode_packed(types, values)).to eq data + end + + it "uint32" do + types = ["uint32"] + values = [17] + data = "00000011" + expect(Util.bin_to_hex Abi.encode_packed(types, values)).to eq data + end + + it "int64" do + types = ["int64"] + values = [17] + data = "0000000000000011" + expect(Util.bin_to_hex Abi.encode_packed(types, values)).to eq data + end + + # it "(uint64)" do + # types = "[(uint64)]" + # values = [[17]] + # data = "0000000000000011" + + # expect(Util.bin_to_hex Abi.encode_packed(types, values)).to eq data + # end + + # it "(int32,uint64)" do + # types = ["(int32,uint64)"] + # values = [[17, 17]] + # data = "000000110000000000000011" + + # expect do + # Abi.encode_packed(type, value) + # end.to raise_error("AbiCoderRb::Tuple with multi inner types is not supported in packed mode") + # end + + it "int32,uint64" do + types = %w[int32 uint64] + values = [17, 17] + data = "000000110000000000000011" + expect(Util.bin_to_hex Abi.encode_packed(types, values)).to eq data + end + + it "uint16[]" do + types = ["uint16[]"] + values = [[1, 2]] + data = "00010002" + expect(Util.bin_to_hex Abi.encode_packed(types, values)).to eq data + end + + it "bool[]" do + types = ["bool[]"] + values = [[true, false]] + data = "0100" + expect(Util.bin_to_hex Abi.encode_packed(types, values)).to eq data + end + + it "uint16[]" do + types = ["uint16[]"] + values = [[1, 2]] + data = "00010002" + expect(Util.bin_to_hex Abi.encode_packed(types, values)).to eq data + end + + it "uint16[2]" do + types = ["uint16[2]"] + values = [[1, 2]] + data = "00010002" + expect(Util.bin_to_hex Abi.encode_packed(types, values)).to eq data + end + + it "bytes[2]" do + types = ["bytes[2]"] + values = [["dave", "dave"]] + data = "6461766564617665" + expect(Util.bin_to_hex Abi.encode_packed(types, values)).to eq data + end + + it "encodes packed types" do + expect(Util.bin_to_hex Abi.encode_packed(["uint8[]"], [[1, 2, 3]])).to eq "010203" + expect(Util.bin_to_hex Abi.encode_packed(["uint16[]"], [[1, 2, 3]])).to eq "000100020003" + expect(Util.bin_to_hex Abi.encode_packed(["uint32"], [17])).to eq "00000011" + expect(Util.bin_to_hex Abi.encode_packed(["uint64"], [17])).to eq "0000000000000011" + expect(Util.bin_to_hex Abi.encode_packed(["bool[]"], [[true, false]])).to eq "0100" + expect(Util.bin_to_hex Abi.encode_packed(["bool"], [true])).to eq "01" + expect(Util.bin_to_hex Abi.encode_packed(["int32[]"], [[1, 2, 3]])).to eq "000000010000000200000003" + expect(Util.bin_to_hex Abi.encode_packed(["int64[]"], [[1, 2, 3]])).to eq "000000000000000100000000000000020000000000000003" + expect(Util.bin_to_hex Abi.encode_packed(["int32"], [17])).to eq "00000011" + expect(Util.bin_to_hex Abi.encode_packed(["int64"], [17])).to eq "0000000000000011" + expect(Util.bin_to_hex Abi.encode_packed(["int128"], [17])).to eq "00000000000000000000000000000011" + end + end end