I am working on a Zig project that involves encoding arguments according to the ABI encoding rules. The project includes a function abiEncodeWithSignature, which aims to take a function signature and a set of arguments, then return the ABI-encoded bytes. The function utilizes a switch statement to handle different argument types, including a specific case for a 256-bit unsigned integer (@"u256") and slices of unsigned bytes ([]const u8 and []u8).
The issue arises when running unit tests on this function. The first test, which encodes a function signature with no arguments, passes without issues. However, the second test, fails when I start adding arguments.
Does anyone have insights or suggestions on how to resolve this issue to ensure all argument types are correctly supported and encoded?
For reference: https://noxx.substack.com/p/evm-deep-dives-the-path-to-shadowy?s=r
const std = @import("std");
const crypto = std.crypto;
const mem = std.mem;
const testing = std.testing;
const @"u256" = [32]u8;
pub fn abiEncodeWithSignature(signature: []const u8, args: anytype) ![]const u8 {
// Convert the function signature to a Keccak-256 hash
var hash: [32]u8 = undefined;
crypto.hash.sha3.Keccak256.hash(signature, &hash, .{});
// Take the first 4 bytes of the hash as the function selector
const selector = hash[0..4];
// Create a list to store the encoded arguments
var encoded_args = std.ArrayList(u8).init(std.heap.page_allocator);
defer encoded_args.deinit();
// Encode each argument according to the ABI encoding rules
inline for (args, 0..) |arg, i| {
const arg_type = @TypeOf(arg);
std.debug.print("Argument at index {}: type={s}, value={any}\n", .{ i, @typeName(arg_type), arg });
switch (arg_type) {
[]const u8 => {
std.debug.print("Appending address argument: {any}\n", .{arg});
// Left-pad the address with 12 zero bytes to make it 32 bytes long
var padded_address: [32]u8 = undefined;
for (padded_address[0..12]) |*byte| {
byte.* = 0; // Pad with zeros
}
std.mem.copy(u8, padded_address[12..], arg[0..20]);
try encoded_args.appendSlice(padded_address[0..]); // Append the padded address
},
*[32]u8 => {
std.debug.print("Appending u256 argument: {any}\n", .{arg});
try encoded_args.appendSlice(arg[0..]); // Convert the array to a slice
},
else => {
std.debug.print("Unsupported argument type at index {}: {s}\n", .{ i, @typeName(arg_type) });
return error.UnsupportedArgumentType;
},
}
}
// Concatenate the function selector and the encoded arguments
var result = try std.heap.page_allocator.alloc(u8, selector.len + encoded_args.items.len);
mem.copy(u8, result[0..selector.len], selector);
mem.copy(u8, result[selector.len..], encoded_args.items);
return result;
}
pub fn main() !void {
std.debug.print("Run `zig test` to run the tests.\n", .{});
}
test "abiEncodeWithSignature.store(uint256)" {
const signature = "store(uint256)";
const encoded = try abiEncodeWithSignature(signature, .{});
defer std.heap.page_allocator.free(encoded);
// Expected result: 6057361d
const expected = [_]u8{ 0x60, 0x57, 0x36, 0x1d };
try testing.expectEqualSlices(u8, &expected, encoded);
}
test "abiEncodeWithSignature.store(uint256, address)" {
const signature = "store(uint256,address)";
// Adjust amount_bytes to be a [32]u8 array, representing a uint256.
var amount_bytes: [32]u8 = undefined;
amount_bytes[31] = 0x01; // Assuming little-endian, place the value at the end.
const address_bytes = [_]u8{ 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0x78, 0x90 };
const encoded = try abiEncodeWithSignature(signature, .{ @as([]const u8, &amount_bytes), @as([]const u8, &address_bytes) });
defer std.heap.page_allocator.free(encoded);
const expected = [_]u8{
0xce, 0xaa, 0x31, 0x82, // Function selector
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // amount
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0x78, 0x90, // address
};
try testing.expectEqualSlices(u8, &expected, encoded);
}
Test [2/2] test.abiEncodeWithSignature.store(uint256, address)... Argument at index 0: type=[]const u8, value={ 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 1 }
Appending address argument: { 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 1 }
Argument at index 1: type=[]const u8, value={ 18, 52, 86, 120, 144, 18, 52, 86, 120, 144, 18, 52, 86, 120, 144, 18, 52, 86, 120, 144 }
Appending address argument: { 18, 52, 86, 120, 144, 18, 52, 86, 120, 144, 18, 52, 86, 120, 144, 18, 52, 86, 120, 144 }
slices differ. first difference occurs at index 16 (0x10)
============ expected this output: ============= len: 68 (0x44)
CE AA 31 82 00 00 00 00 00 00 00 00 00 00 00 00 ..1.............
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 ................
12 34 56 78 90 12 34 56 78 90 12 34 56 78 90 12 .4Vx..4Vx..4Vx..
34 56 78 90 4Vx.
============= instead found this: ============== len: 68 (0x44)
CE AA 31 82 00 00 00 00 00 00 00 00 00 00 00 00 ..1.............
AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA ................
AA AA AA AA 00 00 00 00 00 00 00 00 00 00 00 00 ................
12 34 56 78 90 12 34 56 78 90 12 34 56 78 90 12 .4Vx..4Vx..4Vx..
34 56 78 90 4Vx.
================================================
Test [2/2] test.abiEncodeWithSignature.store(uint256, address)... FAIL (TestExpectedEqual)
/opt/homebrew/Cellar/zig/0.11.0/lib/zig/std/testing.zig:380:5: 0x100ecf113 in expectEqualSlices__anon_1697 (test)
return error.TestExpectedEqual;
^
/Users/christopher.bradley/boringlabs/zigsol/src/main.zig:89:5: 0x100ed0613 in test.abiEncodeWithSignature.store(uint256, address) (test)
try testing.expectEqualSlices(u8, &expected, encoded);
^
1 passed; 0 skipped; 1 failed.
error: the following test command failed with exit code 1:
/Users/christopher.bradley/boringlabs/zigsol/zig-cache/o/29627c5bf39569b46dda51a33e32e0e7/test
amount_bytes
and address_bytes
are not slices, they are arrays:
@compileLog(@TypeOf(amount_bytes)); // @as(type, [1]u8)
@compileLog(@TypeOf(address_bytes)); // @as(type, [20]u8)
Normally casting to a slice is as simple as &array
or array[0..]
, but in this case you're using anytype
, so you have to be explicit: @as([]const u8, &array)
. Like this:
const encoded = try abiEncodeWithSignature(signature, .{ @as([]const u8, &amount_bytes), @as([]const u8, &address_bytes) });